Compare commits

..

25 Commits

Author SHA1 Message Date
John Wesley Walker III
e3e42889af Keep Issue.cs and TimelineRecord.cs aligned in terms of instantiation flow. 2023-02-27 20:48:10 +00:00
John Wesley Walker III
f3961c4895 Refined deserialization fixup. 2023-02-27 17:45:57 +00:00
John Wesley Walker III
cb89be7aac Account for DataContractSerializer vagaries. 2023-02-27 16:38:45 +00:00
John Wesley Walker III
b70f97f183 Restored a using directive that proved to be necessary.
In some cases, references to `System.IO.Compression` members are #ifdef'd and only needed when the OS_WINDOWS compiler directive is present.
2023-02-08 14:15:14 +00:00
John Wesley Walker III
14096a7ee4 Fixed/simplified unit tests.
Close-over KeyValuePairs passed via IssueMetadata to ensure no deferred evaluation.
(An `IEnumerable<KeyValuePair<string, string>>` could very well be a mutable dictionary, so we want to capture its content in the moment -- not some future version of it.)
2023-02-08 13:33:26 +00:00
John Wesley Walker III
4eb9adc958 Added a setter to GitHub.DistributedTask.WebApi.Issue's key-value-pair indexer. 2023-02-07 07:45:00 +00:00
John Wesley Walker III
efc0a92cc7 Let the IssueMetadata class be the mechanism for specifying log message overrides. 2023-01-17 13:43:04 +00:00
John Wesley Walker III
87ababb858 Pivot toward Immutable DistributedTask.WebApi::Issue instances.
The proper way to instantiate a DistributedTask.WebApi::Issue is now via the factory method on an ExecutionContext (IExecutionContext::CreateIssue)
CreateIssue factory.
2023-01-17 11:36:51 +00:00
John Wesley Walker III
3902257e9d Make logging behavior optional in ExecutionContext::AddIssue 2023-01-16 23:34:26 +00:00
Wes Walker
7036627d47 Ensure collected issues are processed only by a non-embedded ExecutionContext.
This was already implicit.  Now, just making it explicit.
2022-12-09 10:37:44 +01:00
Wes Walker
2eeb90a944 [1742] Ensure multiple composite annoations are correctly written.
This implementation uses a collector pattern to allow embedded ExecutionContexts to stash Issue objects for later processing by a non-embedded ancestor ExecutionContext.

Also:
 - Provide explicit constructor implementations for ExecutionContext
 - Leverage explicit constructors to solidify immutability of several ExecutionContext class members.
 - Fixed erroneous call to ExecutionContext.Complete in CompositeActionHandler.cs
 - Use a consistent timestamp for FinishTime in ExecutionContext::Complete
