mirror of
https://github.com/actions/runner.git
synced 2025-12-10 20:36:49 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1413297394 | ||
|
|
6a063ae7be | ||
|
|
76bb4961fb | ||
|
|
bf3d32e631 | ||
|
|
11aa006d30 | ||
|
|
586c8a7fa5 |
@@ -2,10 +2,11 @@
|
||||
- N/A
|
||||
|
||||
## Bugs
|
||||
- Fixed an issue with Strong Name Validation when running as a service on Windows (#185)
|
||||
- Reverted removal of additional fields error and warning fields (#147)
|
||||
- Actions cache would incorrectly cache the action if the tag was updated (#148)
|
||||
|
||||
## Misc
|
||||
- N/A
|
||||
- Updated to .NET Core 3.0 (#127)
|
||||
|
||||
## Agent Downloads
|
||||
|
||||
|
||||
91
src/Misc/dotnet-install.ps1
vendored
91
src/Misc/dotnet-install.ps1
vendored
@@ -37,7 +37,10 @@
|
||||
.PARAMETER SharedRuntime
|
||||
This parameter is obsolete and may be removed in a future version of this script.
|
||||
The recommended alternative is '-Runtime dotnet'.
|
||||
|
||||
Default: false
|
||||
Installs just the shared runtime bits, not the entire SDK.
|
||||
This is equivalent to specifying `-Runtime dotnet`.
|
||||
.PARAMETER Runtime
|
||||
Installs just a shared runtime, not the entire SDK.
|
||||
Possible values:
|
||||
@@ -74,15 +77,11 @@
|
||||
Skips installing non-versioned files if they already exist, such as dotnet.exe.
|
||||
.PARAMETER NoCdn
|
||||
Disable downloading from the Azure CDN, and use the uncached feed directly.
|
||||
.PARAMETER JSonFile
|
||||
Determines the SDK version from a user specified global.json file
|
||||
Note: global.json must have a value for 'SDK:Version'
|
||||
#>
|
||||
[cmdletbinding()]
|
||||
param(
|
||||
[string]$Channel="LTS",
|
||||
[string]$Version="Latest",
|
||||
[string]$JSonFile,
|
||||
[string]$InstallDir="<auto>",
|
||||
[string]$Architecture="<auto>",
|
||||
[ValidateSet("dotnet", "aspnetcore", "windowsdesktop", IgnoreCase = $false)]
|
||||
@@ -259,6 +258,7 @@ function GetHTTPResponse([Uri] $Uri)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
function Get-Latest-Version-Info([string]$AzureFeed, [string]$Channel, [bool]$Coherent) {
|
||||
Say-Invocation $MyInvocation
|
||||
|
||||
@@ -304,64 +304,20 @@ function Get-Latest-Version-Info([string]$AzureFeed, [string]$Channel, [bool]$Co
|
||||
return $VersionInfo
|
||||
}
|
||||
|
||||
function Parse-Jsonfile-For-Version([string]$JSonFile) {
|
||||
|
||||
function Get-Specific-Version-From-Version([string]$AzureFeed, [string]$Channel, [string]$Version) {
|
||||
Say-Invocation $MyInvocation
|
||||
|
||||
If (-Not (Test-Path $JSonFile)) {
|
||||
throw "Unable to find '$JSonFile'"
|
||||
exit 0
|
||||
}
|
||||
try {
|
||||
$JSonContent = Get-Content($JSonFile) -Raw | ConvertFrom-Json | Select-Object -expand "sdk" -ErrorAction SilentlyContinue
|
||||
}
|
||||
catch {
|
||||
throw "Json file unreadable: '$JSonFile'"
|
||||
exit 0
|
||||
}
|
||||
if ($JSonContent) {
|
||||
try {
|
||||
$JSonContent.PSObject.Properties | ForEach-Object {
|
||||
$PropertyName = $_.Name
|
||||
if ($PropertyName -eq "version") {
|
||||
$Version = $_.Value
|
||||
Say-Verbose "Version = $Version"
|
||||
}
|
||||
}
|
||||
switch ($Version.ToLower()) {
|
||||
{ $_ -eq "latest" } {
|
||||
$LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $False
|
||||
return $LatestVersionInfo.Version
|
||||
}
|
||||
catch {
|
||||
throw "Unable to parse the SDK node in '$JSonFile'"
|
||||
exit 0
|
||||
{ $_ -eq "coherent" } {
|
||||
$LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $True
|
||||
return $LatestVersionInfo.Version
|
||||
}
|
||||
}
|
||||
else {
|
||||
throw "Unable to find the SDK node in '$JSonFile'"
|
||||
exit 0
|
||||
}
|
||||
If ($Version -eq $null) {
|
||||
throw "Unable to find the SDK:version node in '$JSonFile'"
|
||||
exit 0
|
||||
}
|
||||
return $Version
|
||||
}
|
||||
|
||||
function Get-Specific-Version-From-Version([string]$AzureFeed, [string]$Channel, [string]$Version, [string]$JSonFile) {
|
||||
Say-Invocation $MyInvocation
|
||||
|
||||
if (-not $JSonFile) {
|
||||
switch ($Version.ToLower()) {
|
||||
{ $_ -eq "latest" } {
|
||||
$LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $False
|
||||
return $LatestVersionInfo.Version
|
||||
}
|
||||
{ $_ -eq "coherent" } {
|
||||
$LatestVersionInfo = Get-Latest-Version-Info -AzureFeed $AzureFeed -Channel $Channel -Coherent $True
|
||||
return $LatestVersionInfo.Version
|
||||
}
|
||||
default { return $Version }
|
||||
}
|
||||
}
|
||||
else {
|
||||
return Parse-Jsonfile-For-Version $JSonFile
|
||||
default { return $Version }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -426,6 +382,23 @@ function Resolve-Installation-Path([string]$InstallDir) {
|
||||
return $InstallDir
|
||||
}
|
||||
|
||||
function Get-Version-Info-From-Version-File([string]$InstallRoot, [string]$RelativePathToVersionFile) {
|
||||
Say-Invocation $MyInvocation
|
||||
|
||||
$VersionFile = Join-Path -Path $InstallRoot -ChildPath $RelativePathToVersionFile
|
||||
Say-Verbose "Local version file: $VersionFile"
|
||||
|
||||
if (Test-Path $VersionFile) {
|
||||
$VersionText = cat $VersionFile
|
||||
Say-Verbose "Local version file text: $VersionText"
|
||||
return Get-Version-Info-From-Version-Text $VersionText
|
||||
}
|
||||
|
||||
Say-Verbose "Local version file not found."
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Is-Dotnet-Package-Installed([string]$InstallRoot, [string]$RelativePathToPackage, [string]$SpecificVersion) {
|
||||
Say-Invocation $MyInvocation
|
||||
|
||||
@@ -561,7 +534,7 @@ function Prepend-Sdk-InstallRoot-To-Path([string]$InstallRoot, [string]$BinFolde
|
||||
}
|
||||
|
||||
$CLIArchitecture = Get-CLIArchitecture-From-Architecture $Architecture
|
||||
$SpecificVersion = Get-Specific-Version-From-Version -AzureFeed $AzureFeed -Channel $Channel -Version $Version -JSonFile $JSonFile
|
||||
$SpecificVersion = Get-Specific-Version-From-Version -AzureFeed $AzureFeed -Channel $Channel -Version $Version
|
||||
$DownloadLink = Get-Download-Link -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture
|
||||
$LegacyDownloadLink = Get-LegacyDownload-Link -AzureFeed $AzureFeed -SpecificVersion $SpecificVersion -CLIArchitecture $CLIArchitecture
|
||||
|
||||
|
||||
125
src/Misc/dotnet-install.sh
vendored
125
src/Misc/dotnet-install.sh
vendored
@@ -435,52 +435,11 @@ get_latest_version_info() {
|
||||
return $?
|
||||
}
|
||||
|
||||
# args:
|
||||
# json_file - $1
|
||||
parse_jsonfile_for_version() {
|
||||
eval $invocation
|
||||
|
||||
local json_file="$1"
|
||||
if [ ! -f "$json_file" ]; then
|
||||
say_err "Unable to find \`$json_file\`"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sdk_section=$(cat $json_file | awk '/"sdk"/,/}/')
|
||||
if [ -z "$sdk_section" ]; then
|
||||
say_err "Unable to parse the SDK node in \`$json_file\`"
|
||||
return 1
|
||||
fi
|
||||
|
||||
sdk_list=$(echo $sdk_section | awk -F"[{}]" '{print $2}')
|
||||
sdk_list=${sdk_list//[\" ]/}
|
||||
sdk_list=${sdk_list//,/$'\n'}
|
||||
sdk_list="$(echo -e "${sdk_list}" | tr -d '[[:space:]]')"
|
||||
|
||||
local version_info=""
|
||||
while read -r line; do
|
||||
IFS=:
|
||||
while read -r key value; do
|
||||
if [[ "$key" == "version" ]]; then
|
||||
version_info=$value
|
||||
fi
|
||||
done <<< "$line"
|
||||
done <<< "$sdk_list"
|
||||
if [ -z "$version_info" ]; then
|
||||
say_err "Unable to find the SDK:version node in \`$json_file\`"
|
||||
return 1
|
||||
fi
|
||||
|
||||
echo "$version_info"
|
||||
return 0
|
||||
}
|
||||
|
||||
# args:
|
||||
# azure_feed - $1
|
||||
# channel - $2
|
||||
# normalized_architecture - $3
|
||||
# version - $4
|
||||
# json_file - $5
|
||||
get_specific_version_from_version() {
|
||||
eval $invocation
|
||||
|
||||
@@ -488,35 +447,27 @@ get_specific_version_from_version() {
|
||||
local channel="$2"
|
||||
local normalized_architecture="$3"
|
||||
local version="$(to_lowercase "$4")"
|
||||
local json_file="$5"
|
||||
|
||||
if [ -z "$json_file" ]; then
|
||||
case "$version" in
|
||||
latest)
|
||||
local version_info
|
||||
version_info="$(get_latest_version_info "$azure_feed" "$channel" "$normalized_architecture" false)" || return 1
|
||||
say_verbose "get_specific_version_from_version: version_info=$version_info"
|
||||
echo "$version_info" | get_version_from_version_info
|
||||
return 0
|
||||
;;
|
||||
coherent)
|
||||
local version_info
|
||||
version_info="$(get_latest_version_info "$azure_feed" "$channel" "$normalized_architecture" true)" || return 1
|
||||
say_verbose "get_specific_version_from_version: version_info=$version_info"
|
||||
echo "$version_info" | get_version_from_version_info
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo "$version"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
else
|
||||
local version_info
|
||||
version_info="$(parse_jsonfile_for_version "$json_file")" || return 1
|
||||
echo "$version_info"
|
||||
return 0
|
||||
fi
|
||||
case "$version" in
|
||||
latest)
|
||||
local version_info
|
||||
version_info="$(get_latest_version_info "$azure_feed" "$channel" "$normalized_architecture" false)" || return 1
|
||||
say_verbose "get_specific_version_from_version: version_info=$version_info"
|
||||
echo "$version_info" | get_version_from_version_info
|
||||
return 0
|
||||
;;
|
||||
coherent)
|
||||
local version_info
|
||||
version_info="$(get_latest_version_info "$azure_feed" "$channel" "$normalized_architecture" true)" || return 1
|
||||
say_verbose "get_specific_version_from_version: version_info=$version_info"
|
||||
echo "$version_info" | get_version_from_version_info
|
||||
return 0
|
||||
;;
|
||||
*)
|
||||
echo "$version"
|
||||
return 0
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# args:
|
||||
@@ -607,6 +558,24 @@ resolve_installation_path() {
|
||||
return 0
|
||||
}
|
||||
|
||||
# args:
|
||||
# install_root - $1
|
||||
get_installed_version_info() {
|
||||
eval $invocation
|
||||
|
||||
local install_root="$1"
|
||||
local version_file="$(combine_paths "$install_root" "$local_version_file_relative_path")"
|
||||
say_verbose "Local version file: $version_file"
|
||||
if [ ! -z "$version_file" ] | [ -r "$version_file" ]; then
|
||||
local version_info="$(cat "$version_file")"
|
||||
echo "$version_info"
|
||||
return 0
|
||||
fi
|
||||
|
||||
say_verbose "Local version file not found."
|
||||
return 0
|
||||
}
|
||||
|
||||
# args:
|
||||
# relative_or_absolute_path - $1
|
||||
get_absolute_path() {
|
||||
@@ -755,7 +724,7 @@ calculate_vars() {
|
||||
normalized_architecture="$(get_normalized_architecture_from_architecture "$architecture")"
|
||||
say_verbose "normalized_architecture=$normalized_architecture"
|
||||
|
||||
specific_version="$(get_specific_version_from_version "$azure_feed" "$channel" "$normalized_architecture" "$version" "$json_file")"
|
||||
specific_version="$(get_specific_version_from_version "$azure_feed" "$channel" "$normalized_architecture" "$version")"
|
||||
say_verbose "specific_version=$specific_version"
|
||||
if [ -z "$specific_version" ]; then
|
||||
say_err "Could not resolve version information."
|
||||
@@ -857,7 +826,6 @@ temporary_file_template="${TMPDIR:-/tmp}/dotnet.XXXXXXXXX"
|
||||
|
||||
channel="LTS"
|
||||
version="Latest"
|
||||
json_file=""
|
||||
install_dir="<auto>"
|
||||
architecture="<auto>"
|
||||
dry_run=false
|
||||
@@ -944,10 +912,6 @@ do
|
||||
runtime_id="$1"
|
||||
non_dynamic_parameters+=" $name "\""$1"\"""
|
||||
;;
|
||||
--jsonfile|-[Jj][Ss]on[Ff]ile)
|
||||
shift
|
||||
json_file="$1"
|
||||
;;
|
||||
--skip-non-versioned-files|-[Ss]kip[Nn]on[Vv]ersioned[Ff]iles)
|
||||
override_non_versioned_files=false
|
||||
non_dynamic_parameters+=" $name"
|
||||
@@ -989,25 +953,22 @@ do
|
||||
echo " Possible values:"
|
||||
echo " - dotnet - the Microsoft.NETCore.App shared runtime"
|
||||
echo " - aspnetcore - the Microsoft.AspNetCore.App shared runtime"
|
||||
echo " --skip-non-versioned-files Skips non-versioned files if they already exist, such as the dotnet executable."
|
||||
echo " -SkipNonVersionedFiles"
|
||||
echo " --dry-run,-DryRun Do not perform installation. Display download link."
|
||||
echo " --no-path, -NoPath Do not set PATH for the current process."
|
||||
echo " --verbose,-Verbose Display diagnostics information."
|
||||
echo " --azure-feed,-AzureFeed Azure feed location. Defaults to $azure_feed, This parameter typically is not changed by the user."
|
||||
echo " --uncached-feed,-UncachedFeed Uncached feed location. This parameter typically is not changed by the user."
|
||||
echo " --feed-credential,-FeedCredential Azure feed shared access token. This parameter typically is not specified."
|
||||
echo " --skip-non-versioned-files Skips non-versioned files if they already exist, such as the dotnet executable."
|
||||
echo " -SkipNonVersionedFiles"
|
||||
echo " --no-cdn,-NoCdn Disable downloading from the Azure CDN, and use the uncached feed directly."
|
||||
echo " --jsonfile <JSONFILE> Determines the SDK version from a user specified global.json file."
|
||||
echo " Note: global.json must have a value for 'SDK:Version'"
|
||||
echo " --feed-credential,-FeedCredential Azure feed shared access token. This parameter typically is not specified."
|
||||
echo " --runtime-id Installs the .NET Tools for the given platform (use linux-x64 for portable linux)."
|
||||
echo " -RuntimeId"
|
||||
echo " -?,--?,-h,--help,-Help Shows this help message"
|
||||
echo ""
|
||||
echo "Obsolete parameters:"
|
||||
echo " --shared-runtime The recommended alternative is '--runtime dotnet'."
|
||||
echo " This parameter is obsolete and may be removed in a future version of this script."
|
||||
echo " Installs just the shared runtime bits, not the entire SDK."
|
||||
echo " -SharedRuntime Installs just the shared runtime bits, not the entire SDK."
|
||||
echo ""
|
||||
echo "Install Location:"
|
||||
echo " Location is chosen in following order:"
|
||||
|
||||
@@ -54,7 +54,6 @@ namespace GitHub.Runner.Common
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.DebugCommandExtension, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.GroupCommandExtension, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.EndGroupCommandExtension, Runner.Worker");
|
||||
Add<T>(extensions, "GitHub.Runner.Worker.EchoCommandExtension, Runner.Worker");
|
||||
break;
|
||||
default:
|
||||
// This should never happen.
|
||||
|
||||
@@ -236,15 +236,15 @@ namespace GitHub.Runner.Plugins.Artifact
|
||||
// try upload all files for the first time.
|
||||
UploadResult uploadResult = await ParallelUploadAsync(context, files, maxConcurrentUploads, _uploadCancellationTokenSource.Token);
|
||||
|
||||
if (uploadResult.RetryFiles.Count == 0)
|
||||
if (uploadResult.FailedFiles.Count == 0)
|
||||
{
|
||||
// all files have been upload succeed.
|
||||
context.Output("File upload complete.");
|
||||
context.Output("File upload succeed.");
|
||||
return uploadResult.TotalFileSizeUploaded;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Output($"{uploadResult.RetryFiles.Count} files failed to upload, retry these files after a minute.");
|
||||
context.Output($"{uploadResult.FailedFiles.Count} files failed to upload, retry these files after a minute.");
|
||||
}
|
||||
|
||||
// Delay 1 min then retry failed files.
|
||||
@@ -255,13 +255,13 @@ namespace GitHub.Runner.Plugins.Artifact
|
||||
}
|
||||
|
||||
// Retry upload all failed files.
|
||||
context.Output($"Start retry {uploadResult.RetryFiles.Count} failed files upload.");
|
||||
UploadResult retryUploadResult = await ParallelUploadAsync(context, uploadResult.RetryFiles, maxConcurrentUploads, _uploadCancellationTokenSource.Token);
|
||||
context.Output($"Start retry {uploadResult.FailedFiles.Count} failed files upload.");
|
||||
UploadResult retryUploadResult = await ParallelUploadAsync(context, uploadResult.FailedFiles, maxConcurrentUploads, _uploadCancellationTokenSource.Token);
|
||||
|
||||
if (retryUploadResult.RetryFiles.Count == 0)
|
||||
if (retryUploadResult.FailedFiles.Count == 0)
|
||||
{
|
||||
// all files have been upload succeed after retry.
|
||||
context.Output("File upload complete after retry.");
|
||||
context.Output("File upload succeed after retry.");
|
||||
return uploadResult.TotalFileSizeUploaded + retryUploadResult.TotalFileSizeUploaded;
|
||||
}
|
||||
else
|
||||
@@ -465,61 +465,75 @@ namespace GitHub.Runner.Plugins.Artifact
|
||||
using (FileStream fs = File.Open(fileToUpload, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
string itemPath = (_containerPath.TrimEnd('/') + "/" + fileToUpload.Remove(0, _sourceParentDirectory.Length + 1)).Replace('\\', '/');
|
||||
bool failAndExit = false;
|
||||
uploadTimer.Restart();
|
||||
bool catchExceptionDuringUpload = false;
|
||||
HttpResponseMessage response = null;
|
||||
try
|
||||
{
|
||||
uploadTimer.Restart();
|
||||
using (HttpResponseMessage response = await _fileContainerHttpClient.UploadFileAsync(_containerId, itemPath, fs, _projectId, cancellationToken: token, chunkSize: 4 * 1024 * 1024))
|
||||
{
|
||||
if (response == null || response.StatusCode != HttpStatusCode.Created)
|
||||
{
|
||||
context.Output($"Unable to copy file to server StatusCode={response?.StatusCode}: {response?.ReasonPhrase}. Source file path: {fileToUpload}. Target server path: {itemPath}");
|
||||
|
||||
if (response?.StatusCode == HttpStatusCode.Conflict)
|
||||
{
|
||||
// fail upload task but continue with any other files
|
||||
context.Error($"Error '{fileToUpload}' has already been uploaded.");
|
||||
}
|
||||
else if (_fileContainerHttpClient.IsFastFailResponse(response))
|
||||
{
|
||||
// Fast fail: we received an http status code where we should abandon our efforts
|
||||
context.Output($"Cannot continue uploading files, so draining upload queue of {_fileUploadQueue.Count} items.");
|
||||
DrainUploadQueue(context);
|
||||
failedFiles.Clear();
|
||||
failAndExit = true;
|
||||
throw new UploadFailedException($"Critical failure uploading '{fileToUpload}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Debug($"Adding '{fileToUpload}' to retry list.");
|
||||
failedFiles.Add(fileToUpload);
|
||||
}
|
||||
throw new UploadFailedException($"Http failure response '{response?.StatusCode}': '{response?.ReasonPhrase}' while uploading '{fileToUpload}'");
|
||||
}
|
||||
|
||||
uploadTimer.Stop();
|
||||
context.Debug($"File: '{fileToUpload}' took {uploadTimer.ElapsedMilliseconds} milliseconds to finish upload");
|
||||
uploadedSize += fs.Length;
|
||||
OutputLogForFile(context, fileToUpload, $"Detail upload trace for file: {itemPath}", context.Debug);
|
||||
}
|
||||
response = await _fileContainerHttpClient.UploadFileAsync(_containerId, itemPath, fs, _projectId, cancellationToken: token, chunkSize: 4 * 1024 * 1024);
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
context.Output($"File upload has been cancelled during upload file: '{fileToUpload}'.");
|
||||
if (response != null)
|
||||
{
|
||||
response.Dispose();
|
||||
response = null;
|
||||
}
|
||||
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
catchExceptionDuringUpload = true;
|
||||
context.Output($"Fail to upload '{fileToUpload}' due to '{ex.Message}'.");
|
||||
context.Output(ex.ToString());
|
||||
}
|
||||
|
||||
OutputLogForFile(context, fileToUpload, $"Detail upload trace for file that fail to upload: {itemPath}", context.Output);
|
||||
|
||||
if (failAndExit)
|
||||
uploadTimer.Stop();
|
||||
if (catchExceptionDuringUpload || (response != null && response.StatusCode != HttpStatusCode.Created))
|
||||
{
|
||||
if (response != null)
|
||||
{
|
||||
context.Debug("Exiting upload.");
|
||||
throw;
|
||||
context.Output($"Unable to copy file to server StatusCode={response.StatusCode}: {response.ReasonPhrase}. Source file path: {fileToUpload}. Target server path: {itemPath}");
|
||||
}
|
||||
|
||||
// output detail upload trace for the file.
|
||||
ConcurrentQueue<string> logQueue;
|
||||
if (_fileUploadTraceLog.TryGetValue(itemPath, out logQueue))
|
||||
{
|
||||
context.Output($"Detail upload trace for file that fail to upload: {itemPath}");
|
||||
string message;
|
||||
while (logQueue.TryDequeue(out message))
|
||||
{
|
||||
context.Output(message);
|
||||
}
|
||||
}
|
||||
|
||||
// tracking file that failed to upload.
|
||||
failedFiles.Add(fileToUpload);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Debug($"File: '{fileToUpload}' took {uploadTimer.ElapsedMilliseconds} milliseconds to finish upload");
|
||||
uploadedSize += fs.Length;
|
||||
// debug detail upload trace for the file.
|
||||
ConcurrentQueue<string> logQueue;
|
||||
if (_fileUploadTraceLog.TryGetValue(itemPath, out logQueue))
|
||||
{
|
||||
context.Debug($"Detail upload trace for file: {itemPath}");
|
||||
string message;
|
||||
while (logQueue.TryDequeue(out message))
|
||||
{
|
||||
context.Debug(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (response != null)
|
||||
{
|
||||
response.Dispose();
|
||||
response = null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -576,30 +590,6 @@ namespace GitHub.Runner.Plugins.Artifact
|
||||
}
|
||||
}
|
||||
|
||||
private void DrainUploadQueue(RunnerActionPluginExecutionContext context)
|
||||
{
|
||||
while (_fileUploadQueue.TryDequeue(out string fileToUpload))
|
||||
{
|
||||
context.Debug($"Clearing upload queue: '{fileToUpload}'");
|
||||
Interlocked.Increment(ref _uploadFilesProcessed);
|
||||
}
|
||||
}
|
||||
|
||||
private void OutputLogForFile(RunnerActionPluginExecutionContext context, string itemPath, string logDescription, Action<string> log)
|
||||
{
|
||||
// output detail upload trace for the file.
|
||||
ConcurrentQueue<string> logQueue;
|
||||
if (_fileUploadTraceLog.TryGetValue(itemPath, out logQueue))
|
||||
{
|
||||
log(logDescription);
|
||||
string message;
|
||||
while (logQueue.TryDequeue(out message))
|
||||
{
|
||||
log(message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UploadFileTraceReportReceived(object sender, ReportTraceEventArgs e)
|
||||
{
|
||||
ConcurrentQueue<string> logQueue = _fileUploadTraceLog.GetOrAdd(e.File, new ConcurrentQueue<string>());
|
||||
@@ -617,22 +607,22 @@ namespace GitHub.Runner.Plugins.Artifact
|
||||
{
|
||||
public UploadResult()
|
||||
{
|
||||
RetryFiles = new List<string>();
|
||||
FailedFiles = new List<string>();
|
||||
TotalFileSizeUploaded = 0;
|
||||
}
|
||||
|
||||
public UploadResult(List<string> retryFiles, long totalFileSizeUploaded)
|
||||
public UploadResult(List<string> failedFiles, long totalFileSizeUploaded)
|
||||
{
|
||||
RetryFiles = retryFiles ?? new List<string>();
|
||||
FailedFiles = failedFiles;
|
||||
TotalFileSizeUploaded = totalFileSizeUploaded;
|
||||
}
|
||||
public List<string> RetryFiles { get; set; }
|
||||
public List<string> FailedFiles { get; set; }
|
||||
|
||||
public long TotalFileSizeUploaded { get; set; }
|
||||
|
||||
public void AddUploadResult(UploadResult resultToAdd)
|
||||
{
|
||||
this.RetryFiles.AddRange(resultToAdd.RetryFiles);
|
||||
this.FailedFiles.AddRange(resultToAdd.FailedFiles);
|
||||
this.TotalFileSizeUploaded += resultToAdd.TotalFileSizeUploaded;
|
||||
}
|
||||
}
|
||||
@@ -667,19 +657,4 @@ namespace GitHub.Runner.Plugins.Artifact
|
||||
this.FailedFiles.AddRange(resultToAdd.FailedFiles);
|
||||
}
|
||||
}
|
||||
|
||||
public class UploadFailedException : Exception
|
||||
{
|
||||
public UploadFailedException()
|
||||
: base()
|
||||
{ }
|
||||
|
||||
public UploadFailedException(string message)
|
||||
: base(message)
|
||||
{ }
|
||||
|
||||
public UploadFailedException(string message, Exception inner)
|
||||
: base(message, inner)
|
||||
{ }
|
||||
}
|
||||
}
|
||||
@@ -74,22 +74,17 @@ namespace GitHub.Runner.Plugins.Artifact
|
||||
context.Output($"Uploading artifact '{artifactName}' from '{fullPath}' for run #{buildId}");
|
||||
|
||||
FileContainerServer fileContainerHelper = new FileContainerServer(context.VssConnection, projectId, containerId, artifactName);
|
||||
long size = await fileContainerHelper.CopyToContainerAsync(context, fullPath, token);
|
||||
var propertiesDictionary = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
try
|
||||
{
|
||||
long size = await fileContainerHelper.CopyToContainerAsync(context, fullPath, token);
|
||||
propertiesDictionary.Add("artifactsize", size.ToString());
|
||||
context.Output($"Uploaded '{size}' bytes from '{fullPath}' to server");
|
||||
}
|
||||
// if any of the results were successful, make sure to attach them to the build
|
||||
finally
|
||||
{
|
||||
string fileContainerFullPath = StringUtil.Format($"#/{containerId}/{artifactName}");
|
||||
BuildServer buildHelper = new BuildServer(context.VssConnection);
|
||||
string jobId = context.Variables.GetValueOrDefault(WellKnownDistributedTaskVariables.JobId).Value ?? string.Empty;
|
||||
var artifact = await buildHelper.AssociateArtifact(projectId, buildId, jobId, artifactName, ArtifactResourceTypes.Container, fileContainerFullPath, propertiesDictionary, token);
|
||||
context.Output($"Associated artifact {artifactName} ({artifact.Id}) with run #{buildId}");
|
||||
}
|
||||
propertiesDictionary.Add("artifactsize", size.ToString());
|
||||
|
||||
string fileContainerFullPath = StringUtil.Format($"#/{containerId}/{artifactName}");
|
||||
context.Output($"Uploaded '{fullPath}' to server");
|
||||
|
||||
BuildServer buildHelper = new BuildServer(context.VssConnection);
|
||||
string jobId = context.Variables.GetValueOrDefault(WellKnownDistributedTaskVariables.JobId).Value ?? string.Empty;
|
||||
var artifact = await buildHelper.AssociateArtifact(projectId, buildId, jobId, artifactName, ArtifactResourceTypes.Container, fileContainerFullPath, propertiesDictionary, token);
|
||||
context.Output($"Associated artifact {artifactName} ({artifact.Id}) with run #{buildId}");
|
||||
}
|
||||
}
|
||||
}
|
||||
BIN
src/Runner.Service/Windows/FinalPublicKey.snk
Normal file
BIN
src/Runner.Service/Windows/FinalPublicKey.snk
Normal file
Binary file not shown.
@@ -9,8 +9,9 @@
|
||||
<AppDesignerFolder>Properties</AppDesignerFolder>
|
||||
<RootNamespace>RunnerService</RootNamespace>
|
||||
<AssemblyName>RunnerService</AssemblyName>
|
||||
<SignAssembly>false</SignAssembly>
|
||||
<DelaySign>false</DelaySign>
|
||||
<SignAssembly>true</SignAssembly>
|
||||
<AssemblyOriginatorKeyFile>FinalPublicKey.snk</AssemblyOriginatorKeyFile>
|
||||
<DelaySign>true</DelaySign>
|
||||
<TargetFrameworkVersion>v4.5</TargetFrameworkVersion>
|
||||
<FileAlignment>512</FileAlignment>
|
||||
<AutoGenerateBindingRedirects>true</AutoGenerateBindingRedirects>
|
||||
@@ -63,6 +64,7 @@
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<None Include="App.config" />
|
||||
<None Include="FinalPublicKey.snk" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Resource.resx">
|
||||
|
||||
@@ -73,7 +73,7 @@ namespace GitHub.Runner.Worker
|
||||
return false;
|
||||
}
|
||||
|
||||
// process action command in serialize order.
|
||||
// process action command in serialize oreder.
|
||||
lock (_commandSerializeLock)
|
||||
{
|
||||
if (_stopProcessCommand)
|
||||
@@ -107,22 +107,26 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
else if (_commandExtensions.TryGetValue(actionCommand.Command, out IActionCommandExtension extension))
|
||||
{
|
||||
if (context.EchoOnActionCommand && !extension.OmitEcho)
|
||||
{
|
||||
context.Output(input);
|
||||
}
|
||||
|
||||
bool omitEcho;
|
||||
try
|
||||
{
|
||||
extension.ProcessCommand(context, input, actionCommand);
|
||||
extension.ProcessCommand(context, input, actionCommand, out omitEcho);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var commandInformation = extension.OmitEcho ? extension.Command : input;
|
||||
context.Error($"Unable to process command '{commandInformation}' successfully.");
|
||||
omitEcho = true;
|
||||
context.Output(input);
|
||||
context.Error($"Unable to process command '{input}' successfully.");
|
||||
context.Error(ex);
|
||||
context.CommandResult = TaskResult.Failed;
|
||||
}
|
||||
|
||||
if (!omitEcho)
|
||||
{
|
||||
context.Output(input);
|
||||
context.Debug($"Processed command");
|
||||
}
|
||||
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -138,19 +142,17 @@ namespace GitHub.Runner.Worker
|
||||
public interface IActionCommandExtension : IExtension
|
||||
{
|
||||
string Command { get; }
|
||||
bool OmitEcho { get; }
|
||||
|
||||
void ProcessCommand(IExecutionContext context, string line, ActionCommand command);
|
||||
void ProcessCommand(IExecutionContext context, string line, ActionCommand command, out bool omitEcho);
|
||||
}
|
||||
|
||||
public sealed class InternalPluginSetRepoPathCommandExtension : RunnerService, IActionCommandExtension
|
||||
{
|
||||
public string Command => "internal-set-repo-path";
|
||||
public bool OmitEcho => false;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, out bool omitEcho)
|
||||
{
|
||||
if (!command.Properties.TryGetValue(SetRepoPathCommandProperties.repoFullName, out string repoFullName) || string.IsNullOrEmpty(repoFullName))
|
||||
{
|
||||
@@ -164,6 +166,8 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
var directoryManager = HostContext.GetService<IPipelineDirectoryManager>();
|
||||
var trackingConfig = directoryManager.UpdateRepositoryDirectory(context, repoFullName, command.Data, StringUtil.ConvertToBoolean(workspaceRepo));
|
||||
|
||||
omitEcho = true;
|
||||
}
|
||||
|
||||
private static class SetRepoPathCommandProperties
|
||||
@@ -176,11 +180,10 @@ namespace GitHub.Runner.Worker
|
||||
public sealed class SetEnvCommandExtension : RunnerService, IActionCommandExtension
|
||||
{
|
||||
public string Command => "set-env";
|
||||
public bool OmitEcho => false;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, out bool omitEcho)
|
||||
{
|
||||
if (!command.Properties.TryGetValue(SetEnvCommandProperties.Name, out string envName) || string.IsNullOrEmpty(envName))
|
||||
{
|
||||
@@ -189,7 +192,9 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
context.EnvironmentVariables[envName] = command.Data;
|
||||
context.SetEnvContext(envName, command.Data);
|
||||
context.Output(line);
|
||||
context.Debug($"{envName}='{command.Data}'");
|
||||
omitEcho = true;
|
||||
}
|
||||
|
||||
private static class SetEnvCommandProperties
|
||||
@@ -201,11 +206,10 @@ namespace GitHub.Runner.Worker
|
||||
public sealed class SetOutputCommandExtension : RunnerService, IActionCommandExtension
|
||||
{
|
||||
public string Command => "set-output";
|
||||
public bool OmitEcho => false;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, out bool omitEcho)
|
||||
{
|
||||
if (!command.Properties.TryGetValue(SetOutputCommandProperties.Name, out string outputName) || string.IsNullOrEmpty(outputName))
|
||||
{
|
||||
@@ -213,7 +217,9 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
|
||||
context.SetOutput(outputName, command.Data, out var reference);
|
||||
context.Output(line);
|
||||
context.Debug($"{reference}='{command.Data}'");
|
||||
omitEcho = true;
|
||||
}
|
||||
|
||||
private static class SetOutputCommandProperties
|
||||
@@ -225,11 +231,10 @@ namespace GitHub.Runner.Worker
|
||||
public sealed class SaveStateCommandExtension : RunnerService, IActionCommandExtension
|
||||
{
|
||||
public string Command => "save-state";
|
||||
public bool OmitEcho => false;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, out bool omitEcho)
|
||||
{
|
||||
if (!command.Properties.TryGetValue(SaveStateCommandProperties.Name, out string stateName) || string.IsNullOrEmpty(stateName))
|
||||
{
|
||||
@@ -238,6 +243,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
context.IntraActionState[stateName] = command.Data;
|
||||
context.Debug($"Save intra-action state {stateName} = {command.Data}");
|
||||
omitEcho = true;
|
||||
}
|
||||
|
||||
private static class SaveStateCommandProperties
|
||||
@@ -249,53 +255,49 @@ namespace GitHub.Runner.Worker
|
||||
public sealed class AddMaskCommandExtension : RunnerService, IActionCommandExtension
|
||||
{
|
||||
public string Command => "add-mask";
|
||||
public bool OmitEcho => true;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, out bool omitEcho)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(command.Data))
|
||||
{
|
||||
context.Warning("Can't add secret mask for empty string in ##[add-mask] command.");
|
||||
context.Warning("Can't add secret mask for empty string.");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (context.EchoOnActionCommand)
|
||||
{
|
||||
context.Output($"::{Command}::***");
|
||||
}
|
||||
|
||||
HostContext.SecretMasker.AddValue(command.Data);
|
||||
Trace.Info($"Add new secret mask with length of {command.Data.Length}");
|
||||
}
|
||||
|
||||
omitEcho = true;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AddPathCommandExtension : RunnerService, IActionCommandExtension
|
||||
{
|
||||
public string Command => "add-path";
|
||||
public bool OmitEcho => false;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, out bool omitEcho)
|
||||
{
|
||||
ArgUtil.NotNullOrEmpty(command.Data, "path");
|
||||
context.PrependPath.RemoveAll(x => string.Equals(x, command.Data, StringComparison.CurrentCulture));
|
||||
context.PrependPath.Add(command.Data);
|
||||
omitEcho = false;
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class AddMatcherCommandExtension : RunnerService, IActionCommandExtension
|
||||
{
|
||||
public string Command => "add-matcher";
|
||||
public bool OmitEcho => false;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, out bool omitEcho)
|
||||
{
|
||||
omitEcho = false;
|
||||
var file = command.Data;
|
||||
|
||||
// File is required
|
||||
@@ -337,26 +339,26 @@ namespace GitHub.Runner.Worker
|
||||
public sealed class RemoveMatcherCommandExtension : RunnerService, IActionCommandExtension
|
||||
{
|
||||
public string Command => "remove-matcher";
|
||||
public bool OmitEcho => false;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, out bool omitEcho)
|
||||
{
|
||||
omitEcho = false;
|
||||
command.Properties.TryGetValue(RemoveMatcherCommandProperties.Owner, out string owner);
|
||||
var file = command.Data;
|
||||
|
||||
// Owner and file are mutually exclusive
|
||||
if (!string.IsNullOrEmpty(owner) && !string.IsNullOrEmpty(file))
|
||||
{
|
||||
context.Warning("Either specify an owner name or a file path in ##[remove-matcher] command. Both values cannot be set.");
|
||||
context.Warning("Either specify a matcher owner name or a file path. Both values cannot be set.");
|
||||
return;
|
||||
}
|
||||
|
||||
// Owner or file is required
|
||||
if (string.IsNullOrEmpty(owner) && string.IsNullOrEmpty(file))
|
||||
{
|
||||
context.Warning("Either an owner name or a file path must be specified in ##[remove-matcher] command.");
|
||||
context.Warning("Either a matcher owner name or a file path must be specified.");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -405,12 +407,12 @@ namespace GitHub.Runner.Worker
|
||||
public sealed class DebugCommandExtension : RunnerService, IActionCommandExtension
|
||||
{
|
||||
public string Command => "debug";
|
||||
public bool OmitEcho => true;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command, out bool omitEcho)
|
||||
{
|
||||
omitEcho = true;
|
||||
context.Debug(command.Data);
|
||||
}
|
||||
}
|
||||
@@ -433,12 +435,12 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
public abstract IssueType Type { get; }
|
||||
public abstract string Command { get; }
|
||||
public bool OmitEcho => true;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string inputLine, ActionCommand command, out bool omitEcho)
|
||||
{
|
||||
omitEcho = true;
|
||||
command.Properties.TryGetValue(IssueCommandProperties.File, out string file);
|
||||
command.Properties.TryGetValue(IssueCommandProperties.Line, out string line);
|
||||
command.Properties.TryGetValue(IssueCommandProperties.Column, out string column);
|
||||
@@ -513,42 +515,13 @@ namespace GitHub.Runner.Worker
|
||||
public abstract class GroupingCommandExtension : RunnerService, IActionCommandExtension
|
||||
{
|
||||
public abstract string Command { get; }
|
||||
public bool OmitEcho => false;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command, out bool omitEcho)
|
||||
{
|
||||
var data = this is GroupCommandExtension ? command.Data : string.Empty;
|
||||
context.Output($"##[{Command}]{data}");
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class EchoCommandExtension : RunnerService, IActionCommandExtension
|
||||
{
|
||||
public string Command => "echo";
|
||||
public bool OmitEcho => false;
|
||||
|
||||
public Type ExtensionType => typeof(IActionCommandExtension);
|
||||
|
||||
public void ProcessCommand(IExecutionContext context, string line, ActionCommand command)
|
||||
{
|
||||
ArgUtil.NotNullOrEmpty(command.Data, "value");
|
||||
|
||||
switch (command.Data.Trim().ToUpperInvariant())
|
||||
{
|
||||
case "ON":
|
||||
context.EchoOnActionCommand = true;
|
||||
context.Debug("Setting echo command value to 'on'");
|
||||
break;
|
||||
case "OFF":
|
||||
context.EchoOnActionCommand = false;
|
||||
context.Debug("Setting echo command value to 'off'");
|
||||
break;
|
||||
default:
|
||||
throw new Exception($"Invalid echo command value. Possible values can be: 'on', 'off'. Current value is: '{command.Data}'.");
|
||||
break;
|
||||
}
|
||||
omitEcho = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,21 +94,7 @@ namespace GitHub.Runner.Worker
|
||||
{
|
||||
postDisplayName = $"Post {this.DisplayName}";
|
||||
}
|
||||
|
||||
var repositoryReference = Action.Reference as RepositoryPathReference;
|
||||
var pathString = string.IsNullOrEmpty(repositoryReference.Path) ? string.Empty : $"/{repositoryReference.Path}";
|
||||
var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
|
||||
$"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";
|
||||
|
||||
ExecutionContext.Debug($"Register post job cleanup for action: {repoString}");
|
||||
|
||||
var actionRunner = HostContext.CreateService<IActionRunner>();
|
||||
actionRunner.Action = Action;
|
||||
actionRunner.Stage = ActionRunStage.Post;
|
||||
actionRunner.Condition = handlerData.CleanupCondition;
|
||||
actionRunner.DisplayName = postDisplayName;
|
||||
|
||||
ExecutionContext.RegisterPostJobStep($"{actionRunner.Action.Name}_post", actionRunner);
|
||||
ExecutionContext.RegisterPostJobAction(postDisplayName, handlerData.CleanupCondition, Action);
|
||||
}
|
||||
|
||||
IStepHost stepHost = HostContext.CreateService<IDefaultStepHost>();
|
||||
|
||||
@@ -11,7 +11,6 @@ using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using Microsoft.Win32;
|
||||
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
{
|
||||
@@ -39,14 +38,6 @@ namespace GitHub.Runner.Worker
|
||||
List<ContainerInfo> containers = data as List<ContainerInfo>;
|
||||
ArgUtil.NotNull(containers, nameof(containers));
|
||||
|
||||
var postJobStep = new JobExtensionRunner(runAsync: this.StopContainersAsync,
|
||||
condition: $"{PipelineTemplateConstants.Always}()",
|
||||
displayName: "Stop containers",
|
||||
data: data);
|
||||
|
||||
executionContext.Debug($"Register post job cleanup for stoping/deleting containers.");
|
||||
executionContext.RegisterPostJobStep(nameof(StopContainersAsync), postJobStep);
|
||||
|
||||
// Check whether we are inside a container.
|
||||
// Our container feature requires to map working directory from host to the container.
|
||||
// If we are already inside a container, we will not able to find out the real working direcotry path on the host.
|
||||
|
||||
@@ -12,14 +12,14 @@ using GitHub.Services.WebApi;
|
||||
using GitHub.DistributedTask.Pipelines;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||
using GitHub.Runner.Common.Util;
|
||||
using GitHub.Runner.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
using Newtonsoft.Json;
|
||||
using System.Text;
|
||||
using System.Collections;
|
||||
using ObjectTemplating = GitHub.DistributedTask.ObjectTemplating;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Worker
|
||||
{
|
||||
@@ -62,8 +62,6 @@ namespace GitHub.Runner.Worker
|
||||
// Only job level ExecutionContext has PostJobSteps
|
||||
Stack<IStep> PostJobSteps { get; }
|
||||
|
||||
bool EchoOnActionCommand { get; set; }
|
||||
|
||||
// Initialize
|
||||
void InitializeJob(Pipelines.AgentJobRequestMessage message, CancellationToken token);
|
||||
void CancelToken();
|
||||
@@ -98,7 +96,7 @@ namespace GitHub.Runner.Worker
|
||||
|
||||
// others
|
||||
void ForceTaskComplete();
|
||||
void RegisterPostJobStep(string refName, IStep step);
|
||||
void RegisterPostJobAction(string displayName, string condition, Pipelines.ActionStep action);
|
||||
}
|
||||
|
||||
public sealed class ExecutionContext : RunnerService, IExecutionContext
|
||||
@@ -155,8 +153,6 @@ namespace GitHub.Runner.Worker
|
||||
// Only job level ExecutionContext has PostJobSteps
|
||||
public Stack<IStep> PostJobSteps { get; private set; }
|
||||
|
||||
public bool EchoOnActionCommand { get; set; }
|
||||
|
||||
|
||||
public TaskResult? Result
|
||||
{
|
||||
@@ -240,10 +236,27 @@ namespace GitHub.Runner.Worker
|
||||
});
|
||||
}
|
||||
|
||||
public void RegisterPostJobStep(string refName, IStep step)
|
||||
public void RegisterPostJobAction(string displayName, string condition, Pipelines.ActionStep action)
|
||||
{
|
||||
step.ExecutionContext = Root.CreatePostChild(step.DisplayName, refName, IntraActionState);
|
||||
Root.PostJobSteps.Push(step);
|
||||
if (action.Reference.Type != ActionSourceType.Repository)
|
||||
{
|
||||
throw new NotSupportedException("Only action that has `action.yml` can define post job execution.");
|
||||
}
|
||||
|
||||
var repositoryReference = action.Reference as RepositoryPathReference;
|
||||
var pathString = string.IsNullOrEmpty(repositoryReference.Path) ? string.Empty : $"/{repositoryReference.Path}";
|
||||
var repoString = string.IsNullOrEmpty(repositoryReference.Ref) ? $"{repositoryReference.Name}{pathString}" :
|
||||
$"{repositoryReference.Name}{pathString}@{repositoryReference.Ref}";
|
||||
|
||||
this.Debug($"Register post job cleanup for action: {repoString}");
|
||||
|
||||
var actionRunner = HostContext.CreateService<IActionRunner>();
|
||||
actionRunner.Action = action;
|
||||
actionRunner.Stage = ActionRunStage.Post;
|
||||
actionRunner.Condition = condition;
|
||||
actionRunner.DisplayName = displayName;
|
||||
actionRunner.ExecutionContext = Root.CreatePostChild(displayName, $"{actionRunner.Action.Name}_post", IntraActionState);
|
||||
Root.PostJobSteps.Push(actionRunner);
|
||||
}
|
||||
|
||||
public IExecutionContext CreateChild(Guid recordId, string displayName, string refName, string scopeName, string contextName, Dictionary<string, string> intraActionState = null, int? recordOrder = null)
|
||||
@@ -279,7 +292,6 @@ namespace GitHub.Runner.Worker
|
||||
child.PrependPath = PrependPath;
|
||||
child.Container = Container;
|
||||
child.ServiceContainers = ServiceContainers;
|
||||
child.EchoOnActionCommand = EchoOnActionCommand;
|
||||
|
||||
if (recordOrder != null)
|
||||
{
|
||||
@@ -692,9 +704,6 @@ namespace GitHub.Runner.Worker
|
||||
_logger = HostContext.CreateService<IPagingLogger>();
|
||||
_logger.Setup(_mainTimelineId, _record.Id);
|
||||
|
||||
// Initialize 'echo on action command success' property, default to false, unless Step_Debug is set
|
||||
EchoOnActionCommand = Variables.Step_Debug ?? false;
|
||||
|
||||
// Verbosity (from GitHub.Step_Debug).
|
||||
WriteDebug = Variables.Step_Debug ?? false;
|
||||
|
||||
|
||||
@@ -110,7 +110,9 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
// Build up 2 lists of steps, pre-job, job
|
||||
// Build up 3 lists of steps, pre-job, job, post-job
|
||||
var postJobStepsBuilder = new Stack<IStep>();
|
||||
|
||||
// Download actions not already in the cache
|
||||
Trace.Info("Downloading actions");
|
||||
var actionManager = HostContext.GetService<IActionManager>();
|
||||
@@ -132,6 +134,10 @@ namespace GitHub.Runner.Worker
|
||||
condition: $"{PipelineTemplateConstants.Success}()",
|
||||
displayName: "Initialize containers",
|
||||
data: (object)containers));
|
||||
postJobStepsBuilder.Push(new JobExtensionRunner(runAsync: containerProvider.StopContainersAsync,
|
||||
condition: $"{PipelineTemplateConstants.Always}()",
|
||||
displayName: "Stop containers",
|
||||
data: (object)containers));
|
||||
}
|
||||
|
||||
// Add action steps
|
||||
@@ -181,9 +187,33 @@ namespace GitHub.Runner.Worker
|
||||
}
|
||||
}
|
||||
|
||||
// Add post-job steps
|
||||
Trace.Info("Adding post-job steps");
|
||||
while (postJobStepsBuilder.Count > 0)
|
||||
{
|
||||
postJobSteps.Add(postJobStepsBuilder.Pop());
|
||||
}
|
||||
|
||||
// Create execution context for post-job steps
|
||||
foreach (var step in postJobSteps)
|
||||
{
|
||||
if (step is JobExtensionRunner)
|
||||
{
|
||||
JobExtensionRunner extensionStep = step as JobExtensionRunner;
|
||||
ArgUtil.NotNull(extensionStep, extensionStep.DisplayName);
|
||||
Guid stepId = Guid.NewGuid();
|
||||
extensionStep.ExecutionContext = jobContext.CreateChild(stepId, extensionStep.DisplayName, stepId.ToString("N"), null, null);
|
||||
}
|
||||
}
|
||||
|
||||
List<IStep> steps = new List<IStep>();
|
||||
steps.AddRange(preJobSteps);
|
||||
steps.AddRange(jobSteps);
|
||||
steps.AddRange(postJobSteps);
|
||||
|
||||
// Start agent log plugin host process
|
||||
// var logPlugin = HostContext.GetService<IAgentLogPlugin>();
|
||||
// await logPlugin.StartAsync(context, steps, jobContext.CancellationToken);
|
||||
|
||||
// Prepare for orphan process cleanup
|
||||
_processCleanup = jobContext.Variables.GetBoolean("process.clean") ?? true;
|
||||
|
||||
@@ -5,7 +5,6 @@ using System.Text;
|
||||
using Minimatch;
|
||||
using System.IO;
|
||||
using System.Security.Cryptography;
|
||||
using GitHub.DistributedTask.Expressions2.Sdk;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using GitHub.DistributedTask.Pipelines.ObjectTemplating;
|
||||
namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
|
||||
@@ -29,50 +28,29 @@ namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
|
||||
string searchRoot = workspaceData.Value;
|
||||
string pattern = Parameters[0].Evaluate(context).ConvertToString();
|
||||
|
||||
// Convert slashes on Windows
|
||||
if (s_isWindows)
|
||||
{
|
||||
pattern = pattern.Replace('\\', '/');
|
||||
}
|
||||
|
||||
// Root the pattern
|
||||
if (!Path.IsPathRooted(pattern))
|
||||
{
|
||||
var patternRoot = s_isWindows ? searchRoot.Replace('\\', '/').TrimEnd('/') : searchRoot.TrimEnd('/');
|
||||
pattern = string.Concat(patternRoot, "/", pattern);
|
||||
}
|
||||
|
||||
// Get all files
|
||||
context.Trace.Info($"Search root directory: '{searchRoot}'");
|
||||
context.Trace.Info($"Search pattern: '{pattern}'");
|
||||
var files = Directory.GetFiles(searchRoot, "*", SearchOption.AllDirectories)
|
||||
.Select(x => s_isWindows ? x.Replace('\\', '/') : x)
|
||||
.OrderBy(x => x, StringComparer.Ordinal)
|
||||
.ToList();
|
||||
var files = Directory.GetFiles(searchRoot, "*", SearchOption.AllDirectories).OrderBy(x => x).ToList();
|
||||
if (files.Count == 0)
|
||||
{
|
||||
throw new ArgumentException($"hashFiles('{ExpressionUtility.StringEscape(pattern)}') failed. Directory '{searchRoot}' is empty");
|
||||
throw new ArgumentException($"'hashFiles({pattern})' failed. Directory '{searchRoot}' is empty");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Trace.Info($"Found {files.Count} files");
|
||||
}
|
||||
|
||||
// Match
|
||||
var matcher = new Minimatcher(pattern, s_minimatchOptions);
|
||||
files = matcher.Filter(files)
|
||||
.Select(x => s_isWindows ? x.Replace('/', '\\') : x)
|
||||
.ToList();
|
||||
files = matcher.Filter(files).ToList();
|
||||
if (files.Count == 0)
|
||||
{
|
||||
throw new ArgumentException($"hashFiles('{ExpressionUtility.StringEscape(pattern)}') failed. Search pattern '{pattern}' doesn't match any file under '{searchRoot}'");
|
||||
throw new ArgumentException($"'hashFiles({pattern})' failed. Search pattern '{pattern}' doesn't match any file under '{searchRoot}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Trace.Info($"{files.Count} matches to hash");
|
||||
}
|
||||
|
||||
// Hash each file
|
||||
List<byte> filesSha256 = new List<byte>();
|
||||
foreach (var file in files)
|
||||
{
|
||||
@@ -86,7 +64,6 @@ namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
|
||||
}
|
||||
}
|
||||
|
||||
// Hash the hashes
|
||||
using (SHA256 sha256hash = SHA256.Create())
|
||||
{
|
||||
var hashBytes = sha256hash.ComputeHash(filesSha256.ToArray());
|
||||
@@ -106,17 +83,11 @@ namespace GitHub.DistributedTask.Expressions2.Sdk.Functions
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly bool s_isWindows = Environment.OSVersion.Platform != PlatformID.Unix && Environment.OSVersion.Platform != PlatformID.MacOSX;
|
||||
|
||||
// Only support basic globbing (* ? and []) and globstar (**)
|
||||
private static readonly Options s_minimatchOptions = new Options
|
||||
{
|
||||
Dot = true,
|
||||
NoBrace = true,
|
||||
NoCase = s_isWindows,
|
||||
NoComment = true,
|
||||
NoExt = true,
|
||||
NoNegate = true,
|
||||
NoCase = Environment.OSVersion.Platform != PlatformID.Unix && Environment.OSVersion.Platform != PlatformID.MacOSX
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -397,11 +397,6 @@ namespace GitHub.Services.FileContainer.Client
|
||||
{
|
||||
break;
|
||||
}
|
||||
else if (IsFastFailResponse(response))
|
||||
{
|
||||
FileUploadTrace(itemPath, $"Chunk '{currentChunk}' attempt '{attempt}' of file '{itemPath}' received non-success status code {response.StatusCode} for sending request and cannot continue.");
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
FileUploadTrace(itemPath, $"Chunk '{currentChunk}' attempt '{attempt}' of file '{itemPath}' received non-success status code {response.StatusCode} for sending request.");
|
||||
@@ -543,17 +538,6 @@ namespace GitHub.Services.FileContainer.Client
|
||||
cancellationToken);
|
||||
}
|
||||
|
||||
public bool IsFastFailResponse(HttpResponseMessage response)
|
||||
{
|
||||
int statusCode = (int)response?.StatusCode;
|
||||
return statusCode >= 400 && statusCode <= 499;
|
||||
}
|
||||
|
||||
protected override bool ShouldThrowError(HttpResponseMessage response)
|
||||
{
|
||||
return !response.IsSuccessStatusCode && !IsFastFailResponse(response);
|
||||
}
|
||||
|
||||
private async Task<HttpResponseMessage> ContainerGetRequestAsync(
|
||||
Int64 containerId,
|
||||
String itemPath,
|
||||
|
||||
@@ -5,7 +5,6 @@ using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Worker;
|
||||
using Moq;
|
||||
using Xunit;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
|
||||
namespace GitHub.Runner.Common.Tests.Worker
|
||||
{
|
||||
@@ -147,159 +146,5 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
Assert.True(commandManager.TryProcessCommand(_ec.Object, "##[set-env name=foo]bar"));
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void EchoProcessCommand()
|
||||
{
|
||||
using (TestHostContext _hc = new TestHostContext(this))
|
||||
{
|
||||
var extensionManager = new Mock<IExtensionManager>();
|
||||
var echoCommand = new EchoCommandExtension();
|
||||
echoCommand.Initialize(_hc);
|
||||
|
||||
extensionManager.Setup(x => x.GetExtensions<IActionCommandExtension>())
|
||||
.Returns(new List<IActionCommandExtension>() { echoCommand });
|
||||
_hc.SetSingleton<IExtensionManager>(extensionManager.Object);
|
||||
|
||||
Mock<IExecutionContext> _ec = new Mock<IExecutionContext>();
|
||||
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns((string tag, string line) =>
|
||||
{
|
||||
_hc.GetTrace().Info($"{tag} {line}");
|
||||
return 1;
|
||||
});
|
||||
|
||||
_ec.SetupAllProperties();
|
||||
|
||||
ActionCommandManager commandManager = new ActionCommandManager();
|
||||
commandManager.Initialize(_hc);
|
||||
|
||||
Assert.False(_ec.Object.EchoOnActionCommand);
|
||||
|
||||
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::on"));
|
||||
Assert.True(_ec.Object.EchoOnActionCommand);
|
||||
|
||||
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::off"));
|
||||
Assert.False(_ec.Object.EchoOnActionCommand);
|
||||
|
||||
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::ON"));
|
||||
Assert.True(_ec.Object.EchoOnActionCommand);
|
||||
|
||||
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::Off "));
|
||||
Assert.False(_ec.Object.EchoOnActionCommand);
|
||||
}
|
||||
}
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void EchoProcessCommandDebugOn()
|
||||
{
|
||||
using (TestHostContext _hc = new TestHostContext(this))
|
||||
{
|
||||
// Set up a few things
|
||||
// 1. Job request message (with ACTIONS_STEP_DEBUG = true)
|
||||
TaskOrchestrationPlanReference plan = new TaskOrchestrationPlanReference();
|
||||
TimelineReference timeline = new TimelineReference();
|
||||
JobEnvironment environment = new JobEnvironment();
|
||||
environment.SystemConnection = new ServiceEndpoint();
|
||||
List<TaskInstance> tasks = new List<TaskInstance>();
|
||||
Guid JobId = Guid.NewGuid();
|
||||
string jobName = "some job name";
|
||||
var jobRequest = Pipelines.AgentJobRequestMessageUtil.Convert(new AgentJobRequestMessage(plan, timeline, JobId, jobName, jobName, environment, tasks));
|
||||
jobRequest.Resources.Repositories.Add(new Pipelines.RepositoryResource()
|
||||
{
|
||||
Alias = Pipelines.PipelineConstants.SelfAlias,
|
||||
Id = "github",
|
||||
Version = "sha1"
|
||||
});
|
||||
jobRequest.ContextData["github"] = new Pipelines.ContextData.DictionaryContextData();
|
||||
jobRequest.Variables["ACTIONS_STEP_DEBUG"] = "true";
|
||||
|
||||
// Some service dependencies
|
||||
var jobServerQueue = new Mock<IJobServerQueue>();
|
||||
jobServerQueue.Setup(x => x.QueueTimelineRecordUpdate(It.IsAny<Guid>(), It.IsAny<TimelineRecord>()));
|
||||
|
||||
_hc.SetSingleton(jobServerQueue.Object);
|
||||
|
||||
var extensionManager = new Mock<IExtensionManager>();
|
||||
var echoCommand = new EchoCommandExtension();
|
||||
echoCommand.Initialize(_hc);
|
||||
|
||||
extensionManager.Setup(x => x.GetExtensions<IActionCommandExtension>())
|
||||
.Returns(new List<IActionCommandExtension>() { echoCommand });
|
||||
_hc.SetSingleton<IExtensionManager>(extensionManager.Object);
|
||||
|
||||
var configurationStore = new Mock<IConfigurationStore>();
|
||||
configurationStore.Setup(x => x.GetSettings()).Returns(new RunnerSettings());
|
||||
_hc.SetSingleton(configurationStore.Object);
|
||||
|
||||
var pagingLogger = new Mock<IPagingLogger>();
|
||||
_hc.EnqueueInstance(pagingLogger.Object);
|
||||
|
||||
ActionCommandManager commandManager = new ActionCommandManager();
|
||||
commandManager.Initialize(_hc);
|
||||
|
||||
var _ec = new Runner.Worker.ExecutionContext();
|
||||
_ec.Initialize(_hc);
|
||||
|
||||
// Initialize the job (to exercise logic that sets EchoOnActionCommand)
|
||||
_ec.InitializeJob(jobRequest, System.Threading.CancellationToken.None);
|
||||
|
||||
_ec.Complete();
|
||||
|
||||
Assert.True(_ec.EchoOnActionCommand);
|
||||
|
||||
Assert.True(commandManager.TryProcessCommand(_ec, "::echo::off"));
|
||||
Assert.False(_ec.EchoOnActionCommand);
|
||||
|
||||
Assert.True(commandManager.TryProcessCommand(_ec, "::echo::on"));
|
||||
Assert.True(_ec.EchoOnActionCommand);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
[Trait("Level", "L0")]
|
||||
[Trait("Category", "Worker")]
|
||||
public void EchoProcessCommandInvalid()
|
||||
{
|
||||
using (TestHostContext _hc = new TestHostContext(this))
|
||||
{
|
||||
var extensionManager = new Mock<IExtensionManager>();
|
||||
var echoCommand = new EchoCommandExtension();
|
||||
echoCommand.Initialize(_hc);
|
||||
|
||||
extensionManager.Setup(x => x.GetExtensions<IActionCommandExtension>())
|
||||
.Returns(new List<IActionCommandExtension>() { echoCommand });
|
||||
_hc.SetSingleton<IExtensionManager>(extensionManager.Object);
|
||||
|
||||
Mock<IExecutionContext> _ec = new Mock<IExecutionContext>();
|
||||
_ec.Setup(x => x.Write(It.IsAny<string>(), It.IsAny<string>()))
|
||||
.Returns((string tag, string line) =>
|
||||
{
|
||||
_hc.GetTrace().Info($"{tag} {line}");
|
||||
return 1;
|
||||
});
|
||||
|
||||
_ec.SetupAllProperties();
|
||||
|
||||
ActionCommandManager commandManager = new ActionCommandManager();
|
||||
commandManager.Initialize(_hc);
|
||||
|
||||
// Echo commands below are considered "processed", but are invalid
|
||||
// 1. Invalid echo value
|
||||
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::invalid"));
|
||||
Assert.Equal(TaskResult.Failed, _ec.Object.CommandResult);
|
||||
Assert.False(_ec.Object.EchoOnActionCommand);
|
||||
|
||||
// 2. No value
|
||||
Assert.True(commandManager.TryProcessCommand(_ec.Object, "::echo::"));
|
||||
Assert.Equal(TaskResult.Failed, _ec.Object.CommandResult);
|
||||
Assert.False(_ec.Object.EchoOnActionCommand);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -206,22 +206,8 @@ namespace GitHub.Runner.Common.Tests.Worker
|
||||
var action2 = jobContext.CreateChild(Guid.NewGuid(), "action_2", "action_2", null, null);
|
||||
action2.IntraActionState["state"] = "2";
|
||||
|
||||
|
||||
var postRunner1 = hc.CreateService<IActionRunner>();
|
||||
postRunner1.Action = new Pipelines.ActionStep() { Name = "post1", DisplayName = "Test 1", Reference = new Pipelines.RepositoryPathReference() { Name = "actions/action" } };
|
||||
postRunner1.Stage = ActionRunStage.Post;
|
||||
postRunner1.Condition = "always()";
|
||||
postRunner1.DisplayName = "post1";
|
||||
|
||||
|
||||
var postRunner2 = hc.CreateService<IActionRunner>();
|
||||
postRunner2.Action = new Pipelines.ActionStep() { Name = "post2", DisplayName = "Test 2", Reference = new Pipelines.RepositoryPathReference() { Name = "actions/action" } };
|
||||
postRunner2.Stage = ActionRunStage.Post;
|
||||
postRunner2.Condition = "always()";
|
||||
postRunner2.DisplayName = "post2";
|
||||
|
||||
action1.RegisterPostJobStep("post1", postRunner1);
|
||||
action2.RegisterPostJobStep("post2", postRunner2);
|
||||
action1.RegisterPostJobAction("post1", "always()", new Pipelines.ActionStep() { Name = "post1", DisplayName = "Test 1", Reference = new Pipelines.RepositoryPathReference() { Name = "actions/action" } });
|
||||
action2.RegisterPostJobAction("post2", "always()", new Pipelines.ActionStep() { Name = "post2", DisplayName = "Test 2", Reference = new Pipelines.RepositoryPathReference() { Name = "actions/action" } });
|
||||
|
||||
Assert.NotNull(jobContext.JobSteps);
|
||||
Assert.NotNull(jobContext.PostJobSteps);
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.160.2
|
||||
2.160.0
|
||||
Reference in New Issue
Block a user