Skip to content

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

Closed

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.12 → 11.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.

Activity

  1. added
    documentationImprovements or additions to documentation
    enhancementNew feature or request
    RFCDesign discussion / RFC. Comment with feedback; consensus shapes the implementation.
    on May 29, 2026
  2. ChrisonSimtian commented on May 29, 2026

    @ChrisonSimtian
    CollaboratorAuthor

    Decision on Open Question #1 — 1a (cut release/v11 now), 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)

    1. Cut release/v11 from current main HEAD as the first concrete action — sub-issue to follow.
    2. Disable the current push: main → publish trigger in .github/workflows/release.yml immediately. Stopgap while the proper tag-triggered shape lands. This alone stops the bleeding.
    3. Bump main's version.json to 12.0 so NB.GV stops computing 11.0.x heights on the trunk.
    4. release/v11's version.json stays at 11.0 — patch height computes within the branch.
    5. First tagged release from release/v11 picks up where the auto-released 11.0.13 left off. Subsequent v11 work merges to main → cherry-picks to release/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.0 section is not abandoned by cutting release/v11 now — it continues to land on main and gets cherry-picked to release/v11 (or batched as release/v11.1) as appropriate. The branching model doesn't change which code ships, only when and from where.

    Closing question #1.

  3. ChrisonSimtian commented on May 29, 2026

    @ChrisonSimtian
    CollaboratorAuthor

    Sub-issues filed under milestone #13. Mapping to the work-breakdown checklist:

    # Sub-issue Depends on
    1 #269 — Cut release/v11 branch from current main Stopgap #268 (done ✓)
    2 #270 — Branch protection on release/v11 #269
    3 #271 — Bump main's version.json to 12.0 #269
    4 #272 — Create three GitHub Environments independent
    5 #273 — Move NUGET_API_KEY to nuget-org env secret #272
    6 #274 — Refactor release.yml to tag-triggered + multi-channel #269, #272, #273
    7 #275 — Validate NB.GV on release branches #269, #271
    8 #276 — Docs: CONTRIBUTING.md + /docs all above
    9 #277 — ADR: release-branch model implementation shipped

    Suggested execution order:

    Phase 1 (parallel-ish, blocks nothing on each other):

    Phase 2 (depend on phase 1):

    Phase 3 (the big one):

    Phase 4 (descriptive — captures what shipped):

    The RFC body's checklist still reflects the design intent; this comment is the execution tracker.

  4. ChrisonSimtian commented on May 29, 2026

    @ChrisonSimtian
    CollaboratorAuthor

    Channel philosophy clarified by Chris — codifying so the RFC reflects intent:

    Tiered channels

    Tier Channel Cadence Gating Versioning Audience
    1 (production) nuget.org Slow, deliberate Approval-gated (nuget-org env, Chris) Clean semver (11.0.0, 11.0.1, ...) Default for everyone — broadest reach, drives Dependabot
    2 (bleeding edge) GitHub Packages More frequent — betas, previews Auto (github-packages env) Prereleases (11.1.0-beta.1, etc.) or NB.GV builds Opt-in beta testers who add the GH Packages source
    3 (pre-merge) Docker local NuGet server Per-PR / per-commit None (ephemeral) PR-derived Contributors testing locally before merge
    (bundled) GitHub Releases With Tier 1 or 2 (bundled) Same tag as the package publish Archival, manual-download, Nuke.* transition shims

    Why 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-org got the approval-required reviewer (Chris) — matches Tier 1 framing.
    • github-packages and github-releases are 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.md so future-me (and future AI agents on this repo) default to this taxonomy when discussing channels.

  5. ChrisonSimtian commented on Jun 16, 2026

    @ChrisonSimtian
    CollaboratorAuthor

    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.

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

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

    Type

    No type

    Projects

    No projects

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions