Skip to content

Proposal: gate breaking changes behind a next prerelease branch (and make /next/ mean something) #142

Description

@jamlen

Summary

The docs site publishes a /next/ section that is meant to show unreleased documentation. Under the current trunk-based auto-publish model it cannot do that, and today it does something worse: it shows already-released documentation that /latest/ is missing.

This proposes gating breaking changes behind a next prerelease branch, which would make /next/ genuinely mean "not yet released" and remove the need to hand-sequence a major release.

Raising for discussion rather than acting — this changes contributor workflow and needs a repo settings change.

The problem

/next/ cannot diverge under auto-publish

release.yml runs on every push to main, and semantic-release releases from main. So:

  • A releasable commit (feat / fix / perf / refactor / revert / breaking) publishes the moment it merges. If its docs ride in the same PR — as they should — next and latest converge at merge time. The divergence window is the length of the release run.
  • The only genuine divergence is doc-only commits (docs:, chore:, test:, ci:), which by definition describe already-released behaviour. That is "corrections to current", not "next".
  • The case that would justify /next/ — writing docs ahead of a feature — is impossible, because merging the feature releases it.

/next/ only makes sense with a release train: accumulate changes, release deliberately. That is the developmaster model this repo deliberately dropped in #114.

What /next/ contains today

docs/next and docs/v2 differ in 7 files. The divergence is documentation for fluent chaining (called.once().withArg(...)), which shipped in v2.2.0 and is live on npm now.

$ diff -rq docs/next docs/v2
Files docs/next/ai/canonical-examples.md and docs/v2/ai/canonical-examples.md differ
Files docs/next/ai/common-mistakes.md   and docs/v2/ai/common-mistakes.md differ
Files docs/next/ai/decision-tree.md     and docs/v2/ai/decision-tree.md differ
Files docs/next/api/index.md            and docs/v2/api/index.md differ
Files docs/next/api/types.md            and docs/v2/api/types.md differ
Files docs/next/guide/expectations.md   and docs/v2/guide/expectations.md differ
Files docs/next/guide/typescript.md     and docs/v2/guide/typescript.md differ