2022-12-09 01:11:21 +01:00
Ava Stancu
369a4eccad Made worker logs available to stdout (#2307)
* Made worker logs available to stdout

* Log Worker Standard out line by line

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>
2022-12-08 16:23:52 -05:00
Ava Stancu
088981a372 Listener stdout logging (#2291)
* Added env variable to control wether the terminal is silent

* Log to stdout if PrintLogToStdout is enabled

* Extracted console logging to stdouttracelistener

* Remove useless usings

* Rewrite TraceListener as superclass

* Only print to stdout if env is set

* Add comment for Console.Out

* Format Listener

* Revert var name in terminal

* Check env in hostcontext instead of Tracing constructor

* Remove superclass & dupe logging code

* Log hostType

* Readonly '_' prefix 'hostType'

* Fix test

* Revert Terminal change

Co-authored-by: Ferenc Hammerl <31069338+fhammerl@users.noreply.github.com>
2022-12-06 16:16:00 +01:00
Nikola Jokic
852a80fcbd Return exit code when MANUALLY_TRAP_SIG is exported (#2285) 2022-11-28 11:33:01 -05:00
Ferenc Hammerl
63640e91fa Backfill notes and version from 'Release 2.299.1 runner.' (#2277)
* Release 2.299.1 runner.

* Fix typo in releaseNote

Co-authored-by: TingluoHuang <TingluoHuang@github.com>
2022-11-22 15:05:47 +01:00
Nikola Jokic
9122fe7e10 added to lowercase on setting github.action_status (#1944) 2022-11-22 15:05:32 +01:00
Nikola Jokic
6b8452170a (delete.sh) Loggin repaired and made runner_name optional defaulting to hostname (#1871)
* Loggin repaired and made runner_name optional defaulting to hostname

* Update scripts/delete.sh

Co-authored-by: Josh Soref <2119212+jsoref@users.noreply.github.com>

Co-authored-by: Josh Soref <2119212+jsoref@users.noreply.github.com>
2022-11-22 14:47:22 +01:00
Nikola Jokic
cc49e65356 added replace to allow create latest svc to apply --replace flag (#2273) 2022-11-22 14:46:11 +01:00
Tatyana Kostromskaya
1632e4a343 Support runner upgrade messages (#2231)
Co-authored-by: Thomas Boop <52323235+thboop@users.noreply.github.com>
2022-11-21 16:17:47 +01:00
Ava Stancu
b465102e7f Updated info on process for requesting features and enhancements (#2259) 2022-11-18 16:07:59 +01:00
Amit Rathi
98c857b927 expose github.actor_id, github.workflow_ref & github.workflow_sha as environment variable (#2249)
* expose workflow refs/sha  as environment variables

* fixes environment variable ordering

* job_workflow_ref/sha aren't available in gh ctx
2022-11-17 11:11:52 -05:00
Chris Patterson
dda53af485 Small change to Node.js 12 deprecation message (#2262)
* Small change to Node.js 12 deprecation message

* Update src/Runner.Common/Constants.cs
2022-11-16 16:25:59 +01:00
Tingluo Huang
c0bc4c02f8 Add RUNNER_ALLOW_RUNASROOT=1 to dockerfile (#2254) 2022-11-15 12:38:11 -05:00
Nikola Jokic
c6630ce285 Dockerfile and workflow change for runner image (#2250)
* Workflow

* add back github-token to id:image

* added handle to image name

* removed core require

* multi line added :latest

* added label

* added repository_owner in IMAGE_NAME

* with the release

* release

* markdown label description

* Remove markdown desciprion

* Remove double quotes in labels

* Reverted back releaseVersion
2022-11-10 12:08:13 -05:00
Tingluo Huang
40ed7f8a40 Forward parameters into run() func in run.sh. (#2240) 2022-11-02 18:13:40 -04:00
49 changed files with 940 additions and 489 deletions

65
.github/workflows/publish-image.yml vendored Normal file
View File

@@ -0,0 +1,65 @@
name: Publish Runner Image
on:
workflow_dispatch:
inputs:
runnerVersion:
type: string
description: Version of the runner being installed
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
jobs:
build:
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Compute image version
id: image
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const inputRunnerVersion = "${{ github.event.inputs.runnerVersion }}"
if (inputRunnerVersion) {
console.log(`Using input runner version ${inputRunnerVersion}`)
core.setOutput('version', inputRunnerVersion);
return
}
const runnerVersion = fs.readFileSync('${{ github.workspace }}/src/runnerversion', 'utf8').replace(/\n$/g, '')
console.log(`Using runner version ${runnerVersion}`)
core.setOutput('version', runnerVersion);
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v2
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v3
with:
context: ./images
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.image.outputs.version }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
build-args: |
RUNNER_VERSION=${{ steps.image.outputs.version }}
push: true
labels: |
org.opencontainers.image.source=${{github.server_url}}/${{github.repository}}
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
org.opencontainers.image.licenses=MIT

View File

@@ -660,3 +660,52 @@ jobs:
asset_path: ${{ github.workspace }}/linux-arm64-trimmedpackages.json
asset_name: actions-runner-linux-arm64-${{ steps.releaseNote.outputs.version }}-trimmedpackages.json
asset_content_type: application/octet-stream
publish-image:
needs: release
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
REGISTRY: ghcr.io
IMAGE_NAME: ${{ github.repository_owner }}/actions-runner
steps:
- name: Checkout repository
uses: actions/checkout@v3
- name: Compute image version
id: image
uses: actions/github-script@v6
with:
script: |
const fs = require('fs');
const runnerVersion = fs.readFileSync('${{ github.workspace }}/releaseVersion', 'utf8').replace(/\n$/g, '')
console.log(`Using runner version ${runnerVersion}`)
core.setOutput('version', runnerVersion);
- name: Setup Docker buildx
uses: docker/setup-buildx-action@v2
- name: Log into registry ${{ env.REGISTRY }}
uses: docker/login-action@v2
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
id: build-and-push
uses: docker/build-push-action@v3
with:
context: ./images
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.image.outputs.version }}
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest
build-args: |
RUNNER_VERSION=${{ steps.image.outputs.version }}
push: true
labels: |
org.opencontainers.image.source=${{github.server_url}}/${{github.repository}}
org.opencontainers.image.description=https://github.com/actions/runner/releases/tag/v${{ steps.image.outputs.version }}
org.opencontainers.image.licenses=MIT

View File

@@ -22,4 +22,4 @@ Runner releases:
## Contribute
We accept contributions in the form of issues and pull requests. [Read more here](docs/contribute.md) before contributing.
We accept contributions in the form of issues and pull requests. The runner typically requires changes across the entire system and we aim for issues in the runner to be entirely self contained and fixable here. Therefore, we will primarily handle bug issues opened in this repo and we kindly request you to create all feature and enhancement requests on the [GitHub Feedback](https://github.com/community/community/discussions/categories/actions-and-packages) page. [Read more about our guidelines here](docs/contribute.md) before contributing.

View File

@@ -1,6 +1,6 @@
# Contributions
We welcome contributions in the form of issues and pull requests. We view the contributions and the process as the same for github and external contributors.
We welcome contributions in the form of issues and pull requests. We view the contributions and the process as the same for github and external contributors.Please note the runner typically requires changes across the entire system and we aim for issues in the runner to be entirely self contained and fixable here. Therefore, we will primarily handle bug issues opened in this repo and we kindly request you to create all feature and enhancement requests on the [GitHub Feedback](https://github.com/community/community/discussions/categories/actions-and-packages) page.
> IMPORTANT: Building your own runner is critical for the dev inner loop process when contributing changes. However, only runners built and distributed by GitHub (releases) are supported in production. Be aware that workflows and orchestrations run service side with the runner being a remote process to run steps. For that reason, the service can pull the runner forward so customizations can be lost.

24
images/Dockerfile Normal file
View File

@@ -0,0 +1,24 @@
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0 as build
ARG RUNNER_VERSION
ARG RUNNER_ARCH="x64"
ARG RUNNER_CONTAINER_HOOKS_VERSION=0.1.3
RUN apt update -y && apt install curl unzip -y
WORKDIR /actions-runner
RUN curl -f -L -o runner.tar.gz https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-${RUNNER_ARCH}-${RUNNER_VERSION}.tar.gz \
&& tar xzf ./runner.tar.gz \
&& rm runner.tar.gz
RUN curl -f -L -o runner-container-hooks.zip https://github.com/actions/runner-container-hooks/releases/download/v${RUNNER_CONTAINER_HOOKS_VERSION}/actions-runner-hooks-k8s-${RUNNER_CONTAINER_HOOKS_VERSION}.zip \
&& unzip ./runner-container-hooks.zip -d ./k8s \
&& rm runner-container-hooks.zip
FROM mcr.microsoft.com/dotnet/runtime-deps:6.0
ENV RUNNER_ALLOW_RUNASROOT=1
ENV RUNNER_MANUALLY_TRAP_SIG=1
WORKDIR /actions-runner
COPY --from=build /actions-runner .

View File

@@ -2,7 +2,7 @@
- Displays the error logs in dedicated sub-sections of the Initialize containers section (#2182)
- Add generateServiceConfig option for configure command (#2226)
- Setting debug using GitHub Action variables (#2234)
- run.sh installs SIGINT and SIGTERM traps to gracefully stop runner (#2233, 2240)
- run.sh installs SIGINT and SIGTERM traps to gracefully stop runner (#2233, #2240)
## Bugs

View File

@@ -1 +1 @@
2.299.1
<Update to ./src/runnerversion when creating release>

View File

@@ -13,7 +13,7 @@ set -e
flags_found=false
while getopts 's:g:n:r:u:l:d' opt; do
while getopts 's:g:n:r:u:l:df' opt; do
flags_found=true
case $opt in
@@ -35,6 +35,9 @@ while getopts 's:g:n:r:u:l:d' opt; do
l)
labels=$OPTARG
;;
f)
replace='true'
;;
d)
disableupdate='true'
;;
@@ -53,7 +56,8 @@ Usage:
-r optional name of the runner group to add the runner to, defaults to the Default group
-u optional user svc will run as, defaults to current
-l optional list of labels (split by comma) applied on the runner
-d optional allow runner to remain on the current version for one month after the release of a newer version"
-d optional allow runner to remain on the current version for one month after the release of a newer version
-f optional replace any existing runner with the same name"
exit 0
;;
esac
@@ -174,7 +178,7 @@ fi
echo
echo "Configuring ${runner_name} @ $runner_url"
echo "./config.sh --unattended --url $runner_url --token *** --name $runner_name ${labels:+--labels $labels} ${runner_group:+--runnergroup \"$runner_group\"} ${disableupdate:+--disableupdate}"
sudo -E -u ${svc_user} ./config.sh --unattended --url $runner_url --token $RUNNER_TOKEN --name $runner_name ${labels:+--labels $labels} ${runner_group:+--runnergroup "$runner_group"} ${disableupdate:+--disableupdate}
sudo -E -u ${svc_user} ./config.sh --unattended --url $runner_url --token $RUNNER_TOKEN ${replace:+--replace} --name $runner_name ${labels:+--labels $labels} ${runner_group:+--runnergroup "$runner_group"} ${disableupdate:+--disableupdate}
#---------------------------------------
# Configuring as a service

View File

@@ -1,4 +1,4 @@
#/bin/bash
#!/bin/bash
set -e
@@ -12,7 +12,7 @@ set -e
#
# Usage:
# export RUNNER_CFG_PAT=<yourPAT>
# ./delete.sh scope name
# ./delete.sh <scope> [<name>]
#
# scope required repo (:owner/:repo) or org (:organization)
# name optional defaults to hostname. name to delete
@@ -26,8 +26,6 @@ set -e
runner_scope=${1}
runner_name=${2}
echo "Deleting runner ${runner_name} @ ${runner_scope}"
function fatal()
{
echo "error: $1" >&2
@@ -35,8 +33,10 @@ function fatal()
}
if [ -z "${runner_scope}" ]; then fatal "supply scope as argument 1"; fi
if [ -z "${runner_name}" ]; then fatal "supply name as argument 2"; fi
if [ -z "${RUNNER_CFG_PAT}" ]; then fatal "RUNNER_CFG_PAT must be set before calling"; fi
if [ -z "${runner_name}" ]; then runner_name=`hostname`; fi
echo "Deleting runner ${runner_name} @ ${runner_scope}"
which curl || fatal "curl required. Please install in PATH with apt-get, brew, etc"
which jq || fatal "jq required. Please install in PATH with apt-get, brew, etc"

View File

@@ -48,7 +48,7 @@ runWithManualTrap() {
trap - INT TERM
# wait for last parts to be logged
wait $PID
exit 0
exit $returnCode
fi
done
}

View File

@@ -164,7 +164,7 @@ namespace GitHub.Runner.Common
public static readonly string UnsupportedCommandMessageDisabled = "The `{0}` command is disabled. Please upgrade to using Environment Files or opt into unsecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_COMMANDS` environment variable to `true`. For more information see: https://github.blog/changelog/2020-10-01-github-actions-deprecating-set-env-and-add-path-commands/";
public static readonly string UnsupportedStopCommandTokenDisabled = "You cannot use a endToken that is an empty string, the string 'pause-logging', or another workflow command. For more information see: https://docs.github.com/actions/learn-github-actions/workflow-commands-for-github-actions#example-stopping-and-starting-workflow-commands or opt into insecure command execution by setting the `ACTIONS_ALLOW_UNSECURE_STOPCOMMAND_TOKENS` environment variable to `true`.";
public static readonly string UnsupportedSummarySize = "$GITHUB_STEP_SUMMARY upload aborted, supports content up to a size of {0}k, got {1}k. For more information see: https://docs.github.com/actions/using-workflows/workflow-commands-for-github-actions#adding-a-markdown-summary";
public static readonly string Node12DetectedAfterEndOfLife = "Node.js 12 actions are deprecated. For more information see: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/. Please update the following actions to use Node.js 16: {0}";
public static readonly string Node12DetectedAfterEndOfLife = "Node.js 12 actions are deprecated. Please update the following actions to use Node.js 16: {0}. For more information see: https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/.";
}
public static class RunnerEvent
@@ -245,6 +245,7 @@ namespace GitHub.Runner.Common
// Set this env var to "node12" to downgrade the node version for internal functions (e.g hashfiles). This does NOT affect the version of node actions.
public static readonly string ForcedInternalNodeVersion = "ACTIONS_RUNNER_FORCED_INTERNAL_NODE_VERSION";
public static readonly string ForcedActionsNodeVersion = "ACTIONS_RUNNER_FORCE_ACTIONS_NODE_VERSION";
public static readonly string PrintLogToStdout = "ACTIONS_RUNNER_PRINT_LOG_TO_STDOUT";
}
public static class System

View File

@@ -94,6 +94,13 @@ namespace GitHub.Runner.Common
this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPreAmpersandEscape);
this.SecretMasker.AddValueEncoder(ValueEncoders.PowerShellPostAmpersandEscape);
// Create StdoutTraceListener if ENV is set
StdoutTraceListener stdoutTraceListener = null;
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Agent.PrintLogToStdout)))
{
stdoutTraceListener = new StdoutTraceListener(hostType);
}
// Create the trace manager.
if (string.IsNullOrEmpty(logFile))
{
@@ -113,11 +120,11 @@ namespace GitHub.Runner.Common
// this should give us _diag folder under runner root directory
string diagLogDirectory = Path.Combine(new DirectoryInfo(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location)).Parent.FullName, Constants.Path.DiagDirectory);
_traceManager = new TraceManager(new HostTraceListener(diagLogDirectory, hostType, logPageSize, logRetentionDays), this.SecretMasker);
_traceManager = new TraceManager(new HostTraceListener(diagLogDirectory, hostType, logPageSize, logRetentionDays), stdoutTraceListener, this.SecretMasker);
}
else
{
_traceManager = new TraceManager(new HostTraceListener(logFile), this.SecretMasker);
_traceManager = new TraceManager(new HostTraceListener(logFile), stdoutTraceListener, this.SecretMasker);
}
_trace = GetTrace(nameof(HostContext));

View File

@@ -600,8 +600,7 @@ namespace GitHub.Runner.Common
{
foreach (var issue in record.Issues)
{
String source;
issue.Data.TryGetValue("sourcepath", out source);
string source = issue["sourcepath"];
Trace.Verbose($" Issue: c={issue.Category}, t={issue.Type}, s={source ?? string.Empty}, m={issue.Message}");
}
}

View File

@@ -38,7 +38,7 @@ namespace GitHub.Runner.Common
Task<TaskAgentSession> CreateAgentSessionAsync(Int32 poolId, TaskAgentSession session, CancellationToken cancellationToken);
Task DeleteAgentMessageAsync(Int32 poolId, Int64 messageId, Guid sessionId, CancellationToken cancellationToken);
Task DeleteAgentSessionAsync(Int32 poolId, Guid sessionId, CancellationToken cancellationToken);
Task<TaskAgentMessage> GetAgentMessageAsync(Int32 poolId, Guid sessionId, Int64? lastMessageId, TaskAgentStatus status, CancellationToken cancellationToken);
Task<TaskAgentMessage> GetAgentMessageAsync(Int32 poolId, Guid sessionId, Int64? lastMessageId, TaskAgentStatus status, string runnerVersion, CancellationToken cancellationToken);
// job request
Task<TaskAgentJobRequest> GetAgentRequestAsync(int poolId, long requestId, CancellationToken cancellationToken);
@@ -272,10 +272,10 @@ namespace GitHub.Runner.Common
return _messageTaskAgentClient.DeleteAgentSessionAsync(poolId, sessionId, cancellationToken: cancellationToken);
}
public Task<TaskAgentMessage> GetAgentMessageAsync(Int32 poolId, Guid sessionId, Int64? lastMessageId, TaskAgentStatus status, CancellationToken cancellationToken)
public Task<TaskAgentMessage> GetAgentMessageAsync(Int32 poolId, Guid sessionId, Int64? lastMessageId, TaskAgentStatus status, string runnerVersion, CancellationToken cancellationToken)
{
CheckConnection(RunnerConnectionType.MessageQueue);
return _messageTaskAgentClient.GetMessageAsync(poolId, sessionId, lastMessageId, status, cancellationToken: cancellationToken);
return _messageTaskAgentClient.GetMessageAsync(poolId, sessionId, lastMessageId, status, runnerVersion, cancellationToken: cancellationToken);
}
//-----------------------------------------------------------------

View File

@@ -0,0 +1,90 @@
using GitHub.Runner.Sdk;
using System;
using System.Diagnostics;
using System.Globalization;
using System.IO;
namespace GitHub.Runner.Common
{
public sealed class StdoutTraceListener : ConsoleTraceListener
{
private readonly string _hostType;
public StdoutTraceListener(string hostType)
{
this._hostType = hostType;
}
// Copied and modified slightly from .Net Core source code. Modification was required to make it compile.
// There must be some TraceFilter extension class that is missing in this source code.
public override void TraceEvent(TraceEventCache eventCache, string source, TraceEventType eventType, int id, string message)
{
if (Filter != null && !Filter.ShouldTrace(eventCache, source, eventType, id, message, null, null, null))
{
return;
}
WriteHeader(source, eventType, id);
WriteLine(message);
WriteFooter(eventCache);
}
internal bool IsEnabled(TraceOptions opts)
{
return (opts & TraceOutputOptions) != 0;
}
// Altered from the original .Net Core implementation.
private void WriteHeader(string source, TraceEventType eventType, int id)
{
string type = null;
switch (eventType)
{
case TraceEventType.Critical:
type = "CRIT";
break;
case TraceEventType.Error:
type = "ERR ";
break;
case TraceEventType.Warning:
type = "WARN";
break;
case TraceEventType.Information:
type = "INFO";
break;
case TraceEventType.Verbose:
type = "VERB";
break;
default:
type = eventType.ToString();
break;
}
Write(StringUtil.Format("[{0} {1:u} {2} {3}] ", _hostType.ToUpperInvariant(), DateTime.UtcNow, type, source));
}
// Copied and modified slightly from .Net Core source code to make it compile. The original code
// accesses a private indentLevel field. In this code it has been modified to use the getter/setter.
private void WriteFooter(TraceEventCache eventCache)
{
if (eventCache == null)
return;
IndentLevel++;
if (IsEnabled(TraceOptions.ProcessId))
WriteLine("ProcessId=" + eventCache.ProcessId);
if (IsEnabled(TraceOptions.ThreadId))
WriteLine("ThreadId=" + eventCache.ThreadId);
if (IsEnabled(TraceOptions.DateTime))
WriteLine("DateTime=" + eventCache.DateTime.ToString("o", CultureInfo.InvariantCulture));
if (IsEnabled(TraceOptions.Timestamp))
WriteLine("Timestamp=" + eventCache.Timestamp);
IndentLevel--;
}
}
}

View File

@@ -18,7 +18,7 @@ namespace GitHub.Runner.Common
string ReadSecret();
void Write(string message, ConsoleColor? colorCode = null);
void WriteLine();
void WriteLine(string line, ConsoleColor? colorCode = null);
void WriteLine(string line, ConsoleColor? colorCode = null, bool skipTracing = false);
void WriteError(Exception ex);
void WriteError(string line);
void WriteSection(string message);
@@ -116,9 +116,12 @@ namespace GitHub.Runner.Common
// Do not add a format string overload. Terminal messages are user facing and therefore
// should be localized. Use the Loc method in the StringUtil class.
public void WriteLine(string line, ConsoleColor? colorCode = null)
public void WriteLine(string line, ConsoleColor? colorCode = null, bool skipTracing = false)
{
Trace.Info($"WRITE LINE: {line}");
if (!skipTracing)
{
Trace.Info($"WRITE LINE: {line}");
}
if (!Silent)
{
if (colorCode != null)

View File

@@ -16,21 +16,23 @@ namespace GitHub.Runner.Common
{
private readonly ConcurrentDictionary<string, Tracing> _sources = new(StringComparer.OrdinalIgnoreCase);
private readonly HostTraceListener _hostTraceListener;
private readonly StdoutTraceListener _stdoutTraceListener;
private TraceSetting _traceSetting;
private ISecretMasker _secretMasker;
public TraceManager(HostTraceListener traceListener, ISecretMasker secretMasker)
: this(traceListener, new TraceSetting(), secretMasker)
public TraceManager(HostTraceListener traceListener, StdoutTraceListener stdoutTraceListener, ISecretMasker secretMasker)
: this(traceListener, stdoutTraceListener, new TraceSetting(), secretMasker)
{
}
public TraceManager(HostTraceListener traceListener, TraceSetting traceSetting, ISecretMasker secretMasker)
public TraceManager(HostTraceListener traceListener, StdoutTraceListener stdoutTraceListener, TraceSetting traceSetting, ISecretMasker secretMasker)
{
// Validate and store params.
ArgUtil.NotNull(traceListener, nameof(traceListener));
ArgUtil.NotNull(traceSetting, nameof(traceSetting));
ArgUtil.NotNull(secretMasker, nameof(secretMasker));
_hostTraceListener = traceListener;
_stdoutTraceListener = stdoutTraceListener;
_traceSetting = traceSetting;
_secretMasker = secretMasker;
@@ -81,7 +83,7 @@ namespace GitHub.Runner.Common
Level = sourceTraceLevel.ToSourceLevels()
};
}
return new Tracing(name, _secretMasker, sourceSwitch, _hostTraceListener);
return new Tracing(name, _secretMasker, sourceSwitch, _hostTraceListener, _stdoutTraceListener);
}
}
}

View File

@@ -12,7 +12,7 @@ namespace GitHub.Runner.Common
private ISecretMasker _secretMasker;
private TraceSource _traceSource;
public Tracing(string name, ISecretMasker secretMasker, SourceSwitch sourceSwitch, HostTraceListener traceListener)
public Tracing(string name, ISecretMasker secretMasker, SourceSwitch sourceSwitch, HostTraceListener traceListener, StdoutTraceListener stdoutTraceListener = null)
{
ArgUtil.NotNull(secretMasker, nameof(secretMasker));
_secretMasker = secretMasker;
@@ -27,6 +27,10 @@ namespace GitHub.Runner.Common
}
_traceSource.Listeners.Add(traceListener);
if (stdoutTraceListener != null)
{
_traceSource.Listeners.Add(stdoutTraceListener);
}
}
public void Info(string message)

View File

@@ -400,6 +400,7 @@ namespace GitHub.Runner.Listener
Task<int> workerProcessTask = null;
object _outputLock = new();
List<string> workerOutput = new();
bool printToStdout = StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable(Constants.Variables.Agent.PrintLogToStdout));
using (var processChannel = HostContext.CreateService<IProcessChannel>())
using (var processInvoker = HostContext.CreateService<IProcessInvoker>())
{
@@ -421,7 +422,15 @@ namespace GitHub.Runner.Listener
{
lock (_outputLock)
{
workerOutput.Add(stdout.Data);
if (!stdout.Data.StartsWith("[WORKER"))
{
workerOutput.Add(stdout.Data);
}
if (printToStdout)
{
term.WriteLine(stdout.Data, skipTracing: true);
}
}
}
};
@@ -503,7 +512,7 @@ namespace GitHub.Runner.Listener
var accessToken = systemConnection?.Authorization?.Parameters["AccessToken"];
notification.JobStarted(message.JobId, accessToken, systemConnection.Url);
HostContext.WritePerfCounter($"SentJobToWorker_{requestId.ToString()}");
HostContext.WritePerfCounter($"SentJobToWorker_{requestId}");
try
{
@@ -1005,7 +1014,7 @@ namespace GitHub.Runner.Listener
}
var unhandledExceptionIssue = new Issue() { Type = IssueType.Error, Message = errorMessage };
unhandledExceptionIssue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
unhandledExceptionIssue[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.WorkerCrash;
jobRecord.ErrorCount++;
jobRecord.Issues.Add(unhandledExceptionIssue);
await jobServer.UpdateTimelineRecordsAsync(message.Plan.ScopeIdentifier, message.Plan.PlanType, message.Plan.PlanId, message.Timeline.Id, new TimelineRecord[] { jobRecord }, CancellationToken.None);

View File

@@ -211,6 +211,7 @@ namespace GitHub.Runner.Listener
_session.SessionId,
_lastMessageId,
runnerStatus,
BuildConstants.RunnerPackage.Version,
_getMessagesTokenSource.Token);
// Decrypt the message body if the session is using encryption

View File

@@ -430,12 +430,22 @@ namespace GitHub.Runner.Listener
message = await getNextMessage; //get next message
HostContext.WritePerfCounter($"MessageReceived_{message.MessageType}");
if (string.Equals(message.MessageType, AgentRefreshMessage.MessageType, StringComparison.OrdinalIgnoreCase))
if (string.Equals(message.MessageType, AgentRefreshMessage.MessageType, StringComparison.OrdinalIgnoreCase) ||
string.Equals(message.MessageType, RunnerRefreshMessage.MessageType, StringComparison.OrdinalIgnoreCase))
{
if (autoUpdateInProgress == false)
{
autoUpdateInProgress = true;
var runnerUpdateMessage = JsonUtility.FromString<AgentRefreshMessage>(message.Body);
AgentRefreshMessage runnerUpdateMessage = null;
if (string.Equals(message.MessageType, AgentRefreshMessage.MessageType, StringComparison.OrdinalIgnoreCase))
{
runnerUpdateMessage = JsonUtility.FromString<AgentRefreshMessage>(message.Body);
}
else
{
var brokerRunnerUpdateMessage = JsonUtility.FromString<RunnerRefreshMessage>(message.Body);
runnerUpdateMessage = new AgentRefreshMessage(brokerRunnerUpdateMessage.RunnerId, brokerRunnerUpdateMessage.TargetVersion, TimeSpan.FromSeconds(brokerRunnerUpdateMessage.TimeoutInSeconds));
}
#if DEBUG
// Can mock the update for testing
if (StringUtil.ConvertToBoolean(Environment.GetEnvironmentVariable("GITHUB_ACTIONS_RUNNER_IS_MOCK_UPDATE")))

View File

@@ -270,12 +270,9 @@ namespace GitHub.Runner.Worker
if (string.Equals(blocked, envName, StringComparison.OrdinalIgnoreCase))
{
// Log Telemetry and let user know they shouldn't do this
var issue = new Issue()
{
Type = IssueType.Error,
Message = $"Can't update {blocked} environment variable using ::set-env:: command."
};
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = $"{Constants.Runner.UnsupportedCommand}_{envName}";
var message = $"Can't update {blocked} environment variable using ::set-env:: command.";
var metadata = new IssueMetadata(Constants.Runner.InternalTelemetryIssueDataKey, $"{Constants.Runner.UnsupportedCommand}_{envName}");
var issue = context.CreateIssue(IssueType.Error, message, metadata, true);
context.AddIssue(issue);
return;
@@ -309,12 +306,9 @@ namespace GitHub.Runner.Worker
{
if (context.Global.Variables.GetBoolean("DistributedTask.DeprecateStepOutputCommands") ?? false)
{
var issue = new Issue()
{
Type = IssueType.Warning,
Message = String.Format(Constants.Runner.UnsupportedCommandMessage, this.Command)
};
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.UnsupportedCommand;
var message = string.Format(Constants.Runner.UnsupportedCommandMessage, this.Command);
var metadata = new IssueMetadata(Constants.Runner.InternalTelemetryIssueDataKey, Constants.Runner.UnsupportedCommand);
var issue = context.CreateIssue(IssueType.Warning, message, metadata, true);
context.AddIssue(issue);
}
@@ -344,12 +338,9 @@ namespace GitHub.Runner.Worker
{
if (context.Global.Variables.GetBoolean("DistributedTask.DeprecateStepOutputCommands") ?? false)
{
var issue = new Issue()
{
Type = IssueType.Warning,
Message = String.Format(Constants.Runner.UnsupportedCommandMessage, this.Command)
};
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.UnsupportedCommand;
var message = string.Format(Constants.Runner.UnsupportedCommandMessage, this.Command);
var metadata = new IssueMetadata(Constants.Runner.InternalTelemetryIssueDataKey, Constants.Runner.UnsupportedCommand);
var issue = context.CreateIssue(IssueType.Warning, message, metadata, true);
context.AddIssue(issue);
}
@@ -618,16 +609,11 @@ namespace GitHub.Runner.Worker
context.Debug("Enhanced Annotations not enabled on the server. The 'title', 'end_line', and 'end_column' fields are unsupported.");
}
Issue issue = new()
{
Category = "General",
Type = this.Type,
Message = command.Data
};
var issueCategory = "General";
if (!string.IsNullOrEmpty(file))
{
issue.Category = "Code";
issueCategory = "Code";
if (container != null)
{
@@ -658,14 +644,13 @@ namespace GitHub.Runner.Worker
}
}
foreach (var property in command.Properties)
{
if (!string.Equals(property.Key, Constants.Runner.InternalTelemetryIssueDataKey, StringComparison.OrdinalIgnoreCase))
{
issue.Data[property.Key] = property.Value;
}
}
string keyToExclude = Constants.Runner.InternalTelemetryIssueDataKey;
var filteredDictionaryEntries = command.Properties
.Where(kvp => !string.Equals(kvp.Key, keyToExclude, StringComparison.OrdinalIgnoreCase))
.ToList();
var metadata = new IssueMetadata(issueCategory, false, null, filteredDictionaryEntries);
var issue = context.CreateIssue(this.Type, command.Data, metadata, true);
context.AddIssue(issue);
}

View File

@@ -1,7 +1,7 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.IO.Compression; // required for OS_WINDOWS
using System.Linq;
using System.Net;
using System.Net.Http;

View File

@@ -5,7 +5,6 @@ using System.IO.Compression;
using GitHub.DistributedTask.WebApi;
using System.Linq;
using System.Globalization;
using System.Threading.Tasks;
using Pipelines = GitHub.DistributedTask.Pipelines;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;

View File

@@ -16,6 +16,7 @@ using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using GitHub.Services.Common;
using Newtonsoft.Json;
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
using Pipelines = GitHub.DistributedTask.Pipelines;
@@ -90,7 +91,8 @@ namespace GitHub.Runner.Worker
void SetGitHubContext(string name, string value);
void SetOutput(string name, string value, out string reference);
void SetTimeout(TimeSpan? timeout);
void AddIssue(Issue issue, string message = null);
IReadOnlyIssue CreateIssue(IssueType issueType, string rawMessage, IssueMetadata metadata, bool writeToLog);
void AddIssue(IReadOnlyIssue issue);
void Progress(int percentage, string currentOperation = null);
void UpdateDetailTimelineRecord(TimelineRecord record);
@@ -124,8 +126,10 @@ namespace GitHub.Runner.Worker
private readonly TimelineRecord _record = new();
private readonly Dictionary<Guid, TimelineRecord> _detailRecords = new();
private readonly List<IReadOnlyIssue> _embeddedIssueCollector;
private readonly object _loggerLock = new();
private readonly object _matchersLock = new();
private readonly ExecutionContext _parentExecutionContext;
private event OnMatcherChanged _onMatcherChanged;
@@ -133,7 +137,6 @@ namespace GitHub.Runner.Worker
private IPagingLogger _logger;
private IJobServerQueue _jobServerQueue;
private ExecutionContext _parentExecutionContext;
private Guid _mainTimelineId;
private Guid _detailTimelineId;
@@ -147,6 +150,29 @@ namespace GitHub.Runner.Worker
private long _totalThrottlingDelayInMilliseconds = 0;
private bool _stepTelemetryPublished = false;
public ExecutionContext()
: this(null, false)
{
}
private ExecutionContext(ExecutionContext parent, bool embedded)
{
if (embedded)
{
ArgUtil.NotNull(parent, nameof(parent));
}
_parentExecutionContext = parent;
this.IsEmbedded = embedded;
this.StepTelemetry = new ActionsStepTelemetry
{
IsEmbedded = embedded
};
//Embedded Execution Contexts pseudo-inherit their parent's embeddedIssueCollector.
_embeddedIssueCollector = embedded ? parent._embeddedIssueCollector : new();
}
public Guid Id => _record.Id;
public Guid EmbeddedId { get; private set; }
public string ScopeName { get; private set; }
@@ -159,7 +185,7 @@ namespace GitHub.Runner.Worker
public Dictionary<string, VariableValue> JobOutputs { get; private set; }
public ActionsEnvironmentReference ActionsEnvironment { get; private set; }
public ActionsStepTelemetry StepTelemetry { get; } = new ActionsStepTelemetry();
public ActionsStepTelemetry StepTelemetry { get; private init; }
public DictionaryContextData ExpressionValues { get; } = new DictionaryContextData();
public IList<IFunctionInfo> ExpressionFunctions { get; } = new List<IFunctionInfo>();
@@ -184,7 +210,7 @@ namespace GitHub.Runner.Worker
// An embedded execution context shares the same record ID, record name, and logger
// as its enclosing execution context.
public bool IsEmbedded { get; private set; }
public bool IsEmbedded { get; private init; }
public TaskResult? Result
{
@@ -319,7 +345,7 @@ namespace GitHub.Runner.Worker
{
Trace.Entering();
var child = new ExecutionContext();
var child = new ExecutionContext(this, isEmbedded);
child.Initialize(HostContext);
child.Global = Global;
child.ScopeName = scopeName;
@@ -344,7 +370,6 @@ namespace GitHub.Runner.Worker
child.ExpressionFunctions.Add(item);
}
child._cancellationTokenSource = cancellationTokenSource ?? new CancellationTokenSource();
child._parentExecutionContext = this;
child.EchoOnActionCommand = EchoOnActionCommand;
if (recordOrder != null)
@@ -365,11 +390,9 @@ namespace GitHub.Runner.Worker
child._logger.Setup(_mainTimelineId, recordId);
}
child.IsEmbedded = isEmbedded;
child.StepTelemetry.StepId = recordId;
child.StepTelemetry.Stage = stage.ToString();
child.StepTelemetry.IsEmbedded = isEmbedded;
child.StepTelemetry.StepContextName = child.GetFullyQualifiedContextName(); ;
child.StepTelemetry.StepContextName = child.GetFullyQualifiedContextName();
return child;
}
@@ -411,13 +434,24 @@ namespace GitHub.Runner.Worker
this.Warning($"The job has experienced {TimeSpan.FromMilliseconds(_totalThrottlingDelayInMilliseconds).TotalSeconds} seconds total delay caused by server throttling.");
}
DateTime now = DateTime.UtcNow;
_record.CurrentOperation = currentOperation ?? _record.CurrentOperation;
_record.ResultCode = resultCode ?? _record.ResultCode;
_record.FinishTime = DateTime.UtcNow;
_record.FinishTime = now;
_record.PercentComplete = 100;
_record.Result = _record.Result ?? TaskResult.Succeeded;
_record.State = TimelineRecordState.Completed;
// Before our main timeline's final QueueTimelineRecordUpdate,
// inject any issues collected by embedded ExecutionContexts.
if (!this.IsEmbedded)
{
foreach (var issue in _embeddedIssueCollector)
{
AddIssue(issue);
}
}
_jobServerQueue.QueueTimelineRecordUpdate(_mainTimelineId, _record);
// complete all detail timeline records.
@@ -425,7 +459,7 @@ namespace GitHub.Runner.Worker
{
foreach (var record in _detailRecords)
{
record.Value.FinishTime = record.Value.FinishTime ?? DateTime.UtcNow;
record.Value.FinishTime = record.Value.FinishTime ?? now;
record.Value.PercentComplete = record.Value.PercentComplete ?? 100;
record.Value.Result = record.Value.Result ?? TaskResult.Succeeded;
record.Value.State = TimelineRecordState.Completed;
@@ -545,76 +579,89 @@ namespace GitHub.Runner.Worker
_jobServerQueue.QueueTimelineRecordUpdate(_mainTimelineId, _record);
}
// This is not thread safe, the caller need to take lock before calling issue()
public void AddIssue(Issue issue, string logMessage = null)
// This is not thread safe, the caller needs to take lock before calling issue()
public IReadOnlyIssue CreateIssue(IssueType issueType, string rawMessage, IssueMetadata metadata, bool writeToLog)
{
string refinedMessage = PrimitiveExtensions.TrimExcess(HostContext.SecretMasker.MaskSecrets(rawMessage), _maxIssueMessageLength);
var result = new Issue() {
Type = issueType,
Message = refinedMessage,
};
if (metadata != null)
{
result.Category = metadata.Category;
result.IsInfrastructureIssue = metadata.IsInfrastructureIssue;
foreach (var kvp in metadata.Data)
{
result[kvp.Key] = kvp.Value;
}
}
// It's important to keep track of the step number (key:stepNumber) and the line number (key:logFileLineNumber) of every issue that gets logged.
// Actions UI from the run summary page use both values to easily link to an exact locations in logs where annotations originate from.
if (_record.Order != null)
{
result["stepNumber"] = _record.Order.ToString();
}
string wellKnownTag = null;
Int32? previousCountForIssueType = null;
switch (issueType)
{
case IssueType.Error:
wellKnownTag = WellKnownTags.Error;
previousCountForIssueType = _record.ErrorCount++;
break;
case IssueType.Warning:
wellKnownTag = WellKnownTags.Warning;
previousCountForIssueType = _record.WarningCount++;
break;
case IssueType.Notice:
wellKnownTag = WellKnownTags.Notice;
previousCountForIssueType = _record.NoticeCount++;
break;
}
if (!string.IsNullOrEmpty(wellKnownTag))
{
if (writeToLog)
{
//Note that ::Write() has it's own secret masking logic
string logText = metadata?.LogMessageOverride ?? result.Message;
if (!string.IsNullOrEmpty(logText))
{
long logLineNumber = Write(wellKnownTag, logText);
result["logFileLineNumber"] = logLineNumber.ToString();
}
}
if (previousCountForIssueType.GetValueOrDefault(0) < _maxIssueCount)
{
_record.Issues.Add(result);
}
}
return result;
}
// This is not thread safe, the caller needs to take lock before calling issue()
public void AddIssue(IReadOnlyIssue issue)
{
ArgUtil.NotNull(issue, nameof(issue));
if (string.IsNullOrEmpty(logMessage))
// Embedded ExecutionContexts (a.k.a. Composite actions) should never upload a timeline record to the server.
// Instead, we store processed issues on a shared (psuedo-inherited) list (belonging to the closest
// non-embedded ancestor ExecutionContext) so that they can be processed when that ancestor completes.
if (this.IsEmbedded)
{
logMessage = issue.Message;
_embeddedIssueCollector.Add(issue);
}
issue.Message = HostContext.SecretMasker.MaskSecrets(issue.Message);
if (issue.Message.Length > _maxIssueMessageLength)
else
{
issue.Message = issue.Message[.._maxIssueMessageLength];
_jobServerQueue.QueueTimelineRecordUpdate(_mainTimelineId, _record);
}
// Tracking the line number (logFileLineNumber) and step number (stepNumber) for each issue that gets created
// Actions UI from the run summary page use both values to easily link to an exact locations in logs where annotations originate from
if (_record.Order != null)
{
issue.Data["stepNumber"] = _record.Order.ToString();
}
if (issue.Type == IssueType.Error)
{
if (!string.IsNullOrEmpty(logMessage))
{
long logLineNumber = Write(WellKnownTags.Error, logMessage);
issue.Data["logFileLineNumber"] = logLineNumber.ToString();
}
if (_record.ErrorCount < _maxIssueCount)
{
_record.Issues.Add(issue);
}
_record.ErrorCount++;
}
else if (issue.Type == IssueType.Warning)
{
if (!string.IsNullOrEmpty(logMessage))
{
long logLineNumber = Write(WellKnownTags.Warning, logMessage);
issue.Data["logFileLineNumber"] = logLineNumber.ToString();
}
if (_record.WarningCount < _maxIssueCount)
{
_record.Issues.Add(issue);
}
_record.WarningCount++;
}
else if (issue.Type == IssueType.Notice)
{
if (!string.IsNullOrEmpty(logMessage))
{
long logLineNumber = Write(WellKnownTags.Notice, logMessage);
issue.Data["logFileLineNumber"] = logLineNumber.ToString();
}
if (_record.NoticeCount < _maxIssueCount)
{
_record.Issues.Add(issue);
}
_record.NoticeCount++;
}
_jobServerQueue.QueueTimelineRecordUpdate(_mainTimelineId, _record);
}
public void UpdateDetailTimelineRecord(TimelineRecord record)
@@ -975,16 +1022,7 @@ namespace GitHub.Runner.Worker
if ((issue.Type == IssueType.Error || issue.Type == IssueType.Warning) &&
!string.IsNullOrEmpty(issue.Message))
{
string issueTelemetry;
if (issue.Message.Length > _maxIssueMessageLengthInTelemetry)
{
issueTelemetry = $"{issue.Message[.._maxIssueMessageLengthInTelemetry]}";
}
else
{
issueTelemetry = issue.Message;
}
string issueTelemetry = PrimitiveExtensions.TrimExcess(issue.Message, _maxIssueMessageLengthInTelemetry);
StepTelemetry.ErrorMessages.Add(issueTelemetry);
// Only send over the first 3 issues to avoid sending too much data.
@@ -1158,19 +1196,23 @@ namespace GitHub.Runner.Worker
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void Error(this IExecutionContext context, string message)
{
context.AddIssue(new Issue() { Type = IssueType.Error, Message = message });
var issue = context.CreateIssue(IssueType.Error, message, null, true);
context.AddIssue(issue);
}
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void InfrastructureError(this IExecutionContext context, string message)
{
context.AddIssue(new Issue() { Type = IssueType.Error, Message = message, IsInfrastructureIssue = true });
var metadata = new IssueMetadata(null, true, null, Enumerable.Empty<KeyValuePair<string, string>>());
var issue = context.CreateIssue(IssueType.Error, message, metadata, true);
context.AddIssue(issue);
}
// Do not add a format string overload. See comment on ExecutionContext.Write().
public static void Warning(this IExecutionContext context, string message)
{
context.AddIssue(new Issue() { Type = IssueType.Warning, Message = message });
var issue = context.CreateIssue(IssueType.Warning, message, null, true);
context.AddIssue(issue);
}
// Do not add a format string overload. See comment on ExecutionContext.Write().

View File

@@ -13,6 +13,7 @@ namespace GitHub.Runner.Worker
"action_repository",
"action",
"actor",
"actor_id",
"api_url",
"base_ref",
"env",
@@ -27,8 +28,10 @@ namespace GitHub.Runner.Worker
"ref_protected",
"ref_type",
"ref",
"repository_owner",
"repository",
"repository_id",
"repository_owner",
"repository_owner_id",
"retention_days",
"run_attempt",
"run_id",
@@ -39,7 +42,9 @@ namespace GitHub.Runner.Worker
"step_summary",
"triggering_actor",
"workflow",
"workspace",
"workflow_ref",
"workflow_sha",
"workspace"
};
public IEnumerable<KeyValuePair<string, string>> GetRuntimeEnvironmentVariables()

View File

@@ -239,7 +239,7 @@ namespace GitHub.Runner.Worker.Handlers
// Set action_status to the success of the current composite action
var actionResult = ExecutionContext.Result?.ToActionResult() ?? ActionResult.Success;
step.ExecutionContext.SetGitHubContext("action_status", actionResult.ToString());
step.ExecutionContext.SetGitHubContext("action_status", actionResult.ToString().ToLowerInvariant());
// Initialize env context
Trace.Info("Initialize Env context for embedded step");
@@ -294,7 +294,7 @@ namespace GitHub.Runner.Worker.Handlers
// Evaluation error
Trace.Info("Caught exception from expression for embedded step.env");
step.ExecutionContext.Error(ex);
step.ExecutionContext.Complete(TaskResult.Failed);
SetStepConclusion(step, TaskResult.Failed);
}
// Register Callback

View File

@@ -4,6 +4,7 @@ using System.Globalization;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker.Container;
@@ -97,7 +98,7 @@ namespace GitHub.Runner.Worker.Handlers
var matchers = _matchers;
// Strip color codes
var stripped = line.Contains(_colorCodePrefix) ? _colorCodeRegex.Replace(line, string.Empty) : line;
var refinedLine = line.Contains(_colorCodePrefix) ? _colorCodeRegex.Replace(line, string.Empty) : line;
foreach (var matcher in matchers)
{
@@ -107,8 +108,7 @@ namespace GitHub.Runner.Worker.Handlers
// Match
try
{
match = matcher.Match(stripped);
match = matcher.Match(refinedLine);
break;
}
catch (RegexMatchTimeoutException ex)
@@ -116,7 +116,7 @@ namespace GitHub.Runner.Worker.Handlers
if (attempt < _maxAttempts)
{
// Debug
_executionContext.Debug($"Timeout processing issue matcher '{matcher.Owner}' against line '{stripped}'. Exception: {ex.ToString()}");
_executionContext.Debug($"Timeout processing issue matcher '{matcher.Owner}' against line '{refinedLine}'. Exception: {ex}");
}
else
{
@@ -139,12 +139,10 @@ namespace GitHub.Runner.Worker.Handlers
// Convert to issue
var issue = ConvertToIssue(match);
if (issue != null)
{
// Log issue
_executionContext.AddIssue(issue, stripped);
_executionContext.AddIssue(issue);
return;
}
}
@@ -196,7 +194,7 @@ namespace GitHub.Runner.Worker.Handlers
}
}
private DTWebApi.Issue ConvertToIssue(IssueMatch match)
private DTWebApi.IReadOnlyIssue ConvertToIssue(IssueMatch match)
{
// Validate the message
if (string.IsNullOrWhiteSpace(match.Message))
@@ -225,18 +223,14 @@ namespace GitHub.Runner.Worker.Handlers
return null;
}
var issue = new DTWebApi.Issue
{
Message = match.Message,
Type = issueType,
};
var issueData = new Dictionary<string, string>();
// Line
if (!string.IsNullOrEmpty(match.Line))
{
if (int.TryParse(match.Line, NumberStyles.None, CultureInfo.InvariantCulture, out var line))
{
issue.Data["line"] = line.ToString(CultureInfo.InvariantCulture);
issueData["line"] = line.ToString(CultureInfo.InvariantCulture);
}
else
{
@@ -249,7 +243,7 @@ namespace GitHub.Runner.Worker.Handlers
{
if (int.TryParse(match.Column, NumberStyles.None, CultureInfo.InvariantCulture, out var column))
{
issue.Data["col"] = column.ToString(CultureInfo.InvariantCulture);
issueData["col"] = column.ToString(CultureInfo.InvariantCulture);
}
else
{
@@ -260,7 +254,7 @@ namespace GitHub.Runner.Worker.Handlers
// Code
if (!string.IsNullOrWhiteSpace(match.Code))
{
issue.Data["code"] = match.Code.Trim();
issueData["code"] = match.Code.Trim();
}
// File
@@ -312,7 +306,7 @@ namespace GitHub.Runner.Worker.Handlers
var relativePath = file.Substring(repositoryPath.Length).TrimStart(Path.DirectorySeparatorChar);
// Prefer `/` on all platforms
issue.Data["file"] = relativePath.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
issueData["file"] = relativePath.Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
}
else
{
@@ -327,9 +321,11 @@ namespace GitHub.Runner.Worker.Handlers
}
catch (Exception ex)
{
_executionContext.Debug($"Dropping file value '{match.File}' and fromPath value '{match.FromPath}'. Exception during validation: {ex.ToString()}");
_executionContext.Debug($"Dropping file value '{match.File}' and fromPath value '{match.FromPath}'. Exception during validation: {ex}");
}
var metadata = new IssueMetadata(null, false, match.SourceText, issueData);
var issue = _executionContext.CreateIssue(issueType, match.Message, metadata, true);
return issue;
}

View File

@@ -8,6 +8,31 @@ namespace GitHub.Runner.Worker
{
public delegate void OnMatcherChanged(object sender, MatcherChangedEventArgs e);
public sealed class IssueMetadata
{
public IssueMetadata(string key, string value)
: this(null, false, null, new []{ KeyValuePair.Create(key, value) })
{
}
public IssueMetadata(string category, bool infrastructureIssue, string logMessageOverride, IEnumerable<KeyValuePair<string, string>> data)
{
this.Category = category;
this.IsInfrastructureIssue = infrastructureIssue;
this.LogMessageOverride = logMessageOverride;
// Close-over the incoming IEnumerable to force immediate evaluation.
var empty = Enumerable.Empty<KeyValuePair<string, string>>();
this.Data = new Dictionary<string, string>(data ?? empty, StringComparer.OrdinalIgnoreCase);
}
public readonly string Category;
public readonly bool IsInfrastructureIssue;
public readonly string LogMessageOverride;
public readonly IEnumerable<KeyValuePair<string, string>> Data;
}
public sealed class MatcherChangedEventArgs : EventArgs
{
public MatcherChangedEventArgs(IssueMatcherConfig config)
@@ -69,7 +94,7 @@ namespace GitHub.Runner.Worker
if (regexMatch.Success)
{
return new IssueMatch(null, pattern, regexMatch.Groups, DefaultSeverity);
return new IssueMatch(line, null, pattern, regexMatch.Groups, DefaultSeverity);
}
return null;
@@ -110,13 +135,13 @@ namespace GitHub.Runner.Worker
}
// Return
return new IssueMatch(runningMatch, pattern, regexMatch.Groups, DefaultSeverity);
return new IssueMatch(line, runningMatch, pattern, regexMatch.Groups, DefaultSeverity);
}
// Not the last pattern
else
{
// Store the match
_state[i] = new IssueMatch(runningMatch, pattern, regexMatch.Groups);
_state[i] = new IssueMatch(line, runningMatch, pattern, regexMatch.Groups);
}
}
// Not matched
@@ -184,8 +209,9 @@ namespace GitHub.Runner.Worker
public sealed class IssueMatch
{
public IssueMatch(IssueMatch runningMatch, IssuePattern pattern, GroupCollection groups, string defaultSeverity = null)
public IssueMatch(string sourceText, IssueMatch runningMatch, IssuePattern pattern, GroupCollection groups, string defaultSeverity = null)
{
SourceText = sourceText;
File = runningMatch?.File ?? GetValue(groups, pattern.File);
Line = runningMatch?.Line ?? GetValue(groups, pattern.Line);
Column = runningMatch?.Column ?? GetValue(groups, pattern.Column);
@@ -200,6 +226,8 @@ namespace GitHub.Runner.Worker
}
}
public string SourceText { get; }
public string File { get; }
public string Line { get; }

View File

@@ -660,8 +660,9 @@ namespace GitHub.Runner.Worker
var freeSpaceInMB = driveInfo.AvailableFreeSpace / 1024 / 1024;
if (freeSpaceInMB < lowDiskSpaceThreshold)
{
var issue = new Issue() { Type = IssueType.Warning, Message = $"You are running out of disk space. The runner will stop working when the machine runs out of disk space. Free space left: {freeSpaceInMB} MB" };
issue.Data[Constants.Runner.InternalTelemetryIssueDataKey] = Constants.Runner.LowDiskSpace;
var message = $"You are running out of disk space. The runner will stop working when the machine runs out of disk space. Free space left: {freeSpaceInMB} MB";
var metadata = new IssueMetadata(Constants.Runner.InternalTelemetryIssueDataKey, Constants.Runner.LowDiskSpace);
var issue = context.CreateIssue(IssueType.Warning, message, metadata, true);
context.AddIssue(issue);
return;
}

View File

@@ -84,7 +84,8 @@ namespace GitHub.Runner.Worker
default:
throw new ArgumentException(HostContext.RunnerShutdownReason.ToString(), nameof(HostContext.RunnerShutdownReason));
}
jobContext.AddIssue(new Issue() { Type = IssueType.Error, Message = errorMessage });
var issue = jobContext.CreateIssue(IssueType.Error, errorMessage, null, true);
jobContext.AddIssue(issue);
});
// Validate directory permissions.

View File

@@ -27,6 +27,18 @@ namespace GitHub.Services.Common
}
}
public static string TrimExcess(string text, int maxLength)
{
string result = text;
if (!string.IsNullOrEmpty(result) && result.Length > maxLength)
{
result = result.Substring(0, maxLength);
}
return result;
}
public static string ToBase64StringNoPaddingFromString(string utf8String)
{
return ToBase64StringNoPadding(Encoding.UTF8.GetBytes(utf8String));

View File

@@ -450,6 +450,8 @@ namespace GitHub.DistributedTask.WebApi
/// <param name="poolId"></param>
/// <param name="sessionId"></param>
/// <param name="lastMessageId"></param>
/// <param name="status"></param>
/// <param name="runnerVersion"></param>
/// <param name="userState"></param>
/// <param name="cancellationToken">The cancellation token to cancel operation.</param>
[EditorBrowsable(EditorBrowsableState.Never)]
@@ -458,6 +460,7 @@ namespace GitHub.DistributedTask.WebApi
Guid sessionId,
long? lastMessageId = null,
TaskAgentStatus? status = null,
string runnerVersion = null,
object userState = null,
CancellationToken cancellationToken = default)
{
@@ -475,12 +478,16 @@ namespace GitHub.DistributedTask.WebApi
{
queryParams.Add("status", status.Value.ToString());
}
if (runnerVersion != null)
{
queryParams.Add("runnerVersion", runnerVersion);
}
return SendAsync<TaskAgentMessage>(
httpMethod,
locationId,
routeValues: routeValues,
version: new ApiResourceVersion(5.1, 1),
version: new ApiResourceVersion(6.0, 1),
queryParameters: queryParams,
userState: userState,
cancellationToken: cancellationToken);

View File

@@ -1,30 +1,38 @@
using System;
using System.Collections.Generic;
using System.Runtime.Serialization;
using GitHub.Services.Common;
namespace GitHub.DistributedTask.WebApi
{
public interface IReadOnlyIssue
{
IssueType Type { get; }
string Category { get; }
string Message { get; }
bool? IsInfrastructureIssue { get; }
string this[string key] { get; }
}
[DataContract]
public class Issue
public class Issue : IReadOnlyIssue
{
public Issue()
: this(null)
{
}
private Issue(Issue issueToBeCloned)
private Issue(Issue original)
{
this.Type = issueToBeCloned.Type;
this.Category = issueToBeCloned.Category;
this.Message = issueToBeCloned.Message;
this.IsInfrastructureIssue = issueToBeCloned.IsInfrastructureIssue;
if (issueToBeCloned.m_data != null)
this.EnsureInitialized();
if (original != null)
{
foreach (var item in issueToBeCloned.m_data)
{
this.Data.Add(item);
}
this.Type = original.Type;
this.Category = original.Category;
this.Message = original.Message;
this.IsInfrastructureIssue = original.IsInfrastructureIssue;
m_data.AddRange(original.m_data);
}
}
@@ -36,14 +44,14 @@ namespace GitHub.DistributedTask.WebApi
}
[DataMember(Order = 2)]
public String Category
public string Category
{
get;
set;
}
[DataMember(Order = 3)]
public String Message
public string Message
{
get;
set;
@@ -56,15 +64,16 @@ namespace GitHub.DistributedTask.WebApi
set;
}
public IDictionary<String, String> Data
public string this[string key]
{
get
{
if (m_data == null)
{
m_data = new Dictionary<String, String>(StringComparer.OrdinalIgnoreCase);
}
return m_data;
m_data.TryGetValue(key, out string result);
return result;
}
set
{
m_data[key] = value;
}
}
@@ -77,6 +86,7 @@ namespace GitHub.DistributedTask.WebApi
private void OnDeserialized(StreamingContext context)
{
SerializationHelper.Copy(ref m_serializedData, ref m_data, StringComparer.OrdinalIgnoreCase, true);
this.EnsureInitialized();
}
[OnSerializing]
@@ -91,9 +101,21 @@ namespace GitHub.DistributedTask.WebApi
m_serializedData = null;
}
[DataMember(Name = "Data", EmitDefaultValue = false, Order = 4)]
private IDictionary<String, String> m_serializedData;
/// <summary>
/// DataContractSerializer bypasses all constructor logic and inline initialization!
/// This method takes the place of a workhorse constructor for baseline initialization.
/// The expectation is for this logic to be accessible to constructors and also to the OnDeserialized helper.
/// </summary>
private void EnsureInitialized()
{
//Note that ?? is a short-circuiting operator.
m_data = m_data ?? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
}
[DataMember(Name = "Data", EmitDefaultValue = false, Order = 4)]
private IDictionary<string, string> m_serializedData;
private IDictionary<string, string> m_data;
private IDictionary<String, String> m_data;
}
}

