Repository navigation
RFC: Release-branch model & multi-channel CD (tag-triggered, GitHub Environments) #267
Description
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:
- Noise — every doc fix, refactor, and chore PR ships a
vN.0.Xrelease. The license-header strip (chore: strip per-file license headers; LICENSE is single source of truth #260) alone bumped11.0.12→11.0.13for zero consumer-facing change. - No hotfix path — once
mainmoves to v12-prep, there's no clean way to ship a v11 patch. The only escape hatch is revertingmainto 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 frommainwhen major N goes live; N.x.y patches release from this branch.release/vN.M— optional minor channel, cut fromrelease/vNwhen 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/v11only 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-notesmakes 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:
maincarries the next major in development (e.g.12.0once v11 ships).release/v11stays at11.0. Patch height is computed within the branch — cherry-picks add to the height.
When v12 ships:
- Tag the last v11 release from
release/v11. - Cut
release/v12frommain. - Bump
main'sversion.jsonto13.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 targetmain); 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/v11now; treat unreleased v11 work onmainas the v11 line until v12-prep.mainandrelease/v11stay roughly in sync until then. - 1b. Finish v11 first (release v11.0.0 from
mainunder the current model), cutrelease/v11after the tag, then flipmainto 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/v11at the agreed point - Refactor
.github/workflows/release.ymlfrompush: maintopush: tags: v*on release branches - Create GitHub Environments (
nuget-orgwith approval,github-packages,github-releases) - Wire
NUGET_API_KEYto thenuget-orgenvironment (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.mdand/or newdocs/branching-and-release.md - ADR capturing the decision and rationale
Out of scope
- Auto-generated release notes / CHANGELOG redesign — tracked in Reconcile CHANGELOG.md vs GitHub release notes (single source of truth?) #263; will integrate, not blocking.
- Consumer-facing
[GitHubActions]generator changes — separate follow-up. - LTS policy for the
Nuke.*transition shims — tracked in Add backwards-compatibility principle to AGENTS.md AI brief #262.
Discussion
Comment with feedback. The RFC label means consensus shapes the implementation — sub-issues land only once the design here is signed off.
Activity
ChrisonSimtian commentedon May 29, 2026
Decision on Open Question #1 — 1a (cut
release/v11now), not 1b.Per Chris: the noise is already hurting consumers — Dependabot fires a PR in every downstream repo on every patch, and the current "merge to main = release" loop has been firing 11.0.x patches for what amounts to internal cleanup (the license-header strip alone shipped at 11.0.13). For consumers that's a daily wall of upgrade PRs to review.
The "wait until v11.0.0 ships from main, then cut" plan optimised for our model-switch cleanliness. That's the wrong axis to optimise. Maintainer pain to migrate mid-major = bounded, one-shot, paid in tokens. Consumer pain from continued noise = recurring, fan-out across the userbase, paid in trust.
Concrete plan (1a)
release/v11from currentmainHEAD as the first concrete action — sub-issue to follow.push: main → publishtrigger in.github/workflows/release.ymlimmediately. Stopgap while the proper tag-triggered shape lands. This alone stops the bleeding.main'sversion.jsonto 12.0 so NB.GV stops computing 11.0.x heights on the trunk.release/v11'sversion.jsonstays at 11.0 — patch height computes within the branch.release/v11picks up where the auto-released11.0.13left off. Subsequent v11 work merges tomain→ cherry-picks torelease/v11→ tag.Updating the milestone's work breakdown to reflect this ordering.
Out-of-scope clarification
The unreleased v11 breaking work in CHANGELOG's
[Unreleased] — 11.0section is not abandoned by cuttingrelease/v11now — it continues to land onmainand gets cherry-picked torelease/v11(or batched asrelease/v11.1) as appropriate. The branching model doesn't change which code ships, only when and from where.Closing question #1.
ci: stopgap — switch release workflow to workflow_dispatch only (#267) (
ChrisonSimtian commentedon May 29, 2026
Sub-issues filed under milestone #13. Mapping to the work-breakdown checklist:
release/v11branch from current mainrelease/v11main'sversion.jsonto 12.0NUGET_API_KEYtonuget-orgenv secretrelease.ymlto tag-triggered + multi-channelCONTRIBUTING.md+/docsSuggested execution order:
Phase 1 (parallel-ish, blocks nothing on each other):
release/v11)Phase 2 (depend on phase 1):
release/v11) — after Cut release/v11 branch from current main #269Phase 3 (the big one):
release.yml)Phase 4 (descriptive — captures what shipped):
The RFC body's checklist still reflects the design intent; this comment is the execution tracker.
docs(agents): release-branch model + branch-protection policy for rel…
ChrisonSimtian commentedon May 29, 2026
Channel philosophy clarified by Chris — codifying so the RFC reflects intent:
Tiered channels
nuget-orgenv, Chris)11.0.0,11.0.1, ...)github-packagesenv)11.1.0-beta.1, etc.) or NB.GV buildsWhy this matters for the RFC
The earlier RFC body described GitHub Environments as "channels keyed by destination" without saying anything about cadence or role. This is the missing layer: the gating intensity matches the cadence and audience, not just "where the bits go."
Implications already reflected in landed work:
nuget-orggot the approval-required reviewer (Chris) — matches Tier 1 framing.github-packagesandgithub-releasesare unguarded — matches Tier 2 / bundled framing.Tier 3 (Docker local) — new sub-issue forthcoming
Filing a separate sub-issue under milestone #13 for the Docker-based local NuGet server. Future direction: this evolves into a devcontainer / Codespaces setup that ships preconfigured.
Memory: saving this as
project_release_channels.mdso future-me (and future AI agents on this repo) default to this taxonomy when discussing channels.ci: declare release job as deploying to nuget-org environment (#273) (#…
ci: tag-triggered + multi-channel fan-out for release.yml (#274) (#288)
docs: release-branch model — CONTRIBUTING + branching-and-release run…
ChrisonSimtian commentedon Jun 16, 2026
Closing as decided & implemented. The release-branch model this RFC proposed has shipped via ADR-0001 (release-branch model), ADR-0004 (calendar versioning & dual-pace channels), and ADR-0007 (cut release branch on demand); milestone #13 is otherwise complete. The v11/v12 framing here is superseded by the calendar-version ladder.