Skip to content

Commit 07a05f8

Browse files
authored
fix(ci): gate tagged publication on qualification (#4198)
* fix(ci): gate tagged publication on qualification Signed-off-by: Simon Scatton <sscatton@nvidia.com> * test(ci): remove release publication regression scaffolding Signed-off-by: Simon Scatton <sscatton@nvidia.com> * refactor(ci): separate snap packaging from publication Signed-off-by: Simon Scatton <sscatton@nvidia.com> --------- Signed-off-by: Simon Scatton <sscatton@nvidia.com>
1 parent 8990656 commit 07a05f8

7 files changed

Lines changed: 127 additions & 68 deletions

File tree

‎.agents/skills/watch-github-actions/SKILL.md‎

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -147,9 +147,12 @@ During `0.x`, a minor train permits compatibility findings
147147
as warnings; a patch train or no active train rejects them. Compare the current
148148
train's version with the latest stable release; commit messages are irrelevant.
149149
Compilation, baseline, and tool errors remain fatal. The `protobuf_compatibility` suite participates in
150-
the `release-tag-v1` qualification profile. Failed qualification prevents stable
151-
publication but still allows pre-release artifacts to publish with the failure
152-
recorded.
150+
the `release-tag-v1` qualification profile. Both tagged pre-release and stable
151+
publication require this profile to pass. Failed, cancelled, or skipped suites
152+
block publication; build artifacts and qualification evidence remain in Actions
153+
storage for diagnosis. Source-SHA images are staging inputs for qualification.
154+
Snap builds run in parallel with qualification, but tagged stable Store uploads
155+
consume those built artifacts only after qualification passes.
153156

154157
View logs for a specific run:
155158

‎.github/workflows/release-dev.yml‎

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -249,10 +249,6 @@ jobs:
249249
uses: ./.github/workflows/snap-package.yml
250250
with:
251251
checkout-ref: ${{ github.sha }}
252-
upload-channel: latest/edge
253-
github-environment: latest/edge
254-
secrets:
255-
publish-credentials: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
256252

257253
build-rpm:
258254
name: Build RPM Packages
@@ -264,6 +260,16 @@ jobs:
264260
rpm-release: ${{ needs.compute-versions.outputs.rpm_release }}
265261
cargo-version: ${{ needs.compute-versions.outputs.cargo_version }}
266262

263+
publish-snap:
264+
name: Publish Snap
265+
needs: build-snap
266+
uses: ./.github/workflows/snap-publish.yml
267+
with:
268+
upload-channel: latest/edge
269+
github-environment: latest/edge
270+
secrets:
271+
publish-credentials: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
272+
267273
# ---------------------------------------------------------------------------
268274
# Create / update the dev GitHub Release with CLI, gateway, driver, and wheels
269275
# ---------------------------------------------------------------------------
@@ -279,7 +285,7 @@ jobs:
279285
- vm-e2e
280286
- build-deb
281287
- build-rpm
282-
- build-snap
288+
- publish-snap
283289
runs-on: linux-amd64-cpu8
284290
timeout-minutes: 10
285291
permissions:

‎.github/workflows/release-tag.yml‎

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -311,10 +311,10 @@ jobs:
311311
retention-days: 90
312312
if-no-files-found: error
313313

314-
- name: Require current qualification profile for stable releases
315-
if: needs.compute-versions.outputs.is_prerelease != 'true' && steps.summary.outputs.current-profile-passed != 'true'
314+
- name: Require current qualification profile before publication
315+
if: steps.summary.outputs.current-profile-passed != 'true'
316316
run: |
317-
echo "Stable release ${RELEASE_TAG} did not pass the current release-tag-v1 qualification profile." >&2
317+
echo "Release ${RELEASE_TAG} did not pass the current release-tag-v1 qualification profile." >&2
318318
exit 1
319319
320320
build-python-wheel:
@@ -370,11 +370,6 @@ jobs:
370370
uses: ./.github/workflows/snap-package.yml
371371
with:
372372
checkout-ref: ${{ needs.compute-versions.outputs.source_sha }}
373-
upload-channel: ${{ needs.compute-versions.outputs.is_prerelease == 'true' && 'latest/edge' || 'latest/stable' }}
374-
github-environment: ${{ needs.compute-versions.outputs.is_prerelease == 'true' && 'latest/edge' || 'latest/stable' }}
375-
publish: ${{ needs.compute-versions.outputs.is_prerelease != 'true' }}
376-
secrets:
377-
publish-credentials: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
378373

379374
build-rpm:
380375
name: Build RPM Packages
@@ -386,11 +381,23 @@ jobs:
386381
rpm-release: ${{ needs.compute-versions.outputs.rpm_release }}
387382
cargo-version: ${{ needs.compute-versions.outputs.cargo_version }}
388383

384+
publish-snap:
385+
name: Publish Snap
386+
needs: [compute-versions, release, qualification-result]
387+
if: needs.compute-versions.outputs.is_prerelease != 'true' && needs.qualification-result.outputs.current-profile-passed == 'true'
388+
uses: ./.github/workflows/snap-publish.yml
389+
with:
390+
upload-channel: latest/stable
391+
github-environment: latest/stable
392+
secrets:
393+
publish-credentials: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}
394+
389395
# ---------------------------------------------------------------------------
390-
# Assemble release artifacts. Stable publication remains qualification-gated.
396+
# Assemble release artifacts after the current qualification profile passes.
391397
# ---------------------------------------------------------------------------
392398
release:
393399
name: Release
400+
if: needs.qualification-result.outputs.current-profile-passed == 'true'
394401
needs:
395402
- compute-versions
396403
- package-binaries
@@ -740,9 +747,10 @@ jobs:
740747
publish-qualification:
741748
name: Publish Qualification Summary (OCI)
742749
if: >-
743-
always()
750+
!cancelled()
744751
&& needs.release.result == 'success'
745752
&& needs.qualification-result.result == 'success'
753+
&& needs.qualification-result.outputs.current-profile-passed == 'true'
746754
needs: [compute-versions, qualification-result, release]
747755
runs-on: ubuntu-latest
748756
timeout-minutes: 5

