Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class GitHubActionsJob : ConfigurationEntity
{
public string Name { get; set; }
public GitHubActionsImage Image { get; set; }
public string[] RunsOnLabels { get; set; } = new string[0];
public int TimeoutMinutes { get; set; }
public string ConcurrencyGroup { get; set; }
public string EnvironmentName { get; set; }
Expand All @@ -24,7 +25,14 @@ public override void Write(CustomFileWriter writer)
using (writer.Indent())
{
writer.WriteLine($"name: {Name}");
writer.WriteLine($"runs-on: {Image.GetValue()}");
if (RunsOnLabels.Length > 0)
{
writer.WriteLine($"runs-on: [{RunsOnLabels.JoinCommaSpace()}]");
}
else
{
writer.WriteLine($"runs-on: {Image.GetValue()}");
}

if (TimeoutMinutes > 0)
{
Expand Down
15 changes: 15 additions & 0 deletions src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ public GitHubActionsAttribute(

public string[] InvokedTargets { get; set; } = new string[0];

/// <summary>
/// Runner labels emitted verbatim as <c>runs-on: [label1, label2, ...]</c>, for selecting a
/// self-hosted runner pool by OS/arch/capability (e.g. <c>["self-hosted", "linux", "x64"]</c>).
/// <para/>
/// When non-empty this replaces the <c>runs-on:</c> image for the job and requires exactly one
/// image (no matrix). The constructor-mandated <see cref="GitHubActionsImage"/> is then ignored
/// for <c>runs-on:</c> and only names the job.
/// </summary>
public string[] RunsOnLabels { get; set; } = new string[0];

public GitHubActionsSubmodules Submodules
{
set => _submodules = value;
Expand Down Expand Up @@ -176,6 +186,10 @@ public override ConfigurationEntity GetConfiguration(IReadOnlyCollection<Executa
$"Workflows can only define either shorthand '{nameof(On)}' or '{nameof(On)}*' triggers");
Assert.True(configuration.ShortTriggers.Length > 0 || configuration.DetailedTriggers.Length > 0,
$"Workflows must define either shorthand '{nameof(On)}' or '{nameof(On)}*' triggers");
Assert.True(RunsOnLabels.Length == 0 || _images.Length == 1,
$"Cannot use '{nameof(RunsOnLabels)}' with multiple images; labels resolve a single job's runner");
Assert.True(RunsOnLabels.All(x => !x.IsNullOrWhiteSpace()),
$"'{nameof(RunsOnLabels)}' entries must not be null, empty, or whitespace");

return configuration;
}
Expand All @@ -185,6 +199,7 @@ protected virtual GitHubActionsJob GetJobs(GitHubActionsImage image, IReadOnlyCo
return new GitHubActionsJob
{
Name = image.GetValue().Replace(".", "_"),
RunsOnLabels = RunsOnLabels,
EnvironmentName = EnvironmentName,
EnvironmentUrl = EnvironmentUrl,
Steps = GetSteps(relevantTargets).ToArray(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# ------------------------------------------------------------------------------
# <auto-generated>
#
# 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
#
# </auto-generated>
# ------------------------------------------------------------------------------

name: test

on: [push]

jobs:
ubuntu-latest:
name: ubuntu-latest
runs-on: [self-hosted, linux, x64]
steps:
- uses: actions/checkout@v6
- 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
11 changes: 11 additions & 0 deletions tests/Fallout.Common.Tests/CI/ConfigurationGenerationTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,17 @@ public class TestBuild : FalloutBuild
}
);

yield return
(
"runs-on-labels",
new TestGitHubActionsAttribute(GitHubActionsImage.UbuntuLatest)
{
On = new[] { GitHubActionsTrigger.Push },
InvokedTargets = new[] { nameof(Test) },
RunsOnLabels = new[] { "self-hosted", "linux", "x64" }
}
);

yield return
(
null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
using System;
using Fallout.Common.CI;
using Fallout.Common.CI.GitHubActions;
using Fallout.Common.Execution;
using FluentAssertions;
using Xunit;

namespace Fallout.Common.Tests.CI;

public class GitHubActionsRunsOnLabelsValidationTest
{
[Fact]
public void Matrix_with_runs_on_labels_throws()
{
var act = () => GetConfiguration(
new[] { GitHubActionsImage.UbuntuLatest, GitHubActionsImage.WindowsLatest },
new[] { "self-hosted", "linux", "x64" });

act.Should().Throw<Exception>().WithMessage("*RunsOnLabels*");
}

[Fact]
public void Single_image_with_runs_on_labels_does_not_throw()
{
var act = () => GetConfiguration(
new[] { GitHubActionsImage.UbuntuLatest },
new[] { "self-hosted", "linux", "x64" });

act.Should().NotThrow();
}

[Fact]
public void Matrix_without_runs_on_labels_does_not_throw()
{
var act = () => GetConfiguration(
new[] { GitHubActionsImage.UbuntuLatest, GitHubActionsImage.WindowsLatest },
new string[0]);

act.Should().NotThrow();
}

[Fact]
public void Single_label_does_not_throw()
{
var act = () => GetConfiguration(
new[] { GitHubActionsImage.UbuntuLatest },
new[] { "self-hosted" });

act.Should().NotThrow();
}

[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void Empty_or_whitespace_label_element_throws(string badLabel)
{
var act = () => GetConfiguration(
new[] { GitHubActionsImage.UbuntuLatest },
new[] { "self-hosted", badLabel });

act.Should().Throw<Exception>().WithMessage("*RunsOnLabels*");
}

private static void GetConfiguration(GitHubActionsImage[] images, string[] runsOnLabels)
{
var build = new ConfigurationGenerationTest.TestBuild();
var relevantTargets = ExecutableTargetFactory.CreateAll(build, x => x.Compile);
var attribute = new TestGitHubActionsAttribute(images[0], images[1..])
{
On = new[] { GitHubActionsTrigger.Push },
InvokedTargets = new[] { nameof(ConfigurationGenerationTest.TestBuild.Test) },
RunsOnLabels = runsOnLabels
};
((ConfigurationAttributeBase)attribute).Build = build;
attribute.GetConfiguration(relevantTargets);
}
}
Loading