Skip to content

Commit 29ff45c

Browse files
authored
refactor(ci): release and publish release workflows (#87)
* 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/<pkg>-v<ver> 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 <pkg>/v<ver>, 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. * ci: address coderabbit review - 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. * 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).
1 parent 91d823e commit 29ff45c

6 files changed

Lines changed: 202 additions & 18 deletions

File tree

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
name: Publish Release
2+
3+
on:
4+
pull_request:
5+
types: [closed]
6+
branches: [main]
7+
# Escape hatch: re-run a publish that failed mid-flow (e.g. after a fix to
8+
# this workflow). Reads the version from the package.json currently on main.
9+
workflow_dispatch:
10+
inputs:
11+
package:
12+
description: "Package to (re)publish"
13+
required: true
14+
type: choice
15+
options:
16+
- compact-builder
17+
- compact-cli
18+
- compact-simulator
19+
20+
jobs:
21+
publish:
22+
name: Publish merged release
23+
if: >-
24+
github.event_name == 'workflow_dispatch' ||
25+
(github.event.pull_request.merged == true &&
26+
contains(github.event.pull_request.labels.*.name, 'release'))
27+
runs-on: ubuntu-24.04
28+
environment: compact-npm-prod # Final approval gate before npm publish
29+
30+
permissions:
31+
contents: write # push the version tag
32+
id-token: write # npm provenance
33+
34+
steps:
35+
- name: Get github app token
36+
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
37+
id: gh-app-token
38+
with:
39+
app-id: ${{ vars.GH_APP_ID }}
40+
private-key: ${{ secrets.GH_APP_PRIVATE_KEY }}
41+
42+
# For pull_request runs: checkout the merge commit (deterministic view of
43+
# what was just merged). For manual dispatch: checkout the dispatched ref
44+
# (defaults to main but can be any branch — hotfix, release-prep, etc.).
45+
# The compact-npm-prod environment approval is the security gate, not the
46+
# branch ref.
47+
- name: Check out target ref
48+
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
49+
with:
50+
fetch-depth: 0
51+
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.merge_commit_sha || github.ref }}
52+
token: ${{ steps.gh-app-token.outputs.token }}
53+
54+
- name: Detect released package
55+
id: pkg
56+
env:
57+
EVENT: ${{ github.event_name }}
58+
MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
59+
INPUT_PACKAGE: ${{ inputs.package }}
60+
run: |
61+
if [[ "$EVENT" == "workflow_dispatch" ]]; then
62+
PKG="$INPUT_PACKAGE"
63+
case "$PKG" in
64+
compact-builder) DIR="builder" ;;
65+
compact-cli) DIR="cli" ;;
66+
compact-simulator) DIR="simulator" ;;
67+
*)
68+
echo "::error::unknown package: $PKG"
69+
exit 1
70+
;;
71+
esac
72+
else
73+
mapfile -t CHANGED < <(git diff --name-only "$MERGE_SHA^" "$MERGE_SHA" -- 'packages/*/package.json')
74+
COUNT=${#CHANGED[@]}
75+
if [[ "$COUNT" -ne 1 ]]; then
76+
echo "::error::expected exactly one packages/*/package.json change, found $COUNT"
77+
printf '%s\n' "${CHANGED[@]}" >&2
78+
exit 1
79+
fi
80+
DIR=$(echo "${CHANGED[0]}" | awk -F/ '{print $2}')
81+
case "$DIR" in
82+
builder) PKG="compact-builder" ;;
83+
cli) PKG="compact-cli" ;;
84+
simulator) PKG="compact-simulator" ;;
85+
*)
86+
echo "::error::unknown package directory: $DIR"
87+
exit 1
88+
;;
89+
esac
90+
fi
91+
VERSION=$(node -p "require('./packages/$DIR/package.json').version")
92+
echo "dir=$DIR" >> $GITHUB_OUTPUT
93+
echo "name=$PKG" >> $GITHUB_OUTPUT
94+
echo "version=$VERSION" >> $GITHUB_OUTPUT
95+
{
96+
echo "### Publishing"
97+
echo "- Package: $PKG"
98+
echo "- Version: $VERSION"
99+
echo "- Trigger: $EVENT"
100+
} >> $GITHUB_STEP_SUMMARY
101+
102+
- name: Setup Environment
103+
uses: ./.github/actions/setup
104+
105+
- name: Build package
106+
run: yarn build --filter=@openzeppelin/${{ steps.pkg.outputs.name }}
107+
108+
- name: Create and push tag
109+
env:
110+
TAG: ${{ steps.pkg.outputs.name }}/v${{ steps.pkg.outputs.version }}
111+
run: |
112+
if git ls-remote --tags --exit-code origin "refs/tags/$TAG" >/dev/null; then
113+
echo "tag $TAG already on origin, leaving it in place"
114+
else
115+
git tag "$TAG"
116+
git push origin "$TAG"
117+
fi
118+
119+
- name: Publish to npm
120+
env:
121+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
122+
run: |
123+
yarn config set npmAuthToken "$NPM_TOKEN"
124+
cd packages/${{ steps.pkg.outputs.dir }}
125+
yarn npm publish --access public --provenance

