ci(release): allow release publishers to be dispatched individually (#4987)

* 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 <mgrossman@nvidia.com>

* ci(release): allow the Go SDK tag to be dispatched individually

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>

* 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 <mgrossman@nvidia.com>

* 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>

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

This reverts commit 8bcf1e1e73.

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>

---------

Signed-off-by: Matthew Grossman <mgrossman@nvidia.com>
This commit is contained in:
Matthew Grossman
2026-10-28 00:07:24 +00:00
committed by GitHub
parent fa2d381eaa
commit 3a512d23ce
7 changed files with 355 additions and 123 deletions
+4 -1
View File
@@ -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
+27 -118
View File
@@ -509,161 +509,70 @@ 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/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 }}
- 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}"
# 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
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
+58
View File
@@ -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"
@@ -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}"
+40 -3
View File
@@ -17,11 +17,36 @@ 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:
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 +65,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 +83,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 +106,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
@@ -92,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##*/}"
+120
View File
@@ -0,0 +1,120 @@
# 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:
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:
- 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}"