Skip to content

Commit 6857fca

Browse files
committed
issue:INFINIPS-296: [CI] Add optional stage parameter to group steps in pipeline
* What: Added a new optional 'stage' property to step_conf that groups steps sharing the same stage name and runs them in parallel within sequential stage boundaries. * How: Introduced getUniqueStages/getStepsForStage helpers, made getTasks/getMatrixTasks/run_parallel_in_chunks stage-aware, and rewired startPipeline to iterate stages sequentially while running each stage's tasks in parallel across all images. * Why: Allow CI pipeline authors to define logical phases (build, test, deploy) so all steps in a phase complete before the next begins, improving pipeline structure and BlueOcean UI clarity. Signed-off-by: Benny Itkin <bitkin@nvidia.com>
1 parent 41a0163 commit 6857fca

3 files changed

Lines changed: 128 additions & 16 deletions

File tree

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
---
2+
job: ci-demo-stages-example
3+
4+
kubernetes:
5+
cloud: swx-k8s-spray
6+
7+
runs_on_dockers:
8+
- {name: 'ubuntu2204', url: 'ubuntu:22.04', arch: 'x86_64'}
9+
- {name: 'rhel9', url: 'redhat/ubi9', arch: 'x86_64'}
10+
11+
# Steps with the same 'stage' value are grouped and run in parallel
12+
# under a single Jenkins stage block. Steps without 'stage' run
13+
# sequentially as usual.
14+
15+
steps:
16+
- name: Build lib
17+
run: echo "building lib"
18+
stage: build
19+
20+
- name: Build tools
21+
run: echo "building tools"
22+
stage: build
23+
24+
- name: Build docs
25+
run: echo "building docs"
26+
stage: build
27+
28+
- name: Unit tests
29+
run: echo "running unit tests"
30+
stage: test
31+
32+
- name: Integration tests
33+
run: echo "running integration tests"
34+
stage: test
35+
36+
- name: Publish
37+
containerSelector: "{name:'ubuntu2204'}"
38+
stage: publish
39+
run: echo "publishing artifacts"

schema_validator/ci_demo_schema.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -139,3 +139,4 @@ step_conf:
139139
credentialsId: any(str(), list(str()), required=False)
140140
args: any(list(), map(), str(), required=False)
141141
env: map(required=False)
142+
stage: str(required=False)

src/com/mellanox/cicd/Matrix.groovy

Lines changed: 88 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1038,10 +1038,10 @@ def runAgent(image, config, branchName=null, axis=null, Closure func, runInDocke
10381038
}
10391039

10401040

