Repository navigation
feat: gate stable promotion on security qualification instead of pre-release publication #3559
Description
User Story
As a maintainer running the weekly release train, I want security scan findings
to gate promotion to stable rather than building and publishing a
pre-release, so that a static-analysis backlog cannot stop the release
pipeline from producing artifacts to qualify.
Problem Statement
release-tag.yml calls the aggregate Security Scan as a precondition for
publication. Any blocking finding from Codex Security, CodeQL, Trivy, Zizmor, or
Cargo Deny fails the workflow before artifacts are published.
RFC 0014 specifies the opposite ordering. In
rfc/0014-release-stability/README.md (Pre-release qualification and stable
publication), every pre-release is tagged and built first, and the four
qualification suites — including Security review, which "verifies security
scan results, reviews changes to security-sensitive boundaries, and confirms
that every finding has the required disposition" — run against the published
artifacts. Qualification determines eligibility for stable promotion; it is not
a precondition for the pre-release existing.
The current wiring conflates the two. A finding backlog unrelated to the
candidate commit stops artifact production entirely.
Impact / Why This Matters
Observed on release-tag run 35725722342 (2026-09-22): the nightly failed with
8 HIGH/CRITICAL CodeQL findings, 27 HIGH Zizmor findings, 1 HIGH Trivy finding,
and one Cargo Deny advisory (RUSTSEC-2026-0295). No pre-release artifacts were
produced, so nothing was available to qualify, and the release train stalled.
The current workaround is the temporary fail-on-static-findings: false
override added in #3552, which makes CodeQL, Trivy, and Zizmor informational on
the release path. That unblocks the train but removes the signal entirely rather
than relocating it — there is no gate anywhere that stops a stable promotion on
those findings, and the override is documented only in prose in CI.md and
architecture/build.md.
The workaround is insufficient because it trades a false blocker for no blocker.
RFC 0014's model keeps the signal and puts it where it belongs.
Proposed Design
User-facing workflow
- A pre-release tag always builds and publishes its artifacts. Scanner
execution errors still fail the build, because a scan that did not run
produces no evidence. - Security scans run as a qualification suite against the published
pre-release, alongside conformance, upgrade, and breaking-API review. - The suite's result — per-scanner counts and the disposition of each finding —
is recorded in the pre-release manifest described in RFC 0014. - Stable promotion requires the security suite to pass. A pre-release with
undisposed blocking findings is ineligible for promotion and is reported as
such, rather than silently skipped. - Maintainers dispose of findings (fix, accept with justification, or mark
false positive) against a pre-release, and that disposition is what the
promotion gate reads.
Externally observable behavior
- A finding backlog delays stable promotion; it never prevents a pre-release
from being built or published. - The reason a pre-release was not promoted is visible in its manifest and
qualification result, not only in a failed workflow log. fail-on-codex-findingsandfail-on-static-findingsonsecurity-scan.yml
are no longer needed on the release path once the qualification gate exists,
and the temporary override inrelease-tag.ymlis removed.
Internal implementation — where the suite runs, how dispositions are stored, and
the manifest schema — is deliberately left open.
Acceptance Criteria
- Pre-release tags build and publish artifacts regardless of scanner
findings; only scanner execution failures block the build. - Security scan results are attached to the pre-release manifest with
per-scanner finding counts. - Stable promotion is blocked when the security qualification suite has
undisposed blocking findings, and the block is reported with the specific
findings responsible. - The temporary
fail-on-static-findings: falseoverride is removed from
release-tag.yml. -
CI.md,architecture/build.md, and the RFC 0014 release qualification
supplement describe the enforcement point consistently. - A pre-release with a known backlog is demonstrated publishing successfully
and being correctly withheld from stable promotion.
Alternatives Considered
- Baseline/delta enforcement inside
release-tag.yml. Keep publication
gated, but only fail on findings new since the previous stable tag. This is
the removal condition currently documented for the ci(security): stage release finding enforcement #3552 override. It fixes
the immediate backlog problem but keeps enforcement in the publication path,
so it still contradicts RFC 0014 and still lets a single new finding block
artifact production. - Keep blocking and burn down the backlog. Triage the 8 CodeQL, 27 Zizmor,
and 1 Trivy findings to zero, then restorefail-on-static-findings: true.
This does not change the structural problem: the next backlog stalls the train
again, and it couples release cadence to triage capacity. - Leave the override in place indefinitely. Rejected — it removes the signal
with no compensating gate before stable promotion.
Agent Investigation
Related material:
- RFC 0014 —
rfc/0014-release-stability/README.md,
rfc/0014-release-stability/release-qualification.md - ci(security): stage release finding enforcement #3552 — introduced the temporary observation mode this issue supersedes
Activity
purp commentedon Sep 23, 2026
RFC 0014 Release Qualification: Design
Status: draft for review. Tracks #3559. Related: #3632 (promotion vs. rebuild).
Goal
Move release enforcement out of pre-release publication and into a
qualification stage that gates stable promotion, as RFC 0014 specifies. Build
the substrate (artifact manifest, durable qualification results, promotion
gate) with security and conformance as the first implemented suites. Upgrade
and breaking-API review are declared but not yet implemented.
Scope
In scope: the OpenShell GitHub repository's release workflows, the pre-release
artifact store, qualification results, security finding dispositions, and
stable promotion.
Out of scope:
Python wheel after it's built.
are peeled off as individual fixes once validated and agreed by maintainers.
Current state
PR #3593 (merged 2026-09-23) is the first increment of this design:
Release Qualificationjob inrelease-tag.ymlaggregates security,conformance integration, feature integration, Docker E2E, and VM E2E results
into the
release-tag-v1profile.that profile passing. Stable publication still does, in the same run.
Observed: this doesn't work yet. In the
v0.1.0-pre.10run(35888840979),
Release Qualificationand every directneedsofReleasesucceeded, but
Releasewas skipped, along with image tagging, Helm, and thequalification summary publish. GitHub skips a job with the default
success()condition when any ancestor job failed, and the suite jobs are ancestors
through
qualification-result.Releaseand its downstream jobs need anexplicit condition such as
!cancelled()plus success of their direct needs.qualification-summary.json(schema v1) to the runsummary, a 90-day Actions artifact, and
ghcr.io/nvidia/openshell/qualification:<version>-run-<run-id>-attempt-<attempt>.rfc_0014_complete: falseand lists the missing suites:upgrade, breaking API change review, and the remaining conformance
configurations.
(
fail-on-static-findings: true).Consequence: with strict reporting restored, any undispositioned High/Critical
finding fails the profile and blocks stable. In
v0.1.0-pre.10, Trivy, CodexSecurity, Zizmor High, and CodeQL (rust) failed, as did Docker E2E (python).
Still missing relative to RFC 0014: an artifact manifest and durable artifact
store, per-suite results, finding dispositions, promotion of the newest
eligible pre-release, qualification decoupled from the build, and the upgrade
and breaking-API suites.
Section 1: Artifact storage and manifest
Each pre-release pushes one immutable OCI artifact,
ghcr.io/nvidia/openshell/release-assets:<version>, holding the file set astable GitHub Release would carry (tarballs, deb, rpm, snap, wheel, checksum
files, Homebrew formula) plus
manifest.json.The manifest records:
Cargo.lock,flake.lock,rust-toolchain.toml,mise.lock, and thevm-runtimehash.digests.
cargo auditablemetadata),and references to the image SBOM attestations.
actions/attestoutput.Pre-releases stay off the curated GitHub Releases page; the OCI store is their
index, following the Bazel rolling-release intent. Fallback, if GHCR storage
proves unworkable: the existing 90-day Actions artifact bundles, which
install.sh'sOPENSHELL_VERSION=prepath already consumes. (A GitHub Releasewith
prerelease: truewas the earlier fallback; it's dropped because it putspre-releases on the curated Releases page.)
Unchanged: the five 90-day pre-release Actions bundles and the
install.shprepath that reads them.Rebuild vs. promotion
Stable rebuilds the qualified commit rather than promoting the qualified bytes.
This is undesirable: each rebuild is another opportunity for a supply-chain
compromise to enter artifacts. We accept it for now in the name of progress.
Every artifact embeds its version (binaries, default supervisor image tag,
images, deb, rpm, wheel, snap, Helm chart), and RFC 0014 keeps versions out of
source, so the stable version can't exist in any artifact until after
qualification. #3632 decides promotion vs. rebuild. If it chooses promotion,
that's a separate project.
Section 2: Promotion gate
Suite declarations
A repository config file defines the qualification profile: every suite, and
every conformance configuration, is either implemented or declared
not-implemented. It replaces the hard-coded
missing_suiteslist ingenerate-qualification-summary.shand versions the profile name(
release-tag-v1,-v2, ...).pass.fail,error, or a missing result blocks.not-implemented; advisory only. It appearsin the promotion summary and never blocks.
not-implementedis treated aserror. Thisfollows the RFC rule that undeclared skips fail qualification.
Initial profile: security, conformance integration, feature integration, Docker
E2E, and VM E2E implemented (the #3593 set); upgrade, breaking-API review, and
the remaining conformance configurations declared not-implemented.
release-promote.ymlTriggers: Tuesday schedule, and manual dispatch with an optional pre-release
version and a dry-run flag. Manual dispatch covers out-of-band patch or minor
releases on any day (for example, an urgent security fix); it still requires
eligibility.
Train: the newest
vX.Y.Z-pre.Nbase with no stable tag. Choosing a patch orminor train stays a manual maintainer decision; a started train never switches.
Nothing new to release: if no
-pre.Ncandidate has commits beyond the lateststable tag (for example, an out-of-band release already shipped this train's
newest candidate), the run skips. It succeeds with a summary saying so, and
publishes nothing. This keeps the weekly cron from re-releasing an out-of-band
release.
Selection: walk
-pre.Nnewest to oldest; a candidate is eligible when thelatest result for every suite implemented in the current profile is
pass.Promote the first eligible candidate. If none qualifies, no release: the run
fails, and its summary lists each candidate and its blocking suites.
Suites added since a candidate was qualified: each result records the profile
version it ran under. When the current profile implements a suite that a
candidate has no result for, promotion runs that suite against the candidate's
stored artifacts via
release-qualify.ymlbefore deciding eligibility, ratherthan failing on the missing result. Under today's rebuild model the final
checks also cover this; under promotion (#3632) this is the only point where
new critical tests reach the artifact before it ships.
Promotion:
release-tag.ymlbuild jobs viaworkflow_call, after checking build inputsagainst the manifest.
profile, as
release-tag.ymldoes for stable today, plus--versionand SBOMchecks. This qualifies the bytes that actually ship, which partly offsets the
rebuild risk in Section 1. If chore(release): evaluate promoting qualified pre-release artifacts instead of rebuilding for stable #3632 moves to promotion, prior results on the
promoted artifacts stand, and only suites added since they were qualified run
(see Selection).
vX.Y.Zon the source commit and run the existing publish jobs(GitHub Release, image retag, Helm, snap, docs).
-pre.Ntags.Pushing a stable tag by hand no longer publishes;
release-promote.ymlcreatesthe tag as its last step. Maintenance branches are out of scope.
Section 3: Running qualification
Pre-release publication in
release-tag.ymlstops depending on the suites (intended by#3593; see Current state for the remaining skip).
release-qualify.ymlruns qualification separately from the build:release-tag.ymlrun, or by manualdispatch with a version and optional suite list.
release-assets:<version>by digest and verifies every file againstthe manifest before any suite runs. Suites use stored artifacts and images by
digest, never the build run's workflow artifacts.
move here.
not-implementedresults for declared suites, so everypre-release has a result for every suite.
Results extend #3593's format rather than replacing it: standalone OCI
artifacts in
ghcr.io/nvidia/openshell/qualification, one per suite perattempt, tagged
<version>-<suite>-run-<run-id>-attempt-<attempt>, annotatedwith the source revision, version, and release-assets manifest digest. This
avoids GHCR's missing OCI referrers API (verified: 404 on the referrers
endpoint). Schema v2 records suite, profile version, status (
pass,fail,not-implemented,error), run URL, timestamp, and details. Latest result per suite wins.Results are append-only.
Errors and reruns: infrastructure failures record
error. A manual dispatchfor one suite re-runs it; the new result supersedes the old. Code fixes produce
a new pre-release; reruns are for flaky or broken infrastructure.
What separating qualification from the build enables
error.before declaring it implemented.
has a candidate.
Section 4: Security finding dispositions
RFC 0014: security review passes "when no unresolved critical or high-severity
finding affects a supported configuration."
security-dispositions.yamlis the unified record of risk decisions. Eachentry: finding ID, disposition, reason, approver, and (for accepted risk) an
expiry date.
false-positive: never blocks.not-affected: doesn't touch a supported configuration; never blocks.accepted-risk: non-blocking until expiry; always listed in results and thepromotion summary.
Tool-native files remain because the tools read them. Every accepted risk in a
native file must also appear in
security-dispositions.yaml; the security suitetreats a native suppression without a matching, unexpired entry as
undispositioned. Today that means the four
deny.tomlignores. The.trivyignore.yamlfile holds only false positives by its own rule and staysas is.
Dispositions are read from
mainat qualification time, not from thepre-release's source commit, and the result records the dispositions commit.
Pass when every High/Critical finding has a current disposition; otherwise
fail, listing undispositioned findings. Changes go through PRs and CODEOWNERS.
Rejected: GitHub code-scanning dismissals (SARIF-only, not code-reviewed).
Section 5: Incremental delivery
Each increment lands on its own. Before starting the next, confirm:
mainCIstays green, a dev release succeeds, and a manually dispatched pre-release
publishes its complete artifact set and records qualification. Increments that
touch the stable path also need a stable dry run.
release-tag-v1profile, summary v1 in GHCR, stable gated.
Release,Tag GHCR Images,Release Helm Chart (OCI), andPublish Qualification Summary (OCI)explicit conditions so a failed suite no longer skips pre-releasepublication. Verify with a pre-release whose profile fails.
security-dispositions.yaml, backfill thedeny.tomlaccepted risks, native-file consistency check, and a securityresult that honors dispositions. First because strict reporting now blocks
stable on the existing finding backlog.
declarations out of the script; per-suite result documents; advisory
not-implemented. Still produced insiderelease-tag.yml.path; summaries gain the manifest digest annotation.
release-qualify.ymlin shadow. Runs against stored artifacts alongsidethe in-workflow profile; compare results across several pre-releases.
release-tag.yml;release-qualify.ymlbecomes the source of results.release-promote.ymldry run. Tuesday selection only; compare withmaintainer judgment.
rebuilds, re-runs the profile, tags, publishes, and deletes
-pre.Ntags.Independent follow-ups, each validated and agreed before landing:
Cargo Deny and weekly workflow-security crons exist; Trivy changes run on PRs;
Codex Security and the image Trivy scan run only when called from a release
or dispatched by hand).
main"): deferred;mainCI isn't stable enoughyet. Nightly remains
mainHEAD at tag time.buf breakingis configured inbuf.yamlbut never run).Testing
check live in scripts under
tasks/scripts/with unit tests, followingtest-qualification-summary.shandsync_docs_website_test.py.through
release-promote.ymldry runs.Decisions recorded
when there's nothing newer than the latest stable release (Section 2).
artifacts.
github-actions commentedon Oct 8, 2026
This issue has had no activity for 14 days and is now marked stale. It may be closed in 7 days if there is no further activity. Comment or remove the state:stale label to keep it open.
elezar commentedon Oct 12, 2026
Proposed follow-up for the stable publication gate: use a GitHub Environment with required reviewers to provide an approval path when qualification fails.
Release Readycould require successful artifact builds plus either successful qualification or a successful environment-protected approval job. The approval job would run only for stable candidates that fail qualification; prereleases would continue publishing automatically despite qualification failures.Approval should be recorded as an explicit exception, preserving the failed qualification evidence. For now, the local workflow change keeps stable qualification mandatory and only adds a placeholder for this future approval path.