Skip to content

ci: auto-tag release commits and fix publish-svn permissions#424

Merged
sherwinski merged 1 commit into
mainfrom
sherwin/ci-tag-on-release-merge
May 21, 2026
Merged

ci: auto-tag release commits and fix publish-svn permissions#424
sherwinski merged 1 commit into
mainfrom
sherwin/ci-tag-on-release-merge

Conversation

@sherwinski

Copy link
Copy Markdown
Contributor

One Line Summary

Adds a tag-on-merge workflow so the release pipeline runs end-to-end without manual git tag, and fixes a pull-requests permission bug that was causing publish-svn.yml to fail at startup.

Motivation

The release flow had two gaps that surfaced when shipping v3.9.0:

  1. No automatic tagging after the release PR merges. release.yml (and release-v2.yml) only opens the release PR. Nothing pushes the vX.Y.Z tag once it merges, so the tag-triggered publish-svn.yml workflow never fires, which means no WordPress.org SVN deploy and no GitHub release. For v3.9.0 I had to manually git tag v3.9.0 <sha> && git push origin v3.9.0 and manually create the GitHub release.

  2. publish-svn.yml has a permissions bug. When I pushed v3.9.0 to verify the tag-triggered path, publish-svn.yml failed at startup with:

    Error calling workflow 'OneSignal/sdk-shared/.github/workflows/github-release.yml@main'. The workflow is requesting 'pull-requests: read', but is only allowed 'pull-requests: none'.

    The shared github-release.yml declares pull-requests: read (needed by github.rest.pulls.list() to look up the release PR body), but the calling github-release job in publish-svn.yml only granted contents: write. Same bug exists in publish-svn-v2.yml.

