Repository navigation
2198 lines (2043 loc) · 111 KB
/
Copy pathrelease.yml
File metadata and controls
2198 lines (2043 loc) · 111 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
name: Release Pipeline
# workflow_dispatch is capped at 10 inputs by GitHub (this block has 9).
# on_demand/release_version are workflow_call-only: the sole on-demand entry
# point is stage-release-rc.yml. The ops gate and the manual approval gate live
# HERE (authorize/approval jobs), so they bind every caller; stage-release-rc.yml
# repeats only the ops gate, as a fast-fail before its long ECR wait.
on:
workflow_dispatch:
inputs:
commit_sha:
description: 'Git commit SHA whose post-merge CI images to publish (full 40-char SHA).'
required: true
type: string
rc_number:
description: 'RC number (e.g., 0 for rc0). Leave empty to auto-increment.'
required: false
type: string
nightly:
description: 'Publish as nightly (nightly-dated NGC tags; dated pre-release Helm charts; skips RC). Must be run from main.'
required: false
type: boolean
default: false
skip_gitlab_pipeline:
description: 'Skip the GitLab release automation pipeline trigger. Emergency use only.'
required: false
type: boolean
default: false
slack_thread_ts:
description: 'Slack thread ts to reply into. Empty skips thread replies. Channel ID is read from secrets.SLACK_RELEASE_CHANNEL_ID.'
required: false
type: string
default: ''
containers:
description: 'Container subset to publish (all|none|csv of vllm-runtime,vllm-efa,sglang-runtime,sglang-efa,trtllm-runtime,trtllm-efa,frontend,operator,planner,sidecar). Partial rc re-publishes supported.'
required: false
type: string
default: all
wheels:
description: 'Wheel subset to stage (all|none|csv of ai-dynamo,ai-dynamo-runtime,kvbm).'
required: false
type: string
default: all
crates:
description: 'Crate subset (all|none|csv of crate names, dependency-closed). Only staged on on-demand runs.'
required: false
type: string
default: all
helm:
description: 'Helm chart subset to push (all|none|platform).'
required: false
type: string
default: all
workflow_call:
inputs:
commit_sha:
required: true
type: string
rc_number:
required: false
type: string
nightly:
required: false
type: boolean
default: false
skip_gitlab_pipeline:
required: false
type: boolean
default: false
slack_thread_ts:
required: false
type: string
default: ''
on_demand:
required: false
type: boolean
default: false
release_version:
required: false
type: string
default: ''
containers:
required: false
type: string
default: all
wheels:
required: false
type: string
default: all
crates:
required: false
type: string
default: all
helm:
required: false
type: string
default: all
# Nightly-only: the result of THIS nightly run's operator-build /
# planner-build (needs.<job>.result — success/failure/skipped/cancelled).
# The copy step layers this on top of the crane probe:
# non-empty and != success -> skip. A failed build tonight must never
# republish yesterday's image when the resolved SHA is unchanged
# (quiet-repo day) — SHA-keyed tags alone can't tell which run
# built them.
# empty (direct workflow_dispatch — no builds in this run) -> the
# probe alone decides, matching the runtimes: a manual nightly
# re-stage copies whatever nightly-built -nightly ECR tags exist
# for the SHA.
operator_build_result:
required: false
type: string
default: ''
planner_build_result:
required: false
type: string
default: ''
frontend_build_result:
required: false
type: string
default: ''
vllm_efa_build_result:
required: false
type: string
default: ''
sglang_efa_build_result:
required: false
type: string
default: ''
trtllm_efa_build_result:
required: false
type: string
default: ''
outputs:
# The exact NGC docker tag published for this run (nightly: <date>-<sha7>).
# Informational for callers — recomputing the date can drift across a
# UTC-midnight boundary; no current caller consumes it.
ngc_version_tag:
description: 'NGC docker version tag published by this run.'
value: ${{ jobs.prepare-release.outputs.ngc_version_tag }}
# Note: workflow_dispatch can only be triggered from release/* branches (or main when nightly=true)
# This is enforced in the prepare-release job via branch validation. workflow_call paths
# (e.g. nightly-ci.yml) inherit the caller's ref — the same branch rules apply.
permissions:
contents: read
env:
ECR_REGISTRY: ${{ vars.ECR_REGISTRY }}
ECR_REPOSITORY: ${{ vars.ECR_REPOSITORY }}
AWS_DEFAULT_REGION: ${{ vars.AWS_DEFAULT_REGION }}
jobs:
# ============================================================================
# GATE: Version Extraction
# ============================================================================
# OPS GATE — every run requires an active ai-dynamo/devops member, except the
# unattended nightly (nightly=true AND a scheduled trigger). A manual nightly
# (dispatch of this workflow or of nightly-ci.yml) is gated like any other run.
# Lives here (not only in the callers) so it binds EVERY caller of this
# reusable workflow — including a rogue workflow that calls it directly.
#
# The two steps are YAML anchors (&ops-token / &ops-check), repeated as the
# first steps of EVERY job that reads automated-release secrets. "Re-run failed
# jobs" reuses this job's earlier success, so a job-local check is the only
# thing that re-verifies whoever triggered the re-run (github.triggering_actor).
authorize:
name: Ops gate
runs-on: prod-default-small-v2
environment: automated-release
permissions:
contents: read
steps:
- &ops-token
name: Mint app token (ops gate)
if: ${{ !(inputs.nightly && github.event_name == 'schedule') }}
id: ops-token
uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- &ops-check
name: Require release-ops team membership
if: ${{ !(inputs.nightly && github.event_name == 'schedule') }}
env:
GH_TOKEN: ${{ steps.ops-token.outputs.token }}
ORG: ${{ github.repository_owner }}
TEAM: devops
ACTOR: ${{ github.actor }}
TRIGGERING_ACTOR: ${{ github.triggering_actor }}
# The ORIGINAL trigger (the caller's, for a reusable workflow; unchanged
# on re-runs). A push/PR-triggered run must never reach release secrets,
# so a future test hook with a push trigger fails here instead.
EVENT: ${{ github.event_name }}
run: |
set -euo pipefail
case "${EVENT}" in
workflow_dispatch|schedule) ;;
*) echo "::error::The release pipeline only runs from a manual dispatch or the nightly schedule (got '${EVENT}')."; exit 1 ;;
esac
for USER in $(printf '%s\n%s\n' "${ACTOR}" "${TRIGGERING_ACTOR}" | sort -u); do
HTTP=$(curl -sS -o "${RUNNER_TEMP}/membership.json" -w '%{http_code}' \
-H "Authorization: Bearer ${GH_TOKEN}" \
-H "Accept: application/vnd.github+json" \
"https://api.github.com/orgs/${ORG}/teams/${TEAM}/memberships/${USER}")
# grep, not python3/jq: this step runs on every secret-using job's
# runner, including the gitlab_ci_runners group.
if [ "${HTTP}" != "200" ] || ! grep -Eq '"state"[[:space:]]*:[[:space:]]*"active"' "${RUNNER_TEMP}/membership.json"; then
echo "::error::@${USER} is not an active member of ${ORG}/${TEAM} (HTTP ${HTTP}); only release ops may run this release job."
exit 1
fi
echo "@${USER} authorized via ${ORG}/${TEAM}"
done
- name: Gate mode
run: echo "ops gate applied=${{ !(inputs.nightly && github.event_name == 'schedule') }} (only scheduled nightly is exempt)"
# APPROVAL GATE — operator runs also require a manual-release-approval reviewer.
# Dynamic environment: empty (no gate) on the nightly path so unattended runs are
# not blocked. Enforced by the environment's required-reviewers rule; no secrets
# here, so its environment scope is independent of the RELEASE_APP secrets.
approval:
name: Approval gate
needs: [authorize]
runs-on: prod-default-small-v2
environment: ${{ (inputs.on_demand || (github.event_name == 'workflow_dispatch' && !inputs.nightly)) && 'manual-release-approval' || '' }}
permissions:
contents: read
steps:
- name: Record approval
env:
SHA: ${{ inputs.commit_sha }}
VER: ${{ inputs.release_version }}
RC: ${{ inputs.rc_number }}
ACTOR: ${{ github.actor }}
run: echo "Release gate cleared for ${VER:-<branch-derived>} rc.${RC} @ ${SHA} by ${ACTOR}"
prepare-release:
name: Prepare Release
needs: [approval]
runs-on: prod-default-small-v2
outputs:
version: ${{ steps.extract.outputs.version }}
commit_sha: ${{ steps.extract.outputs.commit_sha }}
nightly: ${{ steps.extract.outputs.nightly }}
on_demand: ${{ steps.extract.outputs.on_demand }}
rc_number: ${{ steps.rc_tag.outputs.rc_number }}
ngc_version_tag: ${{ steps.rc_tag.outputs.ngc_version_tag }}
helm_chart_version: ${{ steps.rc_tag.outputs.helm_chart_version }}
wheel_version: ${{ steps.rc_tag.outputs.wheel_version }}
crate_version: ${{ steps.rc_tag.outputs.crate_version }}
steps:
- name: Extract version and validate inputs
id: extract
env:
COMMIT_SHA: ${{ inputs.commit_sha }}
BRANCH_NAME: ${{ github.ref_name }}
NIGHTLY: ${{ inputs.nightly }}
ON_DEMAND: ${{ inputs.on_demand }}
RELEASE_VERSION: ${{ inputs.release_version }}
CONTAINER_SUBSET: ${{ inputs.containers }}
WHEEL_SUBSET: ${{ inputs.wheels }}
CRATE_SUBSET: ${{ inputs.crates }}
HELM_SUBSET: ${{ inputs.helm }}
SLACK_THREAD_TS: ${{ inputs.slack_thread_ts }}
run: |
set -euo pipefail
if ! [[ "${COMMIT_SHA}" =~ ^[0-9a-f]{40}$ ]]; then
echo "Error: commit_sha must be a full 40-character hex SHA (got: '${COMMIT_SHA}')"
exit 1
fi
# A typo'd subset token would be silently skipped by every want()
# filter and stage nothing — fail fast instead. Crate names are
# graph-checked by stage_crates.py; only the charset is gated here.
validate_subset() {
local LABEL="$1" SPEC="$2" UNIVERSE="$3" TOK
if [ "${SPEC}" = "all" ] || [ "${SPEC}" = "none" ] || [ -z "${SPEC}" ]; then
return 0
fi
IFS=',' read -ra TOKS <<<"${SPEC}"
for TOK in "${TOKS[@]}"; do
case ",${UNIVERSE}," in
*,"${TOK}",*) ;;
*) echo "Error: unknown ${LABEL} token '${TOK}' (valid: ${UNIVERSE})"; exit 1 ;;
esac
done
}
validate_subset containers "${CONTAINER_SUBSET:-all}" "vllm-runtime,vllm-efa,sglang-runtime,sglang-efa,trtllm-runtime,trtllm-efa,frontend,operator,planner,sidecar"
validate_subset wheels "${WHEEL_SUBSET:-all}" "ai-dynamo,ai-dynamo-runtime,kvbm"
validate_subset helm "${HELM_SUBSET:-all}" "platform"
if [[ ! "${CRATE_SUBSET:-all}" =~ ^[A-Za-z0-9_,-]*$ ]]; then
echo "Error: crate subset may only contain [A-Za-z0-9_-] names separated by commas (got '${CRATE_SUBSET}')"
exit 1
fi
if [[ -n "${SLACK_THREAD_TS}" && ! "${SLACK_THREAD_TS}" =~ ^[0-9]+\.[0-9]+$ ]]; then
echo "Error: slack_thread_ts must look like a Slack ts '1234567890.123456' (got '${SLACK_THREAD_TS}')"
exit 1
fi
if [[ "${ON_DEMAND}" == "true" ]]; then
# Branch + version bump are created/validated by stage-release-rc.yml;
# the version is passed explicitly rather than derived from the ref.
if [[ ! "${RELEASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.(dev|post)[0-9]+)?$ ]]; then
echo "Error: on_demand=true requires release_version X.Y.Z[.devN|.postN] (got: '${RELEASE_VERSION}')"
exit 1
fi
# SemVer has no post-release form: .postN crates/helm would sort
# BEFORE X.Y.Z or collide with it on crates.io. Containers/wheels are fine.
if [[ "${RELEASE_VERSION}" =~ \.post[0-9]+$ ]]; then
if [ "${CRATE_SUBSET:-all}" != "none" ] || [ "${HELM_SUBSET:-all}" != "none" ]; then
echo "Error: post releases (X.Y.Z.postN) cannot ship crates or helm charts; pass crates=none and helm=none"
exit 1
fi
fi
VERSION="${RELEASE_VERSION}"
elif [[ "${NIGHTLY}" == "true" ]]; then
if [[ "${BRANCH_NAME}" != "main" ]]; then
echo "Error: nightly=true requires branch=main (got: ${BRANCH_NAME})"
exit 1
fi
VERSION="nightly"
else
# Fully anchored — a prefix-only match would accept arbitrary trailing text.
if [[ ! "$BRANCH_NAME" =~ ^release/[0-9]+\.[0-9]+\.[0-9]+(\.(dev|post)[0-9]+)?$ ]]; then
echo "Error: workflow_dispatch must be triggered from a release/X.Y.Z[.devN|.postN] branch (or main with nightly=true)"
echo "Current branch: $BRANCH_NAME"
echo "Named releases are not dispatchable here — run stage-release-rc.yml with a BARE v<X.Y.Z>[-<name>] release_branch (a release/X.Y.Z-<name> branch is treated as a throwaway test branch there too)."
exit 1
fi
VERSION="${BRANCH_NAME#release/}"
fi
echo "version=${VERSION}" >> $GITHUB_OUTPUT
echo "commit_sha=${COMMIT_SHA}" >> $GITHUB_OUTPUT
echo "nightly=${NIGHTLY}" >> $GITHUB_OUTPUT
echo "on_demand=${ON_DEMAND}" >> $GITHUB_OUTPUT
echo "Detected version: ${VERSION}"
echo "Source commit SHA: ${COMMIT_SHA}"
echo "Nightly: ${NIGHTLY}"
# Checkout at the source commit with full tags so the RC-number step can
# enumerate existing v<VERSION>-rc<N> tags and auto-increment. Also lets
# downstream jobs (release-publish, stage-wheels-artifactory) consume the
# computed rc_number/ngc_version_tag as needs.prepare-release.outputs.*
# and run in parallel.
- name: Checkout at source commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ steps.extract.outputs.commit_sha }}
fetch-depth: 0
fetch-tags: true
- name: Determine next RC number
id: rc_tag
env:
INPUT_RC_NUMBER: ${{ inputs.rc_number }}
VERSION: ${{ steps.extract.outputs.version }}
COMMIT_SHA: ${{ steps.extract.outputs.commit_sha }}
NIGHTLY: ${{ steps.extract.outputs.nightly }}
ON_DEMAND: ${{ steps.extract.outputs.on_demand }}
run: |
set -euo pipefail
# Base version pinned in pyproject.toml at the source SHA. Same value
# cargo and maturin stamp into the wheel filename / METADATA. The
# optional .devN (pre-release) / .postN (post-release) tail covers dev
# builds and post-release respins.
BASE_VERSION=$(awk '/^version = / {gsub(/"/, "", $3); print $3; exit}' pyproject.toml)
if [[ ! "${BASE_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+(\.(dev|post)[0-9]+)?$ ]]; then
echo "Error: cannot parse base version from pyproject.toml (got '${BASE_VERSION}')"
exit 1
fi
# rc dispatch derives VERSION from the branch name but stamps wheels from
# the tree at COMMIT_SHA — a mismatch ships wheels versioned differently
# from the image tags (e.g. a SHA pasted from the wrong branch, or a
# branch cut whose version bump hasn't landed yet).
if [[ "${NIGHTLY}" != "true" && "${BASE_VERSION}" != "${VERSION}" ]]; then
echo "Error: pyproject version '${BASE_VERSION}' at ${COMMIT_SHA:0:12} != release version '${VERSION}' — is the SHA on this release branch (post version-bump)?"
exit 1
fi
if [[ "${NIGHTLY}" == "true" ]]; then
DATE_UTC=$(date -u +%Y%m%d)
SHORT_SHA="${COMMIT_SHA:0:7}"
# No `nightly-` prefix on the tag: the NGC repo name itself carries
# the `-nightly` suffix (e.g. vllm-runtime-nightly), so the tag stays
# the bare date+sha (e.g. 20260520-abc1234).
NGC_VERSION_TAG="${DATE_UTC}-${SHORT_SHA}"
WHEEL_VERSION="${BASE_VERSION}.dev${DATE_UTC}"
# SemVer pre-release: sorts below the train's rc and GA. The sha
# keeps same-day re-runs unique on immutable ChartMuseum; its `g`
# prefix keeps an all-digit sha from being invalid SemVer.
HELM_CHART_VERSION="${BASE_VERSION}-dev.${DATE_UTC}.g${SHORT_SHA}"
# Crate version: dotted SemVer pre-release identifiers so they sort
# numerically (dev.20260828 < dev.20260901; an undotted -devYYYYMMDD
# is one TEXT identifier and sorts wrong against any other shape,
# e.g. -dev9 > -dev20260828). Date-only, matching the wheel's
# .devYYYYMMDD: a same-day re-run at a new SHA is idempotently
# SKIPPED by the immutable registry — the day's first staging wins.
SEMVER_BASE=$(echo "${BASE_VERSION}" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+)\.(.+)/\1-\2/')
CRATE_VERSION_NIGHTLY="${SEMVER_BASE}-dev.${DATE_UTC}"
echo "rc_number=" >> $GITHUB_OUTPUT
echo "ngc_version_tag=${NGC_VERSION_TAG}" >> $GITHUB_OUTPUT
echo "helm_chart_version=${HELM_CHART_VERSION}" >> $GITHUB_OUTPUT
echo "wheel_version=${WHEEL_VERSION}" >> $GITHUB_OUTPUT
echo "crate_version=${CRATE_VERSION_NIGHTLY}" >> $GITHUB_OUTPUT
echo "Nightly NGC tag: ${NGC_VERSION_TAG}"
echo "Nightly wheel version: ${WHEEL_VERSION}"
echo "Nightly Helm chart version: ${HELM_CHART_VERSION}"
echo "Nightly crate version: ${CRATE_VERSION_NIGHTLY}"
exit 0
fi
if [[ "${ON_DEMAND}" == "true" ]]; then
# Internal staging carries an rc disambiguator: the crate/helm registries
# are immutable, so each re-stage of the same version needs a distinct
# internal version. GA stays clean — the GitLab trigger forwards
# RELEASE_VERSION=${VERSION} + RC_NUMBER and promotes the rc-staged
# artifacts to the clean version.
# NGC tag (docker string): ${VERSION}-rcN (e.g. 0.1.0-rc0, 0.1.0.dev3-rc0)
# helm + crate (SemVer): ${SEMVER_BASE}-rcN where .devN/.postN become a
# -devN/-postN pre-release (e.g. 0.1.0-rc0, 0.1.0-post3-rc0)
# wheel: clean version, disambiguated by the rcN upload subpath.
# Passed through from stage-release-rc.yml's rc_number INPUT (default rc0;
# NO GitHub tag is created). Require it (don't default to 0) so a broken
# hand-off fails loud instead of silently colliding on rc0.
RC_NUM="${INPUT_RC_NUMBER}"
# RC_NUM is <round> or <round>.<sub> (dotted sub-increment). Both form a
# valid, numerically-ordered SemVer pre-release when emitted as ...-rc.<RC_NUM>
# (e.g. 1.3.0-rc.2 or 1.3.0-rc.2.1). The wheel subpath uses the glued rc<RC_NUM>
# (a path segment, not a version) — no SemVer/PEP440 constraint there.
if ! [[ "${RC_NUM}" =~ ^[0-9]+(\.[0-9]+)?$ ]]; then
echo "Error: on_demand requires rc_number as <round> or <round>.<sub> (got '${RC_NUM}')"
exit 1
fi
# Tree at COMMIT_SHA must be stamped with the requested version, or
# images/charts get tagged VERSION while their wheels carry BASE_VERSION.
if [ "${BASE_VERSION}" != "${VERSION}" ]; then
echo "Error: release_version '${VERSION}' does not match pyproject.toml version '${BASE_VERSION}' at ${COMMIT_SHA} (wrong commit_sha, or the version bump never landed)"
exit 1
fi
SEMVER_BASE=$(echo "${VERSION}" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+)\.(.+)/\1-\2/')
echo "rc_number=${RC_NUM}" >> $GITHUB_OUTPUT
echo "ngc_version_tag=${VERSION}-rc.${RC_NUM}" >> $GITHUB_OUTPUT
echo "helm_chart_version=${SEMVER_BASE}-rc.${RC_NUM}" >> $GITHUB_OUTPUT
echo "wheel_version=${BASE_VERSION}" >> $GITHUB_OUTPUT
echo "crate_version=${SEMVER_BASE}-rc.${RC_NUM}" >> $GITHUB_OUTPUT
echo "On-demand internal staging (rc.${RC_NUM}): NGC ${VERSION}-rc.${RC_NUM}, helm ${SEMVER_BASE}-rc.${RC_NUM}, wheel ${BASE_VERSION} @ rc${RC_NUM}, crate ${SEMVER_BASE}-rc.${RC_NUM}"
exit 0
fi
if [ -n "${INPUT_RC_NUMBER}" ]; then
if ! [[ "${INPUT_RC_NUMBER}" =~ ^[0-9]+$ ]]; then
echo "Error: rc_number must be a non-negative integer (got: ${INPUT_RC_NUMBER})"
exit 1
fi
NEXT_RC="${INPUT_RC_NUMBER}"
echo "Using provided RC number: ${NEXT_RC}"
else
echo "No RC number provided. Auto-incrementing..."
RC_PATTERN="v${VERSION}-rc"
EXISTING_RCS=$(git tag -l "${RC_PATTERN}*" | grep -E "^v${VERSION}-rc[0-9]+$" | sort -V || true)
if [ -z "$EXISTING_RCS" ]; then
NEXT_RC=0
echo "No existing RC tags found. Starting with rc0."
else
LAST_RC=$(echo "$EXISTING_RCS" | tail -1)
LAST_RC_NUM=${LAST_RC#v${VERSION}-rc}
NEXT_RC=$((LAST_RC_NUM + 1))
echo "Found existing RC tags:"
echo "$EXISTING_RCS"
echo "Last RC: ${LAST_RC}, Next RC number: ${NEXT_RC}"
fi
fi
# Normalize version for Helm SemVer: X.Y.Z.suffix → X.Y.Z-suffix
SEMVER_VERSION=$(echo "${VERSION}" | sed -E 's/^([0-9]+\.[0-9]+\.[0-9]+)\.(.+)/\1-\2/')
HELM_CHART_VERSION="${SEMVER_VERSION}-rc${NEXT_RC}"
WHEEL_VERSION="${BASE_VERSION}"
echo "rc_number=${NEXT_RC}" >> $GITHUB_OUTPUT
echo "ngc_version_tag=${VERSION}rc${NEXT_RC}" >> $GITHUB_OUTPUT
echo "helm_chart_version=${HELM_CHART_VERSION}" >> $GITHUB_OUTPUT
echo "wheel_version=${WHEEL_VERSION}" >> $GITHUB_OUTPUT
echo "RC number: ${NEXT_RC}"
echo "Helm chart version: ${HELM_CHART_VERSION}"
echo "RC wheel version: ${WHEEL_VERSION}"
# ============================================================================
# NGC PUBLISH: crane copy to NGC, Helm chart push
# Sources images from ECR using SHA-based tags produced by post-merge CI.
# ============================================================================
release-publish:
name: Publish to NGC
needs: [prepare-release]
# Skip only when neither containers nor helm are selected; nightly/rc default
# to 'all' for both, so they always run unless explicitly narrowed.
if: ${{ needs.prepare-release.result == 'success' && !(inputs.containers == 'none' && inputs.helm == 'none') }}
runs-on: prod-builder-amd-v1 # TODO: needs to identify the correct runner here, definitely should not be prod-builder-amd-v1
environment: automated-release
outputs:
# Comma-separated NGC image refs (repo:tag, no registry prefix) actually copied
# to NGC — primary version tags + -efa variants only (the -cuda13 digest aliases
# and floating :nightly tags are dropped: same image, not worth a second scan).
# Forwarded to GitLab as STAGED_CONTAINERS for nspect registration.
staged_containers: ${{ steps.copy_images.outputs.staged_containers }}
# Comma-separated chart:version pairs actually pushed to the NGC helm
# registry — helm_push covers rc/on-demand runs, nightly_helm_push covers
# nightly (the two steps are mutually exclusive on env.NIGHTLY, so exactly
# one contributes). Forwarded to GitLab for registration at the true
# chart version.
staged_helm: ${{ steps.helm_push.outputs.staged_helm || steps.nightly_helm_push.outputs.staged_helm }}
failed_copy_count: ${{ steps.copy_images.outputs.failed_count }}
env:
VERSION: ${{ needs.prepare-release.outputs.version }}
COMMIT_SHA: ${{ needs.prepare-release.outputs.commit_sha }}
NIGHTLY: ${{ needs.prepare-release.outputs.nightly }}
ON_DEMAND: ${{ needs.prepare-release.outputs.on_demand }}
CONTAINER_SUBSET: ${{ inputs.containers }}
HELM_SUBSET: ${{ inputs.helm }}
OPERATOR_BUILD_RESULT: ${{ inputs.operator_build_result }}
PLANNER_BUILD_RESULT: ${{ inputs.planner_build_result }}
FRONTEND_BUILD_RESULT: ${{ inputs.frontend_build_result }}
VLLM_EFA_BUILD_RESULT: ${{ inputs.vllm_efa_build_result }}
SGLANG_EFA_BUILD_RESULT: ${{ inputs.sglang_efa_build_result }}
TRTLLM_EFA_BUILD_RESULT: ${{ inputs.trtllm_efa_build_result }}
steps:
# Re-verify the ops gate in-job (see authorize): blocks re-run bypass.
- *ops-token
- *ops-check
- name: Checkout at source commit
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ needs.prepare-release.outputs.commit_sha }}
- name: Setup crane
env:
CRANE_VERSION: v0.20.2
run: |
curl --retry 5 --retry-delay 2 \
--connect-timeout 10 --max-time 120 \
--fail --show-error -sL \
"https://github.com/google/go-containerregistry/releases/download/${CRANE_VERSION}/go-containerregistry_Linux_x86_64.tar.gz" \
| tar -xzf - crane
sudo mv crane /usr/local/bin/
crane version
- name: Login to ECR
run: |
aws ecr get-login-password --region ${AWS_DEFAULT_REGION} | docker login --username AWS --password-stdin "${ECR_REGISTRY}"
- name: Login to NGC
env:
NGC_TOKEN: ${{ secrets.NGC_PUBLISH_TOKEN }}
NGC_USERNAME: ${{ secrets.NGC_PUBLISH_USERNAME }}
run: |
echo "${NGC_TOKEN}" | docker login nvcr.io -u "${NGC_USERNAME}" --password-stdin
echo "${NGC_TOKEN}" | crane auth login nvcr.io -u "${NGC_USERNAME}" --password-stdin
- name: Inventory selected container images in ECR
run: |
set -uo pipefail
# Report-only inventory of which selected container images are in ECR. Never fails.
# Mirrors the copy step's subset gating, nightly rules, and source tags.
NIGHTLY_TAG_SUFFIX=""
[ "${NIGHTLY}" = "true" ] && NIGHTLY_TAG_SUFFIX="-nightly"
SUBSET="${CONTAINER_SUBSET:-all}"
want() {
[ "${SUBSET}" = "all" ] && return 0
case ",${SUBSET}," in *,"$1",*) return 0 ;; esac
return 1
}
# token|ECR source tag — must match the "Copy images to NGC" sources.
ENTRIES=(
"vllm-runtime|${COMMIT_SHA}-vllm-runtime${NIGHTLY_TAG_SUFFIX}"
"sglang-runtime|${COMMIT_SHA}-sglang-runtime${NIGHTLY_TAG_SUFFIX}"
"trtllm-runtime|${COMMIT_SHA}-trtllm-runtime${NIGHTLY_TAG_SUFFIX}"
)
if [ "${NIGHTLY}" != "true" ]; then
ENTRIES+=(
"vllm-efa|${COMMIT_SHA}-vllm-runtime-efa"
"sglang-efa|${COMMIT_SHA}-sglang-runtime-efa"
"trtllm-efa|${COMMIT_SHA}-trtllm-runtime-efa"
"frontend|${COMMIT_SHA}-dynamo-frontend"
"operator|${COMMIT_SHA}-operator"
"planner|${COMMIT_SHA}-dynamo-planner"
"sidecar|${COMMIT_SHA}-dynamo-sidecar"
)
fi
{
echo "### Container image availability (ECR)"
echo ""
echo "Selection: \`${SUBSET}\`"
echo ""
echo "| Token | ECR tag | Status |"
echo "|-------|---------|--------|"
} >> "${GITHUB_STEP_SUMMARY}"
avail=0; missing=0; selected=0
echo "=== Container image availability (selection: ${SUBSET}) ==="
for E in "${ENTRIES[@]}"; do
IFS='|' read -r TOKEN TAG <<<"${E}"
want "${TOKEN}" || continue
selected=$((selected+1))
REF="${ECR_REGISTRY}/${ECR_REPOSITORY}:${TAG}"
if crane manifest "${REF}" >/dev/null 2>&1; then
echo " available: ${TOKEN} (${TAG})"
echo "| ${TOKEN} | \`${TAG}\` | available |" >> "${GITHUB_STEP_SUMMARY}"
avail=$((avail+1))
else
echo " MISSING: ${TOKEN} (${TAG})"
echo "| ${TOKEN} | \`${TAG}\` | **missing** |" >> "${GITHUB_STEP_SUMMARY}"
missing=$((missing+1))
fi
done
if [ "${selected}" -eq 0 ]; then
echo "No container images selected (subset='${SUBSET}')."
echo "_No container images selected._" >> "${GITHUB_STEP_SUMMARY}"
fi
echo "Summary: ${avail} available, ${missing} missing (of ${selected} selected)."
{
echo ""
echo "**${avail} available, ${missing} missing** (of ${selected} selected)."
} >> "${GITHUB_STEP_SUMMARY}"
- name: Copy images to NGC
id: copy_images
env:
NGC_REGISTRY: nvcr.io
NGC_ORG: ${{ secrets.NGC_PUBLISH_ORG }}
NGC_VERSION_TAG: ${{ needs.prepare-release.outputs.ngc_version_tag }}
run: |
set -euo pipefail
SUCCESSFUL_COPIES=()
FAILED_COPIES=()
# Nightly builds push ECR tags with a `-nightly` suffix so a missing
# or failed nightly build can't fall through to the post-merge image
# at the same SHA (which carries stable, non-dev wheel versions).
NIGHTLY_TAG_SUFFIX=""
if [ "${NIGHTLY}" = "true" ]; then
NIGHTLY_TAG_SUFFIX="-nightly"
fi
echo "========================================"
echo "Copying images from ECR to NGC (registry-to-registry)"
echo "Source commit SHA: ${COMMIT_SHA}"
echo "NGC Version Tag: ${NGC_VERSION_TAG}"
echo "Nightly tag suffix: '${NIGHTLY_TAG_SUFFIX}'"
echo "========================================"
copy_image() {
local SRC="$1" DST="$2" LABEL="$3"
echo "----------------------------------------"
echo "Copying: ${LABEL}"
# crane copy preserves multi-arch manifest lists by default (no --platform needed).
# Returns the real copy status — stage_optional() gates its *_STAGED
# flags and floating-tag pushes on it. Under `set -e`, BARE call
# sites must therefore carry `|| true` (a nonzero return would abort
# the whole copy loop on the first failed image); condition-context
# calls (if/elif) are exempt by shell rules.
if crane copy "${SRC}" "${DST}"; then
echo " Copied: ${LABEL}"
SUCCESSFUL_COPIES+=("${LABEL}")
return 0
else
echo " Warning: Failed to copy ${LABEL}, skipping..."
FAILED_COPIES+=("${LABEL}")
return 1
fi
}
# On nightly, segregate published images into their own NGC repos by
# appending `-nightly` to the repo name (e.g. vllm-runtime-nightly).
# This keeps consumers from accidentally pulling a nightly image from
# the stable repo.
NGC_NAME_SUFFIX=""
if [[ "${NIGHTLY}" == "true" ]]; then
NGC_NAME_SUFFIX="-nightly"
fi
# Copy only the selected container subset; nightly/rc default to the full
# set (SUBSET=all). Tokens: <fw>-runtime (bare + -cuda13 alias),
# <fw>-efa, frontend, operator, planner.
SUBSET="${CONTAINER_SUBSET:-all}"
want() {
[ "${SUBSET}" = "all" ] && return 0
case ",${SUBSET}," in *,"$1",*) return 0 ;; esac
return 1
}
# ---- CUDA 13 runtime images (vllm, sglang, trtllm) — DEFAULT ----
echo ""
echo "=== CUDA 13 Runtime Images (vllm, sglang, trtllm) — default ==="
CUDA13_FRAMEWORKS=("vllm" "sglang" "trtllm")
for FRAMEWORK in "${CUDA13_FRAMEWORKS[@]}"; do
want "${FRAMEWORK}-runtime" || { echo "skip ${FRAMEWORK}-runtime (not selected)"; continue; }
if [ "${FRAMEWORK}" = "trtllm" ]; then
NGC_NAME="tensorrtllm-runtime${NGC_NAME_SUFFIX}"
STABLE_NAME="tensorrtllm-runtime"
else
NGC_NAME="${FRAMEWORK}-runtime${NGC_NAME_SUFFIX}"
STABLE_NAME="${FRAMEWORK}-runtime"
fi
SOURCE="${ECR_REGISTRY}/${ECR_REPOSITORY}:${COMMIT_SHA}-${FRAMEWORK}-runtime${NIGHTLY_TAG_SUFFIX}"
TARGET="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo/${NGC_NAME}:${NGC_VERSION_TAG}"
copy_image "${SOURCE}" "${TARGET}" "${NGC_NAME}:${NGC_VERSION_TAG}" || true
# Explicit `-cuda13` alias on the same digest so consumers can
# pin to cuda13 specifically. The bare tag above stays as the
# implicit default and will move to cuda14+ when that flips.
EXPLICIT_TARGET="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo/${NGC_NAME}:${NGC_VERSION_TAG}-cuda13"
copy_image "${SOURCE}" "${EXPLICIT_TARGET}" "${NGC_NAME}:${NGC_VERSION_TAG}-cuda13" || true
# Mutable `:nightly` floating tag on the stable repo (e.g.
# vllm-runtime:nightly). cuda13 is the default so no cuda suffix.
if [[ "${NIGHTLY}" == "true" ]]; then
MUTABLE_TARGET="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo/${STABLE_NAME}:nightly"
copy_image "${SOURCE}" "${MUTABLE_TARGET}" "${STABLE_NAME}:nightly" || true
# Explicit `:nightly-cuda13` floating alias.
EXPLICIT_MUTABLE_TARGET="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo/${STABLE_NAME}:nightly-cuda13"
copy_image "${SOURCE}" "${EXPLICIT_MUTABLE_TARGET}" "${STABLE_NAME}:nightly-cuda13" || true
fi
done
# Nightly builds the runtime images above (vllm/sglang/trtllm
# cuda13) plus operator and planner (operator-build / planner-build
# in nightly-ci.yml — built there, not reused from post-merge, so
# their compliance-<sha>-<token> artifacts land on the nightly run
# id that GitLab's OSRB attach resolves, and their -nightly ECR
# tags can't fall through to a post-merge image).
# EFA/frontend are built by post-merge and other workflows
# on release branches; for nightly we skip them so we don't try to
# copy ECR tags that don't exist for the resolved SHA.
if [[ "${NIGHTLY}" != "true" ]]; then
# ---- EFA runtime images ----
# All EFA runtime images are multi-arch (amd64+arm64).
# crane copy preserves multi-arch manifests automatically.
echo ""
echo "=== EFA Runtime Images ==="
# vllm EFA (CUDA 13 — default; no cuda suffix on ECR or NGC)
if want vllm-efa; then
SOURCE="${ECR_REGISTRY}/${ECR_REPOSITORY}:${COMMIT_SHA}-vllm-runtime-efa"
TARGET="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo/vllm-runtime:${NGC_VERSION_TAG}-efa"
copy_image "${SOURCE}" "${TARGET}" "vllm-runtime:${NGC_VERSION_TAG}-efa" || true
fi
# sglang EFA (CUDA 13 — default; no cuda suffix on ECR or NGC)
if want sglang-efa; then
SOURCE="${ECR_REGISTRY}/${ECR_REPOSITORY}:${COMMIT_SHA}-sglang-runtime-efa"
TARGET="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo/sglang-runtime:${NGC_VERSION_TAG}-efa"
copy_image "${SOURCE}" "${TARGET}" "sglang-runtime:${NGC_VERSION_TAG}-efa" || true
fi
# trtllm EFA (CUDA 13 — default; no cuda suffix on ECR or NGC)
if want trtllm-efa; then
SOURCE="${ECR_REGISTRY}/${ECR_REPOSITORY}:${COMMIT_SHA}-trtllm-runtime-efa"
TARGET="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo/tensorrtllm-runtime:${NGC_VERSION_TAG}-efa"
copy_image "${SOURCE}" "${TARGET}" "tensorrtllm-runtime:${NGC_VERSION_TAG}-efa" || true
fi
# ---- Frontend image (already multi-arch from build-frontend-image workflow) ----
if want frontend; then
echo ""
echo "=== Frontend Image ==="
SOURCE="${ECR_REGISTRY}/${ECR_REPOSITORY}:${COMMIT_SHA}-dynamo-frontend"
TARGET="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo/dynamo-frontend:${NGC_VERSION_TAG}"
copy_image "${SOURCE}" "${TARGET}" "dynamo-frontend:${NGC_VERSION_TAG}" || true
fi
# ---- Operator image (multi-arch manifest already built by post-merge operator-build) ----
if want operator; then
echo ""
echo "=== Operator Image ==="
OPERATOR_SOURCE="${ECR_REGISTRY}/${ECR_REPOSITORY}:${COMMIT_SHA}-operator"
OPERATOR_TARGET="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo/kubernetes-operator:${NGC_VERSION_TAG}"
copy_image "${OPERATOR_SOURCE}" "${OPERATOR_TARGET}" "kubernetes-operator:${NGC_VERSION_TAG}" || true
fi
# ---- Planner image (CPU-only, multi-arch from post-merge planner-pipeline) ----
if want planner; then
echo ""
echo "=== Planner Image ==="
SOURCE="${ECR_REGISTRY}/${ECR_REPOSITORY}:${COMMIT_SHA}-dynamo-planner"
TARGET="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo/dynamo-planner:${NGC_VERSION_TAG}"
copy_image "${SOURCE}" "${TARGET}" "dynamo-planner:${NGC_VERSION_TAG}" || true
fi
# ---- Sidecar image (unified, multi-arch; built by post-merge's
# sidecar-build as <sha>-dynamo-sidecar) ----
if want sidecar; then
echo ""
echo "=== Sidecar Image ==="
SOURCE="${ECR_REGISTRY}/${ECR_REPOSITORY}:${COMMIT_SHA}-dynamo-sidecar"
TARGET="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo/dynamo-sidecar:${NGC_VERSION_TAG}"
copy_image "${SOURCE}" "${TARGET}" "dynamo-sidecar:${NGC_VERSION_TAG}" || true
fi
else
# Optional nightly images — operator, planner, frontend and the
# three runtime EFA variants — are built by THIS
# nightly run (ordering-only needs of the release, deliberately
# outside its success gate) and staged fail-soft: a build flake
# must warn-skip here, not fail (a bare copy_image failure under
# set -e would abort the step and skip the GitLab trigger for the
# whole nightly).
#
# stage_optional gates each image in three layers:
# 1. Caller-forwarded build result — non-success skips: SHA-keyed
# tags alone can't tell tonight's build from yesterday's when
# the resolved SHA is unchanged (quiet-repo day), and a failed
# build must never republish a previous run's image. An EMPTY
# result means a direct workflow_dispatch (no builds in this
# run): the probe alone decides, matching the runtimes — a
# manual re-stage copies whatever nightly-built tags exist.
# 2. crane-manifest probe — push-integrity check on the ECR tag.
# 3. copy_image — a failure with the source PRESENT points at
# the NGC target (write access / repo config).
# On success it also repushes the image's mutable floating tag on
# the stable repo (parity with the runtimes' :nightly aliases);
# a floating-tag miss records in FAILED_COPIES but is non-fatal
# under set -e (copy_image runs in an if-condition context).
stage_optional() {
local LABEL="$1" RESULT="$2" SRC="$3" DST="$4" FLOAT_DST="$5" FLOAT_LABEL="$6"
echo ""
echo "=== ${LABEL} (nightly) ==="
if [ -n "${RESULT}" ] && [ "${RESULT}" != "success" ]; then
echo "::warning::Skipped ${LABEL} — build result in this run: '${RESULT}'"
FAILED_COPIES+=("${LABEL} (build ${RESULT})")
return 1
elif ! crane manifest "${SRC}" > /dev/null 2>&1; then
echo "::warning::Skipped ${LABEL} — ${SRC} not found (no nightly build exists for this SHA)"
FAILED_COPIES+=("${LABEL} (source missing)")
return 1
elif copy_image "${SRC}" "${DST}" "${LABEL}"; then
if [ -n "${FLOAT_DST}" ]; then
if ! copy_image "${SRC}" "${FLOAT_DST}" "${FLOAT_LABEL}"; then
echo "::warning::Floating tag push failed for ${FLOAT_LABEL} (dated tag staged fine)"
fi
fi
return 0
else
echo "::warning::Copy failed for ${LABEL} — source exists; check NGC write access"
return 1
fi
}
ECR_BASE="${ECR_REGISTRY}/${ECR_REPOSITORY}"
NGC_BASE="${NGC_REGISTRY}/${NGC_ORG}/ai-dynamo"
OPERATOR_STAGED=false
if want operator && stage_optional "kubernetes-operator-nightly:${NGC_VERSION_TAG}" "${OPERATOR_BUILD_RESULT}" \
"${ECR_BASE}:${COMMIT_SHA}-operator${NIGHTLY_TAG_SUFFIX}" \
"${NGC_BASE}/kubernetes-operator-nightly:${NGC_VERSION_TAG}" \
"${NGC_BASE}/kubernetes-operator:nightly" "kubernetes-operator:nightly"; then OPERATOR_STAGED=true; fi
PLANNER_STAGED=false
if want planner && stage_optional "dynamo-planner-nightly:${NGC_VERSION_TAG}" "${PLANNER_BUILD_RESULT}" \
"${ECR_BASE}:${COMMIT_SHA}-dynamo-planner${NIGHTLY_TAG_SUFFIX}" \
"${NGC_BASE}/dynamo-planner-nightly:${NGC_VERSION_TAG}" \
"${NGC_BASE}/dynamo-planner:nightly" "dynamo-planner:nightly"; then PLANNER_STAGED=true; fi
FRONTEND_STAGED=false
if want frontend && stage_optional "dynamo-frontend-nightly:${NGC_VERSION_TAG}" "${FRONTEND_BUILD_RESULT}" \
"${ECR_BASE}:${COMMIT_SHA}-dynamo-frontend${NIGHTLY_TAG_SUFFIX}" \
"${NGC_BASE}/dynamo-frontend-nightly:${NGC_VERSION_TAG}" \
"${NGC_BASE}/dynamo-frontend:nightly" "dynamo-frontend:nightly"; then FRONTEND_STAGED=true; fi
# EFA variants live in the runtime -nightly repos under a -efa tag
# suffix, mirroring the RC convention (vllm-runtime:<ver>-efa).
VLLM_EFA_STAGED=false
if want vllm-efa && stage_optional "vllm-runtime-nightly:${NGC_VERSION_TAG}-efa" "${VLLM_EFA_BUILD_RESULT}" \
"${ECR_BASE}:${COMMIT_SHA}-vllm-runtime-efa${NIGHTLY_TAG_SUFFIX}" \
"${NGC_BASE}/vllm-runtime-nightly:${NGC_VERSION_TAG}-efa" \
"${NGC_BASE}/vllm-runtime:nightly-efa" "vllm-runtime:nightly-efa"; then VLLM_EFA_STAGED=true; fi
SGLANG_EFA_STAGED=false
if want sglang-efa && stage_optional "sglang-runtime-nightly:${NGC_VERSION_TAG}-efa" "${SGLANG_EFA_BUILD_RESULT}" \
"${ECR_BASE}:${COMMIT_SHA}-sglang-runtime-efa${NIGHTLY_TAG_SUFFIX}" \
"${NGC_BASE}/sglang-runtime-nightly:${NGC_VERSION_TAG}-efa" \
"${NGC_BASE}/sglang-runtime:nightly-efa" "sglang-runtime:nightly-efa"; then SGLANG_EFA_STAGED=true; fi
TRTLLM_EFA_STAGED=false
if want trtllm-efa && stage_optional "tensorrtllm-runtime-nightly:${NGC_VERSION_TAG}-efa" "${TRTLLM_EFA_BUILD_RESULT}" \
"${ECR_BASE}:${COMMIT_SHA}-trtllm-runtime-efa${NIGHTLY_TAG_SUFFIX}" \
"${NGC_BASE}/tensorrtllm-runtime-nightly:${NGC_VERSION_TAG}-efa" \
"${NGC_BASE}/tensorrtllm-runtime:nightly-efa" "tensorrtllm-runtime:nightly-efa"; then TRTLLM_EFA_STAGED=true; fi
fi
# ---- Summary ----
echo "successful_count=${#SUCCESSFUL_COPIES[@]}" >> $GITHUB_OUTPUT
echo "failed_count=${#FAILED_COPIES[@]}" >> $GITHUB_OUTPUT
echo "operator_staged=${OPERATOR_STAGED:-}" >> $GITHUB_OUTPUT
echo "planner_staged=${PLANNER_STAGED:-}" >> $GITHUB_OUTPUT
echo "frontend_staged=${FRONTEND_STAGED:-}" >> $GITHUB_OUTPUT
echo "vllm_efa_staged=${VLLM_EFA_STAGED:-}" >> $GITHUB_OUTPUT
echo "sglang_efa_staged=${SGLANG_EFA_STAGED:-}" >> $GITHUB_OUTPUT
echo "trtllm_efa_staged=${TRTLLM_EFA_STAGED:-}" >> $GITHUB_OUTPUT
# Staged container refs for nspect: repo:tag labels only (NO registry/org —
# that's a secret; GitLab prefixes with its own NGC base). Keep primary version
# tags + -efa; drop -cuda13 digest aliases and floating :nightly* (same image).
STAGED_CTRS=""
for L in "${SUCCESSFUL_COPIES[@]:-}"; do
[ -z "${L}" ] && continue
case "${L}" in *-cuda13|*:nightly|*:nightly-*) continue ;; esac
[ -n "${STAGED_CTRS}" ] && STAGED_CTRS="${STAGED_CTRS},"
STAGED_CTRS="${STAGED_CTRS}${L}"
done
echo "staged_containers=${STAGED_CTRS}" >> $GITHUB_OUTPUT
{
echo "### Staged containers (repo:tag)"
echo ""
echo "\`${STAGED_CTRS:-<none>}\`"
} >> "${GITHUB_STEP_SUMMARY}"
printf '%s\n' "${SUCCESSFUL_COPIES[@]}" > /tmp/successful_copies.txt
printf '%s\n' "${FAILED_COPIES[@]}" > /tmp/failed_copies.txt 2>/dev/null || true
echo "========================================"
echo "NGC Publishing Summary:"
echo " Successful: ${#SUCCESSFUL_COPIES[@]}"
echo " Failed: ${#FAILED_COPIES[@]}"
echo "========================================"
if [ "${SUBSET}" = "none" ]; then
echo "Container subset is 'none'; no images copied (helm-only or wheel/crate-only release)."
elif [ ${#SUCCESSFUL_COPIES[@]} -eq 0 ]; then
echo "ERROR: No images were successfully copied to NGC!"
exit 1
fi
- name: Validate chart/image coherence (on-demand)
if: ${{ env.NIGHTLY != 'true' && env.ON_DEMAND == 'true' && env.HELM_SUBSET != 'none' }}
env:
STAGED_CONTAINERS: ${{ steps.copy_images.outputs.staged_containers }}
NGC_VERSION_TAG: ${{ needs.prepare-release.outputs.ngc_version_tag }}
run: |
set -euo pipefail
# A published chart must reference only images published now (in
# CONTAINER_SUBSET, and proven staged — copy_image failures are
# non-fatal) or already resolvable at the exact repository the chart
# references (the GA registry in values.yaml — NOT the staging org,
# where only rc-suffixed tags exist).
HELM="${HELM_SUBSET:-all}"
CSET="${CONTAINER_SUBSET:-all}"
want_helm() { [ "${HELM}" = "all" ] && return 0; case ",${HELM}," in *,"$1",*) return 0 ;; esac; return 1; }
want_ctr() { [ "${CSET}" = "all" ] && return 0; case ",${CSET}," in *,"$1",*) return 0 ;; esac; return 1; }
fail=0
# Each site: chart_token|container_token|values.yaml|image repo (as referenced by the chart)|staged image name
SITES=(
"platform|operator|deploy/helm/charts/platform/components/operator/values.yaml|nvcr.io/nvidia/ai-dynamo/kubernetes-operator|kubernetes-operator"
)
for SITE in "${SITES[@]}"; do
IFS='|' read -r CHART CTOK VALUES REPO STAGED <<<"${SITE}"
want_helm "${CHART}" || { echo "chart ${CHART} not published; skip coherence check"; continue; }
if want_ctr "${CTOK}"; then
# Selected: the copy step tolerates per-image failures, so require
# the image in its successful-copy record before shipping a chart.
if [[ ",${STAGED_CONTAINERS}," == *",${STAGED}:${NGC_VERSION_TAG},"* ]]; then
echo "ok: ${CHART} image ${CTOK} staged this release (${STAGED}:${NGC_VERSION_TAG})"
else
echo "::error::chart ${CHART} needs ${CTOK}, which was selected but ${STAGED}:${NGC_VERSION_TAG} failed to copy — see 'Copy images to NGC'"
fail=1
fi
continue
fi
# Image excluded: read the pinned tag the bump wrote and prove it exists
# at the exact repository string the chart will deploy from.
TAG=$(awk -v r="${REPO}" '
$0 ~ "repository:[[:space:]]*"r {f=1; next}
f && /tag:/ { gsub(/[",]/,""); print $2; exit }' "${VALUES}")
if [ -z "${TAG}" ]; then
echo "::error::could not read pinned tag for ${REPO} in ${VALUES}"; fail=1; continue
fi
if crane manifest "${REPO}:${TAG}" >/dev/null 2>&1; then
echo "ok: ${CHART} references ${REPO}:${TAG} (exists)"
else
echo "::error::chart ${CHART} references ${REPO}:${TAG}, which is NOT in this release (${CTOK} excluded) and does NOT resolve"
fail=1
fi
done
[ "${fail}" -eq 0 ] || { echo "::error::chart/image coherence check failed"; exit 1; }
- name: Package and push Helm charts to NGC
id: helm_push