diff --git a/.agents/skills/release-openshell/SKILL.md b/.agents/skills/release-openshell/SKILL.md index 2cf26fda64..2f641aea3c 100644 --- a/.agents/skills/release-openshell/SKILL.md +++ b/.agents/skills/release-openshell/SKILL.md @@ -15,6 +15,7 @@ release to the maintainer; once approved, tag that exact commit as `vX.Y.Z`. If no prerelease exists, agree on the version and source commit, then create and push `vX.Y.Z-pre.1`. Wait for qualification to pass before promoting it. -Both prerelease and stable tag pushes start Release Tag. Release Auto-Tag uses -an explicit dispatch because its `GITHUB_TOKEN` pushes do not trigger workflows. +Prerelease tag pushes start Pre-release; stable tag pushes start Release. +Release Auto-Tag uses an explicit dispatch because its `GITHUB_TOKEN` pushes do +not trigger workflows. Watch the stable release run and report its result. diff --git a/.agents/skills/sbom/SKILL.md b/.agents/skills/sbom/SKILL.md index 0848363c82..9e80b0838e 100644 --- a/.agents/skills/sbom/SKILL.md +++ b/.agents/skills/sbom/SKILL.md @@ -15,7 +15,7 @@ The OpenShell SBOM tooling produces source-tree CycloneDX JSON SBOMs using Syft, SBOMs are **release artifacts only** -- they are generated on demand and not committed to the repository. Output lands in `deploy/sbom/output/` (gitignored). -Pushed gateway, sandbox, and supervisor images carry an SPDX SBOM and minimal SLSA provenance as OCI attestations. Branch E2E, Release Dev, and Release Tag image binaries embed cargo-auditable metadata, so their image SBOMs include linked Rust crates. +Pushed gateway, sandbox, and supervisor images carry an SPDX SBOM and minimal SLSA provenance as OCI attestations. Branch E2E, Release Dev, Release, and Pre-release image binaries embed cargo-auditable metadata, so their image SBOMs include linked Rust crates. ## Prerequisites diff --git a/.agents/skills/test-release-canary/SKILL.md b/.agents/skills/test-release-canary/SKILL.md index fc6414eeed..5081c85e50 100644 --- a/.agents/skills/test-release-canary/SKILL.md +++ b/.agents/skills/test-release-canary/SKILL.md @@ -33,7 +33,7 @@ they continue to exercise the dev Debian package. Kubernetes pins the matching `0.0.0-dev` chart and `:dev` images. RPM package installation also has tmachine conformance coverage in -`Branch E2E Checks` (with `test:e2e`), `Release Dev`, and `Release Tag`. Those +`Branch E2E Checks` (with `test:e2e`), `Release Dev`, `Release`, and `Pre-release`. Those lanes use the `rpm` installer on `fedora-podman-rootful` and `fedora-podman-rootless` with candidate CLI and gateway RPMs and matching runtime images. Branch RPM package builds run on diff --git a/.agents/skills/watch-github-actions/SKILL.md b/.agents/skills/watch-github-actions/SKILL.md index 6250676268..6a5893a234 100644 --- a/.agents/skills/watch-github-actions/SKILL.md +++ b/.agents/skills/watch-github-actions/SKILL.md @@ -160,21 +160,22 @@ groups and manual runs use their explicit baseline. Findings are reported by `Reject new high or critical findings`; distinguish those from scanner failures. For `Protobuf Compatibility`, check the logged train and comparison baseline. -Branch Checks compares the prospective merge tree with its target; Release Tag +Branch Checks compares the prospective merge tree with its target; release qualification compares the tagged candidate with the previous stable release. Both use the shared `check-protobuf-compatibility` action with `nix run .#check-protobuf-compatibility -- `. During `0.x`, a minor train permits compatibility findings as warnings; a patch train or no active train rejects them. Compare the current train's version with the latest stable release; commit messages are irrelevant. Compilation, baseline, and tool errors remain fatal. The `protobuf_compatibility` suite participates in -the `release-tag-v1` qualification profile. Release Tag calls the reusable +the `release-tag-v1` qualification profile. Release and Pre-release call the reusable `release-qualification.yml` workflow to run the checks and collect their results. -Both tagged pre-release and stable -publication require this profile to pass. Failed, cancelled, or skipped suites -block publication; build artifacts and qualification evidence remain in Actions -storage for diagnosis. Source-SHA images are staging inputs for qualification. -Snap builds run in parallel with qualification, but tagged stable Store uploads -consume those built artifacts only after qualification passes. +Release builds, qualifies, then publishes. Pre-release builds, then publishes +and qualifies in parallel in the same run. Failed, cancelled, or skipped required suites fail +qualification; stable publication is blocked, while published pre-release +artifacts remain available. The Pre-release failure notification includes the +run link. Both callers share `release-build.yml` and `release-publish.yml`. +Qualification evidence remains in Actions; publishing a copy to OCI storage is best effort. +Snap Store uploads happen only for stable releases after qualification passes. For `Codex Compatibility Review`, inspect its job summary and the `compatibility-review-run--attempt-` artifact. Confirm the @@ -192,7 +193,7 @@ share the `codex-release-review` concurrency group with `queue: max`, retaining up to 100 pending reviews while preventing overlapping API usage across runs. Either review can run first; other security scanners run in parallel. The qualification caller waits for both reviews to finish. Do not -rerun Release Tag to debug this reviewer. Use its dedicated manual workflow +rerun Release or Pre-release to debug this reviewer. Use its dedicated manual workflow with an existing candidate tag; that path does not publish release artifacts. View logs for a specific run: diff --git a/.github/workflows/branch-docs.yml b/.github/workflows/branch-docs.yml index 139b14f75b..f42402743b 100644 --- a/.github/workflows/branch-docs.yml +++ b/.github/workflows/branch-docs.yml @@ -12,6 +12,8 @@ on: - "tasks/scripts/check_docs_nav_test.py" - ".github/workflows/branch-docs.yml" - ".github/workflows/release-tag.yml" + - ".github/workflows/release-prerelease.yml" + - ".github/workflows/release-publish.yml" permissions: contents: read diff --git a/.github/workflows/release-auto-tag.yml b/.github/workflows/release-auto-tag.yml index b0ae1a2686..2c5892aa4e 100644 --- a/.github/workflows/release-auto-tag.yml +++ b/.github/workflows/release-auto-tag.yml @@ -85,12 +85,12 @@ jobs: git tag "$NEXT_TAG" git push origin "refs/tags/$NEXT_TAG" - - name: Trigger Release Tag workflow + - name: Trigger Pre-release workflow if: steps.version.outputs.skip != 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} NEXT_TAG: ${{ steps.version.outputs.next }} run: | - gh workflow run release-tag.yml \ + gh workflow run release-prerelease.yml \ --ref main \ -f tag="$NEXT_TAG" diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml new file mode 100644 index 0000000000..957d9130ec --- /dev/null +++ b/.github/workflows/release-build.yml @@ -0,0 +1,215 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Build Release + +on: + workflow_call: + inputs: + release-tag: + description: Git tag to build and publish + required: true + type: string + is-prerelease: + description: Whether the caller is publishing a pre-release + required: true + type: boolean + outputs: + python_version: + description: Python package version + value: ${{ jobs.compute-versions.outputs.python_version }} + npm_version: + description: TypeScript package version + value: ${{ jobs.compute-versions.outputs.npm_version }} + semver: + description: Release version without the v prefix + value: ${{ jobs.compute-versions.outputs.semver }} + source_sha: + description: Source commit resolved from the release tag + value: ${{ jobs.compute-versions.outputs.source_sha }} + secrets: + CACHIX_AUTH_TOKEN: + required: false + +permissions: + contents: read + packages: read + +env: + RELEASE_TAG: ${{ inputs.release-tag }} + +defaults: + run: + shell: bash + +jobs: + compute-versions: + name: Compute Versions + runs-on: linux-amd64-cpu8 + timeout-minutes: 5 + container: + image: ghcr.io/nvidia/openshell/ci:9cb72baa2e61a1b5f12407e6e82da7fdba0aa722@sha256:67a9a0c32cb99825e6d3e9d9eec45d67149ea1ff7c11a1b1e1b3480d2d3df684 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + outputs: + python_version: ${{ steps.v.outputs.python }} + cargo_version: ${{ steps.v.outputs.cargo }} + npm_version: ${{ steps.v.outputs.npm }} + deb_version: ${{ steps.v.outputs.deb }} + rpm_version: ${{ steps.v.outputs.rpm_version }} + rpm_release: ${{ steps.v.outputs.rpm_release }} + # Semver without 'v' prefix (e.g. 0.6.0), used for image tags and release body + semver: ${{ steps.v.outputs.semver }} + # Commit resolved from RELEASE_TAG, used for image tags and downstream metadata + source_sha: ${{ steps.v.outputs.source_sha }} + is_prerelease: ${{ steps.v.outputs.is_prerelease }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ inputs.release-tag }} + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Fetch tags + run: git fetch --tags --force + + - name: Compute all versions + id: v + env: + EXPECTED_PRERELEASE: ${{ inputs.is-prerelease }} + run: | + set -euo pipefail + if [[ "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$ ]]; then + is_prerelease=true + elif [[ "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + is_prerelease=false + else + echo "Unsupported release tag: ${RELEASE_TAG}" >&2 + exit 1 + fi + if [[ "${is_prerelease}" != "${EXPECTED_PRERELEASE}" ]]; then + echo "Tag ${RELEASE_TAG} does not match this release workflow's prerelease mode." >&2 + exit 1 + fi + { + echo "is_prerelease=${is_prerelease}" + echo "python=$(uv run python tasks/scripts/release.py get-version --python)" + echo "cargo=$(uv run python tasks/scripts/release.py get-version --cargo)" + echo "npm=$(uv run python tasks/scripts/release.py get-version --npm)" + echo "deb=$(uv run python tasks/scripts/release.py get-version --deb)" + echo "rpm_version=$(uv run python tasks/scripts/release.py get-version --rpm-version)" + echo "rpm_release=$(uv run python tasks/scripts/release.py get-version --rpm-release)" + echo "semver=${RELEASE_TAG#v}" + echo "source_sha=$(git rev-parse HEAD)" + } >> "$GITHUB_OUTPUT" + + build-binaries: + needs: compute-versions + permissions: + contents: read + uses: ./.github/workflows/build-binaries.yml + with: + cargo-version: ${{ needs.compute-versions.outputs.cargo_version }} + supervisor-image-tag: ${{ needs.compute-versions.outputs.semver }} + checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} + secrets: + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + + build-vm-driver: + needs: [compute-versions, build-binaries] + permissions: + contents: read + uses: ./.github/workflows/build-vm-driver.yml + with: + cargo-version: ${{ needs.compute-versions.outputs.cargo_version }} + supervisor-image-tag: ${{ needs.compute-versions.outputs.semver }} + checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} + secrets: + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + + package-binaries: + needs: [compute-versions, build-binaries, build-vm-driver] + permissions: + actions: read + contents: read + uses: ./.github/workflows/package-release-binaries.yml + with: + checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} + + build-images: + needs: [compute-versions, build-binaries] + permissions: + contents: read + packages: write + uses: ./.github/workflows/build-images.yml + with: + image-tag: ${{ needs.compute-versions.outputs.source_sha }} + checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} + + build-python-wheel: + name: Build Python Wheel + needs: [compute-versions] + runs-on: linux-amd64-cpu8 + timeout-minutes: 20 + container: + image: ghcr.io/nvidia/openshell/ci:9cb72baa2e61a1b5f12407e6e82da7fdba0aa722@sha256:67a9a0c32cb99825e6d3e9d9eec45d67149ea1ff7c11a1b1e1b3480d2d3df684 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + env: + MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL: ${{ needs.compute-versions.outputs.python_version }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + ref: ${{ needs.compute-versions.outputs.source_sha }} + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Sync Python dependencies + run: uv sync + + - name: Build Python wheel + run: | + set -euo pipefail + mise run python:build + ls -la target/wheels/*.whl + + - name: Upload wheel artifact + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: python-wheel + path: target/wheels/*.whl + retention-days: 5 + + build-deb: + name: Build Debian Packages + needs: [compute-versions, build-binaries, build-vm-driver] + uses: ./.github/workflows/deb-package.yml + with: + deb-version: ${{ needs.compute-versions.outputs.deb_version }} + checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} + + build-snap: + name: Build Snap + needs: [compute-versions, build-binaries] + uses: ./.github/workflows/snap-package.yml + with: + checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} + + build-rpm: + name: Build RPM Packages + needs: [compute-versions, build-binaries] + uses: ./.github/workflows/rpm-package.yml + with: + checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} + rpm-version: ${{ needs.compute-versions.outputs.rpm_version }} + rpm-release: ${{ needs.compute-versions.outputs.rpm_release }} + cargo-version: ${{ needs.compute-versions.outputs.cargo_version }} diff --git a/.github/workflows/release-prerelease.yml b/.github/workflows/release-prerelease.yml new file mode 100644 index 0000000000..72486958c1 --- /dev/null +++ b/.github/workflows/release-prerelease.yml @@ -0,0 +1,135 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Pre-release + +on: + push: + tags: + - "v*.*.*-pre.*" + workflow_dispatch: + inputs: + tag: + description: "Pre-release tag to publish and qualify (e.g. v0.0.4-pre.1)" + required: true + type: string + +permissions: + contents: read + +jobs: + build: + name: Build + permissions: + actions: read + contents: read + packages: write + uses: ./.github/workflows/release-build.yml + with: + release-tag: ${{ inputs.tag || github.ref_name }} + is-prerelease: true + secrets: + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + + publish: + name: Publish + needs: build + permissions: + actions: read + contents: write + packages: write + id-token: write + attestations: write + artifact-metadata: write + uses: ./.github/workflows/release-publish.yml + with: + release-tag: ${{ inputs.tag || github.ref_name }} + source-sha: ${{ needs.build.outputs.source_sha }} + semver: ${{ needs.build.outputs.semver }} + python-version: ${{ needs.build.outputs.python_version }} + npm-version: ${{ needs.build.outputs.npm_version }} + is-prerelease: true + + qualify: + name: Qualify + needs: build + permissions: + actions: read + contents: read + packages: write + security-events: write + pull-requests: read + uses: ./.github/workflows/release-qualification.yml + with: + release-tag: ${{ inputs.tag || github.ref_name }} + source-sha: ${{ needs.build.outputs.source_sha }} + is-prerelease: true + secrets: + CODEX_SECURITY_API_KEY: ${{ secrets.CODEX_SECURITY_API_KEY }} + CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + + notify-prerelease-success: + name: Notify Prerelease Success + needs: [build, publish, qualify] + if: success() + runs-on: ubuntu-latest + timeout-minutes: 2 + permissions: {} + steps: + - name: Send Slack notification + continue-on-error: true + env: + RELEASE_TAG: ${{ inputs.tag || github.ref_name }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL }} + RUN_URL: ${{ format('{0}/{1}/actions/runs/{2}/attempts/{3}', github.server_url, github.repository, github.run_id, github.run_attempt) }} + TAG_URL: ${{ format('{0}/{1}/tree/{2}', github.server_url, github.repository, inputs.tag || github.ref_name) }} + run: | + set -euo pipefail + if [[ -z "${SLACK_WEBHOOK_URL}" ]]; then + echo "::notice::SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL is unset; skipping Slack notification." + exit 0 + fi + + jq -n --arg tag "${RELEASE_TAG}" --arg tag_url "${TAG_URL}" \ + --arg attempt "${GITHUB_RUN_ATTEMPT}" --arg run_url "${RUN_URL}" \ + '{text: (":white_check_mark: OpenShell prerelease <" + $tag_url + "|" + $tag + "> succeeded (attempt " + $attempt + ").\n<" + $run_url + "|View release run and artifacts>"), unfurl_links: false, unfurl_media: false}' | + curl --fail --silent --show-error --connect-timeout 5 --max-time 15 \ + --header 'Content-Type: application/json' --data-binary @- "${SLACK_WEBHOOK_URL}" + + notify-prerelease-failure: + name: Notify Prerelease Failure + needs: [build, publish, qualify] + if: failure() + runs-on: ubuntu-latest + timeout-minutes: 2 + permissions: {} + defaults: + run: + shell: bash + steps: + - name: Send Slack notification + continue-on-error: true + env: + RELEASE_TAG: ${{ inputs.tag || github.ref_name }} + PUBLICATION_RESULT: ${{ needs.publish.result }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL }} + SLACK_MENTION: ${{ secrets.SLACK_OPENSHELL_TRIAGE_MENTION }} + RUN_URL: ${{ format('{0}/{1}/actions/runs/{2}/attempts/{3}', github.server_url, github.repository, github.run_id, github.run_attempt) }} + run: | + set -euo pipefail + if [[ -z "${SLACK_WEBHOOK_URL}" ]]; then + echo "::notice::SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL is unset; skipping Slack notification." + exit 0 + fi + + message=":x: OpenShell prerelease ${RELEASE_TAG} failed (attempt ${GITHUB_RUN_ATTEMPT})." + if [[ "${PUBLICATION_RESULT}" == "success" ]]; then + message+=" Published artifacts remain available." + fi + if [[ -n "${SLACK_MENTION}" ]]; then + message+=" ${SLACK_MENTION}" + fi + jq -n --arg text "${message}" --arg run_url "${RUN_URL}" \ + '{text: ($text + "\n<" + $run_url + "|View failed release run>"), unfurl_links: false, unfurl_media: false}' | + curl --fail --silent --show-error --connect-timeout 5 --max-time 15 \ + --header 'Content-Type: application/json' --data-binary @- "${SLACK_WEBHOOK_URL}" diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 0000000000..83302a8873 --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,524 @@ +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: Publish Release + +on: + workflow_call: + inputs: + release-tag: + description: Git tag to build and publish + required: true + type: string + is-prerelease: + description: Whether the caller is publishing a pre-release + required: true + type: boolean + source-sha: + description: Source commit of the built artifacts in the calling run + required: true + type: string + semver: + description: Release version without the v prefix + required: true + type: string + python-version: + description: Python package version + required: true + type: string + npm-version: + description: TypeScript package version + required: true + type: string + secrets: + SNAPCRAFT_STORE_CREDENTIALS: + required: false + FERN_TOKEN: + required: false + GITLAB_CI_TRIGGER_TOKEN: + required: false + GITLAB_CI_TRIGGER_URL: + required: false + +permissions: + contents: read + packages: read + +env: + RELEASE_TAG: ${{ inputs.release-tag }} + +defaults: + run: + shell: bash + +jobs: + assemble: + name: Assemble Release Assets + runs-on: linux-amd64-cpu8 + timeout-minutes: 10 + permissions: + contents: write + id-token: write + attestations: write + artifact-metadata: write + outputs: + wheel_filenames: ${{ steps.wheel_filenames.outputs.wheel_filenames }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ inputs.source-sha }} + + - name: Download all CLI artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: cli-* + path: release/ + merge-multiple: true + + - name: Download gateway binary artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: gateway-binary-* + path: release/ + merge-multiple: true + + - name: Download supervisor binary artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: supervisor-binary-* + path: release/ + merge-multiple: true + + - name: Download sandbox binary artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: sandbox-binary-* + path: release/ + merge-multiple: true + + - name: Download VM driver artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: driver-vm-* + path: release/ + merge-multiple: true + + - name: Download prover binary artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: prover-binary-* + path: release/ + merge-multiple: true + + - name: Download prover checksums + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: prover-checksums + path: release/ + + - name: Download wheel artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: python-wheel + path: release/ + + - name: Download Debian package artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: deb-linux-* + path: release/ + merge-multiple: true + + - name: Download RPM package artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: rpm-linux-* + path: release/ + merge-multiple: true + + - name: Download snap artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: snap-linux-* + path: release/ + merge-multiple: true + + - name: Capture wheel filenames + id: wheel_filenames + run: | + set -euo pipefail + shopt -s nullglob + wheels=(release/*.whl) + if [ "${#wheels[@]}" -ne 1 ]; then + echo "expected exactly one Python wheel, found ${#wheels[@]}" >&2 + exit 1 + fi + wheel_filename=$(basename "${wheels[0]}") + echo "wheel_filenames=${wheel_filename}" >> "$GITHUB_OUTPUT" + + - name: Generate checksums + run: | + set -euo pipefail + cd release + sha256sum -- \ + openshell-x86_64-unknown-linux-musl.tar.gz \ + openshell-aarch64-unknown-linux-musl.tar.gz \ + openshell-aarch64-apple-darwin.tar.gz \ + openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz \ + openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz \ + openshell-driver-vm-aarch64-apple-darwin.tar.gz \ + openshell_*.deb \ + openshell-*.rpm \ + *.whl > openshell-checksums-sha256.txt + cat openshell-checksums-sha256.txt + sha256sum -- \ + openshell-gateway-x86_64-unknown-linux-gnu.tar.gz \ + openshell-gateway-aarch64-unknown-linux-gnu.tar.gz \ + openshell-gateway-aarch64-apple-darwin.tar.gz > openshell-gateway-checksums-sha256.txt + cat openshell-gateway-checksums-sha256.txt + sha256sum -- \ + openshell-sandbox-x86_64-unknown-linux-musl.tar.gz \ + openshell-sandbox-aarch64-unknown-linux-musl.tar.gz > openshell-sandbox-checksums-sha256.txt + cat openshell-sandbox-checksums-sha256.txt + sha256sum -- \ + openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz \ + openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz > openshell-supervisor-checksums-sha256.txt + cat openshell-supervisor-checksums-sha256.txt + + - name: Generate Homebrew formula + run: | + set -euo pipefail + python3 tasks/scripts/release.py generate-homebrew-formula \ + --release-tag "${RELEASE_TAG}" \ + --release-dir release \ + --output release/openshell.rb + cat release/openshell.rb + + - name: Attest release artifacts + id: attest + uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 + with: + subject-path: | + release/*.tar.gz + release/*.deb + release/*.rpm + release/*.whl + + - name: Upload assembled release assets + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: release-assets + path: release/ + retention-days: 5 + if-no-files-found: error + + tag-ghcr-release: + name: Tag GHCR Images + needs: assemble + runs-on: linux-amd64-cpu8 + timeout-minutes: 10 + permissions: + packages: write + steps: + - name: Log in to GHCR + run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin + + - name: Tag images with release version + env: + IS_PRERELEASE: ${{ inputs.is-prerelease }} + SOURCE_TAG: ${{ inputs.source-sha }} + VERSION: ${{ inputs.semver }} + run: | + set -euo pipefail + REGISTRY="ghcr.io/nvidia/openshell" + for component in gateway sandbox supervisor; do + echo "Tagging ${REGISTRY}/${component}:${SOURCE_TAG} as ${VERSION}..." + docker buildx imagetools create \ + --prefer-index=false \ + -t "${REGISTRY}/${component}:${VERSION}" \ + "${REGISTRY}/${component}:${SOURCE_TAG}" + if [[ "${IS_PRERELEASE}" != "true" ]]; then + echo "Tagging ${REGISTRY}/${component}:${SOURCE_TAG} as latest..." + docker buildx imagetools create \ + --prefer-index=false \ + -t "${REGISTRY}/${component}:latest" \ + "${REGISTRY}/${component}:${SOURCE_TAG}" + fi + done + + release-helm: + name: Release Helm Chart (OCI) + needs: tag-ghcr-release + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + packages: write + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ inputs.source-sha }} + + - uses: ./.github/actions/release-helm-oci + with: + chart-version: ${{ inputs.semver }} + app-version: ${{ inputs.semver }} + release-kind: public + + release: + name: Publish Release Assets + needs: [assemble, release-helm] + runs-on: linux-amd64-cpu8 + timeout-minutes: 10 + permissions: + contents: write + outputs: + wheel_filenames: ${{ needs.assemble.outputs.wheel_filenames }} + steps: + - name: Download assembled release assets + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: release-assets + path: release/ + + # Installer-visible packages are uploaded only after images and the chart. + - name: Prune removed VM checksum asset + if: ${{ !inputs.is-prerelease }} + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 + with: + script: | + const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); + let release; + try { + release = await github.rest.repos.getReleaseByTag({ owner, repo, tag: process.env.RELEASE_TAG }); + } catch (err) { + if (err.status === 404) { + core.info(`No existing ${process.env.RELEASE_TAG} release; skipping VM checksum pruning.`); + return; + } + throw err; + } + for (const asset of release.data.assets) { + if (asset.name === 'openshell-driver-vm-checksums-sha256.txt') { + core.info(`Deleting removed VM checksum asset: ${asset.name}`); + await github.rest.repos.deleteReleaseAsset({ owner, repo, asset_id: asset.id }); + } + } + + - name: Create GitHub Release + if: ${{ !inputs.is-prerelease }} + uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 + with: + name: OpenShell ${{ env.RELEASE_TAG }} + prerelease: false + tag_name: ${{ env.RELEASE_TAG }} + generate_release_notes: true + body: | + ## OpenShell ${{ env.RELEASE_TAG }} + + ### Quick install + + ```bash + curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_VERSION=${{ env.RELEASE_TAG }} sh + ``` + + files: | + release/openshell-x86_64-unknown-linux-musl.tar.gz + release/openshell-aarch64-unknown-linux-musl.tar.gz + release/openshell-aarch64-apple-darwin.tar.gz + release/openshell_*.deb + release/openshell-*.rpm + release/*.snap + release/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz + release/openshell-gateway-aarch64-unknown-linux-gnu.tar.gz + release/openshell-gateway-aarch64-apple-darwin.tar.gz + release/openshell-sandbox-x86_64-unknown-linux-musl.tar.gz + release/openshell-sandbox-aarch64-unknown-linux-musl.tar.gz + release/openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz + release/openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz + release/openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz + release/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz + release/openshell-driver-vm-aarch64-apple-darwin.tar.gz + release/openshell-prover-x86_64-unknown-linux-musl.tar.gz + release/openshell-prover-aarch64-unknown-linux-musl.tar.gz + release/openshell-prover-aarch64-apple-darwin.tar.gz + release/*.whl + release/openshell.rb + release/openshell-checksums-sha256.txt + release/openshell-gateway-checksums-sha256.txt + release/openshell-sandbox-checksums-sha256.txt + release/openshell-supervisor-checksums-sha256.txt + release/openshell-prover-checksums-sha256.txt + + - name: Upload amd64 Debian prerelease package + if: inputs.is-prerelease + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: openshell-${{ env.RELEASE_TAG }}-linux-amd64-deb + path: | + release/openshell_*_amd64.deb + release/openshell-checksums-sha256.txt + retention-days: 90 + if-no-files-found: error + + - name: Upload arm64 Debian prerelease package + if: inputs.is-prerelease + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: openshell-${{ env.RELEASE_TAG }}-linux-arm64-deb + path: | + release/openshell_*_arm64.deb + release/openshell-checksums-sha256.txt + retention-days: 90 + if-no-files-found: error + + - name: Upload x86_64 RPM prerelease packages + if: inputs.is-prerelease + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: openshell-${{ env.RELEASE_TAG }}-linux-x86_64-rpm + path: | + release/openshell-*.x86_64.rpm + release/openshell-checksums-sha256.txt + retention-days: 90 + if-no-files-found: error + + - name: Upload aarch64 RPM prerelease packages + if: inputs.is-prerelease + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: openshell-${{ env.RELEASE_TAG }}-linux-aarch64-rpm + path: | + release/openshell-*.aarch64.rpm + release/openshell-checksums-sha256.txt + retention-days: 90 + if-no-files-found: error + + - name: Upload macOS prerelease packages + if: inputs.is-prerelease + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 + with: + name: openshell-${{ env.RELEASE_TAG }}-macos-arm64 + path: | + release/openshell-aarch64-apple-darwin.tar.gz + release/openshell-gateway-aarch64-apple-darwin.tar.gz + release/openshell-driver-vm-aarch64-apple-darwin.tar.gz + release/openshell-prover-aarch64-apple-darwin.tar.gz + release/openshell.rb + release/openshell-checksums-sha256.txt + release/openshell-gateway-checksums-sha256.txt + release/openshell-prover-checksums-sha256.txt + retention-days: 90 + if-no-files-found: error + + publish-snap: + name: Publish Snap + needs: release + if: ${{ !inputs.is-prerelease }} + uses: ./.github/workflows/snap-publish.yml + with: + upload-channel: latest/stable + github-environment: latest/stable + secrets: + publish-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 + container: + image: ghcr.io/nvidia/openshell/ci:9cb72baa2e61a1b5f12407e6e82da7fdba0aa722@sha256:67a9a0c32cb99825e6d3e9d9eec45d67149ea1ff7c11a1b1e1b3480d2d3df684 + credentials: + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ inputs.source-sha }} + + - name: Mark workspace safe for git + run: git config --global --add safe.directory "$GITHUB_WORKSPACE" + + - name: Install tools + run: mise install --locked + + - 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 + + - name: Publish + env: + OPENSHELL_NPM_VERSION: ${{ inputs.npm-version }} + NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: mise run sdk:ts:publish + + 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}" + + publish-fern-docs: + name: Sync and Publish Fern Docs + needs: [release, publish-sdk-typescript, trigger-wheel-publish] + if: ${{ !inputs.is-prerelease }} + permissions: + contents: write + uses: ./.github/workflows/sync-docs.yml + with: + operation: sync + channel: stable + source_ref: ${{ inputs.source-sha }} + release_version: ${{ inputs.semver }} + version_slug: v${{ inputs.semver }} + display_name: Latest (v${{ inputs.semver }}) + publish: true + secrets: + FERN_TOKEN: ${{ secrets.FERN_TOKEN }} diff --git a/.github/workflows/release-qualification.yml b/.github/workflows/release-qualification.yml index e038fc040d..3dc0ca3c9e 100644 --- a/.github/workflows/release-qualification.yml +++ b/.github/workflows/release-qualification.yml @@ -18,13 +18,6 @@ on: description: Whether the candidate is a pre-release required: true type: boolean - outputs: - artifact-name: - description: Artifact containing the qualification result - value: ${{ jobs.qualification-result.outputs.artifact-name }} - current-profile-passed: - description: Whether every required qualification suite passed - value: ${{ jobs.qualification-result.outputs.current-profile-passed }} secrets: CODEX_SECURITY_API_KEY: required: true @@ -173,9 +166,7 @@ jobs: timeout-minutes: 5 permissions: contents: read - outputs: - artifact-name: ${{ steps.summary.outputs.artifact-name }} - current-profile-passed: ${{ steps.summary.outputs.current-profile-passed }} + packages: write steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -233,7 +224,44 @@ jobs: retention-days: 90 if-no-files-found: error - - name: Require current qualification profile before publication + - name: Install ORAS + id: oras + continue-on-error: true + timeout-minutes: 1 + uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2.0.0 + + - name: Log in to GHCR + id: ghcr + if: steps.oras.outcome == 'success' + continue-on-error: true + timeout-minutes: 1 + run: echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin + + - name: Publish qualification summary + id: oci + if: steps.ghcr.outcome == 'success' + continue-on-error: true + timeout-minutes: 1 + env: + SOURCE_SHA: ${{ inputs.source-sha }} + run: | + set -euo pipefail + VERSION="${RELEASE_TAG#v}" + ref="ghcr.io/nvidia/openshell/qualification:${VERSION}-run-${GITHUB_RUN_ID}-attempt-${GITHUB_RUN_ATTEMPT}" + oras push "${ref}" \ + --artifact-type application/vnd.nvidia.openshell.qualification.v1 \ + --annotation "org.opencontainers.image.revision=${SOURCE_SHA}" \ + --annotation "org.opencontainers.image.version=${VERSION}" \ + qualification-summary.json:application/vnd.nvidia.openshell.qualification.summary.v1+json + + echo "Published qualification summary to \`${ref}\`." >> "${GITHUB_STEP_SUMMARY}" + + - name: Warn if OCI summary publication failed + if: ${{ !cancelled() && (steps.oras.outcome == 'failure' || steps.ghcr.outcome == 'failure' || steps.oci.outcome == 'failure') }} + run: | + echo "::warning::Could not publish the qualification summary to OCI. The summary remains available in this run's Actions artifacts." + + - name: Require current qualification profile if: steps.summary.outputs.current-profile-passed != 'true' run: | echo "Release ${RELEASE_TAG} did not pass the current release-tag-v1 qualification profile." >&2 diff --git a/.github/workflows/release-tag.yml b/.github/workflows/release-tag.yml index f8c0b6771c..e3ac6f5947 100644 --- a/.github/workflows/release-tag.yml +++ b/.github/workflows/release-tag.yml @@ -1,774 +1,72 @@ -name: Release Tag +name: Release on: push: tags: - "v*.*.*" + - "!v*.*.*-pre.*" workflow_dispatch: inputs: tag: - description: "Git tag to release (e.g. v0.0.4)" + description: "Stable tag to release (e.g. v0.0.4)" required: true type: string permissions: contents: read - packages: read - -# Resolve the tag once: prefer the workflow_dispatch input, fall back to the -# push-event ref. Every job references this via env.RELEASE_TAG. -env: - RELEASE_TAG: ${{ inputs.tag || github.ref_name }} - -defaults: - run: - shell: bash jobs: - # --------------------------------------------------------------------------- - # Compute all versions once at the start to avoid git-describe race conditions - # --------------------------------------------------------------------------- - compute-versions: - name: Compute Versions - runs-on: linux-amd64-cpu8 - timeout-minutes: 5 - container: - image: ghcr.io/nvidia/openshell/ci:9cb72baa2e61a1b5f12407e6e82da7fdba0aa722@sha256:67a9a0c32cb99825e6d3e9d9eec45d67149ea1ff7c11a1b1e1b3480d2d3df684 - credentials: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - outputs: - python_version: ${{ steps.v.outputs.python }} - cargo_version: ${{ steps.v.outputs.cargo }} - npm_version: ${{ steps.v.outputs.npm }} - deb_version: ${{ steps.v.outputs.deb }} - rpm_version: ${{ steps.v.outputs.rpm_version }} - rpm_release: ${{ steps.v.outputs.rpm_release }} - # Semver without 'v' prefix (e.g. 0.6.0), used for image tags and release body - semver: ${{ steps.v.outputs.semver }} - # Commit resolved from RELEASE_TAG, used for image tags and downstream metadata - source_sha: ${{ steps.v.outputs.source_sha }} - is_prerelease: ${{ steps.v.outputs.is_prerelease }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - ref: ${{ inputs.tag || github.ref }} - - - name: Mark workspace safe for git - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - - name: Fetch tags - run: git fetch --tags --force - - - name: Compute all versions - id: v - run: | - set -euo pipefail - if [[ "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$ ]]; then - is_prerelease=true - elif [[ "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - is_prerelease=false - else - echo "Unsupported release tag: ${RELEASE_TAG}" >&2 - exit 1 - fi - { - echo "is_prerelease=${is_prerelease}" - echo "python=$(uv run python tasks/scripts/release.py get-version --python)" - echo "cargo=$(uv run python tasks/scripts/release.py get-version --cargo)" - echo "npm=$(uv run python tasks/scripts/release.py get-version --npm)" - echo "deb=$(uv run python tasks/scripts/release.py get-version --deb)" - echo "rpm_version=$(uv run python tasks/scripts/release.py get-version --rpm-version)" - echo "rpm_release=$(uv run python tasks/scripts/release.py get-version --rpm-release)" - echo "semver=${RELEASE_TAG#v}" - echo "source_sha=$(git rev-parse HEAD)" - } >> "$GITHUB_OUTPUT" - - build-binaries: - needs: compute-versions - permissions: - contents: read - uses: ./.github/workflows/build-binaries.yml - with: - cargo-version: ${{ needs.compute-versions.outputs.cargo_version }} - supervisor-image-tag: ${{ needs.compute-versions.outputs.semver }} - checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} - secrets: - CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - - build-vm-driver: - needs: [compute-versions, build-binaries] + build: + name: Build permissions: + actions: read contents: read - uses: ./.github/workflows/build-vm-driver.yml + packages: write + uses: ./.github/workflows/release-build.yml with: - cargo-version: ${{ needs.compute-versions.outputs.cargo_version }} - supervisor-image-tag: ${{ needs.compute-versions.outputs.semver }} - checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} + release-tag: ${{ inputs.tag || github.ref_name }} + is-prerelease: false secrets: CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - package-binaries: - needs: [compute-versions, build-binaries, build-vm-driver] + qualify: + name: Qualify + needs: build permissions: actions: read contents: read - uses: ./.github/workflows/package-release-binaries.yml - with: - checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} - - build-images: - needs: [compute-versions, build-binaries] - permissions: - contents: read packages: write - uses: ./.github/workflows/build-images.yml - with: - image-tag: ${{ needs.compute-versions.outputs.source_sha }} - checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} - - qualification-result: - name: Release Qualification - if: always() && needs.compute-versions.result == 'success' - needs: [compute-versions, build-binaries, build-vm-driver, build-images, build-deb, build-rpm] - permissions: - actions: read - contents: read - packages: read security-events: write pull-requests: read uses: ./.github/workflows/release-qualification.yml with: release-tag: ${{ inputs.tag || github.ref_name }} - source-sha: ${{ needs.compute-versions.outputs.source_sha }} - is-prerelease: ${{ needs.compute-versions.outputs.is_prerelease == 'true' }} + source-sha: ${{ needs.build.outputs.source_sha }} + is-prerelease: false secrets: CODEX_SECURITY_API_KEY: ${{ secrets.CODEX_SECURITY_API_KEY }} CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} - build-python-wheel: - name: Build Python Wheel - needs: [compute-versions] - runs-on: linux-amd64-cpu8 - timeout-minutes: 20 - container: - image: ghcr.io/nvidia/openshell/ci:9cb72baa2e61a1b5f12407e6e82da7fdba0aa722@sha256:67a9a0c32cb99825e6d3e9d9eec45d67149ea1ff7c11a1b1e1b3480d2d3df684 - credentials: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - env: - MISE_GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SETUPTOOLS_SCM_PRETEND_VERSION_FOR_OPENSHELL: ${{ needs.compute-versions.outputs.python_version }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - fetch-depth: 0 - persist-credentials: false - ref: ${{ needs.compute-versions.outputs.source_sha }} - - - name: Mark workspace safe for git - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - - name: Sync Python dependencies - run: uv sync - - - name: Build Python wheel - run: | - set -euo pipefail - mise run python:build - ls -la target/wheels/*.whl - - - name: Upload wheel artifact - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: python-wheel - path: target/wheels/*.whl - retention-days: 5 - - build-deb: - name: Build Debian Packages - needs: [compute-versions, build-binaries, build-vm-driver] - uses: ./.github/workflows/deb-package.yml - with: - deb-version: ${{ needs.compute-versions.outputs.deb_version }} - checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} - - build-snap: - name: Build Snap - needs: [compute-versions, build-binaries] - uses: ./.github/workflows/snap-package.yml - with: - checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} - - build-rpm: - name: Build RPM Packages - needs: [compute-versions, build-binaries] - uses: ./.github/workflows/rpm-package.yml - with: - checkout-ref: ${{ needs.compute-versions.outputs.source_sha }} - rpm-version: ${{ needs.compute-versions.outputs.rpm_version }} - rpm-release: ${{ needs.compute-versions.outputs.rpm_release }} - cargo-version: ${{ needs.compute-versions.outputs.cargo_version }} - - publish-snap: - name: Publish Snap - needs: [compute-versions, release, qualification-result] - if: needs.compute-versions.outputs.is_prerelease != 'true' && needs.qualification-result.outputs.current-profile-passed == 'true' - uses: ./.github/workflows/snap-publish.yml - with: - upload-channel: latest/stable - github-environment: latest/stable - secrets: - publish-credentials: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} - - # --------------------------------------------------------------------------- - # Assemble release artifacts after the current qualification profile passes. - # --------------------------------------------------------------------------- - release: - name: Release - if: needs.qualification-result.outputs.current-profile-passed == 'true' - needs: - - compute-versions - - package-binaries - - build-python-wheel - - qualification-result - - build-deb - - build-rpm - - build-snap - runs-on: linux-amd64-cpu8 - timeout-minutes: 10 + publish: + name: Publish + needs: [build, qualify] permissions: + actions: read contents: write + packages: write id-token: write attestations: write artifact-metadata: write - outputs: - wheel_filenames: ${{ steps.wheel_filenames.outputs.wheel_filenames }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ needs.compute-versions.outputs.source_sha }} - - - name: Download all CLI artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: cli-* - path: release/ - merge-multiple: true - - - name: Download gateway binary artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: gateway-binary-* - path: release/ - merge-multiple: true - - - name: Download supervisor binary artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: supervisor-binary-* - path: release/ - merge-multiple: true - - - name: Download sandbox binary artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: sandbox-binary-* - path: release/ - merge-multiple: true - - - name: Download VM driver artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: driver-vm-* - path: release/ - merge-multiple: true - - - name: Download prover binary artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: prover-binary-* - path: release/ - merge-multiple: true - - - name: Download prover checksums - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: prover-checksums - path: release/ - - - name: Download wheel artifact - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: python-wheel - path: release/ - - - name: Download Debian package artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: deb-linux-* - path: release/ - merge-multiple: true - - - name: Download RPM package artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: rpm-linux-* - path: release/ - merge-multiple: true - - - name: Download snap artifacts - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - pattern: snap-linux-* - path: release/ - merge-multiple: true - - - name: Capture wheel filenames - id: wheel_filenames - run: | - set -euo pipefail - shopt -s nullglob - wheels=(release/*.whl) - if [ "${#wheels[@]}" -ne 1 ]; then - echo "expected exactly one Python wheel, found ${#wheels[@]}" >&2 - exit 1 - fi - wheel_filename=$(basename "${wheels[0]}") - echo "wheel_filenames=${wheel_filename}" >> "$GITHUB_OUTPUT" - - - name: Generate checksums - run: | - set -euo pipefail - cd release - sha256sum -- \ - openshell-x86_64-unknown-linux-musl.tar.gz \ - openshell-aarch64-unknown-linux-musl.tar.gz \ - openshell-aarch64-apple-darwin.tar.gz \ - openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz \ - openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz \ - openshell-driver-vm-aarch64-apple-darwin.tar.gz \ - openshell_*.deb \ - openshell-*.rpm \ - *.whl > openshell-checksums-sha256.txt - cat openshell-checksums-sha256.txt - sha256sum -- \ - openshell-gateway-x86_64-unknown-linux-gnu.tar.gz \ - openshell-gateway-aarch64-unknown-linux-gnu.tar.gz \ - openshell-gateway-aarch64-apple-darwin.tar.gz > openshell-gateway-checksums-sha256.txt - cat openshell-gateway-checksums-sha256.txt - sha256sum -- \ - openshell-sandbox-x86_64-unknown-linux-musl.tar.gz \ - openshell-sandbox-aarch64-unknown-linux-musl.tar.gz > openshell-sandbox-checksums-sha256.txt - cat openshell-sandbox-checksums-sha256.txt - sha256sum -- \ - openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz \ - openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz > openshell-supervisor-checksums-sha256.txt - cat openshell-supervisor-checksums-sha256.txt - - - name: Generate Homebrew formula - run: | - set -euo pipefail - python3 tasks/scripts/release.py generate-homebrew-formula \ - --release-tag "${RELEASE_TAG}" \ - --release-dir release \ - --output release/openshell.rb - cat release/openshell.rb - - - name: Attest release artifacts - id: attest - uses: actions/attest@1e69f48acb82d1966a394da916b4c1698aa569d6 # v4.2.2 - with: - subject-path: | - release/*.tar.gz - release/*.deb - release/*.rpm - release/*.whl - - - name: Prune removed VM checksum asset - if: needs.compute-versions.outputs.is_prerelease != 'true' - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9 - with: - script: | - const [owner, repo] = process.env.GITHUB_REPOSITORY.split('/'); - let release; - try { - release = await github.rest.repos.getReleaseByTag({ owner, repo, tag: process.env.RELEASE_TAG }); - } catch (err) { - if (err.status === 404) { - core.info(`No existing ${process.env.RELEASE_TAG} release; skipping VM checksum pruning.`); - return; - } - throw err; - } - for (const asset of release.data.assets) { - if (asset.name === 'openshell-driver-vm-checksums-sha256.txt') { - core.info(`Deleting removed VM checksum asset: ${asset.name}`); - await github.rest.repos.deleteReleaseAsset({ owner, repo, asset_id: asset.id }); - } - } - - - name: Create GitHub Release - if: needs.compute-versions.outputs.is_prerelease != 'true' - uses: softprops/action-gh-release@3d0d9888cb7fd7b750713d6e236d1fcb99157228 # v3.0.2 - with: - name: OpenShell ${{ env.RELEASE_TAG }} - prerelease: false - tag_name: ${{ env.RELEASE_TAG }} - generate_release_notes: true - body: | - ## OpenShell ${{ env.RELEASE_TAG }} - - ### Quick install - - ```bash - curl -LsSf https://raw.githubusercontent.com/NVIDIA/OpenShell/main/install.sh | OPENSHELL_VERSION=${{ env.RELEASE_TAG }} sh - ``` - - files: | - release/openshell-x86_64-unknown-linux-musl.tar.gz - release/openshell-aarch64-unknown-linux-musl.tar.gz - release/openshell-aarch64-apple-darwin.tar.gz - release/openshell_*.deb - release/openshell-*.rpm - release/*.snap - release/openshell-gateway-x86_64-unknown-linux-gnu.tar.gz - release/openshell-gateway-aarch64-unknown-linux-gnu.tar.gz - release/openshell-gateway-aarch64-apple-darwin.tar.gz - release/openshell-sandbox-x86_64-unknown-linux-musl.tar.gz - release/openshell-sandbox-aarch64-unknown-linux-musl.tar.gz - release/openshell-supervisor-x86_64-unknown-linux-gnu.tar.gz - release/openshell-supervisor-aarch64-unknown-linux-gnu.tar.gz - release/openshell-driver-vm-x86_64-unknown-linux-gnu.tar.gz - release/openshell-driver-vm-aarch64-unknown-linux-gnu.tar.gz - release/openshell-driver-vm-aarch64-apple-darwin.tar.gz - release/openshell-prover-x86_64-unknown-linux-musl.tar.gz - release/openshell-prover-aarch64-unknown-linux-musl.tar.gz - release/openshell-prover-aarch64-apple-darwin.tar.gz - release/*.whl - release/openshell.rb - release/openshell-checksums-sha256.txt - release/openshell-gateway-checksums-sha256.txt - release/openshell-sandbox-checksums-sha256.txt - release/openshell-supervisor-checksums-sha256.txt - release/openshell-prover-checksums-sha256.txt - - - name: Upload amd64 Debian prerelease package - if: needs.compute-versions.outputs.is_prerelease == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: openshell-${{ env.RELEASE_TAG }}-linux-amd64-deb - path: | - release/openshell_*_amd64.deb - release/openshell-checksums-sha256.txt - retention-days: 90 - if-no-files-found: error - - - name: Upload arm64 Debian prerelease package - if: needs.compute-versions.outputs.is_prerelease == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: openshell-${{ env.RELEASE_TAG }}-linux-arm64-deb - path: | - release/openshell_*_arm64.deb - release/openshell-checksums-sha256.txt - retention-days: 90 - if-no-files-found: error - - - name: Upload x86_64 RPM prerelease packages - if: needs.compute-versions.outputs.is_prerelease == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: openshell-${{ env.RELEASE_TAG }}-linux-x86_64-rpm - path: | - release/openshell-*.x86_64.rpm - release/openshell-checksums-sha256.txt - retention-days: 90 - if-no-files-found: error - - - name: Upload aarch64 RPM prerelease packages - if: needs.compute-versions.outputs.is_prerelease == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: openshell-${{ env.RELEASE_TAG }}-linux-aarch64-rpm - path: | - release/openshell-*.aarch64.rpm - release/openshell-checksums-sha256.txt - retention-days: 90 - if-no-files-found: error - - - name: Upload macOS prerelease packages - if: needs.compute-versions.outputs.is_prerelease == 'true' - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: openshell-${{ env.RELEASE_TAG }}-macos-arm64 - path: | - release/openshell-aarch64-apple-darwin.tar.gz - release/openshell-gateway-aarch64-apple-darwin.tar.gz - release/openshell-driver-vm-aarch64-apple-darwin.tar.gz - release/openshell-prover-aarch64-apple-darwin.tar.gz - release/openshell.rb - release/openshell-checksums-sha256.txt - release/openshell-gateway-checksums-sha256.txt - release/openshell-prover-checksums-sha256.txt - retention-days: 90 - if-no-files-found: error - - # --------------------------------------------------------------------------- - # Publish OCI artifacts. build-images creates the commit-addressed candidate - # images; these jobs add versioned image tags and charts, and publish the - # qualification summary independently from the release artifacts. - # --------------------------------------------------------------------------- - tag-ghcr-release: - name: Tag GHCR Images - needs: [compute-versions, build-images, release] - runs-on: linux-amd64-cpu8 - timeout-minutes: 10 - permissions: - packages: write - steps: - - name: Log in to GHCR - run: echo "${{ secrets.GITHUB_TOKEN }}" | docker login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin - - - name: Tag images with release version - env: - IS_PRERELEASE: ${{ needs.compute-versions.outputs.is_prerelease }} - SOURCE_TAG: ${{ needs.compute-versions.outputs.source_sha }} - VERSION: ${{ needs.compute-versions.outputs.semver }} - run: | - set -euo pipefail - REGISTRY="ghcr.io/nvidia/openshell" - for component in gateway sandbox supervisor; do - echo "Tagging ${REGISTRY}/${component}:${SOURCE_TAG} as ${VERSION}..." - docker buildx imagetools create \ - --prefer-index=false \ - -t "${REGISTRY}/${component}:${VERSION}" \ - "${REGISTRY}/${component}:${SOURCE_TAG}" - if [[ "${IS_PRERELEASE}" != "true" ]]; then - echo "Tagging ${REGISTRY}/${component}:${SOURCE_TAG} as latest..." - docker buildx imagetools create \ - --prefer-index=false \ - -t "${REGISTRY}/${component}:latest" \ - "${REGISTRY}/${component}:${SOURCE_TAG}" - fi - done - - release-helm: - name: Release Helm Chart (OCI) - needs: [compute-versions, release, tag-ghcr-release] - runs-on: ubuntu-latest - timeout-minutes: 10 - permissions: - packages: write - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ needs.compute-versions.outputs.source_sha }} - - - uses: ./.github/actions/release-helm-oci - with: - chart-version: ${{ needs.compute-versions.outputs.semver }} - app-version: ${{ needs.compute-versions.outputs.semver }} - release-kind: public - - publish-qualification: - name: Publish Qualification Summary (OCI) - if: >- - !cancelled() - && needs.release.result == 'success' - && needs.qualification-result.result == 'success' - && needs.qualification-result.outputs.current-profile-passed == 'true' - needs: [compute-versions, qualification-result, release] - runs-on: ubuntu-latest - timeout-minutes: 5 - permissions: - actions: read - contents: read - packages: write - steps: - - name: Download qualification result - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 - with: - name: ${{ needs.qualification-result.outputs.artifact-name }} - - - name: Install ORAS - uses: oras-project/setup-oras@1d808f7d7f6995cc68b7bf507bfe5c5446e1dc9d # v2.0.0 - - - name: Log in to GHCR - run: echo "${{ secrets.GITHUB_TOKEN }}" | oras login ghcr.io -u "${GITHUB_ACTOR}" --password-stdin - - - name: Publish qualification summary - env: - SOURCE_SHA: ${{ needs.compute-versions.outputs.source_sha }} - VERSION: ${{ needs.compute-versions.outputs.semver }} - run: | - set -euo pipefail - ref="ghcr.io/nvidia/openshell/qualification:${VERSION}-run-${GITHUB_RUN_ID}-attempt-${GITHUB_RUN_ATTEMPT}" - oras push "${ref}" \ - --artifact-type application/vnd.nvidia.openshell.qualification.v1 \ - --annotation "org.opencontainers.image.revision=${SOURCE_SHA}" \ - --annotation "org.opencontainers.image.version=${VERSION}" \ - qualification-summary.json:application/vnd.nvidia.openshell.qualification.summary.v1+json - - echo "Published qualification summary to \`${ref}\`." >> "${GITHUB_STEP_SUMMARY}" - - notify-prerelease-failure: - name: Notify Prerelease Failure - needs: [release-helm, publish-qualification] - if: failure() && contains(inputs.tag || github.ref_name, '-pre.') - runs-on: ubuntu-latest - timeout-minutes: 2 - permissions: {} - steps: - - name: Send Slack notification - continue-on-error: true - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL }} - SLACK_MENTION: ${{ secrets.SLACK_OPENSHELL_TRIAGE_MENTION }} - RUN_URL: ${{ format('{0}/{1}/actions/runs/{2}/attempts/{3}', github.server_url, github.repository, github.run_id, github.run_attempt) }} - run: | - set -euo pipefail - if [[ -z "${SLACK_WEBHOOK_URL}" ]]; then - echo "::notice::SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL is unset; skipping Slack notification." - exit 0 - fi - - message=":x: OpenShell prerelease ${RELEASE_TAG} failed (attempt ${GITHUB_RUN_ATTEMPT})." - if [[ -n "${SLACK_MENTION}" ]]; then - message+=" ${SLACK_MENTION}" - fi - jq -n --arg text "${message}" --arg run_url "${RUN_URL}" \ - '{text: ($text + "\n<" + $run_url + "|View failed release run>"), unfurl_links: false, unfurl_media: false}' | - curl --fail --silent --show-error --connect-timeout 5 --max-time 15 \ - --header 'Content-Type: application/json' --data-binary @- "${SLACK_WEBHOOK_URL}" - - notify-prerelease-success: - name: Notify Prerelease Success - needs: [release-helm, publish-qualification] - if: success() && contains(inputs.tag || github.ref_name, '-pre.') - runs-on: ubuntu-latest - timeout-minutes: 2 - permissions: {} - steps: - - name: Send Slack notification - continue-on-error: true - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL }} - RUN_URL: ${{ format('{0}/{1}/actions/runs/{2}/attempts/{3}', github.server_url, github.repository, github.run_id, github.run_attempt) }} - TAG_URL: ${{ format('{0}/{1}/tree/{2}', github.server_url, github.repository, inputs.tag || github.ref_name) }} - run: | - set -euo pipefail - if [[ -z "${SLACK_WEBHOOK_URL}" ]]; then - echo "::notice::SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL is unset; skipping Slack notification." - exit 0 - fi - - jq -n --arg tag "${RELEASE_TAG}" --arg tag_url "${TAG_URL}" \ - --arg attempt "${GITHUB_RUN_ATTEMPT}" --arg run_url "${RUN_URL}" \ - '{text: (":white_check_mark: OpenShell prerelease <" + $tag_url + "|" + $tag + "> succeeded (attempt " + $attempt + ").\n<" + $run_url + "|View release run and artifacts>"), unfurl_links: false, unfurl_media: false}' | - curl --fail --silent --show-error --connect-timeout 5 --max-time 15 \ - --header 'Content-Type: application/json' --data-binary @- "${SLACK_WEBHOOK_URL}" - - publish-fern-docs: - name: Sync and Publish Fern Docs - needs: [compute-versions, release, publish-sdk-typescript, release-helm, trigger-wheel-publish] - if: needs.compute-versions.outputs.is_prerelease != 'true' - permissions: - contents: write - uses: ./.github/workflows/sync-docs.yml + uses: ./.github/workflows/release-publish.yml with: - operation: sync - channel: stable - source_ref: ${{ needs.compute-versions.outputs.source_sha }} - release_version: ${{ needs.compute-versions.outputs.semver }} - version_slug: v${{ needs.compute-versions.outputs.semver }} - display_name: Latest (v${{ needs.compute-versions.outputs.semver }}) - publish: true + release-tag: ${{ inputs.tag || github.ref_name }} + source-sha: ${{ needs.build.outputs.source_sha }} + semver: ${{ needs.build.outputs.semver }} + python-version: ${{ needs.build.outputs.python_version }} + npm-version: ${{ needs.build.outputs.npm_version }} + is-prerelease: false secrets: + SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} FERN_TOKEN: ${{ secrets.FERN_TOKEN }} - - publish-sdk-typescript: - name: Publish TypeScript SDK - needs: [compute-versions, release] - if: needs.compute-versions.outputs.is_prerelease != 'true' - runs-on: linux-amd64-cpu8 - timeout-minutes: 15 - permissions: - contents: read - packages: write - container: - image: ghcr.io/nvidia/openshell/ci:9cb72baa2e61a1b5f12407e6e82da7fdba0aa722@sha256:67a9a0c32cb99825e6d3e9d9eec45d67149ea1ff7c11a1b1e1b3480d2d3df684 - credentials: - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - steps: - - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: - persist-credentials: false - ref: ${{ needs.compute-versions.outputs.source_sha }} - - - name: Mark workspace safe for git - run: git config --global --add safe.directory "$GITHUB_WORKSPACE" - - - name: Install tools - run: mise install --locked - - - 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 - - - name: Publish - env: - OPENSHELL_NPM_VERSION: ${{ needs.compute-versions.outputs.npm_version }} - NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: mise run sdk:ts:publish - - trigger-wheel-publish: - name: Trigger Wheel Publish - needs: [compute-versions, release] - if: needs.compute-versions.outputs.is_prerelease != 'true' - 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: ${{ needs.compute-versions.outputs.source_sha }} - RELEASE_VERSION: ${{ needs.compute-versions.outputs.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}" + GITLAB_CI_TRIGGER_TOKEN: ${{ secrets.GITLAB_CI_TRIGGER_TOKEN }} + GITLAB_CI_TRIGGER_URL: ${{ secrets.GITLAB_CI_TRIGGER_URL }} diff --git a/AGENTS.md b/AGENTS.md index f703fd2424..0a21595b85 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -226,7 +226,7 @@ When behavior, commands, or development workflows change, review the related age - When changing gateway TOML fields, driver-specific config options, config defaults, or Helm rendering of `gateway.toml`, update `docs/how-it-works/gateways/configuration.mdx` in the same branch. - `fern/` contains the Fern site config, components, preview workflow inputs, publish settings, and publishing documentation in `fern/README.md`. - Follow the docs style guide in [docs/CONTRIBUTING.mdx](docs/CONTRIBUTING.mdx): active voice, minimal formatting, no filler introductions, `shell` fences for copyable commands, and no duplicate body H1. -- Fern PR previews run through `.github/workflows/branch-docs.yml`. Release Dev publishes `dev`, and Release Tag publishes an immutable stable version plus `latest`. Both production paths call `.github/workflows/sync-docs.yml` once. +- Fern PR previews run through `.github/workflows/branch-docs.yml`. Release Dev publishes `dev`, and Release publishes an immutable stable version plus `latest`. Both production paths call `.github/workflows/sync-docs.yml` once. - Use the `update-docs-from-commits` skill to scan recent commits and draft doc updates. ## Terminology diff --git a/CI.md b/CI.md index b9c1e5caf1..b886bd70e3 100644 --- a/CI.md +++ b/CI.md @@ -19,7 +19,7 @@ Merge queue validation is a second integration gate for `main`. After a PR has p ### Protobuf API compatibility -`Protobuf Compatibility` runs in Branch Checks and Release Tag through the shared +`Protobuf Compatibility` runs in Branch Checks, Release, and Pre-release through the shared `check-protobuf-compatibility` action and Nix app. The app supplies Python, Buf, and Git from the flake lockfile. It uses Buf's `FILE` policy for the `proto/` @@ -40,10 +40,11 @@ Commit messages do not affect this decision. No active train also rejects breaking changes. Allowed findings remain visible as warnings; schema errors, missing baselines, merge conflicts, and tool failures always fail the check. -Release Tag compares the tagged candidate cumulatively against the previous +Release qualification compares the tagged candidate cumulatively against the previous stable release, using the same minor-versus-patch policy. Its result is part -of the qualification profile: failure blocks both pre-release and stable -publication. Failed candidates retain build artifacts and evidence in Actions. This checks +of the qualification profile: failure blocks stable publication and fails the +Pre-release workflow without blocking pre-release publication. Qualification evidence +remains in Actions. This checks protobuf compatibility; SDK/configuration compatibility and migration review remain separate qualification work. @@ -149,7 +150,7 @@ agent gate is separate work after calibration. selected. DEB and RPM packages build only when integration qualification is selected. Core integration qualification builds and installs the DEB on Ubuntu with Docker and installs the CLI and gateway RPMs on Fedora with rootful and rootless Podman. These lanes run conformance using the matching runtime -images. Release Dev and Release Tag use the same package installers. +images. Release Dev, Release, and Pre-release use the same package installers. Fedora provider-refresh tests also use RPMs. The Podman driver-specific suites retain the binary installer because their fixtures configure its system service, local HTTP gateway, and CLI path. The manual Integration Tests workflow defaults @@ -189,7 +190,7 @@ diagnostics as `tmachine-diagnostics-*` artifacts. ### Run only the policy advisor conformance tests Manually dispatch `Integration Tests` on the candidate branch with an -`artifact-run-id` from a build of the same commit. When a `Release Tag` run was +`artifact-run-id` from a build of the same commit. When a `Release` or `Pre-release` run was dispatched from a different commit, also set `source-sha` to the release tag's resolved commit so runtime images and test inputs match the candidate binaries. Otherwise, `source-sha` defaults to the artifact run's head SHA. Set `category` @@ -269,13 +270,15 @@ candidate snapshot. Cargo Deny uses its existing NVIDIA self-hosted runner and CI container. Tagged releases treat Cargo Deny findings as failures of the -currently implemented qualification profile. A profile failure prevents both -pre-release and stable publication. CodeQL, Trivy, and Zizmor findings are +currently implemented qualification profile. A profile failure prevents stable +publication. Pre-release publication runs alongside qualification; failed qualification fails the parent +workflow and sends the failure notification while published artifacts remain +available. CodeQL, Trivy, and Zizmor findings are temporarily informational for tagged releases: the existing findings were reviewed and accepted for v0.1.0 and will be addressed in 0.1.x releases. Scanner failures still fail qualification. -Failed pre-release builds send notifications via Slack using a webhook and +Failed pre-release builds, publication, or qualification send notifications via Slack using a webhook and at-mentioning the triage engineer with a link to the failure. The webhook is stored in the repository secret `SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL` and the mention is stored in the repository secret `SLACK_OPENSHELL_TRIAGE_MENTION`. @@ -291,8 +294,9 @@ gh workflow run security-scan.yml --ref main \ ``` To integrate it into a larger workflow, run it after the job that creates the -candidate tag and stages source-addressed artifacts for scanning. Release-facing -publication follows successful qualification. This example assumes an existing +candidate tag and stages source-addressed artifacts for scanning. Stable +publication follows successful qualification; pre-release publication runs alongside it. +This example assumes an existing `build` job with outputs named `candidate_tag`, `gateway_image`, `sandbox_image`, and `chart_ref`; adapt those names to your workflow: @@ -318,17 +322,17 @@ jobs: ``` Set `needs: security` on a downstream promotion job to require successful scans. -The tagged release workflow calls `release-qualification.yml` to run and record -protobuf, security, and integration checks after building its candidate artifacts -and publishing its commit-addressed OCI images. A failed -check remains visible in the workflow, with build artifacts and evidence retained -in Actions storage. Both pre-release and stable publication require the implemented -`release-tag-v1` profile to pass; that profile is an incremental subset of RFC -0014 qualification. +`Release` and `Pre-release` share `release-build.yml`, `release-publish.yml`, and +`release-qualification.yml`. Release builds, qualifies, then publishes. Pre-release +builds, then publishes and qualifies 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 implemented `release-tag-v1` profile is an incremental subset of RFC 0014 +qualification. Failed checks fail the parent workflow and retain evidence in Actions. Each qualification attempt writes its result to the Actions run summary and -uploads `qualification-summary.json` as a 90-day workflow artifact. After the -candidate release is published, the workflow publishes the same summary to +uploads `qualification-summary.json` as a 90-day workflow artifact. The qualification +workflow attempts to publish passing and failing summaries to `ghcr.io/nvidia/openshell/qualification:-run--attempt-`. The summary contains qualification results and policy coverage only; artifact identity belongs in the release manifest. The run ID and attempt distinguish @@ -576,10 +580,13 @@ These workflows run after merge to publish dev/tagged artifacts and verify them. | File | Role | |---|---| | `.github/workflows/release-dev.yml` | Publishes the rolling `dev` build on every push to `main`. Builds gateway, sandbox, and supervisor images and binaries, packages, wheels, and pushes the Helm chart as `oci://ghcr.io/nvidia/openshell/helm-chart:0.0.0-dev` (plus an immutable `0.0.0-dev.` pin). Also dispatchable manually. | -| `.github/workflows/release-tag.yml` | Publishes stable releases and pre-releases on tag pushes or manual dispatch. Release Auto-Tag dispatches it explicitly because its `GITHUB_TOKEN` pushes do not trigger workflows. Both require the currently implemented qualification profile to pass before publication; the summary identifies the remaining RFC 0014 coverage. Failed candidates retain build artifacts and evidence in Actions storage. Source-SHA OCI images remain available as qualification inputs. | +| `.github/workflows/release-tag.yml` | Release orchestrator for stable tags: build, qualify, publish. Automatic tag triggers exclude `-pre.*`. | +| `.github/workflows/release-prerelease.yml` | Pre-release orchestrator, triggered by prerelease tag pushes or dispatched by Release Auto-Tag or manually: build, then publish and qualify in parallel. Qualification failure fails the run and sends the failure notification; published artifacts remain available. | +| `.github/workflows/release-build.yml` | Shared version computation, binary and image builds, and package builds. Validates that the tag matches the caller's release kind. | +| `.github/workflows/release-publish.yml` | Shared artifact assembly, OCI image tags and Helm chart publication, and package publication. Stable releases also publish to GitHub Releases, snap, npm, PyPI, and Fern. | | `.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 and Release Tag 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 Tag calls it for `latest/stable` only for stable releases, after qualification and release assembly succeed. | +| `.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/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/CONTRIBUTING.md b/CONTRIBUTING.md index 9f05b5fd50..fd6d69cda2 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -382,7 +382,7 @@ mise run docs PRs that touch `docs/**` or `fern/**` are validated by `.github/workflows/branch-docs.yml`, and they get a preview when `FERN_TOKEN` is available to the workflow. -Release Dev publishes the `dev` docs version from `main`. Release Tag publishes an immutable stable version and updates `latest`. See [fern/README.md](fern/README.md) for the source layout, version model, and publishing workflows. +Release Dev publishes the `dev` docs version from `main`. Release publishes an immutable stable version and updates `latest`. See [fern/README.md](fern/README.md) for the source layout, version model, and publishing workflows. `docs/` is the source-of-truth docs tree. `fern/` contains the site configuration, components, theme assets, and its README. diff --git a/docs/about/installation.mdx b/docs/about/installation.mdx index 2a2057ddb6..638e994da6 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 artifact from a successful release run for the current platform, and downloads only that artifact. 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, prereleases and explicit release tags use Debian or RPM packages. The rolling [`dev` release](https://github.com/NVIDIA/OpenShell/releases/tag/dev) does not require GitHub authentication: diff --git a/docs/about/support-matrix.mdx b/docs/about/support-matrix.mdx index 25546e0302..db6794d192 100644 --- a/docs/about/support-matrix.mdx +++ b/docs/about/support-matrix.mdx @@ -19,9 +19,10 @@ the release cycle. Use a stable release for production deployments. | Pre-release | Built nightly when `main` has changed and normal CI passes. Versions use the form `X.Y.Z-pre.N`. | Validating the immutable artifact set proposed for the next stable release. Pre-releases use the stable feature set but may not have passed qualification. | | Stable | Promoted from a pre-release that passes conformance, upgrade, API compatibility, artifact, and security checks. Versions use the form `X.Y.Z`. | Production use within this support matrix. | -The tagged release pipeline publishes pre-release and stable artifacts only -after its current qualification profile passes. Failed candidates retain build -artifacts and qualification evidence in CI storage. The current profile covers +Pre-release publication does not depend on qualification. Failed qualification marks the run +as failed while published packages, images, and charts remain available. +Stable artifacts publish only after the current qualification profile passes. +Qualification evidence remains in CI storage. The current profile covers part of the complete release qualification policy; passing it does not establish full RFC 0014 coverage. diff --git a/docs/security/verify-image-contents.mdx b/docs/security/verify-image-contents.mdx index 4b9704d3b5..948caec03f 100644 --- a/docs/security/verify-image-contents.mdx +++ b/docs/security/verify-image-contents.mdx @@ -29,7 +29,7 @@ The same commands work for `ghcr.io/nvidia/openshell/supervisor`. ## Coverage -Every SBOM lists the base-image packages. Release Dev and Release Tag images also list the Rust crates compiled into their OpenShell binary. +Every SBOM lists the base-image packages. Release Dev, Release, and Pre-release images also list the Rust crates compiled into their OpenShell binary. OpenShell also publishes minimal SLSA provenance. It records how BuildKit produced the image, including its source revision, build platform, and base-image materials, without the extra build parameters included by full provenance. diff --git a/fern/README.md b/fern/README.md index f3a0671d7b..71c26fa1e2 100644 --- a/fern/README.md +++ b/fern/README.md @@ -49,15 +49,15 @@ The automated site uses these version types: Release Dev waits for the development artifacts and Helm chart, then calls `.github/workflows/sync-docs.yml` once. The reusable workflow updates `dev`, validates the generated site, commits and pushes the branch when needed, and publishes the production site once. -Release Tag follows the same sequence for a non-prerelease tag after the release artifacts, SDK package, Helm chart, and wheel publication complete. One sync copies the tagged source into both the immutable `vX.Y.Z` snapshot and `latest` when the release is not older than the current version, then publishes the production site once. The version selector pins `latest` and `dev` first, followed by versioned snapshots in descending version order. Other historical entries follow in their existing order. +Release follows the same sequence for a stable tag through `release-publish.yml` after the release artifacts, SDK package, Helm chart, and wheel publication complete. One sync copies the tagged source into both the immutable `vX.Y.Z` snapshot and `latest` when the release is not older than the current version, then publishes the production site once. The version selector pins `latest` and `dev` first, followed by versioned snapshots in descending version order. Other historical entries follow in their existing order. -The sync workflow still accepts an optional Fern availability badge, but the current Release Dev and Release Tag jobs do not request one. The next `dev` sync replaces the existing `dev` Beta badge with an unbadged entry. Other entries retain their badge setting until that entry is synced or removed. +The sync workflow still accepts an optional Fern availability badge, but the current Release Dev and Release jobs do not request one. The next `dev` sync replaces the existing `dev` Beta badge with an unbadged entry. Other entries retain their badge setting until that entry is synced or removed. The sync and publish workflows share the `docs-website` concurrency group. This serializes writes and publication. Queued runs remain pending instead of replacing one another. The `dev` snapshot also owns the shared Fern configuration, components, assets, and CSS on `docs-website`. The `latest` snapshot copies its documentation, navigation, and redirects but does not replace shared components, assets, or CSS. This keeps the site configuration aligned with `main` while preserving the released content. -A `dev` sync copies the top-level `announcement` from the source `fern/docs.yml`. This announcement is the global fallback, and removing it from the source removes it from `docs-website`. Each snapshot sync copies the source version announcement only to the channel being updated. A version announcement overrides the global announcement for that version, so Release Dev cannot change the `latest` announcement and Release Tag cannot change the `dev` announcement. +A `dev` sync copies the top-level `announcement` from the source `fern/docs.yml`. This announcement is the global fallback, and removing it from the source removes it from `docs-website`. Each snapshot sync copies the source version announcement only to the channel being updated. A version announcement overrides the global announcement for that version, so Release Dev cannot change the `latest` announcement and Release cannot change the `dev` announcement. Redirects are synchronized with their mutable snapshot. A redirect whose source starts with `/openshell/dev/` or `/openshell/latest/` belongs to that channel. An unversioned alias to a versioned destination belongs to the destination channel; other shared rules belong to `dev`. Each sync replaces that channel's rules, including removing rules absent from the source. A stable release updates `latest` redirects only when it promotes `latest`, so an older maintenance release cannot roll back live routing. Redirects owned by other versions remain unchanged. diff --git a/install.sh b/install.sh index 620a0beda0..efc4d39b7c 100755 --- a/install.sh +++ b/install.sh @@ -455,12 +455,15 @@ resolve_latest_prerelease_tag() { } for _run_id in $_run_ids; do - _successful="$(gh api "repos/${REPO}/actions/runs/${_run_id}" --jq ' - .status == "completed" and .conclusion == "success" and - (.path | startswith(".github/workflows/release-tag.yml"))')" || { + # Packages are published before qualification; a failed or running + # qualification must not hide an already-published candidate. + _release_workflow="$(gh api "repos/${REPO}/actions/runs/${_run_id}" --jq ' + (.path | split("@")[0]) | + . == ".github/workflows/release-prerelease.yml" or + . == ".github/workflows/release-tag.yml"')" || { error "failed to check prerelease workflow run ${_run_id}" } - if [ "$_successful" = "true" ]; then + if [ "$_release_workflow" = "true" ]; then printf '%s\n' "$_tag" return 0 fi diff --git a/tasks/scripts/sync_docs_website_test.py b/tasks/scripts/sync_docs_website_test.py index ed270c533c..61486702f4 100644 --- a/tasks/scripts/sync_docs_website_test.py +++ b/tasks/scripts/sync_docs_website_test.py @@ -30,7 +30,7 @@ def read_workflow(name: str) -> dict: def test_release_workflows_sync_and_publish_docs_once() -> None: dev = read_workflow("release-dev.yml") - tag = read_workflow("release-tag.yml") + tag = read_workflow("release-publish.yml") dev_job = dev["jobs"]["publish-fern-docs"] tag_job = tag["jobs"]["publish-fern-docs"] @@ -51,30 +51,19 @@ def test_release_workflows_sync_and_publish_docs_once() -> None: assert "availability" not in dev_job["with"] assert tag_job["needs"] == [ - "compute-versions", "release", "publish-sdk-typescript", - "release-helm", "trigger-wheel-publish", ] assert tag_job["uses"] == "./.github/workflows/sync-docs.yml" assert tag_job["with"]["channel"] == "stable" - assert ( - tag_job["with"]["release_version"] - == "${{ needs.compute-versions.outputs.semver }}" - ) - assert ( - tag_job["with"]["version_slug"] - == "v${{ needs.compute-versions.outputs.semver }}" - ) - assert ( - tag_job["with"]["display_name"] - == "Latest (v${{ needs.compute-versions.outputs.semver }})" - ) + assert tag_job["with"]["release_version"] == "${{ inputs.semver }}" + assert tag_job["with"]["version_slug"] == "v${{ inputs.semver }}" + assert tag_job["with"]["display_name"] == "Latest (v${{ inputs.semver }})" assert tag_job["with"]["publish"] == "true" - assert "is_prerelease != 'true'" in tag_job["if"] + assert tag_job["if"] == "${{ !inputs.is-prerelease }}" - for workflow_name in ("release-dev.yml", "release-tag.yml"): + for workflow_name in ("release-dev.yml", "release-publish.yml"): workflow_path = ( Path(__file__).resolve().parents[2] / ".github" diff --git a/tasks/scripts/test-install-sh.sh b/tasks/scripts/test-install-sh.sh index e9ec590850..65d254d114 100755 --- a/tasks/scripts/test-install-sh.sh +++ b/tasks/scripts/test-install-sh.sh @@ -870,8 +870,15 @@ gh() { *"?name=openshell-v0.1.0-pre.9-linux-amd64-deb"*) [ "${MOCK_NO_PRERELEASE:-0}" = "1" ] || printf '123456\n' ;; - *"actions/runs/999"*) printf 'false\n' ;; - *"actions/runs/101"*) printf 'true\n' ;; + *"actions/runs/999"*) + printf '%s\n' '{"path":".github/workflows/branch.yml","status":"completed","conclusion":"success"}' | jq -r "$4" + ;; + *"actions/runs/101"*) + jq -n --arg path "${MOCK_PRERELEASE_WORKFLOW:-.github/workflows/release-prerelease.yml}" \ + --arg status "${MOCK_PRERELEASE_STATUS:-completed}" \ + --arg conclusion "${MOCK_PRERELEASE_CONCLUSION:-failure}" \ + '{path: $path, status: $status, conclusion: $conclusion}' | jq -r "$4" + ;; *) ;; esac @@ -909,13 +916,23 @@ if ! grep -Fq 'actions/artifacts?name=openshell-v1.0.0-pre.2-linux-amd64-deb' "$ fi if ! grep -Fq 'actions/runs/999' "$mock_gh_log" || ! grep -Fq 'actions/runs/101' "$mock_gh_log"; then - echo "FAIL: pre alias did not skip an unsuccessful run" >&2 + echo "FAIL: pre alias did not skip an unrelated workflow" >&2 cat "$mock_gh_log" >&2 exit 1 fi -if ! grep -Fq '.conclusion == "success"' "$mock_gh_log"; then - echo "FAIL: pre alias did not require a successful workflow run" >&2 - cat "$mock_gh_log" >&2 +for workflow_state in completed:failure in_progress:null completed:success; do + resolved_prerelease="$(MOCK_PRERELEASE_STATUS="${workflow_state%:*}" \ + MOCK_PRERELEASE_CONCLUSION="${workflow_state#*:}" OPENSHELL_VERSION=pre resolve_release_tag)" + if [ "$resolved_prerelease" != "v1.0.0-pre.2" ]; then + echo "FAIL: published prerelease was hidden by qualification state ${workflow_state}" >&2 + exit 1 + fi +done + +resolved_prerelease="$(MOCK_PRERELEASE_WORKFLOW=.github/workflows/release-tag.yml \ + MOCK_PRERELEASE_CONCLUSION=success OPENSHELL_VERSION=pre resolve_release_tag)" +if [ "$resolved_prerelease" != "v1.0.0-pre.2" ]; then + echo "FAIL: pre alias did not accept packages published by the legacy release workflow" >&2 exit 1 fi if grep -Fq -- '--paginate' "$mock_gh_log"; then