From 8bcf1e1e7334a0e2b7224ec62148f16a80befa23 Mon Sep 17 00:00:00 2001 From: Matthew Grossman Date: Tue, 27 Oct 2026 16:44:18 -0700 Subject: [PATCH] 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 --- .github/workflows/snap-publish.yml | 52 +++++++++++++++++++++++++++++ .github/workflows/wheel-publish.yml | 2 +- CI.md | 2 +- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/.github/workflows/snap-publish.yml b/.github/workflows/snap-publish.yml index a7ba1a5ad..2bb81331c 100644 --- a/.github/workflows/snap-publish.yml +++ b/.github/workflows/snap-publish.yml @@ -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: diff --git a/.github/workflows/wheel-publish.yml b/.github/workflows/wheel-publish.yml index 451c6ed46..d87c5ad76 100644 --- a/.github/workflows/wheel-publish.yml +++ b/.github/workflows/wheel-publish.yml @@ -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 diff --git a/CI.md b/CI.md index cf251e0bb..11b06a342 100644 --- a/CI.md +++ b/CI.md @@ -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. |