From a9083dc4112f740c0f076558967b7df0b8c411dc Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 20 May 2026 11:10:26 +0200 Subject: [PATCH 1/3] ci: route release version bump through a PR + add publish workflow Branch protection on main blocks direct pushes from the release workflow (it requires PR, semgrep, CodeQL). Split the single release job into two files so the version bump goes through the same gates as every other change. - release.yml: dispatch-triggered. Tests, builds, bumps the version on a release/-v branch, opens a PR with a 'release' label, and enables auto-merge (squash). Auto-merge fires once required checks + code-owner review pass. - release-publish.yml: triggers on the merged release PR. Detects which package changed, builds, tags /v, and publishes to npm with provenance. Tag step is idempotent so retried runs don't fail on an existing tag. A workflow_dispatch input is provided as an escape hatch for manually re-running publish for a specific package after a fix. Supporting fixes needed for the publish to actually succeed: - .yarnrc.yml: set npmPublishRegistry to registry.npmjs.org. Yarn Berry otherwise PUTs to registry.yarnpkg.com (a read-only mirror) and gets a 404. - packages/*/package.json: add a repository field. Required by npm's provenance verification, which cross-checks the package.json against the OIDC-supplied repo URL and rejects with 422 if the field is empty or missing. --- .github/workflows/release-publish.yml | 122 ++++++++++++++++++++++++++ .github/workflows/release.yml | 74 ++++++++++++---- .yarnrc.yml | 2 + packages/builder/package.json | 5 ++ packages/cli/package.json | 5 ++ packages/simulator/package.json | 5 ++ 6 files changed, 195 insertions(+), 18 deletions(-) create mode 100644 .github/workflows/release-publish.yml diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml new file mode 100644 index 0000000..eaaa3d5 --- /dev/null +++ b/.github/workflows/release-publish.yml @@ -0,0 +1,122 @@ +name: Publish Release + +on: + pull_request: + types: [closed] + branches: [main] + # Escape hatch: re-run a publish that failed mid-flow (e.g. after a fix to + # this workflow). Reads the version from the package.json currently on main. + workflow_dispatch: + inputs: + package: + description: "Package to (re)publish" + required: true + type: choice + options: + - compact-builder + - compact-cli + - compact-simulator + +jobs: + publish: + name: Publish merged release + if: >- + github.event_name == 'workflow_dispatch' || + (github.event.pull_request.merged == true && + contains(github.event.pull_request.labels.*.name, 'release')) + runs-on: ubuntu-24.04 + environment: compact-npm-prod # Final approval gate before npm publish + + permissions: + contents: write # push the version tag + id-token: write # npm provenance + + steps: + - name: Get github app token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0 + id: gh-app-token + with: + app-id: ${{ vars.GH_APP_ID }} + private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} + + # For pull_request runs: checkout the merge commit (deterministic view of + # what was just merged). For manual dispatch: checkout main HEAD. + - name: Check out target ref + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || github.ref }} + token: ${{ steps.gh-app-token.outputs.token }} + + - name: Detect released package + id: pkg + env: + EVENT: ${{ github.event_name }} + MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }} + INPUT_PACKAGE: ${{ inputs.package }} + run: | + if [[ "$EVENT" == "workflow_dispatch" ]]; then + PKG="$INPUT_PACKAGE" + case "$PKG" in + compact-builder) DIR="builder" ;; + compact-cli) DIR="cli" ;; + compact-simulator) DIR="simulator" ;; + *) + echo "::error::unknown package: $PKG" + exit 1 + ;; + esac + else + mapfile -t CHANGED < <(git diff --name-only "$MERGE_SHA^" "$MERGE_SHA" -- 'packages/*/package.json') + COUNT=${#CHANGED[@]} + if [[ "$COUNT" -ne 1 ]]; then + echo "::error::expected exactly one packages/*/package.json change, found $COUNT" + printf '%s\n' "${CHANGED[@]}" >&2 + exit 1 + fi + DIR=$(echo "${CHANGED[0]}" | awk -F/ '{print $2}') + case "$DIR" in + builder) PKG="compact-builder" ;; + cli) PKG="compact-cli" ;; + simulator) PKG="compact-simulator" ;; + *) + echo "::error::unknown package directory: $DIR" + exit 1 + ;; + esac + fi + VERSION=$(node -p "require('./packages/$DIR/package.json').version") + echo "dir=$DIR" >> $GITHUB_OUTPUT + echo "name=$PKG" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + { + echo "### Publishing" + echo "- Package: $PKG" + echo "- Version: $VERSION" + echo "- Trigger: $EVENT" + } >> $GITHUB_STEP_SUMMARY + + - name: Setup Environment + uses: ./.github/actions/setup + + - name: Build package + run: yarn build --filter=@openzeppelin/${{ steps.pkg.outputs.name }} + + - name: Create and push tag + env: + TAG: ${{ steps.pkg.outputs.name }}/v${{ steps.pkg.outputs.version }} + run: | + if git ls-remote --tags --exit-code origin "refs/tags/$TAG" >/dev/null; then + echo "tag $TAG already on origin, leaving it in place" + else + git tag "$TAG" + git push origin "$TAG" + fi + + - name: Publish to npm + env: + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + yarn config set npmAuthToken "$NPM_TOKEN" + cd packages/${{ steps.pkg.outputs.dir }} + yarn npm publish --access public --provenance diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1a25ac4..72a322e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -22,12 +22,13 @@ on: jobs: release: - name: Release ${{ inputs.package }} + name: Open release PR for ${{ inputs.package }} runs-on: ubuntu-24.04 - environment: compact-npm-prod # Includes npm token and requires approval + environment: compact-npm-prod # Requires approval before opening the release PR permissions: - contents: write # Required to push commits and tags + contents: write # create the release branch + pull-requests: write # open the PR + enable auto-merge steps: - name: Get github app token @@ -74,35 +75,72 @@ jobs: yarn version ${{ inputs.version_bump }} NEW_VERSION=$(node -p "require('./package.json').version") echo "new=$NEW_VERSION" >> $GITHUB_OUTPUT - echo "### Release Summary" >> $GITHUB_STEP_SUMMARY - echo "- Package: ${{ inputs.package }}" >> $GITHUB_STEP_SUMMARY - echo "- New version: $NEW_VERSION" >> $GITHUB_STEP_SUMMARY - echo "- Bump type: ${{ inputs.version_bump }}" >> $GITHUB_STEP_SUMMARY + echo "branch=release/${{ inputs.package }}-v$NEW_VERSION" >> $GITHUB_OUTPUT + { + echo "### Release Summary" + echo "- Package: ${{ inputs.package }}" + echo "- New version: $NEW_VERSION" + echo "- Bump type: ${{ inputs.version_bump }}" + } >> $GITHUB_STEP_SUMMARY - name: Verify package contents run: | cd packages/${{ steps.pkg.outputs.dir }} yarn pack --dry-run - # Uses GitHub API to create signed commits for verification on protected branches + # Branch protection blocks direct pushes to main, so route the version bump + # through a PR: create branch → signed bot commit → open PR → auto-merge. + - name: Create release branch + env: + GH_TOKEN: ${{ steps.gh-app-token.outputs.token }} + BRANCH: ${{ steps.version.outputs.branch }} + run: | + SHA=$(gh api "/repos/${{ github.repository }}/git/refs/heads/${{ github.ref_name }}" -q .object.sha) + gh api --method POST "/repos/${{ github.repository }}/git/refs" \ + -f ref="refs/heads/$BRANCH" \ + -f sha="$SHA" + - name: Commit version bump uses: iarekylew00t/verified-bot-commit@934fa64df2191ab067d0c0d73f422239b6933392 # v2.2.1 with: - message: "Release ${{ inputs.package }} v${{ steps.version.outputs.new }}" + message: "release: ${{ inputs.package }} v${{ steps.version.outputs.new }}" token: ${{ steps.gh-app-token.outputs.token }} - ref: ${{ github.ref_name }} + ref: ${{ steps.version.outputs.branch }} files: | packages/${{ steps.pkg.outputs.dir }}/package.json - - name: Create and push tag + - name: Ensure release label exists + env: + GH_TOKEN: ${{ steps.gh-app-token.outputs.token }} run: | - git tag "${{ inputs.package }}/v${{ steps.version.outputs.new }}" - git push origin "${{ inputs.package }}/v${{ steps.version.outputs.new }}" + gh label create release \ + --description "Automated release PR" \ + --color ededed \ + --force - - name: Publish to npm + - name: Open release PR + id: open-pr + env: + GH_TOKEN: ${{ steps.gh-app-token.outputs.token }} + BRANCH: ${{ steps.version.outputs.branch }} run: | - yarn config set npmAuthToken "$NPM_TOKEN" - cd packages/${{ steps.pkg.outputs.dir }} - yarn npm publish --access public --provenance + cat > /tmp/pr-body.md <> $GITHUB_OUTPUT + echo "- PR: $PR_URL" >> $GITHUB_STEP_SUMMARY + + # If this step fails with "auto-merge is not allowed", enable it under + # Settings → General → "Allow auto-merge", then re-run the workflow. + - name: Enable auto-merge env: - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + GH_TOKEN: ${{ steps.gh-app-token.outputs.token }} + run: gh pr merge "${{ steps.open-pr.outputs.url }}" --auto --squash diff --git a/.yarnrc.yml b/.yarnrc.yml index 91b1101..053bc28 100644 --- a/.yarnrc.yml +++ b/.yarnrc.yml @@ -3,3 +3,5 @@ compressionLevel: mixed enableGlobalCache: false nodeLinker: node-modules + +npmPublishRegistry: "https://registry.npmjs.org" diff --git a/packages/builder/package.json b/packages/builder/package.json index 5c4667b..938313f 100644 --- a/packages/builder/package.json +++ b/packages/builder/package.json @@ -11,6 +11,11 @@ ], "author": "OpenZeppelin Community ", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/OpenZeppelin/compact-tools.git", + "directory": "packages/builder" + }, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index 11c8d91..92bac10 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -11,6 +11,11 @@ ], "author": "OpenZeppelin Community ", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/OpenZeppelin/compact-tools.git", + "directory": "packages/cli" + }, "type": "module", "exports": { "./run-builder": "./dist/runBuilder.js", diff --git a/packages/simulator/package.json b/packages/simulator/package.json index e73b723..80f8627 100644 --- a/packages/simulator/package.json +++ b/packages/simulator/package.json @@ -10,6 +10,11 @@ ], "author": "OpenZeppelin Community ", "license": "MIT", + "repository": { + "type": "git", + "url": "git+https://github.com/OpenZeppelin/compact-tools.git", + "directory": "packages/simulator" + }, "type": "module", "main": "./dist/index.js", "types": "./dist/index.d.ts", From da3f2a575d20bde6356f3b96897323db50e31c35 Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 20 May 2026 11:25:43 +0200 Subject: [PATCH 2/3] ci: address coderabbit review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - release-publish.yml: pin manual dispatch checkout to refs/heads/main instead of github.ref, so workflow_dispatch from a non-main branch can't publish from an unintended ref. - release.yml: make 'Create release branch' idempotent — skip the POST if the branch already exists, so retried dispatches don't 422 on a partial previous run. --- .github/workflows/release-publish.yml | 2 +- .github/workflows/release.yml | 12 ++++++++---- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index eaaa3d5..e1670ac 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -45,7 +45,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || github.ref }} + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || 'refs/heads/main' }} token: ${{ steps.gh-app-token.outputs.token }} - name: Detect released package diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 72a322e..6f1f301 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -95,10 +95,14 @@ jobs: GH_TOKEN: ${{ steps.gh-app-token.outputs.token }} BRANCH: ${{ steps.version.outputs.branch }} run: | - SHA=$(gh api "/repos/${{ github.repository }}/git/refs/heads/${{ github.ref_name }}" -q .object.sha) - gh api --method POST "/repos/${{ github.repository }}/git/refs" \ - -f ref="refs/heads/$BRANCH" \ - -f sha="$SHA" + if gh api "/repos/${{ github.repository }}/git/refs/heads/$BRANCH" >/dev/null 2>&1; then + echo "branch $BRANCH already exists, reusing it" + else + SHA=$(gh api "/repos/${{ github.repository }}/git/refs/heads/${{ github.ref_name }}" -q .object.sha) + gh api --method POST "/repos/${{ github.repository }}/git/refs" \ + -f ref="refs/heads/$BRANCH" \ + -f sha="$SHA" + fi - name: Commit version bump uses: iarekylew00t/verified-bot-commit@934fa64df2191ab067d0c0d73f422239b6933392 # v2.2.1 From b1f18f36ae5cda25970aa620d3bb4ab94a36ae2c Mon Sep 17 00:00:00 2001 From: 0xisk <0xisk@proton.me> Date: Wed, 20 May 2026 11:29:03 +0200 Subject: [PATCH 3/3] ci: allow workflow_dispatch publish from any branch Reverts the refs/heads/main pin from the previous commit. Publishing from a hotfix or release-prep branch is a legitimate use case, and the compact-npm-prod environment approval is the actual security gate (approving a release is the trust boundary, not the branch ref). --- .github/workflows/release-publish.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-publish.yml b/.github/workflows/release-publish.yml index e1670ac..bb98709 100644 --- a/.github/workflows/release-publish.yml +++ b/.github/workflows/release-publish.yml @@ -40,12 +40,15 @@ jobs: private-key: ${{ secrets.GH_APP_PRIVATE_KEY }} # For pull_request runs: checkout the merge commit (deterministic view of - # what was just merged). For manual dispatch: checkout main HEAD. + # what was just merged). For manual dispatch: checkout the dispatched ref + # (defaults to main but can be any branch — hotfix, release-prep, etc.). + # The compact-npm-prod environment approval is the security gate, not the + # branch ref. - name: Check out target ref uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || 'refs/heads/main' }} + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || github.ref }} token: ${{ steps.gh-app-token.outputs.token }} - name: Detect released package