diff --git a/.agents/skills/release-captain/SKILL.md b/.agents/skills/release-captain/SKILL.md index 90ddc1286f..f1d83f818c 100644 --- a/.agents/skills/release-captain/SKILL.md +++ b/.agents/skills/release-captain/SKILL.md @@ -234,7 +234,7 @@ git worktree remove --force "$wt" | Transient infrastructure failure in a pre-release, including a registry push | After approval, `gh run rerun --failed`. `Release` won't select the run until all of it passes. | | Pre-release run ends in `startup_failure` | The workflow file is invalid at that commit. Fix it in a new `-pre.N` (Monday step 3). | | Code defect or `Cargo Deny` advisory | Rerunning won't help. Fix it in a new `-pre.N` (Monday step 3). | -| `Release` fails after selection | After approval, rerun only the failed jobs that haven't published. | +| `Release` fails after selection | After approval, rerun only the failed jobs that haven't published. If the run's artifacts expired or its jobs were skipped, dispatch `Release` again with `-f tag=vX.Y.Z`; it publishes only what is missing. | ## Related diff --git a/.agents/skills/release-openshell/SKILL.md b/.agents/skills/release-openshell/SKILL.md index f5f382e4ea..470d91467e 100644 --- a/.agents/skills/release-openshell/SKILL.md +++ b/.agents/skills/release-openshell/SKILL.md @@ -32,7 +32,9 @@ passed but publication failed, rerun the failed jobs before promoting the candid Release creates `vX.Y.Z` at that exact commit, then rebuilds and publishes all stable artifacts without rerunning qualification. Embedded versions change, so stable artifacts are not byte-identical to the qualified prerelease. An existing -stable tag may be retried only if it already points to the selected commit. +stable tag may be retried only if it already points to the selected commit. A retry +publishes only missing artifacts; published images, charts, release assets, npm, +and PyPI versions are kept, while the snap is uploaded again. Stable images are saved as OCI archives during the build and pushed under the stable version tags during publication. Their SBOMs are verified before updating `latest`; the qualified prerelease's commit-SHA image tags remain unchanged. diff --git a/.agents/skills/test-release-canary/SKILL.md b/.agents/skills/test-release-canary/SKILL.md index b707920b7b..7be722c3f8 100644 --- a/.agents/skills/test-release-canary/SKILL.md +++ b/.agents/skills/test-release-canary/SKILL.md @@ -95,7 +95,7 @@ Note `install.sh` is pulled from `raw.githubusercontent.com/NVIDIA/OpenShell/${h ## Testing artifacts from a specific SHA -`Release Dev` publishes two chart versions for every dev build (see `.github/actions/release-helm-oci/action.yml:89-102`): +`Release Dev` publishes two chart versions for every dev build (see the push steps in `.github/actions/release-helm-oci/action.yml`): - `oci://ghcr.io/nvidia/openshell/helm-chart:0.0.0-dev` — floating, overwritten on every main push. - `oci://ghcr.io/nvidia/openshell/helm-chart:0.0.0-dev.` — immutable, `appVersion` set to the same SHA so it pulls the matching `gateway`, `sandbox`, and `supervisor` images. diff --git a/.agents/skills/watch-github-actions/SKILL.md b/.agents/skills/watch-github-actions/SKILL.md index f80ef25b12..f6db118f3d 100644 --- a/.agents/skills/watch-github-actions/SKILL.md +++ b/.agents/skills/watch-github-actions/SKILL.md @@ -190,7 +190,7 @@ 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. +Pre-release uploads snaps to `latest/beta`; stable releases upload to `latest/stable`. For a release notes preview, dispatch `release.yml` with `notes-only=true` and `tag=`. The generator resolves the previous reachable stable tag; @@ -207,8 +207,8 @@ and qualification, and keeps the notes only in the `release-notes` artifact. This job is advisory: a notes failure emits a warning without failing the prerelease run or blocking promotion. Stable publication reuses the promoted prerelease's `release-notes` artifact, retagged, and generates notes only when -that preview is unavailable. The `Select Qualified Prerelease` summary says -which happened. +that preview is unavailable. The Release workflow's `Release Notes` job summary +says which happened, and Publish waits for that job. For `Codex Compatibility Review`, inspect its job summary and the `compatibility-review-run--attempt-` artifact. Confirm the diff --git a/.github/actions/release-helm-oci/action.yml b/.github/actions/release-helm-oci/action.yml index a296c9e4ba..aba7504129 100644 --- a/.github/actions/release-helm-oci/action.yml +++ b/.github/actions/release-helm-oci/action.yml @@ -3,8 +3,8 @@ name: Release Helm OCI description: > - Patch chart version/appVersion, refuse duplicate OCI versions on public - releases, package the gateway and workspace charts, and push them to GHCR OCI. + Patch chart version/appVersion, package the gateway and workspace charts, and + push them to GHCR OCI. Public releases never overwrite published versions. inputs: chart-version: @@ -63,22 +63,38 @@ runs: echo "workspace_chart_dir=${WORKSPACE_CHART_DIR}" >> "$GITHUB_OUTPUT" echo "chart_version=${CHART_VERSION}" >> "$GITHUB_OUTPUT" - - name: Refuse duplicate chart version + # Public chart versions are never overwritten. A rerun keeps both published + # charts; a partial publication still fails for manual repair. + - name: Check for published chart version + id: published if: inputs.release-kind == 'public' env: CHART_VERSION: ${{ steps.prep.outputs.chart_version }} shell: bash run: | set -euo pipefail + found=0 for chart in helm-chart openshell-workspace; do OCI_CHART="oci://ghcr.io/nvidia/openshell/${chart}" - if helm show chart "${OCI_CHART}" --version "${CHART_VERSION}" >/dev/null 2>&1; then - echo "::error::Chart ${chart}:${CHART_VERSION} is already published. Use a new tag or delete the existing package first." + # Only "not found" means unpublished; any other lookup error fails + # rather than risk overwriting. + if lookup_error=$(helm show chart "${OCI_CHART}" --version "${CHART_VERSION}" 2>&1 >/dev/null); then + found=$((found + 1)) + elif [[ "${lookup_error}" != *"ghcr.io/nvidia/openshell/${chart}:${CHART_VERSION}: not found"* ]]; then + echo "::error::Could not check whether ${chart}:${CHART_VERSION} is published: ${lookup_error}" exit 1 fi done + if [ "${found}" -eq 2 ]; then + echo "Charts ${CHART_VERSION} are already published; keeping them." + echo "published=true" >> "$GITHUB_OUTPUT" + elif [ "${found}" -ne 0 ]; then + echo "::error::Only one chart is published at ${CHART_VERSION}. Delete it or publish the other chart manually." + exit 1 + fi - name: Package Helm chart + if: steps.published.outputs.published != 'true' env: GATEWAY_CHART_DIR: ${{ steps.prep.outputs.gateway_chart_dir }} WORKSPACE_CHART_DIR: ${{ steps.prep.outputs.workspace_chart_dir }} @@ -91,6 +107,7 @@ runs: ls /tmp/helm-charts/*.tgz - name: Push Helm chart to GHCR OCI + if: steps.published.outputs.published != 'true' shell: bash run: | set -euo pipefail diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml index d4f00b26ea..db3e143414 100644 --- a/.github/workflows/release-notes.yml +++ b/.github/workflows/release-notes.yml @@ -17,16 +17,27 @@ on: description: Keep notes failures from failing the calling workflow default: false type: boolean + preview-run-id: + description: Optional Pre-release run whose release-notes preview is reused instead of generating notes + default: "" + type: string + preview-tag: + description: Prerelease tag in the reused preview, replaced with release-tag + default: "" + type: string secrets: LLM_INFERENCE_API_KEY: description: NVIDIA Inference Hub API key required: true permissions: + actions: read contents: read pull-requests: read jobs: + # Callers depend on this job, so choose reuse or generation with step + # conditions; a skipped job would skip every job that depends on it. release-notes: name: Write Release Notes runs-on: ubuntu-latest @@ -36,7 +47,30 @@ jobs: ANTHROPIC_BASE_URL: ${{ vars.ANTHROPIC_BASE_URL }} ANTHROPIC_MODEL: ${{ vars.ANTHROPIC_MODEL }} steps: + # Without a preview (missing or expired), the notes are generated instead. + - name: Reuse the prerelease notes preview + id: preview + if: ${{ inputs.preview-run-id != '' }} + shell: bash + env: + GH_TOKEN: ${{ github.token }} + RUN_ID: ${{ inputs.preview-run-id }} + PREVIEW_TAG: ${{ inputs.preview-tag }} + RELEASE_TAG: ${{ inputs.release-tag }} + run: | + set -euo pipefail + : "${PREVIEW_TAG:?preview-tag is required with preview-run-id}" + if gh run download "${RUN_ID}" -R "${GITHUB_REPOSITORY}" -n release-notes -D preview; then + sed "s/${PREVIEW_TAG//./\\.}\b/${RELEASE_TAG}/g" preview/release-notes.md > release-notes.md + rm -rf preview + echo "reused=true" >> "${GITHUB_OUTPUT}" + echo "Release notes are reused from the \`${PREVIEW_TAG}\` preview." >> "${GITHUB_STEP_SUMMARY}" + else + echo "Release notes will be regenerated; the \`${PREVIEW_TAG}\` preview is unavailable." >> "${GITHUB_STEP_SUMMARY}" + fi + - name: Check out release source + if: ${{ steps.preview.outputs.reused != 'true' }} uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 @@ -45,6 +79,7 @@ jobs: - name: Prepare release notes id: comparison + if: ${{ steps.preview.outputs.reused != 'true' }} shell: bash env: RELEASE_TAG: ${{ inputs.release-tag }} @@ -63,6 +98,7 @@ jobs: echo "url=${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/compare/${BASE_TAG}...${RELEASE_TAG}" >> "$GITHUB_OUTPUT" - name: Write notes with Claude + if: ${{ steps.preview.outputs.reused != 'true' }} uses: anthropics/claude-code-action@374f254f5efaf968abfe15d399fd90972e8138b9 # v1 env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/release-prerelease.yml b/.github/workflows/release-prerelease.yml index 3d312a88e0..2d54d02b23 100644 --- a/.github/workflows/release-prerelease.yml +++ b/.github/workflows/release-prerelease.yml @@ -99,6 +99,7 @@ jobs: name: Preview Release Notes needs: [resolve, build] permissions: + actions: read contents: read pull-requests: read uses: ./.github/workflows/release-notes.yml @@ -114,7 +115,6 @@ jobs: permissions: actions: read contents: write - pull-requests: read packages: write id-token: write attestations: write diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 2dc221851a..24190aab68 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -19,11 +19,6 @@ on: required: false default: false type: boolean - release-notes-reused: - description: Whether the calling run already uploaded release-notes from the promoted prerelease's preview - required: false - default: false - type: boolean source-sha: description: Source commit of the built artifacts in the calling run required: true @@ -49,9 +44,6 @@ on: required: false GITLAB_CI_TRIGGER_URL: required: false - LLM_INFERENCE_API_KEY: - description: NVIDIA Inference Hub API key for stable release notes - required: false permissions: contents: read @@ -264,11 +256,23 @@ jobs: set -euo pipefail for component in gateway sandbox supervisor; do image="ghcr.io/nvidia/openshell/${component}:${VERSION}" - # Archives are build artifacts from this run, not externally signed images. - nix shell --inputs-from . nixpkgs#skopeo -c \ - skopeo --insecure-policy copy --all --preserve-digests \ - --authfile "${HOME}/.docker/config.json" \ - "oci-archive:artifacts/images/${component}.tar" "docker://${image}" + # Never replace a published stable image; a rerun keeps the original. + # Only a missing manifest means unpublished; any other lookup error + # fails rather than risk overwriting. + if lookup_error=$(nix shell --inputs-from . nixpkgs#skopeo -c \ + skopeo inspect --raw --authfile "${HOME}/.docker/config.json" \ + "docker://${image}" 2>&1 >/dev/null); then + echo "${image} is already published; keeping it." + elif [[ "${lookup_error}" != *"manifest unknown"* ]]; then + echo "::error::Could not check whether ${image} is published: ${lookup_error}" + exit 1 + else + # Archives are build artifacts from this run, not externally signed images. + nix shell --inputs-from . nixpkgs#skopeo -c \ + skopeo --insecure-policy copy --all --preserve-digests \ + --authfile "${HOME}/.docker/config.json" \ + "oci-archive:artifacts/images/${component}.tar" "docker://${image}" + fi tasks/scripts/verify-image-sbom.sh "${image}" --require-cargo done @@ -326,28 +330,24 @@ jobs: persist-credentials: false ref: ${{ inputs.source-sha }} - - uses: ./.github/actions/release-helm-oci + # Charts come from the release source; the publish logic comes from this + # workflow so retrying an older tag uses the current duplicate handling. + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + ref: ${{ github.workflow_sha }} + path: release-workflow + sparse-checkout: .github/actions/release-helm-oci + + - uses: ./release-workflow/.github/actions/release-helm-oci with: chart-version: ${{ inputs.semver }} app-version: ${{ inputs.semver }} release-kind: public - release-notes: - name: Write Release Notes - if: ${{ !inputs.is-prerelease && !inputs.release-notes-reused }} - permissions: - contents: read - pull-requests: read - uses: ./.github/workflows/release-notes.yml - with: - release-tag: ${{ inputs.release-tag }} - secrets: - LLM_INFERENCE_API_KEY: ${{ secrets.LLM_INFERENCE_API_KEY }} - release: name: Publish Release Assets - needs: [assemble, release-helm, release-notes] - if: ${{ !cancelled() && needs.assemble.result == 'success' && needs.release-helm.result == 'success' && (inputs.is-prerelease || inputs.release-notes-reused || needs.release-notes.result == 'success') }} + needs: [assemble, release-helm] runs-on: linux-amd64-cpu8 timeout-minutes: 10 permissions: @@ -393,6 +393,7 @@ jobs: } } + # Stable callers upload release-notes in the same run before calling Publish. - name: Download release notes if: ${{ !inputs.is-prerelease }} uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 @@ -416,6 +417,8 @@ jobs: make_latest: ${{ steps.latest.outputs.make-latest == 'true' && 'true' || 'false' }} tag_name: ${{ env.RELEASE_TAG }} body_path: release/release-notes.md + # A rerun uploads only missing assets and keeps published ones. + overwrite_files: false files: | release/openshell-x86_64-unknown-linux-musl.tar.gz release/openshell-aarch64-unknown-linux-musl.tar.gz @@ -591,3 +594,49 @@ jobs: publish: true secrets: FERN_TOKEN: ${{ secrets.FERN_TOKEN }} + + # GitHub treats skipped jobs as passing, so a publisher that never ran would + # leave the run green. Fail when any publisher expected for this release kind + # did not succeed, so callers' failure notifications fire. + check-publication: + name: Check Publication + needs: + - assemble + - tag-ghcr-release + - release-helm + - release + - publish-snap + - publish-prerelease-snap + - publish-sdk-typescript + - tag-sdk-go + - trigger-wheel-publish + - publish-fern-docs + if: always() + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Require every expected publisher to succeed + env: + NEEDS: ${{ toJSON(needs) }} + IS_PRERELEASE: ${{ inputs.is-prerelease }} + MAKE_LATEST: ${{ inputs.make-latest }} + run: | + set -euo pipefail + expected=(assemble tag-ghcr-release release-helm release) + if [[ "${IS_PRERELEASE}" == "true" ]]; then + expected+=(publish-prerelease-snap) + else + expected+=(publish-sdk-typescript tag-sdk-go trigger-wheel-publish publish-fern-docs) + if [[ "${MAKE_LATEST}" == "true" ]]; then + expected+=(publish-snap) + fi + fi + failed=0 + for job in "${expected[@]}"; do + result=$(jq -r --arg job "${job}" '.[$job].result' <<<"${NEEDS}") + if [[ "${result}" != "success" ]]; then + echo "::error::${job} did not publish (result: ${result})" + failed=1 + fi + done + exit "${failed}" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bf929fac02..15ab451116 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -29,6 +29,7 @@ jobs: name: Preview Release Notes if: ${{ inputs.notes-only }} permissions: + actions: read contents: read pull-requests: read uses: ./.github/workflows/release-notes.yml @@ -49,7 +50,8 @@ jobs: tag: ${{ steps.candidate.outputs.tag }} source-sha: ${{ steps.candidate.outputs.source_sha }} make-latest: ${{ steps.candidate.outputs.make_latest }} - notes-reused: ${{ steps.notes.outputs.reused }} + candidate-tag: ${{ steps.candidate.outputs.candidate_tag }} + qualification-run-id: ${{ steps.candidate.outputs.qualification_run_id }} steps: - name: Require a release branch env: @@ -92,32 +94,6 @@ jobs: retention-days: 90 if-no-files-found: error - # Without a preview (missing or expired), Publish generates the notes instead. - - name: Reuse the prerelease notes preview - id: notes - env: - GH_TOKEN: ${{ github.token }} - RUN_ID: ${{ steps.candidate.outputs.qualification_run_id }} - CANDIDATE_TAG: ${{ steps.candidate.outputs.candidate_tag }} - RELEASE_TAG: ${{ steps.candidate.outputs.tag }} - run: | - set -euo pipefail - if gh run download "${RUN_ID}" -R "${GITHUB_REPOSITORY}" -n release-notes -D notes; then - sed -i "s/${CANDIDATE_TAG//./\\.}\b/${RELEASE_TAG}/g" notes/release-notes.md - echo "reused=true" >> "${GITHUB_OUTPUT}" - echo "Release notes are reused from the \`${CANDIDATE_TAG}\` preview." >> "${GITHUB_STEP_SUMMARY}" - else - echo "Release notes will be regenerated; the \`${CANDIDATE_TAG}\` preview is unavailable." >> "${GITHUB_STEP_SUMMARY}" - fi - - - name: Upload release notes - if: ${{ steps.notes.outputs.reused == 'true' }} - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 - with: - name: release-notes - path: notes/release-notes.md - if-no-files-found: error - - name: Create stable tag at the qualified commit env: GH_TOKEN: ${{ github.token }} @@ -147,13 +123,27 @@ jobs: secrets: CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} + release-notes: + name: Release Notes + needs: promote + permissions: + actions: read + contents: read + pull-requests: read + uses: ./.github/workflows/release-notes.yml + with: + release-tag: ${{ needs.promote.outputs.tag }} + preview-run-id: ${{ needs.promote.outputs.qualification-run-id }} + preview-tag: ${{ needs.promote.outputs.candidate-tag }} + secrets: + LLM_INFERENCE_API_KEY: ${{ secrets.LLM_INFERENCE_API_KEY }} + publish: name: Publish - needs: [promote, build] + needs: [promote, build, release-notes] permissions: actions: read contents: write - pull-requests: read packages: write id-token: write attestations: write @@ -167,10 +157,55 @@ jobs: npm-version: ${{ needs.build.outputs.npm_version }} is-prerelease: false make-latest: ${{ needs.promote.outputs.make-latest == 'true' }} - release-notes-reused: ${{ needs.promote.outputs.notes-reused == 'true' }} secrets: SNAPCRAFT_STORE_CREDENTIALS: ${{ secrets.SNAPCRAFT_STORE_CREDENTIALS }} FERN_TOKEN: ${{ secrets.FERN_TOKEN }} GITLAB_CI_TRIGGER_TOKEN: ${{ secrets.GITLAB_CI_TRIGGER_TOKEN }} GITLAB_CI_TRIGGER_URL: ${{ secrets.GITLAB_CI_TRIGGER_URL }} - LLM_INFERENCE_API_KEY: ${{ secrets.LLM_INFERENCE_API_KEY }} + + notify-release-failure: + name: Notify Release Failure + needs: [promote, build, release-notes, publish] + if: failure() + runs-on: ubuntu-latest + timeout-minutes: 2 + permissions: + actions: read + defaults: + run: + shell: bash + steps: + - name: Send Slack notification + continue-on-error: true + env: + RELEASE_TAG: ${{ needs.promote.outputs.tag || inputs.tag || github.ref_name }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL }} + SLACK_MENTION: ${{ secrets.SLACK_OPENSHELL_TRIAGE_MENTION }} + GH_TOKEN: ${{ github.token }} + 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 release ${RELEASE_TAG} failed (attempt ${GITHUB_RUN_ATTEMPT})." + if [[ -n "${SLACK_MENTION}" ]]; then + message+=" ${SLACK_MENTION}" + fi + + failed_jobs=$(gh api --paginate \ + "repos/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}/jobs?per_page=100" \ + --jq '.jobs[] | select(.conclusion == "failure" or .conclusion == "timed_out") | "• <\(.html_url)|\(.name)>"') || { + echo "::warning::Could not list failed jobs; sending the notification without them." + failed_jobs="" + } + if [[ -n "${failed_jobs}" ]]; then + message+=$'\n\nFailed jobs\n'"${failed_jobs}" + 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/sdk-typescript-publish.yml b/.github/workflows/sdk-typescript-publish.yml index a10ff0d289..1f7904d48f 100644 --- a/.github/workflows/sdk-typescript-publish.yml +++ b/.github/workflows/sdk-typescript-publish.yml @@ -95,6 +95,11 @@ jobs: run: | set -euo pipefail npm pkg set version="${OPENSHELL_NPM_VERSION}" + package="$(npm pkg get name | tr -d '"')@${OPENSHELL_NPM_VERSION}" + if npm view "${package}" version >/dev/null 2>&1; then + echo "${package} is already published." + exit 0 + fi dist_tag=latest if [[ "${MAKE_LATEST}" != "true" ]]; then dist_tag="release-${OPENSHELL_NPM_VERSION%.*}" diff --git a/.github/workflows/wheel-publish.yml b/.github/workflows/wheel-publish.yml index 451c6ed46b..1456b70b4b 100644 --- a/.github/workflows/wheel-publish.yml +++ b/.github/workflows/wheel-publish.yml @@ -100,6 +100,10 @@ jobs: echo "No wheel filenames provided by build job" >&2 exit 1 fi + if curl --fail --silent --output /dev/null "https://pypi.org/pypi/openshell/${RELEASE_VERSION}/json"; then + echo "openshell ${RELEASE_VERSION} is already on PyPI." + exit 0 + fi response=$(curl -X POST \ --fail \ diff --git a/CI.md b/CI.md index cf251e0bb9..431795c959 100644 --- a/CI.md +++ b/CI.md @@ -382,9 +382,9 @@ reruns without overwriting earlier evidence. summarize PRs since the previous stable tag. Prereleases retain a `release-notes` artifact, with advisory failures. Stable releases publish the promoted prerelease's preview with the prerelease tag replaced by the stable tag. If that -preview is missing or expired, stable publication generates new notes and -requires generation to succeed. The promotion summary states whether notes were -reused or regenerated. Configure the `LLM_INFERENCE_API_KEY` Actions secret and +preview is missing or expired, the Release workflow's notes job generates new +notes and requires generation to succeed before publishing. That job's summary +states whether notes were reused or regenerated. Configure the `LLM_INFERENCE_API_KEY` Actions secret and the `ANTHROPIC_BASE_URL` and `ANTHROPIC_MODEL` Actions variables. To preview notes without building or publishing: @@ -446,7 +446,9 @@ same run; the promotion record links to the attempt that produced that evidence. Release rebuilds all stable artifacts with the stable version and publishes without rerunning qualification. These artifacts have different embedded versions from the qualified prerelease. Existing stable tags are never moved; a retry must -resolve to the same qualified candidate. +resolve to the same qualified candidate. A retry rebuilds, then publishes only what +is missing: it keeps published GHCR version tags, Helm charts, GitHub Release assets, +npm versions, and PyPI versions. Snap uploads are not skipped. Stable image builds save multi-platform OCI archives as workflow artifacts without pushing to GHCR. Publication copies those archives, including their attestations, @@ -710,12 +712,12 @@ 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.yml` | Manually promote the latest qualified prerelease for a version and branch, create its stable tag, then rebuild and publish. Support releases preserve shared latest channels. Publishes the prerelease's notes preview, retagged, or generates notes when it is unavailable. The notes-only mode generates a release notes preview artifact. | +| `.github/workflows/release.yml` | Manually promote the latest qualified prerelease for a version and branch, create its stable tag, then rebuild and publish. Support releases preserve shared latest channels. Publishes the prerelease's notes preview, retagged, or generates notes when it is unavailable. The notes-only mode generates a release notes preview artifact. Published artifacts are never overwritten: a rerun fills in what is missing, and a bad published artifact is fixed by releasing a new patch version. | | `.github/workflows/release-auto-tag.yml` | Weekdays at 14:00 Europe/Zurich, tags the next `-pre.N` on `main` when commits landed since the latest one, then dispatches Pre-release. It skips when the train has no `-pre.1` or its version is already stable, so push each train's `-pre.1` tag by hand. | | `.github/workflows/release-prerelease.yml` | Pre-release orchestrator, triggered by prerelease tag pushes or dispatched by Release Auto-Tag or manually. A tagless dispatch creates the next candidate on its branch; explicit tags select existing candidates. Builds, then publishes and qualifies 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-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-notes.yml` | Shared prompt-driven release notes generation through Inference Hub, used by prerelease previews, notes-only previews, and stable releases. Stable releases reuse the promoted prerelease's preview when available. | | `.github/workflows/release-qualification.yml` | Reusable qualification suites and result aggregation. Runs security scans, conformance and feature integration, Docker and VM E2E, protobuf compatibility, and advisory compatibility review. | | `.github/workflows/snap-package.yml` | Builds snap and component artifacts for Release Dev, Release, and Pre-release without Store credentials or publication. | | `.github/workflows/snap-publish.yml` | Uploads existing snap and component artifacts directly to a Store channel without rebuilding. Release Dev uses `latest/edge`, Pre-release uses `latest/beta`, and Release uses `latest/stable` for the stable rebuild. Uploads queue per channel and architecture, so tagged beta and stable jobs recheck repository tags after earlier publication finishes and before updating their floating channels. Manual dispatch uploads the `snap-linux-*` artifacts from `artifact-run-id`. |