diff --git a/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs b/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs index 2f2cdb62..6c7c06e6 100644 --- a/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs +++ b/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsCheckoutStep.cs @@ -24,12 +24,14 @@ public class GitHubActionsCheckoutStep : GitHubActionsStep /// public string Ref { get; set; } + public string[] CheckoutWith { get; set; } = new string[0]; + public override void Write(CustomFileWriter writer) { writer.WriteLine("- uses: actions/checkout@v6"); if (Submodules.HasValue || Lfs.HasValue || FetchDepth.HasValue || Progress.HasValue || - !Filter.IsNullOrWhiteSpace() || !Ref.IsNullOrWhiteSpace()) + !Filter.IsNullOrWhiteSpace() || !Ref.IsNullOrWhiteSpace() || CheckoutWith.Length > 0) { using (writer.Indent()) { @@ -57,6 +59,9 @@ public override void Write(CustomFileWriter writer) writer.WriteLine("repository: ${{ github.event.pull_request.head.repo.full_name || github.repository }}"); writer.WriteLine($"ref: {Ref}"); } + + foreach (var line in CheckoutWith) + writer.WriteLine(line); } } } diff --git a/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs b/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs index 262a7b9b..c9c48d9f 100644 --- a/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs +++ b/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs @@ -139,6 +139,19 @@ public string CheckoutRef get => throw new NotSupportedException(); } + /// + /// Extra actions/checkout inputs, emitted verbatim inside the step's with: block after + /// the typed keys (submodules, lfs, fetch-depth, progress, filter, + /// ref/repository). An escape hatch for inputs the typed knobs don't cover — token, + /// ssh-key, path, clean, persist-credentials, sparse-checkout, + /// set-safe-directory. + /// + /// Each entry is one raw line — passed through unvalidated, so the caller owns correct YAML. Multi-line + /// block scalars work by supplying the key (e.g. sparse-checkout: |) and each continuation line + /// as separate entries, with the caller's own indentation preserved. Empty emits nothing. + /// + public string[] CheckoutWith { get; set; } = new string[0]; + public override CustomFileWriter CreateWriter(StreamWriter streamWriter) { return new CustomFileWriter(streamWriter, indentationFactor: 2, commentPrefix: "#"); @@ -204,7 +217,8 @@ private IEnumerable GetSteps(IReadOnlyCollection +# +# 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: [push] + +jobs: + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + path: src + - 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.Test_testName=checkout-with-sparse_attribute=GitHubActionsAttribute.verified.txt b/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=checkout-with-sparse_attribute=GitHubActionsAttribute.verified.txt new file mode 100644 index 00000000..f6d1be72 --- /dev/null +++ b/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=checkout-with-sparse_attribute=GitHubActionsAttribute.verified.txt @@ -0,0 +1,60 @@ +# ------------------------------------------------------------------------------ +# +# +# 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: [push] + +jobs: + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + sparse-checkout: | + src + build + - 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.Test_testName=checkout-with_attribute=GitHubActionsAttribute.verified.txt b/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=checkout-with_attribute=GitHubActionsAttribute.verified.txt new file mode 100644 index 00000000..1a694dd6 --- /dev/null +++ b/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.Test_testName=checkout-with_attribute=GitHubActionsAttribute.verified.txt @@ -0,0 +1,61 @@ +# ------------------------------------------------------------------------------ +# +# +# 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: [push] + +jobs: + ubuntu-latest: + name: ubuntu-latest + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + token: ${{ secrets.CI_PAT }} + path: src + persist-credentials: false + - 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 bbbe05d5..f8930cc3 100644 --- a/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.cs +++ b/tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.cs @@ -212,6 +212,55 @@ public class TestBuild : FalloutBuild } ); + // Ordering guard: extra CheckoutWith inputs emit verbatim inside the with: block, after + // every typed key (here fetch-depth) and in the order supplied. + yield return + ( + "checkout-with", + new TestGitHubActionsAttribute(GitHubActionsImage.UbuntuLatest) + { + On = new[] { GitHubActionsTrigger.Push }, + InvokedTargets = new[] { nameof(Test) }, + FetchDepth = 0, + CheckoutWith = new[] + { + "token: ${{ secrets.CI_PAT }}", + "path: src", + "persist-credentials: false" + } + } + ); + + // Guard: CheckoutWith with no typed checkout key must still open the with: block. + yield return + ( + "checkout-with-only", + new TestGitHubActionsAttribute(GitHubActionsImage.UbuntuLatest) + { + On = new[] { GitHubActionsTrigger.Push }, + InvokedTargets = new[] { nameof(Test) }, + CheckoutWith = new[] { "path: src" } + } + ); + + // Multi-line block scalar: the case a 'KEY: value' validator would reject. Proves raw + // verbatim emission preserves the caller-supplied continuation lines and indentation. + yield return + ( + "checkout-with-sparse", + new TestGitHubActionsAttribute(GitHubActionsImage.UbuntuLatest) + { + On = new[] { GitHubActionsTrigger.Push }, + InvokedTargets = new[] { nameof(Test) }, + CheckoutWith = new[] + { + "sparse-checkout: |", + " src", + " build" + } + } + ); + yield return ( null,