diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index ebd82ba4..8e25410a 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -6,24 +6,23 @@ 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, …) you fix the workflow on main, then re-dispatch THIS -# workflow with the same tag. ~1 minute, no rebuild. +# 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 # 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 assets remain untouched while the workflow fix +# comes from the explicitly selected dispatch ref. See docs/releasing.md. on: workflow_dispatch: @@ -32,6 +31,18 @@ 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 + 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 @@ -39,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: @@ -50,16 +62,84 @@ 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 }} + source_run_id: ${{ steps.detect.outputs.source_run_id }} steps: + - uses: actions/checkout@v6 + with: + ref: ${{ inputs.tag }} + fetch-depth: 0 + - id: detect env: 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 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 [[ ! "$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" + 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" + exit 1 + fi version="${TAG#v}" if [[ "$version" == *-* ]]; then is_prerelease=true @@ -73,6 +153,8 @@ jobs: echo "version=$version" 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" @@ -82,7 +164,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,33 +184,31 @@ 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 - - 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 @@ -149,11 +229,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 \ @@ -213,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 @@ -243,7 +321,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..c767de39 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 @@ -20,13 +19,12 @@ 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 -# 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`. +# re-runs publish without repeating successful npm versions. The build outputs +# 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 @@ -40,8 +38,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: @@ -56,6 +53,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 @@ -80,11 +85,50 @@ 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 + 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 }}" + 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}") + 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" + 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 +144,31 @@ 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" + + - 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 @@ -116,6 +183,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 +283,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: @@ -301,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: @@ -323,57 +387,26 @@ 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 - # `cnap-tech~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 # works — no PAT needed. env: TAG: ${{ needs.detect-version.outputs.tag }} + 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: | 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" \ + -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" # --------------------------------------------------------------------------- @@ -387,6 +420,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 +450,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 +503,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: @@ -491,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 }} @@ -584,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] @@ -594,13 +637,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 +653,37 @@ jobs: pattern: 'akua-*' path: dist merge-multiple: true - - name: Create release + - name: Create or verify immutable 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 + notes="See [CHANGELOG.md](https://github.com/akua-dev/akua/blob/main/CHANGELOG.md) for details." + 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 [ "${{ needs.detect-version.outputs.is_prerelease }}" = "true" ]; then + if [ "${IS_PRERELEASE}" = "true" ]; then flags+=(--prerelease) 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." \ + 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/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 +693,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 +723,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< ~/.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:cnap-tech/homebrew-tap.git tap - cd tap - git config user.email "akua-tap-bumper@cnap-tech" - 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 <

- Release + Release npm License

@@ -123,14 +123,11 @@ For cross-Package composition (install one Akua package on top of another, with # macOS / Linux curl -fsSL https://cli.akua.dev/install | sh -# Homebrew -brew install cnap-tech/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 +135,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/Taskfile.yml b/Taskfile.yml index ae5f4fe5..3c629611 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -455,8 +455,8 @@ tasks: echo "Bootstrap complete. Next:" echo " 1. https://www.npmjs.com/settings/akua-dev/packages — verify all 10 packages appear" echo " 2. For each package: Settings → Trusted Publishers → add GitHub Actions" - echo " org=cnap-tech repo=akua workflow=release.yml" - echo " 3. Tag v0.6.0 — release.yml handles npm + GitHub Release + Docker + Homebrew." + echo " org=akua-dev repo=akua workflow=release-publish.yml environment=none" + echo " 3. See docs/releasing.md before running a release." release:dry-run: desc: Run the full pre-release pipeline locally (gen + check + build + pack) diff --git a/crates/akua-napi/npm/darwin-arm64/package.json b/crates/akua-napi/npm/darwin-arm64/package.json index 5e09aae3..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/cnap-tech/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 f22bf0df..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/cnap-tech/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 5b5da52c..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/cnap-tech/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 e8a45b9d..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/cnap-tech/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 4317fd04..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/cnap-tech/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 50705bc5..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/cnap-tech/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 741a77e2..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/cnap-tech/akua.git" + "url": "git+https://github.com/akua-dev/akua.git" }, "os": [ "win32" diff --git a/crates/akua-napi/package.json b/crates/akua-napi/package.json index c3b6cdd7..e6dcbba0 100644 --- a/crates/akua-napi/package.json +++ b/crates/akua-napi/package.json @@ -7,7 +7,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" }, "engines": { "node": ">= 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/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/docs/releasing.md b/docs/releasing.md new file mode 100644 index 00000000..83fc7d0b --- /dev/null +++ b/docs/releasing.md @@ -0,0 +1,98 @@ +# Releasing akua + +The build and publish lanes are intentionally separate. `.github/workflows/release.yml` +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 +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 +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. 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. +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 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') +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, 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/docs/sdk-runtime-compat.md b/docs/sdk-runtime-compat.md index 9b34cb6c..1c580e21 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 source-run Actions artifacts in dependency order. The build workflow also drives the CLI binary release and Docker. diff --git a/packages/sdk/README.md b/packages/sdk/README.md index 64bd8c5c..2d5d7b28 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,8 @@ 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. - -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. - -See [`.github/workflows/release.yml`](../../.github/workflows/release.yml) for the full job graph. +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/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/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/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 diff --git a/scripts/check-release-workflows.sh b/scripts/check-release-workflows.sh index 68e55daa..ad7e9bca 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=SC1003,SC2016 set -euo pipefail line_in_job() { @@ -51,6 +53,51 @@ 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 +} + +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. @@ -88,4 +135,193 @@ 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 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_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. +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 +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_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" \ + '--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" \ + '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" +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" +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" +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." 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:**