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:*)"
]
}
}
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 76faad6d7..f1009949a 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
@@ -72,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/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..98f84874f 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
@@ -72,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 d7820252e..10e1e1e4d 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:
@@ -198,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/.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/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/Directory.Packages.props b/Directory.Packages.props
index 55b9e6c2c..d8e9060cb 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -9,6 +9,7 @@
+
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
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..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).
@@ -34,9 +46,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.
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/Fallout.Common/Fallout.Common.csproj b/src/Fallout.Common/Fallout.Common.csproj
index b0c922b14..a24de04f9 100644
--- a/src/Fallout.Common/Fallout.Common.csproj
+++ b/src/Fallout.Common/Fallout.Common.csproj
@@ -27,6 +27,7 @@
+
diff --git a/src/Fallout.Common/IO/FtpTasks.cs b/src/Fallout.Common/IO/FtpTasks.cs
index ad1bd6840..e6b1a0a3f 100644
--- a/src/Fallout.Common/IO/FtpTasks.cs
+++ b/src/Fallout.Common/IO/FtpTasks.cs
@@ -1,9 +1,10 @@
-using System;
-using System.IO;
+using System;
using System.Linq;
using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using FluentFTP;
using Serilog;
-#pragma warning disable SYSLIB0014
namespace Fallout.Common.IO;
@@ -11,73 +12,85 @@ public static class FtpTasks
{
public static NetworkCredential FtpCredentials { get; set; }
- public static void FtpUploadDirectoryRecursively(string directory, string hostRoot)
+ public static void FtpUploadDirectoryRecursively(string host, string directory, string serverRoot)
{
- Log.Information("Uploading directory {Directory} to {HostRoot} ...", directory, hostRoot);
+ FtpUploadDirectoryRecursivelyAsync(host, directory, serverRoot).GetAwaiter().GetResult();
+ }
+
+ public static async Task FtpUploadDirectoryRecursivelyAsync(
+ string host,
+ string directory,
+ string serverRoot,
+ CancellationToken cancellationToken = default)
+ {
+ Log.Information("Uploading directory {Directory} to {ServerRoot} ...", directory, serverRoot);
var files = Globbing.GlobFiles(directory, "**/*").ToList();
+
+ await using var client = CreateClient(host);
+ await client.Connect(cancellationToken);
+
for (var index = 0; index < files.Count; index++)
{
var file = files[index];
var relativePath = PathConstruction.GetRelativePath(directory, file);
- var hostPath = $"{hostRoot}/{relativePath}";
+ var serverPath = CombineServerPath(serverRoot, relativePath);
- FtpUploadFileInternal(file, hostPath, $"[{index + 1}/{files.Count}] ");
+ Log.Debug("[{Index}/{Count}] Uploading to {ServerPath} ...", index + 1, files.Count, serverPath);
+ await client.UploadFile(
+ file,
+ serverPath,
+ createRemoteDir: true,
+ token: cancellationToken);
}
}
- public static void FtpUploadFile(string file, string hostDestination)
+ public static void FtpUploadFile(string host, string file, string serverDestination)
{
- FtpUploadFileInternal(file, hostDestination);
+ FtpUploadFileAsync(host, file, serverDestination).GetAwaiter().GetResult();
}
- private static void FtpUploadFileInternal(string file, string hostDestination, string prefix = null)
+ public static async Task FtpUploadFileAsync(
+ string host,
+ string file,
+ string serverDestination,
+ CancellationToken cancellationToken = default)
{
- Log.Debug($"{prefix}Uploading to {{HostDestination}} ...", hostDestination);
-
- ControlFlow.ExecuteWithRetry(() =>
- {
- FtpMakeDirectory(GetParentPath(hostDestination));
-
- var request = WebRequest.Create(hostDestination);
- request.Credentials = FtpCredentials;
- request.Method = WebRequestMethods.Ftp.UploadFile;
-
- var content = File.ReadAllBytes(file);
- request.ContentLength = content.Length;
+ Log.Debug("Uploading to {ServerDestination} ...", serverDestination);
- using var requestStream = request.GetRequestStream();
- requestStream.Write(content, offset: 0, count: content.Length);
- requestStream.Close();
+ await using var client = CreateClient(host);
+ await client.Connect(cancellationToken);
+ await client.UploadFile(
+ file,
+ serverDestination,
+ createRemoteDir: true,
+ token: cancellationToken);
+ }
- // TODO: check response
- //var response = (FtpWebResponse) request.GetResponse ();
- //response.Close ();
- });
+ public static void FtpMakeDirectory(string host, string path)
+ {
+ FtpMakeDirectoryAsync(host, path).GetAwaiter().GetResult();
}
- public static void FtpMakeDirectory(string path)
+ public static async Task FtpMakeDirectoryAsync(
+ string host,
+ string path,
+ CancellationToken cancellationToken = default)
{
- var parentPath = GetParentPath(path);
- if (parentPath != path)
- FtpMakeDirectory(parentPath);
+ await using var client = CreateClient(host);
+ await client.Connect(cancellationToken);
+ await client.CreateDirectory(path, force: true, token: cancellationToken);
+ }
- var request = WebRequest.Create(path);
- request.Method = WebRequestMethods.Ftp.MakeDirectory;
- request.Credentials = FtpCredentials;
- try
- {
- request.GetResponse().Dispose();
- }
- catch
- {
- // ignored
- }
+ private static AsyncFtpClient CreateClient(string host)
+ {
+ var credentials = FtpCredentials ?? new NetworkCredential();
+ return new AsyncFtpClient(host, credentials);
}
- private static string GetParentPath(string path)
+ private static string CombineServerPath(string serverRoot, string relativePath)
{
- var uri = new Uri(path);
- return uri.AbsoluteUri.Remove(uri.AbsoluteUri.Length - uri.Segments.Last().Length);
+ var normalizedRelative = relativePath.Replace('\\', '/');
+ return $"{serverRoot.TrimEnd('/')}/{normalizedRelative.TrimStart('/')}";
}
}
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.