Skip to content

Commit 1a0d588

Browse files
pfleidirobherley
andauthored
Add support for Step Summary (#1642)
* First prototype of step summary environment variable * Fix file contention issue * Try to simplify cleaning up file references * use step id as md file name, queue file attachment * separate logic into attachment summary func * Fix indentation * Add (experimental) feature flag support * reorganize summary upload determination logic * file i/o exception handling + pr feedback * Revert changes for now to reintroduce them later * Add skeleton SetStepSummaryCommand * Update step summary feature flag name * Port ShouldUploadAttachment from previous iteration * Port QueueStepSummaryUpload from previous iteration * Improve exception handling when uploading attachment * Add some minor logging improvements * Refuse to upload files larger than 128k * Implement secrets scrubbing * Add TODO comment to remove debugging temp files * Add first tests * Add test for secret masking * Add some naming/style fixes suggested in feedback * inline check for feature flag * Inline method for style consistency * Make sure that scrubbed file doesn't exist before creating it * Rename SetStepSummaryCommand to CreateStepSummaryCommand * Fix error handling messages * Fix file command name when registering extension * Remove unnecessary file deletion Co-authored-by: Rob Herley <robherley@github.com>
1 parent 192ebfe commit 1a0d588

File tree

5 files changed

+316
-0
lines changed

5 files changed

+316
-0
lines changed

src/Runner.Common/ExtensionManager.cs

+1
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ private List<IExtension> LoadExtensions<T>() where T : class, IExtension
6060
case "GitHub.Runner.Worker.IFileCommandExtension":
6161
Add<T>(extensions, "GitHub.Runner.Worker.AddPathFileCommand, Runner.Worker");
6262
Add<T>(extensions, "GitHub.Runner.Worker.SetEnvFileCommand, Runner.Worker");
63+
Add<T>(extensions, "GitHub.Runner.Worker.CreateStepSummaryCommand, Runner.Worker");
6364
break;
6465
case "GitHub.Runner.Listener.Check.ICheckExtension":
6566
Add<T>(extensions, "GitHub.Runner.Listener.Check.InternetCheck, Runner.Listener");

src/Runner.Worker/FileCommandManager.cs

+67
Original file line numberDiff line numberDiff line change
@@ -259,4 +259,71 @@ private static string ReadLine(
259259
return text.Substring(originalIndex, lfIndex - originalIndex);
260260
}
261261
}
262+
263+
public sealed class CreateStepSummaryCommand : RunnerService, IFileCommandExtension
264+
{
265+
private const int _attachmentSizeLimit = 128 * 1024;
266+
267+
public string ContextName => "step_summary";
268+
public string FilePrefix => "step_summary_";
269+
270+
public Type ExtensionType => typeof(IFileCommandExtension);
271+
272+
public void ProcessCommand(IExecutionContext context, string filePath, ContainerInfo container)
273+
{
274+
if (!context.Global.Variables.GetBoolean("DistributedTask.UploadStepSummary") ?? true)
275+
{
276+
Trace.Info("Step Summary is disabled; skipping attachment upload");
277+
return;
278+
}
279+
280+
if (String.IsNullOrEmpty(filePath) || !File.Exists(filePath))
281+
{
282+
Trace.Info($"Step Summary file ({filePath}) does not exist; skipping attachment upload");
283+
return;
284+
}
285+
286+
try
287+
{
288+
var fileSize = new FileInfo(filePath).Length;
289+
if (fileSize == 0)
290+
{
291+
Trace.Info($"Step Summary file ({filePath}) is empty; skipping attachment upload");
292+
return;
293+
}
294+
295+
if (fileSize > _attachmentSizeLimit)
296+
{
297+
context.Error($"$GITHUB_STEP_SUMMARY supports content up a size of {_attachmentSizeLimit / 1024}k got {fileSize / 1024}k");
298+
Trace.Info($"Step Summary file ({filePath}) is too large ({fileSize} bytes); skipping attachment upload");
299+
300+
return;
301+
}
302+
303+
Trace.Verbose($"Step Summary file exists: {filePath} and has a file size of {fileSize} bytes");
304+
var scrubbedFilePath = filePath + "-scrubbed";
305+
306+
using (var streamReader = new StreamReader(filePath))
307+
using (var streamWriter = new StreamWriter(scrubbedFilePath))
308+
{
309+
string line;
310+
while ((line = streamReader.ReadLine()) != null)
311+
{
312+
var maskedLine = HostContext.SecretMasker.MaskSecrets(line);
313+
streamWriter.WriteLine(maskedLine);
314+
}
315+
}
316+
317+
var attachmentName = context.Id.ToString();
318+
319+
Trace.Info($"Queueing file ({filePath}) for attachment upload ({attachmentName})");
320+
context.QueueAttachFile(ChecksAttachmentType.StepSummary, attachmentName, scrubbedFilePath);
321+
}
322+
catch (Exception e)
323+
{
324+
Trace.Error($"Error while processing file ({filePath}): {e}");
325+
context.Error($"Failed to create step summary using 'GITHUB_STEP_SUMMARY': {e.Message}");
326+
}
327+
}
328+
}
262329
}