View File

@@ -0,0 +1,49 @@
using Newtonsoft.Json;
using System;
using System.Runtime.Serialization;
namespace GitHub.DistributedTask.WebApi
{
[DataContract]
public sealed class RunnerRefreshMessage
{
public static readonly String MessageType = "RunnerRefresh";
[JsonConstructor]
internal RunnerRefreshMessage()
{
}
public RunnerRefreshMessage(
Int32 runnerId,
String targetVersion,
int? timeoutInSeconds = null)
{
this.RunnerId = runnerId;
this.TimeoutInSeconds = timeoutInSeconds ?? TimeSpan.FromMinutes(60).Seconds;
this.TargetVersion = targetVersion;
}
[DataMember]
public Int32 RunnerId
{
get;
private set;
}
[DataMember]
public int TimeoutInSeconds
{
get;
private set;
}
[DataMember]
public String TargetVersion
{
get;
private set;
}
}
}

View File

@@ -10,69 +10,76 @@ namespace GitHub.DistributedTask.WebApi
public sealed class TimelineRecord
{
public TimelineRecord()
: this(null)
{
this.Attempt = 1;
}
private TimelineRecord(TimelineRecord recordToBeCloned)
{
this.Attempt = recordToBeCloned.Attempt;
this.ChangeId = recordToBeCloned.ChangeId;
this.CurrentOperation = recordToBeCloned.CurrentOperation;
this.FinishTime = recordToBeCloned.FinishTime;
this.Id = recordToBeCloned.Id;
this.Identifier = recordToBeCloned.Identifier;
this.LastModified = recordToBeCloned.LastModified;
this.Location = recordToBeCloned.Location;
this.Name = recordToBeCloned.Name;
this.Order = recordToBeCloned.Order;
this.ParentId = recordToBeCloned.ParentId;
this.PercentComplete = recordToBeCloned.PercentComplete;
this.RecordType = recordToBeCloned.RecordType;
this.Result = recordToBeCloned.Result;
this.ResultCode = recordToBeCloned.ResultCode;
this.StartTime = recordToBeCloned.StartTime;
this.State = recordToBeCloned.State;
this.TimelineId = recordToBeCloned.TimelineId;
this.WorkerName = recordToBeCloned.WorkerName;
this.RefName = recordToBeCloned.RefName;
this.ErrorCount = recordToBeCloned.ErrorCount;
this.WarningCount = recordToBeCloned.WarningCount;
this.NoticeCount = recordToBeCloned.NoticeCount;
this.AgentPlatform = recordToBeCloned.AgentPlatform;
if (recordToBeCloned.Log != null)
this.EnsureInitialized();
if (recordToBeCloned != null)
{
this.Log = new TaskLogReference
this.Attempt = recordToBeCloned.Attempt;
this.ChangeId = recordToBeCloned.ChangeId;
this.CurrentOperation = recordToBeCloned.CurrentOperation;
this.FinishTime = recordToBeCloned.FinishTime;
this.Id = recordToBeCloned.Id;
this.Identifier = recordToBeCloned.Identifier;
this.LastModified = recordToBeCloned.LastModified;
this.Location = recordToBeCloned.Location;
this.Name = recordToBeCloned.Name;
this.Order = recordToBeCloned.Order;
this.ParentId = recordToBeCloned.ParentId;
this.PercentComplete = recordToBeCloned.PercentComplete;
this.RecordType = recordToBeCloned.RecordType;
this.Result = recordToBeCloned.Result;
this.ResultCode = recordToBeCloned.ResultCode;
this.StartTime = recordToBeCloned.StartTime;
this.State = recordToBeCloned.State;
this.TimelineId = recordToBeCloned.TimelineId;
this.WorkerName = recordToBeCloned.WorkerName;
this.RefName = recordToBeCloned.RefName;
this.ErrorCount = recordToBeCloned.ErrorCount;
this.WarningCount = recordToBeCloned.WarningCount;
this.NoticeCount = recordToBeCloned.NoticeCount;
this.AgentPlatform = recordToBeCloned.AgentPlatform;
if (recordToBeCloned.Log != null)
{
Id = recordToBeCloned.Log.Id,
Location = recordToBeCloned.Log.Location,
};
}
this.Log = new TaskLogReference
{
Id = recordToBeCloned.Log.Id,
Location = recordToBeCloned.Log.Location,
};
}
if (recordToBeCloned.Details != null)
{
this.Details = new TimelineReference
if (recordToBeCloned.Details != null)
{
ChangeId = recordToBeCloned.Details.ChangeId,
Id = recordToBeCloned.Details.Id,
Location = recordToBeCloned.Details.Location,
};
}
this.Details = new TimelineReference
{
ChangeId = recordToBeCloned.Details.ChangeId,
Id = recordToBeCloned.Details.Id,
Location = recordToBeCloned.Details.Location,
};
}
if (recordToBeCloned.m_issues?.Count> 0)
{
this.Issues.AddRange(recordToBeCloned.Issues.Select(i => i.Clone()));
}
if (recordToBeCloned.m_issues?.Count > 0)
{
m_issues.AddRange(recordToBeCloned.m_issues.Select(i => i.Clone()));
}
if (recordToBeCloned.m_previousAttempts?.Count > 0)
{
this.PreviousAttempts.AddRange(recordToBeCloned.PreviousAttempts);
}
if (recordToBeCloned.m_previousAttempts?.Count > 0)
{
m_previousAttempts.AddRange(recordToBeCloned.m_previousAttempts);
}
if (recordToBeCloned.m_variables?.Count > 0)
{
this.m_variables = recordToBeCloned.Variables.ToDictionary(k => k.Key, v => v.Value.Clone());
if (recordToBeCloned.m_variables?.Count > 0)
{
// Don't pave over the case-insensitive Dictionary we initialized above.
foreach (var kvp in recordToBeCloned.m_variables) {
m_variables[kvp.Key] = kvp.Value.Clone();
}
}
}
}
@@ -234,10 +241,6 @@ namespace GitHub.DistributedTask.WebApi
{
get
{
if (m_issues == null)
{
m_issues = new List<Issue>();
}
return m_issues;
}
}
@@ -274,22 +277,14 @@ namespace GitHub.DistributedTask.WebApi
{
get
{
if (m_previousAttempts == null)
{
m_previousAttempts = new List<TimelineAttempt>();
}
return m_previousAttempts;
}
}
public IDictionary<String, VariableValue> Variables
public IDictionary<string, VariableValue> Variables
{
get
{
if (m_variables == null)
{
m_variables = new Dictionary<String, VariableValue>(StringComparer.OrdinalIgnoreCase);
}
return m_variables;
}
}
@@ -299,11 +294,32 @@ namespace GitHub.DistributedTask.WebApi
return new TimelineRecord(this);
}
[OnDeserialized]
private void OnDeserialized(StreamingContext context)
{
this.EnsureInitialized();
}
/// <summary>
/// DataContractSerializer bypasses all constructor logic and inline initialization!
/// This method takes the place of a workhorse constructor for baseline initialization.
/// The expectation is for this logic to be accessible to constructors and also to the OnDeserialized helper.
/// </summary>
private void EnsureInitialized()
{
//Note that ?? is a short-circuiting operator.
m_issues = m_issues ?? new List<Issue>();
m_variables = m_variables ?? new Dictionary<string, VariableValue>(StringComparer.OrdinalIgnoreCase);
m_previousAttempts = m_previousAttempts ?? new List<TimelineAttempt>();
this.Attempt = Math.Max(this.Attempt, 1);
}
[DataMember(Name = "Issues", EmitDefaultValue = false, Order = 60)]
private List<Issue> m_issues;
[DataMember(Name = "Variables", EmitDefaultValue = false, Order = 80)]
private Dictionary<String, VariableValue> m_variables;
private Dictionary<string, VariableValue> m_variables;
[DataMember(Name = "PreviousAttempts", EmitDefaultValue = false, Order = 120)]
private List<TimelineAttempt> m_previousAttempts;

