From d8d19a01e8ded37167c8edea458e14c179f09e85 Mon Sep 17 00:00:00 2001 From: Robin Date: Sun, 12 Jul 2026 04:25:53 +0200 Subject: [PATCH 1/9] fix(release): harden immutable tag recovery --- .github/workflows/release-publish.yml | 52 +++++++-- .github/workflows/release.yml | 102 +++++++++++----- Taskfile.yml | 4 +- .../akua-napi/npm/darwin-arm64/package.json | 2 +- crates/akua-napi/npm/darwin-x64/package.json | 2 +- .../npm/linux-arm64-gnu/package.json | 2 +- .../npm/linux-arm64-musl/package.json | 2 +- .../akua-napi/npm/linux-x64-gnu/package.json | 2 +- .../akua-napi/npm/linux-x64-musl/package.json | 2 +- .../akua-napi/npm/win32-x64-msvc/package.json | 2 +- crates/akua-napi/package.json | 2 +- crates/akua-native-engines-npm/package.json | 2 +- docs/releasing.md | 60 ++++++++++ docs/sdk-runtime-compat.md | 4 +- packages/sdk/README.md | 8 +- packages/sdk/package.json | 4 +- scripts/check-release-workflows.sh | 110 ++++++++++++++++++ 17 files changed, 306 insertions(+), 56 deletions(-) create mode 100644 docs/releasing.md diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index ebd82ba4..d8bd7606 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -20,10 +20,10 @@ name: release-publish # out of the GITHUB_TOKEN no-recursion rule, so the upstream workflow # can dispatch this one without a PAT. # -# A failed run can also be re-run via `gh run rerun --failed `; -# the workflow file used will be the version on the dispatched ref -# (typically main), not the one at the original tag's SHA — meaning -# fixes to this workflow apply on rerun. +# Recovery is a new deliberate workflow_dispatch from a reviewed workflow +# head. Do not rerun a failed historical run: the requested tag remains the +# immutable source and Release-asset identity while the workflow fix comes +# from the explicitly selected dispatch ref. See docs/releasing.md. on: workflow_dispatch: @@ -50,7 +50,13 @@ jobs: tag: ${{ steps.detect.outputs.tag }} is_prerelease: ${{ steps.detect.outputs.is_prerelease }} npm_dist_tag: ${{ steps.detect.outputs.npm_dist_tag }} + source_commit: ${{ steps.detect.outputs.source_commit }} steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.tag }} + fetch-depth: 0 + - id: detect env: TAG: ${{ inputs.tag }} @@ -60,6 +66,12 @@ jobs: echo "::error::tag '$TAG' is not a valid semver v* tag" exit 1 fi + source_commit=$(git rev-parse "refs/tags/${TAG}^{commit}") + head_commit=$(git rev-parse 'HEAD^{commit}') + if [[ "$head_commit" != "$source_commit" ]]; then + echo "::error::checkout $head_commit does not match $TAG at $source_commit" + exit 1 + fi version="${TAG#v}" if [[ "$version" == *-* ]]; then is_prerelease=true @@ -73,6 +85,7 @@ jobs: echo "version=$version" echo "is_prerelease=$is_prerelease" echo "npm_dist_tag=$npm_dist_tag" + echo "source_commit=$source_commit" } >> "$GITHUB_OUTPUT" echo "Will publish $version under dist-tag: $npm_dist_tag" @@ -82,7 +95,7 @@ jobs: steps: - uses: actions/checkout@v6 with: - ref: ${{ inputs.tag }} + ref: ${{ needs.detect-version.outputs.source_commit }} - uses: actions/setup-node@v4 # Node 24 ships npm 11.x — required for OIDC trusted publishing. @@ -102,8 +115,11 @@ jobs: run: | set -eu version="${{ needs.detect-version.outputs.version }}" - jq --arg v "$version" \ - '.version = $v | .dependencies["@akua-dev/native-engines"] = $v' \ + repo='git+https://github.com/akua-dev/akua.git' + jq --arg v "$version" --arg repo "$repo" \ + '.version = $v + | .dependencies["@akua-dev/native-engines"] = $v + | .repository.url = $repo' \ crates/akua-napi/package.json > crates/akua-napi/package.json.tmp mv crates/akua-napi/package.json.tmp crates/akua-napi/package.json @@ -149,11 +165,13 @@ jobs: run: | set -eu version="${{ needs.detect-version.outputs.version }}" + repo='git+https://github.com/akua-dev/akua.git' cp crates/akua-napi/artifacts/wasm-bundle/crates/helm-engine-wasm/assets/helm-engine.wasm \ crates/akua-native-engines-npm/helm-engine.wasm cp crates/akua-napi/artifacts/wasm-bundle/crates/kustomize-engine-wasm/assets/kustomize-engine.wasm \ crates/akua-native-engines-npm/kustomize-engine.wasm - jq --arg v "$version" '.version = $v' \ + jq --arg v "$version" --arg repo "$repo" \ + '.version = $v | .repository.url = $repo' \ crates/akua-native-engines-npm/package.json \ > crates/akua-native-engines-npm/package.json.tmp mv crates/akua-native-engines-npm/package.json.tmp \ @@ -243,7 +261,21 @@ jobs: # full node_modules tree. The npm provenance + OIDC publish # steps are unaffected by --ignore-scripts. if: ${{ inputs.dry-run == false }} - working-directory: sdk env: NPM_DIST_TAG: ${{ needs.detect-version.outputs.npm_dist_tag }} - run: npm publish --access public --provenance --tag "$NPM_DIST_TAG" --ignore-scripts + run: | + set -eu + + publish_one() { + local dir="$1" name version + shift + name=$(jq -r '.name' "$dir/package.json") + version=$(jq -r '.version' "$dir/package.json") + if npm view "${name}@${version}" version > /dev/null 2>&1; then + echo "Skipping ${name}@${version} — already on npm" + return 0 + fi + (cd "$dir" && npm publish --access public --provenance --tag "$NPM_DIST_TAG" "$@") + } + + publish_one sdk --ignore-scripts diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9c29ce42..bedab26f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -80,11 +80,27 @@ jobs: version: ${{ steps.parse.outputs.version }} is_prerelease: ${{ steps.parse.outputs.is_prerelease }} npm_dist_tag: ${{ steps.parse.outputs.npm_dist_tag }} + source_commit: ${{ steps.parse.outputs.source_commit }} steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.inputs.tag || github.ref_name }} + fetch-depth: 0 + - id: parse run: | set -eu tag="${{ github.event.inputs.tag || github.ref_name }}" + if [[ ! "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then + echo "::error::tag '$tag' is not a valid semver v* tag" + exit 1 + fi + source_commit=$(git rev-parse "refs/tags/${tag}^{commit}") + head_commit=$(git rev-parse 'HEAD^{commit}') + if [[ "$head_commit" != "$source_commit" ]]; then + echo "::error::checkout $head_commit does not match $tag at $source_commit" + exit 1 + fi version="${tag#v}" # Semver prerelease has a `-` separator after the patch number # (1.2.3-rc1, 1.2.3-alpha.4). A bare `v1.2.3` doesn't match. @@ -100,8 +116,9 @@ jobs: echo "version=$version" echo "is_prerelease=$is_prerelease" echo "npm_dist_tag=$npm_dist_tag" + echo "source_commit=$source_commit" } >> "$GITHUB_OUTPUT" - echo "Resolved: tag=$tag version=$version prerelease=$is_prerelease npm_dist_tag=$npm_dist_tag" + echo "Resolved: tag=$tag commit=$source_commit version=$version prerelease=$is_prerelease npm_dist_tag=$npm_dist_tag" # --------------------------------------------------------------------------- # wasm-bundle — engine + render-worker wasm artefacts built once on @@ -116,6 +133,8 @@ jobs: needs: detect-version steps: - uses: actions/checkout@v6 + with: + ref: ${{ needs.detect-version.outputs.source_commit }} - uses: jdx/mise-action@v4 with: @@ -214,6 +233,8 @@ jobs: runs-on: ${{ matrix.host }} steps: - uses: actions/checkout@v6 + with: + ref: ${{ needs.detect-version.outputs.source_commit }} - uses: actions/setup-node@v4 with: @@ -327,7 +348,7 @@ jobs: with: # Same filter as the old native-publish step — without it, # download-artifact@v4 also pulls the docker job's - # `cnap-tech~akua~.dockerbuild` provenance artifact whose + # `akua-dev~akua~.dockerbuild` provenance artifact whose # `~`-containing name fails path validation. pattern: '{native-*,wasm-bundle,sdk-staged}' path: artifacts @@ -387,6 +408,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + with: + ref: ${{ needs.detect-version.outputs.source_commit }} - uses: jdx/mise-action@v4 with: @@ -415,9 +438,14 @@ jobs: run: | set -eu version="${{ needs.detect-version.outputs.version }}" + repo='git+https://github.com/akua-dev/akua.git' + homepage='https://github.com/akua-dev/akua#readme' echo "Preparing @akua-dev/sdk@${version}" - jq --arg v "$version" \ - '.version = $v | .dependencies["@akua-dev/native"] = $v' \ + jq --arg v "$version" --arg repo "$repo" --arg homepage "$homepage" \ + '.version = $v + | .dependencies["@akua-dev/native"] = $v + | .repository.url = $repo + | .homepage = $homepage' \ packages/sdk/package.json > packages/sdk/package.json.tmp mv packages/sdk/package.json.tmp packages/sdk/package.json @@ -463,6 +491,8 @@ jobs: runs-on: ${{ matrix.runner }} steps: - uses: actions/checkout@v6 + with: + ref: ${{ needs.detect-version.outputs.source_commit }} - uses: jdx/mise-action@v4 with: @@ -594,13 +624,15 @@ jobs: if: github.event_name == 'push' || inputs.dry-run == false steps: - uses: actions/checkout@v6 + with: + ref: ${{ needs.detect-version.outputs.source_commit }} - uses: actions/download-artifact@v8 with: # Filter to the cli-build artifacts this job consumes — # `akua-` per-platform tarballs + sha256 sidecars # (uploaded as `name: akua-${{ matrix.target }}` upstream). # Without the pattern, download-artifact also pulls the - # docker job's `cnap-tech~akua~.dockerbuild` provenance + # docker job's `akua-dev~akua~.dockerbuild` provenance # artifact whose `~`-containing name fails path validation # in v4 (and only silently slips by on v8 because the # merge-multiple flag tolerates it; defensive here so a @@ -608,23 +640,37 @@ jobs: pattern: 'akua-*' path: dist merge-multiple: true - - name: Create release + - name: Create or update release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} + TAG: ${{ needs.detect-version.outputs.tag }} + VERSION: ${{ needs.detect-version.outputs.version }} + SOURCE_COMMIT: ${{ needs.detect-version.outputs.source_commit }} + IS_PRERELEASE: ${{ needs.detect-version.outputs.is_prerelease }} run: | set -eu - flags=() - if [ "${{ needs.detect-version.outputs.is_prerelease }}" = "true" ]; then - flags+=(--prerelease) + notes="See [CHANGELOG.md](https://github.com/akua-dev/akua/blob/main/CHANGELOG.md) for details." + if gh release view "${TAG}" --repo "${GH_REPO}" > /dev/null 2>&1; then + gh release upload "${TAG}" dist/*.tar.gz dist/*.zip dist/*.sha256 --clobber \ + --repo "${GH_REPO}" + else + flags=() + if [ "${IS_PRERELEASE}" = "true" ]; then + flags+=(--prerelease) + fi + gh release create "${TAG}" \ + --repo "${GH_REPO}" \ + --verify-tag \ + --target "${SOURCE_COMMIT}" \ + --title "akua ${VERSION}" \ + --notes "${notes}" \ + "${flags[@]}" \ + dist/*.tar.gz dist/*.zip dist/*.sha256 fi - gh release create "${{ needs.detect-version.outputs.tag }}" \ - --title "akua ${{ needs.detect-version.outputs.version }}" \ - --notes "See [CHANGELOG.md](https://github.com/cnap-tech/akua/blob/main/CHANGELOG.md) for details." \ - "${flags[@]}" \ - dist/*.tar.gz dist/*.zip dist/*.sha256 # --------------------------------------------------------------------------- - # docker — multi-arch image at `ghcr.io/cnap-tech/akua:`. Skips + # docker — multi-arch image at `ghcr.io/akua-dev/akua:`. Skips # the `:latest` tag for prereleases so `docker pull akua:latest` # stays on the most recent stable release. # --------------------------------------------------------------------------- @@ -634,6 +680,8 @@ jobs: if: github.event_name == 'push' || inputs.dry-run == false steps: - uses: actions/checkout@v6 + with: + ref: ${{ needs.detect-version.outputs.source_commit }} - uses: actions/download-artifact@v8 with: pattern: 'akua-*' @@ -662,10 +710,10 @@ jobs: set -eu tag="${{ needs.detect-version.outputs.tag }}" # Versioned tag always; `:latest` only for stable releases. - tags="ghcr.io/cnap-tech/akua:${tag}" + tags="ghcr.io/akua-dev/akua:${tag}" if [ "${{ needs.detect-version.outputs.is_prerelease }}" = "false" ]; then tags="${tags} - ghcr.io/cnap-tech/akua:latest" + ghcr.io/akua-dev/akua:latest" fi { echo "tags< Formula/akua.rb <= 22" diff --git a/crates/akua-native-engines-npm/package.json b/crates/akua-native-engines-npm/package.json index 534c0593..545631c4 100644 --- a/crates/akua-native-engines-npm/package.json +++ b/crates/akua-native-engines-npm/package.json @@ -5,7 +5,7 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/cnap-tech/akua.git" + "url": "git+https://github.com/akua-dev/akua.git" }, "main": "index.js", "files": [ diff --git a/docs/releasing.md b/docs/releasing.md new file mode 100644 index 00000000..3dd571da --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,60 @@ +# Releasing akua + +The build and publish lanes are intentionally separate. `.github/workflows/release.yml` +builds the immutable tag, updates the GitHub Release assets with clobber semantics, +pushes the container, updates Homebrew, and dispatches +`.github/workflows/release-publish.yml`. The publish workflow downloads the assets +owned by that tag and publishes npm packages in dependency order. Neither lane +creates or moves a tag during recovery. + +## npm trusted publisher contract + +npm trusted-publisher configuration is external registry state. Source code can +request a GitHub OIDC token, but it cannot create or repair the trust relationship. +An npm package administrator must confirm the following identity on all ten packages +before any recovery dispatch: + +- GitHub owner/repository: `akua-dev/akua` +- Workflow filename: `release-publish.yml` +- Environment: none (leave the optional npm Environment field empty) +- Allowed action: `npm publish` + +The affected packages are: + +- `@akua-dev/native-engines` +- `@akua-dev/native-darwin-arm64` +- `@akua-dev/native-darwin-x64` +- `@akua-dev/native-linux-arm64-gnu` +- `@akua-dev/native-linux-arm64-musl` +- `@akua-dev/native-linux-x64-gnu` +- `@akua-dev/native-linux-x64-musl` +- `@akua-dev/native-win32-x64-msvc` +- `@akua-dev/native` +- `@akua-dev/sdk` + +Change each package under npm package settings → Trusted Publisher. Do not create, +request, or pass an npm token: the workflow has `id-token: write`, uses GitHub-hosted +runners, and deliberately has no `NODE_AUTH_TOKEN`. Each package manifest also uses +the exact repository URL `git+https://github.com/akua-dev/akua.git`, as required for +trusted publishing. + +## Recovering the partial v0.8.25 release + +Recovery is manual and fail-closed. Do not rerun either failed run, retag +`v0.8.25`, recreate its GitHub Release, or upload assets by hand. The existing tag +resolves to commit `6452eb662445d2ad7c108128f93b9c55138729bb`; the manual build lane +checks out that tag, verifies and propagates its commit, and never builds the current +workflow head as the tagged source. Existing Release assets are overwritten by name, +and npm versions already present are skipped before the missing packages publish. + +A captain may run recovery only after the source PR CI is green and an npm +administrator has confirmed the trusted-publisher identity above for all ten packages: + +```sh +gh workflow run release.yml --repo akua-dev/akua --ref main -f tag=v0.8.25 -f dry-run=false +``` + +That single deliberate dispatch rebuilds from the immutable tag, repairs the GitHub +Release assets, publishes `ghcr.io/akua-dev/akua`, updates the generated Homebrew +formula, and then dispatches the idempotent npm publish lane. There are no automatic +recovery retries. diff --git a/docs/sdk-runtime-compat.md b/docs/sdk-runtime-compat.md index 9b34cb6c..eaf6aec9 100644 --- a/docs/sdk-runtime-compat.md +++ b/docs/sdk-runtime-compat.md @@ -9,7 +9,7 @@ The SDK ships as a normal npm package with a per-platform native addon (`@akua-d | Runtime | Version | OCI fetch | Helm engine | Kustomize engine | cosign verify | Notes | |---|---|---|---|---|---|---| | **Node.js** | 22.x | ✅ | ✅ | ✅ | ✅ | Primary target. CI sweeps green per push. | -| **Node.js** | 24.x | ✅ | ✅ | ✅ | ✅ | release.yml runs npm publish jobs on 24 (npm 11+ for OIDC trusted publishing). | +| **Node.js** | 24.x | ✅ | ✅ | ✅ | ✅ | release-publish.yml runs npm publish jobs on 24 (npm 11+ for OIDC trusted publishing). | | **Bun** | 1.3+ | ✅ | ✅ | ✅ | ✅ | `task sdk:test` runs entirely under bun. Bun's Node-API impl is compatible. | | **Deno** | 2.x | ✅ | ✅ | ✅ | ✅ | Loads `@akua-dev/native` via `npm:` specifier. Requires `--allow-read --allow-net --allow-env`. | @@ -49,4 +49,4 @@ These are not part of CI today. When v0.7 lands the CI matrix follow-up, `.githu - `crates/akua-napi/` — the native crate. - `crates/akua-napi/index.js` — the auto-generated platform-loader (what picks the right `@akua-dev/native-*` per host). -- `.github/workflows/release.yml` — single unified pipeline. `wasm-bundle` → `native-build` (matrix) → `native-publish` → `sdk-publish` (with `sdk-build` running in parallel from tag time). Same workflow also drives the cli binary release + Docker + Homebrew bump. +- `.github/workflows/release.yml` builds and stages the release; `.github/workflows/release-publish.yml` publishes npm packages from the staged Release assets in dependency order. The build workflow also drives the CLI binary release, Docker, and Homebrew bump. diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 64bd8c5c..8ac235e0 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -1,6 +1,6 @@ # @akua-dev/sdk -TypeScript SDK for [akua](https://github.com/cnap-tech/akua). Every verb runs in-process via a bundled native addon (napi-rs) — same `akua-core` the CLI uses, no `akua` binary on `$PATH` required. +TypeScript SDK for [akua](https://github.com/akua-dev/akua). Every verb runs in-process via a bundled native addon (napi-rs) — same `akua-core` the CLI uses, no `akua` binary on `$PATH` required. ## Install @@ -79,13 +79,13 @@ task sdk:publish:check # npm pack --dry-run ## Release flow -SDK versions track the Rust workspace version: one `v` tag drives a single unified `release.yml` workflow that builds the wasm bundle once, fans out into the native + cli matrices in parallel, then chains npm publishes (engines → per-platform native → meta-native → SDK) via job-level `needs:` dependencies. No npm polling, no inter-workflow races. +SDK versions track the Rust workspace version. A `v` tag drives `release.yml`, which builds the wasm bundle once and fans out into the native and CLI matrices. It then dispatches `release-publish.yml`, which publishes engines → per-platform native → meta-native → SDK from durable GitHub Release assets. 1. Land changes on `main`; `task ci` must be green. 2. Bump versions in `Cargo.toml`, `crates/akua-napi/package.json`, `packages/sdk/package.json`, all `crates/akua-napi/npm//package.json`, `crates/akua-native-engines-npm/package.json`. Commit `release: vX.Y.Z`. -3. Tag `v` and push. The single `release.yml` workflow handles everything (npm + GitHub Release + Docker + Homebrew). Prerelease tags like `v-rc1` publish to the npm `next` dist-tag and are marked as prereleases on GitHub. +3. Tag `v` and push. The release workflows handle npm, GitHub Release, Docker, and Homebrew. Prerelease tags like `v-rc1` publish to the npm `next` dist-tag and are marked as prereleases on GitHub. -See [`.github/workflows/release.yml`](../../.github/workflows/release.yml) for the full job graph. +See [`docs/releasing.md`](../../docs/releasing.md) for the release and recovery contract. ## Still coming diff --git a/packages/sdk/package.json b/packages/sdk/package.json index f5c53b93..c744de27 100644 --- a/packages/sdk/package.json +++ b/packages/sdk/package.json @@ -5,9 +5,9 @@ "license": "Apache-2.0", "repository": { "type": "git", - "url": "git+https://github.com/cnap-tech/akua.git" + "url": "git+https://github.com/akua-dev/akua.git" }, - "homepage": "https://github.com/cnap-tech/akua#readme", + "homepage": "https://github.com/akua-dev/akua#readme", "type": "module", "main": "./dist/mod.js", "types": "./dist/mod.d.ts", diff --git a/scripts/check-release-workflows.sh b/scripts/check-release-workflows.sh index 68e55daa..6b1b4649 100644 --- a/scripts/check-release-workflows.sh +++ b/scripts/check-release-workflows.sh @@ -51,6 +51,26 @@ assert_job_contains() { fi } +assert_file_contains() { + local file="$1" + local pattern="$2" + + if ! grep -Fq -- "$pattern" "$file"; then + echo "ERROR: $file is missing '$pattern'" >&2 + exit 1 + fi +} + +assert_file_excludes() { + local file="$1" + local pattern="$2" + + if grep -Fq -- "$pattern" "$file"; then + echo "ERROR: $file still contains forbidden text '$pattern'" >&2 + exit 1 + fi +} + # Release builds must install from the committed lockfile before mutating # package manifests to the tag version. Mutating first invalidates # --frozen-lockfile and can also force unpublished tag versions to resolve. @@ -88,4 +108,94 @@ assert_before ".github/workflows/release.yml" \ "Bump package.json version + native dep pin from the tag" \ "Pack dry-run (manifest sanity)" +# Release-owned coordinates moved with the repository. Keep the image, release, +# and generated Homebrew formula identities on the current GitHub organization. +for workflow in .github/workflows/*.yml; do + assert_file_excludes "$workflow" "cnap-tech" +done +assert_file_contains ".github/workflows/release.yml" "ghcr.io/akua-dev/akua:\${tag}" +assert_file_contains ".github/workflows/release.yml" "git@github.com-tap:akua-dev/homebrew-tap.git" +assert_file_contains ".github/workflows/release.yml" "homepage \"https://github.com/akua-dev/akua\"" +assert_file_contains ".github/workflows/release.yml" "gh release upload \"\${TAG}\" dist/*.tar.gz dist/*.zip dist/*.sha256 --clobber" + +# A manual recovery runs workflow code from a green branch, but every source +# checkout must resolve to the requested immutable tag. The workflow verifies +# and propagates that commit; it must never create or move a tag. +assert_job_contains ".github/workflows/release.yml" \ + "detect-version" \ + 'git rev-parse "refs/tags/${tag}^{commit}"' +assert_file_contains ".github/workflows/release.yml" "source_commit: \${{ steps.parse.outputs.source_commit }}" +for job in wasm-bundle native-build sdk-build cli-build github-release docker; do + assert_job_contains ".github/workflows/release.yml" \ + "$job" \ + 'ref: ${{ needs.detect-version.outputs.source_commit }}' +done +assert_job_contains ".github/workflows/release-publish.yml" \ + "native-publish" \ + 'ref: ${{ needs.detect-version.outputs.source_commit }}' +for workflow in .github/workflows/release.yml .github/workflows/release-publish.yml; do + if grep -Eq '^[[:space:]]*(git tag|git push .*refs/tags/|gh release delete)' "$workflow"; then + echo "ERROR: $workflow contains a tag/Release replacement command" >&2 + exit 1 + fi +done + +# Recovery publication is tokenless and idempotent. Every npm package is +# published by release-publish.yml, in dependency order, through the same +# probe-before-publish helper. The SDK must not be a one-off duplicate publish. +assert_file_contains ".github/workflows/release-publish.yml" "id-token: write" +if grep -Eq '^[[:space:]]*NODE_AUTH_TOKEN:' .github/workflows/release-publish.yml; then + echo "ERROR: release-publish.yml must not configure NODE_AUTH_TOKEN" >&2 + exit 1 +fi +assert_job_contains ".github/workflows/release-publish.yml" \ + "sdk-publish" \ + 'npm view "${name}@${version}" version' +assert_job_contains ".github/workflows/release-publish.yml" \ + "sdk-publish" \ + "publish_one sdk --ignore-scripts" +assert_before ".github/workflows/release-publish.yml" \ + "native-publish" \ + "publish_one crates/akua-native-engines-npm" \ + "publish_one crates/akua-napi" +assert_job_contains ".github/workflows/release-publish.yml" \ + "sdk-publish" \ + "needs: [detect-version, native-publish]" + +# npm requires repository.url to exactly match the trusted GitHub repository. +for manifest in \ + crates/akua-native-engines-npm/package.json \ + crates/akua-napi/package.json \ + crates/akua-napi/npm/*/package.json \ + packages/sdk/package.json; do + if [[ "$(jq -r '.repository.url' "$manifest")" != "git+https://github.com/akua-dev/akua.git" ]]; then + echo "ERROR: $manifest has stale npm repository provenance" >&2 + exit 1 + fi +done +if (( $(grep -Fc '.repository.url = $repo' .github/workflows/release-publish.yml) < 2 )); then + echo "ERROR: release-publish.yml must normalize native + engine provenance after tag checkout" >&2 + exit 1 +fi +assert_job_contains ".github/workflows/release.yml" \ + "sdk-build" \ + '.repository.url = $repo' +assert_job_contains ".github/workflows/release.yml" \ + "sdk-build" \ + '.homepage = $homepage' + +# The external trusted-publisher identity and the deliberately gated recovery +# command are authoritative release contract, not tribal knowledge. +assert_file_contains "docs/releasing.md" "akua-dev/akua" +assert_file_contains "docs/releasing.md" "release-publish.yml" +assert_file_contains "docs/releasing.md" "Environment: none" +assert_file_contains "docs/releasing.md" "--ref main -f tag=v0.8.25" +assert_file_contains "docs/releasing.md" "only after the source PR CI is green" +assert_file_contains "docs/releasing.md" "all ten packages" +assert_file_contains "Taskfile.yml" "org=akua-dev repo=akua workflow=release-publish.yml environment=none" +assert_file_excludes "Taskfile.yml" "org=cnap-tech repo=akua workflow=release.yml" +assert_file_excludes "packages/sdk/README.md" "github.com/cnap-tech/akua" +assert_file_contains "packages/sdk/README.md" "docs/releasing.md" +assert_file_contains "docs/sdk-runtime-compat.md" "release-publish.yml runs npm publish jobs" + echo "Release workflow ordering checks passed." From 70ad76120655a4d71db1b4ff158652ce59fde774 Mon Sep 17 00:00:00 2001 From: Robin Date: Sun, 12 Jul 2026 04:45:39 +0200 Subject: [PATCH 2/9] no-mistakes(review): Bind release publication to immutable source and workflow SHAs --- .github/workflows/release-publish.yml | 31 +++++++++++++++++++++++++-- .github/workflows/release.yml | 15 ++++++++----- docs/releasing.md | 4 ++++ scripts/check-release-workflows.sh | 26 ++++++++++++++++++++++ 4 files changed, 69 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index d8bd7606..7663fbfd 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -13,8 +13,8 @@ name: release-publish # Release assets and runs `npm publish`. # # When publish fails (npm hiccup, OIDC config drift, version-pin -# mismatch, …) you fix the workflow on main, then re-dispatch THIS -# workflow with the same tag. ~1 minute, no rebuild. +# mismatch, …), recovery re-dispatches this workflow with the tag and +# the expected source/workflow commits. Already-published versions skip. # # Per GitHub Actions docs: `workflow_dispatch` is explicitly carved # out of the GITHUB_TOKEN no-recursion rule, so the upstream workflow @@ -32,6 +32,14 @@ on: description: 'Tag to publish (e.g. v0.8.7).' required: true type: string + expected-source-commit: + description: 'Commit the tag must resolve to.' + required: true + type: string + expected-workflow-commit: + description: 'Commit containing this reviewed workflow.' + required: true + type: string dry-run: description: 'Pack only, do not publish.' required: false @@ -60,13 +68,32 @@ jobs: - id: detect env: TAG: ${{ inputs.tag }} + EXPECTED_SOURCE_COMMIT: ${{ inputs.expected-source-commit }} + EXPECTED_WORKFLOW_COMMIT: ${{ inputs.expected-workflow-commit }} + ACTUAL_WORKFLOW_COMMIT: ${{ github.sha }} run: | set -eu if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then echo "::error::tag '$TAG' is not a valid semver v* tag" exit 1 fi + if [[ ! "$EXPECTED_SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::expected source commit is not a full lowercase SHA" + exit 1 + fi + if [[ ! "$EXPECTED_WORKFLOW_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::expected workflow commit is not a full lowercase SHA" + exit 1 + fi + if [[ "$ACTUAL_WORKFLOW_COMMIT" != "$EXPECTED_WORKFLOW_COMMIT" ]]; then + echo "::error::workflow commit $ACTUAL_WORKFLOW_COMMIT does not match expected $EXPECTED_WORKFLOW_COMMIT" + exit 1 + fi source_commit=$(git rev-parse "refs/tags/${TAG}^{commit}") + if [[ "$source_commit" != "$EXPECTED_SOURCE_COMMIT" ]]; then + echo "::error::$TAG resolves to $source_commit, expected $EXPECTED_SOURCE_COMMIT" + exit 1 + fi head_commit=$(git rev-parse 'HEAD^{commit}') if [[ "$head_commit" != "$source_commit" ]]; then echo "::error::checkout $head_commit does not match $TAG at $source_commit" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index bedab26f..3d7cc237 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,9 +20,9 @@ name: release # version-pin mismatch). When it fails today, fixing it required # re-running the whole build matrix (~hours). With the split: # -# $ gh workflow run release-publish.yml -f tag=vX.Y.Z +# See docs/releasing.md for the SHA-bound recovery dispatch. # -# re-runs publish only — ~1 minute, no rebuild. The build outputs +# re-runs publish without repeating successful npm versions. The build outputs # this workflow produces (native-*.tar.gz / wasm-bundle.tar.gz / # sdk-staged.tar.gz) are uploaded to the GitHub Release as durable # assets, addressable by tag, with no 90-day workflow-artifact TTL. @@ -387,14 +387,19 @@ jobs: # works — no PAT needed. env: TAG: ${{ needs.detect-version.outputs.tag }} + EXPECTED_SOURCE_COMMIT: ${{ needs.detect-version.outputs.source_commit }} + EXPECTED_WORKFLOW_COMMIT: ${{ github.workflow_sha }} + WORKFLOW_REF: ${{ github.ref_name }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eu gh workflow run release-publish.yml \ --repo "$GITHUB_REPOSITORY" \ - --ref main \ - -f tag="$TAG" - echo "Dispatched release-publish.yml with tag=$TAG. Watch:" + --ref "$WORKFLOW_REF" \ + -f tag="$TAG" \ + -f expected-source-commit="$EXPECTED_SOURCE_COMMIT" \ + -f expected-workflow-commit="$EXPECTED_WORKFLOW_COMMIT" + echo "Dispatched release-publish.yml with tag=$TAG source=$EXPECTED_SOURCE_COMMIT workflow=$EXPECTED_WORKFLOW_COMMIT. Watch:" echo " https://github.com/$GITHUB_REPOSITORY/actions/workflows/release-publish.yml" # --------------------------------------------------------------------------- diff --git a/docs/releasing.md b/docs/releasing.md index 3dd571da..b5341438 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -46,6 +46,10 @@ resolves to commit `6452eb662445d2ad7c108128f93b9c55138729bb`; the manual build checks out that tag, verifies and propagates its commit, and never builds the current workflow head as the tagged source. Existing Release assets are overwritten by name, and npm versions already present are skipped before the missing packages publish. +The build lane passes its expected source and workflow commit SHAs to the publish +lane. The publisher verifies that the tag still resolves to the expected source and +that `github.sha` is the reviewed workflow commit; if either ref advances, recovery +fails before npm publication. A captain may run recovery only after the source PR CI is green and an npm administrator has confirmed the trusted-publisher identity above for all ten packages: diff --git a/scripts/check-release-workflows.sh b/scripts/check-release-workflows.sh index 6b1b4649..49d0b8b5 100644 --- a/scripts/check-release-workflows.sh +++ b/scripts/check-release-workflows.sh @@ -125,6 +125,30 @@ assert_job_contains ".github/workflows/release.yml" \ "detect-version" \ 'git rev-parse "refs/tags/${tag}^{commit}"' assert_file_contains ".github/workflows/release.yml" "source_commit: \${{ steps.parse.outputs.source_commit }}" +assert_file_contains ".github/workflows/release.yml" "EXPECTED_SOURCE_COMMIT: \${{ needs.detect-version.outputs.source_commit }}" +assert_file_contains ".github/workflows/release.yml" "EXPECTED_WORKFLOW_COMMIT: \${{ github.workflow_sha }}" +assert_file_contains ".github/workflows/release.yml" 'WORKFLOW_REF: ${{ github.ref_name }}' +assert_job_contains ".github/workflows/release.yml" \ + "trigger-publish" \ + '--ref "$WORKFLOW_REF"' +assert_job_contains ".github/workflows/release.yml" \ + "trigger-publish" \ + '-f expected-source-commit="$EXPECTED_SOURCE_COMMIT"' +assert_job_contains ".github/workflows/release.yml" \ + "trigger-publish" \ + '-f expected-workflow-commit="$EXPECTED_WORKFLOW_COMMIT"' +for input in expected-source-commit expected-workflow-commit; do + assert_file_contains ".github/workflows/release-publish.yml" "$input:" +done +assert_job_contains ".github/workflows/release-publish.yml" \ + "detect-version" \ + 'ACTUAL_WORKFLOW_COMMIT: ${{ github.sha }}' +assert_job_contains ".github/workflows/release-publish.yml" \ + "detect-version" \ + 'if [[ "$ACTUAL_WORKFLOW_COMMIT" != "$EXPECTED_WORKFLOW_COMMIT" ]]' +assert_job_contains ".github/workflows/release-publish.yml" \ + "detect-version" \ + 'if [[ "$source_commit" != "$EXPECTED_SOURCE_COMMIT" ]]' for job in wasm-bundle native-build sdk-build cli-build github-release docker; do assert_job_contains ".github/workflows/release.yml" \ "$job" \ @@ -192,6 +216,8 @@ assert_file_contains "docs/releasing.md" "Environment: none" assert_file_contains "docs/releasing.md" "--ref main -f tag=v0.8.25" assert_file_contains "docs/releasing.md" "only after the source PR CI is green" assert_file_contains "docs/releasing.md" "all ten packages" +assert_file_contains "docs/releasing.md" "expected source and workflow commit SHAs" +assert_file_contains "docs/releasing.md" "fails before npm publication" assert_file_contains "Taskfile.yml" "org=akua-dev repo=akua workflow=release-publish.yml environment=none" assert_file_excludes "Taskfile.yml" "org=cnap-tech repo=akua workflow=release.yml" assert_file_excludes "packages/sdk/README.md" "github.com/cnap-tech/akua" From 5246201d0809150732430b5d2690bef2dead94a5 Mon Sep 17 00:00:00 2001 From: Robin Date: Sun, 12 Jul 2026 04:55:09 +0200 Subject: [PATCH 3/9] no-mistakes(document): Refresh release documentation and lint guards --- CONTRIBUTING.md | 2 +- README.md | 10 +++++----- SECURITY.md | 2 +- docs/agent-usage.md | 6 +++--- packages/sdk/src/mod.ts | 2 +- scripts/check-release-workflows.sh | 2 ++ skills/README.md | 2 +- 7 files changed, 14 insertions(+), 12 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7e73023b..36aa1252 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -19,7 +19,7 @@ Prerequisites are managed via [mise](https://mise.jdx.dev/) — task, helmfile, wasm-pack, etc.) at pinned versions. ```bash -git clone git@github.com:cnap-tech/akua.git +git clone git@github.com:akua-dev/akua.git cd akua mise install diff --git a/README.md b/README.md index 6b34ba3a..972d51d7 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@