src/Runner.Worker/GitHubContext.cs

+1
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ public sealed class GitHubContext : DictionaryContextData, IEnvironmentContextDa
3434
"run_number",
3535
"server_url",
3636
"sha",
37+
"step_summary",
3738
"workflow",
3839
"workspace",
3940
};

src/Sdk/DTWebApi/WebApi/TaskAttachment.cs

+6
Original file line numberDiff line numberDiff line change
@@ -102,4 +102,10 @@ public class CoreAttachmentType
102102
public static readonly String FileAttachment = "DistributedTask.Core.FileAttachment";
103103
public static readonly String DiagnosticLog = "DistributedTask.Core.DiagnosticLog";
104104
}
105+
106+
[GenerateAllConstants]
107+
public class ChecksAttachmentType
108+
{
109+
public static readonly String StepSummary = "Checks.Step.Summary";
110+
}
105111
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,241 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.IO;
4+
using System.Text;
5+
using System.Runtime.CompilerServices;
6+
using GitHub.Runner.Common.Util;
7+
using GitHub.Runner.Sdk;
8+
using GitHub.Runner.Worker;
9+
using Moq;
10+
using Xunit;
11+
using DTWebApi = GitHub.DistributedTask.WebApi;
12+
using GitHub.DistributedTask.WebApi;
13+
14+
namespace GitHub.Runner.Common.Tests.Worker
15+
{
16+
public sealed class CreateStepSummaryCommandL0
17+
{
18+
private Mock<IExecutionContext> _executionContext;
19+
private List<Tuple<DTWebApi.Issue, string>> _issues;
20+
private Variables _variables;
21+
private string _rootDirectory;
22+
private CreateStepSummaryCommand _createStepCommand;
23+
private ITraceWriter _trace;
24+
25+
[Fact]
26+
[Trait("Level", "L0")]
27+
[Trait("Category", "Worker")]
28+
public void CreateStepSummaryCommand_FeatureDisabled()
29+
{
30+
using (var hostContext = Setup(featureFlagState: "false"))
31+
{
32+
var stepSummaryFile = Path.Combine(_rootDirectory, "feature-off");
33+
34+
_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());
35+
36+
Assert.Equal(0, _issues.Count);
37+
}
38+
}
39+
40+
[Fact]
41+
[Trait("Level", "L0")]
42+
[Trait("Category", "Worker")]
43+
public void CreateStepSummaryCommand_FileNull()
44+
{
45+
using (var hostContext = Setup())
46+
{
47+
_createStepCommand.ProcessCommand(_executionContext.Object, null, null);
48+
49+
_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());
50+
Assert.Equal(0, _issues.Count);
51+
}
52+
}
53+
54+
[Fact]
55+
[Trait("Level", "L0")]
56+
[Trait("Category", "Worker")]
57+
public void CreateStepSummaryCommand_DirectoryNotFound()
58+
{
59+
using (var hostContext = Setup())
60+
{
61+
var stepSummaryFile = Path.Combine(_rootDirectory, "directory-not-found", "env");
62+
63+
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
64+
65+
_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());
66+
Assert.Equal(0, _issues.Count);
67+
}
68+
}
69+
70+
[Fact]
71+
[Trait("Level", "L0")]
72+
[Trait("Category", "Worker")]
73+
public void CreateStepSummaryCommand_FileNotFound()
74+
{
75+
using (var hostContext = Setup())
76+
{
77+
var stepSummaryFile = Path.Combine(_rootDirectory, "file-not-found");
78+
79+
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
80+
81+
_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());
82+
Assert.Equal(0, _issues.Count);
83+
}
84+
}
85+
86+
[Fact]
87+
[Trait("Level", "L0")]
88+
[Trait("Category", "Worker")]
89+
public void CreateStepSummaryCommand_EmptyFile()
90+
{
91+
using (var hostContext = Setup())
92+
{
93+
var stepSummaryFile = Path.Combine(_rootDirectory, "empty-file");
94+
File.Create(stepSummaryFile).Dispose();
95+
96+
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
97+
98+
_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());
99+
Assert.Equal(0, _issues.Count);
100+
}
101+
}
102+
103+
[Fact]
104+
[Trait("Level", "L0")]
105+
[Trait("Category", "Worker")]
106+
public void CreateStepSummaryCommand_LargeFile()
107+
{
108+
using (var hostContext = Setup())
109+
{
110+
var stepSummaryFile = Path.Combine(_rootDirectory, "empty-file");
111+
File.WriteAllBytes(stepSummaryFile, new byte[128 * 1024 + 1]);
112+
113+
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
114+
115+
_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, It.IsAny<string>(), It.IsAny<string>()), Times.Never());
116+
Assert.Equal(1, _issues.Count);
117+
}
118+
}
119+
120+
[Fact]
121+
[Trait("Level", "L0")]
122+
[Trait("Category", "Worker")]
123+
public void CreateStepSummaryCommand_Simple()
124+
{
125+
using (var hostContext = Setup())
126+
{
127+
var stepSummaryFile = Path.Combine(_rootDirectory, "simple");
128+
var content = new List<string>
129+
{
130+
"# This is some markdown content",
131+
"",
132+
"## This is more markdown content",
133+
};
134+
WriteContent(stepSummaryFile, content);
135+
136+
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
137+
138+
_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, _executionContext.Object.Id.ToString(), stepSummaryFile + "-scrubbed"), Times.Once());
139+
Assert.Equal(0, _issues.Count);
140+
}
141+
}
142+
143+
[Fact]
144+
[Trait("Level", "L0")]
145+
[Trait("Category", "Worker")]
146+
public void CreateStepSummaryCommand_ScrubSecrets()
147+
{
148+
using (var hostContext = Setup())
149+
{
150+
// configure secretmasker to actually mask secrets
151+
hostContext.SecretMasker.AddRegex("Password=.*");
152+
hostContext.SecretMasker.AddRegex("ghs_.*");
153+
154+
var stepSummaryFile = Path.Combine(_rootDirectory, "simple");
155+
var scrubbedFile = stepSummaryFile + "-scrubbed";
156+
var content = new List<string>
157+
{
158+
"# Password=ThisIsMySecretPassword!",
159+
"",
160+
"# GITHUB_TOKEN ghs_verysecuretoken",
161+
};
162+
WriteContent(stepSummaryFile, content);
163+
164+
_createStepCommand.ProcessCommand(_executionContext.Object, stepSummaryFile, null);
165+
166+
var scrubbedFileContents = File.ReadAllText(scrubbedFile);
167+
Assert.DoesNotContain("ThisIsMySecretPassword!", scrubbedFileContents);
168+
Assert.DoesNotContain("ghs_verysecuretoken", scrubbedFileContents);
169+
170+
_executionContext.Verify(e => e.QueueAttachFile(ChecksAttachmentType.StepSummary, _executionContext.Object.Id.ToString(), scrubbedFile), Times.Once());
171+
Assert.Equal(0, _issues.Count);
172+
}
173+
}
174+
175+
private void WriteContent(
176+
string path,
177+
List<string> content,
178+
string newline = null)
179+
{
180+
if (string.IsNullOrEmpty(newline))
181+
{
182+
newline = Environment.NewLine;
183+
}
184+
185+
var encoding = new UTF8Encoding(true); // Emit BOM
186+
var contentStr = string.Join(newline, content);
187+
File.WriteAllText(path, contentStr, encoding);
188+
}
189+
190+
private TestHostContext Setup([CallerMemberName] string name = "", string featureFlagState = "true")
191+
{
192+
_issues = new List<Tuple<DTWebApi.Issue, string>>();
193+
194+
var hostContext = new TestHostContext(this, name);
195+
196+
// Trace
197+
_trace = hostContext.GetTrace();
198+
199+
_variables = new Variables(hostContext, new Dictionary<string, VariableValue>
200+
{
201+
{ "MySecretName", new VariableValue("My secret value", true) },
202+
{ "DistributedTask.UploadStepSummary", featureFlagState },
203+
});
204+
205+
// Directory for test data
206+
var workDirectory = hostContext.GetDirectory(WellKnownDirectory.Work);
207+
ArgUtil.NotNullOrEmpty(workDirectory, nameof(workDirectory));
208+
Directory.CreateDirectory(workDirectory);
209+
_rootDirectory = Path.Combine(workDirectory, nameof(CreateStepSummaryCommandL0));
210+
Directory.CreateDirectory(_rootDirectory);
211+
212+
// Execution context
213+
_executionContext = new Mock<IExecutionContext>();
214+
_executionContext.Setup(x => x.Global)
215+
.Returns(new GlobalContext
216+
{
217+
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
218+
WriteDebug = true,
219+
Variables = _variables,
220+
});
221+
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
222+
.Callback((DTWebApi.Issue issue, string logMessage) =>
223+
{
224+
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
225+
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
226+
_trace.Info($"Issue '{issue.Type}': {message}");
227+
});
228+
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
229+
.Callback((string tag, string message) =>
230+
{
231+
_trace.Info($"{tag}{message}");
232+
});
233+
234+
//CreateStepSummaryCommand
235+
_createStepCommand = new CreateStepSummaryCommand();
236+
_createStepCommand.Initialize(hostContext);
237+
238+
return hostContext;
239+
}
240+
}
241+
}

0 commit comments

Comments
 (0)