View File

@@ -192,8 +192,8 @@ namespace GitHub.Runner.Common.Tests.Listener
_runnerServer
.Setup(x => x.GetAgentMessageAsync(
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<CancellationToken>()))
.Returns(async (Int32 poolId, Guid sessionId, Int64? lastMessageId, TaskAgentStatus status, CancellationToken cancellationToken) =>
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Returns(async (Int32 poolId, Guid sessionId, Int64? lastMessageId, TaskAgentStatus status, string runnerVersion, CancellationToken cancellationToken) =>
{
await Task.Yield();
return messages.Dequeue();
@@ -208,7 +208,7 @@ namespace GitHub.Runner.Common.Tests.Listener
//Assert
_runnerServer
.Verify(x => x.GetAgentMessageAsync(
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<CancellationToken>()), Times.Exactly(arMessages.Length));
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Exactly(arMessages.Length));
}
}
@@ -293,7 +293,7 @@ namespace GitHub.Runner.Common.Tests.Listener
_runnerServer
.Setup(x => x.GetAgentMessageAsync(
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<CancellationToken>()))
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<CancellationToken>()))
.Throws(new TaskAgentAccessTokenExpiredException("test"));
try
{
@@ -311,7 +311,7 @@ namespace GitHub.Runner.Common.Tests.Listener
//Assert
_runnerServer
.Verify(x => x.GetAgentMessageAsync(
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<CancellationToken>()), Times.Once);
_settings.PoolId, expectedSession.SessionId, It.IsAny<long?>(), TaskAgentStatus.Online, It.IsAny<string>(), It.IsAny<CancellationToken>()), Times.Once);
_runnerServer
.Verify(x => x.DeleteAgentSessionAsync(

View File

@@ -59,7 +59,7 @@ namespace GitHub.Runner.Common.Tests
_secretMasker = new SecretMasker();
_secretMasker.AddValueEncoder(ValueEncoders.JsonStringEscape);
_secretMasker.AddValueEncoder(ValueEncoders.UriDataEscape);
_traceManager = new TraceManager(traceListener, _secretMasker);
_traceManager = new TraceManager(traceListener, null, _secretMasker);
_trace = GetTrace(nameof(TestHostContext));
// inject a terminal in silent mode so all console output

View File

@@ -2,6 +2,8 @@
using Xunit;
using GitHub.Runner.Sdk;
using System.Runtime.CompilerServices;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker;
namespace GitHub.Runner.Common.Tests
{
@@ -41,5 +43,26 @@ namespace GitHub.Runner.Common.Tests
Assert.True(Directory.Exists(testDataDir));
return testDataDir;
}
public static IReadOnlyIssue CreateTestIssue(IssueType type, string message, IssueMetadata metadata, bool writeToLog)
{
var result = new Issue()
{
Type = type,
Message = message,
};
if (metadata != null)
{
result.Category = metadata.Category;
result.IsInfrastructureIssue = metadata.IsInfrastructureIssue;
foreach (var kvp in metadata.Data)
{
result[kvp.Key] = kvp.Value;
}
}
return result;
}
}
}

