Skip to content

Add initial GitHub Actions adapter #2824 #2826

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Apr 17, 2025
Merged
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
2 changes: 2 additions & 0 deletions docs/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers
What's changed since pre-release v3.0.0-B0453:

- Engineering:
- Added GitHub Actions support to CLI by @BernieWhite.
[#2824](https://github.com/microsoft/PSRule/issues/2824)
- Bump vscode engine to v1.99.1.
[#2858](https://github.com/microsoft/PSRule/pull/2858)

Expand Down
5 changes: 5 additions & 0 deletions src/PSRule.CommandLine/Commands/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,11 @@ public static async Task<int> RunAsync(RunOptions operationOptions, ClientContex
clientContext.Option.Output.Format = operationOptions.OutputFormat.Value;
}

if (operationOptions.JobSummaryPath != null)
{
clientContext.Option.Output.JobSummaryPath = operationOptions.JobSummaryPath;
}

// Run restore command.
if (!operationOptions.NoRestore)
{
Expand Down
5 changes: 5 additions & 0 deletions src/PSRule.CommandLine/Models/RunOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,4 +55,9 @@ public sealed class RunOptions
/// Do not restore modules before running rules.
/// </summary>
public bool NoRestore { get; set; }

/// <summary>
/// The path to write the job summary file.
/// </summary>
public string? JobSummaryPath { get; set; }
}
125 changes: 125 additions & 0 deletions src/PSRule.Tool/Adapters/GitHubActionsAdapter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.Tool.Adapters;

/// <summary>
/// This is an adapter for handling GitHub Actions specific functionality
/// for the official PSRule GitHub Action.
/// </summary>
internal sealed class GitHubActionsAdapter
{
private const char COMMA = ',';

public string[] BuildArgs(string[] args)
{
WriteVersion();

args = GetArgs(args);

Console.WriteLine("");
Console.WriteLine("---");

return args;
}

private void WriteVersion()
{
Console.WriteLine($"[info] Using Version: {ClientBuilder.Version}");
// Console.WriteLine($"[info] GitHub Action Version: {Environment.GetGitHubActionVersion()}");
// Console.WriteLine($"[info] GitHub Action Name: {Environment.GetGitHubActionName()}");
// Console.WriteLine($"[info] GitHub Action ID: {Environment.GetGitHubActionId()}");
// Console.WriteLine($"[info] GitHub Action Path: {Environment.GetGitHubActionPath()}");
}

private string[] GetArgs(string[] args)
{
var result = new List<string>(args);

if (Environment.TryString("INPUT_INCLUDEPATH", out var includePath) && !string.IsNullOrWhiteSpace(includePath))
{
result.Add("--path");
result.Add(includePath);
Console.WriteLine($"[info] Using IncludePath: {includePath}");
}

if (Environment.TryString("INPUT_BASELINE", out var baseline) && !string.IsNullOrWhiteSpace(baseline))
{
result.Add("--baseline");
result.Add(baseline);
Console.WriteLine($"[info] Using Baseline: {baseline}");
}

// CLI does not support this yet.
// if (Environment.TryString("INPUT_CONVENTIONS", out var conventions) && !string.IsNullOrWhiteSpace(conventions))
// {
// foreach (var convention in conventions.Split([COMMA], StringSplitOptions.RemoveEmptyEntries))
// {
// if (string.IsNullOrWhiteSpace(convention))
// continue;

// result.Add("--convention");
// result.Add(convention.Trim());
// }
// Console.WriteLine($"[info] Using Conventions: {conventions}");
// }

if (Environment.TryString("INPUT_INPUTPATH", out var inputPath) && !string.IsNullOrWhiteSpace(inputPath))
{
result.Add("--input-path");
result.Add(inputPath);
Console.WriteLine($"[info] Using InputPath: {inputPath}");
}

if (Environment.TryString("INPUT_OPTION", out var option) && !string.IsNullOrWhiteSpace(option))
{
result.Add("--option");
result.Add(option);
Console.WriteLine($"[info] Using Option: {option}");
}

if (Environment.TryString("INPUT_OUTCOME", out var outcome) && !string.IsNullOrWhiteSpace(outcome))
{
result.Add("--outcome");
result.Add(outcome);
Console.WriteLine($"[info] Using Outcome: {outcome}");
}

if (Environment.TryString("INPUT_OUTPUTFORMAT", out var outputFormat) && !string.IsNullOrWhiteSpace(outputFormat))
{
result.Add("--output-format");
result.Add(outputFormat);
Console.WriteLine($"[info] Using OutputFormat: {outputFormat}");
}

if (Environment.TryString("INPUT_OUTPUTPATH", out var outputPath) && !string.IsNullOrWhiteSpace(outputPath))
{
result.Add("--output-path");
result.Add(outputPath);
Console.WriteLine($"[info] Using OutputPath: {outputPath}");
}

if (Environment.TryString("INPUT_SUMMARY", out var summary) && !string.IsNullOrWhiteSpace(summary) && summary == "true" &&
Environment.TryString("GITHUB_STEP_SUMMARY", out var jobSummaryPath) && !string.IsNullOrWhiteSpace(jobSummaryPath))
{
result.Add("--job-summary-path");
result.Add(jobSummaryPath);
Console.WriteLine($"[info] Using Summary: {jobSummaryPath}");
}

if (Environment.TryString("INPUT_MODULES", out var modules) && !string.IsNullOrWhiteSpace(modules))
{
foreach (var module in modules.Split([COMMA], StringSplitOptions.RemoveEmptyEntries))
{
if (string.IsNullOrWhiteSpace(module))
continue;

result.Add("--module");
result.Add(module.Trim());
}
Console.WriteLine($"[info] Using Modules: {modules}");
}

return [.. result];
}
}
30 changes: 22 additions & 8 deletions src/PSRule.Tool/ClientBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,13 @@ internal sealed class ClientBuilder
{
private const string ARG_FORCE = "--force";

private static readonly string? _Version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
internal static readonly string? Version = (Assembly.GetEntryAssembly() ?? Assembly.GetExecutingAssembly()).GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;

private readonly Option<string> _Global_Option;
private readonly Option<bool> _Global_Verbose;
private readonly Option<bool> _Global_Debug;
private readonly Option<bool> _Global_WaitForDebugger;
private readonly Option<bool> _Global_InGitHubActions;
private readonly Option<bool> _Module_Restore_Force;
private readonly Option<bool> _Module_Init_Force;
private readonly Option<string> _Module_Add_Version;
Expand All @@ -39,6 +40,7 @@ internal sealed class ClientBuilder
private readonly Option<string[]> _Run_Formats;
private readonly Option<string[]> _Run_Outcome;
private readonly Option<bool> _Run_NoRestore;
private readonly Option<string> _Run_JobSummaryPath;

private ClientBuilder(RootCommand cmd)
{
Expand All @@ -58,17 +60,22 @@ private ClientBuilder(RootCommand cmd)
["--debug"],
description: CmdStrings.Global_Debug_Description
);
_Global_Path = new Option<string[]>(
["-p", "--path"],
description: CmdStrings.Global_Path_Description
);

// Arguments that are hidden because they are intercepted early in the process.
_Global_WaitForDebugger = new Option<bool>(
["--wait-for-debugger"],
description: ""
//CmdStrings.Global_WaitForDebugger_Description
description: string.Empty
);
_Global_WaitForDebugger.IsHidden = true;

_Global_Path = new Option<string[]>(
["-p", "--path"],
description: CmdStrings.Global_Path_Description
_Global_InGitHubActions = new Option<bool>(
["--in-github-actions"],
description: string.Empty
);
_Global_InGitHubActions.IsHidden = true;

// Options for the run command.
_Run_OutputPath = new Option<string>(
Expand Down Expand Up @@ -105,6 +112,10 @@ private ClientBuilder(RootCommand cmd)
"--no-restore",
description: CmdStrings.Run_NoRestore_Description
);
_Run_JobSummaryPath = new Option<string>(
["--job-summary-path"],
description: CmdStrings.Run_JobSummaryPath_Description
);

// Options for the module command.
_Module_Init_Force = new Option<bool>(
Expand Down Expand Up @@ -137,13 +148,14 @@ private ClientBuilder(RootCommand cmd)
cmd.AddGlobalOption(_Global_Verbose);
cmd.AddGlobalOption(_Global_Debug);
cmd.AddGlobalOption(_Global_WaitForDebugger);
cmd.AddGlobalOption(_Global_InGitHubActions);
}

public RootCommand Command { get; }

public static Command New()
{
var cmd = new RootCommand(string.Concat(CmdStrings.Cmd_Description, " v", _Version))
var cmd = new RootCommand(string.Concat(CmdStrings.Cmd_Description, " v", Version))
{
Name = "ps-rule"
};
Expand All @@ -169,6 +181,7 @@ private void AddRun()
cmd.AddOption(_Run_Formats);
cmd.AddOption(_Run_Outcome);
cmd.AddOption(_Run_NoRestore);
cmd.AddOption(_Run_JobSummaryPath);
cmd.SetHandler(async (invocation) =>
{
var option = new RunOptions
Expand All @@ -182,6 +195,7 @@ private void AddRun()
OutputPath = invocation.ParseResult.GetValueForOption(_Run_OutputPath),
OutputFormat = invocation.ParseResult.GetValueForOption(_Run_OutputFormat).ToOutputFormat(),
NoRestore = invocation.ParseResult.GetValueForOption(_Run_NoRestore),
JobSummaryPath = invocation.ParseResult.GetValueForOption(_Run_JobSummaryPath),
};

var client = GetClientContext(invocation);
Expand Down
33 changes: 31 additions & 2 deletions src/PSRule.Tool/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.CommandLine;
using System.CommandLine.Parsing;
using System.Management.Automation;
using PSRule.Tool.Adapters;

namespace PSRule.Tool;

Expand All @@ -30,15 +31,43 @@ static async Task<int> Main(string[] args)

System.Environment.SetEnvironmentVariable("PSModulePath", modulePath, EnvironmentVariableTarget.Process);

if (ShouldWaitForDebugger(args))
var execute = async (string[] args) =>
{
return await ClientBuilder.New().InvokeAsync(args);
};

if (ShouldUseGitHubActionAdapter(args))
{
var adapter = new GitHubActionsAdapter();
args = adapter.BuildArgs(args);

execute = async (string[] args) =>
{
return await ClientBuilder.New().InvokeAsync(args);
};
}
else if (ShouldWaitForDebugger(args))
{
if (!await WaitForDebuggerAsync())
return ERROR_DEBUGGER_ATTACH_TIMEOUT;

System.Diagnostics.Debugger.Break();
}

return await ClientBuilder.New().InvokeAsync(args);
return await execute(args);
}

private static bool ShouldUseGitHubActionAdapter(string[] args)
{
if (args == null || args.Length == 0)
return false;

foreach (var arg in args)
{
if (arg.Equals("--in-github-actions", StringComparison.OrdinalIgnoreCase))
return true;
}
return false;
}

private static bool ShouldWaitForDebugger(string[] args)
Expand Down
9 changes: 9 additions & 0 deletions src/PSRule.Tool/Resources/CmdStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 4 additions & 1 deletion src/PSRule.Tool/Resources/CmdStrings.resx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Expand Down Expand Up @@ -207,4 +207,7 @@
<data name="Run_Formats_Description" xml:space="preserve">
<value>A list of formats to enable.</value>
</data>
<data name="Run_JobSummaryPath_Description" xml:space="preserve">
<value>The file path to write a job summary.</value>
</data>
</root>
10 changes: 7 additions & 3 deletions src/PSRule/Pipeline/Output/JobSummaryWriter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

namespace PSRule.Pipeline.Output;

#nullable enable

/// <summary>
/// Define an pipeline writer to write a job summary to disk.
/// </summary>
Expand All @@ -24,11 +26,11 @@
private readonly JobSummaryFormat _JobSummary;
private readonly Source[] _Source;

private Stream _Stream;
private Stream? _Stream;
private StreamWriter _Writer;
private bool _IsDisposed;

public JobSummaryWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess, string outputPath = null, Stream stream = null, Source[] source = null)

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / Build extension

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / Build extension

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / Build extension

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (ubuntu-latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (ubuntu-latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (ubuntu-latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / Build module

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / Build module

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / Build module

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (macos-latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (macos-latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (macos-latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (windows-latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (windows-latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 33 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (windows-latest)

Cannot convert null literal to non-nullable reference type.
: base(inner, option, shouldProcess)
{
_OutputPath = outputPath ?? Environment.GetRootedPath(Option.Output.JobSummaryPath);
Expand Down Expand Up @@ -60,8 +62,9 @@
if (string.IsNullOrEmpty(_OutputPath) || _IsDisposed || !CreateFile(_OutputPath))
return;

_Stream ??= new FileStream(_OutputPath, FileMode.Create, FileAccess.ReadWrite, FileShare.Read);
_Stream ??= new FileStream(_OutputPath, FileMode.Append, FileAccess.ReadWrite, FileShare.Read);
_Writer = new StreamWriter(_Stream, _Encoding, 2048, false);
_Writer ??= File.AppendText(_OutputPath);
}

private void Source()
Expand Down Expand Up @@ -111,7 +114,7 @@
WriteLine();
}

private void WriteLine(string text = null)

Check warning on line 117 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / Build extension

Cannot convert null literal to non-nullable reference type.

Check warning on line 117 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (ubuntu-latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 117 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / Build module

Cannot convert null literal to non-nullable reference type.

Check warning on line 117 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (macos-latest)

Cannot convert null literal to non-nullable reference type.

Check warning on line 117 in src/PSRule/Pipeline/Output/JobSummaryWriter.cs

View workflow job for this annotation

GitHub Actions / 🧪 Test extension (windows-latest)

Cannot convert null literal to non-nullable reference type.
{
if (_Writer == null || _IsDisposed)
return;
Expand Down Expand Up @@ -228,7 +231,6 @@
if (disposing)
{
_Writer?.Dispose();
_Stream?.Dispose();
}
_IsDisposed = true;
}
Expand All @@ -237,3 +239,5 @@

#endregion IDisposable
}

#nullable restore
Loading