1041-
Map getTasks(axes, image, config, include, exclude) {
1041+
Map getTasks(axes, image, config, include, exclude, stageName=null) {
10421042

10431043

1044-
config.logger.trace(3, "getTasks() --> image=" + image)
1044+
config.logger.trace(3, "getTasks() --> image=" + image + " stage=" + stageName)
10451045

10461046
int serialNum = 1
10471047
Map tasks = [:]
@@ -1067,7 +1067,8 @@ Map getTasks(axes, image, config, include, exclude) {
10671067
continue
10681068
}
10691069

1070-
if (!config.steps) {
1070+
def stepsToCheck = stageName ? getStepsForStage(config, stageName) : config.steps
1071+
if (!stepsToCheck) {
10711072
continue
10721073
}
10731074

@@ -1081,8 +1082,8 @@ Map getTasks(axes, image, config, include, exclude) {
10811082
def branchName = resolveTemplate(axis, tmpl, config)
10821083

10831084
def canSkip = true
1084-
for (int j = 0; j < config.steps.size(); j++) {
1085-
def oneStep = config.steps[j]
1085+
for (int j = 0; j < stepsToCheck.size(); j++) {
1086+
def oneStep = stepsToCheck[j]
10861087
if (!check_skip_stage(image, config, branchName, oneStep, axis)) {
10871088
canSkip = false
10881089
break
@@ -1106,6 +1107,7 @@ Map getTasks(axes, image, config, include, exclude) {
11061107
}
11071108

11081109
config.logger.trace(5, "task name " + branchName)
1110+
def taskSteps = stageName ? getStepsForStage(config, stageName) : config.steps
11091111
tasks[branchName] = { ->
11101112
withEnv(axisEnv) {
11111113
if ((config.get("kubernetes") == null) &&
@@ -1120,11 +1122,11 @@ Map getTasks(axes, image, config, include, exclude) {
11201122
runBareMetal = false
11211123
}
11221124
def callback = {pimage, pconfig, pname, paxis, pruntime ->
1123-
runSteps(pimage, pconfig, pname, paxis, pruntime)
1125+
runSteps(pimage, pconfig, pname, paxis, taskSteps, pruntime)
11241126
}
11251127
runAgent(image, config, branchName, axis, callback, runBareMetal)
11261128
} else {
1127-
runK8(image, branchName, config, axis)
1129+
runK8(image, branchName, config, axis, taskSteps)
11281130
}
11291131
}
11301132
}
@@ -1154,10 +1156,31 @@ def resolveIncludeExcludeTemplates(filters, config) {
11541156
return resolvedFilters
11551157
}
11561158

1157-
def getMatrixTasks(image, config) {
1159+
@NonCPS
1160+
def getUniqueStages(config) {
1161+
// generate a uniq list of stages from the steps, if no stage is defined, add a default stage
1162+
def stages = config.steps.collect { it.stage ?: "default" }.unique()
1163+
return stages
1164+
}
1165+
1166+
def getStepsForStage(config, stageName) {
1167+
// generate a list of steps for a given stage
1168+
def stageSteps = []
1169+
if (config.steps) {
1170+
for (step in config.steps) {
1171+
def stepStage = step.get("stage", "default")
1172+
if (stepStage == stageName) {
1173+
stageSteps.add(step)
1174+
}
1175+
}
1176+
}
1177+
return stageSteps
1178+
}
1179+
1180+
def getMatrixTasks(image, config, stageName=null) {
11581181

11591182
def include = [], exclude = [], axes = []
1160-
config.logger.debug("getMatrixTasks() --> image=" + image)
1183+
config.logger.debug("getMatrixTasks() --> image=" + image + " stage=" + stageName)
11611184

11621185
if (config.get("matrix")) {
11631186
axes = getMatrixAxes(config.matrix.axes).findAll()
@@ -1177,7 +1200,7 @@ def getMatrixTasks(image, config) {
11771200
exclude.size() +
11781201
"] = " + exclude
11791202
)
1180-
return getTasks(axes, image, config, include, exclude)
1203+
return getTasks(axes, image, config, include, exclude, stageName)
11811204
}
11821205

11831206
def buildImage(img, filename, extra_args, config, image) {
@@ -1503,7 +1526,7 @@ spec:
15031526
}
15041527
}
15051528

1506-
def run_parallel_in_chunks(config, myTasks, depth) {
1529+
def run_parallel_in_chunks(config, myTasks, depth, stageName=null) {
15071530

15081531
if (myTasks.size() == 0) {
15091532
return
@@ -1518,11 +1541,22 @@ def run_parallel_in_chunks(config, myTasks, depth) {
15181541
def val = getConfigVal(config, ['failFast'], false)
15191542

15201543
config.logger.trace(3, "run_parallel_in_chunks: batch size is ${bSize}")
1521-
(myTasks.keySet() as List).collate(bSize).each {
1522-
def batchMap = myTasks.subMap(it)
1523-
batchMap['failFast'] = val
1524-
parallel batchMap
1544+
if (stageName) {
1545+
stage(stageName) {
1546+
(myTasks.keySet() as List).collate(bSize).each {
1547+
def batchMap = myTasks.subMap(it)
1548+
batchMap['failFast'] = val
1549+
parallel batchMap
1550+
}
1551+
}
1552+
} else {
1553+
(myTasks.keySet() as List).collate(bSize).each {
1554+
def batchMap = myTasks.subMap(it)
1555+
batchMap['failFast'] = val
1556+
parallel batchMap
1557+
}
15251558
}
1559+
15261560
}
15271561

15281562

@@ -1689,7 +1723,45 @@ def startPipeline(String label) {
16891723
run_step(null, config, "pipeline start", config.pipeline_start, null)
16901724
}
16911725
}
1692-
run_parallel_in_chunks(config, branches, bSize)
1726+
1727+
def stage_arch_distro_map = gen_image_map(config)
1728+
// Get unique stages and execute sequentially
1729+
def stages = getUniqueStages(config)
1730+
if (stages.size() > 1) {
1731+
config.logger.debug("Unique stages identified: " + stages)
1732+
for (stageName in stages) {
1733+
config.logger.info("Starting stage: ${stageName}")
1734+
1735+
def stageBranches = [:]
1736+
1737+
for (def entry in entrySet(stage_arch_distro_map)) {
1738+
def images = entry.value
1739+
for (int j=0; j<images.size(); j++) {
1740+
def image = images[j]
1741+
stageBranches += getMatrixTasks(image, config, stageName)
1742+
}
1743+
}
1744+
1745+
if (config.runs_on_agents) {
1746+
for (int a=0; a<config.runs_on_agents.size();a++) {
1747+
def image = config.runs_on_agents[a]
1748+
image.name = image.nodeLabel
1749+
image.arch = 'x86_64'
1750+
stageBranches += getMatrixTasks(image, config, stageName)
1751+
}
1752+
}
1753+
1754+
// Run all tasks for this stage in parallel
1755+
if (stageBranches.size() > 0) {
1756+
run_parallel_in_chunks(config, stageBranches, bSize,stageName)
1757+
}
1758+
1759+
config.logger.info("Completed stage: ${stageName}")
1760+
}
1761+
} else {
1762+
// Fallback if no stages defined
1763+
run_parallel_in_chunks(config, branches, bSize)
1764+
}
16931765
}
16941766
}
16951767
config.env.pipeline_status = 'SUCCESS'

0 commit comments

Comments
 (0)