diff --git a/.github/workflows/clawhub-cli-github-release.yml b/.github/workflows/clawhub-cli-github-release.yml index b7ba8312..7c030817 100644 --- a/.github/workflows/clawhub-cli-github-release.yml +++ b/.github/workflows/clawhub-cli-github-release.yml @@ -236,6 +236,47 @@ jobs: exit 1 fi + - name: Verify publish proof artifact + if: ${{ inputs.publish_run_id != '' }} + env: + GH_TOKEN: ${{ github.token }} + PUBLISH_RUN_ID: ${{ inputs.publish_run_id }} + RELEASE_TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + PROOF_DIR="$RUNNER_TEMP/clawhub-cli-github-release-publish-proof" + rm -rf "$PROOF_DIR" + mkdir -p "$PROOF_DIR" + gh run download "$PUBLISH_RUN_ID" \ + --repo "$GITHUB_REPOSITORY" \ + --name "clawhub-cli-npm-publish-${RELEASE_TAG}" \ + --dir "$PROOF_DIR" + + if [[ "$(tr -d '\r\n' < "$PROOF_DIR/release-tag.txt")" != "$RELEASE_TAG" ]]; then + echo "Publish artifact tag does not match ${RELEASE_TAG}." >&2 + exit 1 + fi + if [[ "$(tr -d '\r\n' < "$PROOF_DIR/release-sha.txt")" != "$RELEASE_SHA" ]]; then + echo "Publish artifact SHA does not match ${RELEASE_SHA}." >&2 + exit 1 + fi + if [[ "$(tr -d '\r\n' < "$PROOF_DIR/package-version.txt")" != "$PACKAGE_VERSION" ]]; then + echo "Publish artifact version does not match ${PACKAGE_VERSION}." >&2 + exit 1 + fi + if [[ "$(tr -d '\r\n' < "$PROOF_DIR/preflight-only.txt")" != "false" ]]; then + echo "Publish artifact must come from a real publish run." >&2 + exit 1 + fi + if [[ "$(tr -d '\r\n' < "$PROOF_DIR/npm-tarball-url.txt")" != "$NPM_TARBALL_URL" ]]; then + echo "Publish artifact tarball URL does not match npm metadata." >&2 + exit 1 + fi + if [[ "$(tr -d '\r\n' < "$PROOF_DIR/npm-integrity.txt")" != "$NPM_INTEGRITY" ]]; then + echo "Publish artifact integrity does not match npm metadata." >&2 + exit 1 + fi + - name: Build release notes env: RELEASE_TAG: ${{ inputs.tag }} diff --git a/.github/workflows/clawhub-cli-npm-release.yml b/.github/workflows/clawhub-cli-npm-release.yml index 9159c502..82e3bf15 100644 --- a/.github/workflows/clawhub-cli-npm-release.yml +++ b/.github/workflows/clawhub-cli-npm-release.yml @@ -381,6 +381,31 @@ jobs: echo "RELEASE_TITLE=clawhub ${PACKAGE_VERSION}" } >> "$GITHUB_ENV" + - name: Write npm publish proof artifact + id: publish_proof + env: + RELEASE_TAG: ${{ inputs.tag }} + run: | + set -euo pipefail + PUBLISH_PROOF_DIR="$RUNNER_TEMP/clawhub-cli-npm-publish-proof" + rm -rf "$PUBLISH_PROOF_DIR" + mkdir -p "$PUBLISH_PROOF_DIR" + printf '%s\n' "$RELEASE_TAG" > "$PUBLISH_PROOF_DIR/release-tag.txt" + git rev-parse HEAD > "$PUBLISH_PROOF_DIR/release-sha.txt" + printf '%s\n' "$PACKAGE_VERSION" > "$PUBLISH_PROOF_DIR/package-version.txt" + printf '%s\n' "$NPM_TARBALL_URL" > "$PUBLISH_PROOF_DIR/npm-tarball-url.txt" + printf '%s\n' "$NPM_INTEGRITY" > "$PUBLISH_PROOF_DIR/npm-integrity.txt" + printf '%s\n' "$GITHUB_RUN_ID" > "$PUBLISH_PROOF_DIR/publish-run-id.txt" + printf '%s\n' "false" > "$PUBLISH_PROOF_DIR/preflight-only.txt" + echo "dir=$PUBLISH_PROOF_DIR" >> "$GITHUB_OUTPUT" + + - name: Upload npm publish proof artifact + uses: actions/upload-artifact@v7 + with: + name: clawhub-cli-npm-publish-${{ inputs.tag }} + path: ${{ steps.publish_proof.outputs.dir }} + if-no-files-found: error + - name: Build GitHub Release notes env: RELEASE_TAG: ${{ inputs.tag }} diff --git a/src/__tests__/clawhub-cli-release-workflow.test.ts b/src/__tests__/clawhub-cli-release-workflow.test.ts new file mode 100644 index 00000000..84a35eb5 --- /dev/null +++ b/src/__tests__/clawhub-cli-release-workflow.test.ts @@ -0,0 +1,29 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { describe, expect, it } from "vitest"; + +describe("ClawHub CLI release workflows", () => { + it("requires publish-specific proof before adding npm publish release proof", () => { + const npmRelease = readFileSync( + resolve(".github/workflows/clawhub-cli-npm-release.yml"), + "utf8", + ); + const githubRelease = readFileSync( + resolve(".github/workflows/clawhub-cli-github-release.yml"), + "utf8", + ); + + expect(npmRelease).toContain("Write npm publish proof artifact"); + expect(npmRelease).toContain("Upload npm publish proof artifact"); + expect(npmRelease).toContain("clawhub-cli-npm-publish-${{ inputs.tag }}"); + expect(npmRelease).toContain( + 'printf \'%s\\n\' "false" > "$PUBLISH_PROOF_DIR/preflight-only.txt"', + ); + + expect(githubRelease).toContain("Verify publish proof artifact"); + expect(githubRelease).toContain("clawhub-cli-npm-publish-${RELEASE_TAG}"); + expect(githubRelease).toContain("Publish artifact must come from a real publish run."); + expect(githubRelease).toContain("Publish artifact tarball URL does not match npm metadata."); + expect(githubRelease).toContain("Publish artifact integrity does not match npm metadata."); + }); +});