ci(release): verify the provenance of dispatched snap artifacts

A manual snap upload now requires artifact-run-id to be this repository's
publishing workflow for the channel, and stable snaps must match the GitHub
Release asset digests. A skipped stable upload now reports a notice.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
This commit is contained in:
Matthew Grossman
2026-10-27 16:44:18 -07:00
parent 48fc588c3e
commit 8bcf1e1e73
3 changed files with 54 additions and 2 deletions
+52
View File
@@ -86,6 +86,52 @@ jobs:
run-id: ${{ inputs.artifact-run-id || github.run_id }}
github-token: ${{ inputs.artifact-run-id && github.token || '' }}
# Artifacts from another run must come from this repository's publishing
# workflow for the channel, and stable snaps must match the GitHub Release.
- name: Verify artifact provenance
if: inputs.artifact-run-id != ''
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
env:
RUN_ID: ${{ inputs.artifact-run-id }}
UPLOAD_CHANNEL: ${{ inputs.upload-channel }}
RELEASE_TAG: ${{ inputs.release-tag }}
with:
script: |
const fs = require('fs');
const crypto = require('crypto');
const { RUN_ID, UPLOAD_CHANNEL, RELEASE_TAG } = process.env;
const workflows = {
'latest/stable': '.github/workflows/release.yml',
'latest/beta': '.github/workflows/release-prerelease.yml',
'latest/edge': '.github/workflows/release-dev.yml',
};
const repo = `${context.repo.owner}/${context.repo.repo}`;
const { data: run } = await github.rest.actions.getWorkflowRun({
...context.repo,
run_id: Number(RUN_ID),
});
if (run.path !== workflows[UPLOAD_CHANNEL]) {
core.setFailed(`Run ${RUN_ID} is ${run.path}, not ${workflows[UPLOAD_CHANNEL]}`);
return;
}
if (run.head_repository?.full_name !== repo) {
core.setFailed(`Run ${RUN_ID} is from ${run.head_repository?.full_name}, not ${repo}`);
return;
}
if (UPLOAD_CHANNEL !== 'latest/stable') return;
const { data: release } = await github.rest.repos.getReleaseByTag({
...context.repo,
tag: RELEASE_TAG,
});
for (const file of fs.readdirSync('.').filter(name => name.endsWith('.snap'))) {
const digest = `sha256:${crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex')}`;
const asset = release.assets.find(asset => asset.name === file);
if (asset?.digest !== digest) {
core.setFailed(`${file} does not match the ${RELEASE_TAG} release asset`);
return;
}
}
- name: Install snapcraft
run: |
set -euo pipefail
@@ -103,6 +149,12 @@ jobs:
with:
release-tag: ${{ inputs.release-tag }}
- name: Report skipped upload
if: ${{ inputs.release-tag != '' && steps.latest.outputs.make-latest != 'true' }}
env:
INPUTS_RELEASE_TAG: ${{ inputs.release-tag }}
run: echo "::notice::Snap upload skipped; ${INPUTS_RELEASE_TAG} is no longer the newest release."
- name: Upload snap to Snap Store
if: ${{ inputs.release-tag == '' || steps.latest.outputs.make-latest == 'true' }}
env:
+1 -1
View File
@@ -97,7 +97,7 @@ jobs:
run: |
set -euo pipefail
if [ -z "${WHEEL_FILENAMES}" ]; then
echo "No wheel filenames provided by build job" >&2
echo "No wheel filenames provided" >&2
exit 1
fi
+1 -1
View File
@@ -718,7 +718,7 @@ These workflows run after merge to publish dev/tagged artifacts and verify them.
| `.github/workflows/release-notes.yml` | Shared prompt-driven release notes generation through Inference Hub, used by prerelease previews, notes-only previews, and stable publication when no preview is reused. |
| `.github/workflows/release-qualification.yml` | Reusable qualification suites and result aggregation. Runs security scans, conformance and feature integration, Docker and VM E2E, protobuf compatibility, and advisory compatibility review. |
| `.github/workflows/snap-package.yml` | Builds snap and component artifacts for Release Dev, Release, and Pre-release without Store credentials or publication. |
| `.github/workflows/snap-publish.yml` | Uploads existing snap and component artifacts directly to a Store channel without rebuilding. Release Dev uses `latest/edge`, Pre-release uses `latest/beta`, and Release uses `latest/stable` for the stable rebuild. Uploads queue per channel and architecture, so tagged beta and stable jobs recheck repository tags after earlier publication finishes and before updating their floating channels. Manual dispatch uploads the `snap-linux-*` artifacts from `artifact-run-id`. |
| `.github/workflows/snap-publish.yml` | Uploads existing snap and component artifacts directly to a Store channel without rebuilding. Release Dev uses `latest/edge`, Pre-release uses `latest/beta`, and Release uses `latest/stable` for the stable rebuild. Uploads queue per channel and architecture, so tagged beta and stable jobs recheck repository tags after earlier publication finishes and before updating their floating channels. Manual dispatch uploads the `snap-linux-*` artifacts from `artifact-run-id`, which must be that channel's publishing workflow in this repository and still have its artifacts (kept 5 days). Stable snaps must match the GitHub Release assets. |
| `.github/workflows/sdk-typescript-publish.yml` | Publishes the TypeScript SDK to GitHub Packages. Called by stable Release; manual dispatch publishes a given tag and npm version. |
| `.github/workflows/sdk-go-tag.yml` | Creates the `sdk/go/vX.Y.Z` tag and asks the Go module proxy to fetch it. Called by stable Release; manual dispatch tags a given version and commit. |
| `.github/workflows/wheel-publish.yml` | Triggers the GitLab pipeline that publishes wheels from a GitHub Release to PyPI. Called by stable Release; manual dispatch republishes a given tag. |