.github/workflows/release.yml

Lines changed: 60 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -22,12 +22,13 @@ on:
2222

2323
jobs:
2424
release:
25-
name: Release ${{ inputs.package }}
25+
name: Open release PR for ${{ inputs.package }}
2626
runs-on: ubuntu-24.04
27-
environment: compact-npm-prod # Includes npm token and requires approval
27+
environment: compact-npm-prod # Requires approval before opening the release PR
2828

2929
permissions:
30-
contents: write # Required to push commits and tags
30+
contents: write # create the release branch
31+
pull-requests: write # open the PR + enable auto-merge
3132

3233
steps:
3334
- name: Get github app token
@@ -74,35 +75,76 @@ jobs:
7475
yarn version ${{ inputs.version_bump }}
7576
NEW_VERSION=$(node -p "require('./package.json').version")
7677
echo "new=$NEW_VERSION" >> $GITHUB_OUTPUT
77-
echo "### Release Summary" >> $GITHUB_STEP_SUMMARY
78-
echo "- Package: ${{ inputs.package }}" >> $GITHUB_STEP_SUMMARY
79-
echo "- New version: $NEW_VERSION" >> $GITHUB_STEP_SUMMARY
80-
echo "- Bump type: ${{ inputs.version_bump }}" >> $GITHUB_STEP_SUMMARY
78+
echo "branch=release/${{ inputs.package }}-v$NEW_VERSION" >> $GITHUB_OUTPUT
79+
{
80+
echo "### Release Summary"
81+
echo "- Package: ${{ inputs.package }}"
82+
echo "- New version: $NEW_VERSION"
83+
echo "- Bump type: ${{ inputs.version_bump }}"
84+
} >> $GITHUB_STEP_SUMMARY
8185
8286
- name: Verify package contents
8387
run: |
8488
cd packages/${{ steps.pkg.outputs.dir }}
8589
yarn pack --dry-run
8690
87-
# Uses GitHub API to create signed commits for verification on protected branches
91+
# Branch protection blocks direct pushes to main, so route the version bump
92+
# through a PR: create branch → signed bot commit → open PR → auto-merge.
93+
- name: Create release branch
94+
env:
95+
GH_TOKEN: ${{ steps.gh-app-token.outputs.token }}
96+
BRANCH: ${{ steps.version.outputs.branch }}
97+
run: |
98+
if gh api "/repos/${{ github.repository }}/git/refs/heads/$BRANCH" >/dev/null 2>&1; then
99+
echo "branch $BRANCH already exists, reusing it"
100+
else
101+
SHA=$(gh api "/repos/${{ github.repository }}/git/refs/heads/${{ github.ref_name }}" -q .object.sha)
102+
gh api --method POST "/repos/${{ github.repository }}/git/refs" \
103+
-f ref="refs/heads/$BRANCH" \
104+
-f sha="$SHA"
105+
fi
106+
88107
- name: Commit version bump
89108
uses: iarekylew00t/verified-bot-commit@934fa64df2191ab067d0c0d73f422239b6933392 # v2.2.1
90109
with:
91-
message: "Release ${{ inputs.package }} v${{ steps.version.outputs.new }}"
110+
message: "release: ${{ inputs.package }} v${{ steps.version.outputs.new }}"
92111
token: ${{ steps.gh-app-token.outputs.token }}
93-
ref: ${{ github.ref_name }}
112+
ref: ${{ steps.version.outputs.branch }}
94113
files: |
95114
packages/${{ steps.pkg.outputs.dir }}/package.json
96115
97-
- name: Create and push tag
116+
- name: Ensure release label exists
117+
env:
118+
GH_TOKEN: ${{ steps.gh-app-token.outputs.token }}
98119
run: |
99-
git tag "${{ inputs.package }}/v${{ steps.version.outputs.new }}"
100-
git push origin "${{ inputs.package }}/v${{ steps.version.outputs.new }}"
120+
gh label create release \
121+
--description "Automated release PR" \
122+
--color ededed \
123+
--force
101124
102-
- name: Publish to npm
125+
- name: Open release PR
126+
id: open-pr
127+
env:
128+
GH_TOKEN: ${{ steps.gh-app-token.outputs.token }}
129+
BRANCH: ${{ steps.version.outputs.branch }}
103130
run: |
104-
yarn config set npmAuthToken "$NPM_TOKEN"
105-
cd packages/${{ steps.pkg.outputs.dir }}
106-
yarn npm publish --access public --provenance
131+
cat > /tmp/pr-body.md <<EOF
132+
Automated release PR for **${{ inputs.package }}** v${{ steps.version.outputs.new }} (${{ inputs.version_bump }} bump).
133+
134+
This PR was opened by the release workflow. Once required checks pass (semgrep, CodeQL, code-owner review), it will auto-merge. Merging will trigger the publish workflow, which tags the release and publishes to npm.
135+
EOF
136+
PR_URL=$(gh pr create \
137+
--base "${{ github.ref_name }}" \
138+
--head "$BRANCH" \
139+
--title "release: ${{ inputs.package }} v${{ steps.version.outputs.new }}" \
140+
--label release \
141+
--body-file /tmp/pr-body.md)
142+
echo "url=$PR_URL" >> $GITHUB_OUTPUT
143+
echo "- PR: $PR_URL" >> $GITHUB_STEP_SUMMARY
144+
145+
# If this step fails with "auto-merge is not allowed", enable it under
146+
# Settings → General → "Allow auto-merge", then re-run the workflow.
147+
- name: Enable auto-merge
107148
env:
108-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
149+
GH_TOKEN: ${{ steps.gh-app-token.outputs.token }}
150+
run: gh pr merge "${{ steps.open-pr.outputs.url }}" --auto --squash

