From e56bbce127f8c83d5f1e8fb66393e6fd0f25d4d3 Mon Sep 17 00:00:00 2001
From: Chrison Simtian
Date: Sat, 30 May 2026 15:02:05 +1200
Subject: [PATCH 1/7] ci: realign triggers to the 3-tier ladder + hygiene
(milestone #18) (#330)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Implements the trigger/hygiene slice of milestone #18 (CI cost & pipeline
structure). #325 (publish-lane realignment) already landed in the ladder PR.
- #318/#326 Cross-platform gated to release intent. windows/macos no longer run
on main/experimental pushes (or any routine push). They run only on PR-to-
release/* or support/*, and on v* tag pushes. ("On main we've got our edge":
the ubuntu-latest PR gate + alpha/preview pipelines.) workflow_dispatch is not
emitted — the generator only writes it with inputs; GitHub's run re-run covers
on-demand cross-platform.
- #322 concurrency cancel-in-progress on ubuntu/windows/macos (generator) +
experimental.yml + preview.yml. NOT on release.yml (never cancel a publish).
- #323/#328 Canonical CI-ignore list (docs/**, .assets/**, **/*.md) on every
PR/push trigger. (release.yml is tag-triggered, so path-ignore is N/A there.)
- #327 Codified "feature branches run zero CI until PR'd" + the trigger model in
docs/agents/conventions.md, with what-not-to-do guards.
- #329 Dropped dead 'submodules: recursive' from all checkouts + the generator
(no .gitmodules; full build passes without it) and the stale vendor comment.
Generated workflows regenerated from build/Build.CI.GitHubActions.cs.
Deferred (own follow-ups): #324 split Build/Test/Pack stages; #328 caching
deep-dive; #327 automated reflective guard-test (docs guard in place now).
Also unblocked publishing separately: the github-packages environment
deployment policy now allows experimental/main/release/*/support/* + v* tags.
Co-authored-by: Claude Opus 4.8 (1M context)
---
.github/workflows/experimental.yml | 6 +++-
.github/workflows/macos-latest.yml | 14 +++++++--
.github/workflows/preview.yml | 6 +++-
.github/workflows/release.yml | 1 -
.github/workflows/ubuntu-latest.yml | 5 +++-
.github/workflows/windows-latest.yml | 14 +++++++--
build/Build.CI.GitHubActions.cs | 44 ++++++++++++++++++----------
docs/agents/conventions.md | 14 +++++++++
8 files changed, 79 insertions(+), 25 deletions(-)
diff --git a/.github/workflows/experimental.yml b/.github/workflows/experimental.yml
index 76faad6d7..f84d89f5b 100644
--- a/.github/workflows/experimental.yml
+++ b/.github/workflows/experimental.yml
@@ -28,6 +28,11 @@ on:
- '.assets/**'
- '**/*.md'
+# Cancel a superseded alpha build when a newer commit lands (#322). Never on release.yml.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
permissions:
contents: read
@@ -44,7 +49,6 @@ jobs:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # Nerdbank.GitVersioning needs full history
- submodules: recursive
- name: 'Cache: .fallout/temp, ~/.nuget/packages'
uses: actions/cache@v4
with:
diff --git a/.github/workflows/macos-latest.yml b/.github/workflows/macos-latest.yml
index b17aacacd..426d42ffa 100644
--- a/.github/workflows/macos-latest.yml
+++ b/.github/workflows/macos-latest.yml
@@ -18,11 +18,20 @@ name: macos-latest
on:
push:
+ tags:
+ - 'v*'
+ pull_request:
branches:
- - experimental
- - main
- 'release/*'
- 'support/*'
+ paths-ignore:
+ - 'docs/**'
+ - '.assets/**'
+ - '**/*.md'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
macos-latest:
@@ -31,7 +40,6 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
- submodules: recursive
fetch-depth: 0
- name: 'Cache: .fallout/temp, ~/.nuget/packages'
uses: actions/cache@v4
diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml
index 5b90bedfa..ec62ed2e3 100644
--- a/.github/workflows/preview.yml
+++ b/.github/workflows/preview.yml
@@ -28,6 +28,11 @@ on:
- '.assets/**'
- '**/*.md'
+# Cancel a superseded preview build when a newer commit lands (#322). Never on release.yml.
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
permissions:
contents: read
@@ -44,7 +49,6 @@ jobs:
- uses: actions/checkout@v6
with:
fetch-depth: 0 # Nerdbank.GitVersioning needs full history
- submodules: recursive
- name: 'Cache: .fallout/temp, ~/.nuget/packages'
uses: actions/cache@v4
with:
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index d7820252e..9af4be383 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -94,7 +94,6 @@ jobs:
with:
ref: ${{ inputs.tag || github.ref }}
fetch-depth: 0 # Nerdbank.GitVersioning needs full history
- submodules: recursive # vendor/vs-solutionpersistence
- name: 'Cache: .fallout/temp, ~/.nuget/packages'
uses: actions/cache@v4
with:
diff --git a/.github/workflows/ubuntu-latest.yml b/.github/workflows/ubuntu-latest.yml
index e1e942332..45f548d6e 100644
--- a/.github/workflows/ubuntu-latest.yml
+++ b/.github/workflows/ubuntu-latest.yml
@@ -28,6 +28,10 @@ on:
- '.assets/**'
- '**/*.md'
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
+
jobs:
ubuntu-latest:
name: ubuntu-latest
@@ -35,7 +39,6 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
- submodules: recursive
fetch-depth: 0
repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}
ref: ${{ github.head_ref }}
diff --git a/.github/workflows/windows-latest.yml b/.github/workflows/windows-latest.yml
index abfd9026f..bc97f4c73 100644
--- a/.github/workflows/windows-latest.yml
+++ b/.github/workflows/windows-latest.yml
@@ -18,11 +18,20 @@ name: windows-latest
on:
push:
+ tags:
+ - 'v*'
+ pull_request:
branches:
- - experimental
- - main
- 'release/*'
- 'support/*'
+ paths-ignore:
+ - 'docs/**'
+ - '.assets/**'
+ - '**/*.md'
+
+concurrency:
+ group: ${{ github.workflow }}-${{ github.ref }}
+ cancel-in-progress: true
jobs:
windows-latest:
@@ -31,7 +40,6 @@ jobs:
steps:
- uses: actions/checkout@v6
with:
- submodules: recursive
fetch-depth: 0
- name: 'Cache: .fallout/temp, ~/.nuget/packages'
uses: actions/cache@v4
diff --git a/build/Build.CI.GitHubActions.cs b/build/Build.CI.GitHubActions.cs
index 835e50ea9..6a272c419 100644
--- a/build/Build.CI.GitHubActions.cs
+++ b/build/Build.CI.GitHubActions.cs
@@ -1,37 +1,51 @@
using Fallout.Common.CI.GitHubActions;
using Fallout.Components;
-// macOS and Windows runs are reserved for post-merge validation on the
-// long-lived branches (experimental, main, release/YYYY, support/*). PRs and
-// feature-branch pushes get Linux-only for fast, cheap feedback. Cross-platform
-// regressions on those branches surface as a red commit — same fail-fast model.
+// Cross-platform (macOS/Windows) full Test+Pack is gated to RELEASE INTENT
+// (#318/#326): it runs only on a PR into a production branch (release/YYYY,
+// support/*) and on a release tag push (v*) — never on routine pushes to
+// main/experimental, never on a per-merge basis. On main/experimental "we've
+// got our edge": the ubuntu-latest PR gate + the alpha/preview pipelines.
+// (workflow_dispatch as a manual cross-platform trigger isn't emitted here —
+// the generator only writes workflow_dispatch when it has inputs; GitHub's
+// built-in run re-run covers the on-demand case.)
+//
+// concurrency cancel-in-progress (#322): superseded runs are cancelled rather
+// than stacked. Never applied to release.yml (a publish must not be cancelled).
[GitHubActions(
"macos-latest",
GitHubActionsImage.MacOsLatest,
FetchDepth = 0,
- Submodules = GitHubActionsSubmodules.Recursive,
- OnPushBranches = new[] { ExperimentalBranch, MainBranch, ReleaseBranchPattern, SupportBranchPattern },
+ ConcurrencyGroup = "${{ github.workflow }}-${{ github.ref }}",
+ ConcurrencyCancelInProgress = true,
+ OnPushTags = new[] { "v*" },
+ OnPullRequestBranches = new[] { ReleaseBranchPattern, SupportBranchPattern },
+ OnPullRequestExcludePaths = new[] { "docs/**", ".assets/**", "**/*.md" },
InvokedTargets = new[] { nameof(ITest.Test), nameof(IPack.Pack) },
PublishArtifacts = false)]
[GitHubActions(
"windows-latest",
GitHubActionsImage.WindowsLatest,
FetchDepth = 0,
- Submodules = GitHubActionsSubmodules.Recursive,
- OnPushBranches = new[] { ExperimentalBranch, MainBranch, ReleaseBranchPattern, SupportBranchPattern },
+ ConcurrencyGroup = "${{ github.workflow }}-${{ github.ref }}",
+ ConcurrencyCancelInProgress = true,
+ OnPushTags = new[] { "v*" },
+ OnPullRequestBranches = new[] { ReleaseBranchPattern, SupportBranchPattern },
+ OnPullRequestExcludePaths = new[] { "docs/**", ".assets/**", "**/*.md" },
InvokedTargets = new[] { nameof(ITest.Test), nameof(IPack.Pack) },
PublishArtifacts = false)]
-// pull_request only — same-repo branches would otherwise fire both push and
-// pull_request events on every push, double-running the validation.
-//
-// CheckoutRef = github.head_ref pins checkout to the PR source branch instead of the merge SHA,
-// keeping HEAD attached so GitHubTasksTest.GitHubRepositoryFromLocalDirectoryTest (which reads
-// .git/HEAD via GitRepository.FromLocalDirectory) resolves a non-null branch.
+// The Linux PR gate — the only required status check. pull_request only:
+// feature-branch pushes run zero CI until a PR is opened against a long-lived
+// branch (#327). CheckoutRef = github.head_ref pins checkout to the PR source
+// branch instead of the merge SHA, keeping HEAD attached so
+// GitHubTasksTest.GitHubRepositoryFromLocalDirectoryTest (which reads .git/HEAD
+// via GitRepository.FromLocalDirectory) resolves a non-null branch.
[GitHubActions(
"ubuntu-latest",
GitHubActionsImage.UbuntuLatest,
FetchDepth = 0,
- Submodules = GitHubActionsSubmodules.Recursive,
+ ConcurrencyGroup = "${{ github.workflow }}-${{ github.ref }}",
+ ConcurrencyCancelInProgress = true,
CheckoutRef = "${{ github.head_ref }}",
// Trigger for PRs targeting experimental, main, or any release/YYYY / support/*
// branch — all are long-lived and protected; all require the ubuntu-latest check.
diff --git a/docs/agents/conventions.md b/docs/agents/conventions.md
index b85d0e49f..6d591bdde 100644
--- a/docs/agents/conventions.md
+++ b/docs/agents/conventions.md
@@ -34,9 +34,23 @@ public sealed class NewPluginHost
- **Channel discipline differs.** On the `experimental` (alpha) / `main` (preview) test lanes, churn is expected and the attribute is a courtesy. On a `release/YYYY` **production line**, any risky-but-shipped public surface **must** wear `[Experimental]` — that contract is what keeps the stable line trustworthy while still carrying new work.
- **Don't apply it speculatively.** Because the diagnostic is error-by-default, marking an API that's already used internally breaks the build everywhere it's referenced. Only add `[Experimental]` to a genuinely not-yet-stable API, and suppress every internal usage in the same change so the build stays green.
+## CI pipeline & triggers
+
+Shaped by [milestone #18](https://github.com/ChrisonSimtian/Fallout/milestone/18) and the [ADR-0004](../adr/0004-calendar-versioning-and-dual-pace-channels.md) ladder. Invariants:
+
+- **Feature branches run zero CI until a PR is opened.** Push triggers list **only** long-lived branches; nothing fires on `feature/*`, `bugfix/*`, etc. until they're PR'd against `experimental`/`main`/`release/*`/`support/*`. Do **not** add a working-branch pattern to any `OnPush*`/`branches:` trigger.
+- **The Linux PR gate (`ubuntu-latest`) is the only required check** — runs on PRs to the four long-lived branches.
+- **`experimental` (push) → `-alpha`, `main` (push) → `-preview`** to GitHub Packages (`experimental.yml` / `preview.yml`).
+- **Cross-platform `windows`/`macos` are gated to release intent** — PR-to-`release/*`/`support/*` or a `v*` tag push only. They do **not** run on `main`/`experimental` pushes. ("On `main` we've got our edge.")
+- **`concurrency: cancel-in-progress` on every build workflow except `release.yml`** — never cancel a publish mid-flight.
+- **Canonical CI-ignore paths:** `docs/**`, `.assets/**`, `**/*.md` — applied to every PR/push trigger.
+- The `ubuntu-latest` / `windows-latest` / `macos-latest` workflows are **generated** from `build/Build.CI.GitHubActions.cs` — edit the attributes + constants there and regenerate (`./build.sh`), never hand-edit the `.yml`. `experimental.yml` / `preview.yml` / `release.yml` are hand-written.
+
## What not to do
- Don't reintroduce `source/` — production code lives under `src/`, tests under `tests/`. Same for `images/` (now `.assets/`).
+- Don't add `main`/`experimental` (or any working-branch pattern) to the **push** triggers of the cross-platform workflows — they're release-intent-gated on purpose (milestone #18 / #318 / #326).
+- Don't add `submodules: recursive` to checkouts — there are no submodules (no `.gitmodules`); it's a dead init step.
- Don't add `secret`/`default` defaults to tool JSON files (see CONTRIBUTING.md).
- Don't introduce a new test framework or assertion library — stay on xUnit + FluentAssertions + Verify.
- Don't commit `output/` or any `bin/`/`obj/` directory.
From 5ee0600a99f4d7f22174e152943f165233873c02 Mon Sep 17 00:00:00 2001
From: Chrison Simtian
Date: Sat, 30 May 2026 15:20:54 +1200
Subject: [PATCH 2/7] ci: test before publishing on every lane + cache
restore-keys (#324, #328) (#331)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
#324 — experimental.yml and preview.yml ran `dotnet fallout Pack` only, publishing
alpha/preview packages WITHOUT running tests. Both now run `dotnet fallout Test Pack`
(release.yml + the PR gate already did). One invocation = NUKE's discrete internal
stages (Restore → Compile → Test → Pack), failing at the breaking stage; a test
failure stops the job before the push step, so untested packages never publish.
Separate per-step `dotnet fallout` invocations are avoided on purpose — each re-runs
the dependency graph (double-compile); the single invocation is the staged build.
#328 — added `restore-keys:` prefix fallback to the hand-written workflows' caches
for faster partial restores on key miss. Evaluation: current key (global.json +
*.csproj + Directory.Packages.props) is the right dependency set; no packages.lock.json
exists to add; build-output (bin/obj) caching deliberately not done (stale-artifact
risk). Canonical ignore list (docs/.assets/md) already applied in the trigger PR.
Codified both in docs/agents/conventions.md.
Deferred: #327 automated reflective guard-test (docs guard already in place).
Co-authored-by: Claude Opus 4.8 (1M context)
---
.github/workflows/experimental.yml | 11 +++++++++--
.github/workflows/preview.yml | 11 +++++++++--
.github/workflows/release.yml | 2 ++
docs/agents/conventions.md | 2 ++
4 files changed, 22 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/experimental.yml b/.github/workflows/experimental.yml
index f84d89f5b..ad9ca7fee 100644
--- a/.github/workflows/experimental.yml
+++ b/.github/workflows/experimental.yml
@@ -56,14 +56,21 @@ jobs:
.fallout/temp
~/.nuget/packages
key: ${{ runner.os }}-experimental-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props', 'version.json') }}
+ restore-keys: |
+ ${{ runner.os }}-experimental-
- name: 'Setup: .NET SDK'
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: 'Restore: dotnet tools'
run: dotnet tool restore
- - name: 'Run: Pack'
- run: dotnet fallout Pack
+ # build + unit-test before publishing alpha (#324). One invocation: NUKE runs
+ # this as discrete internal stages (Restore → Compile → Test → Pack) and fails
+ # at the breaking stage — separate `dotnet fallout` steps would re-run the
+ # dependency graph (double-compile), so we keep it a single invocation.
+ # If Test fails, the job stops here and nothing is published.
+ - name: 'Run: Test + Pack'
+ run: dotnet fallout Test Pack
- name: 'Push: all *.nupkg to GitHub Packages (alpha)'
run: |
set -euo pipefail
diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml
index ec62ed2e3..f3d34cc1a 100644
--- a/.github/workflows/preview.yml
+++ b/.github/workflows/preview.yml
@@ -56,14 +56,21 @@ jobs:
.fallout/temp
~/.nuget/packages
key: ${{ runner.os }}-preview-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props', 'version.json') }}
+ restore-keys: |
+ ${{ runner.os }}-preview-
- name: 'Setup: .NET SDK'
uses: actions/setup-dotnet@v4
with:
global-json-file: global.json
- name: 'Restore: dotnet tools'
run: dotnet tool restore
- - name: 'Run: Pack'
- run: dotnet fallout Pack
+ # build + unit-test before publishing preview (#324). One invocation: NUKE runs
+ # this as discrete internal stages (Restore → Compile → Test → Pack) and fails
+ # at the breaking stage — separate `dotnet fallout` steps would re-run the
+ # dependency graph (double-compile), so we keep it a single invocation.
+ # If Test fails, the job stops here and nothing is published.
+ - name: 'Run: Test + Pack'
+ run: dotnet fallout Test Pack
- name: 'Push: all *.nupkg to GitHub Packages (preview)'
run: |
set -euo pipefail
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9af4be383..ce115724e 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -101,6 +101,8 @@ jobs:
.fallout/temp
~/.nuget/packages
key: ${{ runner.os }}-release-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props', 'version.json') }}
+ restore-keys: |
+ ${{ runner.os }}-release-
- name: 'Setup: .NET SDK'
uses: actions/setup-dotnet@v4
with:
diff --git a/docs/agents/conventions.md b/docs/agents/conventions.md
index 6d591bdde..555ce8125 100644
--- a/docs/agents/conventions.md
+++ b/docs/agents/conventions.md
@@ -45,6 +45,8 @@ Shaped by [milestone #18](https://github.com/ChrisonSimtian/Fallout/milestone/18
- **`concurrency: cancel-in-progress` on every build workflow except `release.yml`** — never cancel a publish mid-flight.
- **Canonical CI-ignore paths:** `docs/**`, `.assets/**`, `**/*.md` — applied to every PR/push trigger.
- The `ubuntu-latest` / `windows-latest` / `macos-latest` workflows are **generated** from `build/Build.CI.GitHubActions.cs` — edit the attributes + constants there and regenerate (`./build.sh`), never hand-edit the `.yml`. `experimental.yml` / `preview.yml` / `release.yml` are hand-written.
+- **Every publishing lane runs `Test` before it publishes** (#324). `experimental.yml`, `preview.yml`, and `release.yml` all run a single `dotnet fallout Test Pack` invocation — NUKE executes it as discrete internal stages (Restore → Compile → Test → Pack) and fails at the breaking stage, so a test failure stops the job before the push step. Don't split a lane into separate `dotnet fallout Compile`/`Test`/`Pack` steps — each invocation re-runs the dependency graph (double-compile); the single invocation *is* the staged build.
+- **Caching** (#328): every workflow caches `~/.nuget/packages` + `.fallout/temp`, keyed on `global.json` + `**/*.csproj` + `Directory.Packages.props` (the dependency-affecting set), with a `restore-keys:` prefix fallback for partial restores. There is no `packages.lock.json` to add to the key, and build outputs (`bin`/`obj`) are deliberately **not** cached (stale-artifact correctness risk).
## What not to do
From 58f1ca94e17af0265c0db9611feff1f7b3069081 Mon Sep 17 00:00:00 2001
From: Chrison Simtian
Date: Sun, 31 May 2026 23:37:16 +1200
Subject: [PATCH 3/7] chore: repoint to Fallout-build org after repo transfer
(main) (#345)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
Repo moved ChrisonSimtian/Fallout → Fallout-build/Fallout. Owner-scoped refs that
don't auto-redirect, fixed on main:
- GitHub Packages feed URL nuget.pkg.github.com/ChrisonSimtian → /Fallout-build
(release.yml, preview.yml, experimental.yml, + consumer docs: shim READMEs,
from-nuke.md, release-and-versioning.md, CHANGELOG mention)
- CODEOWNERS @ChrisonSimtian → @Fallout-build/maintainers (team now has Maintain access)
Feed stays PAT-gated (GitHub Packages has no anonymous access even when public; #344).
Web links / badges / issue refs redirect automatically and are left for a later sweep.
Co-authored-by: Claude Opus 4.8 (1M context)
---
.github/CODEOWNERS | 4 ++--
.github/workflows/experimental.yml | 2 +-
.github/workflows/preview.yml | 2 +-
.github/workflows/release.yml | 2 +-
CHANGELOG.md | 2 +-
docs/agents/release-and-versioning.md | 2 +-
docs/migration/from-nuke.md | 2 +-
src/Shims/Nuke.Build/README.md | 2 +-
src/Shims/Nuke.Common/README.md | 4 ++--
src/Shims/Nuke.Components/README.md | 2 +-
10 files changed, 12 insertions(+), 12 deletions(-)
diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS
index 3b08bb53b..713f0c064 100644
--- a/.github/CODEOWNERS
+++ b/.github/CODEOWNERS
@@ -8,5 +8,5 @@
# changelogs, etc.) have no code owner and are gated only by the required CI
# status check.
-/src/** @ChrisonSimtian
-/tests/** @ChrisonSimtian
+/src/** @Fallout-build/maintainers
+/tests/** @Fallout-build/maintainers
diff --git a/.github/workflows/experimental.yml b/.github/workflows/experimental.yml
index ad9ca7fee..f1009949a 100644
--- a/.github/workflows/experimental.yml
+++ b/.github/workflows/experimental.yml
@@ -83,7 +83,7 @@ jobs:
for pkg in "${packages[@]}"; do
echo "Pushing $pkg to GitHub Packages (alpha)..."
dotnet nuget push "$pkg" \
- --source "https://nuget.pkg.github.com/ChrisonSimtian/index.json" \
+ --source "https://nuget.pkg.github.com/Fallout-build/index.json" \
--api-key "${{ secrets.GITHUB_TOKEN }}" \
--skip-duplicate
done
diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml
index f3d34cc1a..98f84874f 100644
--- a/.github/workflows/preview.yml
+++ b/.github/workflows/preview.yml
@@ -83,7 +83,7 @@ jobs:
for pkg in "${packages[@]}"; do
echo "Pushing $pkg to GitHub Packages (preview)..."
dotnet nuget push "$pkg" \
- --source "https://nuget.pkg.github.com/ChrisonSimtian/index.json" \
+ --source "https://nuget.pkg.github.com/Fallout-build/index.json" \
--api-key "${{ secrets.GITHUB_TOKEN }}" \
--skip-duplicate
done
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index ce115724e..10e1e1e4d 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -199,7 +199,7 @@ jobs:
for pkg in "${packages[@]}"; do
echo "Pushing $pkg to GitHub Packages..."
dotnet nuget push "$pkg" \
- --source "https://nuget.pkg.github.com/ChrisonSimtian/index.json" \
+ --source "https://nuget.pkg.github.com/Fallout-build/index.json" \
--api-key "${{ secrets.GITHUB_TOKEN }}" \
--skip-duplicate
done
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 8ac51bd0f..49a482d1a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -123,7 +123,7 @@ The NUKE → Fallout hard fork. Originally NUKE by [@matkoch](https://github.com
### Backwards-compat: Nuke.* transition shims
- **`Nuke.Common` MVP shim package** (#70): thin wrapper assembly in the `Nuke.*` namespace whose types inherit from the corresponding `Fallout.*` types — lets pre-rename consumers compile against the new packages without source changes. Lives at `src/Shims/Nuke.Common/`.
- **`TransitionShimGenerator` source generator** (#69): emits shim type wrappers per-target, covering the Nuke.Common "Easy tier" surface (plain types, interfaces, enums) and static-class method delegation. Hard-tier types (sealed structs, delegates, etc.) raise `SHIM001` warnings with a pointer to `fallout-migrate`.
-- **Shim packages publish to GitHub Packages** (#47): `Nuke.*` package IDs belong to matkoch on nuget.org, so the transition shim feed lives at GH Packages (`https://nuget.pkg.github.com/ChrisonSimtian/index.json`). Production-build feed is nuget.org with `Fallout.*` only.
+- **Shim packages publish to GitHub Packages** (#47): `Nuke.*` package IDs belong to matkoch on nuget.org, so the transition shim feed lives at GH Packages (`https://nuget.pkg.github.com/Fallout-build/index.json`). Production-build feed is nuget.org with `Fallout.*` only.
### Vendored fork of `Microsoft.VisualStudio.SolutionPersistence`
- **Replaced `matkoch.Microsoft.VisualStudio.SolutionPersistence` with a vendored fork** (#86). The upstream Microsoft package only ships `net472` + `net8.0`; our source generators need `netstandard2.0`. Matt's fork had the netstandard2.0 patches — we forked his repo at [`ChrisonSimtian/vs-solutionpersistence`](https://github.com/ChrisonSimtian/vs-solutionpersistence) (preserves the MIT license + upstream Microsoft history + attribution chain). Sources are a submodule at `vendor/vs-solutionpersistence/`; wrapper csproj at `src/Fallout.VisualStudio.SolutionPersistence/` compiles them with the TFMs we need. Assembly name stays `Microsoft.VisualStudio.SolutionPersistence` so type identity is preserved. (Note: in 10.2 the wrapper packed as `IsPackable=false` and caused the restore bug fixed in 10.3.0 above — known issue, fix-forward.)
diff --git a/docs/agents/release-and-versioning.md b/docs/agents/release-and-versioning.md
index 64493bf57..b6ab8dec4 100644
--- a/docs/agents/release-and-versioning.md
+++ b/docs/agents/release-and-versioning.md
@@ -92,7 +92,7 @@ If you only discover the breaking nature mid-review, apply all relevant steps be
| Job | Environment | Fires on tag push? | What ships | Gating |
|---|---|---|---|---|
| `publish-nuget-org` | `nuget-org` | **No — opt-in only** via `workflow_dispatch` flag | `Fallout.*.nupkg` to https://api.nuget.org/v3/index.json | Workflow flag + approval-gated env |
-| `publish-github-packages` | `github-packages` | Yes | **All** `*.nupkg` (Fallout.* + Nuke.*) to https://nuget.pkg.github.com/ChrisonSimtian/index.json | None |
+| `publish-github-packages` | `github-packages` | Yes | **All** `*.nupkg` (Fallout.* + Nuke.*) to https://nuget.pkg.github.com/Fallout-build/index.json | None |
| `publish-github-releases` | `github-releases` | Yes | All `*.nupkg` attached to a GitHub Release on the tag, auto-generated notes | None |
### Test lanes (from `experimental` and `main`)
diff --git a/docs/migration/from-nuke.md b/docs/migration/from-nuke.md
index 1332f6d86..f1eba5a94 100644
--- a/docs/migration/from-nuke.md
+++ b/docs/migration/from-nuke.md
@@ -82,7 +82,7 @@ The shim ships on **GitHub Packages** (not nuget.org — that namespace belongs
-
+
diff --git a/src/Shims/Nuke.Build/README.md b/src/Shims/Nuke.Build/README.md
index c4d6797d8..69f5a348b 100644
--- a/src/Shims/Nuke.Build/README.md
+++ b/src/Shims/Nuke.Build/README.md
@@ -11,7 +11,7 @@ Coverage and limitations are the same as the `Nuke.Common` shim — see [`../Nuk
Add this fork's GitHub Packages feed to your `nuget.config`:
```xml
-
+
```
Then bump your `Nuke.Build` package reference to the latest 10.3.x or later.
diff --git a/src/Shims/Nuke.Common/README.md b/src/Shims/Nuke.Common/README.md
index 3d7a48380..6d515d5c7 100644
--- a/src/Shims/Nuke.Common/README.md
+++ b/src/Shims/Nuke.Common/README.md
@@ -45,14 +45,14 @@ Until automated dual-publish lands (tracked in [#69](https://github.com/ChrisonS
dotnet pack src/Shims/Nuke.Common/Nuke.Common.csproj -c Release
dotnet nuget push **/Nuke.Common.*.nupkg `
--api-key $env:GITHUB_TOKEN `
- --source https://nuget.pkg.github.com/ChrisonSimtian/index.json
+ --source https://nuget.pkg.github.com/Fallout-build/index.json
```
## Migration path
For projects that just want their `class Build : NukeBuild { ... }` to compile against our framework:
-1. Add `https://nuget.pkg.github.com/ChrisonSimtian/index.json` as a NuGet source (`nuget.config`).
+1. Add `https://nuget.pkg.github.com/Fallout-build/index.json` as a NuGet source (`nuget.config`).
2. Bump the `Nuke.Common` package version to the latest published here.
3. Build. If the shim covers what you use, you're done. Otherwise:
4. Run `fallout-migrate` to rewrite the remaining references.
diff --git a/src/Shims/Nuke.Components/README.md b/src/Shims/Nuke.Components/README.md
index afe0142e7..65c2f49d2 100644
--- a/src/Shims/Nuke.Components/README.md
+++ b/src/Shims/Nuke.Components/README.md
@@ -11,7 +11,7 @@ Limitations are the same as the `Nuke.Common` shim — see [`../Nuke.Common/READ
Add this fork's GitHub Packages feed to your `nuget.config`:
```xml
-
+
```
Then bump your `Nuke.Components` package reference to the latest 10.3.x or later.
From d84fad5cd7a61c8c3349eeb148545296bc20bc6d Mon Sep 17 00:00:00 2001
From: Chrison Simtian
Date: Tue, 2 Jun 2026 15:19:34 +1200
Subject: [PATCH 4/7] simplify claude.json
---
.claude/settings.json | 46 ++++---------------------------------------
1 file changed, 4 insertions(+), 42 deletions(-)
diff --git a/.claude/settings.json b/.claude/settings.json
index 8cc878e58..33fe326e2 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -1,52 +1,14 @@
{
"$schema": "https://json.schemastore.org/claude-code-settings.json",
- "statusLine": {
- "type": "command",
- "command": "pwsh -NoProfile -Command '$input | & (Get-ChildItem $HOME/.claude/plugins/cache/myfoodbag/mfb-statusline/*/statusline-command.ps1 | Select-Object -Last 1)'"
- },
- "enabledPlugins": {
- "mfb-statusline@myfoodbag": true
- },
"permissions": {
"allow": [
- "Bash(dotnet build:*)",
- "Bash(dotnet test:*)",
- "Bash(dotnet restore:*)",
- "Bash(dotnet pack:*)",
- "Bash(dotnet --info)",
- "Bash(dotnet --version)",
- "Bash(dotnet list:*)",
+ "Bash(dotnet:*)",
"Bash(./build.ps1:*)",
"Bash(./build.sh:*)",
"Bash(./build.cmd:*)",
- "Bash(pwsh ./build.ps1:*)",
- "Bash(pwsh -NoProfile ./build.ps1:*)",
- "Bash(git status)",
- "Bash(git status:*)",
- "Bash(git diff:*)",
- "Bash(git log:*)",
- "Bash(git show:*)",
- "Bash(git branch:*)",
- "Bash(git remote -v)",
- "Bash(git rev-parse:*)",
- "Bash(git ls-files:*)",
- "Bash(git config --get:*)",
- "Bash(gh pr list:*)",
- "Bash(gh pr view:*)",
- "Bash(gh pr diff:*)",
- "Bash(gh issue list:*)",
- "Bash(gh issue view:*)",
- "Bash(gh run list:*)",
- "Bash(gh run view:*)",
- "Bash(gh workflow list:*)",
- "Bash(gh api:*)"
- ],
- "deny": [
- "Bash(git push --force:*)",
- "Bash(git push -f:*)",
- "Bash(git reset --hard:*)",
- "Bash(git clean -f:*)",
- "Bash(rm -rf:*)"
+ "Bash(pwsh:*)",
+ "Bash(git:*)",
+ "Bash(gh:*)"
]
}
}
From 7adf167507e4c629de9af7f55a90c638fbff6eee Mon Sep 17 00:00:00 2001
From: Chrison Simtian
Date: Wed, 3 Jun 2026 15:29:57 +1200
Subject: [PATCH 5/7] Replace star-history with repostars for tracking
Updated star tracking links and attribution in README.
---
README.md | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/README.md b/README.md
index d8c82e46b..3efa64741 100644
--- a/README.md
+++ b/README.md
@@ -107,9 +107,9 @@ Generated by [Repobeats](https://repobeats.axiom.co).
### Stars over time
-[](https://star-history.com/#ChrisonSimtian/Fallout&Date)
+[](https://repostars.dev/?repos=Fallout-build%2FFallout&theme=terminal)
-Generated by [star-history.com](https://star-history.com). Auto-updates as new stargazers arrive.
+Generated by [repostars.dev](https://www.repostars.dev/). Auto-updates as new stargazers arrive.
## Sponsorship
From b4866d9259cbadda2685e39c889494d290fffdf5 Mon Sep 17 00:00:00 2001
From: Chrison Simtian
Date: Wed, 3 Jun 2026 19:24:56 +1200
Subject: [PATCH 6/7] docs(conventions): add writing-style rule for issues,
PRs, commits (#361)
User feedback: AI-written issues are too complex and hard to read, especially for non-native English speakers. Require short, plain, bulleted output for issues, PR bodies/comments, and commit messages.
Co-authored-by: Claude Opus 4.8 (1M context)
---
docs/agents/conventions.md | 12 ++++++++++++
1 file changed, 12 insertions(+)
diff --git a/docs/agents/conventions.md b/docs/agents/conventions.md
index 555ce8125..646e2f979 100644
--- a/docs/agents/conventions.md
+++ b/docs/agents/conventions.md
@@ -12,6 +12,18 @@ Three groups: conventions to respect, things never to do, and the tool-wrapper r
- **No per-file license headers.** The MIT notice lives in [`LICENSE`](../../LICENSE) at the repo root, and NuGet packages declare MIT via `PackageLicenseExpression`. Per-file headers were stripped in v11 (one source of truth + the header URL would have rotted on the repo-org transfer). Vendored third-party code keeps its own copyright headers — don't touch those (e.g. files under `src/Persistence/Fallout.Persistence.Solution/` retain Microsoft's MIT notice).
- **`[Experimental]` for opt-in unstable public APIs.** Not-yet-stable public surface is marked with `[Experimental("FALLOUT0xx")]` rather than held back or shipped silently. See [the `[Experimental]` convention](#experimental-for-opt-in-unstable-apis) below and the [diagnostic-ID registry](../experimental-apis.md).
+## Writing style for issues, PRs, and commits
+
+Applies to AI tools and humans. Many readers are non-native English speakers — keep it readable.
+
+- Be short and precise. Lead with the point.
+- Prefer bullet points over paragraphs.
+- Use plain, simple English. Short sentences, common words.
+- Cut filler: no preamble, no hedging, no AI-flavored padding.
+- Say what changed and why. Drop the rest.
+
+Covers GitHub issues, PR titles/bodies/comments, and commit messages.
+
## `[Experimental]` for opt-in unstable APIs
Per [ADR-0004 §5](../adr/0004-calendar-versioning-and-dual-pace-channels.md), public APIs that aren't ready to commit to a stability guarantee are marked with [`System.Diagnostics.CodeAnalysis.ExperimentalAttribute`](https://learn.microsoft.com/dotnet/api/system.diagnostics.codeanalysis.experimentalattribute) instead of being held back or shipped silently. The attribute ships in the .NET 8+ BCL — **no package reference needed** (the repo targets .NET 10).
From 1c5fea66899762c47e4d26f8c40af32613b8e009 Mon Sep 17 00:00:00 2001
From: Chrison Simtian
Date: Wed, 3 Jun 2026 19:43:32 +1200
Subject: [PATCH 7/7] docs: record keep-rationale for justified hand-rolled
code
- add docs/dependencies-kept.md listing the deliberate keeps
(crypto, path API, CI config writers, parameter schema)
- add 'why hand-rolled' XML-doc remarks to each area
- cross-link from dependencies.md
Closes #358
Co-Authored-By: Claude Opus 4.8 (1M context)
---
docs/dependencies-kept.md | 24 +++++++++++++++++++
docs/dependencies.md | 2 ++
src/Fallout.Build/CICD/ConfigurationEntity.cs | 8 +++++++
src/Fallout.Build/CICD/CustomFileWriter.cs | 11 +++++++++
src/Fallout.Build/Utilities/SchemaUtility.cs | 3 +++
src/Fallout.Utilities/IO/AbsolutePath.cs | 7 ++++++
src/Fallout.Utilities/IO/PathConstruction.cs | 4 ++++
.../Security/EncryptionUtility.cs | 5 ++++
8 files changed, 64 insertions(+)
create mode 100644 docs/dependencies-kept.md
diff --git a/docs/dependencies-kept.md b/docs/dependencies-kept.md
new file mode 100644
index 000000000..490dba265
--- /dev/null
+++ b/docs/dependencies-kept.md
@@ -0,0 +1,24 @@
+# Deliberately hand-rolled code (do not "use a library")
+
+Companion to [dependencies.md](dependencies.md). That page lists the libraries we *do* pull in;
+this page records the areas we keep hand-rolled **on purpose**, with the reason, so they aren't
+re-litigated in future "replace the reinvented wheel" passes.
+
+These keeps come out of the dependency-consolidation audit (2026-06-02, [#358](https://github.com/ChrisonSimtian/Fallout/issues/358)).
+Each area also carries the same rationale as an XML-doc remark on the type itself.
+
+**Before proposing a library swap for anything below, read the reason and the linked discussion first.**
+
+| Area | Files | Why it stays hand-rolled |
+|---|---|---|
+| Secret encryption | `src/Fallout.Utilities/Security/EncryptionUtility.cs` | Built directly on BCL crypto (`AesGcm` + `Rfc2898DeriveBytes`): AES-GCM, random salt/nonce, PBKDF2-SHA256 at 600,000 iterations. OWASP-aligned and security-audited under [#212](https://github.com/ChrisonSimtian/Fallout/issues/212). This is correct BCL crypto — a third-party crypto library would add risk, not remove it. |
+| Path API | `src/Fallout.Utilities/IO/AbsolutePath.cs`, `src/Fallout.Utilities/IO/PathConstruction.cs` (~500 LOC) | Type-safe, OS-independent path model — a core selling point of the framework. The BCL `System.IO.Path` API is string-typed and OS-dependent; our type captures the rooted/relative distinction, the `/` and `+` append operators, and automatic normalization. No general-purpose NuGet path library matches this surface. |
+| CI/CD config writers | `src/Fallout.Build/CICD/CustomFileWriter.cs`, `src/Fallout.Build/CICD/ConfigurationEntity.cs`, `src/Fallout.Common/CI/**/Configuration/*Configuration.cs` (~1,800 LOC) | TeamCity and SpaceAutomation emit a **Kotlin DSL** — no YAML/JSON serializer can produce that. The YAML targets (GitHub Actions, Azure Pipelines, AppVeyor) need exact control over comments, quoting, and indentation; a serializer like YamlDotNet would silently rewrite those and break round-trips users depend on. The hand-rolled `CustomFileWriter` gives us that control. |
+| Parameter schema | `src/Fallout.Build/Utilities/SchemaUtility.cs` (~393 LOC) | Emits the draft-04 JSON Schema envelope (definitions block + `allOf[user, base]`) that the NUKE ecosystem has emitted since day one. The exact output shape is a contract consumed by `Fallout.Cli` for editor autocomplete/validation of `parameters.json`. A schema library (NJsonSchema, Newtonsoft) would change the shape. Already hand-rolled on `System.Text.Json` nodes — no extra dependency. |
+
+## When does a keep get revisited?
+
+A keep is not permanent. Re-open the question only when the reason no longer holds — e.g. a CI target
+drops its Kotlin DSL, the schema contract is versioned so the shape can change, or a vetted crypto/path
+library covers the exact surface with no behavioral drift. In that case, raise it on the issue tracker
+referencing this page rather than silently swapping in a dependency.
diff --git a/docs/dependencies.md b/docs/dependencies.md
index d579f84c7..61e8cb534 100644
--- a/docs/dependencies.md
+++ b/docs/dependencies.md
@@ -6,6 +6,8 @@ A flat overview of the non-trivial libraries Fallout pulls in, what each is for,
Central package versions are pinned in `Directory.Packages.props`; this page links each entry back to it.
+For the flip side — areas we keep **hand-rolled on purpose** instead of pulling in a library (crypto, paths, CI config writers, schema) — see [dependencies-kept.md](dependencies-kept.md).
+
---
## Microsoft / .NET BCL
diff --git a/src/Fallout.Build/CICD/ConfigurationEntity.cs b/src/Fallout.Build/CICD/ConfigurationEntity.cs
index f04a12d98..319372897 100644
--- a/src/Fallout.Build/CICD/ConfigurationEntity.cs
+++ b/src/Fallout.Build/CICD/ConfigurationEntity.cs
@@ -4,6 +4,14 @@
namespace Fallout.Common.CI;
+///
+/// Base for a CI/CD config node that writes itself through a .
+///
+///
+/// The CI config writers are hand-rolled on purpose (Kotlin DSL targets + exact comment/quote/indent
+/// control on YAML targets); see CustomFileWriter and
+/// docs/dependencies-kept.md.
+///
public abstract class ConfigurationEntity
{
public abstract void Write(CustomFileWriter writer);
diff --git a/src/Fallout.Build/CICD/CustomFileWriter.cs b/src/Fallout.Build/CICD/CustomFileWriter.cs
index ba334ab2e..f988e23c0 100644
--- a/src/Fallout.Build/CICD/CustomFileWriter.cs
+++ b/src/Fallout.Build/CICD/CustomFileWriter.cs
@@ -4,6 +4,17 @@
namespace Fallout.Common.Utilities;
+///
+/// Low-level, indentation-aware line writer backing the CI/CD configuration generators
+/// (ConfigurationEntity.Write in Fallout.Common.CI.*).
+///
+///
+/// Deliberately hand-rolled, not replaced by a serializer (e.g. YamlDotNet). TeamCity and
+/// SpaceAutomation targets emit a Kotlin DSL, which no YAML/JSON serializer can produce; the
+/// YAML targets (GitHub Actions, Azure Pipelines, AppVeyor) need exact control over comments, quoting,
+/// and indentation that a serializer would silently rewrite. See
+/// docs/dependencies-kept.md.
+///
public class CustomFileWriter
{
private readonly StreamWriter _streamWriter;
diff --git a/src/Fallout.Build/Utilities/SchemaUtility.cs b/src/Fallout.Build/Utilities/SchemaUtility.cs
index 7c6e5c171..4a5d2ac62 100644
--- a/src/Fallout.Build/Utilities/SchemaUtility.cs
+++ b/src/Fallout.Build/Utilities/SchemaUtility.cs
@@ -20,6 +20,9 @@ namespace Fallout.Common.Execution;
/// can autocomplete and validate the consumer's parameters.json. Hand-rolled on top of
/// — no NJsonSchema, no Newtonsoft. The output shape is the draft-04 envelope
/// the NUKE ecosystem has emitted since day one (definitions block + allOf[user, base]).
+/// The exact output shape is a contract consumed by Fallout.Cli, so it stays hand-rolled
+/// rather than swapped for a schema library. See
+/// docs/dependencies-kept.md.
///
public static class SchemaUtility
{
diff --git a/src/Fallout.Utilities/IO/AbsolutePath.cs b/src/Fallout.Utilities/IO/AbsolutePath.cs
index c95fd5136..31231c4eb 100644
--- a/src/Fallout.Utilities/IO/AbsolutePath.cs
+++ b/src/Fallout.Utilities/IO/AbsolutePath.cs
@@ -13,6 +13,13 @@ namespace Fallout.Common.IO;
///
/// Represents an absolute path without distinction between files and directories.
///
+///
+/// Deliberately hand-rolled, not delegated to a NuGet path library. The BCL
+/// API is string-typed and OS-dependent; this type plus give a
+/// type-safe, OS-independent path model (rooted/relative distinction, / and + operators,
+/// automatic normalization) that is a core selling point of the framework. See
+/// docs/dependencies-kept.md.
+///
[Serializable]
[TypeConverter(typeof(TypeConverter))]
[DebuggerDisplay("{" + nameof(_path) + "}")]
diff --git a/src/Fallout.Utilities/IO/PathConstruction.cs b/src/Fallout.Utilities/IO/PathConstruction.cs
index 6761ff189..e1e9bc727 100644
--- a/src/Fallout.Utilities/IO/PathConstruction.cs
+++ b/src/Fallout.Utilities/IO/PathConstruction.cs
@@ -17,6 +17,10 @@ namespace Fallout.Common.IO;
/// used intentionally. Casting with (AbsolutePath) ensures that the path is rooted as Windows/Unix/UNC-path. The operators
/// / and + allow to append sub-directories.
/// Resulting paths are automatically normalized if possible. So C:\foo\..\bar\. will become C:\bar.
+/// Deliberately hand-rolled rather than delegated to a NuGet path library: the BCL
+/// API is string-typed and OS-dependent, whereas this type-safe,
+/// OS-independent path model is a core selling point of the framework. See
+/// docs/dependencies-kept.md.
///
///
///
diff --git a/src/Fallout.Utilities/Security/EncryptionUtility.cs b/src/Fallout.Utilities/Security/EncryptionUtility.cs
index f5978a29f..ce87fd969 100644
--- a/src/Fallout.Utilities/Security/EncryptionUtility.cs
+++ b/src/Fallout.Utilities/Security/EncryptionUtility.cs
@@ -28,6 +28,11 @@ namespace Fallout.Common.Utilities;
/// See #212 for the security
/// audit that motivated the v2 format.
///
+///
+/// Deliberately built on the BCL crypto primitives ( + ),
+/// not a third-party crypto library. The construction is OWASP-aligned and security-audited (#212); no
+/// swap is warranted. See docs/dependencies-kept.md.
+///
internal static class EncryptionUtility
{
private const string V1Prefix = "v1:";