‎.github/workflows/snap-package.yml‎

Lines changed: 0 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -9,24 +9,6 @@ on:
99
checkout-ref:
1010
required: true
1111
type: string
12-
upload-channel:
13-
required: true
14-
type: string
15-
description: "Snap Store channel to upload to (e.g., latest/edge, latest/candidate, latest/stable)"
16-
github-environment:
17-
required: true
18-
type: string
19-
description: "GitHub deployment environment for approval gates (e.g., latest/edge, latest/stable)"
20-
publish:
21-
required: false
22-
type: boolean
23-
default: true
24-
description: "Whether to upload the built snap to the Snap Store"
25-
26-
secrets:
27-
publish-credentials:
28-
required: true
29-
description: "Snap Store credentials (SNAPCRAFT_STORE_CREDENTIALS)"
3012

3113
permissions:
3214
contents: read
@@ -49,7 +31,6 @@ jobs:
4931
runner: linux-arm64-cpu8
5032
runs-on: ${{ matrix.runner }}
5133
timeout-minutes: 60
52-
environment: ${{ inputs.github-environment }}
5334
steps:
5435
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
5536
with:
@@ -172,24 +153,3 @@ jobs:
172153
${{ steps.capture.outputs.snap-file }}
173154
*.comp
174155
retention-days: 5
175-
176-
- name: Upload snap to Snap Store
177-
if: inputs.publish
178-
env:
179-
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.publish-credentials }}
180-
INPUTS_UPLOAD_CHANNEL: ${{ inputs.upload-channel }}
181-
run: |
182-
set -euo pipefail
183-
SNAP_FILE="${{ steps.capture.outputs.snap-file }}"
184-
SNAP_NAME="${SNAP_FILE%.snap}"
185-
SNAP_NAME="${SNAP_NAME%%_*}"
186-
187-
COMPONENT_ARGS=()
188-
shopt -s nullglob
189-
for comp in "${SNAP_NAME}"+*.comp; do
190-
echo "Adding component: $comp"
191-
COMPONENT_ARGS+=(--component "$comp")
192-
done
193-
194-
echo "Uploading $SNAP_FILE to ${INPUTS_UPLOAD_CHANNEL}"
195-
snapcraft upload --release "${INPUTS_UPLOAD_CHANNEL}" "$SNAP_FILE" "${COMPONENT_ARGS[@]}"