- Release + Release npm License

@@ -124,13 +124,13 @@ For cross-Package composition (install one Akua package on top of another, with curl -fsSL https://cli.akua.dev/install | sh # Homebrew -brew install cnap-tech/tap/akua +brew install akua-dev/tap/akua # Windows irm https://cli.akua.dev/install.ps1 | iex # From source -cargo install --git https://github.com/cnap-tech/akua akua-cli +cargo install --git https://github.com/akua-dev/akua akua-cli ``` ```sh @@ -138,10 +138,10 @@ cargo install --git https://github.com/cnap-tech/akua akua-cli bun add @akua-dev/sdk # Agent skills (universal — works across 25+ agents) -npx skills install github:cnap-tech/akua/skills +npx skills install github:akua-dev/akua/skills ``` -Prebuilt binaries: [Releases](https://github.com/cnap-tech/akua/releases). Container image: `ghcr.io/cnap-tech/akua`. Agent-specific setup: [`docs/agent-usage.md`](docs/agent-usage.md). +Prebuilt binaries: [Releases](https://github.com/akua-dev/akua/releases). Container image: `ghcr.io/akua-dev/akua`. Agent-specific setup: [`docs/agent-usage.md`](docs/agent-usage.md). ## Documentation diff --git a/SECURITY.md b/SECURITY.md index b232c882..1f4ee5de 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -139,6 +139,6 @@ retained as an escape hatch; it's safe only for trusted packages. ## Reporting Please report vulnerabilities privately via GitHub Security Advisories -at https://github.com/cnap-tech/akua/security/advisories/new. +at https://github.com/akua-dev/akua/security/advisories/new. Fixes are prioritized ahead of feature work; we'll coordinate disclosure timing with you. diff --git a/docs/agent-usage.md b/docs/agent-usage.md index 37c14ca7..51adc595 100644 --- a/docs/agent-usage.md +++ b/docs/agent-usage.md @@ -60,7 +60,7 @@ cp -r path/to/akua/skills/* ~/.claude/skills/ Install via the Codex skills manager: ```sh -codex skills install github:cnap-tech/akua/skills +codex skills install github:akua-dev/akua/skills ``` ### Cursor @@ -84,7 +84,7 @@ gemini extensions install @akua/skills All support the open [Agent Skills Specification](https://agentskills.io). Any of: - Symlink `skills/` into the agent's expected location -- Use `npx skills install github:cnap-tech/akua/skills` +- Use `npx skills install github:akua-dev/akua/skills` - Follow each agent's skill-installation instructions (linked from [agentskills.io/overview](https://agentskills.io/)) ### Universal: `npx skills` @@ -92,7 +92,7 @@ All support the open [Agent Skills Specification](https://agentskills.io). Any o The [Vercel Labs skills manager](https://github.com/vercel-labs/skills) works across all Agent Skills compatible agents: ```sh -npx skills install github:cnap-tech/akua/skills +npx skills install github:akua-dev/akua/skills npx skills list npx skills remove akua-* ``` diff --git a/packages/sdk/src/mod.ts b/packages/sdk/src/mod.ts index a69a6c08..ce2bafe8 100644 --- a/packages/sdk/src/mod.ts +++ b/packages/sdk/src/mod.ts @@ -459,7 +459,7 @@ export class Akua { * * Field-level docstrings become `description`; `@ui(...)` decorators * become OpenAPI-3.1-compliant `x-ui` extensions. See - * [`docs/cli.md`](https://github.com/cnap-tech/akua/blob/main/docs/cli.md#akua-export) + * [`docs/cli.md`](https://github.com/akua-dev/akua/blob/main/docs/cli.md#akua-export) * for the full schema contract. */ async export(opts: ExportOptions = {}): Promise> { diff --git a/scripts/check-release-workflows.sh b/scripts/check-release-workflows.sh index 49d0b8b5..8c8bfc60 100644 --- a/scripts/check-release-workflows.sh +++ b/scripts/check-release-workflows.sh @@ -1,4 +1,6 @@ #!/usr/bin/env bash +# Patterns passed to the assertion helpers intentionally remain literal. +# shellcheck disable=SC2016 set -euo pipefail line_in_job() { diff --git a/skills/README.md b/skills/README.md index 07438fc2..f014780d 100644 --- a/skills/README.md +++ b/skills/README.md @@ -47,7 +47,7 @@ These skills install wherever the Agent Skills Specification is supported. Use w **With `npx skills` (Vercel Labs skill manager):** ```sh -npx skills install github:cnap-tech/akua/skills +npx skills install github:akua-dev/akua/skills ``` **With Claude Code:** From 023e59a4962ca7862907f27b60872478a7fe01b7 Mon Sep 17 00:00:00 2001 From: Robin Date: Sun, 12 Jul 2026 10:02:23 +0200 Subject: [PATCH 4/9] fix(release): remove Akua core Homebrew ownership --- .github/workflows/release.yml | 119 +---------------------------- README.md | 3 - docs/releasing.md | 12 ++- docs/sdk-runtime-compat.md | 2 +- packages/sdk/README.md | 2 +- scripts/check-release-workflows.sh | 21 ++++- 6 files changed, 28 insertions(+), 131 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3d7cc237..9e8a66d0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,7 +12,6 @@ name: release # │ │ │ # │ └─> cli-build ─┬─> github-release ┬─> trigger-publish ──> release-publish.yml # │ └─> docker │ -# │ └─> homebrew-tap # └─ (version + is_prerelease propagated to every job) # # Why split publish into a separate workflow: `npm publish` is the @@ -40,8 +39,7 @@ name: release # # Prerelease handling: tags containing `-` (e.g. `v0.8.7-rc1`) publish # to the npm `next` dist-tag instead of `latest`, are marked as -# prereleases on GitHub Releases, skip `:latest` Docker tag, and skip -# the Homebrew tap bump entirely. Stable tags get the full surface. +# prereleases on GitHub Releases, and skip the `:latest` Docker tag. on: push: @@ -733,118 +731,3 @@ jobs: platforms: linux/amd64,linux/arm64 push: true tags: ${{ steps.tags.outputs.tags }} - - # --------------------------------------------------------------------------- - # homebrew-tap — auto-bump akua-dev/homebrew-tap's `Formula/akua.rb`. - # Skipped for prereleases (regular `brew install akua` users - # shouldn't get RC builds — they have to opt in via - # `brew install --HEAD akua` or pin a specific version). - # --------------------------------------------------------------------------- - homebrew-tap: - needs: [detect-version, github-release] - runs-on: ubuntu-latest - if: | - (github.event_name == 'push' || inputs.dry-run == false) && - needs.detect-version.outputs.is_prerelease == 'false' - steps: - - uses: actions/download-artifact@v8 - with: - pattern: 'akua-*' - path: dist - merge-multiple: true - - - name: Configure SSH for the tap repo - env: - TAP_BUMP_SSH_KEY: ${{ secrets.TAP_BUMP_SSH_KEY }} - run: | - mkdir -p ~/.ssh - chmod 700 ~/.ssh - printf '%s\n' "$TAP_BUMP_SSH_KEY" > ~/.ssh/tap_bump_key - chmod 600 ~/.ssh/tap_bump_key - ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null - cat > ~/.ssh/config <<'EOF' - Host github.com-tap - HostName github.com - User git - IdentityFile ~/.ssh/tap_bump_key - IdentitiesOnly yes - EOF - chmod 600 ~/.ssh/config - - - name: Clone tap, rewrite formula, push - env: - TAG: ${{ needs.detect-version.outputs.tag }} - VERSION: ${{ needs.detect-version.outputs.version }} - run: | - set -eu - - # Read each target's sha256 from the per-asset .sha256 file. - get_sha() { - awk '{print $1}' "dist/akua-${TAG}-$1.tar.gz.sha256" - } - darwin_arm_sha=$(get_sha aarch64-apple-darwin) - darwin_x86_sha=$(get_sha x86_64-apple-darwin) - linux_arm_sha=$(get_sha aarch64-unknown-linux-gnu) - linux_x86_sha=$(get_sha x86_64-unknown-linux-gnu) - - git clone git@github.com-tap:akua-dev/homebrew-tap.git tap - cd tap - git config user.email "akua-tap-bumper@akua.dev" - git config user.name "akua-tap-bumper" - - # Rewrite the whole formula. Simpler than four sed passes, - # and the formula shape is our own — no surprise stanzas - # another maintainer might have added mid-file. - cat > Formula/akua.rb <` tag drives `release 1. Land changes on `main`; `task ci` must be green. 2. Bump versions in `Cargo.toml`, `crates/akua-napi/package.json`, `packages/sdk/package.json`, all `crates/akua-napi/npm//package.json`, `crates/akua-native-engines-npm/package.json`. Commit `release: vX.Y.Z`. -3. Tag `v` and push. The release workflows handle npm, GitHub Release, Docker, and Homebrew. Prerelease tags like `v-rc1` publish to the npm `next` dist-tag and are marked as prereleases on GitHub. +3. Tag `v` and push. The release workflows handle npm, GitHub Release, and Docker. Prerelease tags like `v-rc1` publish to the npm `next` dist-tag and are marked as prereleases on GitHub. See [`docs/releasing.md`](../../docs/releasing.md) for the release and recovery contract. diff --git a/scripts/check-release-workflows.sh b/scripts/check-release-workflows.sh index 8c8bfc60..9edb6fe7 100644 --- a/scripts/check-release-workflows.sh +++ b/scripts/check-release-workflows.sh @@ -110,16 +110,24 @@ assert_before ".github/workflows/release.yml" \ "Bump package.json version + native dep pin from the tag" \ "Pack dry-run (manifest sanity)" -# Release-owned coordinates moved with the repository. Keep the image, release, -# and generated Homebrew formula identities on the current GitHub organization. +# Release-owned coordinates moved with the repository. Keep the image and +# release identities on the current GitHub organization. for workflow in .github/workflows/*.yml; do assert_file_excludes "$workflow" "cnap-tech" done assert_file_contains ".github/workflows/release.yml" "ghcr.io/akua-dev/akua:\${tag}" -assert_file_contains ".github/workflows/release.yml" "git@github.com-tap:akua-dev/homebrew-tap.git" -assert_file_contains ".github/workflows/release.yml" "homepage \"https://github.com/akua-dev/akua\"" assert_file_contains ".github/workflows/release.yml" "gh release upload \"\${TAG}\" dist/*.tar.gz dist/*.zip dist/*.sha256 --clobber" +# Homebrew belongs to akua-dev/cli and its dedicated tap lane. Akua core must +# never configure tap credentials, generate a formula, or dispatch a tap writer. +if grep -Eiq 'homebrew|TAP_BUMP|github\.com-tap|Formula/akua\.rb' .github/workflows/release.yml; then + echo "ERROR: release.yml contains an Akua-core Homebrew/tap side effect or ownership claim" >&2 + exit 1 +fi +assert_file_excludes "README.md" "brew install akua-dev/tap/akua" +assert_file_excludes "packages/sdk/README.md" "Homebrew" +assert_file_excludes "docs/sdk-runtime-compat.md" "Homebrew" + # A manual recovery runs workflow code from a green branch, but every source # checkout must resolve to the requested immutable tag. The workflow verifies # and propagates that commit; it must never create or move a tag. @@ -220,6 +228,11 @@ assert_file_contains "docs/releasing.md" "only after the source PR CI is green" assert_file_contains "docs/releasing.md" "all ten packages" assert_file_contains "docs/releasing.md" "expected source and workflow commit SHAs" assert_file_contains "docs/releasing.md" "fails before npm publication" +assert_file_contains "docs/releasing.md" "Homebrew is explicitly outside this recovery" +assert_file_contains "docs/releasing.md" "akua-dev/cli" +assert_file_contains "docs/releasing.md" 'dedicated `akua-dev/homebrew-tap` lane' +assert_file_excludes "docs/releasing.md" "updates Homebrew" +assert_file_excludes "docs/releasing.md" "updates the generated Homebrew" assert_file_contains "Taskfile.yml" "org=akua-dev repo=akua workflow=release-publish.yml environment=none" assert_file_excludes "Taskfile.yml" "org=cnap-tech repo=akua workflow=release.yml" assert_file_excludes "packages/sdk/README.md" "github.com/cnap-tech/akua" From d3fa727d36a3eb90b10f9aaa7b588f6c408fc24b Mon Sep 17 00:00:00 2001 From: Robin Date: Sun, 12 Jul 2026 10:34:59 +0200 Subject: [PATCH 5/9] no-mistakes(review): Harden SHA-bound release recovery and Homebrew guards --- .github/workflows/release.yml | 35 +++++++++++++++- docs/releasing.md | 18 +++++++-- scripts/check-release-workflows.sh | 65 ++++++++++++++++++++++++++---- 3 files changed, 106 insertions(+), 12 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9e8a66d0..c5e18812 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -54,6 +54,14 @@ on: tag: description: 'Tag name (e.g. v0.8.7) — required for manual runs' required: true + expected-source-commit: + description: 'Reviewed commit the tag must resolve to — required for manual runs' + required: true + type: string + expected-workflow-commit: + description: 'Reviewed commit containing this workflow — required for manual runs' + required: true + type: string permissions: contents: write # github-release: create Releases @@ -86,6 +94,11 @@ jobs: fetch-depth: 0 - id: parse + env: + EVENT_NAME: ${{ github.event_name }} + EXPECTED_SOURCE_COMMIT: ${{ inputs.expected-source-commit }} + EXPECTED_WORKFLOW_COMMIT: ${{ inputs.expected-workflow-commit }} + ACTUAL_WORKFLOW_COMMIT: ${{ github.workflow_sha }} run: | set -eu tag="${{ github.event.inputs.tag || github.ref_name }}" @@ -94,6 +107,24 @@ jobs: exit 1 fi source_commit=$(git rev-parse "refs/tags/${tag}^{commit}") + if [[ "$EVENT_NAME" == "workflow_dispatch" ]]; then + if [[ ! "$EXPECTED_SOURCE_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::expected source commit is not a full lowercase SHA" + exit 1 + fi + if [[ ! "$EXPECTED_WORKFLOW_COMMIT" =~ ^[0-9a-f]{40}$ ]]; then + echo "::error::expected workflow commit is not a full lowercase SHA" + exit 1 + fi + if [[ "$source_commit" != "$EXPECTED_SOURCE_COMMIT" ]]; then + echo "::error::$tag resolves to $source_commit, expected $EXPECTED_SOURCE_COMMIT" + exit 1 + fi + if [[ "$ACTUAL_WORKFLOW_COMMIT" != "$EXPECTED_WORKFLOW_COMMIT" ]]; then + echo "::error::workflow commit $ACTUAL_WORKFLOW_COMMIT does not match expected $EXPECTED_WORKFLOW_COMMIT" + exit 1 + fi + fi head_commit=$(git rev-parse 'HEAD^{commit}') if [[ "$head_commit" != "$source_commit" ]]; then echo "::error::checkout $head_commit does not match $tag at $source_commit" @@ -385,8 +416,8 @@ jobs: # works — no PAT needed. env: TAG: ${{ needs.detect-version.outputs.tag }} - EXPECTED_SOURCE_COMMIT: ${{ needs.detect-version.outputs.source_commit }} - EXPECTED_WORKFLOW_COMMIT: ${{ github.workflow_sha }} + EXPECTED_SOURCE_COMMIT: ${{ github.event_name == 'workflow_dispatch' && inputs.expected-source-commit || needs.detect-version.outputs.source_commit }} + EXPECTED_WORKFLOW_COMMIT: ${{ github.event_name == 'workflow_dispatch' && inputs.expected-workflow-commit || github.workflow_sha }} WORKFLOW_REF: ${{ github.ref_name }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | diff --git a/docs/releasing.md b/docs/releasing.md index 89cc56da..1f607cf6 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -55,11 +55,23 @@ lane. The publisher verifies that the tag still resolves to the expected source that `github.sha` is the reviewed workflow commit; if either ref advances, recovery fails before npm publication. -A captain may run recovery only after the source PR CI is green and an npm -administrator has confirmed the trusted-publisher identity above for all ten packages: +A captain may run recovery only after PR #69 CI is green, the corrected commit is +merged into `main`, that exact reviewed merge commit is verified as the current +`main` commit, and an npm administrator has confirmed the trusted-publisher identity +above for all ten packages: ```sh -gh workflow run release.yml --repo akua-dev/akua --ref main -f tag=v0.8.25 -f dry-run=false +reviewed_workflow_commit=$(gh pr view 69 --repo akua-dev/akua --json mergeCommit --jq '.mergeCommit.oid') +main_commit=$(gh api repos/akua-dev/akua/commits/main --jq '.sha') +test "$main_commit" = "$reviewed_workflow_commit" + +gh workflow run release.yml \ + --repo akua-dev/akua \ + --ref main \ + -f tag=v0.8.25 \ + -f expected-source-commit=6452eb662445d2ad7c108128f93b9c55138729bb \ + -f expected-workflow-commit="$reviewed_workflow_commit" \ + -f dry-run=false ``` That single deliberate dispatch rebuilds from the immutable tag, repairs the GitHub diff --git a/scripts/check-release-workflows.sh b/scripts/check-release-workflows.sh index 9edb6fe7..a1403e11 100644 --- a/scripts/check-release-workflows.sh +++ b/scripts/check-release-workflows.sh @@ -73,6 +73,31 @@ assert_file_excludes() { fi } +assert_file_excludes_pattern() { + local file="$1" + local pattern="$2" + + if grep -Eiq -- "$pattern" "$file"; then + echo "ERROR: $file matches forbidden pattern '$pattern'" >&2 + exit 1 + fi +} + +assert_dispatch_input_required() { + local file="$1" + local input="$2" + + if ! awk -v input="$input" ' + $0 == " " input ":" { in_input = 1; next } + in_input && $0 ~ /^ [^ ]/ { exit } + in_input && $0 == " required: true" { found = 1 } + END { exit !found } + ' "$file"; then + echo "ERROR: $file workflow_dispatch input '$input' must be required" >&2 + exit 1 + fi +} + # Release builds must install from the committed lockfile before mutating # package manifests to the tag version. Mutating first invalidates # --frozen-lockfile and can also force unpublished tag versions to resolve. @@ -124,19 +149,41 @@ if grep -Eiq 'homebrew|TAP_BUMP|github\.com-tap|Formula/akua\.rb' .github/workfl echo "ERROR: release.yml contains an Akua-core Homebrew/tap side effect or ownership claim" >&2 exit 1 fi -assert_file_excludes "README.md" "brew install akua-dev/tap/akua" -assert_file_excludes "packages/sdk/README.md" "Homebrew" -assert_file_excludes "docs/sdk-runtime-compat.md" "Homebrew" +for file in README.md packages/sdk/README.md docs/sdk-runtime-compat.md; do + assert_file_excludes_pattern "$file" 'brew[[:space:]]+install[^[:cntrl:]]*akua|homebrew[^[:cntrl:]]*(formula|tap|install|update|publish|release)|(^|[^[:alnum:]_])(formula|tap)([^[:alnum:]_]|$)[^[:cntrl:]]*akua' +done # A manual recovery runs workflow code from a green branch, but every source # checkout must resolve to the requested immutable tag. The workflow verifies # and propagates that commit; it must never create or move a tag. +for input in expected-source-commit expected-workflow-commit; do + assert_dispatch_input_required ".github/workflows/release.yml" "$input" +done +assert_job_contains ".github/workflows/release.yml" \ + "detect-version" \ + 'EXPECTED_SOURCE_COMMIT: ${{ inputs.expected-source-commit }}' +assert_job_contains ".github/workflows/release.yml" \ + "detect-version" \ + 'EXPECTED_WORKFLOW_COMMIT: ${{ inputs.expected-workflow-commit }}' +assert_job_contains ".github/workflows/release.yml" \ + "detect-version" \ + 'ACTUAL_WORKFLOW_COMMIT: ${{ github.workflow_sha }}' +assert_job_contains ".github/workflows/release.yml" \ + "detect-version" \ + 'if [[ "$source_commit" != "$EXPECTED_SOURCE_COMMIT" ]]' +assert_job_contains ".github/workflows/release.yml" \ + "detect-version" \ + 'if [[ "$ACTUAL_WORKFLOW_COMMIT" != "$EXPECTED_WORKFLOW_COMMIT" ]]' assert_job_contains ".github/workflows/release.yml" \ "detect-version" \ 'git rev-parse "refs/tags/${tag}^{commit}"' assert_file_contains ".github/workflows/release.yml" "source_commit: \${{ steps.parse.outputs.source_commit }}" -assert_file_contains ".github/workflows/release.yml" "EXPECTED_SOURCE_COMMIT: \${{ needs.detect-version.outputs.source_commit }}" -assert_file_contains ".github/workflows/release.yml" "EXPECTED_WORKFLOW_COMMIT: \${{ github.workflow_sha }}" +assert_job_contains ".github/workflows/release.yml" \ + "trigger-publish" \ + "inputs.expected-source-commit" +assert_job_contains ".github/workflows/release.yml" \ + "trigger-publish" \ + "inputs.expected-workflow-commit" assert_file_contains ".github/workflows/release.yml" 'WORKFLOW_REF: ${{ github.ref_name }}' assert_job_contains ".github/workflows/release.yml" \ "trigger-publish" \ @@ -223,8 +270,12 @@ assert_job_contains ".github/workflows/release.yml" \ assert_file_contains "docs/releasing.md" "akua-dev/akua" assert_file_contains "docs/releasing.md" "release-publish.yml" assert_file_contains "docs/releasing.md" "Environment: none" -assert_file_contains "docs/releasing.md" "--ref main -f tag=v0.8.25" -assert_file_contains "docs/releasing.md" "only after the source PR CI is green" +assert_file_contains "docs/releasing.md" "--ref main" +assert_file_contains "docs/releasing.md" "-f expected-source-commit=6452eb662445d2ad7c108128f93b9c55138729bb" +assert_file_contains "docs/releasing.md" '-f expected-workflow-commit="$reviewed_workflow_commit"' +assert_file_contains "docs/releasing.md" 'merged into `main`' +assert_file_contains "docs/releasing.md" 'test "$main_commit" = "$reviewed_workflow_commit"' +assert_file_contains "docs/releasing.md" "CI is green" assert_file_contains "docs/releasing.md" "all ten packages" assert_file_contains "docs/releasing.md" "expected source and workflow commit SHAs" assert_file_contains "docs/releasing.md" "fails before npm publication" From 270805e71fe397918030540844e22830f9e6fc0c Mon Sep 17 00:00:00 2001 From: Robin Date: Sun, 12 Jul 2026 14:08:53 +0200 Subject: [PATCH 6/9] no-mistakes(document): Align release recovery documentation and formatting --- AGENTS.md | 2 +- .../akua-napi/npm/darwin-arm64/package.json | 2 +- crates/akua-napi/npm/darwin-x64/package.json | 2 +- .../npm/linux-arm64-gnu/package.json | 2 +- .../npm/linux-arm64-musl/package.json | 2 +- .../akua-napi/npm/linux-x64-gnu/package.json | 2 +- .../akua-napi/npm/linux-x64-musl/package.json | 2 +- .../akua-napi/npm/win32-x64-msvc/package.json | 2 +- docs/releasing.md | 21 +++++++++++++++---- packages/sdk/README.md | 9 ++------ scripts/aur/akua-bin/PKGBUILD | 6 +++--- 11 files changed, 30 insertions(+), 22 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 697ad554..64613edc 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -103,7 +103,7 @@ That's it. akua does **not** specify `App`, `Environment`, `Cluster`, `Secret`, **Embedded-engine builds + feature wiring** (these cost real time when missed): the engine wasm assets (`crates/{helm,kustomize}-engine-wasm/assets/*.wasm`) are **gitignored** — a fresh clone or `git worktree` can't compile `akua-core` until `task build:engines` runs (or you copy the built assets in). The Go engine source (`crates/helm-engine-wasm/go-src/`) rebuilds via `task build:helm-engine-wasm` (Go→wasip1) — a *different* artifact from the Rust render-worker's `task build:render-worker`; when you change Go engine code, rebuild the engine, not the worker. And a new `akua-core` feature is **dead in the shipped binary/SDK** unless it's added to the `akua-core` dep `features = [...]` in **both** `crates/akua-cli/Cargo.toml` and `crates/akua-napi/Cargo.toml` — otherwise it silently compiles to its `#[cfg(not(feature))]` stub there even though `cargo test -p akua-core --features …` passes. -**Releases are tag-triggered and expensive — batch them.** A pushed `v*` tag fires the full build matrix (Windows + macOS jobs dominate at ~30min and can't be self-hosted) plus npm + Homebrew. Accumulate fixes on `main` and cut **one** tag when a human explicitly asks — never tag per change/chunk. The release derives its version from the tag (`scripts/set-cargo-version.sh` + a smoke-test guard asserting `akua -V == tag`); the committed `Cargo.toml` version is a dev placeholder, so don't expect hand-bumping it to affect a release. If a tagged release is wrong, cancel the run before `github-release` publishes, then delete + re-push the tag. +**Releases are tag-triggered and expensive — batch them.** A pushed `v*` tag fires the full build matrix (Windows + macOS jobs dominate at ~30min and can't be self-hosted), npm publishing, and container publishing. Accumulate fixes on `main` and cut **one** tag when a human explicitly asks — never tag per change/chunk. The release derives its version from the tag (`scripts/set-cargo-version.sh` + a smoke-test guard asserting `akua -V == tag`); the committed `Cargo.toml` version is a dev placeholder, so don't expect hand-bumping it to affect a release. Tags are immutable: never delete and re-push one. Follow [docs/releasing.md](docs/releasing.md) for the release and fail-closed recovery contract. **Project domain is `akua.dev`.** Reverse-DNS namespaces (OCI annotations, Java-style package roots, anything following the `org.kcllang.*` shape) use **`dev.akua.*`** — *not* `org.akua.*`. The npm scope is `@akua-dev` because npm scopes have to be unique on the registry and bare `@akua` was taken; the scope name doesn't follow the reverse-DNS rule. diff --git a/crates/akua-napi/npm/darwin-arm64/package.json b/crates/akua-napi/npm/darwin-arm64/package.json index 05ebe7bc..603e35a4 100644 --- a/crates/akua-napi/npm/darwin-arm64/package.json +++ b/crates/akua-napi/npm/darwin-arm64/package.json @@ -24,7 +24,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/akua-dev/akua.git" + "url": "git+https://github.com/akua-dev/akua.git" }, "os": [ "darwin" diff --git a/crates/akua-napi/npm/darwin-x64/package.json b/crates/akua-napi/npm/darwin-x64/package.json index 6a555189..0cb4eb7a 100644 --- a/crates/akua-napi/npm/darwin-x64/package.json +++ b/crates/akua-napi/npm/darwin-x64/package.json @@ -24,7 +24,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/akua-dev/akua.git" + "url": "git+https://github.com/akua-dev/akua.git" }, "os": [ "darwin" diff --git a/crates/akua-napi/npm/linux-arm64-gnu/package.json b/crates/akua-napi/npm/linux-arm64-gnu/package.json index cf090821..1f561afb 100644 --- a/crates/akua-napi/npm/linux-arm64-gnu/package.json +++ b/crates/akua-napi/npm/linux-arm64-gnu/package.json @@ -24,7 +24,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/akua-dev/akua.git" + "url": "git+https://github.com/akua-dev/akua.git" }, "os": [ "linux" diff --git a/crates/akua-napi/npm/linux-arm64-musl/package.json b/crates/akua-napi/npm/linux-arm64-musl/package.json index 0d6a717b..fbee9bc1 100644 --- a/crates/akua-napi/npm/linux-arm64-musl/package.json +++ b/crates/akua-napi/npm/linux-arm64-musl/package.json @@ -24,7 +24,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/akua-dev/akua.git" + "url": "git+https://github.com/akua-dev/akua.git" }, "os": [ "linux" diff --git a/crates/akua-napi/npm/linux-x64-gnu/package.json b/crates/akua-napi/npm/linux-x64-gnu/package.json index c08fba7f..48904d57 100644 --- a/crates/akua-napi/npm/linux-x64-gnu/package.json +++ b/crates/akua-napi/npm/linux-x64-gnu/package.json @@ -24,7 +24,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/akua-dev/akua.git" + "url": "git+https://github.com/akua-dev/akua.git" }, "os": [ "linux" diff --git a/crates/akua-napi/npm/linux-x64-musl/package.json b/crates/akua-napi/npm/linux-x64-musl/package.json index ec9ff5f1..7f228416 100644 --- a/crates/akua-napi/npm/linux-x64-musl/package.json +++ b/crates/akua-napi/npm/linux-x64-musl/package.json @@ -24,7 +24,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/akua-dev/akua.git" + "url": "git+https://github.com/akua-dev/akua.git" }, "os": [ "linux" diff --git a/crates/akua-napi/npm/win32-x64-msvc/package.json b/crates/akua-napi/npm/win32-x64-msvc/package.json index 9e8ec45b..b06ab13b 100644 --- a/crates/akua-napi/npm/win32-x64-msvc/package.json +++ b/crates/akua-napi/npm/win32-x64-msvc/package.json @@ -24,7 +24,7 @@ }, "repository": { "type": "git", - "url": "git+https://github.com/akua-dev/akua.git" + "url": "git+https://github.com/akua-dev/akua.git" }, "os": [ "win32" diff --git a/docs/releasing.md b/docs/releasing.md index 1f607cf6..2ce41101 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -11,6 +11,19 @@ Homebrew is explicitly outside this recovery. Formula ownership belongs to `akua-dev/cli` and the dedicated `akua-dev/homebrew-tap` lane; Akua core does not write, dispatch, or claim ownership of that formula. +## Normal releases + +Land and verify release changes on `main`, batch them, and push one immutable +`v` tag only when a human explicitly requests the release. The tag triggers +the build workflow, which derives the workspace and package version from the tag; +the committed `Cargo.toml` version remains a development placeholder. Stable tags +publish npm packages under `latest` and update the container's `latest` tag. +Prerelease tags publish npm packages under `next`, mark the GitHub Release as a +prerelease, and do not update the container's `latest` tag. + +Do not delete and re-push a release tag. If a tagged run fails, use the SHA-bound +recovery path below after correcting and reviewing the workflow on `main`. + ## npm trusted publisher contract npm trusted-publisher configuration is external registry state. Source code can @@ -55,10 +68,10 @@ lane. The publisher verifies that the tag still resolves to the expected source that `github.sha` is the reviewed workflow commit; if either ref advances, recovery fails before npm publication. -A captain may run recovery only after PR #69 CI is green, the corrected commit is -merged into `main`, that exact reviewed merge commit is verified as the current -`main` commit, and an npm administrator has confirmed the trusted-publisher identity -above for all ten packages: +A captain may run this recovery only after PR #69 CI is green, the corrected +commit is merged into `main`, that exact reviewed merge commit is verified as the +current `main` commit, and an npm administrator has confirmed the +trusted-publisher identity above for all ten packages: ```sh reviewed_workflow_commit=$(gh pr view 69 --repo akua-dev/akua --json mergeCommit --jq '.mergeCommit.oid') diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 96a6a377..2d5d7b28 100644 --- a/packages/sdk/README.md +++ b/packages/sdk/README.md @@ -79,13 +79,8 @@ task sdk:publish:check # npm pack --dry-run ## Release flow -SDK versions track the Rust workspace version. A `v` tag drives `release.yml`, which builds the wasm bundle once and fans out into the native and CLI matrices. It then dispatches `release-publish.yml`, which publishes engines → per-platform native → meta-native → SDK from durable GitHub Release assets. - -1. Land changes on `main`; `task ci` must be green. -2. Bump versions in `Cargo.toml`, `crates/akua-napi/package.json`, `packages/sdk/package.json`, all `crates/akua-napi/npm//package.json`, `crates/akua-native-engines-npm/package.json`. Commit `release: vX.Y.Z`. -3. Tag `v` and push. The release workflows handle npm, GitHub Release, and Docker. Prerelease tags like `v-rc1` publish to the npm `next` dist-tag and are marked as prereleases on GitHub. - -See [`docs/releasing.md`](../../docs/releasing.md) for the release and recovery contract. +SDK versions track the release tag. See [`docs/releasing.md`](../../docs/releasing.md) +for the authoritative release and recovery contract. ## Still coming diff --git a/scripts/aur/akua-bin/PKGBUILD b/scripts/aur/akua-bin/PKGBUILD index d2d30804..cdd56505 100644 --- a/scripts/aur/akua-bin/PKGBUILD +++ b/scripts/aur/akua-bin/PKGBUILD @@ -11,9 +11,9 @@ # git add PKGBUILD .SRCINFO && git commit -m "akua-bin " # git push # -# Version bumps happen via the cli-release workflow's Homebrew bump -# job; we'd need to add a separate AUR-push job once the maintainer -# SSH key is in CI. For now this is manual. +# AUR version bumps are manual. Automation would require a dedicated +# AUR-push lane and maintainer SSH key; the Akua core release does not +# update external package-manager repositories. pkgname=akua-bin _pkgname=akua From 6a09b2b0a8c063078608ab4b22c16abb91e93161 Mon Sep 17 00:00:00 2001 From: Robin Date: Sat, 18 Jul 2026 21:30:05 +0200 Subject: [PATCH 7/9] fix(release): preserve immutable recovery assets Rationale: recovery of v0.8.25 must leave all 19 existing GitHub Release assets byte-for-byte unchanged while retaining a SHA-bound source-build path for future authorized npm and GHCR recovery.\n\nRejected: replacing named release assets or using --clobber; that conflicts with the immutable-release policy.\n\nRisk: publication now relies on short-lived artifacts from the exact recovery source run; the publisher rejects an unbound run or mismatched tag/source/workflow manifest.\n\nTested: task release:check-workflows; git diff --check; bash -n scripts/check-release-workflows.sh; actionlint .github/workflows/release.yml .github/workflows/release-publish.yml (one pre-existing informational SC2086 at release.yml:533).\n\nNot-tested: no workflow, release, npm, GHCR, credential, or provider action was dispatched or used. --- .github/workflows/release-publish.yml | 105 ++++++++++++++-------- .github/workflows/release.yml | 124 +++++++++++--------------- docs/releasing.md | 27 +++--- scripts/check-release-workflows.sh | 36 +++++++- 4 files changed, 171 insertions(+), 121 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index 7663fbfd..8e25410a 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -6,15 +6,14 @@ name: release-publish # from `release.yml` so the retry boundary is scoped: # # release.yml — tag-triggered. Builds binaries, creates the -# GitHub Release, uploads native + wasm + sdk -# tarballs as Release assets, then dispatches -# this workflow. +# GitHub Release, then dispatches this workflow +# with the source-run artifact identity. # release-publish.yml — workflow_dispatch with `tag` input. Downloads -# Release assets and runs `npm publish`. +# source-run artifacts and runs `npm publish`. # # When publish fails (npm hiccup, OIDC config drift, version-pin -# mismatch, …), recovery re-dispatches this workflow with the tag and -# the expected source/workflow commits. Already-published versions skip. +# mismatch, …), recovery starts a new source build and dispatches this +# workflow with that run's verified artifacts. Already-published versions skip. # # Per GitHub Actions docs: `workflow_dispatch` is explicitly carved # out of the GITHUB_TOKEN no-recursion rule, so the upstream workflow @@ -22,8 +21,8 @@ name: release-publish # # Recovery is a new deliberate workflow_dispatch from a reviewed workflow # head. Do not rerun a failed historical run: the requested tag remains the -# immutable source and Release-asset identity while the workflow fix comes -# from the explicitly selected dispatch ref. See docs/releasing.md. +# immutable source and Release assets remain untouched while the workflow fix +# comes from the explicitly selected dispatch ref. See docs/releasing.md. on: workflow_dispatch: @@ -40,6 +39,10 @@ on: description: 'Commit containing this reviewed workflow.' required: true type: string + source-run-id: + description: 'Release workflow run whose artifacts are the publication inputs.' + required: true + type: string dry-run: description: 'Pack only, do not publish.' required: false @@ -47,7 +50,8 @@ on: default: false permissions: - contents: read # gh release download + actions: read # gh run download + contents: read id-token: write # npm publish: OIDC trusted publishing jobs: @@ -59,6 +63,7 @@ jobs: is_prerelease: ${{ steps.detect.outputs.is_prerelease }} npm_dist_tag: ${{ steps.detect.outputs.npm_dist_tag }} source_commit: ${{ steps.detect.outputs.source_commit }} + source_run_id: ${{ steps.detect.outputs.source_run_id }} steps: - uses: actions/checkout@v6 with: @@ -70,7 +75,9 @@ jobs: TAG: ${{ inputs.tag }} EXPECTED_SOURCE_COMMIT: ${{ inputs.expected-source-commit }} EXPECTED_WORKFLOW_COMMIT: ${{ inputs.expected-workflow-commit }} + SOURCE_RUN_ID: ${{ inputs.source-run-id }} ACTUAL_WORKFLOW_COMMIT: ${{ github.sha }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | set -eu if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then @@ -85,10 +92,44 @@ jobs: echo "::error::expected workflow commit is not a full lowercase SHA" exit 1 fi + if [[ ! "$SOURCE_RUN_ID" =~ ^[0-9]+$ ]]; then + echo "::error::source run ID must be numeric" + exit 1 + fi if [[ "$ACTUAL_WORKFLOW_COMMIT" != "$EXPECTED_WORKFLOW_COMMIT" ]]; then echo "::error::workflow commit $ACTUAL_WORKFLOW_COMMIT does not match expected $EXPECTED_WORKFLOW_COMMIT" exit 1 fi + run_json=$(gh run view "$SOURCE_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --json workflowName,status,conclusion,headSha) + if ! jq -e --arg expected_workflow "$EXPECTED_WORKFLOW_COMMIT" ' + .workflowName == "release" + and ( + .status == "in_progress" + or (.status == "completed" and .conclusion == "success") + ) + and .headSha == $expected_workflow + ' <<< "$run_json" > /dev/null; then + echo "::error::source run is not the active or successful release workflow at the expected workflow commit" + exit 1 + fi + mkdir source-run + gh run download "$SOURCE_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --name release-inputs \ + --dir source-run + if ! jq -e \ + --arg tag "$TAG" \ + --arg source_commit "$EXPECTED_SOURCE_COMMIT" \ + --arg workflow_commit "$EXPECTED_WORKFLOW_COMMIT" ' + .tag == $tag + and .source_commit == $source_commit + and .workflow_commit == $workflow_commit + ' source-run/release-inputs.json > /dev/null; then + echo "::error::source-run artifact identity does not match the requested immutable recovery inputs" + exit 1 + fi source_commit=$(git rev-parse "refs/tags/${TAG}^{commit}") if [[ "$source_commit" != "$EXPECTED_SOURCE_COMMIT" ]]; then echo "::error::$TAG resolves to $source_commit, expected $EXPECTED_SOURCE_COMMIT" @@ -113,6 +154,7 @@ jobs: echo "is_prerelease=$is_prerelease" echo "npm_dist_tag=$npm_dist_tag" echo "source_commit=$source_commit" + echo "source_run_id=$SOURCE_RUN_ID" } >> "$GITHUB_OUTPUT" echo "Will publish $version under dist-tag: $npm_dist_tag" @@ -150,28 +192,23 @@ jobs: crates/akua-napi/package.json > crates/akua-napi/package.json.tmp mv crates/akua-napi/package.json.tmp crates/akua-napi/package.json - - name: Download release assets (native + wasm) + - name: Download source-run artifacts (native + wasm) env: - TAG: ${{ needs.detect-version.outputs.tag }} + SOURCE_RUN_ID: ${{ needs.detect-version.outputs.source_run_id }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_REPO: ${{ github.repository }} run: | set -eu mkdir -p crates/akua-napi/artifacts cd crates/akua-napi/artifacts - # Each `native-` and `wasm-bundle` was uploaded as - # `.tar.gz` by release.yml's trigger-publish job. - gh release download "$TAG" \ - --pattern 'native-*.tar.gz' \ - --pattern 'wasm-bundle.tar.gz' - for f in native-*.tar.gz; do - name="${f%.tar.gz}" - mkdir "$name" - tar -xzf "$f" -C "$name" - rm "$f" - done - mkdir wasm-bundle - tar -xzf wasm-bundle.tar.gz -C wasm-bundle - rm wasm-bundle.tar.gz + gh run download "$SOURCE_RUN_ID" \ + --repo "$GH_REPO" \ + --pattern 'native-*' \ + --dir . + gh run download "$SOURCE_RUN_ID" \ + --repo "$GH_REPO" \ + --name wasm-bundle \ + --dir wasm-bundle ls -R - name: Generate per-platform npm dirs @@ -258,23 +295,19 @@ jobs: with: node-version: '24' - - name: Download release asset (sdk-staged) - # `gh release download` infers the repo from the cwd's git - # config — pass --repo explicitly so the job doesn't need a - # checkout (the sdk staging tree is fully reconstructed from - # the asset tarball). + - name: Download source-run artifact (sdk-staged) env: - TAG: ${{ needs.detect-version.outputs.tag }} + SOURCE_RUN_ID: ${{ needs.detect-version.outputs.source_run_id }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} run: | set -eu mkdir sdk - cd sdk - gh release download "$TAG" --pattern 'sdk-staged.tar.gz' --repo "$GH_REPO" - tar -xzf sdk-staged.tar.gz - rm sdk-staged.tar.gz - ls -la + gh run download "$SOURCE_RUN_ID" \ + --repo "$GH_REPO" \ + --name sdk-staged \ + --dir sdk + ls -la sdk - name: Pack dry-run (manifest sanity) working-directory: sdk diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c5e18812..ef8d3037 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,10 +22,9 @@ name: release # See docs/releasing.md for the SHA-bound recovery dispatch. # # re-runs publish without repeating successful npm versions. The build outputs -# this workflow produces (native-*.tar.gz / wasm-bundle.tar.gz / -# sdk-staged.tar.gz) are uploaded to the GitHub Release as durable -# assets, addressable by tag, with no 90-day workflow-artifact TTL. -# The publish workflow downloads them via `gh release download`. +# remain Actions artifacts for the source run; a recovery preserves existing +# GitHub Release assets byte-for-byte and publishes only from the exact verified +# source-run artifacts. # # `trigger-publish` calls `gh workflow run release-publish.yml` — # `workflow_dispatch` is explicitly carved out of the GITHUB_TOKEN @@ -149,6 +148,28 @@ jobs: } >> "$GITHUB_OUTPUT" echo "Resolved: tag=$tag commit=$source_commit version=$version prerelease=$is_prerelease npm_dist_tag=$npm_dist_tag" + - name: Record immutable recovery inputs + env: + TAG: ${{ steps.parse.outputs.tag }} + SOURCE_COMMIT: ${{ steps.parse.outputs.source_commit }} + WORKFLOW_COMMIT: ${{ github.workflow_sha }} + run: | + set -eu + jq -n \ + --arg tag "$TAG" \ + --arg source_commit "$SOURCE_COMMIT" \ + --arg workflow_commit "$WORKFLOW_COMMIT" \ + '{tag: $tag, source_commit: $source_commit, workflow_commit: $workflow_commit}' \ + > release-inputs.json + + - name: Upload immutable recovery inputs + uses: actions/upload-artifact@v4 + with: + name: release-inputs + path: release-inputs.json + if-no-files-found: error + retention-days: 1 + # --------------------------------------------------------------------------- # wasm-bundle — engine + render-worker wasm artefacts built once on # Linux. wasm32-wasip1 is byte-deterministic across runners, so the @@ -351,21 +372,14 @@ jobs: retention-days: 1 # --------------------------------------------------------------------------- - # native-publish — stages npm packages from the matrix artefacts + - # the wasm bundle, publishes via OIDC trusted publishing. - # --------------------------------------------------------------------------- - # --------------------------------------------------------------------------- - # trigger-publish — last step of release.yml. Bundles workflow-artifact - # outputs (native binaries, wasm engines, staged sdk tree) into - # tarballs, uploads them to the GitHub Release as durable assets, and - # dispatches `release-publish.yml` which runs `npm publish` against - # those assets. + # trigger-publish — last step of release.yml. Dispatches + # `release-publish.yml` with this run ID so it can consume the verified + # native binaries, wasm engines, and staged SDK from Actions artifacts. + # Recovery must never alter the immutable GitHub Release assets. # - # Why split: `npm publish` is the most failure-prone step in the - # pipeline (OIDC config drift, registry hiccups, version-pin - # mismatch). Putting it in its own dispatchable workflow means a - # publish-only retry takes ~1 minute instead of re-running the full - # build matrix (~hours). See release-publish.yml for the publish + # Why split: npm publication has its own idempotency boundary, but every + # recovery starts a fresh immutable source build so publish inputs never + # depend on mutable Release assets. See release-publish.yml for the publish # logic itself. # --------------------------------------------------------------------------- trigger-publish: @@ -373,43 +387,6 @@ jobs: runs-on: ubuntu-latest if: github.event_name == 'push' || inputs.dry-run == false steps: - - uses: actions/download-artifact@v4 - with: - # Same filter as the old native-publish step — without it, - # download-artifact@v4 also pulls the docker job's - # `akua-dev~akua~.dockerbuild` provenance artifact whose - # `~`-containing name fails path validation. - pattern: '{native-*,wasm-bundle,sdk-staged}' - path: artifacts - - - name: Bundle artifacts into release-asset tarballs - # Each per-platform `native-` and the shared `wasm-bundle` - # / `sdk-staged` directories get tar+gz'd. Downloading from a - # release is one HTTP request per file; the .tar.gz wrapper keeps - # internal directory layout intact (e.g. wasm-bundle preserves - # `crates//assets/.wasm`) so release-publish.yml - # can reproduce the workflow-artifact view byte-for-byte. - run: | - set -eu - mkdir -p bundles - for d in artifacts/native-*; do - name=$(basename "$d") - tar -czf "bundles/${name}.tar.gz" -C "$d" . - done - tar -czf bundles/wasm-bundle.tar.gz -C artifacts/wasm-bundle . - tar -czf bundles/sdk-staged.tar.gz -C artifacts/sdk-staged . - ls -la bundles/ - - - name: Upload bundles to the GitHub Release - # `--clobber` means re-runs of trigger-publish are idempotent - # (overwrites existing assets with the same name). - env: - TAG: ${{ needs.detect-version.outputs.tag }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - set -eu - gh release upload "$TAG" bundles/*.tar.gz --clobber --repo "$GITHUB_REPOSITORY" - - name: Dispatch release-publish.yml # workflow_dispatch is explicitly carved out of the GITHUB_TOKEN # no-recursion rule (per Actions docs), so the default token @@ -427,7 +404,8 @@ jobs: --ref "$WORKFLOW_REF" \ -f tag="$TAG" \ -f expected-source-commit="$EXPECTED_SOURCE_COMMIT" \ - -f expected-workflow-commit="$EXPECTED_WORKFLOW_COMMIT" + -f expected-workflow-commit="$EXPECTED_WORKFLOW_COMMIT" \ + -f source-run-id="$GITHUB_RUN_ID" echo "Dispatched release-publish.yml with tag=$TAG source=$EXPECTED_SOURCE_COMMIT workflow=$EXPECTED_WORKFLOW_COMMIT. Watch:" echo " https://github.com/$GITHUB_REPOSITORY/actions/workflows/release-publish.yml" @@ -674,7 +652,7 @@ jobs: pattern: 'akua-*' path: dist merge-multiple: true - - name: Create or update release + - name: Create or verify immutable release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_REPO: ${{ github.repository }} @@ -685,23 +663,23 @@ jobs: run: | set -eu notes="See [CHANGELOG.md](https://github.com/akua-dev/akua/blob/main/CHANGELOG.md) for details." - if gh release view "${TAG}" --repo "${GH_REPO}" > /dev/null 2>&1; then - gh release upload "${TAG}" dist/*.tar.gz dist/*.zip dist/*.sha256 --clobber \ - --repo "${GH_REPO}" - else - flags=() - if [ "${IS_PRERELEASE}" = "true" ]; then - flags+=(--prerelease) - fi - gh release create "${TAG}" \ - --repo "${GH_REPO}" \ - --verify-tag \ - --target "${SOURCE_COMMIT}" \ - --title "akua ${VERSION}" \ - --notes "${notes}" \ - "${flags[@]}" \ - dist/*.tar.gz dist/*.zip dist/*.sha256 + if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]; then + gh release view "${TAG}" --repo "${GH_REPO}" > /dev/null + echo "Verified existing immutable Release ${TAG}; recovery does not upload or replace assets." + exit 0 + fi + flags=() + if [ "${IS_PRERELEASE}" = "true" ]; then + flags+=(--prerelease) fi + gh release create "${TAG}" \ + --repo "${GH_REPO}" \ + --verify-tag \ + --target "${SOURCE_COMMIT}" \ + --title "akua ${VERSION}" \ + --notes "${notes}" \ + "${flags[@]}" \ + dist/*.tar.gz dist/*.zip dist/*.sha256 # --------------------------------------------------------------------------- # docker — multi-arch image at `ghcr.io/akua-dev/akua:`. Skips diff --git a/docs/releasing.md b/docs/releasing.md index 2ce41101..1b833917 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -1,11 +1,12 @@ # Releasing akua The build and publish lanes are intentionally separate. `.github/workflows/release.yml` -builds the immutable tag, updates the GitHub Release assets with clobber semantics, -pushes the container, and dispatches -`.github/workflows/release-publish.yml`. The publish workflow downloads the assets -owned by that tag and publishes npm packages in dependency order. Neither lane -creates or moves a tag during recovery. +builds the immutable tag, creates Release assets only for a new tag push, pushes the +container, and dispatches `.github/workflows/release-publish.yml` with the source-run +artifact identity. The publish workflow downloads those Actions artifacts and +publishes npm packages in dependency order. A recovery verifies the existing Release +but never uploads, replaces, or otherwise changes its assets. Neither lane creates or +moves a tag during recovery. Homebrew is explicitly outside this recovery. Formula ownership belongs to `akua-dev/cli` and the dedicated `akua-dev/homebrew-tap` lane; Akua core does not @@ -61,8 +62,12 @@ Recovery is manual and fail-closed. Do not rerun either failed run, retag `v0.8.25`, recreate its GitHub Release, or upload assets by hand. The existing tag resolves to commit `6452eb662445d2ad7c108128f93b9c55138729bb`; the manual build lane checks out that tag, verifies and propagates its commit, and never builds the current -workflow head as the tagged source. Existing Release assets are overwritten by name, -and npm versions already present are skipped before the missing packages publish. +workflow head as the tagged source. Existing Release assets are verified and left +byte-for-byte unchanged, and npm versions already present are skipped before the +missing packages publish. +Recovery starts a new full source build so the publisher can consume that run's +short-lived verified Actions artifacts; do not dispatch `release-publish.yml` alone +with a stale or guessed run ID. The build lane passes its expected source and workflow commit SHAs to the publish lane. The publisher verifies that the tag still resolves to the expected source and that `github.sha` is the reviewed workflow commit; if either ref advances, recovery @@ -87,7 +92,7 @@ gh workflow run release.yml \ -f dry-run=false ``` -That single deliberate dispatch rebuilds from the immutable tag, repairs the GitHub -Release assets, publishes `ghcr.io/akua-dev/akua`, and then dispatches the idempotent -npm publish lane. It has no Homebrew side effects. There are no automatic recovery -retries. +That single deliberate dispatch rebuilds from the immutable tag, verifies the existing +GitHub Release without changing any asset bytes, publishes `ghcr.io/akua-dev/akua`, +and then dispatches the idempotent npm publish lane from that run's artifacts. It has +no Homebrew side effects. There are no automatic recovery retries. diff --git a/scripts/check-release-workflows.sh b/scripts/check-release-workflows.sh index a1403e11..95b06599 100644 --- a/scripts/check-release-workflows.sh +++ b/scripts/check-release-workflows.sh @@ -141,7 +141,41 @@ for workflow in .github/workflows/*.yml; do assert_file_excludes "$workflow" "cnap-tech" done assert_file_contains ".github/workflows/release.yml" "ghcr.io/akua-dev/akua:\${tag}" -assert_file_contains ".github/workflows/release.yml" "gh release upload \"\${TAG}\" dist/*.tar.gz dist/*.zip dist/*.sha256 --clobber" +assert_file_excludes_pattern ".github/workflows/release.yml" 'gh[[:space:]]+release[[:space:]]+upload' +assert_file_excludes ".github/workflows/release.yml" "--clobber" +assert_file_excludes ".github/workflows/release-publish.yml" "gh release download" +assert_file_contains ".github/workflows/release-publish.yml" "source-run-id:" +assert_file_contains ".github/workflows/release.yml" '-f source-run-id="$GITHUB_RUN_ID"' +assert_job_contains ".github/workflows/release.yml" \ + "detect-version" \ + "release-inputs.json" +assert_job_contains ".github/workflows/release-publish.yml" \ + "detect-version" \ + 'gh run view "$SOURCE_RUN_ID"' +assert_job_contains ".github/workflows/release-publish.yml" \ + "detect-version" \ + '.status == "in_progress"' +assert_job_contains ".github/workflows/release-publish.yml" \ + "detect-version" \ + '.status == "completed"' +assert_job_contains ".github/workflows/release-publish.yml" \ + "detect-version" \ + '.conclusion == "success"' +assert_job_contains ".github/workflows/release-publish.yml" \ + "detect-version" \ + "release-inputs" +assert_job_contains ".github/workflows/release.yml" \ + "trigger-publish" \ + '-f expected-workflow-commit="$EXPECTED_WORKFLOW_COMMIT" \\' +assert_job_contains ".github/workflows/release-publish.yml" \ + "native-publish" \ + "--pattern 'native-*'" +assert_job_contains ".github/workflows/release-publish.yml" \ + "sdk-publish" \ + 'gh run download "$SOURCE_RUN_ID"' +assert_job_contains ".github/workflows/release.yml" \ + "github-release" \ + 'if [[ "$GITHUB_EVENT_NAME" == "workflow_dispatch" ]]' # Homebrew belongs to akua-dev/cli and its dedicated tap lane. Akua core must # never configure tap credentials, generate a formula, or dispatch a tap writer. From f33f1463018dc2752590dd02a6a33fc53f03645e Mon Sep 17 00:00:00 2001 From: Robin Date: Sat, 18 Jul 2026 21:37:24 +0200 Subject: [PATCH 8/9] no-mistakes(review): Correct release artifact handoff documentation --- docs/sdk-runtime-compat.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sdk-runtime-compat.md b/docs/sdk-runtime-compat.md index 23e1b044..1c580e21 100644 --- a/docs/sdk-runtime-compat.md +++ b/docs/sdk-runtime-compat.md @@ -49,4 +49,4 @@ These are not part of CI today. When v0.7 lands the CI matrix follow-up, `.githu - `crates/akua-napi/` — the native crate. - `crates/akua-napi/index.js` — the auto-generated platform-loader (what picks the right `@akua-dev/native-*` per host). -- `.github/workflows/release.yml` builds and stages the release; `.github/workflows/release-publish.yml` publishes npm packages from the staged Release assets in dependency order. The build workflow also drives the CLI binary release and Docker. +- `.github/workflows/release.yml` builds and stages the release; `.github/workflows/release-publish.yml` publishes npm packages from the source-run Actions artifacts in dependency order. The build workflow also drives the CLI binary release and Docker. From b73db1fe7be04fb4730ec69657a541e5a6cfbe93 Mon Sep 17 00:00:00 2001 From: Robin Date: Sat, 18 Jul 2026 21:51:47 +0200 Subject: [PATCH 9/9] no-mistakes(document): Document immutable assets and clean release lint --- .github/workflows/release.yml | 9 +++++---- docs/releasing.md | 6 +++--- scripts/check-release-workflows.sh | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ef8d3037..c767de39 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -533,7 +533,7 @@ jobs: run: | sudo apt-get update sudo apt-get install -y gcc-aarch64-linux-gnu - echo 'CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc' >> $GITHUB_ENV + echo 'CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER=aarch64-linux-gnu-gcc' >> "$GITHUB_ENV" - name: Rust target run: mise exec -- rustup target add ${{ matrix.target }} @@ -626,9 +626,10 @@ jobs: dist/*.zip* # --------------------------------------------------------------------------- - # github-release — uploads the per-target tarballs as Release assets. - # Marks the Release as prerelease for `v*-rc*` tags so the GitHub UI - # surfaces "Pre-release" badge + `gh release view --latest` skips it. + # github-release — creates per-target Release assets on a new tag push; + # manual recovery only verifies that the immutable Release exists. + # Prerelease tags surface the "Pre-release" badge so + # `gh release view --latest` skips them. # --------------------------------------------------------------------------- github-release: needs: [detect-version, cli-build] diff --git a/docs/releasing.md b/docs/releasing.md index 1b833917..83fc7d0b 100644 --- a/docs/releasing.md +++ b/docs/releasing.md @@ -62,9 +62,9 @@ Recovery is manual and fail-closed. Do not rerun either failed run, retag `v0.8.25`, recreate its GitHub Release, or upload assets by hand. The existing tag resolves to commit `6452eb662445d2ad7c108128f93b9c55138729bb`; the manual build lane checks out that tag, verifies and propagates its commit, and never builds the current -workflow head as the tagged source. Existing Release assets are verified and left -byte-for-byte unchanged, and npm versions already present are skipped before the -missing packages publish. +workflow head as the tagged source. The existing Release is verified; all 19 assets +are left byte-for-byte unchanged, and npm versions already present are skipped before +the missing packages publish. Recovery starts a new full source build so the publisher can consume that run's short-lived verified Actions artifacts; do not dispatch `release-publish.yml` alone with a stale or guessed run ID. diff --git a/scripts/check-release-workflows.sh b/scripts/check-release-workflows.sh index 95b06599..ad7e9bca 100644 --- a/scripts/check-release-workflows.sh +++ b/scripts/check-release-workflows.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # Patterns passed to the assertion helpers intentionally remain literal. -# shellcheck disable=SC2016 +# shellcheck disable=SC1003,SC2016 set -euo pipefail line_in_job() {