Skip to content

fix(publish): land bumps via PR + squash auto-merge; guard build reproducibility - #671

Open
John-David Dalton (jdalton) wants to merge 2 commits into
mainfrom
fix/release-tooling-ff-and-reproducible
Open

fix(publish): land bumps via PR + squash auto-merge; guard build reproducibility#671
John-David Dalton (jdalton) wants to merge 2 commits into
mainfrom
fix/release-tooling-ff-and-reproducible

Conversation

@jdalton

Copy link
Copy Markdown
Collaborator

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 main to the release-app-signed bump commit. It did this with a direct fast-forward ref PATCH (updateBranchRef), which a branch-protected main rejects with 422 "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). promoteReleaseBranch now 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 to chore: bump version to <version> so the reconcile anchor survives the squash.

How it works + the provenance tradeoff
  • New scripts/fleet/lib/github-pull-requests.mts:
    • createPullRequest — idempotent (a re-run whose branch already has an open PR reuses it on 422 instead of duplicating).
    • enablePullRequestAutoMerge — GraphQL (the only auto-merge surface); returns false (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.
    • All calls go over node:http (httpJson), so nock intercepts them in tests.
  • ReleaseBranch now carries version, used to pin the squash subject and title the PR.
  • Provenance. The repo's branch protection allows squash-merge only (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
  1. Auto-merge is already enabled on the repo (allow_auto_merge: true) — no action needed.
  2. Required review. main requires 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.
  3. App permission. The release App token must have pull_requests: write (it already has contents: write for the bump commit).
  4. Cascade ownership. 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 --reconcile

Investigated the claimed non-determinism — the build is already byte-reproducible. The premise (non-deterministic rolldown bundling) does not hold:

  • Two consecutive local pnpm run build runs produce byte-identical dist/ (all 23 files, including the content-hashed rolldown-runtime-*.js chunk).
  • A fresh local build's dist/ is byte-identical to the published v4.1.1 npm tarball's dist/ — reproducible across NODE_ENV and across absolute checkout paths (local /tmp/... vs CI /home/runner/...).
  • The only non-dist/ differences vs the published tarball (README badge URL, pruned preinstall script) 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 CI release-reconcile.yml workflow already does the right thing (checkout content commit → pnpm install --frozen-lockfilepnpm 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.mts builds 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.
  • New tests pass: github-pull-requests.test.mts (8), build-reproducible.test.mts (2).
  • Two-build byte-identical dist/ proven (diff -rq exit 0); fresh build == published v4.1.1 tarball dist/ (diff -rq exit 0).
  • Full suite: the only failures are pre-existing and environment-specific — utils.test.mts asserts the checkout dir is named socket-sdk-js, but this worktree is /tmp/sdk-release-tooling. Unrelated to this change.

Do not merge — release-critical, for review.

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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant