diff --git a/AGENTS.md b/AGENTS.md index 6ff5b2072..d636ebdce 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -53,6 +53,8 @@ Full conventions + what-not-to-do list: [docs/agents/conventions.md](docs/agents - **[docs/agents/repository-layout.md](docs/agents/repository-layout.md)** — full directory structure, project groupings, transition-shim strategy - **[docs/agents/release-and-versioning.md](docs/agents/release-and-versioning.md)** — branching, semver policy, PR-creation flow, release pipeline, NuGet gotchas +- **[docs/branching-and-release.md](docs/branching-and-release.md)** — maintainer runbook for cutting releases, hotfixing older majors, cutting new `release/vN` branches +- **[docs/adr/](docs/adr/)** — Architecture Decision Records (start with `0001-release-branch-model.md`) - **[docs/agents/conventions.md](docs/agents/conventions.md)** — conventions, what-not-to-do list, tool-wrapper recipe - **[docs/architecture.md](docs/architecture.md)** — high-level architecture overview - **[docs/rebrand-plan.md](docs/rebrand-plan.md)** — namespace mapping + bridge strategy diff --git a/CHANGELOG.md b/CHANGELOG.md index e9f009ee9..7c708f41b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,19 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - **Disabled auto-release on every push to `main`** (stopgap for milestone [#13](https://github.com/ChrisonSimtian/Fallout/milestone/13) / RFC [#267](https://github.com/ChrisonSimtian/Fallout/issues/267)). The `release` workflow now triggers on `workflow_dispatch` only — releases are explicit, run manually from Actions UI. Auto-publishing was firing a Fallout.* release on every merge (≈20 patch versions accumulated in recent days for what was mostly internal cleanup), generating Dependabot upgrade PRs across every downstream consumer. The proper restructure — tag-triggered publishes on `release/vN` branches with three GitHub Environments (`nuget-org` / `github-packages` / `github-releases`) — is being implemented under milestone #13. This stopgap stops the consumer-facing noise immediately while that lands. +- **Release-branch model + multi-channel CD (umbrella entry for milestone [#13](https://github.com/ChrisonSimtian/Fallout/milestone/13)).** Fallout now ships from `release/vN` branches via tag-triggered, multi-channel publish — not from `main`. Captures the full scope of the milestone in one entry to keep CHANGELOG history readable. Documented in [`docs/branching-and-release.md`](docs/branching-and-release.md) (maintainer runbook) and [`docs/adr/0001-release-branch-model.md`](docs/adr/0001-release-branch-model.md) (decision record). + - **Branching model.** `main` is the integration trunk — merges land here but no longer auto-publish. `release/vN` (e.g. `release/v11`, cut in [#269](https://github.com/ChrisonSimtian/Fallout/issues/269)) is the long-lived release channel per major version. Hotfixes flow `main → cherry-pick → release/vN → tag`. `develop`, `master`, `hotfix/*` not used. + - **Tag-triggered releases.** Pushing a `v*` tag to a `release/vN` branch fires `release.yml`. A `validate-ref` job confirms the tag is reachable from a `release/v*` branch. `workflow_dispatch` is preserved as a fallback for re-runs after transient publish failures ([#274](https://github.com/ChrisonSimtian/Fallout/issues/274)). + - **Multi-channel publish.** Three parallel jobs fan out from a shared Test+Pack: `publish-nuget-org` (Tier 1, approval-gated via the `nuget-org` GitHub Environment, Fallout.* only) → `publish-github-packages` (Tier 2, Nuke.* transition shims) → `publish-github-releases` (bundled nupkg artifacts on the tag's GitHub Release). Each produces a per-environment deployment record. `--skip-duplicate` on every push keeps re-runs idempotent. + - **GitHub Environments** ([#272](https://github.com/ChrisonSimtian/Fallout/issues/272)). Three env shells keyed by channel — `nuget-org` (required-reviewer: `@ChrisonSimtian`), `github-packages` (auto), `github-releases` (auto). Branch deployment policies restrict deploys to `release/v*` (`main` permitted transitionally during the cut). + - **`NUGET_API_KEY` env-scoped** ([#273](https://github.com/ChrisonSimtian/Fallout/issues/273)). Migrated from repo secret to environment secret on `nuget-org`. The release job declares `environment: nuget-org`, so the secret resolves only inside the gated job. + - **Branch protection on `release/v11`** ([#270](https://github.com/ChrisonSimtian/Fallout/issues/270)). Mirrors `main`'s profile — required `ubuntu-latest` check, linear history, CODEOWNER review, no force-push/delete, conversation resolution required. + - **Tag protection ruleset.** Repository ruleset blocks creation/deletion/update of `v*` tags except for repo admins. Two-layer gating with the `nuget-org` env approval: who-can-tag + who-can-approve-publish. + - **PR-validation triggers extended to `release/v*`** ([#291](https://github.com/ChrisonSimtian/Fallout/pull/291), [#292](https://github.com/ChrisonSimtian/Fallout/pull/292)). `ubuntu-latest.yml` and `ubuntu-latest-docs.yml` now fire on PRs targeting `release/v*` so branch protection's required check actually gets a result. Cross-platform post-merge triggers (`windows-latest`, `macos-latest`) tracked separately in [#293](https://github.com/ChrisonSimtian/Fallout/issues/293). + - **NB.GV `publicReleaseRefSpec`** ([#275](https://github.com/ChrisonSimtian/Fallout/issues/275)). Extended on `release/v11` to include the branch itself, so versions are clean `11.0.X` instead of `11.0.X-g`. + - **`main`'s `version.json` major** stays at 11 for now ([#271](https://github.com/ChrisonSimtian/Fallout/issues/271) deferred until v12 work begins). + - **Tier 3 Docker local NuGet server** ([#279](https://github.com/ChrisonSimtian/Fallout/issues/279)) — pre-merge testing channel — landed as a separate work item; initial setup in PR [#287](https://github.com/ChrisonSimtian/Fallout/pull/287). + ## [10.2.0] / 2026-05-21 The NUKE → Fallout hard fork. Originally NUKE by [@matkoch](https://github.com/matkoch) and contributors; under new maintenance and rebranded to Fallout. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 75442d049..d6e6d1ce3 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -8,7 +8,7 @@ Fallout welcomes contributions. As a community, we want to help each other, prov - Discuss non-trivial changes in an [issue](https://github.com/ChrisonSimtian/Fallout/issues) first. - Small fixes (typos, broken links, tool wrapper additions) can go straight to a PR against `main`. -- We're trunk-based: branch from `main`, open a PR against `main`. No `develop` or `release/*` branches. +- Branch from `main`, open a PR against `main`. The trunk integrates everything; **releases ship from `release/vN` branches** (see [Branching and release flow](docs/branching-and-release.md) for the full model). The only time you target a release branch directly is for a maintainer-driven hotfix cherry-pick — and even then, the fix lands on `main` first. ## Baseline contributions @@ -73,7 +73,7 @@ Tool wrapper JSON lives under `src/Fallout.Common/Tools//.json`. Whe ### After opening a PR -- The PR gate is `ubuntu-latest` only (docs-only PRs hit a noop workflow). `windows-latest` and `macos-latest` run post-merge on `main` as release validation. +- The PR gate is `ubuntu-latest` only — fires on PRs against `main` or any `release/vN` branch. Docs-only PRs hit a no-op shim workflow that reports the same status check name. `windows-latest` and `macos-latest` run post-merge on `main` for cross-platform validation (with `release/v*` push triggers tracked in [#293](https://github.com/ChrisonSimtian/Fallout/issues/293) as a parity follow-up). - Address review feedback in additional commits rather than force-pushing — easier to review the changes. - If CI fails on something unrelated to your change, ping a maintainer. @@ -86,3 +86,14 @@ The repo allows both **squash** and **rebase** merge buttons; plain merge commit - If you're using rebase, run `git rebase -i` to squash "address review feedback" / "fix typo" commits before requesting final approval — every commit landing on `main` is a bisect target. The merger (typically a CODEOWNER) picks the button; the PR description can request a preference but isn't binding. + +## Releases + +Merging to `main` doesn't publish anything — it's the integration trunk. Releases fire from `release/vN` branches via tag push, with a multi-channel publish fan-out (nuget.org, GitHub Packages, GitHub Releases). The full lifecycle is documented in [docs/branching-and-release.md](docs/branching-and-release.md): + +- How releases happen (tag a `release/vN` branch, three parallel publish jobs) +- The channel taxonomy (Tier 1 nuget.org, Tier 2 GitHub Packages, Tier 3 Docker local for pre-merge) +- Hotfix flow (cherry-pick from `main` to `release/vN`) +- When to cut a new `release/vN` + +Contributors don't usually need to do any of this — releases are maintainer-driven. But if you're filing a PR labelled `target/v` where N is older than the current major, expect the maintainer to cherry-pick it to `release/vN` after merging to `main`. diff --git a/docs/adr/0001-release-branch-model.md b/docs/adr/0001-release-branch-model.md new file mode 100644 index 000000000..3d9a80ad0 --- /dev/null +++ b/docs/adr/0001-release-branch-model.md @@ -0,0 +1,123 @@ +# ADR-0001: Release-branch model with tag-triggered multi-channel CD + +## Status + +Accepted (2026-05-29). Implementation shipped under [milestone #13](https://github.com/ChrisonSimtian/Fallout/milestone/13). + +## Context + +Pre-decision, Fallout's release pipeline auto-published on every merge to `main`. Nerdbank.GitVersioning bumped the patch via git-height, the tag fired, and `.github/workflows/release.yml` pushed to nuget.org. Two compounding problems surfaced: + +1. **Consumer-facing noise.** Every merge — including internal cleanup like the license-header strip ([#260](https://github.com/ChrisonSimtian/Fallout/pull/260)) — produced a Fallout.* release on nuget.org. Within a few days of v11-prep the patch counter climbed from `11.0.x` to `11.0.13+`. Each release fires a Dependabot upgrade PR in every downstream consumer repo. The fan-out cost to the userbase was disproportionate to the change content. + +2. **No hotfix path for older majors.** Once `main` advances to v12-prep territory, there's no clean way to ship a v11 patch for downstream consumers. The escape hatch would be reverting `main` to a v11 base, fixing, releasing, then reapplying v12 work — non-starter. + +Chris (project maintainer) called out the consumer pain explicitly: *"the pain is real, while OUR pain is just a bunch of tokens and then we're done"* — explicit guidance to favor maintainer cost over downstream cost when the two trade off. This shaped the timing of the cutover (immediate, not "after v11.0.0 ships from main") and the willingness to absorb migration overhead. + +## Decision + +Adopt a **release-branch model with tag-triggered, multi-channel CD**: + +### Branching + +- `main` = integration trunk. Every PR lands here. Merges to `main` do **not** publish. +- `release/vN` = release channel per major version. Tags pushed here fire the release pipeline. Cut from `main` at the point major N goes live. Long-lived; old release branches stay supportable. +- `release/vN.M` = optional minor-line branches, cut on demand. +- Hotfixes flow **main → cherry-pick → release/vN**, never direct-to-release except for declared emergencies. + +### Release trigger + +**Tag push on a `release/vN` branch.** The workflow's `validate-ref` job confirms the tag's commit is reachable from a `release/v*` branch and fails fast otherwise. A `workflow_dispatch` fallback with a `tag` input handles re-runs after transient publish failures (`--skip-duplicate` keeps re-runs idempotent). + +### Multi-channel fan-out via GitHub Environments + +Three environments keyed by **channel**, not by major (the major is captured in the deployment `ref`): + +| Environment | Tier | Audience | Gating | +|---|---|---|---| +| `nuget-org` | 1 — production | All consumers via nuget.org | Required-reviewer approval | +| `github-packages` | 2 — bleeding edge | Opt-in beta testers; Nuke.* transition shims | None | +| `github-releases` | (bundled) | Archival + manual download | None | + +Future Tier 3 (Docker local NuGet server for pre-merge testing) tracked in [#279](https://github.com/ChrisonSimtian/Fallout/issues/279). + +### Tag protection + +Repository ruleset blocks creation/deletion/update of `v*` tags except by repo admins. Combined with the `nuget-org` approval gate, this is two-layer gating: who-can-tag + who-can-approve-publish. + +### Versioning + +`Nerdbank.GitVersioning` is per-branch via `version.json`. `main` carries the next major in development; each `release/vN` keeps `"version": "N.0"`. Patch heights compute within each branch. `publicReleaseRefSpec` is extended on each release branch to include itself, so versions are git-sha-clean (`11.0.20`, not `11.0.20-g`). + +## Consequences + +### Positive + +- **Consumer noise gone.** No more 20-patch-per-week nuget.org bombardment from internal cleanup. Releases are explicit, deliberate, batched. +- **Older majors are supportable.** A v11 patch can ship from `release/v11` while `main` works on v12, without revert-then-replay gymnastics. +- **Two-layer release gating** (tag protection + env approval) makes accidental or unauthorized publishes correspondingly less likely. +- **Per-channel deployment records** on the GitHub Environments UI give a clean audit trail of what shipped where, when. +- **Channel taxonomy is encoded in infrastructure**, not just convention — wrong-channel publishes are blocked structurally. + +### Negative + +- **Cherry-pick overhead for hotfixes.** Maintainer mental model is "fix lands on main, then cherry-pick to release branch" instead of "fix where it belongs and ship." Mitigation: documented in [docs/branching-and-release.md](../branching-and-release.md), and the cherry-pick step is single-command. +- **Two-step merge for some changes.** A v11 hotfix takes two PRs (main + cherry-pick) instead of one. Cost is small per incident. +- **Workflow complexity.** `release.yml` went from one job to five jobs across three environments. More YAML, more triggers, more places a bug can hide. Smoke tests via `workflow_dispatch` mitigate. +- **CI workflow triggers had to grow.** Branch protection on `release/vN` requires `ubuntu-latest`, which forced `ubuntu-latest.yml` and `-docs.yml` to extend their `pull_request.branches` to include `release/v*`. A latent chicken-and-egg (no CI fires on a release-branch PR until those workflow updates land *on the release branch*) surfaced during initial setup and required a one-time admin-bypass merge to resolve. +- **`publicReleaseRefSpec` requires per-branch tuning.** Each new `release/vN` cut needs its `version.json` updated to include itself in the public-release ref spec, otherwise its versions are git-sha-suffixed. Documented as part of the "cutting a new `release/vN`" runbook. + +### Neutral + +- **CHANGELOG.md cadence unchanged.** PRs still add entries under `[Unreleased] — ` per the existing flow. The CHANGELOG-vs-GitHub-Releases reconciliation is a separate concern, tracked in [#263](https://github.com/ChrisonSimtian/Fallout/issues/263). +- **`Build.cs`'s `IPublish.Publish` target is now unused by CI** — the workflow calls `dotnet nuget push` and `gh release create` directly. The target stays available for local invocation. Consolidating or removing it is a possible future cleanup, not urgent. + +## Alternatives considered + +### A. Keep main-triggered, reduce noise via path filters + +Stick with the existing model and add aggressive `paths-ignore` rules to skip releases on doc-only or test-only changes. + +**Rejected because:** + +- Doesn't solve the hotfix problem. +- Path-based skip rules are brittle — easy to miss edge cases, and "internal cleanup" doesn't always map cleanly to file paths. +- A no-publish merge to main still bumps NB.GV's patch height, so the next "real" release jumps numbers, which is also confusing. + +### B. Release-per-merge to `release/vN` + +Merging to `release/v11` automatically publishes (mirror of the old `main → release` shape but scoped to a release branch). + +**Rejected because:** + +- Solves the hotfix problem but reintroduces the noise problem (now scoped to the release branch instead of main, but still per-merge). +- Loses the "explicit decision to ship" property that tag-driven gives. + +### C. `workflow_dispatch`-only releases as the permanent shape + +Stay on the stopgap. All releases are manual button clicks. + +**Rejected because:** + +- Loses the git-native tag-as-release-marker discoverability. `git tag --list` should tell you what shipped, not "look at the GitHub Actions history." +- Easier to lose track of which commits made it into which release. +- The tag-trigger approach is the same amount of work to maintain and reads more cleanly externally. + +### D. Defer this work to milestone #8 (v13 CD vision) + +Treat the noise as a v13 problem; ship v11 under the existing model. + +**Rejected** based on Chris's consumer-pain-first principle (see Context). The noise was actively hurting consumers *now*; deferring to v13 meant continued daily pain until then. + +## References + +- [Milestone #13](https://github.com/ChrisonSimtian/Fallout/milestone/13) — work-breakdown. +- [RFC #267](https://github.com/ChrisonSimtian/Fallout/issues/267) — design discussion. +- [docs/branching-and-release.md](../branching-and-release.md) — maintainer runbook for the model. +- [docs/agents/release-and-versioning.md](../agents/release-and-versioning.md) — PR-flow + release-pipeline reference. +- Related: [#262](https://github.com/ChrisonSimtian/Fallout/issues/262) (backwards-compat principle), [#263](https://github.com/ChrisonSimtian/Fallout/issues/263) (CHANGELOG vs GH Releases), [#279](https://github.com/ChrisonSimtian/Fallout/issues/279) (Tier 3 Docker). + +## Memory artifacts (AI agent context) + +- `project_release_channels.md` — channel taxonomy in agent memory. +- `feedback_consumer_pain_first.md` — the principle that drove the cutover timing. diff --git a/docs/branching-and-release.md b/docs/branching-and-release.md new file mode 100644 index 000000000..786480a2d --- /dev/null +++ b/docs/branching-and-release.md @@ -0,0 +1,162 @@ +# Branching and release flow + +Maintainer reference for how Fallout branches, ships releases, hotfixes older majors, and uses GitHub Environments to gate publishes. Driven by [milestone #13](https://github.com/ChrisonSimtian/Fallout/milestone/13) / [RFC #267](https://github.com/ChrisonSimtian/Fallout/issues/267). + +> **Audience.** Repository maintainers cutting releases or hotfixing older majors. Contributors filing PRs against `main` don't need to read this — see [CONTRIBUTING.md](../CONTRIBUTING.md) instead. AI coding tools should read both this file and [docs/agents/release-and-versioning.md](agents/release-and-versioning.md). + +## Branches at a glance + +| Branch | Purpose | Lifetime | Protected | Source of releases? | +|---|---|---|---|---| +| `main` | Integration trunk. Every PR lands here. | Long-lived | Yes | No (since milestone #13) | +| `release/vN` | Release channel for major version N (e.g. `release/v11`) | Long-lived per major | Yes | **Yes** — tags pushed here fire the release pipeline | +| `release/vN.M` | Optional minor channel for divergent patch lines | Cut on demand | Yes | Yes — same shape as `release/vN` | +| `feature/`, `bugfix/`, `chore/`, `docs/`, `pr/-` | Working branches | Short-lived; PR-and-merge then deleted | No | No | + +`develop`, `master`, `hotfix/*` are not used. Hotfixes flow through `main` first, then cherry-pick to `release/vN` — see the [hotfix flow](#hotfixing-an-older-major) below. + +## Channel taxonomy + +Releases fire to three independent channels, each with its own GitHub Environment: + +| Tier | Channel | Cadence | Gating | Versioning | +|---|---|---|---|---| +| **1** | `nuget-org` env → nuget.org | Slow, deliberate | **Approval-gated** (maintainer reviewer) | Clean semver (`11.0.0`, `11.0.1`, ...) | +| **2** | `github-packages` env → GitHub Packages | Faster — betas, previews | None | Prereleases or NB.GV builds | +| **3** | (planned) Docker local NuGet server | Per-PR / per-commit | None (ephemeral) | PR-derived | +| (bundled) | `github-releases` env → GitHub Release with nupkgs attached | Same tag as the package publish | None | Same as the tag | + +Tier 3 setup is tracked in [#279](https://github.com/ChrisonSimtian/Fallout/issues/279). See [`reference_release_channels` in agent memory](https://github.com/ChrisonSimtian/Fallout/issues/267#issuecomment-4570408325) for the design discussion. + +## Cutting a release + +The release pipeline fires on **tag push** to a `release/vN` branch. The tag must match `v*` and must point at a commit reachable from `release/v*` (the `validate-ref` job enforces this). + +### Step-by-step + +```bash +# 1. Make sure your local release/vN is up to date +git fetch +git switch release/v11 +git pull --ff-only + +# 2. (Optional) Verify what version NB.GV will compute +dotnet nbgv get-version # should report 11.0.X clean, no -g + +# 3. Create the tag + GitHub Release in one step +gh release create v11.0.X \ + --target release/v11 \ + --title "v11.0.X" \ + --generate-notes +``` + +`gh release create` publishes a tag + a GitHub Release in one call. That tag push triggers `.github/workflows/release.yml`, which: + +1. **`validate-ref`** confirms the tag points at a commit reachable from `release/v*`. +2. **`test-and-pack`** runs `dotnet fallout Test Pack`, uploads `output/packages/*.nupkg` as an artifact. +3. **Three parallel publish jobs** consume the artifact and fan out per channel: + - `publish-nuget-org` → pushes `Fallout.*.nupkg` to nuget.org (**pauses for your approval**) + - `publish-github-packages` → pushes `Nuke.*.nupkg` transition shims to GitHub Packages + - `publish-github-releases` → attaches all `*.nupkg` to the GitHub Release page + +### Approving the `nuget-org` publish + +When `publish-nuget-org` starts, it pauses for an environment-protection approval. You'll get a GitHub notification (and an entry on the run page). Click "Review deployments" → check `nuget-org` → "Approve and deploy". This is the production-grade gate — review what's about to publish before approving. + +### If a publish fails partway through + +Each `dotnet nuget push` uses `--skip-duplicate`, so re-running a publish job is idempotent on packages that already made it through. If nuget.org transient-fails on one of fifteen packages: + +```bash +# Re-run the workflow against the same tag +gh workflow run release.yml -f tag=v11.0.X +``` + +The `workflow_dispatch` fallback re-checks out the tag and re-runs all three publish jobs. `--skip-duplicate` skips anything that already published. + +## Hotfixing an older major + +Workflow: fix lands on `main` first via a normal PR, then cherry-pick to `release/vN`, then tag. + +```bash +# 1. Fix lands on main via standard PR flow +gh pr create --base main ... +# (review, merge to main) + +# 2. Cherry-pick to release/vN +git fetch +git switch release/v11 +git pull --ff-only +git cherry-pick + +# 3. Open a PR against release/v11 with the cherry-picked commit +# (yes, even a one-commit cherry-pick goes through a PR — branch protection +# blocks direct pushes and requires the ubuntu-latest status check) +git push origin HEAD:cherry-pick-XXXX-to-v11 +gh pr create --base release/v11 ... + +# 4. Once that PR merges, tag a new patch +gh release create v11.0.X+1 --target release/v11 --generate-notes +``` + +Cherry-pick-first guarantees forward compatibility: any fix in `release/v11` is also in `main` (and therefore in `release/v12` once cut). + +### When direct-PR-to-release-branch is OK + +The standard flow above is the default. In rare cases — security-incident fixes where main has diverged into incompatible v12 territory, or prod-down emergencies — a PR can target the release branch directly. Apply the `hotfix-direct` label and get explicit maintainer sign-off in the PR description. Not the default; not common. + +## Cutting a new `release/vN` + +When a new major version is about to ship from `main`: + +```bash +# 1. Make sure main is at the commit you want to start the new major from +git fetch +git switch main +git pull --ff-only + +# 2. Create the branch +git switch -c release/v12 main +git push -u origin release/v12 + +# 3. Apply branch protection (see docs/agents/release-and-versioning.md → Branch protection on release/vN +# for the canonical settings) +gh api -X PUT repos/ChrisonSimtian/Fallout/branches/release/v12/protection \ + --input scripts/release-branch-protection.json # mirror main's profile + +# 4. Add the new branch to publicReleaseRefSpec on release/v12 (the branch itself), +# so NB.GV produces clean versions instead of git-sha-suffixed ones: +# version.json publicReleaseRefSpec += "^refs/heads/release/v12$" +# Commit via PR targeting release/v12. + +# 5. (Optional, when v12 is fully on-trunk) bump main's version.json major to 13.0 +``` + +### Step 4 — why on `release/v12`, not `main` + +`publicReleaseRefSpec` is per-branch. Putting `^refs/heads/release/v12$` in `main`'s `publicReleaseRefSpec` while `main` still has `"version": "12.0"` would create a patch-height collision (both branches public-mapped to the same major+minor). Editing on `release/v12` only avoids that, and the edit naturally stays on the release branch where it belongs. + +When `main` later bumps to `13.0`, this concern goes away — `release/v12` keeps 12.0, `main` is 13.0, no overlap. + +## Deprecating an old `release/vN` + +Once a major hits end-of-life: + +1. Final patch release (`v..`). +2. Announce EoL in the README + CHANGELOG. +3. Leave the branch in place — don't delete it. Future archaeology + historical hotfix-on-demand should remain possible. +4. Optionally apply a more restrictive protection profile (e.g. require admin approval on every merge) to make accidental tags less likely. + +Branches are cheap. Deletion is destructive. Default to keeping. + +## Tag protection + +A repository ruleset blocks creation/deletion/update of tags matching `v*` for non-admins ([ruleset 17017817](https://github.com/ChrisonSimtian/Fallout/rules/17017817)). Bypass actors: repo admins (`RepositoryRole 5`). Combined with the `nuget-org` env approval gate, that's two layers of "who can fire a production release." + +## See also + +- [docs/agents/release-and-versioning.md](agents/release-and-versioning.md) — PR-creation flow, semver policy, release pipeline reference, branch protection settings. +- [docs/adr/0001-release-branch-model.md](adr/0001-release-branch-model.md) — Architecture Decision Record for the model documented here. +- [milestone #13](https://github.com/ChrisonSimtian/Fallout/milestone/13) — full work-breakdown of how this shape was implemented. +- [RFC #267](https://github.com/ChrisonSimtian/Fallout/issues/267) — original design discussion. +- [CONTRIBUTING.md](../CONTRIBUTING.md) — contributor-facing flow.