Release SBOM and audit gate¶
The version-tag Release workflow requires Release audit before creating GitHub release assets or publishing the Python package. Image builders publish first so the audit can read back exact registry content. An audit failure blocks the remaining publication; it does not remove already pushed images.
Image inventory¶
Each image ships linux/amd64 and linux/arm64. BuildKit generates an SPDX 2.3
inventory from each final filesystem and attaches it to the platform manifest.
The generator is pinned by digest in the workflows.
| Image | Workflow | Version tag |
|---|---|---|
ghcr.io/preloop/preloop |
Release | version without v |
ghcr.io/preloop/console |
Release | version without v |
ghcr.io/preloop/preloop-codex-scanner |
Publish Scanner Image | full v… tag |
ghcr.io/preloop/preloop-codex-universal |
Publish Universal Image | full v… tag |
When enabled, Docker Hub backend/console mirror inventories are also read back
and compared with GHCR. Moving aliases such as latest are not additional image
variants. Runtime plugins publish npm/PyPI packages; their test Dockerfiles are
not release images. GitLab development builds and Enterprise Edition images are
outside this GitHub version-tag inventory.
Scanner builds use native runners per architecture because inventory generation unpacks the Codex base. Universal's amd64 export retains its build cache until SBOM export and checks the published image config against the config that passed its offline smoke. Final scanner/universal indexes merge platform indexes with their attestations.
The three existing CycloneDX SBOMs remain: installed Python dependencies, the npm
build tree, and the linux/amd64 Go build graph. They complement container
inventories: the console image contains nginx and bundled JavaScript rather than
its original node_modules tree.
Gate and evidence¶
scripts/image_sbom.py requires both image architectures and attached
attestations, checks SPDX identity, a nonempty package inventory and its
relationship graph, then exports per-platform files with content hashes and
index/platform digests. This validates required SPDX fields, not every optional
field. It does not invent supplier or license metadata.
scripts/release_audit.py collect waits for the latest push run for the same
tag and source commit of CI, CodeQL, Secret Scan, Workflow Audit, Publish
Scanner Image and Publish Universal Image. Main-branch success cannot substitute
for tag evidence. Failed, skipped, cancelled and timed-out runs fail the gate;
missing/pending runs have a bounded wait. CodeQL must also have no open
code-scanning alerts on that tag. API and download failures fail the job.
validate requires all four families, eight platform inventories, matching
hashes and commit/tag identities, and all three component SBOMs. It re-measures
component minimum elements using the generator job's existing implementation.
The release job also verifies version tags still resolve to recorded digests and
compares the assets with registry-attached SBOMs, including configured mirrors.
With --verify-provenance it runs gh attestation verify on every image digest
and requires a Sigstore build provenance attestation signed by the workflow that
builds that image (Release for backend/console, Publish Scanner Image, Publish
Universal Image) for the tag's ref and commit. Each of those workflows attests
its image digest and pushes the attestation to GHCR.
The existing component generator's schema validation remains mandatory.
This gate checks existing security workflows and findings; it does not invoke a hosted Release Security Audit flow. An inventory alone is not a vulnerability clearance. No severity exclusions, waivers or VEX suppression are added here.
For a version such as 1.2.3, published evidence includes:
preloop-sbom-{backend,frontend,cli}-1.2.3.cdx.jsonpreloop-sbom-image-{backend,frontend,scanner,universal}-linux-{amd64,arm64}-1.2.3.spdx.jsonpreloop-image-{backend,frontend,scanner,universal}-1.2.3.json: commit, tag, image digests, mirror references and SBOM hashesrelease-audit-runs.json: exact workflow run IDs, attempts and conclusionsrelease-audit.json: validated inventory, required images and verification modes
These enter the existing SHA256SUMS and signed provenance bundle; see
release verification. Asset preparation validates the
downloaded inventory again before hashing it. The release CLI treats missing
image/audit assets as incomplete publication.
Nonpublishing validation¶
The Release validation (nonpublishing) workflow runs regression tests and a small real two-platform OCI build on relevant PRs, or by manual dispatch on a branch. It has read-only permissions, no registry login and no push commands. Repeat locally from the repository root:
export PRELOOP_DISABLE_TELEMETRY=true
work_dir="$(mktemp -d)"
python3 -m unittest discover -s backend/tests/scripts -p test_image_sbom.py
python3 -m unittest discover -s backend/tests/scripts -p test_release_audit.py
docker buildx build --platform linux/amd64,linux/arm64 --provenance=false \
--sbom=generator=docker/buildkit-syft-scanner@sha256:ae4f3b554449e7e25548e7d8ccc029d17357348e30c6e3df01b92bc93654d6a9 \
--output "type=oci,dest=$work_dir/image.tar" scripts/tests/sbom_fixture
python3 scripts/image_sbom.py --name backend --oci-archive "$work_dir/image.tar" \
--tag v0.0.0-fixture --source-sha "$(git rev-parse HEAD)" \
--output "$work_dir/evidence"
The synthetic version creates no git tag. OCI inspection verifies blob hashes and never extracts archive paths, loads images, or pushes them. This fixture does not prove production builds or registry publication succeeded.
After downloading a real release's evidence into release-evidence/, validation
remains read-only:
python3 scripts/release_audit.py validate --directory release-evidence \
--tag "$RELEASE_TAG" --source-sha "$RELEASE_SHA" \
--verify-registry --require-workflows --verify-provenance
Release rehearsal¶
The Release workflow itself runs as a rehearsal on pull requests that change
release.yml, scripts/image_sbom.py or scripts/release_audit.py, and on a
manual run with a rehearsal_tag input (default v1.0.0). A rehearsal builds
the backend and console images for both platforms to local OCI archives with
the release SBOM generator, exports their SBOMs under the rehearsal tag name,
generates the component SBOMs and runs release_audit.py validate --images
backend,frontend offline. It logs in to no registry, pushes, attests and
publishes nothing, and every other job is skipped; a manual run on a tag ref
skips them too. Scanner and universal images come from their own workflows on
a tag, so the rehearsal does not require them. A run with --images never
prints a passing tag audit.
First gated release¶
No published tag carries image SBOM evidence yet, so CI cannot exercise the
remote paths (collect, the tag code-scanning query and --verify-registry)
before the first tag that runs this workflow. Push a release candidate tag
such as v1.0.0-rc.1 first and treat its Release audit job as the first
remote execution. Expected evidence on that run:
collectlogs noWaiting forline after the tag workflows complete and writesrelease-audit-runs.jsonwith one successful push run per workflow for the tag and commit.- The tag has a code-scanning analysis for
refs/tags/<tag>at that commit. Without one the job fails withNo code-scanning analysis recorded; it never reads a missing analysis as zero findings. release-audit.jsonreportsregistry_verified: true,tag_workflows_verified: trueandprovenance_verified: true, and the log ends withRelease SBOM audit passed.
Any other outcome blocks publication of that candidate; fix it before the
final tag. To repeat the remote checks for an existing tag without the release
workflow, dispatch Release validation (nonpublishing) with the tag input.
Its read-only remote audit job downloads that tag's release-run artifacts and
runs collect and validate --verify-registry --require-workflows
--verify-provenance.
Offline validation omits those three flags and reports the verification modes
as false when --report is requested. Without all three flags and the full
image set, the command prints that it is not a passing tag audit.
The final 1.0.0 tag check in #1870 remains pending until that tag is released
and its actual Release audit job passes.