Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
40cbce4
initial cbts structure
crazydemo Apr 23, 2026
f010300
simplify
crazydemo Apr 23, 2026
ae2c0aa
[None][chore] align CBTS copyright headers with CODING_GUIDELINES
crazydemo Apr 23, 2026
084340d
[None][chore] drop empty CBTS __init__.py files; use namespace packages
crazydemo Apr 23, 2026
9ede6d3
[None][chore] drop CBTS Layer 3 per-test filter; run full block for s…
crazydemo Apr 23, 2026
5a29bec
[None][chore] delete CBTS DESIGN.md; consolidate into README.md
crazydemo Apr 23, 2026
ed11478
[None][chore] document CBTS rules inventory in rules/README.md
crazydemo Apr 23, 2026
413495d
[None][test] validate CBTS in this PR via waive edit+add (revert befo…
crazydemo Apr 23, 2026
0d5b0f0
[None][chore] CBTS defers to explicit /bot run flags; activates only …
crazydemo Apr 23, 2026
2a9e0df
[None][chore] CBTS: pip install pyyaml in getCbtsResult before first …
crazydemo Apr 24, 2026
ed3fc70
[None][chore] CBTS: don't treat REUSE_* as user flag (auto-set on bot…
crazydemo Apr 24, 2026
3190378
[None][chore] CBTS: log which specific flag triggered the defer, for …
crazydemo Apr 24, 2026
b5db115
[None][test] TESTING: WaivesRule claims all changed_files so CBTS fir…
crazydemo Apr 24, 2026
a6eabf6
[None][chore] CBTS: log specific defer reason for every branch (not j…
crazydemo Apr 24, 2026
75d7ca2
[None][fix] CBTS: install pyyaml via apt-get (buildpack-deps has no p…
crazydemo Apr 24, 2026
4c67e02
[None][chore] CBTS: under --post-merge, narrow Layer 2 to post-merge …
crazydemo Apr 27, 2026
a9cbdeb
[None][chore] CBTS: fix RUF012 mutable default in Rule base class
crazydemo Apr 27, 2026
0a377a5
[None][fix] CBTS: index normalized test ids so TIMEOUT-decorated YAML…
crazydemo Apr 27, 2026
0f4d1ed
[None][fix] CBTS: reset cpu_arch on unfamiliar Groovy map openings
crazydemo Apr 27, 2026
27e17d7
[None][chore] CBTS: make Groovy consumers scope-agnostic so new rules…
crazydemo Apr 27, 2026
6f101ff
[None][chore] CBTS: support Ant glob in needs_diff_for so future rule…
crazydemo Apr 27, 2026
f2e6502
[None][doc] CBTS: tighten READMEs and reflect scope-agnostic + Ant gl…
crazydemo Apr 27, 2026
99dd20b
[None][chore] CBTS: simplify defer-flag list and clarify two supporte…
crazydemo Apr 27, 2026
0ad6210
[None][fix] CBTS: include post-merge stages in main.py so post-merge-…
crazydemo Apr 27, 2026
d569669
[None][fix] CBTS: implement Ant glob in pure Groovy (hudson.util.AntP…
crazydemo Apr 27, 2026
7a2b36e
[None][fix] CBTS: avoid \u in comment — Groovy lexer treats it as uni…
crazydemo Apr 27, 2026
ed522e3
[None][chore] CBTS: revert testing artifacts; WaivesRule fires only w…
crazydemo Apr 28, 2026
747608b
[None][fix] CBTS: parent-chain test-id lookup + scope=None safety net…
crazydemo Apr 28, 2026
14db404
[None][feat] CBTS Layer 3: within-stage test filtering via filtered t…
crazydemo Apr 29, 2026
0a5eae3
[None][doc] CBTS: document Layer 3 (within-stage filtering) in READMEs
crazydemo Apr 29, 2026
e5d177d
[None][fix] CBTS: tolerate SLURM configs with extra positional args i…
crazydemo Apr 29, 2026
9a095b4
[None][fix] CBTS Layer 3: tolerate empty shards after narrowing
crazydemo Apr 29, 2026
1a2fa0d
[None][test] TESTING: enable CBTS on infra-only PR + Layer 3 e2e waives
crazydemo Apr 29, 2026
db17f77
[None][fix] CBTS: exclude perf stages from Layer 2/3
crazydemo Apr 29, 2026
8058125
[None][test] TESTING: skip Debug build smoke stage to unblock CBTS te…
crazydemo Apr 29, 2026
718b08a
[None][fix] CBTS: drop redundant perfMode guard in renderTestDB
crazydemo Apr 29, 2026
dd1a5ab
[None][test] TESTING: CBTS diagnostics — log affected_stages and rend…
crazydemo Apr 30, 2026
be167c7
[None][test] TESTING: CBTS Python — dump full decision to stderr for …
crazydemo Apr 30, 2026
fba4612
[None][test] TESTING: drop redundant Layer 2 echoes (Python now cover…
crazydemo Apr 30, 2026
254d2c9
[None][fix] CBTS Layer 3: drop unaffected blocks from narrowed test-db
crazydemo Apr 30, 2026
254cddf
[None][fix] CBTS Layer 3: -k keyword guard against parent-chain over-…
crazydemo Apr 30, 2026
b12231c
[None][fix] CBTS Layer 3: regenerate cbts_test_db on each stage agent
crazydemo Apr 30, 2026
9726ce6
[None][fix] CBTS Layer 3: write cbts_input.json via shell base64 to b…
crazydemo Apr 30, 2026
e5dd808
[None][fix] CBTS Layer 3: cap cbts_input_json piggyback at 256 KB
crazydemo Apr 30, 2026
a72c4dd
[None][refactor] CBTS Layer 3: replace base64 hack with Utils.createT…
crazydemo Apr 30, 2026
9cd8da2
[None][fix] CBTS Layer 3: pass pipeline through renderTestDB
crazydemo Apr 30, 2026
7c51554
[None][fix] CBTS Layer 3: tolerate pytest exit 5 on SLURM path
crazydemo May 1, 2026
c5b52db
[None][feat] CBTS Layer 3: collapse pytest-split to splits=1 below 20…
crazydemo May 1, 2026
ef0e9b4
[None][fix] CBTS: inline split-collapse threshold to fix MissingPrope…
crazydemo May 1, 2026
0c3c5b9
[None][refactor] CBTS Layer 3: drop empty-shard safety nets supersede…
crazydemo May 1, 2026
a72593c
[None][doc] CBTS: bring README up to date with recent Layer 3 work
crazydemo May 1, 2026
be4389b
[None][chore] revert TEMP Debug build skip workaround
crazydemo May 1, 2026
bc9b41f
[None][chore] revert TESTING entries used to validate CBTS Layer 3
crazydemo May 2, 2026
2370627
[None][feat] CBTS: add /bot run --disable-cbts flag
crazydemo May 2, 2026
5217449
[None][chore] revert /bot run --disable-cbts flag
crazydemo May 2, 2026
f476716
fix conflicts
crazydemo May 5, 2026
c243c0f
[None][fix] CBTS: filter affected_stages by /bot run trigger mode
crazydemo May 5, 2026
d9edb8d
[None][refactor] CBTS: scope to test cases only; preserve Build and P…
crazydemo May 5, 2026
aa659df
[None][refactor] CBTS: trim redundant JSON fields (affected_cpu_arch,…
crazydemo May 5, 2026
48d1837
[None][docs] CBTS: trim verbose comments and README
crazydemo May 5, 2026
7c61b72
[None][chore] TESTING CBTS scenario A — REVERT before merge
crazydemo May 5, 2026
3337f0b
[None][feat] CBTS: skip PackageSanityCheck when waive doesn't touch s…
crazydemo May 5, 2026
824fff1
[None][feat] CBTS: add perfsanity_required gate symmetric to sanity_r…
crazydemo May 5, 2026
1fb7255
[None][chore] revert TESTING entries from CBTS scenario validation
crazydemo May 6, 2026
446d966
[None][fix] CBTS: rethrow InterruptedException so aborts propagate
crazydemo May 8, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
159 changes: 159 additions & 0 deletions jenkins/L0_MergeRequest.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,8 @@ def AUTO_TRIGGER_TAG_LIST = "auto_trigger_tag_list"
def DEBUG_MODE = "debug"
@Field
def DETAILED_LOG = "detailed_log"
@Field
def CBTS_RESULT = "cbts_result"

def testFilter = [
(REUSE_TEST): gitlabParamsFromBot.get(REUSE_TEST, null),
Expand All @@ -138,6 +140,7 @@ def testFilter = [
(DEBUG_MODE): gitlabParamsFromBot.get(DEBUG_MODE, false),
(AUTO_TRIGGER_TAG_LIST): [],
(DETAILED_LOG): gitlabParamsFromBot.get(DETAILED_LOG, false),
(CBTS_RESULT): null,
]

String reuseBuild = gitlabParamsFromBot.get('reuse_build', null)
Expand Down Expand Up @@ -308,6 +311,7 @@ def setupPipelineEnvironment(pipeline, testFilter, globalVars)
testFilter[(MULTI_GPU_FILE_CHANGED)] = getMultiGpuFileChanged(pipeline, testFilter, globalVars)
testFilter[(ONLY_ONE_GROUP_CHANGED)] = getOnlyOneGroupChanged(pipeline, testFilter, globalVars)
testFilter[(AUTO_TRIGGER_TAG_LIST)] = getAutoTriggerTagList(pipeline, testFilter, globalVars)
testFilter[(CBTS_RESULT)] = getCbtsResult(pipeline, testFilter, globalVars)
getContainerURIs().each { k, v ->
globalVars[k] = v
}
Expand Down Expand Up @@ -696,6 +700,154 @@ def getAutoTriggerTagList(pipeline, testFilter, globalVars) {
return autoTriggerTagList
}

// ============================================================================
// CBTS (Change-Based Testing Selection)
//
// Calls jenkins/scripts/cbts/main.py with PR changed_files + diffs and returns
// a result map (or null = defer to existing filter chain). Result keys:
// scope, affected_stages, reasons, test_db_dir_override, affected_stage_test_counts.
// CBTS narrows test cases only — Build always runs. See cbts/README.md.
// ============================================================================

def getCbtsResult(pipeline, testFilter, globalVars)
{
def isOfficialPostMergeJob = (env.JOB_NAME ==~ /.*PostMerge.*/)
if (env.alternativeTRT || isOfficialPostMergeJob) {
pipeline.echo("CBTS: deferring — post-merge job or alternativeTRT set")
return null
}

// CBTS only activates on bare `/bot run`. If the user specified any
// stage-selection flag, defer entirely to their explicit choice.
def triggeredFlags = _cbtsTriggeredUserFlags(testFilter)
if (!triggeredFlags.isEmpty()) {
pipeline.echo("CBTS: deferring — user-specified /bot run flag(s): ${triggeredFlags.join(', ')}")
return null
}

def changedFiles = getMergeRequestChangedFileList(pipeline, globalVars).unique()
if (!changedFiles) {
pipeline.echo("CBTS: deferring — no changed files detected")
return null
}

try {
// pyyaml is needed by main.py's blocks.py to parse test-db YAMLs.
sh "apt-get update -qq && apt-get install -y -qq python3-yaml"

// Ask Python which file patterns need diffs, fetch them.
def patternsOut = sh(
script: "cd ${LLM_ROOT} && python3 jenkins/scripts/cbts/main.py --list-needed-diffs",
returnStdout: true,
).trim()
def needsDiffFor = patternsOut ? patternsOut.readLines().collect { it.trim() }.findAll { it } : []
def diffs = [:]
for (f in changedFiles) {
if (_cbtsMatchesAnyPattern(f, needsDiffFor)) {
diffs[f] = getMergeRequestOneFileChanges(pipeline, globalVars, f)
}
}

// Write INPUT_JSON; Python reads stages/yaml itself.
def inputJson = groovy.json.JsonOutput.toJson([
changed_files: changedFiles,
diffs: diffs,
post_merge: testFilter[(IS_POST_MERGE)] ?: false,
])
def inputPath = "${LLM_ROOT}/cbts_input.json"
writeFile file: inputPath, text: inputJson

def output = sh(
script: "cd ${LLM_ROOT} && python3 jenkins/scripts/cbts/main.py cbts_input.json",
returnStdout: true,
)

def result = _cbtsParseSelectionResult(output)
if (result.scope == null) {
pipeline.echo("CBTS: deferring — Python returned scope=null. " +
"Reasons: ${result.reasons.join('; ')}")
return null
}
// Piggyback input JSON on testFilter so each L0_Test stage agent can
// re-run main.py and regenerate cbts_test_db/ locally. Capped at
// 256 KB; oversize → drop piggyback, Layer 3 falls back to source.
final int CBTS_INPUT_PIGGYBACK_MAX_BYTES = 256000
def inputJsonSize = inputJson.length()
if (inputJsonSize <= CBTS_INPUT_PIGGYBACK_MAX_BYTES) {
result.cbts_input_json = inputJson
pipeline.echo("CBTS Layer 3: cbts_input_json piggyback enabled (${inputJsonSize} bytes)")
} else {
pipeline.echo("CBTS Layer 3: cbts_input_json is ${inputJsonSize} bytes, " +
"exceeds ${CBTS_INPUT_PIGGYBACK_MAX_BYTES}-byte piggyback limit; " +
"downstream stages will fall back to source test-db " +
"(Layer 2 stage filtering still applies)")
}
pipeline.echo("CBTS: scope=${result.scope}, " +
"stages=${result.affected_stages.size()}")
return result
} catch (InterruptedException e) {
throw e
} catch (Exception e) {
Comment thread
crazydemo marked this conversation as resolved.
pipeline.echo("CBTS failed, falling back to full run: ${e}")
return null
}
Comment thread
crazydemo marked this conversation as resolved.
}

// Translate an Ant-style glob to a regex:
// **/ zero or more path segments
// ** any chars (including /)
// * any chars except /
// ? single char except /
def _cbtsGlobToRegex(String glob)
{
def escaped = glob.collect { c ->
(c == '*' || c == '?') ? c
: ('.+()[]{}|^$\\'.contains(c) ? '\\' + c : c)
}.join('')
return '^' + escaped
.replace('**/', '__CBTSDOUBLESLASH__')
.replace('**', '__CBTSDOUBLESTAR__')
.replace('*', '[^/]*')
.replace('?', '[^/]')
.replace('__CBTSDOUBLESLASH__', '(?:.*/)?')
.replace('__CBTSDOUBLESTAR__', '.*') + '$'
}

def _cbtsMatchesAnyPattern(String filePath, List patterns)
{
return patterns.any { filePath ==~ _cbtsGlobToRegex(it) }
}

// Returns user-set stage-selection flags that should force CBTS to defer.
// IS_POST_MERGE and orthogonal flags (REUSE_*, DEBUG_MODE, DETAILED_LOG, ...)
// are intentionally absent.
def _cbtsTriggeredUserFlags(testFilter)
{
def deferFlags = [
ENABLE_SKIP_TEST, TEST_STAGE_LIST, EXTRA_STAGE_LIST, GPU_TYPE_LIST,
TEST_BACKEND, ADD_MULTI_GPU_TEST, ONLY_MULTI_GPU_TEST, DISABLE_MULTI_GPU_TEST,
]
return deferFlags.findAll { testFilter[it] }
.collect { "${it}=${testFilter[it]}" }
}

// Parse CBTS JSON stdout into a map. `scope == null` → no decision; caller
// logs reasons and defers.
def _cbtsParseSelectionResult(String text)
{
def data = new groovy.json.JsonSlurper().parseText(text)
return [
scope: data.scope,
affected_stages: data.affected_stages ?: [],
reasons: data.reasons ?: [],
test_db_dir_override: data.test_db_dir_override,
affected_stage_test_counts: data.affected_stage_test_counts ?: [:],
// Explicit null check preserves `false`; default True is safe.
sanity_required: data.sanity_required != null ? data.sanity_required : true,
perfsanity_required: data.perfsanity_required != null ? data.perfsanity_required : true,
]
}

def getMultiGpuFileChanged(pipeline, testFilter, globalVars)
{
if (testFilter[(DISABLE_MULTI_GPU_TEST)]) {
Expand Down Expand Up @@ -1083,6 +1235,11 @@ def launchStages(pipeline, reuseBuild, testFilter, enableFailFast, globalVars)
},
"x86_64-Linux": {
script {
// CBTS deliberately does NOT short-circuit at the arch / Build
// layer. Build always runs so a wheel exists for sanity checks
// and post-merge consumers; case-level narrowing happens later
// in L0_Test.groovy::launchTestJobs (Layer 2) and renderTestDB
// (Layer 3).
def testStageName = "[Build-x86_64] Remote Run"
stage(testStageName) {
def additionalParameters = [
Expand Down Expand Up @@ -1194,6 +1351,8 @@ def launchStages(pipeline, reuseBuild, testFilter, enableFailFast, globalVars)
echo "SBSA build job is skipped due to Jenkins configuration or conditional pipeline run"
return
}
// CBTS deliberately does NOT short-circuit the SBSA Build —
// see x86 track above for the rationale.

def testStageName = "[Build-SBSA] Remote Run"
stage(testStageName) {
Expand Down
112 changes: 109 additions & 3 deletions jenkins/L0_Test.groovy
Original file line number Diff line number Diff line change
Expand Up @@ -1282,7 +1282,7 @@ def runLLMTestlistWithSbatch(pipeline, platform, testList, config=VANILLA_CONFIG
// line is "Mako options:", maybe we can make it more generic, which
// if the line cannot be split by "=", just ignore that line.
def makoOptsJson = transformMakoArgsToJson(["Mako options:"] + makoArgs)
def testListPathLocal = renderTestDB(testList, llmSrcLocal, stageName, makoOptsJson)
def testListPathLocal = renderTestDB(pipeline, testList, llmSrcLocal, stageName, makoOptsJson)
Utils.copyFileToRemoteHost(
pipeline,
remote,
Expand Down Expand Up @@ -1692,8 +1692,48 @@ def runLLMTestlistWithSbatch(pipeline, platform, testList, config=VANILLA_CONFIG
}
}

// CBTS Layer 3 split-collapse heuristic: when the affected stage's narrowed
// test count (from main.py compute_stage_test_counts) is below 20, collapse
// pytest-split's splits to 1 — only group 1 runs everything; groups 2..N
// skip and don't allocate a machine. At/above 20, splits stay at the
// stage's default and pytest-split parallelizes normally.
//
// The 20 below is hard-coded inline rather than a top-level constant
// because Groovy script-level `def` is not visible from method bodies
// without `@Field`, and we don't otherwise need this value elsewhere.
//
// Returns [skip: bool, splits: int, splitId: int]. Callers should
// `return` early when skip == true and otherwise overwrite splits/splitId
// with the returned values before continuing their existing dispatch logic.
def _cbtsMaybeCollapseSplits(stageName, splitId, splits) {
def cbts = testFilter[(CBTS_RESULT)]
def counts = cbts?.affected_stage_test_counts
if (!counts) {
return [skip: false, splits: splits, splitId: splitId]
}
def count = counts[stageName]
if (count == null || count >= 20) {
return [skip: false, splits: splits, splitId: splitId]
}
if (splitId > 1) {
echo "CBTS [${cbts.scope}]: ${stageName} narrowed to ${count} (< 20), skipping group ${splitId}/${splits}"
return [skip: true, splits: splits, splitId: splitId]
}
if (splits > 1) {
echo "CBTS [${cbts.scope}]: ${stageName} narrowed to ${count} (< 20), collapsing splits=${splits} → 1"
}
return [skip: false, splits: 1, splitId: 1]
}

def runLLMTestlistOnSlurm(pipeline, platform, testList, config=VANILLA_CONFIG, perfMode=false, stageName="Undefined", splitId=1, splits=1, gpuCount=1, nodeCount=1, runWithSbatch=false, skipInstallWheel=false, cpver="cp312")
{
def collapse = _cbtsMaybeCollapseSplits(stageName, splitId, splits)
if (collapse.skip) {
return
}
splits = collapse.splits
splitId = collapse.splitId

echo "Run Slurm job with native sbatch: $runWithSbatch"

def attempt = 0
Expand Down Expand Up @@ -1817,6 +1857,8 @@ def DEBUG_MODE = "debug"
@Field
def DETAILED_LOG = "detailed_log"
@Field
def CBTS_RESULT = "cbts_result"
@Field
def testFilter = [
(REUSE_TEST): null,
(REUSE_STAGE_LIST): null,
Expand All @@ -1834,6 +1876,7 @@ def testFilter = [
(DEBUG_MODE): false,
(AUTO_TRIGGER_TAG_LIST): [],
(DETAILED_LOG): false,
(CBTS_RESULT): null,
]

@Field
Expand Down Expand Up @@ -2554,7 +2597,7 @@ def getMakoArgsFromStageName(stageName, parseSysinfo=false) {
return makoArgs
}

def renderTestDB(testContext, llmSrc, stageName, preDefinedMakoOpts=null) {
def renderTestDB(pipeline, testContext, llmSrc, stageName, preDefinedMakoOpts=null) {
def makoOpts = preDefinedMakoOpts

if (!makoOpts) {
Expand All @@ -2564,7 +2607,30 @@ def renderTestDB(testContext, llmSrc, stageName, preDefinedMakoOpts=null) {
}

sh "pip3 install --extra-index-url https://urm.nvidia.com/artifactory/api/pypi/sw-tensorrt-pypi/simple --ignore-installed trt-test-db==1.8.5+bc6df7"
// CBTS Layer 3: regenerate cbts_test_db/ on this stage agent from the
// piggybacked input JSON if not already present.
def cbts = testFilter[(CBTS_RESULT)]
if (cbts != null && cbts.test_db_dir_override && cbts.cbts_input_json) {
def overrideDir = "${llmSrc}/${cbts.test_db_dir_override}"
def dirExists = sh(returnStdout: true, script: "test -d ${overrideDir} && echo yes || echo no").trim()
if (dirExists != "yes") {
def cbtsInputLocal = Utils.createTempLocation(pipeline, "./cbts_input.json")
pipeline.writeFile(file: cbtsInputLocal, text: cbts.cbts_input_json)
sh "apt-get update -qq && apt-get install -y -qq python3-yaml || true"
sh "cd ${llmSrc} && python3 jenkins/scripts/cbts/main.py ${cbtsInputLocal} > /dev/null 2>&1 || true"
}
}
def testDBPath = "${llmSrc}/tests/integration/test_lists/test-db"
if (cbts != null && cbts.test_db_dir_override) {
def overrideYaml = "${llmSrc}/${cbts.test_db_dir_override}/${testContext}.yml"
def overrideOk = sh(returnStdout: true, script: "test -s ${overrideYaml} && echo yes || echo no").trim()
if (overrideOk == "yes") {
testDBPath = "${llmSrc}/${cbts.test_db_dir_override}"
echo "CBTS [${cbts.scope}]: rendering test list from filtered test-db at ${testDBPath}"
} else {
echo "CBTS [${cbts.scope}]: ${overrideYaml} missing/empty -- falling back to source test-db"
}
}
def testList = "${llmSrc}/${testContext}.txt"
def testDBQueryCmd = [
"trt-test-db",
Expand All @@ -2580,6 +2646,9 @@ def renderTestDB(testContext, llmSrc, stageName, preDefinedMakoOpts=null) {
].join(" ")

sh(label: "Render test list from test-db", script: testDBQueryCmd)
def testCount = sh(returnStdout: true, script: "wc -l < ${testList} | tr -d ' '").trim()
def testDBLabel = (cbts != null && cbts.test_db_dir_override) ? "CBTS-narrowed [${cbts.scope}]" : "source"
echo "renderTestDB: stage=${stageName} context=${testContext} test-db=${testDBLabel} dir=${testDBPath} -> ${testCount} tests"
sh(script: "cat ${testList}")

return testList
Expand Down Expand Up @@ -3081,7 +3150,7 @@ def runLLMTestlistOnPlatformImpl(pipeline, platform, testList, config=VANILLA_CO
def noRegularTests = false
def noIsolateTests = false
def rerunFailed = false
def testDBList = renderTestDB(testList, llmSrc, stageName)
def testDBList = renderTestDB(pipeline, testList, llmSrc, stageName)

// Download and Merge waives.txt
mergeWaivesTxt(pipeline, llmSrc, stageName)
Expand Down Expand Up @@ -3281,6 +3350,13 @@ def runLLMTestlistOnPlatformImpl(pipeline, platform, testList, config=VANILLA_CO
// and junit() for intermediate retryable failures).
def runLLMTestlistOnPlatform(pipeline, platform, testList, config=VANILLA_CONFIG, perfMode=false, stageName="Undefined", splitId=1, splits=1, skipInstallWheel=false, cpver="cp312", postTag="", typeCheck=false, boolean isFinalAttempt=true)
{
def collapse = _cbtsMaybeCollapseSplits(stageName, splitId, splits)
if (collapse.skip) {
return
}
splits = collapse.splits
splitId = collapse.splitId

cacheErrorAndUploadResult(stageName, {
runLLMTestlistOnPlatformImpl(pipeline, platform, testList, config, perfMode, stageName, splitId, splits, skipInstallWheel, cpver, typeCheck)
}, {
Expand Down Expand Up @@ -4402,6 +4478,36 @@ def launchTestJobs(pipeline, testFilter)
checkStageNameSet(testFilter[(EXTRA_STAGE_LIST)], fullSet, EXTRA_STAGE_LIST)
}

// CBTS Layer 2: replace `parallelJobsFiltered` with affected stages plus
// PackageSanityCheck (kept iff sanity_required) and PerfSanity (kept iff
// perfsanity_required). Pure -Perf- stages always excluded (own trigger
// model, full-list benchmarks).
def cbts = testFilter[(CBTS_RESULT)]
if (cbts != null) {
def affectedSet = (cbts.affected_stages ?: []) as Set
def needsSanity = cbts.sanity_required
def needsPerfSanity = cbts.perfsanity_required
parallelJobsFiltered = parallelJobs.findAll { key, _ ->
if (key =~ /-Perf-/) return false
return affectedSet.contains(key) ||
(needsSanity && key =~ /PackageSanityCheck/) ||
(needsPerfSanity && key =~ /PerfSanity/)
}
if (affectedSet.isEmpty()) {
if (parallelJobsFiltered.isEmpty()) {
echo "CBTS [${cbts.scope}]: trigger-mode mismatch + nothing force-kept → no-op"
} else {
echo "CBTS [${cbts.scope}]: trigger-mode mismatch — running " +
"${parallelJobsFiltered.size()} force-kept stage(s) only"
}
} else if (parallelJobsFiltered) {
echo "CBTS [${cbts.scope}]: limiting to ${parallelJobsFiltered.size()} stages " +
"(sanity_required=${needsSanity}, perfsanity_required=${needsPerfSanity})"
} else {
echo "CBTS [${cbts.scope}]: empty stage set after filtering"
}
}

echo "Check the passed GitLab bot testFilter parameters."
def keysStr = parallelJobsFiltered.keySet().join(",\n")
pipeline.echo "Now we will run stages: [\n${keysStr}\n]"
Expand Down
Loading
Loading