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 -[![Star History Chart](https://api.star-history.com/svg?repos=ChrisonSimtian/Fallout&type=Date)](https://star-history.com/#ChrisonSimtian/Fallout&Date) +[![RepoStars](https://repostars.dev/api/embed?repo=Fallout-build%2FFallout&theme=terminal)](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.