View File

@@ -32,10 +32,10 @@ namespace GitHub.Runner.Common.Tests.Worker
hc.GetTrace().Info($"{tag} {line}");
return 1;
});
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>()))
.Callback((Issue issue, string message) =>
_ec.Setup(x => x.AddIssue(It.IsAny<IReadOnlyIssue>()))
.Callback((IReadOnlyIssue issue) =>
{
hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
hc.GetTrace().Info($"{issue.Type} {issue.Message}");
});
_commandManager.EnablePluginInternalCommand();
@@ -59,10 +59,10 @@ namespace GitHub.Runner.Common.Tests.Worker
hc.GetTrace().Info($"{tag} {line}");
return 1;
});
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>()))
.Callback((Issue issue, string message) =>
_ec.Setup(x => x.AddIssue(It.IsAny<IReadOnlyIssue>()))
.Callback((IReadOnlyIssue issue) =>
{
hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
hc.GetTrace().Info($"{issue.Type} {issue.Message}");
});
_commandManager.EnablePluginInternalCommand();
@@ -92,10 +92,12 @@ namespace GitHub.Runner.Common.Tests.Worker
return 1;
});
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>()))
.Callback((Issue issue, string message) =>
_ec.Setup(x => x.CreateIssue(It.IsAny<IssueType>(), It.IsAny<string>(), It.IsAny<IssueMetadata>(), It.IsAny<bool>()))
.Returns(TestUtil.CreateTestIssue);
_ec.Setup(x => x.AddIssue(It.IsAny<IReadOnlyIssue>()))
.Callback((IReadOnlyIssue issue) =>
{
hc.GetTrace().Info($"{issue.Type} {issue.Message} {message ?? string.Empty}");
hc.GetTrace().Info($"{issue.Type} {issue.Message}");
});
_ec.Object.Global.EnvironmentVariables = new Dictionary<string, string>();

View File

@@ -1,7 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Net.Http;
using System.Runtime.CompilerServices;
@@ -14,6 +13,11 @@ using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
#if OS_WINDOWS
using System.IO.Compression;
#endif
using Moq;
using Moq.Protected;
using Xunit;
@@ -2147,7 +2151,9 @@ runs:
_ec.Object.Global.FileTable = new List<String>();
_ec.Object.Global.Plan = new TaskOrchestrationPlanReference();
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"[{tag}]{message}"); });
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>())).Callback((Issue issue, string message) => { _hc.GetTrace().Info($"[{issue.Type}]{issue.Message ?? message}"); });
_ec.Setup(x => x.CreateIssue(It.IsAny<IssueType>(), It.IsAny<string>(), It.IsAny<IssueMetadata>(), It.IsAny<bool>()))
.Returns(TestUtil.CreateTestIssue);
_ec.Setup(x => x.AddIssue(It.IsAny<IReadOnlyIssue>())).Callback((IReadOnlyIssue issue) => { _hc.GetTrace().Info($"[{issue.Type}]{issue.Message}"); });
_ec.Setup(x => x.GetGitHubContext("workspace")).Returns(Path.Combine(_workFolder, "actions", "actions"));
_dockerManager = new Mock<IDockerCommandManager>();

