From fc01d7b95c926c8bbc4e061db9d28b1cd0142f14 Mon Sep 17 00:00:00 2001 From: Dennis Doomen Date: Thu, 22 Oct 2026 21:29:20 +0200 Subject: [PATCH] Use GitVersion and let GA and rc tags publish to nuget.org - Replace Nerdbank.GitVersioning and version.json with GitVersion (GitFlow preset). develop builds are -alpha.N, release/* builds are -beta.N. - Tags have no "v" prefix. The release workflow passes the tag to the build as ReleaseTag, and the tag becomes the package version. - GA and -rc tags publish to nuget.org without approval. Other prerelease tags skip nuget.org. - Preview workflow also runs on release/** pushes. - Prune workflow also removes old -alpha and -beta versions. - Add ADR-0011 and update the release docs. Co-Authored-By: Claude Sonnet 5.5 --- .agents/skills/cutting-a-release/SKILL.md | 5 +- .fallout/build.schema.json | 3 + .github/dependabot.yml | 1 - .github/workflows/build-cross-platform.yml | 2 +- .github/workflows/prune-preview-packages.yml | 21 ++-- .../workflows/publish-packages-preview.yml | 22 ++-- .../workflows/publish-packages-release.yml | 111 ++++++++---------- AGENTS.md | 8 +- Directory.Build.props | 1 - Directory.Build.targets | 18 --- Directory.Packages.props | 1 - GitVersion.yml | 14 +++ build/Build.CI.GitHubActions.cs | 4 +- build/Build.cs | 65 +++++++--- build/_build.csproj | 9 +- ...0011-gitversion-and-tag-driven-releases.md | 61 ++++++++++ docs/adr/README.md | 3 +- docs/architecture.md | 10 +- docs/branching-and-release.md | 53 +++++---- version.json | 24 ---- 20 files changed, 247 insertions(+), 189 deletions(-) create mode 100644 GitVersion.yml create mode 100644 docs/adr/0011-gitversion-and-tag-driven-releases.md delete mode 100644 version.json diff --git a/.agents/skills/cutting-a-release/SKILL.md b/.agents/skills/cutting-a-release/SKILL.md index 23cf8f0ea..af7915f04 100644 --- a/.agents/skills/cutting-a-release/SKILL.md +++ b/.agents/skills/cutting-a-release/SKILL.md @@ -27,7 +27,8 @@ gotchas worth knowing before you start. - **A green pipeline run is not proof anything published** — every publish job is conditional. Verify job conclusions and the nuget.org index after a release (commands in the runbook). -- **nuget.org is always opt-in** via `workflow_dispatch -f publish-to-nugetorg=true`; - a tag push alone only reaches GitHub Packages + GitHub Releases. +- **GA (`10.5.0`) and `-rc` (`10.5.0-rc.1`) tags publish to nuget.org on their own**, + with no approval. Tags have no `v` prefix. Any other prerelease tag skips nuget.org. + Do not reuse a tag name. - Never bypass a hotfix through direct pushes — branch protection blocks it everywhere; even a one-commit cherry-pick goes through a PR. diff --git a/.fallout/build.schema.json b/.fallout/build.schema.json index 7cf04eed9..44f8d7329 100644 --- a/.fallout/build.schema.json +++ b/.fallout/build.schema.json @@ -163,6 +163,9 @@ "type": "string" } }, + "ReleaseTag": { + "type": "string" + }, "Solution": { "type": "string", "description": "Path to a solution file that is automatically loaded" diff --git a/.github/dependabot.yml b/.github/dependabot.yml index e14b0628c..0b49f25cc 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -34,7 +34,6 @@ updates: - dependency-name: "Microsoft.CodeAnalysis.CSharp.CodeFix.Testing.XUnit" # Build-time only (PrivateAssets="all", never in a package's dependencies) - dependency-name: "Microsoft.SourceLink.GitHub" - - dependency-name: "Nerdbank.GitVersioning" - dependency-name: "PolySharp" - dependency-name: "IsExternalInit" # Only used by the fallout / fallout-migrate tools, not by the libraries diff --git a/.github/workflows/build-cross-platform.yml b/.github/workflows/build-cross-platform.yml index 8add798a6..8d36e0a78 100644 --- a/.github/workflows/build-cross-platform.yml +++ b/.github/workflows/build-cross-platform.yml @@ -19,7 +19,7 @@ name: build-cross-platform on: push: tags: - - 'v*' + - '[0-9]*.[0-9]*.[0-9]*' pull_request: branches: - main diff --git a/.github/workflows/prune-preview-packages.yml b/.github/workflows/prune-preview-packages.yml index f23bc7847..0b8977c9e 100644 --- a/.github/workflows/prune-preview-packages.yml +++ b/.github/workflows/prune-preview-packages.yml @@ -13,10 +13,11 @@ # Shape: # - Discovers the org's NuGet packages from the API rather than hard-coding a # list, so a newly-added package is covered the moment it first publishes. -# - Prunes ONLY `-preview.` versions, keeping the newest `keep` of them. -# - `ignore-versions` protects everything that is not a per-commit preview: -# GA, `-rc.N`, and the legacy `-alpha.N` versions are never touched. Those are -# the durable release markers and must outlive any cleanup. +# - Prunes ONLY `-alpha.`, `-beta.` and (legacy) `-preview.` versions, keeping the +# newest `keep` of them. +# - `ignore-versions` protects everything that is not a per-commit build: +# GA and `-rc.N` versions are never touched. Those are the durable release +# markers and must outlive any cleanup. # - Runs weekly, matching the Dependabot cadence, and on demand. # # "Newest" means most-recently-created, not highest version number: the action @@ -103,7 +104,7 @@ jobs: } >> "$GITHUB_STEP_SUMMARY" for p in $(echo '${{ steps.list.outputs.packages }}' | jq -r '.[]'); do n=$(gh api "/orgs/${{ github.repository_owner }}/packages/nuget/$p/versions?per_page=100" \ - --paginate -q '.[].name' | grep -c -- '-preview\.' || true) + --paginate -q '.[].name' | grep -cE -- '-(preview|alpha|beta)\.' || true) d=$(( n > KEEP ? n - KEEP : 0 )) total=$(( total + d )) echo "| \`$p\` | $n | $d |" >> "$GITHUB_STEP_SUMMARY" @@ -130,14 +131,14 @@ jobs: matrix: package: ${{ fromJSON(needs.discover.outputs.packages) }} steps: - - name: 'Prune: old -preview versions' + - name: 'Prune: old -alpha, -beta and -preview versions' uses: actions/delete-package-versions@v5 with: owner: ${{ github.repository_owner }} package-name: ${{ matrix.package }} package-type: nuget min-versions-to-keep: ${{ env.KEEP }} - # Protect every version that is NOT a per-commit preview — GA, `-rc.N`, - # and legacy `-alpha.N` all match this and are ignored. Deliberately not - # `delete-only-pre-release-versions`, which would also eat rc + alpha. - ignore-versions: '^(?!.*-preview\.).*$' + # Protect every version that is NOT a per-commit build — GA and `-rc.N` + # match this and are ignored. Deliberately not `delete-only-pre-release-versions`, + # which would also eat rc. + ignore-versions: '^(?!.*-(preview|alpha|beta)\.).*$' diff --git a/.github/workflows/publish-packages-preview.yml b/.github/workflows/publish-packages-preview.yml index bc1032c07..162f6faa0 100644 --- a/.github/workflows/publish-packages-preview.yml +++ b/.github/workflows/publish-packages-preview.yml @@ -1,20 +1,19 @@ -# Hand-written. The "preview" channel — `develop` (ADR-0009, superseding ADR-0004). +# Hand-written. The "preview" channels — `develop` and `release/*` (ADR-0011, +# superseding ADR-0009's versioning). # # Shape: -# - Trigger: push to `develop`. `develop` is the integration trunk / `-preview` -# channel — the deliberate lane where work stabilises. Nerdbank.GitVersioning -# produces a prerelease of the form `10.5.0-preview..g` -# (`develop` is intentionally NOT a public-release ref in version.json), so these -# never look like a stable release. +# - Trigger: push to `develop` or a `release/*` branch. GitVersion (GitVersion.yml) +# computes the version from the branch: `10.5.0-alpha.` on `develop`, +# `10.5.0-beta.` on `release/v10.5`. Neither looks like a stable release. +# `-rc` and GA versions only come from tags (publish-packages-release.yml). # - Builds + packs, then pushes every *.nupkg to GitHub Packages ONLY. # - Deliberately does NOT touch nuget.org and does NOT create a GitHub Release. # GitHub Packages is the test/preview channel and is opt-in for consumers # (they add the feed), so it causes none of the nuget.org Dependabot # fan-out that ADR-0001 / ADR-0002 were avoiding. # -# `develop` is the only continuous prerelease lane. ADR-0008 folded the former -# `experimental` `-alpha` lane into the trunk; ADR-0009 keeps that, just renamed -# onto `develop`. Stable and legacy releases are deliberate and tag-triggered — +# `develop` and `release/*` are the continuous prerelease lanes. Stable and +# legacy releases are deliberate and tag-triggered — # see publish-packages-release.yml. Cross-platform validation (macOS/Windows) # only runs on release intent (build-cross-platform.yml), not on `develop` # pushes — `develop` relies on the ubuntu-latest gate (build.yml) instead. @@ -26,6 +25,7 @@ on: push: branches: - develop + - 'release/**' paths-ignore: - 'docs/**' - '.assets/**' @@ -51,14 +51,14 @@ jobs: steps: - uses: actions/checkout@v7 with: - fetch-depth: 0 # Nerdbank.GitVersioning needs full history + fetch-depth: 0 # GitVersion needs full history - name: 'Cache: .fallout/temp, ~/.nuget/packages' uses: actions/cache@v6 with: path: | .fallout/temp ~/.nuget/packages - key: ${{ runner.os }}-preview-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props', 'version.json') }} + key: ${{ runner.os }}-preview-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} restore-keys: | ${{ runner.os }}-preview- - name: 'Setup: .NET SDK' diff --git a/.github/workflows/publish-packages-release.yml b/.github/workflows/publish-packages-release.yml index f6f6d7d11..44561d645 100644 --- a/.github/workflows/publish-packages-release.yml +++ b/.github/workflows/publish-packages-release.yml @@ -3,33 +3,28 @@ # NuGetApiKey parameter, which would otherwise have to share a name). # # This is the DELIBERATE (production) release path. The fast test lane — -# develop (-preview), push → GitHub Packages — lives in publish-packages-preview.yml. -# See ADR-0009 (superseding ADR-0004, and carrying forward ADR-0008's no-`experimental` -# decision onto `develop`). +# develop (-alpha) and release/* (-beta), push → GitHub Packages — lives in +# publish-packages-preview.yml. See ADR-0011 (GitVersion and tag-driven releases). # # Shape: -# - Trigger: tag push `v*` on a production branch — the classic GitFlow -# production trunk (main → tag v10.5.0), release stabilization lines -# (release/v10.5 → tag v10.5.0-rc.N), or gitflow support lines (support/v10 → -# v10.x, and eventually a retired support/v11 once a later major replaces it). -# develop is NEVER tagged for release. Tags keep the `v` prefix so the `v*` -# tag-protection ruleset applies; the package version core is the bare semver -# (e.g. 10.5.0). Tag protection restricts who can create those tags to repo -# admins. +# - Trigger: tag push `X.Y.Z[-label]` (no `v` prefix) on a production branch — +# the classic GitFlow production trunk (main → tag 10.5.0), release +# stabilization lines (release/v10.5 or release/10.5 → tag 10.5.0-rc.N), or +# gitflow support lines (support/v10). develop is NEVER tagged for release. +# Tag protection restricts who can create those tags to repo admins. +# - The tag IS the package version. It is passed to the build as ReleaseTag +# (see Build.cs); the branch does not matter for the version. # - Optional fallback: workflow_dispatch with an existing-tag input, for -# re-runs after a transient publish-API failure, OR to opt into a -# nuget.org publish (see below). +# re-runs after a transient publish-API failure. # - validate-ref → test-and-pack → fan out to three publish jobs in parallel: -# - publish-nuget-org (opt-in via workflow_dispatch flag, approval-gated) +# - publish-nuget-org (GA and -rc tags only) # - publish-github-packages (all packages — Fallout.* + Nuke.*) # - publish-github-releases (artifact bundling on the tag's GH Release) # -# Channel routing (ADR-0009): -# - nuget.org is OPT-IN. Tag pushes do NOT auto-publish there. To publish -# Fallout.* to nuget.org you invoke workflow_dispatch with -# publish-to-nugetorg=true — a conscious "this release is stabilised enough" -# switch, used when a `main` GA release is ready for the broad consumer -# audience, or for a support/v10 legacy security patch. +# Channel routing (ADR-0011): +# - nuget.org gets Fallout.* from a GA tag (10.5.0) or an -rc tag +# (10.5.0-rc.2). Any other prerelease tag (10.5.0-beta.1, ...) skips it. +# There is no approval step. # - GitHub Packages gets every Fallout.* and Nuke.* build via tag push. # - GitHub Releases bundles nupkgs on the tag's release page regardless. # @@ -45,24 +40,14 @@ name: publish-packages-release on: push: tags: - - 'v*' - # Manual fallback for two scenarios: - # 1. Re-running a partial publish after a transient API failure (pick the - # tag, leave publish-to-nugetorg at its default). - # 2. Opting into a nuget.org publish for a stabilised release (pick the - # tag, set publish-to-nugetorg to true). Still gated by the nuget-org - # env approval before the actual push. - # `--skip-duplicate` on each push makes re-runs idempotent. + - '[0-9]*.[0-9]*.[0-9]*' + # Manual fallback: re-run a partial publish after a transient API failure + # (pick the tag). `--skip-duplicate` on each push makes re-runs idempotent. workflow_dispatch: inputs: tag: - description: 'Existing tag to (re-)release (e.g. v10.5.5)' + description: 'Existing tag to (re-)release (e.g. 10.5.5)' required: true - publish-to-nugetorg: - description: 'Publish Fallout.* to nuget.org? Default false — opt-in for stabilised releases only.' - required: false - type: boolean - default: false permissions: contents: read @@ -84,7 +69,7 @@ jobs: # stabilization lines (release/v10.5, …, rc tags), and gitflow # support lines (support/v10 legacy semver, and eventually a retired # support/v11, …). - REACHABLE=$(git branch -r --contains "$TAG_SHA" | grep -E 'origin/(main|release/v[0-9]+(\.[0-9]+)?|support/v[0-9]+)$' || true) + REACHABLE=$(git branch -r --contains "$TAG_SHA" | grep -E 'origin/(main|release/v?[0-9]+(\.[0-9]+)?|support/v[0-9]+)$' || true) if [ -z "$REACHABLE" ]; then echo "::error::Tag ${GITHUB_REF_NAME} at $TAG_SHA is not reachable from any production branch." echo "Tag-triggered releases only fire from main (GA), release/vX.Y (rc), or support/* (legacy/retired) branches." @@ -99,18 +84,31 @@ jobs: needs: [validate-ref] # Run when validate-ref succeeded (tag push) OR was skipped (workflow_dispatch). if: always() && (needs.validate-ref.result == 'success' || needs.validate-ref.result == 'skipped') + outputs: + push-to-nugetorg: ${{ steps.route.outputs.push }} steps: + - name: 'Route: does this tag go to nuget.org?' + id: route + run: | + TAG="${{ inputs.tag || github.ref_name }}" + if [[ "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then + echo "push=true" >> "$GITHUB_OUTPUT" + echo "Tag $TAG goes to nuget.org." + else + echo "push=false" >> "$GITHUB_OUTPUT" + echo "Tag $TAG is neither GA nor -rc. Skipping nuget.org." + fi - uses: actions/checkout@v7 with: ref: ${{ inputs.tag || github.ref }} - fetch-depth: 0 # Nerdbank.GitVersioning needs full history + fetch-depth: 0 # GitVersion needs full history - name: 'Cache: .fallout/temp, ~/.nuget/packages' uses: actions/cache@v6 with: path: | .fallout/temp ~/.nuget/packages - key: ${{ runner.os }}-release-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props', 'version.json') }} + key: ${{ runner.os }}-release-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} restore-keys: | ${{ runner.os }}-release- - name: 'Setup: .NET SDK' @@ -122,12 +120,10 @@ jobs: - name: 'Run: Test + Pack + PackageGuard' run: dotnet fallout Test Pack PackageGuard env: - # We check out the tag above, so HEAD is detached and matches none of - # version.json's publicReleaseRefSpec entries (they're all branch refs, - # and nbgv never matches that spec against refs/tags/*). Without this, - # NB.GV treats the build as non-public and stamps a git-height suffix - # onto every package — v10.4.0-rc.3 shipped as 10.4.0-rc.3.geabd043cc2. - PublicRelease: true + # The tag is the package version. The tag checkout above leaves HEAD + # detached, where GitVersion ignores a tag whose label differs from the + # branch label, so Build.cs takes the version straight from ReleaseTag. + ReleaseTag: ${{ inputs.tag || github.ref_name }} # PackageGuard's own target (build/Build.PackageGuard.cs) is gated on either being on # a long-lived branch or running in this workflow (GitHubActions.Workflow == # "publish-packages-release") — the tag checkout above leaves HEAD detached, so the @@ -150,23 +146,10 @@ jobs: retention-days: 7 if-no-files-found: error - # Tier 1 — production / nuget.org. **OPT-IN ONLY.** Tag pushes do NOT trigger - # this job; you must invoke workflow_dispatch with publish-to-nugetorg=true. - # The env approval gate on `nuget-org` is the second layer — even after opting - # in via the input flag, you still approve the deployment. - # - # The opt-in defaults to false because the default channel is GitHub Packages - # (see header comment). When a `main` GA release is ready for the broad - # audience — or for cutting a support/v10 maintenance patch — invoke - # workflow_dispatch with the flag set. - # - # The condition below accepts the string 'true' as well as the boolean. A - # `type: boolean` input only arrives as a real boolean when the run is started - # from the Actions UI; the REST API — and therefore `gh workflow run`, which - # rejects a JSON boolean outright — can only send strings. Comparing a string - # to a boolean casts both to numbers ('true' → NaN, true → 1), so a bare - # `== true` silently skipped every CLI-triggered publish while the run still - # reported success. See the runbook's `gh workflow run` invocation. + # Tier 1 — production / nuget.org. A GA tag (10.5.0) or an -rc tag (10.5.0-rc.2) + # publishes Fallout.* here automatically. Any other prerelease tag + # (10.5.0-beta.1, ...) skips the push. There is no approval step: nuget.org + # pushes cannot be undone, so tag protection (admins only) is the gate. publish-nuget-org: name: publish → nuget.org runs-on: ubuntu-latest @@ -176,10 +159,10 @@ jobs: # propagates through the graph — test-and-pack's own always() rescues only # itself, not its dependents. Without this the whole dispatch path silently # published nothing while the run still reported success. + # Routed at job level, so a skipped tag never creates a nuget-org deployment. if: >- always() && needs.test-and-pack.result == 'success' - && github.event_name == 'workflow_dispatch' - && (inputs.publish-to-nugetorg == true || inputs.publish-to-nugetorg == 'true') + && needs.test-and-pack.outputs.push-to-nugetorg == 'true' environment: name: nuget-org url: https://www.nuget.org/profiles/Fallout.build @@ -213,8 +196,8 @@ jobs: # Tier 2 — GitHub Packages. The default release channel. Pushes both # Fallout.* AND Nuke.* transition shims. The Nuke.* shim IDs are owned by the # original NUKE maintainer on nuget.org (#47) — GitHub Packages is their - # permanent home. Fallout.* is here because nuget.org is opt-in by default - # (see header). + # permanent home. Fallout.* is here for every tag, including the ones that + # skip nuget.org (see header). publish-github-packages: name: publish → GitHub Packages runs-on: ubuntu-latest diff --git a/AGENTS.md b/AGENTS.md index 2383be1b5..607c194d8 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -19,13 +19,13 @@ One topic, one home — anything that duplicates another layer gets deleted, not ## Versioning & channels -Classic GitFlow, staying on semver `10.x` — see [ADR-0009](docs/adr/0009-gitflow-and-semver-reversion.md) (replaces the calendar-versioning [ADR-0004](docs/adr/0004-calendar-versioning-and-dual-pace-channels.md)). **GitHub Packages = test/preview/rc; nuget.org = production.** +Classic GitFlow, staying on semver `10.x` — see [ADR-0009](docs/adr/0009-gitflow-and-semver-reversion.md) (replaces the calendar-versioning [ADR-0004](docs/adr/0004-calendar-versioning-and-dual-pace-channels.md)). **GitHub Packages = every build; nuget.org = GA and `-rc` tags only.** GitVersion computes versions from the branch and tags — see [ADR-0011](docs/adr/0011-gitversion-and-tag-driven-releases.md). Tags have no `v` prefix. | Branch | Role | Publishes | | --- | --- | --- | -| `develop` | Integration trunk, default branch, sole preview lane. All work lands here, including breaking work gated behind `[Experimental]`. | `-preview` → GitHub Packages only | -| `release/vX.Y` | Stabilizes the next release; cut from `develop` on demand, not ahead of time. Non-breaking fixes only after the cut. | `-rc.N` → GitHub Packages | -| `main` | Production trunk; takes only `release/vX.Y` (GA) or `hotfix/vX.Y.Z` merges. | GA tags → nuget.org (opt-in) + GitHub Packages + Releases | +| `develop` | Integration trunk, default branch, sole preview lane. All work lands here, including breaking work gated behind `[Experimental]`. | `-alpha.N` → GitHub Packages only | +| `release/vX.Y` | Stabilizes the next release; cut from `develop` on demand, not ahead of time. Non-breaking fixes only after the cut. | `-beta.N` → GitHub Packages. `-rc.N` tags → nuget.org + GitHub Packages | +| `main` | Production trunk; takes only `release/vX.Y` (GA) or `hotfix/vX.Y.Z` merges. | GA tags → nuget.org + GitHub Packages + Releases | | `support/v10` (+ `hotfix/v10.x`) | Legacy line for versions before `10.4`. Security/critical fixes only. | Tags, same channels as `main` | `MAJOR` stays `10` until a breaking change is actually needed — there's no fixed date for v11, and it's deferred as long as possible. Not-yet-stable public APIs ship behind `[Experimental("FALLOUT0xx")]` on any channel — see the `marking-experimental-apis` skill. Full model: [docs/branching-and-release.md](docs/branching-and-release.md). diff --git a/Directory.Build.props b/Directory.Build.props index 7365dbf02..e6605d252 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -50,7 +50,6 @@ - diff --git a/Directory.Build.targets b/Directory.Build.targets index 8dadf10e0..cb4f5cd8a 100644 --- a/Directory.Build.targets +++ b/Directory.Build.targets @@ -38,22 +38,4 @@ - - - - - - - - - - diff --git a/Directory.Packages.props b/Directory.Packages.props index 43f014fd0..0ac7f8099 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -99,7 +99,6 @@ - diff --git a/GitVersion.yml b/GitVersion.yml new file mode 100644 index 000000000..ec276fb18 --- /dev/null +++ b/GitVersion.yml @@ -0,0 +1,14 @@ +workflow: GitFlow/v1 +commit-message-incrementing: Disabled +# Major.Minor.0.0, so a patch release keeps the assembly version (as Nerdbank.GitVersioning did). +assembly-versioning-scheme: MajorMinor +branches: + release: + prevent-increment: + when-current-commit-tagged: true + pull-request: + regex: ((pull|pull\-requests|pr)[/-]|[/-](merge)) + label: pr + hotfix: + prevent-increment: + when-current-commit-tagged: true diff --git a/build/Build.CI.GitHubActions.cs b/build/Build.CI.GitHubActions.cs index be01442dc..4b4c634b3 100644 --- a/build/Build.CI.GitHubActions.cs +++ b/build/Build.CI.GitHubActions.cs @@ -28,7 +28,7 @@ // Cross-platform full Test+Pack is gated to RELEASE // INTENT (#318/#326): it runs only on a PR into a // production branch (main, release/*, support/*) and on -// a release tag push (v*) — never on routine pushes/PRs to +// a release tag push (X.Y.Z*) — never on routine pushes/PRs to // develop. On develop "we've got our edge": the // ubuntu-latest gate above + the preview pipeline // (.github/workflows/publish-packages-preview.yml). @@ -81,7 +81,7 @@ ConcurrencyCancelInProgress = true, OnPushTags = new[] { - "v*" + "[0-9]*.[0-9]*.[0-9]*" }, // main is the production trunk now (ADR-0009). A PR into it is always a // release/vX.Y GA merge or a hotfix, so it belongs on this release-intent gate, diff --git a/build/Build.cs b/build/Build.cs index fac62d358..3c9fa83f8 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -12,9 +12,11 @@ using Fallout.Common.Tooling; using Fallout.Common.Tools.DotNet; using Fallout.Common.Tools.GitHub; +using Fallout.Common.Tools.GitVersion; using Fallout.Common.Utilities; using Fallout.Components; using Fallout.Solutions; +using NuGet.Versioning; using Octokit; using static Fallout.Common.ControlFlow; using static Fallout.Common.Tools.DotNet.DotNetTasks; @@ -26,6 +28,7 @@ partial class Build : FalloutBuild, IHasChangelog, IHasGitRepository, + IHasGitVersion, IHasSolution, IRestore, ICompile, @@ -82,21 +85,52 @@ partial class Build // Same protected-branch validation reason as ReleaseBranchPattern. const string SupportBranchPattern = "support/*"; - // Trunk-based versioning. Nerdbank.GitVersioning computes the canonical version - // (major.minor from version.json + git-height as patch). ThisAssembly is generated by - // NB.GV at compile time; AssemblyInformationalVersion looks like "10.3.5+abc1234" so we - // strip the "+commitsha" tail. #81. + // GitVersion computes the canonical version from the branch and tags (GitVersion.yml): + // develop is -alpha.N, release/* is -beta.N, and a tag is exactly that version. + // The tool comes from the GitVersion.Tool PackageDownload in _build.csproj. + [GitVersion(Framework = "net10.0", NoFetch = true, NoCache = true)] + readonly GitVersion GitVersion; + + // The release workflow passes the tag it builds (tag push or re-run). A tag is the + // version. GitVersion cannot return it on a detached checkout: it ignores a tag whose + // label (e.g. -rc) differs from the branch label, so 10.5.0-rc.1 would build as + // 10.5.0--no-branch-.1. [Parameter] - readonly bool Major; + readonly string ReleaseTag; - string MajorMinorPatchVersion => Major - ? $"{ParseMajor(ThisAssembly.AssemblyInformationalVersion) + 1}.0.0" - : ThisAssembly.AssemblyInformationalVersion.Split('+')[0]; + // GitVersion 6 no longer outputs NuGetVersionV2, which IHasGitVersion uses for packing. + GitVersion IHasGitVersion.Versioning => ReleaseTag.IsNullOrWhiteSpace() + ? GitVersion with { NuGetVersionV2 = GitVersion.SemVer } + : WithVersionFromTag(GitVersion, ReleaseTag); - string MilestoneTitle => $"v{MajorMinorPatchVersion}"; + // Sets only the fields that ICompile, IPack and this build read. + static GitVersion WithVersionFromTag(GitVersion version, string tag) + { + Assert.True(SemanticVersion.TryParse(tag, out var tagVersion), + $"Tag '{tag}' is not a semantic version like 10.5.0 or 10.5.0-rc.1"); + + var semVer = tagVersion.ToNormalizedString(); + var majorMinorPatch = $"{tagVersion.Major}.{tagVersion.Minor}.{tagVersion.Patch}"; - static int ParseMajor(string informationalVersion) - => int.Parse(informationalVersion.Split('.')[0]); + return version with + { + Major = tagVersion.Major, + MajorMinorPatch = majorMinorPatch, + FullSemVer = semVer, + NuGetVersionV2 = semVer, + InformationalVersion = $"{semVer}+{version.ShortSha}", + // Matches assembly-versioning-scheme: MajorMinor in GitVersion.yml. + AssemblySemVer = $"{tagVersion.Major}.{tagVersion.Minor}.0.0", + AssemblySemFileVer = $"{majorMinorPatch}.0", + }; + } + + [Parameter] + readonly bool Major; + + string MajorMinorPatchVersion => Major + ? $"{From().Versioning.Major + 1}.0.0" + : From().Versioning.MajorMinorPatch; AbsolutePath IHasArtifacts.ArtifactsDirectory => RootDirectory / "output"; @@ -108,12 +142,14 @@ static int ParseMajor(string informationalVersion) OutputDirectory.CreateOrCleanDirectory(); }); + // On a server build the version properties must stay: the release workflow runs Test Pack + // without Publish, and the shipped assemblies must carry the GitVersion version. Configure ICompile.CompileSettings => _ => _ - .When(!ScheduledTargets.Contains(((IPublish)this).Publish) && !ScheduledTargets.Contains(Install), _ => _ + .When(!IsServerBuild && !ScheduledTargets.Contains(((IPublish)this).Publish) && !ScheduledTargets.Contains(Install), _ => _ .ClearProperties()); Configure ICompile.PublishSettings => _ => _ - .When(!ScheduledTargets.Contains(((IPublish)this).Publish) && !ScheduledTargets.Contains(Install), _ => _ + .When(!IsServerBuild && !ScheduledTargets.Contains(((IPublish)this).Publish) && !ScheduledTargets.Contains(Install), _ => _ .ClearProperties()); IEnumerable<(Project Project, string Framework)> ICompile.PublishConfigurations => @@ -139,8 +175,7 @@ from framework in project.GetTargetFrameworks() bool IReportCoverage.ReportToCodecov => false; // Local Terminal runs use a placeholder version so packed nupkgs don't - // collide with real releases. CI runs let Nerdbank.GitVersioning inject the - // real version via MSBuild ($PackageVersion). + // collide with real releases. CI runs use the GitVersion version (IHasGitVersion). Configure IPack.PackSettings => _ => _ .When(Host is Terminal, _ => _ .SetVersion(DefaultDeploymentVersion)); diff --git a/build/_build.csproj b/build/_build.csproj index 08da3edd1..72fda8672 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -1,4 +1,4 @@ - + @@ -54,10 +54,9 @@ - - + +