File: WorkflowScripts\LintHandwrittenWorkflowsScriptTests.cs
Web Access
Project: src\tests\Infrastructure.Tests\Infrastructure.Tests.csproj (Infrastructure.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using Aspire.TestUtilities;
using Xunit;
 
namespace Infrastructure.Tests;
 
/// <summary>
/// Tests for .github/workflows/lint-handwritten-workflows.sh, the file selection shared by
/// the blocking ci.yml actionlint gate and the update-actionlint.yml candidate check.
/// </summary>
[SkipOnPlatform(TestPlatforms.Windows, "The script runs only on Linux runners and the fake actionlint relies on Unix PATH lookup.")]
public sealed class LintHandwrittenWorkflowsScriptTests(ITestOutputHelper output) : IDisposable
{
    private static readonly string s_scriptPath = Path.Combine(
        RepoRoot.Path,
        ".github",
        "workflows",
        "lint-handwritten-workflows.sh");
 
    private readonly TemporaryWorkspace _workspace = TemporaryWorkspace.Create(output);
 
    [Fact]
    [RequiresTools(["bash"])]
    public async Task LintsOnlyTopLevelHandwrittenWorkflows()
    {
        CreateFiles(
            ".github/workflows/ci.yml",
            ".github/workflows/release.yaml",
            ".github/workflows/agent.lock.yml",
            ".github/workflows/agent.md",
            ".github/workflows/agentics-maintenance-microsoft-aspire.dev.yml",
            ".github/workflows/helper.js",
            ".github/workflows/nested/inner.yml",
            ".github/actions/local/action.yml");
 
        var result = await RunScriptAsync(actionlintExitCode: 0);
 
        Assert.Equal(0, result.ExitCode);
        Assert.Equal(
            [
                "-shellcheck=",
                "-pyflakes=",
                ".github/workflows/ci.yml",
                ".github/workflows/release.yaml",
            ],
            await ReadActionlintArgumentsAsync());
    }
 
    [Fact]
    [RequiresTools(["bash"])]
    public async Task PropagatesActionlintFailure()
    {
        CreateFiles(".github/workflows/ci.yml");
 
        var result = await RunScriptAsync(actionlintExitCode: 1);
 
        Assert.Equal(1, result.ExitCode);
    }
 
    [Fact]
    [RequiresTools(["bash"])]
    public async Task FailsWithoutRunningActionlintWhenNoWorkflowsMatch()
    {
        CreateFiles(".github/workflows/agent.lock.yml");
 
        var result = await RunScriptAsync(actionlintExitCode: 0);
 
        Assert.Equal(1, result.ExitCode);
        Assert.False(File.Exists(ArgumentsPath));
    }
 
    private string ArgumentsPath => Path.Combine(_workspace.Path, "actionlint-arguments.txt");
 
    private void CreateFiles(params string[] relativePaths)
    {
        foreach (var relativePath in relativePaths)
        {
            var path = Path.Combine(_workspace.Path, "repo", relativePath);
            Directory.CreateDirectory(Path.GetDirectoryName(path)!);
            File.WriteAllText(path, "name: test\n");
        }
    }
 
    private async Task<string[]> ReadActionlintArgumentsAsync() => await File.ReadAllLinesAsync(ArgumentsPath);
 
    private async Task<ProcessResult> RunScriptAsync(int actionlintExitCode)
    {
        // The fake actionlint records its arguments so the test can assert the exact
        // file set the script selects, without depending on a real actionlint install.
        var binDirectory = Directory.CreateDirectory(Path.Combine(_workspace.Path, "bin")).FullName;
        var fakeActionlint = Path.Combine(binDirectory, "actionlint");
        await File.WriteAllTextAsync(
            fakeActionlint,
            """
            #!/usr/bin/env bash
            printf '%s\n' "$@" > "$FAKE_ACTIONLINT_ARGUMENTS"
            exit "$FAKE_ACTIONLINT_EXIT_CODE"
            """);
        if (!OperatingSystem.IsWindows())
        {
            File.SetUnixFileMode(fakeActionlint, UnixFileMode.UserRead | UnixFileMode.UserWrite | UnixFileMode.UserExecute);
        }
 
        return await ProcessRunner.RunAsync(
            output,
            "bash",
            [s_scriptPath],
            Path.Combine(_workspace.Path, "repo"),
            new Dictionary<string, string>
            {
                ["PATH"] = $"{binDirectory}{Path.PathSeparator}{Environment.GetEnvironmentVariable("PATH")}",
                ["FAKE_ACTIONLINT_ARGUMENTS"] = ArgumentsPath,
                ["FAKE_ACTIONLINT_EXIT_CODE"] = actionlintExitCode.ToString(),
            });
    }
 
    public void Dispose() => _workspace.Dispose();
}