Skip to content

Execute the first v2/main → main milestone merge and cut the release #1876

Description

@cliffhall

Status 2026-08-02 — the merge is built and waiting on one approval.

PR #1903 is open, MERGEABLE, with npm run ci green on the merged tree. Everything below that was a question has been answered; the original plan needed three corrections, all applied. The only thing left is a maintainer approval (olaservo / BobDickinson) — then merge, then cut the release.

The full v2.1.0 payload — 14 commits, grouped — is in the milestone overview comment.

Why this merge is different from every one that follows

This is the first v2/mainmain milestone merge, and the only one that will look like this. Read the sequence below as a one-off, not as the template.

  1. The branches only just acquired a shared history. They had unrelated histories until the go-live merge (chore: replace main's tree with v2 #1830) established merge base 4d30d1cd. That makes this an ordinary merge — but it is the first time that has been true, which is why the "never use --allow-unrelated-histories" warning needs saying at all.
  2. It needed convergence work that future merges will not. docs: converge README Test servers section with main's restructure #1880 (README) and chore(deps): bring main's rc-series bumps to v2/main (@hono/node-server 2.x, vite 8.1.5) #1881 (main's rc-series deps) existed purely to shrink this merge. Nothing analogous will be needed again.
  3. The payload is repo infrastructure, not product work. The tree swap dropped files that lived only in the old tree, so most of this milestone restores them. Exactly one PR (fix(core): mirror SEP-2243 x-mcp-header args to Mcp-Param-* on tools/call #1847) changes runtime behavior. A normal milestone will invert that ratio.

Do not "fix" the divergence with a back-merge of main into v2/main. Built and rejected — see #1868 (closed). It would make the merge conflict-free, but only by pulling 224 commits, 212 of them pre-swap v1-tree commits, permanently into the develop branch's ancestry. git log, bisect, and every GitHub compare on v2/main would carry the v1 lineage from then on.

The sequence

Step 1 is done. #1903 carries it.

# 1. DONE — the merge, resolved on a branch cut from main so v2/main
#    stays untouched (see "the eight conflicts" below).
#      git checkout -b v2/chore/milestone-merge-v2.1.0 origin/main
#      git merge --no-ff origin/v2/main
#    → merge commit 6233d8bb, parents fb1b0cb4 (main) + 834fd2d7 (v2/main)
#    → npm run ci green on the merged tree
#    → opened as PR #1903

# 2. Get ONE approving review on #1903, then merge it with the button
#    (or `gh pr merge 1903 --merge`). A local merge + `git push origin main`
#    is REJECTED — see "main is ruleset-protected" below.

# 3. Cut the release. The version bump is ALREADY on #1903's branch
#    (dedee5af, root package.json 2.0.0 -> 2.1.0), so there is no
#    second bump PR. Two equivalent ways to do this after #1903 merges:
#
#    (a) ENTIRELY IN THE GITHUB UI — no local git needed. Releases > Draft
#        a new release > "Choose a tag" > type 2.1.0 > "Create new tag on
#        publish", with Target = main. Publishing creates the tag AND the
#        release in one step.
#
#    (b) Or from the CLI, if you prefer the tag to exist first:
#          git fetch origin && git checkout main && git pull
#          git tag 2.1.0 && git push origin 2.1.0   # tags are NOT ruleset-protected
#        then draft & publish the release for that tag (UI, or
#        `gh release create 2.1.0 --title 2.1.0 --notes "..."`).
#
#    Either way, publishing the RELEASE is what publishes to npm. Both
#    publish jobs are gated on `github.event_name == 'release'`, so a tag
#    push alone runs the `build` job (the workflow's `on: push:` is
#    unqualified) but will NOT publish. The `publish` job runs pack:verify,
#    asserts the tag matches package.json's version, and pushes the
#    `latest` dist-tag.

Why the tag comes after the merge, not with the bump. #1903 merges as a merge commit, so dedee5af lands on main intact — but only once it merges. A tag created now would point at a commit that isn't on the release branch yet, which is why the bump used npm version --no-git-tag-version. With option (a) the tag is never pushed by hand at all; GitHub creates it against main when you publish.

The eight conflicts — resolved, and the prediction that was wrong

This issue originally predicted zero conflicts once #1880 landed. That went stale: main subsequently took 8 post-swap commits v2/main never did (#1843, #1842, #1841, #1837, #1836, #1835, #1834, #1832, #1831), and #1899/#1900 regenerated every lockfile. A direct v2/mainmain PR (#1901, closed) came up CONFLICTING in 8 files.

All eight took v2/main's side in #1903, each verified rather than assumed:

File Conflict Why v2/main wins
README.md content main's Mcp-Param-* callout is stale; v2/main's is corrected by #1847
SECURITY.md add/add restored independently (#1843 / #1867); differ by one line — the CONTRIBUTORS.mdCONTRIBUTING.md rename (#1884)
clients/web/package.json content vitest ^4.1.0^4.1.10, the #1899 security fix
root + 4 client package-lock.json content took v2/main's, then regenerated from a clean npm installzero drift

⚠️ Never resolve these with GitHub's web "Resolve conflicts" button. It commits to the head branch — merging main into v2/main, i.e. exactly the #1868 back-merge. Resolving on a branch cut from main avoids that, and the merge commit still carries v2/main's tip as its second parent, so the self-sustaining property below still holds.

main is ruleset-protected

Classic branch protections are not configured (GET /branches/main/protection404), which is misleading — protection comes from rulesets. Two target ~DEFAULT_BRANCH, and 2749948 grants bypass to nobody, repo admins included.

  • A PR is mandatory. git push origin main is rejected. Applies to npm version too.
  • 1 approving review, from someone other than the author. A Copilot approval does not count.
  • The build check must pass.
  • allowed_merge_methods includes merge, so the --no-ff merge commit is permitted — merge via the button, not a local merge + push.
  • Not blockers, verified: require_code_owner_review is inert (no CODEOWNERS anywhere), and the release tag is unrestricted (the only tag ruleset matches refs/tags/1.*).

Pre-flight

  • Branch protection on main permits a maintainer merge commit — it does, but only via a PR.
  • Decide PR vs. direct maintainer mergenot a choice; a PR is mandatory.
  • Conflict expectations — 8 conflicts, all resolved in chore: merge v2/main for the v2.1.0 milestone release #1903, npm run ci green.
  • Approving reviewer available — olaservo and BobDickinson.
  • Get the approval on chore: merge v2/main for the v2.1.0 milestone release #1903 and merge it.
  • Cut the release — push the 2.1.0 tag, then draft & publish the GitHub Release (step 3 above). The version bump is already on the branch, so no second PR.

After it lands

Then it's self-sustaining

Once v2/main's tip is an ancestor of main, and main makes no content changes of its own, every subsequent milestone merge is conflict-free with no back-merge needed. This one-time conflict is the whole cost.

The last thing standing in the way of that was .github/workflows/main.yml — the only file where main still had content v2/main lacked (#1831 least-privilege token, #1834 dist-tag derivation, #1836 OIDC trusted publishing). Fixed by #1904 (Closes #1902, merged 07a2b4bd): since v2/main had nothing main lacked except the superseded publish step, it took main's file verbatim. git diff origin/main origin/v2/main -- .github/workflows/main.yml is now empty.

Metadata

Metadata

Assignees

Labels

v2Issues and PRs for v2

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions