From d3284948d326ec648d2aaa7f9813a7c20ec88c33 Mon Sep 17 00:00:00 2001 From: Matthew Grossman Date: Tue, 27 Oct 2026 16:15:05 -0700 Subject: [PATCH 1/5] ci(release): allow release publishers to be dispatched individually Move the TypeScript SDK and wheel publishers into reusable workflows and add workflow_dispatch to them and to snap-publish.yml, following sync-docs.yml. Release still calls each one. A maintainer can now publish one missing component for an existing release without rebuilding or rerunning Release. The snap publisher can upload artifacts from an earlier run, and a manual latest/stable upload requires a release tag so it still rechecks the newest stable release. Signed-off-by: Matthew Grossman --- .github/workflows/release-dev.yml | 5 +- .github/workflows/release-publish.yml | 116 ++++--------------- .github/workflows/sdk-typescript-publish.yml | 102 ++++++++++++++++ .github/workflows/snap-publish.yml | 33 +++++- .github/workflows/wheel-publish.yml | 85 ++++++++++++++ CI.md | 4 +- 6 files changed, 247 insertions(+), 98 deletions(-) create mode 100644 .github/workflows/sdk-typescript-publish.yml create mode 100644 .github/workflows/wheel-publish.yml diff --git a/.github/workflows/release-dev.yml b/.github/workflows/release-dev.yml index aec77972ee..cb489c3ee0 100644 --- a/.github/workflows/release-dev.yml +++ b/.github/workflows/release-dev.yml @@ -265,12 +265,15 @@ jobs: publish-snap: name: Publish snap needs: build-snap + permissions: + actions: read + contents: read uses: ./.github/workflows/snap-publish.yml with: upload-channel: latest/edge github-environment: latest/edge secrets: - publish-credentials: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} # --------------------------------------------------------------------------- # Create / update the dev GitHub Release with CLI, gateway, driver, and wheels diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index ba8493f510..d48bb9cc29 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -509,89 +509,45 @@ jobs: name: Publish Snap needs: release if: ${{ !inputs.is-prerelease && inputs.make-latest }} + permissions: + actions: read + contents: read uses: ./.github/workflows/snap-publish.yml with: upload-channel: latest/stable github-environment: latest/stable release-tag: ${{ inputs.release-tag }} secrets: - publish-credentials: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} publish-prerelease-snap: name: Publish Prerelease Snap needs: release if: inputs.is-prerelease + permissions: + actions: read + contents: read uses: ./.github/workflows/snap-publish.yml with: upload-channel: latest/beta release-tag: ${{ inputs.release-tag }} github-environment: latest/beta secrets: - publish-credentials: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} publish-sdk-typescript: name: Publish TypeScript SDK needs: release if: ${{ !inputs.is-prerelease }} - runs-on: linux-amd64-cpu8 - timeout-minutes: 15 permissions: contents: read packages: write - defaults: - run: - shell: nix shell --inputs-from . nixpkgs#nodejs_24 -c bash -euo pipefail {0} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ inputs.source-sha }} - - - uses: ./.github/actions/setup-nix - - - name: Install TypeScript dependencies - working-directory: ./sdk/typescript - run: npm ci - - - name: Configure npm auth for GitHub Packages - working-directory: ./sdk/typescript - run: | - { - echo "@nvidia:registry=https://npm.pkg.github.com" - # Keep the token reference literal for npm to expand at publish time. - # shellcheck disable=SC2016 - echo '//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}' - } > .npmrc - - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - if: inputs.make-latest - with: - persist-credentials: false - ref: ${{ github.workflow_sha }} - path: release-workflow - sparse-checkout: .github/actions/check-latest-release - - - name: Check current latest release - id: latest - if: inputs.make-latest - uses: ./release-workflow/.github/actions/check-latest-release - with: - release-tag: ${{ inputs.release-tag }} - - - name: Publish - working-directory: ./sdk/typescript - env: - OPENSHELL_NPM_VERSION: ${{ inputs.npm-version }} - MAKE_LATEST: ${{ steps.latest.outputs.make-latest }} - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -euo pipefail - npm pkg set version="${OPENSHELL_NPM_VERSION}" - dist_tag=latest - if [[ "${MAKE_LATEST}" != "true" ]]; then - dist_tag="release-${OPENSHELL_NPM_VERSION%.*}" - fi - npm publish --tag "${dist_tag}" + uses: ./.github/workflows/sdk-typescript-publish.yml + with: + release-tag: ${{ inputs.release-tag }} + source-ref: ${{ inputs.source-sha }} + npm-version: ${{ inputs.npm-version }} + make-latest: ${{ inputs.make-latest }} # Go resolves submodule versions from directory-prefixed tags, so the SDK at # sdk/go needs its own sdk/go/vX.Y.Z tag to show up as a release on pkg.go.dev. @@ -629,41 +585,15 @@ jobs: name: Trigger Wheel Publish needs: release if: ${{ !inputs.is-prerelease }} - runs-on: [self-hosted, nv] - timeout-minutes: 10 - steps: - - name: Trigger GitLab CI - env: - GITLAB_CI_TRIGGER_TOKEN: ${{ secrets.GITLAB_CI_TRIGGER_TOKEN }} - GITLAB_CI_TRIGGER_URL: ${{ secrets.GITLAB_CI_TRIGGER_URL }} - COMMIT_SHA: ${{ inputs.source-sha }} - RELEASE_VERSION: ${{ inputs.python-version }} - RELEASE_TAG: ${{ env.RELEASE_TAG }} - WHEEL_FILENAMES: ${{ needs.release.outputs.wheel_filenames }} - run: | - set -euo pipefail - if [ -z "${WHEEL_FILENAMES}" ]; then - echo "No wheel filenames provided by build job" >&2 - exit 1 - fi - - response=$(curl -X POST \ - --fail \ - --silent \ - --show-error \ - -F "token=${GITLAB_CI_TRIGGER_TOKEN}" \ - -F "ref=main" \ - -F "variables[PIPELINE_ACTION]=publish_wheels" \ - -F "variables[GITHUB_REPOSITORY]=${GITHUB_REPOSITORY}" \ - -F "variables[COMMIT_SHA]=${COMMIT_SHA}" \ - -F "variables[RELEASE_TAG]=${RELEASE_TAG}" \ - -F "variables[RELEASE_VERSION]=${RELEASE_VERSION}" \ - -F "variables[RELEASE_KIND]=stable" \ - -F "variables[WHEEL_FILENAMES]=${WHEEL_FILENAMES}" \ - "${GITLAB_CI_TRIGGER_URL}") - pipeline_id=$(printf '%s' "$response" | sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p') - pipeline_status=$(printf '%s' "$response" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p') - echo "Triggered GitLab pipeline ${pipeline_id:-unknown} with status=${pipeline_status:-unknown}" + uses: ./.github/workflows/wheel-publish.yml + with: + release-tag: ${{ inputs.release-tag }} + source-sha: ${{ inputs.source-sha }} + python-version: ${{ inputs.python-version }} + wheel-filenames: ${{ needs.release.outputs.wheel_filenames }} + secrets: + GITLAB_CI_TRIGGER_TOKEN: ${{ secrets.GITLAB_CI_TRIGGER_TOKEN }} + GITLAB_CI_TRIGGER_URL: ${{ secrets.GITLAB_CI_TRIGGER_URL }} publish-fern-docs: name: Sync and Publish Fern Docs diff --git a/.github/workflows/sdk-typescript-publish.yml b/.github/workflows/sdk-typescript-publish.yml new file mode 100644 index 0000000000..a10ff0d289 --- /dev/null +++ b/.github/workflows/sdk-typescript-publish.yml @@ -0,0 +1,102 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Publish TypeScript SDK + +on: + workflow_call: + inputs: + release-tag: + required: true + type: string + source-ref: + description: Commit to build the package from + required: true + type: string + npm-version: + required: true + type: string + make-latest: + description: Whether this release may update the npm latest dist-tag + required: false + default: false + type: boolean + workflow_dispatch: + inputs: + release-tag: + description: Stable tag to publish, e.g. v0.1.6 + required: true + type: string + npm-version: + description: npm package version, e.g. 0.1.6 + required: true + type: string + make-latest: + description: Update the latest dist-tag if this is still the newest stable release + required: false + default: true + type: boolean + +permissions: + contents: read + packages: write + +jobs: + publish: + name: Publish TypeScript SDK + runs-on: linux-amd64-cpu8 + timeout-minutes: 15 + defaults: + run: + shell: nix shell --inputs-from . nixpkgs#nodejs_24 -c bash -euo pipefail {0} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ inputs.source-ref || inputs.release-tag }} + + - uses: ./.github/actions/setup-nix + + - name: Install TypeScript dependencies + working-directory: ./sdk/typescript + run: npm ci + + - name: Configure npm auth for GitHub Packages + working-directory: ./sdk/typescript + run: | + { + echo "@nvidia:registry=https://npm.pkg.github.com" + # Keep the token reference literal for npm to expand at publish time. + # shellcheck disable=SC2016 + echo '//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}' + } > .npmrc + + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + if: inputs.make-latest + with: + persist-credentials: false + ref: ${{ github.workflow_sha }} + path: release-workflow + sparse-checkout: .github/actions/check-latest-release + + - name: Check current latest release + id: latest + if: inputs.make-latest + uses: ./release-workflow/.github/actions/check-latest-release + with: + release-tag: ${{ inputs.release-tag }} + + - name: Publish + working-directory: ./sdk/typescript + env: + OPENSHELL_NPM_VERSION: ${{ inputs.npm-version }} + MAKE_LATEST: ${{ steps.latest.outputs.make-latest }} + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + set -euo pipefail + npm pkg set version="${OPENSHELL_NPM_VERSION}" + dist_tag=latest + if [[ "${MAKE_LATEST}" != "true" ]]; then + dist_tag="release-${OPENSHELL_NPM_VERSION%.*}" + fi + npm publish --tag "${dist_tag}" diff --git a/.github/workflows/snap-publish.yml b/.github/workflows/snap-publish.yml index 670522fd34..b15616fb1c 100644 --- a/.github/workflows/snap-publish.yml +++ b/.github/workflows/snap-publish.yml @@ -18,10 +18,30 @@ on: type: string default: "" secrets: - publish-credentials: + SNAPCRAFT_STORE_CREDENTIALS: required: true + workflow_dispatch: + inputs: + upload-channel: + description: Snap Store channel, also used as the GitHub environment + required: true + type: choice + options: + - latest/stable + - latest/beta + - latest/edge + release-tag: + description: Stable tag to recheck before updating the stable channel; required for latest/stable + required: false + type: string + default: "" + artifact-run-id: + description: Run whose snap-linux-* artifacts are uploaded + required: true + type: string permissions: + actions: read contents: read defaults: @@ -40,7 +60,7 @@ jobs: runner: linux-arm64-cpu8 runs-on: ${{ matrix.runner }} timeout-minutes: 10 - environment: ${{ inputs.github-environment }} + environment: ${{ inputs.github-environment || inputs.upload-channel }} concurrency: group: snap-publish-${{ inputs.upload-channel }}-${{ matrix.arch }} cancel-in-progress: false @@ -58,6 +78,8 @@ jobs: uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: name: snap-linux-${{ matrix.arch }} + run-id: ${{ inputs.artifact-run-id || github.run_id }} + github-token: ${{ inputs.artifact-run-id && github.token || '' }} - name: Install snapcraft run: | @@ -79,10 +101,15 @@ jobs: - name: Upload snap to Snap Store if: ${{ inputs.release-tag == '' || steps.latest.outputs.make-latest == 'true' }} env: - SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.publish-credentials }} + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} INPUTS_UPLOAD_CHANNEL: ${{ inputs.upload-channel }} + INPUTS_RELEASE_TAG: ${{ inputs.release-tag }} run: | set -euo pipefail + if [[ "${INPUTS_UPLOAD_CHANNEL}" == latest/stable && -z "${INPUTS_RELEASE_TAG}" ]]; then + echo "latest/stable uploads require release-tag" >&2 + exit 1 + fi shopt -s nullglob snaps=(*.snap) if [[ "${#snaps[@]}" -ne 1 ]]; then diff --git a/.github/workflows/wheel-publish.yml b/.github/workflows/wheel-publish.yml new file mode 100644 index 0000000000..fa2ad83d0e --- /dev/null +++ b/.github/workflows/wheel-publish.yml @@ -0,0 +1,85 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Publish Wheels + +on: + workflow_call: + inputs: + release-tag: + required: true + type: string + source-sha: + required: true + type: string + python-version: + required: true + type: string + wheel-filenames: + required: true + type: string + secrets: + GITLAB_CI_TRIGGER_TOKEN: + required: false + GITLAB_CI_TRIGGER_URL: + required: false + workflow_dispatch: + inputs: + release-tag: + description: Stable tag whose GitHub Release holds the wheels, e.g. v0.1.6 + required: true + type: string + source-sha: + description: Commit the tag points to + required: true + type: string + python-version: + description: Python package version, e.g. 0.1.6 + required: true + type: string + wheel-filenames: + description: Wheel filenames attached to the GitHub Release + required: true + type: string + +permissions: {} + +jobs: + trigger: + name: Trigger Wheel Publish + runs-on: [self-hosted, nv] + timeout-minutes: 10 + steps: + - name: Trigger GitLab CI + shell: bash + env: + GITLAB_CI_TRIGGER_TOKEN: ${{ secrets.GITLAB_CI_TRIGGER_TOKEN }} + GITLAB_CI_TRIGGER_URL: ${{ secrets.GITLAB_CI_TRIGGER_URL }} + COMMIT_SHA: ${{ inputs.source-sha }} + RELEASE_VERSION: ${{ inputs.python-version }} + RELEASE_TAG: ${{ inputs.release-tag }} + WHEEL_FILENAMES: ${{ inputs.wheel-filenames }} + run: | + set -euo pipefail + if [ -z "${WHEEL_FILENAMES}" ]; then + echo "No wheel filenames provided by build job" >&2 + exit 1 + fi + + response=$(curl -X POST \ + --fail \ + --silent \ + --show-error \ + -F "token=${GITLAB_CI_TRIGGER_TOKEN}" \ + -F "ref=main" \ + -F "variables[PIPELINE_ACTION]=publish_wheels" \ + -F "variables[GITHUB_REPOSITORY]=${GITHUB_REPOSITORY}" \ + -F "variables[COMMIT_SHA]=${COMMIT_SHA}" \ + -F "variables[RELEASE_TAG]=${RELEASE_TAG}" \ + -F "variables[RELEASE_VERSION]=${RELEASE_VERSION}" \ + -F "variables[RELEASE_KIND]=stable" \ + -F "variables[WHEEL_FILENAMES]=${WHEEL_FILENAMES}" \ + "${GITLAB_CI_TRIGGER_URL}") + pipeline_id=$(printf '%s' "$response" | sed -n 's/.*"id":\([0-9][0-9]*\).*/\1/p') + pipeline_status=$(printf '%s' "$response" | sed -n 's/.*"status":"\([^"]*\)".*/\1/p') + echo "Triggered GitLab pipeline ${pipeline_id:-unknown} with status=${pipeline_status:-unknown}" diff --git a/CI.md b/CI.md index 5252a4f330..31aef9d70a 100644 --- a/CI.md +++ b/CI.md @@ -718,7 +718,9 @@ 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. | +| `.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/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/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. | | `.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 `ubuntu-snap-system-docker` job verifies a compatible system Docker lifecycle, while `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 `). See the `test-release-canary` skill for the playbook and local kind reproduction. | ## Required status contexts From 930f17763872ab9fc7988bab914a8fa3e16f8fa5 Mon Sep 17 00:00:00 2001 From: Matthew Grossman Date: Tue, 27 Oct 2026 16:21:32 -0700 Subject: [PATCH 2/5] ci(release): allow the Go SDK tag to be dispatched individually Signed-off-by: Matthew Grossman --- .github/workflows/release-publish.yml | 29 ++------------ .github/workflows/sdk-go-tag.yml | 58 +++++++++++++++++++++++++++ CI.md | 1 + 3 files changed, 63 insertions(+), 25 deletions(-) create mode 100644 .github/workflows/sdk-go-tag.yml diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index d48bb9cc29..2dc221851a 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -549,37 +549,16 @@ jobs: npm-version: ${{ inputs.npm-version }} make-latest: ${{ inputs.make-latest }} - # Go resolves submodule versions from directory-prefixed tags, so the SDK at - # sdk/go needs its own sdk/go/vX.Y.Z tag to show up as a release on pkg.go.dev. tag-sdk-go: name: Tag Go SDK needs: release if: ${{ !inputs.is-prerelease }} - runs-on: ubuntu-latest - timeout-minutes: 5 permissions: contents: write - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VERSION: ${{ inputs.semver }} - SOURCE_SHA: ${{ inputs.source-sha }} - steps: - - name: Create Go SDK tag - run: | - set -euo pipefail - tag="sdk/go/v${VERSION}" - # Lightweight tag, so the ref points at the commit directly. - if ! existing=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${tag}" --jq .object.sha 2>/dev/null); then - gh api "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/tags/${tag}" -f sha="${SOURCE_SHA}" - elif [[ "${existing}" != "${SOURCE_SHA}" ]]; then - echo "${tag} already exists at ${existing}, expected ${SOURCE_SHA}" >&2 - exit 1 - fi - - # pkg.go.dev picks up versions the Go module proxy has fetched. - - name: Request version from Go module proxy - continue-on-error: true - run: curl --fail --silent --show-error --max-time 60 "https://proxy.golang.org/github.com/!n!v!i!d!i!a/!open!shell/sdk/go/@v/v${VERSION}.info" + uses: ./.github/workflows/sdk-go-tag.yml + with: + version: ${{ inputs.semver }} + source-sha: ${{ inputs.source-sha }} trigger-wheel-publish: name: Trigger Wheel Publish diff --git a/.github/workflows/sdk-go-tag.yml b/.github/workflows/sdk-go-tag.yml new file mode 100644 index 0000000000..38af6507bd --- /dev/null +++ b/.github/workflows/sdk-go-tag.yml @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Go resolves submodule versions from directory-prefixed tags, so the SDK at +# sdk/go needs its own sdk/go/vX.Y.Z tag to show up as a release on pkg.go.dev. +name: Tag Go SDK + +on: + workflow_call: + inputs: + version: + required: true + type: string + source-sha: + required: true + type: string + workflow_dispatch: + inputs: + version: + description: Release version without the v prefix, e.g. 0.1.6 + required: true + type: string + source-sha: + description: Commit the stable tag points to + required: true + type: string + +permissions: + contents: write + +jobs: + tag: + name: Tag Go SDK + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VERSION: ${{ inputs.version }} + SOURCE_SHA: ${{ inputs.source-sha }} + steps: + - name: Create Go SDK tag + shell: bash + run: | + set -euo pipefail + tag="sdk/go/v${VERSION}" + # Lightweight tag, so the ref points at the commit directly. + if ! existing=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${tag}" --jq .object.sha 2>/dev/null); then + gh api "repos/${GITHUB_REPOSITORY}/git/refs" -f ref="refs/tags/${tag}" -f sha="${SOURCE_SHA}" + elif [[ "${existing}" != "${SOURCE_SHA}" ]]; then + echo "${tag} already exists at ${existing}, expected ${SOURCE_SHA}" >&2 + exit 1 + fi + + # pkg.go.dev picks up versions the Go module proxy has fetched. + - name: Request version from Go module proxy + continue-on-error: true + shell: bash + run: curl --fail --silent --show-error --max-time 60 "https://proxy.golang.org/github.com/!n!v!i!d!i!a/!open!shell/sdk/go/@v/v${VERSION}.info" diff --git a/CI.md b/CI.md index 31aef9d70a..cf251e0bb9 100644 --- a/CI.md +++ b/CI.md @@ -720,6 +720,7 @@ These workflows run after merge to publish dev/tagged artifacts and verify them. | `.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/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. | | `.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 `ubuntu-snap-system-docker` job verifies a compatible system Docker lifecycle, while `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 `). See the `test-release-canary` skill for the playbook and local kind reproduction. | From 48fc588c3eb545e3ca17776ecf26dbdf5d7d118e Mon Sep 17 00:00:00 2001 From: Matthew Grossman Date: Tue, 27 Oct 2026 16:23:25 -0700 Subject: [PATCH 3/5] ci(release): verify dispatched snap and wheel inputs match the release A dispatched snap upload checks that its artifacts were built for the release tag. A wheel publish first confirms the tag points to the given commit and that each wheel is attached to that release at the given version. artifact-run-id is declared on workflow_call so callers do not rely on an undeclared input. Signed-off-by: Matthew Grossman --- .github/workflows/snap-publish.yml | 10 ++++++++ .github/workflows/wheel-publish.yml | 37 ++++++++++++++++++++++++++++- 2 files changed, 46 insertions(+), 1 deletion(-) diff --git a/.github/workflows/snap-publish.yml b/.github/workflows/snap-publish.yml index b15616fb1c..a7ba1a5ad0 100644 --- a/.github/workflows/snap-publish.yml +++ b/.github/workflows/snap-publish.yml @@ -17,6 +17,11 @@ on: required: false type: string default: "" + artifact-run-id: + description: Run whose snap-linux-* artifacts are uploaded; defaults to the current run + required: false + type: string + default: "" secrets: SNAPCRAFT_STORE_CREDENTIALS: required: true @@ -119,6 +124,11 @@ jobs: snap_file="${snaps[0]}" snap_name="${snap_file%.snap}" snap_name="${snap_name%%_*}" + # Artifacts can come from another run, so confirm they were built for this tag. + if [[ -n "${INPUTS_RELEASE_TAG}" && "${snap_file}" != "${snap_name}_${INPUTS_RELEASE_TAG#v}_"* ]]; then + echo "${snap_file} was not built for ${INPUTS_RELEASE_TAG}" >&2 + exit 1 + fi component_args=() for comp in "${snap_name}"+*.comp; do component_file="${comp##*/}" diff --git a/.github/workflows/wheel-publish.yml b/.github/workflows/wheel-publish.yml index fa2ad83d0e..451c6ed46b 100644 --- a/.github/workflows/wheel-publish.yml +++ b/.github/workflows/wheel-publish.yml @@ -42,11 +42,46 @@ on: required: true type: string -permissions: {} +permissions: + contents: read jobs: + # Dispatch inputs are free-form, so confirm they describe a published release. + verify: + name: Verify Release Inputs + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check tag, commit and wheels + shell: bash + env: + GH_TOKEN: ${{ github.token }} + COMMIT_SHA: ${{ inputs.source-sha }} + RELEASE_VERSION: ${{ inputs.python-version }} + RELEASE_TAG: ${{ inputs.release-tag }} + WHEEL_FILENAMES: ${{ inputs.wheel-filenames }} + run: | + set -euo pipefail + tag_sha=$(gh api "repos/${GITHUB_REPOSITORY}/commits/${RELEASE_TAG}" --jq .sha) + if [[ "${tag_sha}" != "${COMMIT_SHA}" ]]; then + echo "${RELEASE_TAG} points to ${tag_sha}, not ${COMMIT_SHA}" >&2 + exit 1 + fi + assets=$(gh release view "${RELEASE_TAG}" -R "${GITHUB_REPOSITORY}" --json assets --jq '.assets[].name') + for wheel in ${WHEEL_FILENAMES//,/ }; do + if ! grep -qxF "${wheel}" <<<"${assets}"; then + echo "${wheel} is not attached to the ${RELEASE_TAG} release" >&2 + exit 1 + fi + if [[ "${wheel}" != *"-${RELEASE_VERSION}-"* ]]; then + echo "${wheel} is not version ${RELEASE_VERSION}" >&2 + exit 1 + fi + done + trigger: name: Trigger Wheel Publish + needs: verify runs-on: [self-hosted, nv] timeout-minutes: 10 steps: From 8bcf1e1e7334a0e2b7224ec62148f16a80befa23 Mon Sep 17 00:00:00 2001 From: Matthew Grossman Date: Tue, 27 Oct 2026 16:44:18 -0700 Subject: [PATCH 4/5] 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 a7ba1a5ad0..2bb81331cc 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 451c6ed46b..d87c5ad762 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 cf251e0bb9..11b06a3422 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. | From 1d9b50fdcce799fbeef227dacc3985c4591093a5 Mon Sep 17 00:00:00 2001 From: Matthew Grossman Date: Tue, 27 Oct 2026 16:45:43 -0700 Subject: [PATCH 5/5] Revert "ci(release): verify the provenance of dispatched snap artifacts" This reverts commit 8bcf1e1e7334a0e2b7224ec62148f16a80befa23. Signed-off-by: Matthew Grossman --- .github/workflows/snap-publish.yml | 52 ----------------------------- .github/workflows/wheel-publish.yml | 2 +- CI.md | 2 +- 3 files changed, 2 insertions(+), 54 deletions(-) diff --git a/.github/workflows/snap-publish.yml b/.github/workflows/snap-publish.yml index 2bb81331cc..a7ba1a5ad0 100644 --- a/.github/workflows/snap-publish.yml +++ b/.github/workflows/snap-publish.yml @@ -86,52 +86,6 @@ 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 @@ -149,12 +103,6 @@ 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 d87c5ad762..451c6ed46b 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" >&2 + echo "No wheel filenames provided by build job" >&2 exit 1 fi diff --git a/CI.md b/CI.md index 11b06a3422..cf251e0bb9 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`, 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/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/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. |