View File

@@ -4,10 +4,10 @@ using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Expressions;
using GitHub.Services.Common;
using Moq;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Runtime.CompilerServices;
using System.Threading;
@@ -715,7 +715,7 @@ namespace GitHub.Runner.Common.Tests.Worker
//Assert
var err = Assert.Throws<ArgumentException>(() => actionManifest.Load(_ec.Object, action_path));
Assert.Contains($"Fail to load {action_path}", err.Message);
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12' or 'node16'.")), It.IsAny<string>()), Times.Once);
_ec.Verify(x => x.AddIssue(It.Is<IReadOnlyIssue>(s => s.Message.Contains("Missing 'using' value. 'using' requires 'composite', 'docker', 'node12' or 'node16'."))), Times.Once);
}
finally
{
@@ -860,7 +860,10 @@ namespace GitHub.Runner.Common.Tests.Worker
_ec.Setup(x => x.ExpressionValues).Returns(new DictionaryContextData());
_ec.Setup(x => x.ExpressionFunctions).Returns(new List<IFunctionInfo>());
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"{tag}{message}"); });
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>())).Callback((Issue issue, string message) => { _hc.GetTrace().Info($"[{issue.Type}]{issue.Message ?? message}"); });
_ec.Setup(x => x.CreateIssue(It.IsAny<IssueType>(), It.IsAny<string>(), It.IsAny<IssueMetadata>(), It.IsAny<bool>()))
.Returns(TestUtil.CreateTestIssue);
_ec.Setup(x => x.AddIssue(It.IsAny<IReadOnlyIssue>())).Callback((IReadOnlyIssue issue) => { _hc.GetTrace().Info($"[{issue.Type}]{issue.Message}"); });
}
private void Teardown()

View File

@@ -3,20 +3,15 @@ using GitHub.DistributedTask.ObjectTemplating.Tokens;
using GitHub.DistributedTask.Pipelines;
using GitHub.DistributedTask.Pipelines.ContextData;
using GitHub.DistributedTask.WebApi;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using GitHub.Services.Common;
using Moq;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Threading;
using System.Threading.Tasks;
using Xunit;
using Pipelines = GitHub.DistributedTask.Pipelines;
@@ -330,7 +325,7 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal("invalid1", finialInputs["invalid1"]);
Assert.Equal("invalid2", finialInputs["invalid2"]);
_ec.Verify(x => x.AddIssue(It.Is<Issue>(s => s.Message.Contains("Unexpected input(s) 'invalid1', 'invalid2'")), It.IsAny<string>()), Times.Once);
_ec.Verify(x => x.AddIssue(It.Is<IReadOnlyIssue>(s => s.Message.Contains("Unexpected input(s) 'invalid1', 'invalid2'"))), Times.Once);
}
[Fact]
@@ -449,7 +444,9 @@ namespace GitHub.Runner.Common.Tests.Worker
_ec.Setup(x => x.CancellationToken).Returns(_ecTokenSource.Token);
_ec.Object.Global.Variables = new Variables(_hc, new Dictionary<string, VariableValue>());
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>())).Callback((string tag, string message) => { _hc.GetTrace().Info($"[{tag}]{message}"); });
_ec.Setup(x => x.AddIssue(It.IsAny<Issue>(), It.IsAny<string>())).Callback((Issue issue, string message) => { _hc.GetTrace().Info($"[{issue.Type}]{issue.Message ?? message}"); });
_ec.Setup(x => x.CreateIssue(It.IsAny<IssueType>(), It.IsAny<string>(), It.IsAny<IssueMetadata>(), It.IsAny<bool>()))
.Returns(TestUtil.CreateTestIssue);
_ec.Setup(x => x.AddIssue(It.IsAny<IReadOnlyIssue>())).Callback((IReadOnlyIssue issue) => { _hc.GetTrace().Info($"[{issue.Type}]{issue.Message}"); });
_hc.SetSingleton<IActionManager>(_actionManager.Object);
_hc.SetSingleton<IHandlerFactory>(_handlerFactory.Object);