‎.github/workflows/snap-publish.yml‎

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
name: Publish Snap
5+
6+
on:
7+
workflow_call:
8+
inputs:
9+
upload-channel:
10+
required: true
11+
type: string
12+
github-environment:
13+
required: true
14+
type: string
15+
secrets:
16+
publish-credentials:
17+
required: true
18+
19+
permissions:
20+
contents: read
21+
22+
defaults:
23+
run:
24+
shell: bash
25+
26+
jobs:
27+
publish:
28+
name: Publish Snap (Linux ${{ matrix.arch }})
29+
strategy:
30+
matrix:
31+
include:
32+
- arch: amd64
33+
runner: linux-amd64-cpu8
34+
- arch: arm64
35+
runner: linux-arm64-cpu8
36+
runs-on: ${{ matrix.runner }}
37+
timeout-minutes: 10
38+
environment: ${{ inputs.github-environment }}
39+
steps:
40+
- name: Download built snap and components
41+
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
42+
with:
43+
name: snap-linux-${{ matrix.arch }}
44+
45+
- name: Install snapcraft
46+
run: |
47+
set -euo pipefail
48+
sudo apt-get update
49+
sudo apt-get install -y snapd
50+
sudo systemctl enable --now snapd.socket
51+
sudo systemctl start snapd
52+
sudo snap wait system seed.loaded
53+
sudo snap install snapcraft --classic
54+
55+
- name: Upload snap to Snap Store
56+
env:
57+
SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.publish-credentials }}
58+
INPUTS_UPLOAD_CHANNEL: ${{ inputs.upload-channel }}
59+
run: |
60+
set -euo pipefail
61+
shopt -s nullglob
62+
snaps=(*.snap)
63+
if [[ "${#snaps[@]}" -ne 1 ]]; then
64+
echo "Expected exactly one built snap, found ${#snaps[@]}" >&2
65+
exit 1
66+
fi
67+
snap_file="${snaps[0]}"
68+
snap_name="${snap_file%.snap}"
69+
snap_name="${snap_name%%_*}"
70+
component_args=()
71+
for comp in "${snap_name}"+*.comp; do
72+
component_args+=(--component "$comp")
73+
done
74+
snapcraft upload --release "${INPUTS_UPLOAD_CHANNEL}" "$snap_file" "${component_args[@]}"

‎CI.md‎

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ missing baselines, merge conflicts, and tool failures always fail the check.
4242

4343
Release Tag compares the tagged candidate cumulatively against the previous
4444
stable release, using the same minor-versus-patch policy. Its result is part
45-
of the qualification profile: failure blocks stable publication, while a
46-
pre-release can still publish with failed qualification recorded. This checks
45+
of the qualification profile: failure blocks both pre-release and stable
46+
publication. Failed candidates retain build artifacts and evidence in Actions. This checks
4747
protobuf compatibility; SDK/configuration compatibility and migration review
4848
remain separate qualification work.
4949

@@ -188,9 +188,8 @@ candidate snapshot. Cargo Deny uses its existing NVIDIA self-hosted runner and
188188
CI container.
189189