.yarnrc.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,5 @@ compressionLevel: mixed
33
enableGlobalCache: false
44

55
nodeLinker: node-modules
6+
7+
npmPublishRegistry: "https://registry.npmjs.org"

packages/builder/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
],
1212
"author": "OpenZeppelin Community <maintainers@openzeppelin.org>",
1313
"license": "MIT",
14+
"repository": {
15+
"type": "git",
16+
"url": "git+https://github.com/OpenZeppelin/compact-tools.git",
17+
"directory": "packages/builder"
18+
},
1419
"type": "module",
1520
"main": "./dist/index.js",
1621
"types": "./dist/index.d.ts",

packages/cli/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@
1111
],
1212
"author": "OpenZeppelin Community <maintainers@openzeppelin.org>",
1313
"license": "MIT",
14+
"repository": {
15+
"type": "git",
16+
"url": "git+https://github.com/OpenZeppelin/compact-tools.git",
17+
"directory": "packages/cli"
18+
},
1419
"type": "module",
1520
"exports": {
1621
"./run-builder": "./dist/runBuilder.js",

packages/simulator/package.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@
1010
],
1111
"author": "OpenZeppelin Community <maintainers@openzeppelin.org>",
1212
"license": "MIT",
13+
"repository": {
14+
"type": "git",
15+
"url": "git+https://github.com/OpenZeppelin/compact-tools.git",
16+
"directory": "packages/simulator"
17+
},
1318
"type": "module",
1419
"main": "./dist/index.js",
1520
"types": "./dist/index.d.ts",

0 commit comments

Comments
 (0)