Skip to content

Fix: Auto-publish on release-PR merge (@W-23235833@) - #454

Merged
joeluong-sfcc merged 3 commits into
mainfrom
ju/publish-autotrigger-W-23235833
Jul 15, 2026
Merged

joeluong-sfcc merged 3 commits into
mainfrom
ju/publish-autotrigger-W-23235833

Conversation

@joeluong-sfcc

@joeluong-sfcc joeluong-sfcc commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • Same fix the isomorphic SDK just landed in PR Add CCDC object ids of inventory and slas admin apis #293 (W-23235820): both release-on-merge.yml and publish.yml fire off the same pull_request: closed event on a merged release/v* branch, both authenticate with GITHUB_TOKEN, and run in parallel. Neither depends on a downstream event from the other.
  • Previous shape had publish.yml on release: published — bit us because GitHub blocks workflow chains authored by GITHUB_TOKEN, so the release fired but no runner observed it. v6.4.0 would have hit the same wall on merge.
  • workflow_dispatch stays on publish.yml as a manual fallback that takes an existing tag and re-publishes that ref.
  • publish.yml now runs the same tri-source version cross-check (branch/tag ↔ package.jsonCHANGELOG.md) that release-on-merge.yml does, 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 suite src/test/publishAutoTriggerWorkflow.mjs locks the trigger surface (regex-on-YAML): pull_request: closed on main, merged==true && same-repo && startsWith(head.ref, 'release/v') gate, merge_commit_sha checkout ref, absence of release: published, required workflow_dispatch tag input, that release-on-merge never 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).
  • Sensitivity-checked each assertion by reverting the surface it locks and confirming the matching test goes red before restoring.
  • actionlint .github/workflows/publish.yml .github/workflows/release-on-merge.yml — clean.
  • npm run lint — clean.

Verification

What the local checks cover

  • The YAML surface: trigger, if-condition, checkout ref, workflow_dispatch input, the GITHUB_TOKEN / no-PAT invariant on release-on-merge, and the tri-source version cross-check on publish.yml. Locked in CI via test:workflow.

What it does NOT cover

  • The workflow-runtime plumbing: does GitHub actually fire publish.yml on pull_request: closed given this if: gate, does the runner resolve merge_commit_sha to a checkoutable commit, do release-on-merge and publish.yml both 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

  • First real closure is v6.4.0's merge (release PR Release v6.4.0 for ECOM v26.7 (@W-23232606@) #452, W-23232606). Expected: both workflows fire in parallel from the same pull_request: closed event; both pass the tri-source cross-check; release-on-merge tags + creates the release with GITHUB_TOKEN; publish.yml checks out the merge commit and npm publish succeeds. If publish.yml doesn't fire, the workflow_dispatch fallback unblocks: run it with tag: v6.4.0 and it checks out the tag release-on-merge created.

Follow-ups

  • Ticket's original AC-3 ("Workflow uses a non-GITHUB_TOKEN credential to create the release.") is superseded by this pivot. The pivot's premise is that no downstream release: published event is needed, so the release can stay GITHUB_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 to SalesforceCommerceCloud).

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.

  • Loose branch-name gate. startsWith(head.ref, 'release/v') accepts release/v-foo, release/vBAD, release/v-hotfix. release-on-merge catches 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.
  • No idempotency guard. Re-running the workflow after a successful publish 403s on npm (cannot publish over previously published versions). Red X on a release PR that already succeeded, false alarm. release-on-merge handles this pattern with a tag-exists / release-exists check; publish.yml could add a npm view <version> guard before publish.
  • No concurrency: group. Two rapid release-PR merges (or a workflow_dispatch overlapping a merge-triggered run) race on npm 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.
  • No timeout-minutes. A hung npm publish (registry outage) pins the runner for GitHub's 360-minute default. timeout-minutes: 15 fails fast.
  • merge_commit_sha on squash/rebase-merged PRs is the synthetic "test-merge" candidate, not the SHA that lands on main. Inherited from release-on-merge — this diff perpetuates it. git checkout v6.5.0 would land on a detached HEAD diverging from main's linear history. Cleanup would touch release-on-merge too.
  • Lock-test regex brittleness. Tests hard-code whitespace, key order, and the if: | block-scalar form. A benign yamlfmt/prettier reformat could red the CI without changing YAML semantics. Also, the release: published anti-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.
  • .mjs excluded from eslint. lint:dev runs eslint . --ext .ts. The new lock-test file has no lint coverage — a missing header or unused import would go undetected. Sibling releaseOnMergeWorkflow.mjs has the same gap.

Out of scope

  • Migrating to a GitHub App-based token (peter-evans/create-github-app-token). Mooted by the pivot — no PAT means no App-replacement need.
  • Trusted Publishing (OIDC) on npm — tracked separately as W-22028201.
  • Bumping actions/checkout@v3 in test.yml. Separate concern, keeps this diff focused.

Ticket

W-23235833

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>
@joeluong-sfcc
joeluong-sfcc requested a review from a team as a code owner July 14, 2026 07:48
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>

@vmarta vmarta left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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>
@joeluong-sfcc

Copy link
Copy Markdown
Collaborator Author

Good callout, folded the tri-source cross-check into publish.yml — same shape as release-on-merge.yml (branch/tag ↔ package.jsonCHANGELOG.md, fail-loud on mismatch). Two source paths (PR head.ref, workflow_dispatch tag input) both land in the same downstream comparison; error labels differ per arm so the two workflow logs are easy to correlate on a mismatched merge.

Lock test #6 covers step presence, both regex arms, package.json read, CHANGELOG awk read, and the exit 1. Each assertion sensitivity-verified red-on-revert, green-on-restore. Full suite is 21/21.

Rest of the future-considerations list (loose gate, idempotency, concurrency, timeout, squash SHA, lock-test brittleness, .mjs lint gap) still deferred as you called out.

@joeluong-sfcc
joeluong-sfcc merged commit 282fddf into main Jul 15, 2026
6 checks passed
@joeluong-sfcc
joeluong-sfcc deleted the ju/publish-autotrigger-W-23235833 branch July 15, 2026 00:17
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