Scope

  • Affected:
    • New .github/workflows/tag-on-release-merge.yml triggers on push to main / v2, parses the squash-merge subject for chore: Release X.Y.Z (#NNN), and pushes the vX.Y.Z tag using GH_PUSH_TOKEN (the default GITHUB_TOKEN cannot trigger downstream workflows on tag push).
    • publish-svn.yml and publish-svn-v2.yml: add pull-requests: read to the github-release job permissions.
  • Not affected: plugin source code, release.yml, release-v2.yml, the SVN deploy logic itself.

Testing

Manual

This PR is what the next release would exercise. Validation plan once merged:

  1. Confirm Tag on Release Merge is listed under Actions.
  2. On the next release (e.g. 3.9.1), after the release PR merges into main, verify:
    • Tag on Release Merge runs and pushes v3.9.1.
    • Publish to WordPress.org (v3 trunk) triggers from the tag push.
    • Both publish and github-release jobs complete (the latter previously got startup_failure).
    • A GitHub release for 3.9.1 is created automatically.

Local sanity checks on the new workflow:

  • Subject regex matches chore: Release 3.9.0 (#423) and extracts 3.9.0.
  • Non-matching subjects (e.g. fix: ...) are skipped via the job-level if.
  • Tag creation is idempotent — re-runs against an existing tag exit cleanly.

Unit / Integration

  • N/A — CI workflow changes only, no PHP source touched.
  • composer test not applicable to this PR

Affected code checklist

  • PHP plugin code (v3/)
  • v2/ legacy code
  • JS / CSS assets
  • Build / CI
  • Tests
  • Documentation (README, readme.txt, etc.)

Checklist

  • Code follows existing style in the touched files
  • Tested manually in the relevant editor(s) — N/A
  • No new lint or test errors introduced
  • Linear ticket / GitHub issue linked above — N/A, infra fix
  • readme.txt and plugin version bumped if user-facing — N/A

Follow-ups (not in this PR)

  • The repo does not appear to have SVN_USERNAME / SVN_PASSWORD secrets at the repo or visible org level. The SVN deploy step in publish-svn.yml will still fail on the next release if those aren't configured. Worth confirming whether they live in a restricted org scope.
  • v3.9.0 was released manually outside this flow (tag + GitHub release published by hand) before this PR was opened.

Adds a tag-on-merge workflow that pushes a vX.Y.Z tag when a
"chore: Release X.Y.Z" commit lands on main or v2, so the tag-
triggered publish-svn workflows fire automatically and the
release pipeline runs end-to-end without a manual git tag step.

The tag is pushed using GH_PUSH_TOKEN; tags pushed with the
default GITHUB_TOKEN do not trigger other workflows.

Also fixes publish-svn{,-v2}.yml: the github-release reusable
workflow declares pull-requests: read, but the caller jobs only
granted contents: write, causing startup_failure with:

  The workflow is requesting 'pull-requests: read', but is only
  allowed 'pull-requests: none'.
@sherwinski sherwinski requested a review from fadi-george May 21, 2026 23:28
@sherwinski sherwinski merged commit 44473f1 into main May 21, 2026
10 checks passed
@sherwinski sherwinski deleted the sherwin/ci-tag-on-release-merge branch May 21, 2026 23:33

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

LGTM — straightforward CI fix (permissions) plus a small, idempotent tag-on-merge workflow.

Extended reasoning...

Overview

This PR makes two CI-only changes: (1) adds pull-requests: read permission to the github-release job in both publish-svn.yml and publish-svn-v2.yml (a one-line permissions fix for a documented startup failure), and (2) adds a new tag-on-release-merge.yml workflow that watches main and v2 for chore: Release X.Y.Z commits and pushes the corresponding vX.Y.Z tag using GH_PUSH_TOKEN so downstream tag-triggered workflows fire.

Security risks

Minimal. The new workflow only triggers on push to main/v2 (both protected via PR-only merges), uses a regex anchored to the full subject line, and exits cleanly if the tag exists. The permission addition is the narrowest scope (read) needed by the called shared workflow. GH_PUSH_TOKEN is a PAT that already exists in the repo's secrets and is used elsewhere — no new credentials introduced.

Level of scrutiny

Moderate — this is release/deploy infrastructure, but the changes themselves are small, mechanical, and well-scoped. The permissions fix is unambiguously correct. The new tag workflow's logic is short enough to read end-to-end and matches the documented behavior in the PR description.

Other factors

The one bug surfaced by the bug-hunting pass is explicitly filed as a nit — a defense-in-depth gap where mistyping a major version into the wrong of two near-identical workflow_dispatch UIs could cross-publish trees. The inline comment captures the exploit path and a two-line guard; the author can decide whether to add it now or in a follow-up. Not a blocker for this PR.

Comment on lines +34 to +38
VERSION=$(echo "$SUBJECT" | sed -nE 's/^chore: Release ([0-9]+\.[0-9]+\.[0-9]+)( \(#[0-9]+\))?$/\1/p')
if [ -z "$VERSION" ]; then
echo "Commit subject is not a release commit; skipping: $SUBJECT"
exit 0
fi

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🟡 Defense-in-depth gap: the workflow extracts any X.Y.Z from chore: Release X.Y.Z and pushes vX.Y.Z without verifying that the major matches the branch (main → 3.x, v2 → 2.x). Because the version input to release.yml / release-v2.yml is a free-form string with no validation, an operator running the wrong Release workflow (e.g. Release (v2) with 3.9.1) would land a chore: Release 3.9.1 commit on v2; this job would then push tag v3.9.1 at a v2 commit, and publish-svn.yml (which has no trunk-version guard, unlike its v2 sibling) would silently deploy v2 source to the v3 trunk on wordpress.org. Consider rejecting when the version major doesn't match the branch (fail if [[ $GITHUB_REF_NAME == main && $VERSION != 3.* ]] || [[ $GITHUB_REF_NAME == v2 && $VERSION != 2.* ]]).

Extended reasoning...

What the bug is

tag-on-release-merge.yml triggers on push to both main and v2 and extracts the version with this regex:

VERSION=$(echo "$SUBJECT" | sed -nE 's/^chore: Release ([0-9]+\.[0-9]+\.[0-9]+)( \(#[0-9]+\))?$/\1/p')

It then pushes v$VERSION at $GITHUB_SHA with no cross-check that the major component of VERSION matches the branch the commit landed on. Because publish-svn.yml triggers on v3.* tags and publish-svn-v2.yml on v2.* tags, a tag whose major doesn't match its commit's branch routes the wrong source tree to the wrong WordPress.org slot.

The code path that triggers it

release.yml and release-v2.yml both expose a free-form string version input via workflow_dispatch with no validation:

version:
  description: 'Version to release (e.g., 3.9.0)'
  required: true
  type: string

release.yml hardcodes target_branch: main; release-v2.yml hardcodes target_branch: v2. The branch is locked, but the version isn't validated against it.

Step-by-step proof (mistyped version)

  1. Operator opens the Actions UI and runs Release (v2) with version: 3.9.1 (typo / wrong workflow).
  2. release-v2.yml calls prep-release.yml with version=3.9.1, target_branch=v2, which opens a PR titled chore: Release 3.9.1 (#NNN) against v2. The bump-version step only edits version files on a branch off v2, so the PR diff is small and looks normal.
  3. PR is squash-merged into v2. The merge commit subject is chore: Release 3.9.1 (#NNN).
  4. tag-on-release-merge.yml fires on the push to v2. The regex extracts 3.9.1. The workflow pushes tag v3.9.1 at the v2 head SHA.
  5. publish-svn.yml triggers on the v3.* tag, checks out v3.9.1 (= a v2 commit), rsyncs the v2 source tree into /tmp/release-files, and svn commits it to the v3 trunk on wordpress.org as version 3.9.1.

The mirror case (Release (v3) with version 2.4.6) lands a chore: Release 2.4.6 commit on main, pushes v2.4.6, and triggers publish-svn-v2.yml. In that direction, the Verify trunk is untouched step (publish-svn-v2.yml:110-124) would catch it — but the v3 direction has no equivalent guard.

Why existing code doesn't prevent it

  • The release workflows lock target_branch but not major(version) == branch_major.
  • tag-on-release-merge.yml only checks startsWith(message, 'chore: Release ') and regex shape — not major-vs-branch.
  • publish-svn.yml has no trunk-version sanity check (only its v2 sibling does), so the v3 direction silently publishes.

Addressing the refutation

The refutation argues this requires manually retargeting the auto-PR base branch, which is implausible. That scenario is indeed implausible — but it isn't the only path. The mistyped-version-input path above doesn't require retargeting anything; it only requires running the wrong of two near-identically-named release workflows or making a typo on the major digit. That's a normal-human-error category (the inputs are free-form strings on workflows named Release (v2) and Release (v3) sitting next to each other in the Actions list). The refutation also claims the payload outcome is technically incorrect because the bump-version branch would carry v3 code; that's exactly right and exactly the problem — v3 code (the entire v3/ tree) gets published to wordpress.org under a v2 version number (or vice versa).

The refutation's point that the right place to harden against this is the prep workflow (validate version major against target_branch) is fair; either layer works. A guard here is the simpler and more local change since this PR introduces the new automatic-tag step.

Impact

A successful misroute publishes the wrong major's source tree to the WordPress.org plugin directory under the wrong version number. Reverting an SVN trunk commit and a wordpress.org-visible plugin version is operationally painful and user-visible. The v3 direction is unguarded.

How to fix

Two-line guard before pushing the tag:

case "$GITHUB_REF_NAME" in
  main) [[ "$VERSION" == 3.* ]] || { echo "::error::version $VERSION on main; expected 3.x"; exit 1; } ;;
  v2)   [[ "$VERSION" == 2.* ]] || { echo "::error::version $VERSION on v2; expected 2.x"; exit 1; } ;;
esac

This converts the silent mis-publish into a CI failure on the tag step, before anything reaches SVN.

Severity

Filing as nit: requires operator error, but the fix is trivial and the blast radius (wrong major published to wordpress.org with no v3-side guard) is non-trivial.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants