Skip to content
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

Add support for Step Summary #1642

Merged
merged 32 commits into from
Feb 4, 2022
Merged
Show file tree
Hide file tree
Changes from 30 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
25f6cb1
First prototype of step summary environment variable
pfleidi Jan 21, 2022
0361ffc
Merge branch 'main' of github.com:actions/runner into pfleidi/step_su…
pfleidi Jan 21, 2022
412dc5c
Fix file contention issue
pfleidi Jan 21, 2022
386fa86
Try to simplify cleaning up file references
pfleidi Jan 22, 2022
e112193
use step id as md file name, queue file attachment
robherley Jan 26, 2022
e552c0c
separate logic into attachment summary func
robherley Jan 26, 2022
aacf1ba
Fix indentation
pfleidi Jan 26, 2022
3ef3cae
Add (experimental) feature flag support
pfleidi Jan 27, 2022
ed74b5c
Merge branch 'main' of github.com:actions/runner into feature/step-su…
pfleidi Jan 28, 2022
d9d0963
reorganize summary upload determination logic
robherley Jan 31, 2022
901a406
file i/o exception handling + pr feedback
robherley Feb 1, 2022
f6b60c9
Merge branch 'main' of github.com:actions/runner into feature/step-su…
pfleidi Feb 1, 2022
9572ebf
Revert changes for now to reintroduce them later
pfleidi Feb 1, 2022
44ab871
Add skeleton SetStepSummaryCommand
pfleidi Feb 1, 2022
6b12914
Update step summary feature flag name
pfleidi Feb 2, 2022
a39bbf2
Port ShouldUploadAttachment from previous iteration
pfleidi Feb 2, 2022
5757734
Port QueueStepSummaryUpload from previous iteration
pfleidi Feb 2, 2022
d1e2740
Improve exception handling when uploading attachment
pfleidi Feb 2, 2022
4dddab6
Add some minor logging improvements
pfleidi Feb 2, 2022
10935cd
Refuse to upload files larger than 128k
pfleidi Feb 2, 2022
51c110a
Implement secrets scrubbing
pfleidi Feb 2, 2022
88f916b
Add TODO comment to remove debugging temp files
pfleidi Feb 2, 2022
c292c4d
Add first tests
pfleidi Feb 3, 2022
cad400b
Add test for secret masking
pfleidi Feb 3, 2022
e69dd47
Add some naming/style fixes suggested in feedback
pfleidi Feb 3, 2022
27decc4
inline check for feature flag
pfleidi Feb 3, 2022
2c455ff
Inline method for style consistency
pfleidi Feb 3, 2022
bfdfa44
Make sure that scrubbed file doesn't exist before creating it
pfleidi Feb 3, 2022
29680f5
Rename SetStepSummaryCommand to CreateStepSummaryCommand
pfleidi Feb 3, 2022
03182cf
Fix error handling messages
pfleidi Feb 3, 2022
e46424a
Fix file command name when registering extension
pfleidi Feb 4, 2022
8ce4203
Remove unnecessary file deletion
pfleidi Feb 4, 2022
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
1 change: 1 addition & 0 deletions src/Runner.Common/ExtensionManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ private List<IExtension> LoadExtensions<T>() where T : class, IExtension
case "GitHub.Runner.Worker.IFileCommandExtension":
Add<T>(extensions, "GitHub.Runner.Worker.AddPathFileCommand, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.SetEnvFileCommand, Runner.Worker");
Add<T>(extensions, "GitHub.Runner.Worker.SetStepSummaryCommand, Runner.Worker");
break;
case "GitHub.Runner.Listener.Check.ICheckExtension":
Add<T>(extensions, "GitHub.Runner.Listener.Check.InternetCheck, Runner.Listener");
Expand Down
72 changes: 72 additions & 0 deletions src/Runner.Worker/FileCommandManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -259,4 +259,76 @@ private static string ReadLine(
return text.Substring(originalIndex, lfIndex - originalIndex);
}
}

