From d2dad483e365d79822705e6c1de7135008e960c3 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Wed, 27 May 2026 22:56:22 +1200 Subject: [PATCH] fix(ci): [GitHubActions] CheckoutRef now resolves cross-repo PRs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Regression introduced by #175. The generated workflow emitted: - uses: actions/checkout@v6 with: ref: ${{ github.head_ref }} …but didn't set `repository:`. Checkout defaulted to `${{ github.repository }}` (this repo) and tried to resolve `fix/whatever` as a local branch — which fails for fork PRs because the source branch only exists on the contributor's fork. Symptom: cross-repo PRs (e.g. Dennis Doomen's #222 from his fork) fail at the `Run actions/checkout@v6` step with: fatal: A branch or tag with the name 'fix/...' could not be found #175's `CheckoutRef = "${{ github.head_ref }}"` was added so GitVersion / GitRepository.FromLocalDirectory see the source branch in `.git/HEAD` (otherwise checkout puts you on the merge ref in detached-HEAD state). Goal correct; cross-repo coverage missed. Fix: - GitHubActionsCheckoutStep.Write now emits a `repository:` line whenever `Ref` is non-empty: repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} The `||` fallback covers push events (no pull_request context). - Doc comments on CheckoutRef + Ref updated. - New regression test case `checkout-ref` in ConfigurationGenerationTest locks the YAML shape. Verified snapshot committed alongside. - .github/workflows/ubuntu-latest.yml regenerated by the build. macos-latest / windows-latest / release.yml unchanged (none set CheckoutRef). - CHANGELOG.md entry under [Unreleased] — 11.0. After this lands, Dennis's PR #222 will pick up the fix on the next workflow run (any rerun, or a push to his branch). Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ubuntu-latest.yml | 1 + CHANGELOG.md | 1 + .../GitHubActionsCheckoutStep.cs | 14 ++++- .../GitHubActions/GitHubActionsAttribute.cs | 5 ++ ...ribute=GitHubActionsAttribute.verified.txt | 62 +++++++++++++++++++ .../CI/ConfigurationGenerationTest.cs | 16 +++++ 6 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=checkout-ref_attribute=GitHubActionsAttribute.verified.txt diff --git a/.github/workflows/ubuntu-latest.yml b/.github/workflows/ubuntu-latest.yml index 2e25f0563..73c0e46c4 100644 --- a/.github/workflows/ubuntu-latest.yml +++ b/.github/workflows/ubuntu-latest.yml @@ -34,6 +34,7 @@ jobs: with: submodules: recursive fetch-depth: 0 + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} ref: ${{ github.head_ref }} - name: 'Cache: .fallout/temp, ~/.nuget/packages' uses: actions/cache@v4 diff --git a/CHANGELOG.md b/CHANGELOG.md index cc806f1b7..313e41e4e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -61,6 +61,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - **Retired `GitVersion.Tool`** (#81) — version is now sourced exclusively from `Nerdbank.GitVersioning` via `version.json`. `MajorMinorPatchVersion` and friends in `Build.cs` read NB.GV's `$(Version)` MSBuild output. - **Trimmed unused / replaceable dependencies** (#75, #78, #79, #80, #82): dropped `JetBrains.ReSharper.GlobalTools`, the matkoch Spectre.Console fork (swapped for upstream `Spectre.Console`), `Microsoft.ApplicationInsights`, `Codecov.Tool`, and `xunit.runner.console`. See `docs/dependencies.md` for the current graph. - **Bumped Scriban 7.1.0 → 7.2.0** (#84) to clear NU1903. +- **Fixed `[GitHubActions]` `CheckoutRef` breaking cross-repo / fork PRs**. The generator emitted `ref: ${{ github.head_ref }}` but didn't set `repository:`, so the default `${{ github.repository }}` was used. For PRs from a fork, the source branch only exists on the fork; checkout tried to resolve it on origin and failed with "branch or tag could not be found" (regressed in #175). Generator now also emits `repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}` whenever `CheckoutRef` is set — works for fork PRs, same-repo PRs, AND push events (the `||` fallback). Regenerate via any `dotnet fallout ` to pick up the fix. - **Fixed `fallout-migrate` producing unrestorable `_build.csproj`** (closes #217). The Nuke → Fallout namespace rewrite carried the original NUKE `Version="10.1.0"` pins onto `Fallout.*` packages — but `Fallout.*` was never published at `10.1.0`, so NuGet hit NU1603 ("not found, falling back to next-higher") and `WarningsAsErrors` in the migrated project escalated to fatal. Migrate now bumps inline `Version="..."` attributes on Nuke → Fallout PackageReferences to the running migrate tool's own version in the same regex pass, and strips the stale `System.Security.Cryptography.Xml` `` (NUKE-era projects often pinned an older major that conflicts with `Fallout.Common`'s transitive ≥ 10.0.6 — NU1605 downgrade). CPM-managed references (no inline Version) keep namespace-only rewrite — version stays in Directory.Packages.props. ## [10.2.0] / 2026-05-21 diff --git a/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs b/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs index 78cea71ff..c40f737a8 100644 --- a/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs +++ b/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs @@ -22,7 +22,10 @@ public class GitHubActionsCheckoutStep : GitHubActionsStep /// (the merge SHA on pull_request triggers, which leaves HEAD detached). Set to /// ${{ github.head_ref }} on PR workflows that read .git/HEAD directly /// (e.g. ) so the branch - /// resolves correctly. + /// resolves correctly. When set, the generator also emits a repository: line that + /// resolves to the PR head's repo for fork PRs and falls back to the current repo for push + /// events — without it, fork PRs fail with "branch or tag could not be found" because the + /// branch only exists on the fork, not on origin. /// public string Ref { get; set; } @@ -49,7 +52,16 @@ public override void Write(CustomFileWriter writer) if (!Filter.IsNullOrWhiteSpace()) writer.WriteLine($"filter: {Filter}"); if (!Ref.IsNullOrWhiteSpace()) + { + // Pin checkout to the source repo of the PR head (or the current repo on + // push events). Required when `ref:` is set to `${{ github.head_ref }}` — + // for cross-repo PRs, that branch only exists on the fork, not on origin, + // and checkout's default `repository: ${{ github.repository }}` would fail + // to resolve it. The `||` fallback handles push events where there's no + // pull_request context. + writer.WriteLine("repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}"); writer.WriteLine($"ref: {Ref}"); + } } } } diff --git a/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs b/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs index 25bc1341d..f8e57a623 100644 --- a/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs +++ b/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs @@ -122,6 +122,11 @@ public string Filter /// Forwarded to actions/checkout's ref input. Set to /// ${{ github.head_ref }} on PR-triggered workflows that need .git/HEAD /// to stay attached (e.g. ones that read the current branch via GitRepository). + /// + /// When set, the generator also emits repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + /// so cross-repo PRs (from forks) resolve the branch name on the fork instead of failing + /// on origin. The fallback to github.repository keeps push-triggered workflows + /// (where there's no PR context) working unchanged. /// public string CheckoutRef { diff --git a/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=checkout-ref_attribute=GitHubActionsAttribute.verified.txt b/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=checkout-ref_attribute=GitHubActionsAttribute.verified.txt new file mode 100644 index 000000000..dc14198ea --- /dev/null +++ b/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=checkout-ref_attribute=GitHubActionsAttribute.verified.txt @@ -0,0 +1,62 @@ +# ------------------------------------------------------------------------------ +# +# +# This code was generated. +# +# - To turn off auto-generation set: +# +# [TestGitHubActions (AutoGenerate = false)] +# +# - To trigger manual generation invoke: +# +# fallout --generate-configuration GitHubActions_test --host GitHubActions +# +# +# ------------------------------------------------------------------------------ + +name: test + +on: + pull_request: + branches: + - main + +jobs: + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }} + ref: ${{ github.head_ref }} + - name: 'Cache: .fallout/temp, ~/.nuget/packages' + uses: actions/cache@v4 + with: + path: | + .fallout/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - 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: Test' + run: dotnet fallout Test + - name: 'Publish: src' + uses: actions/upload-artifact@v5 + with: + name: src + path: src + - name: 'Publish: test-results' + uses: actions/upload-artifact@v5 + with: + name: test-results + path: output/test-results + - name: 'Publish: coverage-report.zip' + uses: actions/upload-artifact@v5 + with: + name: coverage-report.zip + path: output/coverage-report.zip diff --git a/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.cs b/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.cs index 20d59afc4..b4658e6f2 100644 --- a/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.cs +++ b/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.cs @@ -168,6 +168,22 @@ public class TestBuild : FalloutBuild } ); + // Regression guard: when CheckoutRef is set, the generator must also emit a + // repository: line that resolves to the PR's head repo (for cross-repo / fork PRs) + // with a fallback to the current repo (for push events). Without this, fork PRs + // fail at the checkout step because the branch only exists on the fork, not on + // origin. + yield return + ( + "checkout-ref", + new TestGitHubActionsAttribute(GitHubActionsImage.UbuntuLatest) + { + InvokedTargets = new[] { nameof(Test) }, + OnPullRequestBranches = new[] { "main" }, + CheckoutRef = "${{ github.head_ref }}" + } + ); + yield return ( null,