Both directories were last touched by the same commit (75bd576, 2026-04-25). docs/v2 was snapshotted from a state predating the v2.1/v2.2 feature docs, and the release-time snapshot step has never worked (see #132).

Net effect: /latest/ — where the site root redirects every visitor — is missing documentation for features released four months ago, and the fix is filed under a heading that says "unreleased".

Why it matters now

#135 is a breaking change (feat!) sitting green against main. Merging it publishes v3.0.0 immediately, because release.yml fires on every push to main. There is no gap in which to prepare docs, README, or the version switcher — the merge is the release.

That is workable with careful sequencing, but it means every future major carries the same one-shot risk.

Proposal

Use semantic-release's prerelease branch support.

// .releaserc.json
"branches": [
  "main",
  { "name": "next", "prerelease": true }
]
Branch Publishes dist-tag Install
main 2.3.0, 2.2.1 latest npm i deride
next 3.0.0-next.1, .2 next npm i deride@next
# release.yml
on:
  push:
    branches: [main, next]

Auto-publish on main is unchanged — non-breaking changes keep shipping on merge. Breaking changes never touch main, so they cannot trigger a major before we intend it.

Day-to-day

Change Target Result
fix: / feat: / perf: main auto-publishes 2.x, exactly as today
feat!: / BREAKING CHANGE: next publishes 3.0.0-next.N on the next tag
Ship the major PR nextmain cuts 3.0.0 stable

next can be ephemeral

semantic-release/lib/branches/expand.js intersects the configured branches with the branches that actually exist on the remote, so a configured-but-missing branch produces no entries rather than an error. The config can stay in .releaserc.json permanently while the branch only exists when a breaking change is in flight, then gets deleted after the major ships.

That matters here — deride has had 3 majors in its life (v0, v1, v2), so a permanently-maintained parallel branch would be idle almost all the time.

npm authentication is unaffected

The trusted publisher configured for #134 is pinned to org / repo / workflow filename / environment — not to a branch. Prereleases from next authenticate over the same OIDC path with no registry-side change.

Docs wiring

A Pages deploy replaces the whole site, and a build on one branch cannot see the other's docs. So main owns the deploy and pulls the prerelease docs at build time:

# docs.yml
on:
  push:
    branches: [main, next]
    paths: ['docs/**', '.github/workflows/docs.yml', 'package.json', 'pnpm-lock.yaml', 'brand/**']

steps:
  - uses: actions/checkout@v6
    with: { ref: main, fetch-depth: 0 }

  - name: Overlay in-development docs from the next branch
    run: |
      if git ls-remote --exit-code --heads origin next >/dev/null; then
        git fetch origin next --depth=1
        git checkout FETCH_HEAD -- docs/v3
      fi                                   # absent -> no prerelease section, cleanly

  - run: pnpm docs:build

config.ts would list whichever docs/v* directories exist, so the switcher gains and loses the prerelease entry automatically. hasNextVersion becomes a real signal instead of the always-true comparison it is today.

Suggested naming: use /v3/, not /next/. A version-pinned URL works from day one, survives the stable release without changing, and means /v2/ and /v3/ are both available throughout. Label it "v3 (prerelease)" in the switcher. /next/ could remain as a redirect to the current in-development major if a stable "show me unreleased" link is wanted.

Blocker — repo merge settings

The repository currently allows squash merges only:

allow_squash_merge: true
allow_merge_commit: false
allow_rebase_merge: false

Promoting nextmain by squash collapses every conventional commit into one, so semantic-release sees a single message and the release is wrong or no-ops.

CLAUDE.md already states:

Do NOT squash-merge release PRs. … Merge with "Create a merge commit" or "Rebase and merge" — preserves the individual conventional-commit messages.

That instruction is currently impossible to follow, because squash is the only method enabled. This is worth fixing regardless of whether this proposal is adopted.

Costs

  • Two branches to keep in sync; docs and README are the likeliest conflict sites.
  • A repo settings change (enable merge commits) that is not optional.
  • Contributors need to know which branch to target — requires a CONTRIBUTING / CLAUDE.md rule.
  • More CI minutes, and npm accumulates prerelease versions (normal, but visible on the versions page).
  • Roughly three workflow/config files plus the docs overlay logic.

Worth it if a major is something we want to develop and dogfood over days or weeks. Overkill if a major is a single PR merged the same afternoon.

Alternative — drop /next/ instead

If the answer is "majors are rare and land in one go", the honest move is to delete /next/ rather than fix it:

  • docs/v2/ — the v2 line, frozen when v3 ships
  • docs/v3/ — the live tree for the current major
  • docs/latest — symlink to the current major

No prerelease branch, no overlay, no release-time git writes. Version-pinned URLs still work. This is less machinery and removes the same class of bug, but gives up the ability to publish and dogfood an unreleased major.

Decisions needed

  1. Do we want a prerelease channel at all, or is "majors land in one PR" the accepted model? This is the fork in the road; everything else follows.
  2. Enable merge commits? Needed for this proposal, and needed for CLAUDE.md's existing guidance to be followable either way.
  3. /v3/ or /next/ as the prerelease URL.
  4. Does feat!: constrain stub<T> method names to keyof T #135 wait? Under this proposal it retargets to next and ships as 3.0.0-next.1 first. Under the alternative it merges to main and cuts v3.0.0 directly.

Verified vs still to verify

Verified while writing this:

  • expand.js drops configured branches that do not exist on the remote — so next can be ephemeral
  • the prerelease branch validator shape in lib/definitions/branches.js
  • repo merge settings are squash-only
  • the npm trusted publisher is pinned to workflow filename, not branch
  • the docs/nextdocs/v2 divergence is v2.2.0 fluent-chaining docs, not unreleased content

Not yet verified, and worth a throwaway branch before committing to this:

  • end-to-end dist-tag behaviour on a real prerelease publish
  • how semantic-release behaves when next is behind main
  • VitePress handling the overlaid directory alongside the latest symlink

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions