Fix: Auto-publish on release-PR merge (@W-23235833@) - #454
Conversation
Both release-on-merge.yml and publish.yml now fire off the same pull_request: closed event on a merged release/v* branch, both authenticate with GITHUB_TOKEN, neither depends on a release: published event authored by the other. This sidesteps GitHub's chain-block on GITHUB_TOKEN-authored release events (a release created by GITHUB_TOKEN cannot trigger a downstream workflow), which is why the previous release: published trigger left Publish to NPM dormant. Adds src/test/publishAutoTriggerWorkflow.mjs to lock the design at the string level: trigger surface, if-condition, checkout ref, workflow_dispatch input, and the GITHUB_TOKEN / no-PAT invariant on release-on-merge. Ports the pivot the isomorphic SDK shipped in PR #293 (W-23235820). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fork-PR attack surface: a maintainer merging a fork PR whose head branch is literally named release/v* would pass the gate and run npm publish with base-repo secrets on fork-authored code. Add head.repo.full_name == github.repository to the if-expression. Also drop the explicit npm run build — prepack: npm run build in package.json already runs it during npm publish, so the build was running twice per release. Lock test #2 updated in lockstep so a future revert of the fork guard trips CI. Red-on-revert / green-on-restore verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Approving — design is sound and honestly documented. Verified the dropped npm run build is safe: prepack: npm run build runs on npm publish, so the build still happens.
nit (would be nice to fold into this PR if it's little work): the version cross-check gap you flagged as future-consideration 1 is the one finding that's unrecoverable rather than just a ceremony cost. release-on-merge.yml runs the tri-source guard (branch ↔ package.json ↔ CHANGELOG); publish.yml fires in parallel with none. A version-mismatch merge aborts release-on-merge (no tag, no release) but still ships the npm publish — and npm unpublish is time-limited, so the "tag exists iff published" invariant breaks with no undo.
Everything else on your future-considerations list (loose branch gate, raw dispatch tag, no idempotency guard, no concurrency, no timeout-minutes) is recoverable via npm's 409/403 and fine to defer. This one's cheap-ish (re-run the same branch↔package.json compare, or an npm view <version> guard before publish) and closes the only data-loss path — worth pulling in now if it's low-effort. Not a blocker either way.
publish.yml fires in parallel with release-on-merge.yml on merge — with no downstream ordering. A version-mismatched merge would abort release-on-merge (no tag, no release) but publish.yml still ships to npm, breaking the "tag exists iff npm publish exists" invariant with no recovery (npm unpublish is time-limited). Add the tri-source check (branch/tag ↔ package.json ↔ CHANGELOG.md) before install. Two source paths (release-branch head.ref on the PR event, tag input on workflow_dispatch) both land in the same downstream comparison; error labels differ per arm so the two workflow logs are easier to correlate. Lock test #6 covers step presence, PR-path regex, dispatch-path regex, package.json read, CHANGELOG.md awk read, and the exit 1 on mismatch. Each assertion sensitivity-verified red-on-revert / green-on-restore. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
|
Good callout, folded the tri-source cross-check into Lock test #6 covers step presence, both regex arms, Rest of the future-considerations list (loose gate, idempotency, concurrency, timeout, squash SHA, lock-test brittleness, |
Summary
release-on-merge.ymlandpublish.ymlfire off the samepull_request: closedevent on a mergedrelease/v*branch, both authenticate withGITHUB_TOKEN, and run in parallel. Neither depends on a downstream event from the other.publish.ymlonrelease: published— bit us because GitHub blocks workflow chains authored byGITHUB_TOKEN, so the release fired but no runner observed it. v6.4.0 would have hit the same wall on merge.workflow_dispatchstays onpublish.ymlas a manual fallback that takes an existing tag and re-publishes that ref.publish.ymlnow runs the same tri-source version cross-check (branch/tag ↔package.json↔CHANGELOG.md) thatrelease-on-merge.ymldoes, so a version-mismatched merge can't ship npm without a matching tag.Test plan
npm run test:workflow— 21/21 pass (14 existing + 7 new). New suitesrc/test/publishAutoTriggerWorkflow.mjslocks the trigger surface (regex-on-YAML):pull_request: closedonmain,merged==true && same-repo && startsWith(head.ref, 'release/v')gate,merge_commit_shacheckout ref, absence ofrelease: published, requiredworkflow_dispatchtag input, thatrelease-on-mergenever reintroduces a PAT, and the new tri-source cross-check (step presence, PR-path regex, dispatch-path regex, package.json read, CHANGELOG awk read, exit 1 on mismatch).actionlint .github/workflows/publish.yml .github/workflows/release-on-merge.yml— clean.npm run lint— clean.Verification
What the local checks cover
GITHUB_TOKEN/ no-PAT invariant onrelease-on-merge, and the tri-source version cross-check onpublish.yml. Locked in CI viatest:workflow.What it does NOT cover
publish.ymlonpull_request: closedgiven thisif:gate, does the runner resolvemerge_commit_shato a checkoutable commit, dorelease-on-mergeandpublish.ymlboth fire on the same merge event. Not observable from a branch — we can't fake a merged PR without merging one, and this repo's release trigger doesn't fire on branch pushes.Plan to close the remaining gaps
pull_request: closedevent; both pass the tri-source cross-check;release-on-mergetags + creates the release withGITHUB_TOKEN;publish.ymlchecks out the merge commit andnpm publishsucceeds. Ifpublish.ymldoesn't fire, theworkflow_dispatchfallback unblocks: run it withtag: v6.4.0and it checks out the tagrelease-on-mergecreated.Follow-ups
GITHUB_TOKENcredential to create the release.") is superseded by this pivot. The pivot's premise is that no downstreamrelease: publishedevent is needed, so the release can stayGITHUB_TOKEN-authored. Iso ate this same AC change at PR Add CCDC object ids of inventory and slas admin apis #293 for the same reason (org-policy pushback on fine-grained PATs scoped toSalesforceCommerceCloud).Future considerations
Findings from a second-pass code review — surfaced here for visibility, may be revisited in a future hardening pass. No follow-up tickets filed; no guarantee they'll be picked up.
startsWith(head.ref, 'release/v')acceptsrelease/v-foo,release/vBAD,release/v-hotfix.release-on-mergecatches these downstream via^release/v([0-9]+\.[0-9]+\.[0-9]+)$—publish.yml's job-level gate has no such backstop, though the new tri-source cross-check inside the job now rejects them at step 3 anyway. Defense-in-depth gap at the gate level.cannot publish over previously published versions). Red X on a release PR that already succeeded, false alarm.release-on-mergehandles this pattern with a tag-exists / release-exists check;publish.ymlcould add anpm view <version>guard before publish.concurrency:group. Two rapid release-PR merges (or aworkflow_dispatchoverlapping a merge-triggered run) race onnpm publish. Not corrupting — npm 409s on duplicate versions and it's recoverable — but a real ceremony cost.concurrency: { group: publish, cancel-in-progress: false }serializes. Same trade-off iso PR Add CCDC object ids of inventory and slas admin apis #293 explicitly accepted.timeout-minutes. A hungnpm publish(registry outage) pins the runner for GitHub's 360-minute default.timeout-minutes: 15fails fast.merge_commit_shaon squash/rebase-merged PRs is the synthetic "test-merge" candidate, not the SHA that lands on main. Inherited fromrelease-on-merge— this diff perpetuates it.git checkout v6.5.0would land on a detached HEAD diverging from main's linear history. Cleanup would touchrelease-on-mergetoo.if: |block-scalar form. A benign yamlfmt/prettier reformat could red the CI without changing YAML semantics. Also, therelease: publishedanti-regex misses quoted / spaced / block-list evasions (types: ['published'],types:\n - published, flow-syntax) — a future revert of the pivot could pass CI. A YAML-parsing approach (assert on the parsed structure) would be robust to both..mjsexcluded from eslint.lint:devrunseslint . --ext .ts. The new lock-test file has no lint coverage — a missing header or unused import would go undetected. SiblingreleaseOnMergeWorkflow.mjshas the same gap.Out of scope
peter-evans/create-github-app-token). Mooted by the pivot — no PAT means no App-replacement need.actions/checkout@v3intest.yml. Separate concern, keeps this diff focused.Ticket
W-23235833