diff --git a/docs/website/05-cicd/github-actions.md b/docs/website/05-cicd/github-actions.md index ec80f02ea..0ceaacc15 100644 --- a/docs/website/05-cicd/github-actions.md +++ b/docs/website/05-cicd/github-actions.md @@ -263,3 +263,14 @@ CheckoutAction = "releases/v1" // error: ambiguous, names neither form :::note `actions/cache@v5` and later run on the node24 runtime and require a self-hosted runner of at least `2.327.1`. Set `CacheAction = "v4"` if your runners are older. ::: + +### Pinning the .NET SDK + +By default the setup step reads the SDK from `global.json` at the repo root (`global-json-file: global.json`). Set `SetupDotNetVersions` to emit `dotnet-version` instead. That covers a `global.json` kept in a subfolder, and pinning an exact SDK stops self-hosted runners that share one dotnet directory from all installing each new patch at the same time. One value is written inline, several as a `|` block; `global-json-file` is dropped. Left empty, the output is unchanged. + +```csharp title="Build.cs" +[GitHubActions( + // ... + SetupDotNetVersions = new[] { "8.0.x", "10.0.103" })] +class Build : FalloutBuild { /* ... */ } +``` diff --git a/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsRunStep.cs b/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsRunStep.cs index 28a4b48da..1fb42b989 100644 --- a/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsRunStep.cs +++ b/src/Fallout.Common/CI/GitHubActions/Configuration/GitHubActionsRunStep.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using System.Linq; using Fallout.Common.Utilities; using Fallout.Common.Utilities.Collections; @@ -19,6 +20,13 @@ public string SetupDotNetAction GitHubActionsDefaults.SetupDotNetAction, value, $"{nameof(GitHubActionsRunStep)}.{nameof(SetupDotNetAction)}"); } = GitHubActionsDefaults.SetupDotNetAction; + /// + /// SDK versions to install, emitted as dotnet-version in place of global-json-file. One value + /// is written inline, several as a | block. Null, empty, or only blank entries keep + /// global-json-file: global.json. + /// + public string[] SetupDotNetVersions { get; set; } + public string[] InvokedTargets { get; set; } public Dictionary Imports { get; set; } @@ -32,7 +40,21 @@ public override void Write(CustomFileWriter writer) writer.WriteLine("with:"); using (writer.Indent()) { - writer.WriteLine("global-json-file: global.json"); + var versions = (SetupDotNetVersions ?? []) + .Where(x => !string.IsNullOrWhiteSpace(x)) + .Select(x => x.Trim()) + .ToArray(); + + if (versions.Length == 0) + writer.WriteLine("global-json-file: global.json"); + else if (versions.Length == 1) + writer.WriteLine($"dotnet-version: {versions[0]}"); + else + { + writer.WriteLine("dotnet-version: |"); + using (writer.Indent()) + versions.ForEach(x => writer.WriteLine(x)); + } } } diff --git a/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs b/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs index f9ce74091..2452263e3 100644 --- a/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs +++ b/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs @@ -251,6 +251,14 @@ public string CheckoutRef /// public string SetupDotNetAction { get; set; } = GitHubActionsDefaults.SetupDotNetAction; + /// + /// SDK versions the generated actions/setup-dotnet step installs, emitted as dotnet-version + /// (one value inline, several as a | block). Use it when global.json isn't at the repo root, + /// or to pin an exact SDK so runners sharing one dotnet directory don't all install each new patch at once. + /// Empty (the default) keeps global-json-file: global.json. + /// + public string[] SetupDotNetVersions { get; set; } = []; + /// /// The actions/upload-artifact reference the generated workflow uses; same forms as /// . @@ -354,6 +362,7 @@ private GitHubActionsStep[] GetSteps(IReadOnlyCollection relev var run = new GitHubActionsRunStep { SetupDotNetAction = SetupDotNetAction, + SetupDotNetVersions = SetupDotNetVersions, InvokedTargets = InvokedTargets, Imports = GetImports().ToDictionary(x => x.Key, x => x.Value) }; diff --git a/tests/Fallout.Common.Specs/CI/ConfigurationGenerationSpecs.Test_testName=setup-dotnet-version_attribute=GitHubActionsAttribute.verified.txt b/tests/Fallout.Common.Specs/CI/ConfigurationGenerationSpecs.Test_testName=setup-dotnet-version_attribute=GitHubActionsAttribute.verified.txt new file mode 100644 index 000000000..765c94703 --- /dev/null +++ b/tests/Fallout.Common.Specs/CI/ConfigurationGenerationSpecs.Test_testName=setup-dotnet-version_attribute=GitHubActionsAttribute.verified.txt @@ -0,0 +1,56 @@ +# ------------------------------------------------------------------------------ +# +# +# 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@v7 + - name: 'Cache: .fallout/temp, ~/.nuget/packages' + uses: actions/cache@v6 + with: + path: | + .fallout/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Setup: .NET SDK' + uses: actions/setup-dotnet@v6 + with: + dotnet-version: 10.0.103 + - name: 'Restore: dotnet tools' + run: dotnet tool restore + - name: 'Run: Test' + run: dotnet fallout Test + - name: 'Publish: src' + uses: actions/upload-artifact@v7 + with: + name: src + path: src + - name: 'Publish: test-results' + uses: actions/upload-artifact@v7 + with: + name: test-results + path: output/test-results + - name: 'Publish: coverage-report.zip' + uses: actions/upload-artifact@v7 + with: + name: coverage-report.zip + path: output/coverage-report.zip diff --git a/tests/Fallout.Common.Specs/CI/ConfigurationGenerationSpecs.Test_testName=setup-dotnet-versions_attribute=GitHubActionsAttribute.verified.txt b/tests/Fallout.Common.Specs/CI/ConfigurationGenerationSpecs.Test_testName=setup-dotnet-versions_attribute=GitHubActionsAttribute.verified.txt new file mode 100644 index 000000000..f0d545051 --- /dev/null +++ b/tests/Fallout.Common.Specs/CI/ConfigurationGenerationSpecs.Test_testName=setup-dotnet-versions_attribute=GitHubActionsAttribute.verified.txt @@ -0,0 +1,58 @@ +# ------------------------------------------------------------------------------ +# +# +# 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@v7 + - name: 'Cache: .fallout/temp, ~/.nuget/packages' + uses: actions/cache@v6 + with: + path: | + .fallout/temp + ~/.nuget/packages + key: ${{ runner.os }}-${{ hashFiles('**/global.json', '**/*.csproj', '**/Directory.Packages.props') }} + - name: 'Setup: .NET SDK' + uses: actions/setup-dotnet@v6 + with: + dotnet-version: | + 8.0.x + 10.0.103 + - name: 'Restore: dotnet tools' + run: dotnet tool restore + - name: 'Run: Test' + run: dotnet fallout Test + - name: 'Publish: src' + uses: actions/upload-artifact@v7 + with: + name: src + path: src + - name: 'Publish: test-results' + uses: actions/upload-artifact@v7 + with: + name: test-results + path: output/test-results + - name: 'Publish: coverage-report.zip' + uses: actions/upload-artifact@v7 + with: + name: coverage-report.zip + path: output/coverage-report.zip diff --git a/tests/Fallout.Common.Specs/CI/ConfigurationGenerationSpecs.cs b/tests/Fallout.Common.Specs/CI/ConfigurationGenerationSpecs.cs index 6100d5a76..4ba326106 100644 --- a/tests/Fallout.Common.Specs/CI/ConfigurationGenerationSpecs.cs +++ b/tests/Fallout.Common.Specs/CI/ConfigurationGenerationSpecs.cs @@ -612,6 +612,29 @@ public class TestBuild : FalloutBuild } ); + // Pinned SDK versions replace global-json-file; one is written inline, several as a block. + yield return + ( + "setup-dotnet-version", + new TestGitHubActionsAttribute(GitHubActionsImage.UbuntuLatest) + { + On = new[] { GitHubActionsTrigger.Push }, + InvokedTargets = new[] { nameof(Test) }, + SetupDotNetVersions = new[] { "10.0.103" } + } + ); + + yield return + ( + "setup-dotnet-versions", + new TestGitHubActionsAttribute(GitHubActionsImage.UbuntuLatest) + { + On = new[] { GitHubActionsTrigger.Push }, + InvokedTargets = new[] { nameof(Test) }, + SetupDotNetVersions = new[] { "8.0.x", "10.0.103" } + } + ); + // Every emitted action is overridable, in each accepted form: a bare ref appended to the default // action name, a complete reference, a SHA pin carrying its version as a trailing comment, and a // fork. Pack is invoked because it produces artifacts, so the upload step actually renders.