View File

@@ -11,6 +11,7 @@ using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
using GitHub.DistributedTask.WebApi;
using Pipelines = GitHub.DistributedTask.Pipelines;
using GitHub.Services.Common;
namespace GitHub.Runner.Common.Tests.Worker
{
@@ -19,7 +20,7 @@ namespace GitHub.Runner.Common.Tests.Worker
private Mock<IExecutionContext> _executionContext;
private Mock<IJobServerQueue> _jobServerQueue;
private ExecutionContext _jobExecutionContext;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private List<DTWebApi.IReadOnlyIssue> _issues;
private Variables _variables;
private string _rootDirectory;
private CreateStepSummaryCommand _createStepCommand;
@@ -186,7 +187,7 @@ namespace GitHub.Runner.Common.Tests.Worker
{
var hostContext = new TestHostContext(this, name);
_issues = new List<Tuple<DTWebApi.Issue, string>>();
_issues = new List<DTWebApi.IReadOnlyIssue>();
// Setup a job request
TaskOrchestrationPlanReference plan = new();
@@ -247,13 +248,14 @@ namespace GitHub.Runner.Common.Tests.Worker
WriteDebug = true,
Variables = _variables,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
.Callback((DTWebApi.Issue issue, string logMessage) =>
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.IReadOnlyIssue>()))
.Callback((DTWebApi.IReadOnlyIssue issue) =>
{
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
_trace.Info($"Issue '{issue.Type}': {message}");
_issues.Add(issue);
_trace.Info($"Issue '{issue.Type}': {issue.Message}");
});
_executionContext.Setup(x => x.CreateIssue(It.IsAny<IssueType>(), It.IsAny<string>(), It.IsAny<IssueMetadata>(), It.IsAny<bool>()))
.Returns(TestUtil.CreateTestIssue);
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>
{

View File

@@ -52,36 +52,36 @@ namespace GitHub.Runner.Common.Tests.Worker
// Act.
ec.InitializeJob(jobRequest, CancellationToken.None);
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.Complete();
@@ -190,9 +190,9 @@ namespace GitHub.Runner.Common.Tests.Worker
bigMessage += "a";
}
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = bigMessage });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = bigMessage });
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = bigMessage });
ec.AddIssue(ec.CreateIssue(IssueType.Error, bigMessage, null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, bigMessage, null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Notice, bigMessage, null, true));
ec.Complete();
@@ -242,13 +242,13 @@ namespace GitHub.Runner.Common.Tests.Worker
var embeddedStep = ec.CreateChild(Guid.NewGuid(), "action_1_pre", "action_1_pre", null, null, ActionRunStage.Main, isEmbedded: true);
embeddedStep.Start();
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error annotation that should have step and line number information" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning annotation that should have step and line number information" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice annotation that should have step and line number information" });
embeddedStep.AddIssue(embeddedStep.CreateIssue(IssueType.Error, "error annotation that should have step and line number information", null, true));
embeddedStep.AddIssue(embeddedStep.CreateIssue(IssueType.Warning, "warning annotation that should have step and line number information", null, true));
embeddedStep.AddIssue(embeddedStep.CreateIssue(IssueType.Notice, "notice annotation that should have step and line number information", null, true));
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Error).Count() == 1)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Warning).Count() == 1)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i.Data.ContainsKey("stepNumber") && i.Data.ContainsKey("logFileLineNumber") && i.Type == IssueType.Notice).Count() == 1)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i["stepNumber"] != null && i["logFileLineNumber"] != null && i.Type == IssueType.Error).Count() == 1)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i["stepNumber"] != null && i["logFileLineNumber"] != null && i.Type == IssueType.Warning).Count() == 1)), Times.AtLeastOnce);
jobServerQueue.Verify(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.Is<TimelineRecord>(t => t.Issues.Where(i => i["stepNumber"] != null && i["logFileLineNumber"] != null && i.Type == IssueType.Notice).Count() == 1)), Times.AtLeastOnce);
}
}
@@ -626,12 +626,12 @@ namespace GitHub.Runner.Common.Tests.Worker
ec.StepTelemetry.StepId = Guid.NewGuid();
ec.StepTelemetry.Stage = "main";
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
ec.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
ec.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
ec.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Notice, "notice", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
ec.AddIssue(ec.CreateIssue(IssueType.Notice, "notice", null, true));
ec.Complete();
@@ -692,9 +692,9 @@ namespace GitHub.Runner.Common.Tests.Worker
embeddedStep.StepTelemetry.Action = "actions/checkout";
embeddedStep.StepTelemetry.Ref = "v2";
embeddedStep.AddIssue(new Issue() { Type = IssueType.Error, Message = "error" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Warning, Message = "warning" });
embeddedStep.AddIssue(new Issue() { Type = IssueType.Notice, Message = "notice" });
embeddedStep.AddIssue(ec.CreateIssue(IssueType.Error, "error", null, true));
embeddedStep.AddIssue(ec.CreateIssue(IssueType.Warning, "warning", null, true));
embeddedStep.AddIssue(ec.CreateIssue(IssueType.Notice, "notice", null, true));
embeddedStep.PublishStepTelemetry();

View File

