Skip to content

RFC: Release-branch model & multi-channel CD (tag-triggered, GitHub Environments) #267

Description

@ChrisonSimtian

Design RFC for Milestone #13.

Problem

The CD pipeline auto-releases on every push to main: Nerdbank.GitVersioning bumps the patch, the tag fires, publish-to-NuGet runs. Two problems:

  1. Noise — every doc fix, refactor, and chore PR ships a vN.0.X release. The license-header strip (chore: strip per-file license headers; LICENSE is single source of truth #260) alone bumped 11.0.1211.0.13 for zero consumer-facing change.
  2. No hotfix path — once main moves to v12-prep, there's no clean way to ship a v11 patch. The only escape hatch is reverting main to a v11 base, fixing, releasing, reapplying v12 work — a non-starter.

Proposal

Branching model

  • main — integration trunk. All PRs merge here. Merging does not publish to public channels.
  • release/vN — release channel for major N (e.g. release/v11). Cut from main when major N goes live; N.x.y patches release from this branch.
  • release/vN.M — optional minor channel, cut from release/vN when a minor line needs independent patching. Deferred until demand-driven.
  • Old release branches stay alive as long as we support that major, then get archived (not deleted).

Release trigger

Tag-triggered, on a release branch. Pushing a vX.Y.Z tag at a commit in release/vX (or release/vX.Y) fires the publish workflow.

  • Merging to release/v11 only stages commits; nothing publishes.
  • The maintainer cuts the tag when ready — this is the quality gate.
  • gh release create vX.Y.Z --target release/vX --generate-notes makes the tag and release in one step.

Hotfix flow

Always cherry-pick from main. Fix lands on main first via normal PR, then:

git fetch
git switch release/v11
git cherry-pick <fix-sha>
git push
gh release create v11.0.14 --target release/v11 --generate-notes

Forward-compat is guaranteed: any fix in release/v11 is also in main (and in release/v12 once cut). Exceptions (security incident, prod-down with main on a divergent v12 architecture) need a hotfix-direct label and maintainer sign-off — not the default.

Multi-channel CD via GitHub Environments

Three environments keyed by channel, not by major:

Environment Target Approval gate?
nuget-org https://api.nuget.org/v3/index.json Yes — irreversible publish
github-packages https://nuget.pkg.github.com/ChrisonSimtian/index.json No
github-releases GitHub Release with built nupkgs attached No

One tag-push workflow → three deployment records (one per environment) → three publish steps. Per-major granularity lives in the deployment ref (the tag), so no v11-nuget-org / v12-nuget-org fan-out. Failed publishes retry independently per environment.

Versioning (Nerdbank.GitVersioning)

version.json lives on each branch:

  • main carries the next major in development (e.g. 12.0 once v11 ships).
  • release/v11 stays at 11.0. Patch height is computed within the branch — cherry-picks add to the height.

When v12 ships:

  1. Tag the last v11 release from release/v11.
  2. Cut release/v12 from main.
  3. Bump main's version.json to 13.0.

This is the .NET runtime's pattern (release/8.0, release/9.0, main = next).

Documentation deliverables

  • CONTRIBUTING.md — contributor flow: where to branch from, where PRs merge, how releases happen.
  • docs/agents/release-and-versioning.md — extend with the new model. PR-creation flow stays (PRs still target main); release-pipeline section gets rewritten.
  • New docs/branching-and-release.md (or absorbed above) — full maintainer reference for cutting releases.

Open implementation questions

1. When do we cut release/v11? Chris confirmed: now, off current main. But main has unreleased v11 breaking-change work in flight (the [Unreleased] — 11.0 CHANGELOG section is substantial):

  • 1a. Cut release/v11 now; treat unreleased v11 work on main as the v11 line until v12-prep. main and release/v11 stay roughly in sync until then.
  • 1b. Finish v11 first (release v11.0.0 from main under the current model), cut release/v11 after the tag, then flip main to v12-prep.

Leaning 1b — avoids mid-major model switches; gives one clean "v11.0.0 ships from main; v11.0.1+ ships from release/v11" transition.

2. Branch protection on release/vN — required: linear history, status checks (ubuntu-latest), CODEOWNER review. Forbidden: direct pushes by anyone but the release tagger (or restrict tag creation via tag protection). Cherry-picks land via PR, not direct push.

3. gh release --generate-notes template — move to auto-generated notes from PR titles + labels. Needs .github/release.yml (don't have it yet — see #263). Resolving #263 unblocks part of this.

4. [GitHubActions] generator implications — should the consumer-facing generator output reflect a similar release-branch model? Out of scope here; file a follow-up.

5. Backfill / migration — existing 11.0.x tags stay as-is. The new model takes effect from the first tag after cut. No retroactive renaming.

Work breakdown (filed as sub-issues once RFC settles)

  • Branching policy + branch protection on release/vN (settings + docs)
  • Cut release/v11 at the agreed point
  • Refactor .github/workflows/release.yml from push: main to push: tags: v* on release branches
  • Create GitHub Environments (nuget-org with approval, github-packages, github-releases)
  • Wire NUGET_API_KEY to the nuget-org environment (currently a repo secret)
  • Validate Nerdbank.GitVersioning on release branches (commit-height boundary at branch point)
  • Document in CONTRIBUTING.md
  • Document in docs/agents/release-and-versioning.md and/or new docs/branching-and-release.md
  • ADR capturing the decision and rationale

Out of scope

Discussion

Comment with feedback. The RFC label means consensus shapes the implementation — sub-issues land only once the design here is signed off.

Metadata

Metadata

Assignees

No one assigned

    Labels

    RFCDesign discussion / RFC. Comment with feedback; consensus shapes the implementation.documentationImprovements or additions to documentationenhancementNew feature or request

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions