From 0aa5ca0ea73574767f96ac9cac59656777c0e864 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Sat, 30 May 2026 14:20:22 +1200 Subject: [PATCH 01/10] chore(experimental): set version.json to 2026.1.0-alpha.{height} Seeds the fast/AI lane (ADR-0004). experimental is a non-public NB.GV ref, so builds get -alpha..g, sorting below main's -preview. Same core as main (2026.1.0); breaking surface rides [Experimental] until the yearly cut. Co-Authored-By: Claude Opus 4.8 (1M context) --- version.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/version.json b/version.json index b5a80df54..c4d5b2508 100644 --- a/version.json +++ b/version.json @@ -1,6 +1,6 @@ { "$schema": "https://raw.githubusercontent.com/dotnet/Nerdbank.GitVersioning/main/src/NerdBank.GitVersioning/version.schema.json", - "version": "2026.1.0-preview.{height}", + "version": "2026.1.0-alpha.{height}", "publicReleaseRefSpec": [ "^refs/heads/release/\\d{4}$", "^refs/heads/support/\\d{4}$", @@ -18,7 +18,7 @@ }, "release": { "versionIncrement": "minor", - "firstUnstableTag": "preview", + "firstUnstableTag": "alpha", "branchName": "release/{version}" } } From f8d631cb7ece7c31e1d5f64debab14dab75529af Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Sun, 31 May 2026 12:49:13 +1200 Subject: [PATCH 02/10] =?UTF-8?q?chore:=20forward-port=20main=20=E2=86=92?= =?UTF-8?q?=20experimental=20(CI=20hygiene=20#322/#324/#329)=20(#338)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * ci: realign triggers to the 3-tier ladder + hygiene (milestone #18) (#330) 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) * ci: test before publishing on every lane + cache restore-keys (#324, #328) (#331) #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) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/experimental.yml | 17 +++++++++-- .github/workflows/macos-latest.yml | 14 +++++++-- .github/workflows/preview.yml | 17 +++++++++-- .github/workflows/release.yml | 3 +- .github/workflows/ubuntu-latest.yml | 5 +++- .github/workflows/windows-latest.yml | 14 +++++++-- build/Build.CI.GitHubActions.cs | 44 ++++++++++++++++++---------- docs/agents/conventions.md | 16 ++++++++++ 8 files changed, 101 insertions(+), 29 deletions(-) diff --git a/.github/workflows/experimental.yml b/.github/workflows/experimental.yml index 76faad6d7..ad9ca7fee 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: @@ -52,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/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..f3d34cc1a 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: @@ -52,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 d7820252e..ce115724e 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: @@ -102,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/.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..555ce8125 100644 --- a/docs/agents/conventions.md +++ b/docs/agents/conventions.md @@ -34,9 +34,25 @@ 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. +- **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 - 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 8fa4b9589efd40ddb9a15f9083b1f61bb6b0fbd8 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Sun, 31 May 2026 13:16:57 +1200 Subject: [PATCH 03/10] feat(publish): multi-channel, package-ID-aware publishing + dogfood the lanes (#333) (#339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(publish): multi-channel, package-ID-aware publishing surface (#333, FALLOUT001) IPublish gains [Experimental("FALLOUT001")] PublishTargets + a --publish-to selector; Publish now routes one Pack output across multiple feeds via the pure, unit-tested PublishPackageRouter (glob include/exclude by package name). Existing single-source members stay as a back-compat default target. Build.cs wires the two real channels: github-packages (every package incl Nuke.*, keyed by the GitHub token) and nuget.org (Fallout.* only, never Nuke.*, keyed by NUGET_API_KEY) — replacing the legacy single-feed push. - src/Fallout.Components/PublishTarget.cs — PublishTarget record + PublishPackageRouter - tests/Fallout.Components.Tests — 10 router tests (added to fallout.slnx) - docs/experimental-apis.md — FALLOUT001 registered Framework compiles clean (0 errors); router tests green. Workflow rewire to `dotnet fallout Publish --publish-to …` follows once experimental is synced with main. Co-Authored-By: Claude Opus 4.8 (1M context) * ci(publish): dogfood `dotnet fallout Publish` on the alpha/preview lanes (#333) experimental.yml and preview.yml now publish via `dotnet fallout Publish --publish-to github-packages` instead of a hand-rolled `dotnet nuget push` loop. Publish depends on Test + Pack, so one invocation runs Restore → Compile → Test → Pack → Publish as NUKE stages; package routing (Fallout.* + Nuke.* → GitHub Packages) lives in Build.cs IPublish.PublishTargets. The GitHub token is passed via the GitHubToken env. Also restore IPublish.PackagePushSettings + PushSettingsBase (kept for back-compat) so the multi-channel reshape stays additive — the new PublishTargets/PublishTo surface is the only opt-in change, behind [Experimental("FALLOUT001")]. release.yml's publish jobs still use raw nuget push (they're coupled to the artifact handoff + nuget-org approval gate) — dogfooding those is folded into #336. Co-Authored-By: Claude Opus 4.8 (1M context) * fix(publish): make PublishTarget a sealed class so the shim generator skips it CI caught CS0509: the TransitionShimGenerator tried to derive a Nuke.Components shim from the sealed *record* PublishTarget. Sealed classes are skipped by design (SHIM001), but the sealed-record shape slipped past that guard. PublishTarget is a new type with no pre-rename consumers, so skipping its shim is correct; switching record→sealed class hits the documented skip path. We don't use record equality/`with`. Verified: Nuke.Components builds 0 errors (SHIM001 warning only). Co-Authored-By: Claude Opus 4.8 (1M context) * test(generators): accept solution-generator snapshot for new Fallout.Components.Tests Adding tests/Fallout.Components.Tests to fallout.slnx makes the StronglyTypedSolution generator emit a Fallout_Components_Tests accessor; update the Verify snapshot to match (one added line — the new project's strongly-typed Solution property). Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- .github/workflows/experimental.yml | 33 ++----- .github/workflows/preview.yml | 33 ++----- build/Build.cs | 50 ++++++---- docs/experimental-apis.md | 2 +- fallout.slnx | 1 + src/Fallout.Components/IPublish.cs | 98 +++++++++++++++---- src/Fallout.Components/PublishTarget.cs | 83 ++++++++++++++++ .../Fallout.Components.Tests.csproj | 11 +++ .../PublishPackageRouterTests.cs | 62 ++++++++++++ ...nGeneratorTest.Test#Solution.g.verified.cs | 1 + 10 files changed, 288 insertions(+), 86 deletions(-) create mode 100644 src/Fallout.Components/PublishTarget.cs create mode 100644 tests/Fallout.Components.Tests/Fallout.Components.Tests.csproj create mode 100644 tests/Fallout.Components.Tests/PublishPackageRouterTests.cs diff --git a/.github/workflows/experimental.yml b/.github/workflows/experimental.yml index ad9ca7fee..e5d88e09e 100644 --- a/.github/workflows/experimental.yml +++ b/.github/workflows/experimental.yml @@ -64,26 +64,13 @@ jobs: global-json-file: global.json - name: 'Restore: dotnet tools' run: dotnet tool restore - # 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 - shopt -s nullglob - packages=(output/packages/*.nupkg) - if [ ${#packages[@]} -eq 0 ]; then - echo "::error::No packages found — nothing to publish to the experimental channel." - exit 1 - fi - 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" \ - --api-key "${{ secrets.GITHUB_TOKEN }}" \ - --skip-duplicate - done + # Test + Pack + push alpha → GitHub Packages, all through our own framework (#333) — + # dogfooding `dotnet fallout Publish` instead of a hand-rolled `dotnet nuget push`. + # `Publish` depends on Test + Pack, so NUKE runs Restore → Compile → Test → Pack → + # Publish as discrete internal stages and fails at the breaking stage; if Test fails + # the job stops before any push. Which packages go where (Fallout.* + Nuke.* → GitHub + # Packages) is decided by Build.cs IPublish.PublishTargets, not here. + - name: 'Publish: alpha → GitHub Packages' + run: dotnet fallout Publish --publish-to github-packages + env: + GitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/preview.yml b/.github/workflows/preview.yml index f3d34cc1a..5d71ade08 100644 --- a/.github/workflows/preview.yml +++ b/.github/workflows/preview.yml @@ -64,26 +64,13 @@ jobs: global-json-file: global.json - name: 'Restore: dotnet tools' run: dotnet tool restore - # 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 - shopt -s nullglob - packages=(output/packages/*.nupkg) - if [ ${#packages[@]} -eq 0 ]; then - echo "::error::No packages found — nothing to publish to the preview channel." - exit 1 - fi - 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" \ - --api-key "${{ secrets.GITHUB_TOKEN }}" \ - --skip-duplicate - done + # Test + Pack + push preview → GitHub Packages, all through our own framework (#333) — + # dogfooding `dotnet fallout Publish` instead of a hand-rolled `dotnet nuget push`. + # `Publish` depends on Test + Pack, so NUKE runs Restore → Compile → Test → Pack → + # Publish as discrete internal stages and fails at the breaking stage; if Test fails + # the job stops before any push. Which packages go where (Fallout.* + Nuke.* → GitHub + # Packages) is decided by Build.cs IPublish.PublishTargets, not here. + - name: 'Publish: preview → GitHub Packages' + run: dotnet fallout Publish --publish-to github-packages + env: + GitHubToken: ${{ secrets.GITHUB_TOKEN }} diff --git a/build/Build.cs b/build/Build.cs index 823c3535f..40d2f3ffc 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -127,31 +127,41 @@ from framework in project.GetTargetFrameworks() [Parameter] [Secret] readonly string NuGetApiKey; - // Publishing to nuget.org now that the Fallout.* rename has landed (#54). - // Requires NUGET_API_KEY in repo secrets — see release.yml. - string IPublish.NuGetSource => "https://api.nuget.org/v3/index.json"; - string IPublish.NuGetApiKey => NuGetApiKey; - + // Two publish channels (FALLOUT001 — see IPublish.PublishTargets). Routing replaces the + // old single-feed push + the hand-rolled `dotnet nuget push` in the workflows (#333): + // - github-packages: EVERY package, incl. the Nuke.* shims — that ID is owned by the + // original NUKE maintainer (#47), so the shims only ever go here. Keyed by the GitHub token. + // - nuget.org: Fallout.* ONLY, never the Nuke.* shims. Keyed by NUGET_API_KEY. + // Select per run from CI with `dotnet fallout Publish --publish-to `. PublishTarget.SkipDuplicate + // (default true) keeps re-runs idempotent if a version already exists on a feed. +#pragma warning disable FALLOUT001 // opting our own build into the experimental multi-channel publish surface + IEnumerable IPublish.PublishTargets => new[] + { + new PublishTarget + { + Name = "github-packages", + Source = "https://nuget.pkg.github.com/ChrisonSimtian/index.json", + ApiKey = From().GitHubToken, + }, + new PublishTarget + { + Name = "nuget.org", + Source = "https://api.nuget.org/v3/index.json", + ApiKey = NuGetApiKey, + IncludePackages = new[] { "Fallout.*" }, + ExcludePackages = new[] { "Nuke.*" }, + }, + }; +#pragma warning restore FALLOUT001 + + // The workflows now gate which channel publishes (via --publish-to); no on-branch + // requirement here. Missing keys fail per-target inside Publish, so a stray local + // `dotnet fallout Publish` is safe (no key → clear error, no push). Target IPublish.Publish => _ => _ .Inherit() .Consumes(From().Pack) - .Requires(() => GitRepository.IsOnMainBranch() && Host is GitHubActions && GitHubActions.Workflow == ReleaseWorkflow) .WhenSkipped(DependencyBehavior.Execute); - // Filter `Nuke.*` shim packages out of the nuget.org push — that ID is owned by - // the original NUKE maintainer. The shims still build and pack as artifacts; they - // are pushed to GitHub Packages by a follow-up step in .github/workflows/release.yml - // (#47). - IEnumerable IPublish.PushPackageFiles - => From().PackagesDirectory.GlobFiles("*.nupkg") - .Where(x => !x.NameWithoutExtension.StartsWith("Nuke.", StringComparison.OrdinalIgnoreCase)); - - // `--skip-duplicate` makes nuget push idempotent: if a version is already on the feed, - // skip it instead of erroring. Lets us rerun release.yml safely when a single package - // fails mid-batch without nuking the whole pipeline on retry. - Configure IPublish.PushSettings => _ => _ - .EnableSkipDuplicate(); - IEnumerable NuGetPackageFiles => From().PackagesDirectory.GlobFiles("*.nupkg"); diff --git a/docs/experimental-apis.md b/docs/experimental-apis.md index e51405716..3bf307a2d 100644 --- a/docs/experimental-apis.md +++ b/docs/experimental-apis.md @@ -48,7 +48,7 @@ Status values: **Experimental** (live, opt-in), **Promoted** (attribute removed, | ID | Surface | Introduced | Status | Notes | |----|---------|------------|--------|-------| -| _none allocated yet_ | — | — | — | First experimental API to land claims `FALLOUT001`. | +| `FALLOUT001` | `Fallout.Components.IPublish.PublishTargets` / `.PublishTo` (multi-channel publishing) | 2026.1 | Experimental | Fan a single `Pack` output across multiple feeds with per-feed package-ID routing (`PublishTarget`). Shape may change while the CD model firms up (epic #332). Promote by deleting the attribute once `ReleaseChannel`/`DeploymentTarget` (#334) settle. |