public sealed class CreateStepSummaryCommand : RunnerService, IFileCommandExtension
{
private const int _attachmentSizeLimit = 128 * 1024;

public string ContextName => "step_summary";
public string FilePrefix => "step_summary_";

public Type ExtensionType => typeof(IFileCommandExtension);

public void ProcessCommand(IExecutionContext context, string filePath, ContainerInfo container)
{
if (!context.Global.Variables.GetBoolean("DistributedTask.UploadStepSummary") ?? true)
{
Trace.Info("Step Summary is disabled; skipping attachment upload");
return;
}

if (String.IsNullOrEmpty(filePath) || !File.Exists(filePath))
{
Trace.Info($"Step Summary file ({filePath}) does not exist; skipping attachment upload");
return;
}

try
{
var fileSize = new FileInfo(filePath).Length;
if (fileSize == 0)
{
Trace.Info($"Step Summary file ({filePath}) is empty; skipping attachment upload");
return;
}

if (fileSize > _attachmentSizeLimit)
{
context.Error($"$GITHUB_STEP_SUMMARY supports content up a size of {_attachmentSizeLimit / 1024}k got {fileSize / 1024}k");
Trace.Info($"Step Summary file ({filePath}) is too large ({fileSize} bytes); skipping attachment upload");

return;
}

Trace.Verbose($"Step Summary file exists: {filePath} and has a file size of {fileSize} bytes");
var scrubbedFilePath = filePath + "-scrubbed";

if (File.Exists(scrubbedFilePath))
{
File.Delete(scrubbedFilePath);
}

using (var streamReader = new StreamReader(filePath))
using (var streamWriter = new StreamWriter(scrubbedFilePath))
{
string line;
while ((line = streamReader.ReadLine()) != null)
{
var maskedLine = HostContext.SecretMasker.MaskSecrets(line);
streamWriter.WriteLine(maskedLine);
}
}

var attachmentName = context.Id.ToString();

Trace.Info($"Queueing file ({filePath}) for attachment upload ({attachmentName})");
context.QueueAttachFile(ChecksAttachmentType.StepSummary, attachmentName, scrubbedFilePath);
}
catch (Exception e)
{
Trace.Error($"Error while processing file ({filePath}): {e}");
context.Error($"Failed to create step summary using 'GITHUB_STEP_SUMMARY': {e.Message}");
}
}
}
}
1 change: 1 addition & 0 deletions src/Runner.Worker/GitHubContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ public sealed class GitHubContext : DictionaryContextData, IEnvironmentContextDa
"run_number",
"server_url",
"sha",
"step_summary",
"workflow",
"workspace",
};
Expand Down
6 changes: 6 additions & 0 deletions src/Sdk/DTWebApi/WebApi/TaskAttachment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -102,4 +102,10 @@ public class CoreAttachmentType
public static readonly String FileAttachment = "DistributedTask.Core.FileAttachment";
public static readonly String DiagnosticLog = "DistributedTask.Core.DiagnosticLog";
}

[GenerateAllConstants]
public class ChecksAttachmentType
{
public static readonly String StepSummary = "Checks.Step.Summary";
}
}
241 changes: 241 additions & 0 deletions src/Test/L0/Worker/CreateStepSummaryCommandL0.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,241 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Runtime.CompilerServices;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using Moq;
using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
using GitHub.DistributedTask.WebApi;

