diff --git a/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs b/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs index f9ce74091..7107b1ea3 100644 --- a/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs +++ b/src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs @@ -161,6 +161,16 @@ public GitHubActionsAttribute( /// public string[] RunsOnLabels { get; set; } = new string[0]; + /// + /// Overrides the generated job's id and name:, which otherwise default to the runner image + /// (e.g. windows-latest). Useful with , where the image is only a + /// placeholder, since the job name is what shows in the PR checks list and what branch protection matches. + /// + /// Must be a valid job id (letters, digits, - and _, starting with a letter or _) + /// and requires exactly one image, as a matrix would produce duplicate ids. Unset keeps the default. + /// + public string JobName { get; set; } + public GitHubActionsSubmodules Submodules { set => submodules = value; @@ -307,6 +317,10 @@ public override ConfigurationEntity GetConfiguration(IReadOnlyCollection !x.IsNullOrWhiteSpace()), $"'{nameof(RunsOnLabels)}' entries must not be null, empty, or whitespace"); + Assert.True(JobName == null || images.Length == 1, + $"Cannot use '{nameof(JobName)}' with multiple images; each job in the matrix would get the same id"); + Assert.True(JobName == null || System.Text.RegularExpressions.Regex.IsMatch(JobName, "^[A-Za-z_][A-Za-z0-9_-]*$"), + $"'{nameof(JobName)}' must be a valid job id: letters, digits, '-' and '_', starting with a letter or '_'"); return configuration; } @@ -315,7 +329,7 @@ protected virtual GitHubActionsJob GetJobs(GitHubActionsImage image, IReadOnlyCo { return new GitHubActionsJob { - Name = image.GetValue().Replace(".", "_"), + Name = JobName ?? image.GetValue().Replace(".", "_"), RunsOnLabels = RunsOnLabels, EnvironmentName = EnvironmentName, EnvironmentUrl = EnvironmentUrl, diff --git a/tests/Fallout.Common.Specs/CI/GitHubActionsJobNameSpecs.cs b/tests/Fallout.Common.Specs/CI/GitHubActionsJobNameSpecs.cs new file mode 100644 index 000000000..54ced9d80 --- /dev/null +++ b/tests/Fallout.Common.Specs/CI/GitHubActionsJobNameSpecs.cs @@ -0,0 +1,93 @@ +using System; +using System.Linq; +using Fallout.Common.CI; +using Fallout.Common.CI.GitHubActions; +using Fallout.Common.CI.GitHubActions.Configuration; +using Fallout.Common.Execution; +using Fallout.Common.Utilities; +using FluentAssertions; +using Xunit; + +namespace Fallout.Common.Specs.CI; + +public class GitHubActionsJobNameSpecs +{ + [Fact] + public void Unset_job_name_uses_image_value() + { + var job = GetJobs(new[] { GitHubActionsImage.WindowsLatest }, jobName: null).Single(); + + job.Name.Should().Be("windows-latest"); + } + + [Fact] + public void Unset_job_name_keeps_dots_replaced_in_matrix() + { + var jobs = GetJobs(new[] { GitHubActionsImage.UbuntuLatest, GitHubActionsImage.Ubuntu2204 }, jobName: null); + + jobs.Select(x => x.Name).Should().Equal("ubuntu-latest", "ubuntu-22_04"); + } + + [Fact] + public void Job_name_is_used_as_job_id_and_name() + { + var job = GetJobs(new[] { GitHubActionsImage.WindowsLatest }, "build-windows").Single(); + + job.Name.Should().Be("build-windows"); + using var stream = new System.IO.MemoryStream(); + using (var streamWriter = new System.IO.StreamWriter(stream, leaveOpen: true)) + { + job.Write(new CustomFileWriter(streamWriter, 2, "#")); + } + + var output = System.Text.Encoding.UTF8.GetString(stream.ToArray()); + output.Should().StartWith("build-windows:").And.Contain("name: build-windows"); + } + + [Fact] + public void Job_name_with_multiple_images_throws() + { + var act = () => GetJobs(new[] { GitHubActionsImage.UbuntuLatest, GitHubActionsImage.WindowsLatest }, "build"); + + act.Should().Throw().WithMessage("*JobName*"); + } + + [Theory] + [InlineData("build")] + [InlineData("_build")] + [InlineData("Build-1_x")] + public void Valid_job_name_does_not_throw(string jobName) + { + var act = () => GetJobs(new[] { GitHubActionsImage.UbuntuLatest }, jobName); + + act.Should().NotThrow(); + } + + [Theory] + [InlineData("")] + [InlineData(" ")] + [InlineData("1build")] + [InlineData("-build")] + [InlineData("build job")] + [InlineData("build.job")] + public void Invalid_job_name_throws(string jobName) + { + var act = () => GetJobs(new[] { GitHubActionsImage.UbuntuLatest }, jobName); + + act.Should().Throw().WithMessage("*JobName*"); + } + + private static GitHubActionsJob[] GetJobs(GitHubActionsImage[] images, string jobName) + { + var build = new ConfigurationGenerationSpecs.TestBuild(); + var relevantTargets = ExecutableTargetFactory.CreateAll(build, x => x.Compile); + var attribute = new TestGitHubActionsAttribute(images[0], images[1..]) + { + On = new[] { GitHubActionsTrigger.Push }, + InvokedTargets = new[] { nameof(ConfigurationGenerationSpecs.TestBuild.Test) }, + JobName = jobName + }; + ((ConfigurationAttributeBase)attribute).Build = build; + return ((GitHubActionsConfiguration)attribute.GetConfiguration(relevantTargets)).Jobs; + } +}