Skip to content

refactor(ci): release and publish release workflows #87

Merged
0xisk merged 3 commits into
mainfrom
ci/pr-based-release
May 20, 2026
Merged

refactor(ci): release and publish release workflows #87
0xisk merged 3 commits into
mainfrom
ci/pr-based-release

Conversation

@0xisk
Copy link
Copy Markdown
Member

@0xisk 0xisk commented May 20, 2026

Changes:

  • Branch protection on main blocks the current release workflow. it tries to push the version bump directly, but the ruleset requires PR + semgrep + CodeQL.
  • Split into two workflows so the version bump goes through the same gates as every other change:
    • release.yml (dispatch) — tests, builds, bumps version on a release/<pkg>-v<ver> branch, opens a PR with a release label, enables squash auto-merge.
    • release-publish.yml (PR-closed/merged) — detects which package changed, builds, tags <pkg>/v<ver>, publishes to npm with provenance.
  • Tag step is idempotent: retried runs no longer crash on an existing tag.
  • workflow_dispatch escape hatch on release-publish.yml — manually re-publish a specific package after a fix without bumping the version.
  • .yarnrc.yml: set npmPublishRegistry to registry.npmjs.org: Yarn Berry was PUTting to the read-only registry.yarnpkg.com mirror and getting 404s on publish.
  • packages/*/package.json: add repository field, required by npm provenance verification (rejects with 422 when missing).

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.
@0xisk 0xisk requested review from a team as code owners May 20, 2026 09:13
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 20, 2026

Important

Review skipped

Auto incremental reviews are disabled on this repository.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: df163f56-71d5-40d7-a423-a4730617d6c5

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Walkthrough

This PR refactors the release process from a single-step workflow into a two-stage pipeline. The release.yml workflow now opens a release PR with version bumps and auto-merges it, while a new release-publish.yml workflow handles publishing merged packages to npm. Supporting npm and package configuration is added across yarnrc and package.json files.

Changes

Release Automation Refactoring

Layer / File(s) Summary
Package publishing configuration and metadata
.yarnrc.yml, packages/builder/package.json, packages/cli/package.json, packages/simulator/package.json
npm registry URL is configured and repository metadata (type, url, directory) is added to all three package.json files to support the refactored release workflows.
Release PR workflow
.github/workflows/release.yml
The workflow is restructured to write release summaries, verify packages, create a release branch via GitHub refs API, commit version bumps, and auto-merge the PR back to the current branch via gh pr merge --auto --squash. Direct tagging and publishing steps are removed, delegating those to downstream workflows.
Release publish workflow
.github/workflows/release-publish.yml
New workflow publishes packages to npm when a PR labeled release closes or via manual dispatch. It detects which package changed, extracts the version, builds the package, creates and pushes a version tag, and runs yarn npm publish with provenance enabled.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 Two workflows dance, a hop and a bound,
PR opens first, then publish comes around,
npm registry calls, with metadata in place,
Release automation finds its steady pace!

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title "refactor(ci): release and publish release workflows" is partially related to the changeset. It describes a refactoring of CI workflows, which is accurate, but omits key details about the two-workflow split design and the purpose (adding branch protection via PR-based releases). The title is somewhat vague about what exactly was refactored. Consider a more specific title such as "refactor(ci): split release into PR-based version bump and publish workflows" to better convey the main architectural change and avoid ambiguity about what was refactored.
✅ Passed checks (4 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch ci/pr-based-release

Comment @coderabbitai help to get the list of available commands and usage tips.

@0xisk 0xisk added the enhancement New feature or request label May 20, 2026
@0xisk 0xisk self-assigned this May 20, 2026
@0xisk 0xisk added this to the v1.0.0 milestone May 20, 2026
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/release-publish.yml:
- Line 48: The checkout ref for manual dispatch uses github.ref which can point
to any branch; update the conditional that sets ref (the expression using
github.event_name == 'pull_request' &&
github.event.pull_request.merge_commit_sha || github.ref) so that when
github.event_name == 'workflow_dispatch' it uses the fixed ref 'refs/heads/main'
instead of github.ref, preserving the existing behavior for pull_request merges;
adjust the conditional expression accordingly so manual dispatch always checks
out main.

In @.github/workflows/release.yml:
- Around line 93-101: The "Create release branch" step currently always POSTs a
git ref and fails on retries; update the script that uses GH_TOKEN, BRANCH and
SHA (the gh api calls) to first check for the ref's existence (gh api GET
/repos/.../git/refs/heads/$BRANCH) and only POST the new ref if the GET returns
not found, or alternately catch a 422 from the POST and treat it as success;
modify the logic around the gh api calls in that step so branch creation is
idempotent on workflow reruns.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f7593053-eb45-49fe-aca7-ffc7d41aa47f

📥 Commits

Reviewing files that changed from the base of the PR and between 91d823e and a9083dc.

📒 Files selected for processing (6)
  • .github/workflows/release-publish.yml
  • .github/workflows/release.yml
  • .yarnrc.yml
  • packages/builder/package.json
  • packages/cli/package.json
  • packages/simulator/package.json

Comment thread .github/workflows/release-publish.yml
Comment thread .github/workflows/release.yml Outdated
Comment on lines +93 to +101
- 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"
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 20, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, locate and read the release.yml file
find . -path "./.git" -prune -o -name "release.yml" -type f -print

Repository: OpenZeppelin/compact-tools

Length of output: 102


🏁 Script executed:

# Read the release.yml file to see the actual code
cat -n .github/workflows/release.yml | head -120

Repository: OpenZeppelin/compact-tools

Length of output: 4883


🏁 Script executed:

# Check if there are any error handling or try-catch patterns in the workflow
rg "git/refs" .github/workflows/ -A 3 -B 3

Repository: OpenZeppelin/compact-tools

Length of output: 695


🏁 Script executed:

# Check if there's any documentation or other workflows using similar patterns
rg "git/refs" --type yaml -B 5 -A 5

Repository: OpenZeppelin/compact-tools

Length of output: 52


🌐 Web query:

GitHub API POST /repos/{owner}/{repo}/git/refs error handling duplicate ref

💡 Result:

When using the GitHub REST API endpoint POST /repos/{owner}/{repo}/git/refs to create a reference, attempting to create a reference that already exists will result in a 422 Unprocessable Entity error [1][2]. In this scenario, the API response body typically includes a message indicating that the reference update failed, often accompanied by a documentation URL [3]. It is important to ensure that the ref parameter provided in your request body is fully qualified (e.g., refs/heads/my-branch-name or refs/tags/my-tag-name) [1][2][4]. If the provided ref is not fully qualified or is malformed, the API may also return a 422 error [1][3]. To handle this in your application: 1. Check for the 422 status code [1][2]. 2. Parse the error message to confirm the failure is due to a duplicate reference or validation issue [3]. 3. If the goal is to update an existing reference rather than create a new one, you should use the PATCH /repos/{owner}/{repo}/git/refs/{ref} endpoint instead [5]. Note that a 409 Conflict error may also occur if the repository is empty or currently being initialized, which is distinct from a duplicate reference error [5].

Citations:


🌐 Web query:

gh cli GitHub API ref endpoint error handling 404 422

💡 Result:

In the GitHub CLI (gh), the gh api command handles HTTP errors by printing the error message to stderr and exiting with a non-zero status code by default [1][2]. Because error handling is tightly coupled with output formatting, users have limited control over these responses [1][3]. Handling 404 and 422 Errors: 404 Not Found: This error indicates that the requested resource could not be found [4][5]. In the context of gh api, common causes include: - Incorrect URL paths or typos [5]. Notably, adding a leading slash (/) to an endpoint path can sometimes cause issues in specific shells or configurations [6][7]. - Accessing a private resource without proper authentication [5]. GitHub returns a 404 rather than a 403 for private resources to prevent confirming their existence [5]. - Missing required OAuth scopes (e.g., workflow scope), which can sometimes manifest as a 404 for certain operations depending on the underlying git objects [8]. 422 Unprocessable Entity: This error signifies that the request was syntactically correct but could not be processed due to validation failures [5]. - Common causes include missing required parameters, incorrect parameter types, or invalid field formatting [5]. - The response body typically contains an errors property with a code field (e.g., missing, missing_field, invalid, already_exists) to help diagnose the issue [5]. Best Practices for Error Handling: - Command Scripting: When scripting with gh api, use the || true pattern or check exit codes to prevent the entire script from failing immediately upon an API error [3]. - Debugging: Use the --verbose flag with gh api to inspect the full HTTP request and response, which is essential for diagnosing why a 404 or 422 is occurring [2]. - Output Control: Note that gh api may return raw JSON error responses to stdout if you are using filters (like -q or -t), which can complicate parsing [3]. Ensure your scripts are prepared to handle or ignore these error outputs [3]. - Tool Maturity: Be aware that gh api error handling is a known area for improvement in the GitHub CLI project [1][9][3]. Complex scripts may require manual status code checks or parsing of the response body to provide granular, user-friendly feedback [7][10].

Citations:


Make release-branch creation idempotent for reruns.

Line 99 always creates the ref unconditionally. If the workflow is retried after branch creation succeeds, this step fails with a 422 error and blocks recovery.

🔧 Proposed fix
       - 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"
+          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
🧰 Tools
🪛 zizmor (1.25.2)

[error] 98-98: code injection via template expansion (template-injection): may expand into attacker-controllable code

(template-injection)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/release.yml around lines 93 - 101, The "Create release
branch" step currently always POSTs a git ref and fails on retries; update the
script that uses GH_TOKEN, BRANCH and SHA (the gh api calls) to first check for
the ref's existence (gh api GET /repos/.../git/refs/heads/$BRANCH) and only POST
the new ref if the GET returns not found, or alternately catch a 422 from the
POST and treat it as success; modify the logic around the gh api calls in that
step so branch creation is idempotent on workflow reruns.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this makes sense @0xisk

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

Comment thread .github/workflows/release.yml Outdated
Comment on lines +93 to +101
- 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"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this makes sense @0xisk

0xisk added 2 commits May 20, 2026 11:25
- 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.
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).
@0xisk 0xisk merged commit 29ff45c into main May 20, 2026
10 checks passed
@0xisk 0xisk deleted the ci/pr-based-release branch May 20, 2026 09:37
@github-project-automation github-project-automation Bot moved this from Backlog to Complete/Stable Release in OZ Development for Midnight May 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Complete/Stable Release

Development

Successfully merging this pull request may close these issues.

2 participants