From e97dc84707d57f394e5b301fa7728636e679f3c6 Mon Sep 17 00:00:00 2001 From: Oliver Calder Date: Fri, 23 Oct 2026 08:38:48 -0500 Subject: [PATCH 1/3] feat(snap): publish prerelease builds to latest/beta channel Signed-off-by: Oliver Calder --- .../actions/check-latest-release/action.yml | 30 ++++++++++++------- .github/workflows/release-prerelease.yml | 2 ++ .github/workflows/release-publish.yml | 12 ++++++++ CI.md | 3 +- docs/about/installation.mdx | 12 +++++++- install.sh | 5 ++-- tasks/scripts/test-install-sh.sh | 27 +++++++++++++++++ tasks/scripts/test-packaging-assets.sh | 12 ++++++++ 8 files changed, 89 insertions(+), 14 deletions(-) diff --git a/.github/actions/check-latest-release/action.yml b/.github/actions/check-latest-release/action.yml index dc8006f93e..e6ca096917 100644 --- a/.github/actions/check-latest-release/action.yml +++ b/.github/actions/check-latest-release/action.yml @@ -2,16 +2,16 @@ # SPDX-License-Identifier: Apache-2.0 name: Check Latest Release -description: Check current remote stable tags before updating a shared latest channel +description: Check current remote release tags before updating a shared latest channel inputs: release-tag: - description: Stable version being published + description: Stable or prerelease version being published required: true outputs: make-latest: - description: Whether this is still the highest stable version in the repository + description: Whether this is still the highest release version in the repository value: ${{ steps.latest.outputs.make-latest }} runs: @@ -24,23 +24,33 @@ runs: with: script: | const tag = process.env.RELEASE_TAG; - const parse = value => /^v([0-9]+)\.([0-9]+)\.([0-9]+)$/.exec(value)?.slice(1).map(BigInt); + const includePrereleases = tag.includes('-pre.'); + const parse = value => { + const match = /^v([0-9]+)\.([0-9]+)\.([0-9]+)(?:-pre\.([1-9][0-9]*))?$/.exec(value); + if (!match) return null; + return [BigInt(match[1]), BigInt(match[2]), BigInt(match[3]), match[4] ? 0n : 1n, BigInt(match[4] || 0)]; + }; + const compare = (left, right) => { + for (let i = 0; i < left.length; i++) { + if (left[i] !== right[i]) return left[i] < right[i] ? -1 : 1; + } + return 0; + }; const version = parse(tag); if (!version) { - throw new Error(`Invalid stable release tag: ${tag}`); + throw new Error(`Invalid release tag: ${tag}`); } const tags = await github.paginate(github.rest.repos.listTags, { ...context.repo, per_page: 100, }); if (!tags.some(candidate => candidate.name === tag)) { - throw new Error(`Stable release tag ${tag} is missing from the repository`); + throw new Error(`Release tag ${tag} is missing from the repository`); } const newer = tags.find(candidate => { + if (!includePrereleases && candidate.name.includes('-pre.')) return false; const other = parse(candidate.name); - if (!other) return false; - const index = other.findIndex((part, i) => part !== version[i]); - return index !== -1 && other[index] > version[index]; + return other && compare(other, version) > 0; }); - core.info(newer ? `${tag} is superseded by ${newer.name}; preserving latest` : `${tag} is the latest stable version`); + core.info(newer ? `${tag} is superseded by ${newer.name}; preserving latest` : `${tag} is the latest release version`); core.setOutput('make-latest', !newer); diff --git a/.github/workflows/release-prerelease.yml b/.github/workflows/release-prerelease.yml index 7617cb0923..c3cbf2ce2e 100644 --- a/.github/workflows/release-prerelease.yml +++ b/.github/workflows/release-prerelease.yml @@ -63,6 +63,8 @@ jobs: python-version: ${{ needs.build.outputs.python_version }} npm-version: ${{ needs.build.outputs.npm_version }} is-prerelease: true + secrets: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} qualify: name: Qualify diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index bad8b4a284..ba8493f510 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -517,6 +517,18 @@ jobs: secrets: publish-credentials: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} + publish-prerelease-snap: + name: Publish Prerelease Snap + needs: release + if: inputs.is-prerelease + 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 }} + publish-sdk-typescript: name: Publish TypeScript SDK needs: release diff --git a/CI.md b/CI.md index b647d73001..f911ebbaf3 100644 --- a/CI.md +++ b/CI.md @@ -327,6 +327,7 @@ Pre-release builds, then publishes and calls `release-qualification.yml` in parallel within the same run, using native job dependencies. Its platform package artifacts are uploaded after versioned images and the chart, so the installer can use them while qualification runs or after it fails. +The pre-release snap artifact is published to the `latest/beta` channel. The implemented `release-tag-v1` profile is an incremental subset of RFC 0014 qualification. Failed checks fail the parent workflow and retain evidence in Actions. @@ -644,7 +645,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 to the Store without rebuilding. Release Dev calls it directly for `latest/edge` after snap builds and requires it to succeed before creating the dev release. Release calls it through `release-publish.yml` for `latest/stable` after qualification and release publication succeed. | +| `.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. Tagged beta and stable publication recheck repository tags before updating their floating channels. | | `.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 diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 2afc236edc..e33e089c49 100644 --- a/docs/about/installation.mdx +++ b/docs/about/installation.mdx @@ -38,7 +38,7 @@ curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_VERSION=pre sh ``` -The installer checks prerelease tags from newest to oldest, selects an unexpired published package for the current platform, and downloads only that artifact. The selected candidate may still be qualifying or may have failed qualification. Installed packages keep the candidate's exact version, such as `0.1.0-pre.3`. Prerelease tags do not create entries on the GitHub Releases page. On Linux, prereleases and explicit release tags use Debian or RPM packages. +The installer checks prerelease tags from newest to oldest, selects an unexpired published package for the current platform, and downloads only that artifact. The selected candidate may still be qualifying or may have failed qualification. Installed packages keep the candidate's exact version, such as `0.1.0-pre.3`. Prerelease tags do not create entries on the GitHub Releases page. On Linux, explicit release tags use Debian or RPM packages, as do prereleases by default. Set `OPENSHELL_INSTALL_METHOD=snap` with `OPENSHELL_VERSION=pre` to track the newest published prerelease snap in `latest/beta`. Beta publication runs alongside qualification, so the channel may contain a candidate that later fails qualification. The rolling [`dev` release](https://github.com/NVIDIA/OpenShell/releases/tag/dev) does not require GitHub authentication: @@ -128,6 +128,16 @@ sudo snap install openshell systemctl --user enable --now snap.openshell.user-gateway.service ``` +To install a prerelease build, select the corresponding Store channel: + +```shell +sudo snap install openshell --channel=latest/beta +# or, if already installed +sudo snap refresh openshell --channel=latest/beta +``` + +`latest/beta` contains the newest prerelease published by CI and may not have passed qualification. Stable releases rebuild the selected qualified commit with the final stable version before publishing to `latest/stable`. + The user gateway is disabled globally by default. The install script enables it only for the user who ran the script. After a direct snap installation, choose one user account to run the gateway and enable it with the command above. Do not enable the gateway for multiple users who may be logged in at the same time: every user gateway binds to `127.0.0.1:17670`, so only one can run on the system at a time. Older OpenShell snap revisions could enable the user gateway globally, causing it to start for every user. Remove that global enablement once, then enable the gateway from the account that will run it: diff --git a/install.sh b/install.sh index 6440bff0fc..10dd62f59a 100755 --- a/install.sh +++ b/install.sh @@ -73,8 +73,8 @@ NOTES: x86_64/aarch64, depending on the host package manager. Set OPENSHELL_INSTALL_METHOD=snap to install the OpenShell snap instead; hosts that already have the OpenShell snap keep refreshing it. snap installs use - latest/stable by default and latest/edge for dev, and do not support - explicit release tags or prereleases. The OpenShell snap requires a running + latest/stable by default, latest/beta for pre, and latest/edge for dev. They + do not support explicit release tags. The OpenShell snap requires a running Docker Engine installed from a system package or Docker's package repository. The Docker snap is not currently compatible with OpenShell. macOS installs the release Homebrew formula on Apple Silicon and starts a @@ -1271,6 +1271,7 @@ install_linux_rpm() { openshell_snap_channel() { case "${OPENSHELL_VERSION:-}" in dev) printf '%s\n' "latest/edge" ;; + pre) printf '%s\n' "latest/beta" ;; '') printf '%s\n' "latest/stable" ;; *) error "snap installs do not support OPENSHELL_VERSION=${OPENSHELL_VERSION}; use a native package" ;; esac diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index 80ac03d2c6..9021f45921 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -237,6 +237,21 @@ if [ -s "$err" ]; then exit 1 fi +if ! OPENSHELL_VERSION=pre openshell_snap_channel >"$out" 2>"$err"; then + echo "FAIL: pre must select the latest/beta snap channel" >&2 + cat "$err" >&2 || true + exit 1 +fi +if [ "$(cat "$out")" != "latest/beta" ]; then + echo "FAIL: pre must select the latest/beta snap channel" >&2 + exit 1 +fi +if [ -s "$err" ]; then + echo "FAIL: pre must not warn about an ignored version" >&2 + cat "$err" >&2 || true + exit 1 +fi + if ! OPENSHELL_VERSION="" openshell_snap_channel >"$out" 2>"$err"; then echo "FAIL: unset OPENSHELL_VERSION must select the latest/stable snap channel" >&2 cat "$err" >&2 || true @@ -392,6 +407,18 @@ wait:user-gateway-listener register:user-gateway wait:gateway-status" +assert_snap_install_flow \ + "prerelease snap tracks beta" \ + 1 0 "pre" \ + "wait:user-docker +root:snap install openshell --channel=latest/beta +root:systemctl --global disable snap.openshell.user-gateway.service +target:systemctl --user daemon-reload +target:systemctl --user enable --now snap.openshell.user-gateway.service +wait:user-gateway-listener +register:user-gateway +wait:gateway-status" + assert_snap_user_refresh_flow assert_snap_install_flow \ diff --git a/tasks/scripts/test-packaging-assets.sh b/tasks/scripts/test-packaging-assets.sh index 5600370b60..54cb01a9db 100755 --- a/tasks/scripts/test-packaging-assets.sh +++ b/tasks/scripts/test-packaging-assets.sh @@ -117,6 +117,9 @@ assert_not_contains "$development_gateway" 'supervisor_image = "{{ openshell_sup snap_wrapper="${ROOT}/tasks/scripts/snap-gateway-wrapper.sh" snapcraft="${ROOT}/snapcraft.yaml" snap_workflow="${ROOT}/.github/workflows/snap-package.yml" +snap_publish_workflow="${ROOT}/.github/workflows/snap-publish.yml" +release_publish_workflow="${ROOT}/.github/workflows/release-publish.yml" +release_prerelease_workflow="${ROOT}/.github/workflows/release-prerelease.yml" snap_install_docs="${ROOT}/docs/about/installation.mdx" snap_canary="${ROOT}/.github/workflows/release-canary.yml" snap_configure_hook="${ROOT}/snap/hooks/configure" @@ -126,6 +129,9 @@ package_deb="${ROOT}/tasks/scripts/package-deb.sh" assert_file_exists "$snap_wrapper" assert_file_exists "$snapcraft" assert_file_exists "$snap_workflow" +assert_file_exists "$snap_publish_workflow" +assert_file_exists "$release_publish_workflow" +assert_file_exists "$release_prerelease_workflow" assert_file_exists "$snap_install_docs" assert_file_exists "$snap_canary" assert_file_exists "$snap_configure_hook" @@ -208,6 +214,12 @@ bash "$ROOT/tasks/scripts/test-snap-post-refresh-hook.sh" "$snap_post_refresh_ho assert_contains "$snap_workflow" 'name: openshell-prover-${{ matrix.rust_arch }}-unknown-linux-musl' assert_contains "$snap_workflow" 'chmod +x prebuilt/prover/openshell-prover' assert_contains "$snap_workflow" 'cp prebuilt/prover/openshell-prover snap/prebuilt/openshell-prover' +assert_contains "$release_publish_workflow" 'uses: ./.github/workflows/snap-publish.yml' +assert_contains "$release_publish_workflow" 'upload-channel: latest/beta' +assert_contains "$release_publish_workflow" 'github-environment: latest/beta' +assert_contains "$release_prerelease_workflow" 'SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}' +assert_contains "$snap_publish_workflow" 'snapcraft upload --release "${INPUTS_UPLOAD_CHANNEL}"' +assert_contains "$snap_publish_workflow" 'component_args+=(--component "${component_name}=${comp}")' assert_contains "$snapcraft" 'for bin in openshell openshell-prover openshell-gateway openshell-sandbox openshell-gateway-wrapper; do' assert_contains "$snapcraft" '"$CRAFT_PART_INSTALL/bin/openshell-prover"' if ! awk ' From b847b6554d577401d1fed56aa1375964156c5b14 Mon Sep 17 00:00:00 2001 From: Oliver Calder Date: Fri, 23 Oct 2026 09:09:57 -0500 Subject: [PATCH 2/3] fixup! feat(snap): publish prerelease builds to latest/beta channel Signed-off-by: Oliver Calder --- .github/workflows/snap-publish.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/snap-publish.yml b/.github/workflows/snap-publish.yml index 4b571985c6..8b81caa138 100644 --- a/.github/workflows/snap-publish.yml +++ b/.github/workflows/snap-publish.yml @@ -90,6 +90,9 @@ jobs: snap_name="${snap_name%%_*}" component_args=() for comp in "${snap_name}"+*.comp; do - component_args+=(--component "$comp") + component_file="${comp##*/}" + component_name="${component_file#${snap_name}+}" + component_name="${component_name%%_*}" + component_args+=(--component "${component_name}=${comp}") done snapcraft upload --release "${INPUTS_UPLOAD_CHANNEL}" "$snap_file" "${component_args[@]}" From d6861c94fa46e8ce6faa2192705d5247333c9be2 Mon Sep 17 00:00:00 2001 From: Oliver Calder Date: Fri, 23 Oct 2026 09:15:14 -0500 Subject: [PATCH 3/3] fix(snap): use concurrency group when checking version to publish snap Signed-off-by: Oliver Calder --- .github/workflows/snap-publish.yml | 4 ++++ CI.md | 2 +- tasks/scripts/test-packaging-assets.sh | 3 +++ 3 files changed, 8 insertions(+), 1 deletion(-) diff --git a/.github/workflows/snap-publish.yml b/.github/workflows/snap-publish.yml index 8b81caa138..670522fd34 100644 --- a/.github/workflows/snap-publish.yml +++ b/.github/workflows/snap-publish.yml @@ -41,6 +41,10 @@ jobs: runs-on: ${{ matrix.runner }} timeout-minutes: 10 environment: ${{ inputs.github-environment }} + concurrency: + group: snap-publish-${{ inputs.upload-channel }}-${{ matrix.arch }} + cancel-in-progress: false + queue: max steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 if: inputs.release-tag != '' diff --git a/CI.md b/CI.md index f911ebbaf3..fb63409c41 100644 --- a/CI.md +++ b/CI.md @@ -645,7 +645,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. Tagged beta and stable publication recheck repository tags 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. | | `.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 diff --git a/tasks/scripts/test-packaging-assets.sh b/tasks/scripts/test-packaging-assets.sh index 54cb01a9db..879957380d 100755 --- a/tasks/scripts/test-packaging-assets.sh +++ b/tasks/scripts/test-packaging-assets.sh @@ -220,6 +220,9 @@ assert_contains "$release_publish_workflow" 'github-environment: latest/beta' assert_contains "$release_prerelease_workflow" 'SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }}' assert_contains "$snap_publish_workflow" 'snapcraft upload --release "${INPUTS_UPLOAD_CHANNEL}"' assert_contains "$snap_publish_workflow" 'component_args+=(--component "${component_name}=${comp}")' +assert_contains "$snap_publish_workflow" 'group: snap-publish-${{ inputs.upload-channel }}-${{ matrix.arch }}' +assert_contains "$snap_publish_workflow" 'cancel-in-progress: false' +assert_contains "$snap_publish_workflow" 'queue: max' assert_contains "$snapcraft" 'for bin in openshell openshell-prover openshell-gateway openshell-sandbox openshell-gateway-wrapper; do' assert_contains "$snapcraft" '"$CRAFT_PART_INSTALL/bin/openshell-prover"' if ! awk '