190190
Tagged releases treat Cargo Deny and Codex Security findings as failures of the
191-
currently implemented qualification profile. A profile failure does not prevent
192-
a pre-release candidate's complete artifact set from being published, but it
193-
does prevent stable publication. CodeQL, Trivy, and Zizmor findings are
191+
currently implemented qualification profile. A profile failure prevents both
192+
pre-release and stable publication. CodeQL, Trivy, and Zizmor findings are
194193
temporarily informational for tagged releases: the existing findings were
195194
reviewed and accepted for v0.1.0 and will be addressed in 0.1.x releases.
196195
Scanner failures still fail qualification.
@@ -209,8 +208,9 @@ gh workflow run security-scan.yml --ref main \
209208
-F fail-on-static-findings=false
210209
```
211210

212-
To integrate it into a larger workflow, run it after the job that pushes the
213-
candidate tag and publishes the artifacts. This example assumes an existing
211+
To integrate it into a larger workflow, run it after the job that creates the
212+
candidate tag and stages source-addressed artifacts for scanning. Release-facing
213+
publication follows successful qualification. This example assumes an existing
214214
`build` job with outputs named `candidate_tag`, `gateway_image`, `sandbox_image`,
215215
and `chart_ref`; adapt those names to your workflow:
216216

@@ -241,8 +241,8 @@ jobs:
241241
Set `needs: security` on a downstream promotion job to require successful scans.
242242
The tagged release workflow records protobuf, security, and integration outcomes in a
243243
qualification job after publishing its commit-addressed OCI images. A failed
244-
check remains visible in the workflow, but pre-release artifact assembly and
245-
publication continue. Stable publication currently requires the implemented
244+
check remains visible in the workflow, with build artifacts and evidence retained
245+
in Actions storage. Both pre-release and stable publication require the implemented
246246
`release-tag-v1` profile to pass; that profile is an incremental subset of RFC
247247
0014 qualification.
248248

@@ -498,7 +498,9 @@ These workflows run after merge to publish dev/tagged artifacts and verify them.
498498
| File | Role |
499499
|---|---|
500500
| `.github/workflows/release-dev.yml` | Publishes the rolling `dev` build on every push to `main`. Builds gateway, sandbox, and supervisor images and binaries, packages, wheels, and pushes the Helm chart as `oci://ghcr.io/nvidia/openshell/helm-chart:0.0.0-dev` (plus an immutable `0.0.0-dev.<sha>` pin). Also dispatchable manually. |
501-
| `.github/workflows/release-tag.yml` | Publishes tagged stable releases and manually dispatched pre-releases. Its automatic tag trigger excludes `-pre.*`. Protobuf, security, and integration failures do not block pre-release artifact publication. Stable publication requires the currently implemented qualification profile to pass; the summary identifies the remaining RFC 0014 coverage. |
501+
| `.github/workflows/release-tag.yml` | Publishes tagged stable releases and manually dispatched pre-releases. Its automatic tag trigger excludes `-pre.*`. Both require the currently implemented qualification profile to pass before publication; the summary identifies the remaining RFC 0014 coverage. Failed candidates retain build artifacts and evidence in Actions storage. Source-SHA OCI images remain available as qualification inputs. |
502+
| `.github/workflows/snap-package.yml` | Builds Snap and component artifacts for Release Dev and Release Tag without Store credentials or publication. |
503+
| `.github/workflows/snap-publish.yml` | Uploads existing Snap and component artifacts to the Store without rebuilding. Release Dev calls it directly for `latest/edge` after Snap builds and requires it to succeed before creating the dev release. Release Tag calls it for `latest/stable` only for stable releases, after qualification and release assembly succeed. |
502504
| `.github/workflows/release-canary.yml` | Smoke-tests published dev artifacts in the `macos`, `ubuntu-deb`, `ubuntu-snap-system-docker`, `fedora`, and `kubernetes` (kind + Helm) jobs. Each job reaches its gateway and creates, exercises, and deletes a sandbox. The Snap lanes verify a compatible system Docker lifecycle and `ubuntu-snap-docker-preflight` tests fail-fast behavior when Docker is absent or supplied by the Docker snap. The positive Snap lane also runs a local policy containment check with the packaged prover. It runs automatically after `Release Dev` succeeds and supports manual dispatch (`gh workflow run release-canary.yml --ref <branch>`). See the `test-release-canary` skill for the playbook and local kind reproduction. |
503505

504506
## Required status contexts

‎docs/about/support-matrix.mdx‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,12 @@ the release cycle. Use a stable release for production deployments.
1919
| Pre-release | Built nightly when `main` has changed and normal CI passes. Versions use the form `X.Y.Z-pre.N`. | Validating the immutable artifact set proposed for the next stable release. Pre-releases use the stable feature set but may not have passed qualification. |
2020
| Stable | Promoted from a pre-release that passes conformance, upgrade, API compatibility, artifact, and security checks. Versions use the form `X.Y.Z`. | Production use within this support matrix. |
2121

22+
The tagged release pipeline publishes pre-release and stable artifacts only
23+
after its current qualification profile passes. Failed candidates retain build
24+
artifacts and qualification evidence in CI storage. The current profile covers
25+
part of the complete release qualification policy; passing it does not establish
26+
full RFC 0014 coverage.
27+
2228
Tagged stable releases generally go out every week. OpenShell targets Tuesday
2329
publication when there are changes and every blocking qualification check
2430
passes. A security release may ship sooner.

0 commit comments

Comments
 (0)