@@ -9,6 +9,7 @@ using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using GitHub.Services.Common;
using Moq;
using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
@@ -21,7 +22,7 @@ namespace GitHub.Runner.Common.Tests.Worker
private Mock<IActionCommandManager> _commandManager;
private Variables _variables;
private OnMatcherChanged _onMatcherChanged;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private List<DTWebApi.IReadOnlyIssue> _issues;
private List<string> _messages;
private List<string> _commands;
private OutputManager _outputManager;
@@ -82,10 +83,10 @@ namespace GitHub.Runner.Common.Tests.Worker
Process("ERROR: message 4");
Process("NOT GOOD: message 5");
Assert.Equal(4, _issues.Count);
Assert.Equal("message 1", _issues[0].Item1.Message);
Assert.Equal("message 2", _issues[1].Item1.Message);
Assert.Equal("message 3", _issues[2].Item1.Message);
Assert.Equal("message 5", _issues[3].Item1.Message);
Assert.Equal("message 1", _issues[0].Message);
Assert.Equal("message 2", _issues[1].Message);
Assert.Equal("message 3", _issues[2].Message);
Assert.Equal("message 5", _issues[3].Message);
Assert.Equal(0, _commands.Count);
Assert.Equal(1, _messages.Count);
Assert.Equal("ERROR: message 4", _messages[0]);
@@ -148,11 +149,11 @@ namespace GitHub.Runner.Common.Tests.Worker
Process("ERROR: message 4");
Process("NOT GOOD: message 5");
Assert.Equal(5, _issues.Count);
Assert.Equal("message 1", _issues[0].Item1.Message);
Assert.Equal("message 2", _issues[1].Item1.Message);
Assert.Equal("message 3", _issues[2].Item1.Message);
Assert.Equal("message 4", _issues[3].Item1.Message);
Assert.Equal("message 5", _issues[4].Item1.Message);
Assert.Equal("message 1", _issues[0].Message);
Assert.Equal("message 2", _issues[1].Message);
Assert.Equal("message 3", _issues[2].Message);
Assert.Equal("message 4", _issues[3].Message);
Assert.Equal("message 5", _issues[4].Message);
Assert.Equal(0, _commands.Count);
Assert.Equal(0, _messages.Count);
}
@@ -188,10 +189,10 @@ namespace GitHub.Runner.Common.Tests.Worker
Process("BAD: real bad");
Process(": not working");
Assert.Equal(2, _issues.Count);
Assert.Equal("real bad", _issues[0].Item1.Message);
Assert.Equal("BAD", _issues[0].Item1.Data["code"]);
Assert.Equal("not working", _issues[1].Item1.Message);
Assert.False(_issues[1].Item1.Data.ContainsKey("code"));
Assert.Equal("real bad", _issues[0].Message);
Assert.Equal("BAD", _issues[0]["code"]);
Assert.Equal("not working", _issues[1].Message);
Assert.Null(_issues[1]["code"]);
Assert.Equal(0, _commands.Count);
Assert.Equal(0, _messages.Count);
}
@@ -238,11 +239,11 @@ namespace GitHub.Runner.Common.Tests.Worker
Process("Error: real bad");
Process("regular message 2");
Assert.Equal(5, _issues.Count);
Assert.Equal("it broke", _issues[0].Item1.Message);
Assert.Equal("oh no", _issues[1].Item1.Message);
Assert.Equal("not good", _issues[2].Item1.Message);
Assert.Equal("it broke again", _issues[3].Item1.Message);
Assert.Equal("real bad", _issues[4].Item1.Message);
Assert.Equal("it broke", _issues[0].Message);
Assert.Equal("oh no", _issues[1].Message);
Assert.Equal("not good", _issues[2].Message);
Assert.Equal("it broke again", _issues[3].Message);
Assert.Equal("real bad", _issues[4].Message);
Assert.Equal(0, _commands.Count);
Assert.Equal(4, _messages.Count);
Assert.Equal("Start: hello", _messages[0]);
@@ -293,8 +294,8 @@ namespace GitHub.Runner.Common.Tests.Worker
Process("ERROR: it is broken");
Process("NOT GOOD: that did not work");
Assert.Equal(2, _issues.Count);
Assert.Equal("it is broken", _issues[0].Item1.Message);
Assert.Equal("that did not work", _issues[1].Item1.Message);
Assert.Equal("it is broken", _issues[0].Message);
Assert.Equal("that did not work", _issues[1].Message);
Assert.Equal(0, _commands.Count);
Assert.Equal(0, _messages.Count);
}
@@ -332,15 +333,15 @@ namespace GitHub.Runner.Common.Tests.Worker
Process("(12,thirty-four): it is broken");
Process("(twelve,34): not working");
Assert.Equal(3, _issues.Count);
Assert.Equal("real bad", _issues[0].Item1.Message);
Assert.Equal("12", _issues[0].Item1.Data["line"]);
Assert.Equal("34", _issues[0].Item1.Data["col"]);
Assert.Equal("it is broken", _issues[1].Item1.Message);
Assert.Equal("12", _issues[1].Item1.Data["line"]);
Assert.False(_issues[1].Item1.Data.ContainsKey("col"));
Assert.Equal("not working", _issues[2].Item1.Message);
Assert.False(_issues[2].Item1.Data.ContainsKey("line"));
Assert.Equal("34", _issues[2].Item1.Data["col"]);
Assert.Equal("real bad", _issues[0].Message);
Assert.Equal("12", _issues[0]["line"]);
Assert.Equal("34", _issues[0]["col"]);
Assert.Equal("it is broken", _issues[1].Message);
Assert.Equal("12", _issues[1]["line"]);
Assert.Null(_issues[1]["col"]);
Assert.Equal("not working", _issues[2].Message);
Assert.Null(_issues[2]["line"]);
Assert.Equal("34", _issues[2]["col"]);
Assert.Equal(0, _commands.Count);
Assert.Equal(2, _messages.Count);
Assert.Equal("##[debug]Unable to parse column number 'thirty-four'", _messages[0]);
@@ -373,8 +374,8 @@ namespace GitHub.Runner.Common.Tests.Worker
Process("this line is a command too ##[some-command]even though it contains ERROR: not working again");
Process("##[not-command]this line is an ERROR: it is broken again");
Assert.Equal(2, _issues.Count);
Assert.Equal("it is broken", _issues[0].Item1.Message);
Assert.Equal("it is broken again", _issues[1].Item1.Message);
Assert.Equal("it is broken", _issues[0].Message);
Assert.Equal("it is broken again", _issues[1].Message);
Assert.Equal(2, _commands.Count);
Assert.Equal("##[some-command]this line is a command even though it contains ERROR: not working", _commands[0]);
Assert.Equal("this line is a command too ##[some-command]even though it contains ERROR: not working again", _commands[1]);
@@ -404,8 +405,7 @@ namespace GitHub.Runner.Common.Tests.Worker
});
Process("the error: \033[31mred, \033[1;31mbright red, \033[mreset");
Assert.Equal(1, _issues.Count);
Assert.Equal("red, bright red, reset", _issues[0].Item1.Message);
Assert.Equal("the error: red, bright red, reset", _issues[0].Item2);
Assert.Equal("red, bright red, reset", _issues[0].Message);
Assert.Equal(0, _commands.Count);
Assert.Equal(0, _messages.Count);
}
@@ -455,9 +455,9 @@ namespace GitHub.Runner.Common.Tests.Worker
Process("ERROR: message 3");
Process("NOT GOOD: message 4");
Assert.Equal(3, _issues.Count);
Assert.Equal("message 1", _issues[0].Item1.Message);
Assert.Equal("message 2", _issues[1].Item1.Message);
Assert.Equal("message 4", _issues[2].Item1.Message);
Assert.Equal("message 1", _issues[0].Message);
Assert.Equal("message 2", _issues[1].Message);
Assert.Equal("message 4", _issues[2].Message);
Assert.Equal(0, _commands.Count);
Assert.Equal(1, _messages.Count);
Assert.Equal("ERROR: message 3", _messages[0]);
@@ -517,8 +517,8 @@ namespace GitHub.Runner.Common.Tests.Worker
Process("Matches both line 1: hello again");
Process("oh no, another error");
Assert.Equal(2, _issues.Count);
Assert.Equal("it broke", _issues[0].Item1.Message);
Assert.Equal("oh no, another error", _issues[1].Item1.Message);
Assert.Equal("it broke", _issues[0].Message);
Assert.Equal("oh no, another error", _issues[1].Message);
Assert.Equal(0, _commands.Count);
Assert.Equal(4, _messages.Count);
Assert.Equal("Matches both line 1: hello", _messages[0]);
@@ -573,14 +573,14 @@ namespace GitHub.Runner.Common.Tests.Worker
Process(": not working");
Process("ERROR! uh oh");
Assert.Equal(4, _issues.Count);
Assert.Equal("real bad", _issues[0].Item1.Message);
Assert.Equal(DTWebApi.IssueType.Error, _issues[0].Item1.Type);
Assert.Equal("not great", _issues[1].Item1.Message);
Assert.Equal(DTWebApi.IssueType.Warning, _issues[1].Item1.Type);
Assert.Equal("not working", _issues[2].Item1.Message);
Assert.Equal(DTWebApi.IssueType.Error, _issues[2].Item1.Type);
Assert.Equal("uh oh", _issues[3].Item1.Message);
Assert.Equal(DTWebApi.IssueType.Error, _issues[3].Item1.Type);
Assert.Equal("real bad", _issues[0].Message);
Assert.Equal(DTWebApi.IssueType.Error, _issues[0].Type);
Assert.Equal("not great", _issues[1].Message);
Assert.Equal(DTWebApi.IssueType.Warning, _issues[1].Type);
Assert.Equal("not working", _issues[2].Message);
Assert.Equal(DTWebApi.IssueType.Error, _issues[2].Type);
Assert.Equal("uh oh", _issues[3].Message);
Assert.Equal(DTWebApi.IssueType.Error, _issues[3].Type);
Assert.Equal(0, _commands.Count);
Assert.Equal(2, _messages.Count);
Assert.StartsWith("##[debug]Skipped", _messages[0]);
@@ -633,9 +633,9 @@ namespace GitHub.Runner.Common.Tests.Worker
Process("jane.doe@contoso.com");
Process("ERR: this error");
Assert.Equal(3, _issues.Count);
Assert.Equal("john.doe@contoso.com", _issues[0].Item1.Message);
Assert.Contains("Removing issue matcher 'email'", _issues[1].Item1.Message);
Assert.Equal("this error", _issues[2].Item1.Message);
Assert.Equal("john.doe@contoso.com", _issues[0].Message);
Assert.Contains("Removing issue matcher 'email'", _issues[1].Message);
Assert.Equal("this error", _issues[2].Message);
Assert.Equal(0, _commands.Count);
Assert.Equal(2, _messages.Where(x => x.StartsWith("##[debug]Timeout processing issue matcher")).Count());
Assert.Equal(1, _messages.Where(x => x.Equals("t@t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.t.c%20")).Count());
@@ -725,32 +725,32 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal(9, _issues.Count);
Assert.Equal("some error 1", _issues[0].Item1.Message);
Assert.False(_issues[0].Item1.Data.ContainsKey("file"));
Assert.Equal("some error 1", _issues[0].Message);
Assert.Null(_issues[0]["file"]);
Assert.Equal("some error 2", _issues[1].Item1.Message);
Assert.Equal(file_workflowRepository.Substring(workflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[1].Item1.Data["file"]);
Assert.Equal("some error 2", _issues[1].Message);
Assert.Equal(file_workflowRepository.Substring(workflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[1]["file"]);
Assert.Equal("some error 3", _issues[2].Item1.Message);
Assert.Equal(file_workflowRepository.Substring(workflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[2].Item1.Data["file"]);
Assert.Equal("some error 3", _issues[2].Message);
Assert.Equal(file_workflowRepository.Substring(workflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[2]["file"]);
Assert.Equal("some error 4", _issues[3].Item1.Message);
Assert.Equal(file_workflowRepository_nestedDirectory.Substring(workflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[3].Item1.Data["file"]);
Assert.Equal("some error 4", _issues[3].Message);
Assert.Equal(file_workflowRepository_nestedDirectory.Substring(workflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[3]["file"]);
Assert.Equal("some error 5", _issues[4].Item1.Message);
Assert.False(_issues[4].Item1.Data.ContainsKey("file"));
Assert.Equal("some error 5", _issues[4].Message);
Assert.Null(_issues[4]["file"]);
Assert.Equal("some error 6", _issues[5].Item1.Message);
Assert.False(_issues[5].Item1.Data.ContainsKey("file"));
Assert.Equal("some error 6", _issues[5].Message);
Assert.Null(_issues[5]["file"]);
Assert.Equal("some error 7", _issues[6].Item1.Message);
Assert.False(_issues[6].Item1.Data.ContainsKey("file"));
Assert.Equal("some error 7", _issues[6].Message);
Assert.Null(_issues[6]["file"]);
Assert.Equal("some error 8", _issues[7].Item1.Message);
Assert.Equal(file_nestedWorkflowRepository.Substring(nestedWorkflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[7].Item1.Data["file"]);
Assert.Equal("some error 8", _issues[7].Message);
Assert.Equal(file_nestedWorkflowRepository.Substring(nestedWorkflowRepository.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[7]["file"]);
Assert.Equal("some error 9", _issues[8].Item1.Message);
Assert.Equal(file_workflowRepositoryUsingSsh.Substring(workflowRepositoryUsingSsh.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[8].Item1.Data["file"]);
Assert.Equal("some error 9", _issues[8].Message);
Assert.Equal(file_workflowRepositoryUsingSsh.Substring(workflowRepositoryUsingSsh.Length + 1).Replace(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), _issues[8]["file"]);
}
Environment.SetEnvironmentVariable("RUNNER_TEST_GET_REPOSITORY_PATH_FAILSAFE", "");
@@ -810,11 +810,11 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal(2, _issues.Count);
Assert.Equal("some error 1", _issues[0].Item1.Message);
Assert.Equal("some-file.txt", _issues[0].Item1.Data["file"]);
Assert.Equal("some error 1", _issues[0].Message);
Assert.Equal("some-file.txt", _issues[0]["file"]);
Assert.Equal("some error 2", _issues[1].Item1.Message);
Assert.Equal("some-file.txt", _issues[1].Item1.Data["file"]);
Assert.Equal("some error 2", _issues[1].Message);
Assert.Equal("some-file.txt", _issues[1]["file"]);
}
}
@@ -871,11 +871,11 @@ namespace GitHub.Runner.Common.Tests.Worker
Assert.Equal(2, _issues.Count);
Assert.Equal("some error 1", _issues[0].Item1.Message);
Assert.Equal("some-file.txt", _issues[0].Item1.Data["file"]);
Assert.Equal("some error 1", _issues[0].Message);
Assert.Equal("some-file.txt", _issues[0]["file"]);
Assert.Equal("some error 2", _issues[1].Item1.Message);
Assert.Equal("some-file.txt", _issues[1].Item1.Data["file"]);
Assert.Equal("some error 2", _issues[1].Message);
Assert.Equal("some-file.txt", _issues[1]["file"]);
}
}
#endif
@@ -929,8 +929,8 @@ namespace GitHub.Runner.Common.Tests.Worker
// Process
Process("some-directory/some-file.txt: some error [workflow-repo/some-project/some-project.proj]");
Assert.Equal(1, _issues.Count);
Assert.Equal("some error", _issues[0].Item1.Message);
Assert.Equal("some-project/some-directory/some-file.txt", _issues[0].Item1.Data["file"]);
Assert.Equal("some error", _issues[0].Message);
Assert.Equal("some-project/some-directory/some-file.txt", _issues[0]["file"]);
Assert.Equal(0, _commands.Count);
Assert.Equal(0, _messages.Count);
}
@@ -958,7 +958,7 @@ namespace GitHub.Runner.Common.Tests.Worker
matchers?.Validate();
_onMatcherChanged = null;
_issues = new List<Tuple<DTWebApi.Issue, string>>();
_issues = new List<DTWebApi.IReadOnlyIssue>();
_messages = new List<string>();
_commands = new List<string>();
@@ -983,10 +983,12 @@ namespace GitHub.Runner.Common.Tests.Worker
{
_onMatcherChanged = handler;
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
.Callback((DTWebApi.Issue issue, string logMessage) =>
_executionContext.Setup(x => x.CreateIssue(It.IsAny<DTWebApi.IssueType>(), It.IsAny<string>(), It.IsAny<IssueMetadata>(), It.IsAny<bool>()))
.Returns(TestUtil.CreateTestIssue);
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.IReadOnlyIssue>()))
.Callback((DTWebApi.IReadOnlyIssue issue) =>
{
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
_issues.Add(issue);
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>

View File

@@ -21,7 +21,7 @@ namespace GitHub.Runner.Common.Tests.Worker
public sealed class SaveStateFileCommandL0
{
private Mock<IExecutionContext> _executionContext;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private List<DTWebApi.IReadOnlyIssue> _issues;
private string _rootDirectory;
private SaveStateFileCommand _saveStateFileCommand;
private Dictionary<string, string> _intraActionState;
@@ -390,7 +390,7 @@ namespace GitHub.Runner.Common.Tests.Worker
private TestHostContext Setup([CallerMemberName] string name = "")
{
_issues = new List<Tuple<DTWebApi.Issue, string>>();
_issues = new List<DTWebApi.IReadOnlyIssue>();
_intraActionState = new Dictionary<string, string>();
var hostContext = new TestHostContext(this, name);
@@ -413,12 +413,11 @@ namespace GitHub.Runner.Common.Tests.Worker
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
.Callback((DTWebApi.Issue issue, string logMessage) =>
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.IReadOnlyIssue>()))
.Callback((DTWebApi.IReadOnlyIssue issue) =>
{
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
_trace.Info($"Issue '{issue.Type}': {message}");
_issues.Add(issue);
_trace.Info($"Issue '{issue.Type}': {issue.Message}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>

View File

@@ -1,17 +1,11 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
@@ -21,7 +15,7 @@ namespace GitHub.Runner.Common.Tests.Worker
public sealed class SetEnvFileCommandL0
{
private Mock<IExecutionContext> _executionContext;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private List<DTWebApi.IReadOnlyIssue> _issues;
private string _rootDirectory;
private SetEnvFileCommand _setEnvFileCommand;
private ITraceWriter _trace;
@@ -389,7 +383,7 @@ namespace GitHub.Runner.Common.Tests.Worker
private TestHostContext Setup([CallerMemberName] string name = "")
{
_issues = new List<Tuple<DTWebApi.Issue, string>>();
_issues = new List<DTWebApi.IReadOnlyIssue>();
var hostContext = new TestHostContext(this, name);
@@ -411,12 +405,11 @@ namespace GitHub.Runner.Common.Tests.Worker
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
.Callback((DTWebApi.Issue issue, string logMessage) =>
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.IReadOnlyIssue>()))
.Callback((DTWebApi.IReadOnlyIssue issue) =>
{
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
_trace.Info($"Issue '{issue.Type}': {message}");
_issues.Add(issue);
_trace.Info($"Issue '{issue.Type}': {issue.Message}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>

View File

@@ -1,17 +1,11 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
using GitHub.Runner.Common.Util;
using GitHub.Runner.Sdk;
using GitHub.Runner.Worker;
using GitHub.Runner.Worker.Container;
using GitHub.Runner.Worker.Handlers;
using Moq;
using Xunit;
using DTWebApi = GitHub.DistributedTask.WebApi;
@@ -21,7 +15,7 @@ namespace GitHub.Runner.Common.Tests.Worker
public sealed class SetOutputFileCommandL0
{
private Mock<IExecutionContext> _executionContext;
private List<Tuple<DTWebApi.Issue, string>> _issues;
private List<DTWebApi.IReadOnlyIssue> _issues;
private Dictionary<string, string> _outputs;
private string _rootDirectory;
private SetOutputFileCommand _setOutputFileCommand;
@@ -390,7 +384,7 @@ namespace GitHub.Runner.Common.Tests.Worker
private TestHostContext Setup([CallerMemberName] string name = "")
{
_issues = new List<Tuple<DTWebApi.Issue, string>>();
_issues = new List<DTWebApi.IReadOnlyIssue>();
_outputs = new Dictionary<string, string>();
var hostContext = new TestHostContext(this, name);
@@ -413,12 +407,11 @@ namespace GitHub.Runner.Common.Tests.Worker
EnvironmentVariables = new Dictionary<string, string>(VarUtil.EnvironmentVariableKeyComparer),
WriteDebug = true,
});
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.Issue>(), It.IsAny<string>()))
.Callback((DTWebApi.Issue issue, string logMessage) =>
_executionContext.Setup(x => x.AddIssue(It.IsAny<DTWebApi.IReadOnlyIssue>()))
.Callback((DTWebApi.IReadOnlyIssue issue) =>
{
_issues.Add(new Tuple<DTWebApi.Issue, string>(issue, logMessage));
var message = !string.IsNullOrEmpty(logMessage) ? logMessage : issue.Message;
_trace.Info($"Issue '{issue.Type}': {message}");
_issues.Add(issue);
_trace.Info($"Issue '{issue.Type}': {issue.Message}");
});
_executionContext.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
.Callback((string tag, string message) =>