Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<sha>`.
- **`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.

Expand Down
15 changes: 13 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -73,7 +73,7 @@ Tool wrapper JSON lives under `src/Fallout.Common/Tools/<Tool>/<Tool>.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.

Expand All @@ -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<N>` where N is older than the current major, expect the maintainer to cherry-pick it to `release/vN` after merging to `main`.
123 changes: 123 additions & 0 deletions docs/adr/0001-release-branch-model.md
Original file line number Diff line number Diff line change
@@ -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<sha>`).

## 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] — <next-major>` 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.
Loading