fix(publish): land bumps via PR + squash auto-merge; guard build reproducibility - #671
Open
John-David Dalton (jdalton) wants to merge 2 commits into
Open
fix(publish): land bumps via PR + squash auto-merge; guard build reproducibility#671John-David Dalton (jdalton) wants to merge 2 commits into
John-David Dalton (jdalton) wants to merge 2 commits into
Conversation
The publish pipeline promotes a successful release by advancing main to the
release-app-signed bump commit. It did this with a direct fast-forward ref
PATCH, which a branch-protected main rejects with 422 ("Changes must be made
through a pull request") because the release App is not on main's push-bypass
allowlist. Every release therefore needed a maintainer to hand-land the bump.
promoteReleaseBranch now opens a PR from the release branch and enables squash
auto-merge (with an immediate-merge fallback when there is nothing to wait on),
so the bump lands within branch protection without a bypass. The squash commit
subject is pinned to `chore: bump version to <version>` so the reconcile anchor
survives the squash.
- Add github-pull-requests.mts: createPullRequest (idempotent on 422),
enablePullRequestAutoMerge (GraphQL), mergePullRequest (REST fallback), all
over node:http so nock intercepts them.
- Thread the version through ReleaseBranch for the pinned squash subject.
- Cover the helpers + promote flow with nock unit tests.
publish-pipeline `--reconcile <ver>` cuts a version's tag + GH release only when a local re-pack byte-matches the published npm tarball, so the bundle must be byte-reproducible run-to-run. It already is — a fresh build's dist/ is byte-identical to the published v4.1.1 tarball's dist/ (all files, including the content-hashed rolldown-runtime chunk), and two consecutive builds are byte-identical. This adds a guard so a future config/toolchain change that reintroduces non-determinism (timestamp banner, unstable chunk hash, order churn) fails here instead of silently breaking the next reconcile. Builds each rolldown config (node + browser) twice into isolated temp dirs and asserts the emitted bytes and content-hashed file names match between runs.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes two release-tooling gaps found during the 4.1.0 / 4.1.1 releases.
#75 — release App can't fast-forward main (every release needed a hand-land)
Problem. On a successful publish, the pipeline advances
mainto the release-app-signed bump commit. It did this with a direct fast-forward refPATCH(updateBranchRef), which a branch-protectedmainrejects with422 "Changes must be made through a pull request"— the release App (SOCKET_RELEASE_CLIENT_ID) is not on main's push-bypass allowlist. So every 4.1.0 / 4.1.1 release required a maintainer to hand-land the bump.Fix (in the tooling, not a settings change).
promoteReleaseBranchnow opens a PR from the release branch and enables squash auto-merge, so the bump lands within branch protection — no bypass needed. The squash commit subject is pinned tochore: bump version to <version>so the reconcile anchor survives the squash.How it works + the provenance tradeoff
scripts/fleet/lib/github-pull-requests.mts:createPullRequest— idempotent (a re-run whose branch already has an open PR reuses it on422instead of duplicating).enablePullRequestAutoMerge— GraphQL (the only auto-merge surface); returnsfalse(doesn't throw) on GitHub's "clean status" refusal so the caller can merge immediately.mergePullRequest— REST fallback for the "already mergeable, nothing to wait on" case.node:http(httpJson), sonockintercepts them in tests.ReleaseBranchnow carriesversion, used to pin the squash subject and title the PR.allow_merge_commit: false,allow_rebase_merge: false), so the release App's exact app-signed commit SHA can't be preserved verbatim under any PR-based flow. The squashed commit is created under the release App and GitHub-signed (Verified), carrying byte-identical bump content and the same subject. Preserving the exact SHA would require enabling merge/rebase merges + adding the App to a push-bypass — i.e. the settings change this fix avoids.Prerequisites / decisions needed before this fully automates
allow_auto_merge: true) — no action needed.mainrequires 1 approving review. Auto-merge will queue the PR but only completes once that requirement clears. For hands-free releases the release App needs to satisfy/bypass the PR-review requirement (a ruleset bypass actor for the App). Without it, the worst case is a maintainer clicking "approve" — still strictly better than today's manual hand-land. This is the one piece a workflow change can't remove on its own; flagging it as a decision.pull_requests: write(it already hascontents: writefor the bump commit).scripts/fleet/*are cascade-owned mirror files (the pre-commit reports them as "cascade payload — gated at the template source"). To persist fleet-wide, this change should also land in the wheelhouse template; otherwise the next cascade reverts it here.#76 — build reproducibility for
--reconcileInvestigated the claimed non-determinism — the build is already byte-reproducible. The premise (non-deterministic rolldown bundling) does not hold:
pnpm run buildruns produce byte-identicaldist/(all 23 files, including the content-hashedrolldown-runtime-*.jschunk).dist/is byte-identical to the published v4.1.1 npm tarball'sdist/— reproducible across NODE_ENV and across absolute checkout paths (local/tmp/...vs CI/home/runner/...).dist/differences vs the published tarball (README badge URL, prunedpreinstallscript) are exactly what the reconcile's own pack brackets (withPinnedReadme+withPrunedPackManifest) apply, so the reconcile re-pack matches too.So the historical reconcile refusals weren't bundler non-determinism — they were operational: the local re-pack compared against a
dist/that wasn't a fresh rebuild from the content commit with the frozen lockfile. The CIrelease-reconcile.ymlworkflow already does the right thing (checkout content commit →pnpm install --frozen-lockfile→pnpm run build→--reconcile), so the production reconcile path is already sound.Rather than fabricate a config change that just restates already-correct defaults, this adds a determinism guard test that locks in the reproducibility contract.
The guard test
test/repo/unit/build-reproducible.test.mtsbuilds each rolldown config (node + browser) twice into isolated temp dirs and asserts the emitted bytes and content-hashed file names are identical between runs. A future config/toolchain change that reintroduces non-determinism (timestamp banner, unstable chunk hash, order churn) fails here instead of silently breaking the next reconcile.Verification
pnpm run type— clean.pnpm run lint— clean.github-pull-requests.test.mts(8),build-reproducible.test.mts(2).dist/proven (diff -rqexit 0); fresh build == published v4.1.1 tarballdist/(diff -rqexit 0).utils.test.mtsasserts the checkout dir is namedsocket-sdk-js, but this worktree is/tmp/sdk-release-tooling. Unrelated to this change.Do not merge — release-critical, for review.