From 22d616f608899b654041739a2eee2cebbe2c8a4f Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Mon, 25 May 2026 17:08:49 +1200 Subject: [PATCH] feat(ci): teach [GitHubActions] about CheckoutRef so regen preserves the ref pin MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The ubuntu-latest workflow YAML carried a manual ref:github.head_ref patch that keeps HEAD attached on PR-triggered builds. Without it, GitHubTasksTest.GitHubRepositoryFromLocalDirectoryTest fails because GitRepository.FromLocalDirectory can't resolve a branch from a detached merge SHA (seen on PR #172 when GenerateTools clobbered the patch and CI went red). Fix: surface the actions/checkout 'ref' input as first-class config on GitHubActionsCheckoutStep + a CheckoutRef setter on [GitHubActions]. The ubuntu-latest attribute in Build.CI.GitHubActions.cs now sets CheckoutRef = "${{ github.head_ref }}", so subsequent GenerateTools runs re-emit the patch automatically instead of dropping it. macos-latest and windows-latest are unchanged — they run on push-to-main where HEAD is already attached on the requested commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/ubuntu-latest.yml | 4 ---- build/Build.CI.GitHubActions.cs | 5 +++++ .../Configuration/GitHubActionsCheckoutStep.cs | 14 +++++++++++++- .../CI/GitHubActions/GitHubActionsAttribute.cs | 15 ++++++++++++++- 4 files changed, 32 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ubuntu-latest.yml b/.github/workflows/ubuntu-latest.yml index 7aaa4355a..dfd3e733f 100644 --- a/.github/workflows/ubuntu-latest.yml +++ b/.github/workflows/ubuntu-latest.yml @@ -34,10 +34,6 @@ jobs: with: submodules: recursive fetch-depth: 0 - # Check out the source branch (not the merge SHA) so HEAD stays - # attached. GitHubTasksTest.GitHubRepositoryFromLocalDirectoryTest - # requires repository.Branch to be non-null. NOTE: this file is - # auto-generated by Fallout; next regen will overwrite. Track in #110. ref: ${{ github.head_ref }} - name: 'Cache: .fallout/temp, ~/.nuget/packages' uses: actions/cache@v4 diff --git a/build/Build.CI.GitHubActions.cs b/build/Build.CI.GitHubActions.cs index a27ebb397..67ee7df96 100644 --- a/build/Build.CI.GitHubActions.cs +++ b/build/Build.CI.GitHubActions.cs @@ -27,11 +27,16 @@ 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. [GitHubActions( "ubuntu-latest", GitHubActionsImage.UbuntuLatest, FetchDepth = 0, Submodules = GitHubActionsSubmodules.Recursive, + CheckoutRef = "${{ github.head_ref }}", OnPullRequestBranches = new[] { MainBranch }, OnPullRequestExcludePaths = new[] { "docs/**", ".assets/**", "**/*.md" }, InvokedTargets = new[] { nameof(ITest.Test), nameof(IPack.Pack) }, diff --git a/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs b/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs index 67d95dfb1..78cea71ff 100644 --- a/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs +++ b/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs @@ -17,11 +17,21 @@ public class GitHubActionsCheckoutStep : GitHubActionsStep public bool? Progress { get; set; } public string Filter { get; set; } + /// + /// The git ref to check out. When unset, actions/checkout picks the default for the event + /// (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. + /// + public string Ref { get; set; } + public override void Write(CustomFileWriter writer) { writer.WriteLine("- uses: actions/checkout@v6"); - if (Submodules.HasValue || Lfs.HasValue || FetchDepth.HasValue || Progress.HasValue || !Filter.IsNullOrWhiteSpace()) + if (Submodules.HasValue || Lfs.HasValue || FetchDepth.HasValue || Progress.HasValue || + !Filter.IsNullOrWhiteSpace() || !Ref.IsNullOrWhiteSpace()) { using (writer.Indent()) { @@ -38,6 +48,8 @@ public override void Write(CustomFileWriter writer) writer.WriteLine($"progress: {Progress.ToString().ToLowerInvariant()}"); if (!Filter.IsNullOrWhiteSpace()) writer.WriteLine($"filter: {Filter}"); + if (!Ref.IsNullOrWhiteSpace()) + writer.WriteLine($"ref: {Ref}"); } } } diff --git a/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs b/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs index aac79378f..195ed005a 100644 --- a/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs +++ b/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs @@ -29,6 +29,7 @@ public class GitHubActionsAttribute : ConfigurationAttributeBase private uint? _fetchDepth; private bool? _progress; private string _filter; + private string _ref; public GitHubActionsAttribute( string name, @@ -117,6 +118,17 @@ public string Filter get => throw new NotSupportedException(); } + /// + /// 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). + /// + public string CheckoutRef + { + set => _ref = value; + get => throw new NotSupportedException(); + } + public override CustomFileWriter CreateWriter(StreamWriter streamWriter) { return new CustomFileWriter(streamWriter, indentationFactor: 2, commentPrefix: "#"); @@ -167,7 +179,8 @@ private IEnumerable GetSteps(GitHubActionsImage image, IReadO Lfs = _lfs, FetchDepth = _fetchDepth, Progress = _progress, - Filter = _filter + Filter = _filter, + Ref = _ref }; if (CacheKeyFiles.Any())