namespace GitHub.Runner.Common.Tests.Worker
{
public sealed class CreateStepSummaryCommandL0
{
private Mock<IExecutionContext> _executionContext;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private Variables _variables;
private string _rootDirectory;
private CreateStepSummaryCommand _createStepCommand;
private ITraceWriter _trace;

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepSummaryCommand_FeatureDisabled()
{
using (var hostContext = Setup(featureFlagState: "false"))
{
var stepSummaryFile = Path.Combine(_rootDirectory, "feature-off");

_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());

Assert.Equal(0, _issues.Count);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepSummaryCommand_FileNull()
{
using (var hostContext = Setup())
{
_createStepCommand.ProcessCommand(_executionContext.Object, null, null);

_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Assert.Equal(0, _issues.Count);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepSummaryCommand_DirectoryNotFound()
{
using (var hostContext = Setup())
{
var stepSummaryFile = Path.Combine(_rootDirectory, "directory-not-found", "env");

_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);

_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Assert.Equal(0, _issues.Count);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepSummaryCommand_FileNotFound()
{
using (var hostContext = Setup())
{
var stepSummaryFile = Path.Combine(_rootDirectory, "file-not-found");

_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);

_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Assert.Equal(0, _issues.Count);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepSummaryCommand_EmptyFile()
{
using (var hostContext = Setup())
{
var stepSummaryFile = Path.Combine(_rootDirectory, "empty-file");
File.Create(stepSummaryFile).Dispose();

_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);

_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Assert.Equal(0, _issues.Count);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepSummaryCommand_LargeFile()
{
using (var hostContext = Setup())
{
var stepSummaryFile = Path.Combine(_rootDirectory, "empty-file");
File.WriteAllBytes(stepSummaryFile, new byte[128 * 1024 + 1]);

_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);

_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());
Assert.Equal(1, _issues.Count);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepSummaryCommand_Simple()
{
using (var hostContext = Setup())
{
var stepSummaryFile = Path.Combine(_rootDirectory, "simple");
var content = new List<string>
{
"# This is some markdown content",
"",
"## This is more markdown content",
};
WriteContent(stepSummaryFile, content);

_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);

_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, _executionContext.Object.Id.ToString(), stepSummaryFile + "-scrubbed"), Times.Once());
Assert.Equal(0, _issues.Count);
}
}

[Fact]
[Trait("Level", "L0")]
[Trait("Category", "Worker")]
public void CreateStepSummaryCommand_ScrubSecrets()
{
using (var hostContext = Setup())
{
// configure secretmasker to actually mask secrets
hostContext.SecretMasker.AddRegex("Password=.*");
hostContext.SecretMasker.AddRegex("ghs_.*");

var stepSummaryFile = Path.Combine(_rootDirectory, "simple");
var scrubbedFile = stepSummaryFile + "-scrubbed";
var content = new List<string>
{
"# Password=ThisIsMySecretPassword!",
"",
"# GITHUB_TOKEN ghs_verysecuretoken",
};
WriteContent(stepSummaryFile, content);

_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);

var scrubbedFileContents = File.ReadAllText(scrubbedFile);
Assert.DoesNotContain("ThisIsMySecretPassword!", scrubbedFileContents);
Assert.DoesNotContain("ghs_verysecuretoken", scrubbedFileContents);

_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, _executionContext.Object.Id.ToString(), scrubbedFile), Times.Once());
Assert.Equal(0, _issues.Count);
}
}

private void WriteContent(
string path,
List<string> content,
string newline = null)
{
if (string.IsNullOrEmpty(newline))
{
newline = Environment.NewLine;
}

var encoding = new UTF8Encoding(true); // Emit BOM
var contentStr = string.Join(newline, content);
File.WriteAllText(path, contentStr, encoding);
}

private TestHostContext Setup([CallerMemberName] string name = "", string featureFlagState = "true")
{
_issues = new List<Tuple<DTWebApi.Issue, string>>();

var hostContext = new TestHostContext(this, name);

// Trace
_trace = hostContext.GetTrace();

_variables = new Variables(hostContext, new Dictionary<string, VariableValue>
{
{ "MySecretName", new VariableValue("My secret value", true) },
{ "DistributedTask.UploadStepSummary", featureFlagState },
});

// Directory for test data
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
Directory.CreateDirectory(workDirectory);
_rootDirectory = Path.Combine(workDirectory, nameof(CreateStepSummaryCommandL0));
Directory.CreateDirectory(_rootDirectory);

// Execution context
_executionContext = new Mock<IExecutionContext>();
_executionContext.Setup(x => x.Global)
.Returns(new GlobalContext
{
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
Variables = _variables,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
.Callback((DTWebApi.Issue issue, string logMessage) =>
{
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
_trace.Info($"Issue '{issue.Type}': {message}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>
{
_trace.Info($"{tag}{message}");
});

//CreateStepSummaryCommand
_createStepCommand = new CreateStepSummaryCommand();
_createStepCommand.Initialize(hostContext);

return hostContext;
}
}
}