mirror of
https://github.com/actions/runner.git
synced 2025-12-19 00:36:55 +00:00
GitHub Actions Runner
This commit is contained in:
58
src/Runner.Plugins/Artifact/BuildServer.cs
Normal file
58
src/Runner.Plugins/Artifact/BuildServer.cs
Normal file
@@ -0,0 +1,58 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.WebApi;
|
||||
using GitHub.Build.WebApi;
|
||||
|
||||
namespace GitHub.Runner.Plugins.Artifact
|
||||
{
|
||||
// A client wrapper interacting with Build's Artifact API
|
||||
public class BuildServer
|
||||
{
|
||||
private readonly BuildHttpClient _buildHttpClient;
|
||||
|
||||
public BuildServer(VssConnection connection)
|
||||
{
|
||||
ArgUtil.NotNull(connection, nameof(connection));
|
||||
_buildHttpClient = connection.GetClient<BuildHttpClient>();
|
||||
}
|
||||
|
||||
// Associate the specified artifact with a build, along with custom data.
|
||||
public async Task<BuildArtifact> AssociateArtifact(
|
||||
Guid projectId,
|
||||
int pipelineId,
|
||||
string jobId,
|
||||
string name,
|
||||
string type,
|
||||
string data,
|
||||
Dictionary<string, string> propertiesDictionary,
|
||||
CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
BuildArtifact artifact = new BuildArtifact()
|
||||
{
|
||||
Name = name,
|
||||
Source = jobId,
|
||||
Resource = new ArtifactResource()
|
||||
{
|
||||
Data = data,
|
||||
Type = type,
|
||||
Properties = propertiesDictionary
|
||||
}
|
||||
};
|
||||
|
||||
return await _buildHttpClient.CreateArtifactAsync(artifact, projectId, pipelineId, cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
// Get named artifact from a build
|
||||
public async Task<BuildArtifact> GetArtifact(
|
||||
Guid projectId,
|
||||
int pipelineId,
|
||||
string name,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
return await _buildHttpClient.GetArtifactAsync(projectId, pipelineId, name, cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
79
src/Runner.Plugins/Artifact/DownloadArtifact.cs
Normal file
79
src/Runner.Plugins/Artifact/DownloadArtifact.cs
Normal file
@@ -0,0 +1,79 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Build.WebApi;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Plugins.Artifact
|
||||
{
|
||||
public class DownloadArtifact : IRunnerActionPlugin
|
||||
{
|
||||
|
||||
private static class DownloadArtifactInputNames
|
||||
{
|
||||
public static readonly string Name = "name";
|
||||
public static readonly string ArtifactName = "artifact";
|
||||
public static readonly string Path = "path";
|
||||
}
|
||||
|
||||
public async Task RunAsync(
|
||||
RunnerActionPluginExecutionContext context,
|
||||
CancellationToken token)
|
||||
{
|
||||
ArgUtil.NotNull(context, nameof(context));
|
||||
string artifactName = context.GetInput(DownloadArtifactInputNames.ArtifactName, required: false); // Back compat since we rename input `artifact` to `name`
|
||||
if (string.IsNullOrEmpty(artifactName))
|
||||
{
|
||||
artifactName = context.GetInput(DownloadArtifactInputNames.Name, required: true);
|
||||
}
|
||||
|
||||
string targetPath = context.GetInput(DownloadArtifactInputNames.Path, required: false);
|
||||
string defaultWorkingDirectory = context.GetGitHubContext("workspace");
|
||||
|
||||
if (string.IsNullOrEmpty(targetPath))
|
||||
{
|
||||
targetPath = artifactName;
|
||||
}
|
||||
|
||||
targetPath = Path.IsPathFullyQualified(targetPath) ? targetPath : Path.GetFullPath(Path.Combine(defaultWorkingDirectory, targetPath));
|
||||
|
||||
// Project ID
|
||||
Guid projectId = new Guid(context.Variables.GetValueOrDefault(BuildVariables.TeamProjectId)?.Value ?? Guid.Empty.ToString());
|
||||
|
||||
// Build ID
|
||||
string buildIdStr = context.Variables.GetValueOrDefault(BuildVariables.BuildId)?.Value ?? string.Empty;
|
||||
if (!int.TryParse(buildIdStr, out int buildId))
|
||||
{
|
||||
throw new ArgumentException($"Run Id is not an Int32: {buildIdStr}");
|
||||
}
|
||||
|
||||
context.Output($"Download artifact '{artifactName}' to: '{targetPath}'");
|
||||
|
||||
BuildServer buildHelper = new BuildServer(context.VssConnection);
|
||||
BuildArtifact buildArtifact = await buildHelper.GetArtifact(projectId, buildId, artifactName, token);
|
||||
|
||||
if (string.Equals(buildArtifact.Resource.Type, "Container", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
string containerUrl = buildArtifact.Resource.Data;
|
||||
string[] parts = containerUrl.Split(new[] { '/' }, 3);
|
||||
if (parts.Length < 3 || !long.TryParse(parts[1], out long containerId))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"Invalid container url '{containerUrl}' for artifact '{buildArtifact.Name}'");
|
||||
}
|
||||
|
||||
string containerPath = parts[2];
|
||||
FileContainerServer fileContainerServer = new FileContainerServer(context.VssConnection, projectId, containerId, containerPath);
|
||||
await fileContainerServer.DownloadFromContainerAsync(context, targetPath, token);
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException($"Invalid artifact type: {buildArtifact.Resource.Type}");
|
||||
}
|
||||
|
||||
context.Output("Artifact download finished.");
|
||||
}
|
||||
}
|
||||
}
|
||||
660
src/Runner.Plugins/Artifact/FileContainerServer.cs
Normal file
660
src/Runner.Plugins/Artifact/FileContainerServer.cs
Normal file
@@ -0,0 +1,660 @@
|
||||
using GitHub.Services.FileContainer.Client;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Diagnostics;
|
||||
using GitHub.Services.WebApi;
|
||||
using System.Net.Http;
|
||||
using System.Net;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.FileContainer;
|
||||
using GitHub.Services.Common;
|
||||
|
||||
namespace GitHub.Runner.Plugins.Artifact
|
||||
{
|
||||
public class FileContainerServer
|
||||
{
|
||||
private const int _defaultFileStreamBufferSize = 4096;
|
||||
|
||||
//81920 is the default used by System.IO.Stream.CopyTo and is under the large object heap threshold (85k).
|
||||
private const int _defaultCopyBufferSize = 81920;
|
||||
|
||||
private readonly ConcurrentQueue<string> _fileUploadQueue = new ConcurrentQueue<string>();
|
||||
private readonly ConcurrentQueue<DownloadInfo> _fileDownloadQueue = new ConcurrentQueue<DownloadInfo>();
|
||||
private readonly ConcurrentDictionary<string, ConcurrentQueue<string>> _fileUploadTraceLog = new ConcurrentDictionary<string, ConcurrentQueue<string>>();
|
||||
private readonly ConcurrentDictionary<string, ConcurrentQueue<string>> _fileUploadProgressLog = new ConcurrentDictionary<string, ConcurrentQueue<string>>();
|
||||
private readonly FileContainerHttpClient _fileContainerHttpClient;
|
||||
|
||||
private CancellationTokenSource _uploadCancellationTokenSource;
|
||||
private CancellationTokenSource _downloadCancellationTokenSource;
|
||||
private TaskCompletionSource<int> _uploadFinished;
|
||||
private TaskCompletionSource<int> _downloadFinished;
|
||||
private Guid _projectId;
|
||||
private long _containerId;
|
||||
private string _containerPath;
|
||||
private int _uploadFilesProcessed = 0;
|
||||
private int _downloadFilesProcessed = 0;
|
||||
private string _sourceParentDirectory;
|
||||
|
||||
public FileContainerServer(
|
||||
VssConnection connection,
|
||||
Guid projectId,
|
||||
long containerId,
|
||||
string containerPath)
|
||||
{
|
||||
_projectId = projectId;
|
||||
_containerId = containerId;
|
||||
_containerPath = containerPath;
|
||||
|
||||
// default file upload/download request timeout to 600 seconds
|
||||
var fileContainerClientConnectionSetting = connection.Settings.Clone();
|
||||
if (fileContainerClientConnectionSetting.SendTimeout < TimeSpan.FromSeconds(600))
|
||||
{
|
||||
fileContainerClientConnectionSetting.SendTimeout = TimeSpan.FromSeconds(600);
|
||||
}
|
||||
|
||||
var fileContainerClientConnection = new VssConnection(connection.Uri, connection.Credentials, fileContainerClientConnectionSetting);
|
||||
_fileContainerHttpClient = fileContainerClientConnection.GetClient<FileContainerHttpClient>();
|
||||
}
|
||||
|
||||
public async Task DownloadFromContainerAsync(
|
||||
RunnerActionPluginExecutionContext context,
|
||||
String destination,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Find out all container items need to be processed
|
||||
List<FileContainerItem> containerItems = new List<FileContainerItem>();
|
||||
int retryCount = 0;
|
||||
while (retryCount < 3)
|
||||
{
|
||||
try
|
||||
{
|
||||
containerItems = await _fileContainerHttpClient.QueryContainerItemsAsync(_containerId,
|
||||
_projectId,
|
||||
_containerPath,
|
||||
cancellationToken: cancellationToken);
|
||||
break;
|
||||
}
|
||||
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
context.Debug($"Container query has been cancelled.");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex) when (retryCount < 2)
|
||||
{
|
||||
retryCount++;
|
||||
context.Warning($"Fail to query container items under #/{_containerId}/{_containerPath}, Error: {ex.Message}");
|
||||
context.Debug(ex.ToString());
|
||||
}
|
||||
|
||||
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(15));
|
||||
context.Warning($"Back off {backOff.TotalSeconds} seconds before retry.");
|
||||
await Task.Delay(backOff);
|
||||
}
|
||||
|
||||
if (containerItems.Count == 0)
|
||||
{
|
||||
context.Output($"There is nothing under #/{_containerId}/{_containerPath}");
|
||||
return;
|
||||
}
|
||||
|
||||
// container items will include both folders, files and even file with zero size
|
||||
// Create all required empty folders and emptry files, gather a list of files that we need to download from server.
|
||||
int foldersCreated = 0;
|
||||
int emptryFilesCreated = 0;
|
||||
List<DownloadInfo> downloadFiles = new List<DownloadInfo>();
|
||||
foreach (var item in containerItems.OrderBy(x => x.Path))
|
||||
{
|
||||
if (!item.Path.StartsWith(_containerPath, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
throw new ArgumentOutOfRangeException($"Item {item.Path} is not under #/{_containerId}/{_containerPath}");
|
||||
}
|
||||
|
||||
var localRelativePath = item.Path.Substring(_containerPath.Length).TrimStart('/');
|
||||
var localPath = Path.Combine(destination, localRelativePath);
|
||||
|
||||
if (item.ItemType == ContainerItemType.Folder)
|
||||
{
|
||||
context.Debug($"Ensure folder exists: {localPath}");
|
||||
Directory.CreateDirectory(localPath);
|
||||
foldersCreated++;
|
||||
}
|
||||
else if (item.ItemType == ContainerItemType.File)
|
||||
{
|
||||
if (item.FileLength == 0)
|
||||
{
|
||||
context.Debug($"Create empty file at: {localPath}");
|
||||
var parentDirectory = Path.GetDirectoryName(localPath);
|
||||
Directory.CreateDirectory(parentDirectory);
|
||||
IOUtil.DeleteFile(localPath);
|
||||
using (new FileStream(localPath, FileMode.Create))
|
||||
{
|
||||
}
|
||||
emptryFilesCreated++;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Debug($"Prepare download {item.Path} to {localPath}");
|
||||
downloadFiles.Add(new DownloadInfo(item.Path, localPath));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotSupportedException(item.ItemType.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
if (foldersCreated > 0)
|
||||
{
|
||||
context.Output($"{foldersCreated} folders created.");
|
||||
}
|
||||
|
||||
if (emptryFilesCreated > 0)
|
||||
{
|
||||
context.Output($"{emptryFilesCreated} empty files created.");
|
||||
}
|
||||
|
||||
if (downloadFiles.Count == 0)
|
||||
{
|
||||
context.Output($"There is nothing to download");
|
||||
return;
|
||||
}
|
||||
|
||||
// Start multi-task to download all files.
|
||||
using (_downloadCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
// try download all files for the first time.
|
||||
DownloadResult downloadResult = await ParallelDownloadAsync(context, downloadFiles.AsReadOnly(), Math.Min(downloadFiles.Count, Environment.ProcessorCount), _downloadCancellationTokenSource.Token);
|
||||
if (downloadResult.FailedFiles.Count == 0)
|
||||
{
|
||||
// all files have been download succeed.
|
||||
context.Output($"{downloadFiles.Count} files download succeed.");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Output($"{downloadResult.FailedFiles.Count} files failed to download, retry these files after a minute.");
|
||||
}
|
||||
|
||||
// Delay 1 min then retry failed files.
|
||||
for (int timer = 60; timer > 0; timer -= 5)
|
||||
{
|
||||
context.Output($"Retry file download after {timer} seconds.");
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), _uploadCancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
// Retry download all failed files.
|
||||
context.Output($"Start retry {downloadResult.FailedFiles.Count} failed files upload.");
|
||||
DownloadResult retryDownloadResult = await ParallelDownloadAsync(context, downloadResult.FailedFiles.AsReadOnly(), Math.Min(downloadResult.FailedFiles.Count, Environment.ProcessorCount), _downloadCancellationTokenSource.Token);
|
||||
if (retryDownloadResult.FailedFiles.Count == 0)
|
||||
{
|
||||
// all files have been download succeed after retry.
|
||||
context.Output($"{downloadResult.FailedFiles} files download succeed after retry.");
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception($"{retryDownloadResult.FailedFiles.Count} files failed to download even after retry.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<long> CopyToContainerAsync(
|
||||
RunnerActionPluginExecutionContext context,
|
||||
String source,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
//set maxConcurrentUploads up to 2 until figure out how to use WinHttpHandler.MaxConnectionsPerServer modify DefaultConnectionLimit
|
||||
int maxConcurrentUploads = Math.Min(Environment.ProcessorCount, 2);
|
||||
//context.Output($"Max Concurrent Uploads {maxConcurrentUploads}");
|
||||
|
||||
List<String> files;
|
||||
if (File.Exists(source))
|
||||
{
|
||||
files = new List<String>() { source };
|
||||
_sourceParentDirectory = Path.GetDirectoryName(source);
|
||||
}
|
||||
else
|
||||
{
|
||||
files = Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories).ToList();
|
||||
_sourceParentDirectory = source.TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
|
||||
context.Output($"Uploading {files.Count()} files");
|
||||
using (_uploadCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken))
|
||||
{
|
||||
// hook up reporting event from file container client.
|
||||
_fileContainerHttpClient.UploadFileReportTrace += UploadFileTraceReportReceived;
|
||||
_fileContainerHttpClient.UploadFileReportProgress += UploadFileProgressReportReceived;
|
||||
|
||||
try
|
||||
{
|
||||
// try upload all files for the first time.
|
||||
UploadResult uploadResult = await ParallelUploadAsync(context, files, maxConcurrentUploads, _uploadCancellationTokenSource.Token);
|
||||
|
||||
if (uploadResult.FailedFiles.Count == 0)
|
||||
{
|
||||
// all files have been upload succeed.
|
||||
context.Output("File upload succeed.");
|
||||
return uploadResult.TotalFileSizeUploaded;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Output($"{uploadResult.FailedFiles.Count} files failed to upload, retry these files after a minute.");
|
||||
}
|
||||
|
||||
// Delay 1 min then retry failed files.
|
||||
for (int timer = 60; timer > 0; timer -= 5)
|
||||
{
|
||||
context.Output($"Retry file upload after {timer} seconds.");
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), _uploadCancellationTokenSource.Token);
|
||||
}
|
||||
|
||||
// Retry upload all failed files.
|
||||
context.Output($"Start retry {uploadResult.FailedFiles.Count} failed files upload.");
|
||||
UploadResult retryUploadResult = await ParallelUploadAsync(context, uploadResult.FailedFiles, maxConcurrentUploads, _uploadCancellationTokenSource.Token);
|
||||
|
||||
if (retryUploadResult.FailedFiles.Count == 0)
|
||||
{
|
||||
// all files have been upload succeed after retry.
|
||||
context.Output("File upload succeed after retry.");
|
||||
return uploadResult.TotalFileSizeUploaded + retryUploadResult.TotalFileSizeUploaded;
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new Exception("File upload failed even after retry.");
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_fileContainerHttpClient.UploadFileReportTrace -= UploadFileTraceReportReceived;
|
||||
_fileContainerHttpClient.UploadFileReportProgress -= UploadFileProgressReportReceived;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<DownloadResult> ParallelDownloadAsync(RunnerActionPluginExecutionContext context, IReadOnlyList<DownloadInfo> files, int concurrentDownloads, CancellationToken token)
|
||||
{
|
||||
// return files that fail to download
|
||||
var downloadResult = new DownloadResult();
|
||||
|
||||
// nothing needs to download
|
||||
if (files.Count == 0)
|
||||
{
|
||||
return downloadResult;
|
||||
}
|
||||
|
||||
// ensure the file download queue is empty.
|
||||
if (!_fileDownloadQueue.IsEmpty)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(_fileDownloadQueue));
|
||||
}
|
||||
|
||||
// enqueue file into download queue.
|
||||
foreach (var file in files)
|
||||
{
|
||||
_fileDownloadQueue.Enqueue(file);
|
||||
}
|
||||
|
||||
// Start download monitor task.
|
||||
_downloadFilesProcessed = 0;
|
||||
_downloadFinished = new TaskCompletionSource<int>();
|
||||
Task downloadMonitor = DownloadReportingAsync(context, files.Count(), token);
|
||||
|
||||
// Start parallel download tasks.
|
||||
List<Task<DownloadResult>> parallelDownloadingTasks = new List<Task<DownloadResult>>();
|
||||
for (int downloader = 0; downloader < concurrentDownloads; downloader++)
|
||||
{
|
||||
parallelDownloadingTasks.Add(DownloadAsync(context, downloader, token));
|
||||
}
|
||||
|
||||
// Wait for parallel download finish.
|
||||
await Task.WhenAll(parallelDownloadingTasks);
|
||||
foreach (var downloadTask in parallelDownloadingTasks)
|
||||
{
|
||||
// record all failed files.
|
||||
downloadResult.AddDownloadResult(await downloadTask);
|
||||
}
|
||||
|
||||
// Stop monitor task;
|
||||
_downloadFinished.TrySetResult(0);
|
||||
await downloadMonitor;
|
||||
|
||||
return downloadResult;
|
||||
}
|
||||
|
||||
private async Task<UploadResult> ParallelUploadAsync(RunnerActionPluginExecutionContext context, IReadOnlyList<string> files, int concurrentUploads, CancellationToken token)
|
||||
{
|
||||
// return files that fail to upload and total artifact size
|
||||
var uploadResult = new UploadResult();
|
||||
|
||||
// nothing needs to upload
|
||||
if (files.Count == 0)
|
||||
{
|
||||
return uploadResult;
|
||||
}
|
||||
|
||||
// ensure the file upload queue is empty.
|
||||
if (!_fileUploadQueue.IsEmpty)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(nameof(_fileUploadQueue));
|
||||
}
|
||||
|
||||
// enqueue file into upload queue.
|
||||
foreach (var file in files)
|
||||
{
|
||||
_fileUploadQueue.Enqueue(file);
|
||||
}
|
||||
|
||||
// Start upload monitor task.
|
||||
_uploadFilesProcessed = 0;
|
||||
_uploadFinished = new TaskCompletionSource<int>();
|
||||
_fileUploadTraceLog.Clear();
|
||||
_fileUploadProgressLog.Clear();
|
||||
Task uploadMonitor = UploadReportingAsync(context, files.Count(), _uploadCancellationTokenSource.Token);
|
||||
|
||||
// Start parallel upload tasks.
|
||||
List<Task<UploadResult>> parallelUploadingTasks = new List<Task<UploadResult>>();
|
||||
for (int uploader = 0; uploader < concurrentUploads; uploader++)
|
||||
{
|
||||
parallelUploadingTasks.Add(UploadAsync(context, uploader, _uploadCancellationTokenSource.Token));
|
||||
}
|
||||
|
||||
// Wait for parallel upload finish.
|
||||
await Task.WhenAll(parallelUploadingTasks);
|
||||
foreach (var uploadTask in parallelUploadingTasks)
|
||||
{
|
||||
// record all failed files.
|
||||
uploadResult.AddUploadResult(await uploadTask);
|
||||
}
|
||||
|
||||
// Stop monitor task;
|
||||
_uploadFinished.TrySetResult(0);
|
||||
await uploadMonitor;
|
||||
|
||||
return uploadResult;
|
||||
}
|
||||
|
||||
private async Task<DownloadResult> DownloadAsync(RunnerActionPluginExecutionContext context, int downloaderId, CancellationToken token)
|
||||
{
|
||||
List<DownloadInfo> failedFiles = new List<DownloadInfo>();
|
||||
Stopwatch downloadTimer = new Stopwatch();
|
||||
while (_fileDownloadQueue.TryDequeue(out DownloadInfo fileToDownload))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
int retryCount = 0;
|
||||
bool downloadFailed = false;
|
||||
while (true)
|
||||
{
|
||||
try
|
||||
{
|
||||
context.Debug($"Start downloading file: '{fileToDownload.ItemPath}' (Downloader {downloaderId})");
|
||||
downloadTimer.Restart();
|
||||
using (FileStream fs = new FileStream(fileToDownload.LocalPath, FileMode.Create, FileAccess.Write, FileShare.None, bufferSize: _defaultFileStreamBufferSize, useAsync: true))
|
||||
using (var downloadStream = await _fileContainerHttpClient.DownloadFileAsync(_containerId, fileToDownload.ItemPath, token, _projectId))
|
||||
{
|
||||
await downloadStream.CopyToAsync(fs, _defaultCopyBufferSize, token);
|
||||
await fs.FlushAsync(token);
|
||||
downloadTimer.Stop();
|
||||
context.Debug($"File: '{fileToDownload.LocalPath}' took {downloadTimer.ElapsedMilliseconds} milliseconds to finish download (Downloader {downloaderId})");
|
||||
break;
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException) when (token.IsCancellationRequested)
|
||||
{
|
||||
context.Debug($"Download has been cancelled while downloading {fileToDownload.ItemPath}. (Downloader {downloaderId})");
|
||||
throw;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
retryCount++;
|
||||
context.Warning($"Fail to download '{fileToDownload.ItemPath}', error: {ex.Message} (Downloader {downloaderId})");
|
||||
context.Debug(ex.ToString());
|
||||
}
|
||||
|
||||
if (retryCount < 3)
|
||||
{
|
||||
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(10), TimeSpan.FromSeconds(30));
|
||||
context.Warning($"Back off {backOff.TotalSeconds} seconds before retry. (Downloader {downloaderId})");
|
||||
await Task.Delay(backOff);
|
||||
}
|
||||
else
|
||||
{
|
||||
// upload still failed after 3 tries.
|
||||
downloadFailed = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadFailed)
|
||||
{
|
||||
// tracking file that failed to download.
|
||||
failedFiles.Add(fileToDownload);
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _downloadFilesProcessed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// We should never
|
||||
context.Error($"Error '{ex.Message}' when downloading file '{fileToDownload}'. (Downloader {downloaderId})");
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
return new DownloadResult(failedFiles);
|
||||
}
|
||||
|
||||
private async Task<UploadResult> UploadAsync(RunnerActionPluginExecutionContext context, int uploaderId, CancellationToken token)
|
||||
{
|
||||
List<string> failedFiles = new List<string>();
|
||||
long uploadedSize = 0;
|
||||
string fileToUpload;
|
||||
Stopwatch uploadTimer = new Stopwatch();
|
||||
while (_fileUploadQueue.TryDequeue(out fileToUpload))
|
||||
{
|
||||
token.ThrowIfCancellationRequested();
|
||||
try
|
||||
{
|
||||
using (FileStream fs = File.Open(fileToUpload, FileMode.Open, FileAccess.Read, FileShare.Read))
|
||||
{
|
||||
string itemPath = (_containerPath.TrimEnd('/') + "/" + fileToUpload.Remove(0, _sourceParentDirectory.Length + 1)).Replace('\\', '/');
|
||||
uploadTimer.Restart();
|
||||
bool catchExceptionDuringUpload = false;
|
||||
HttpResponseMessage response = null;
|
||||
try
|
||||
{
|
||||
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());
|
||||
}
|
||||
|
||||
uploadTimer.Stop();
|
||||
if (catchExceptionDuringUpload || (response != null && response.StatusCode != HttpStatusCode.Created))
|
||||
{
|
||||
if (response != null)
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
Interlocked.Increment(ref _uploadFilesProcessed);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Output($"File error '{ex.Message}' when uploading file '{fileToUpload}'.");
|
||||
throw ex;
|
||||
}
|
||||
}
|
||||
|
||||
return new UploadResult(failedFiles, uploadedSize);
|
||||
}
|
||||
|
||||
private async Task UploadReportingAsync(RunnerActionPluginExecutionContext context, int totalFiles, CancellationToken token)
|
||||
{
|
||||
int traceInterval = 0;
|
||||
while (!_uploadFinished.Task.IsCompleted && !token.IsCancellationRequested)
|
||||
{
|
||||
bool hasDetailProgress = false;
|
||||
foreach (var file in _fileUploadProgressLog)
|
||||
{
|
||||
string message;
|
||||
while (file.Value.TryDequeue(out message))
|
||||
{
|
||||
hasDetailProgress = true;
|
||||
context.Output(message);
|
||||
}
|
||||
}
|
||||
|
||||
// trace total file progress every 25 seconds when there is no file level detail progress
|
||||
if (++traceInterval % 2 == 0 && !hasDetailProgress)
|
||||
{
|
||||
context.Output($"Total file: {totalFiles} ---- Processed file: {_uploadFilesProcessed} ({(_uploadFilesProcessed * 100) / totalFiles}%)");
|
||||
}
|
||||
|
||||
await Task.WhenAny(_uploadFinished.Task, Task.Delay(5000, token));
|
||||
}
|
||||
}
|
||||
|
||||
private async Task DownloadReportingAsync(RunnerActionPluginExecutionContext context, int totalFiles, CancellationToken token)
|
||||
{
|
||||
int traceInterval = 0;
|
||||
while (!_downloadFinished.Task.IsCompleted && !token.IsCancellationRequested)
|
||||
{
|
||||
// trace total file progress every 10 seconds when there is no file level detail progress
|
||||
if (++traceInterval % 2 == 0)
|
||||
{
|
||||
context.Output($"Total file: {totalFiles} ---- Downloaded file: {_downloadFilesProcessed} ({(_downloadFilesProcessed * 100) / totalFiles}%)");
|
||||
}
|
||||
|
||||
await Task.WhenAny(_downloadFinished.Task, Task.Delay(5000, token));
|
||||
}
|
||||
}
|
||||
|
||||
private void UploadFileTraceReportReceived(object sender, ReportTraceEventArgs e)
|
||||
{
|
||||
ConcurrentQueue<string> logQueue = _fileUploadTraceLog.GetOrAdd(e.File, new ConcurrentQueue<string>());
|
||||
logQueue.Enqueue(e.Message);
|
||||
}
|
||||
|
||||
private void UploadFileProgressReportReceived(object sender, ReportProgressEventArgs e)
|
||||
{
|
||||
ConcurrentQueue<string> progressQueue = _fileUploadProgressLog.GetOrAdd(e.File, new ConcurrentQueue<string>());
|
||||
progressQueue.Enqueue($"Uploading '{e.File}' ({(e.CurrentChunk * 100) / e.TotalChunks}%)");
|
||||
}
|
||||
}
|
||||
|
||||
public class UploadResult
|
||||
{
|
||||
public UploadResult()
|
||||
{
|
||||
FailedFiles = new List<string>();
|
||||
TotalFileSizeUploaded = 0;
|
||||
}
|
||||
|
||||
public UploadResult(List<string> failedFiles, long totalFileSizeUploaded)
|
||||
{
|
||||
FailedFiles = failedFiles;
|
||||
TotalFileSizeUploaded = totalFileSizeUploaded;
|
||||
}
|
||||
public List<string> FailedFiles { get; set; }
|
||||
|
||||
public long TotalFileSizeUploaded { get; set; }
|
||||
|
||||
public void AddUploadResult(UploadResult resultToAdd)
|
||||
{
|
||||
this.FailedFiles.AddRange(resultToAdd.FailedFiles);
|
||||
this.TotalFileSizeUploaded += resultToAdd.TotalFileSizeUploaded;
|
||||
}
|
||||
}
|
||||
|
||||
public class DownloadInfo
|
||||
{
|
||||
public DownloadInfo(string itemPath, string localPath)
|
||||
{
|
||||
this.ItemPath = itemPath;
|
||||
this.LocalPath = localPath;
|
||||
}
|
||||
|
||||
public string ItemPath { get; set; }
|
||||
public string LocalPath { get; set; }
|
||||
}
|
||||
|
||||
public class DownloadResult
|
||||
{
|
||||
public DownloadResult()
|
||||
{
|
||||
FailedFiles = new List<DownloadInfo>();
|
||||
}
|
||||
|
||||
public DownloadResult(List<DownloadInfo> failedFiles)
|
||||
{
|
||||
FailedFiles = failedFiles;
|
||||
}
|
||||
public List<DownloadInfo> FailedFiles { get; set; }
|
||||
|
||||
public void AddDownloadResult(DownloadResult resultToAdd)
|
||||
{
|
||||
this.FailedFiles.AddRange(resultToAdd.FailedFiles);
|
||||
}
|
||||
}
|
||||
}
|
||||
90
src/Runner.Plugins/Artifact/PublishArtifact.cs
Normal file
90
src/Runner.Plugins/Artifact/PublishArtifact.cs
Normal file
@@ -0,0 +1,90 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Build.WebApi;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Runner.Sdk;
|
||||
|
||||
namespace GitHub.Runner.Plugins.Artifact
|
||||
{
|
||||
public class PublishArtifact : IRunnerActionPlugin
|
||||
{
|
||||
private static class PublishArtifactInputNames
|
||||
{
|
||||
public static readonly string ArtifactName = "artifactName";
|
||||
public static readonly string Name = "name";
|
||||
public static readonly string Path = "path";
|
||||
}
|
||||
|
||||
public async Task RunAsync(
|
||||
RunnerActionPluginExecutionContext context,
|
||||
CancellationToken token)
|
||||
{
|
||||
string artifactName = context.GetInput(PublishArtifactInputNames.ArtifactName, required: false); // Back compat since we rename input `artifactName` to `name`
|
||||
if (string.IsNullOrEmpty(artifactName))
|
||||
{
|
||||
artifactName = context.GetInput(PublishArtifactInputNames.Name, required: true);
|
||||
}
|
||||
|
||||
string targetPath = context.GetInput(PublishArtifactInputNames.Path, required: true);
|
||||
string defaultWorkingDirectory = context.GetGitHubContext("workspace");
|
||||
|
||||
targetPath = Path.IsPathFullyQualified(targetPath) ? targetPath : Path.GetFullPath(Path.Combine(defaultWorkingDirectory, targetPath));
|
||||
|
||||
if (String.IsNullOrWhiteSpace(artifactName))
|
||||
{
|
||||
throw new ArgumentException($"Artifact name can not be empty string");
|
||||
}
|
||||
|
||||
if (Path.GetInvalidFileNameChars().Any(x => artifactName.Contains(x)))
|
||||
{
|
||||
throw new ArgumentException($"Artifact name is not valid: {artifactName}. It cannot contain '\\', '/', \"', ':', '<', '>', '|', '*', and '?'");
|
||||
}
|
||||
|
||||
// Project ID
|
||||
Guid projectId = new Guid(context.Variables.GetValueOrDefault(BuildVariables.TeamProjectId)?.Value ?? Guid.Empty.ToString());
|
||||
|
||||
// Build ID
|
||||
string buildIdStr = context.Variables.GetValueOrDefault(BuildVariables.BuildId)?.Value ?? string.Empty;
|
||||
if (!int.TryParse(buildIdStr, out int buildId))
|
||||
{
|
||||
throw new ArgumentException($"Run Id is not an Int32: {buildIdStr}");
|
||||
}
|
||||
|
||||
string fullPath = Path.GetFullPath(targetPath);
|
||||
bool isFile = File.Exists(fullPath);
|
||||
bool isDir = Directory.Exists(fullPath);
|
||||
if (!isFile && !isDir)
|
||||
{
|
||||
// if local path is neither file nor folder
|
||||
throw new FileNotFoundException($"Path does not exist {targetPath}");
|
||||
}
|
||||
|
||||
// Container ID
|
||||
string containerIdStr = context.Variables.GetValueOrDefault(BuildVariables.ContainerId)?.Value ?? string.Empty;
|
||||
if (!long.TryParse(containerIdStr, out long containerId))
|
||||
{
|
||||
throw new ArgumentException($"Container Id is not a Int64: {containerIdStr}");
|
||||
}
|
||||
|
||||
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);
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
686
src/Runner.Plugins/Repository/GitCliManager.cs
Normal file
686
src/Runner.Plugins/Repository/GitCliManager.cs
Normal file
@@ -0,0 +1,686 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using GitHub.Runner.Sdk;
|
||||
using GitHub.Services.Common;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
|
||||
namespace GitHub.Runner.Plugins.Repository
|
||||
{
|
||||
public class GitCliManager
|
||||
{
|
||||
#if OS_WINDOWS
|
||||
private static readonly Encoding s_encoding = Encoding.UTF8;
|
||||
#else
|
||||
private static readonly Encoding s_encoding = null;
|
||||
#endif
|
||||
private readonly Dictionary<string, string> gitEnv = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ "GIT_TERMINAL_PROMPT", "0" },
|
||||
};
|
||||
|
||||
private string gitPath = null;
|
||||
private Version gitVersion = null;
|
||||
private string gitLfsPath = null;
|
||||
private Version gitLfsVersion = null;
|
||||
|
||||
public GitCliManager(Dictionary<string, string> envs = null)
|
||||
{
|
||||
if (envs != null)
|
||||
{
|
||||
foreach (var env in envs)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(env.Key))
|
||||
{
|
||||
gitEnv[env.Key] = env.Value ?? string.Empty;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public bool EnsureGitVersion(Version requiredVersion, bool throwOnNotMatch)
|
||||
{
|
||||
ArgUtil.NotNull(gitPath, nameof(gitPath));
|
||||
ArgUtil.NotNull(gitVersion, nameof(gitVersion));
|
||||
|
||||
if (gitVersion < requiredVersion && throwOnNotMatch)
|
||||
{
|
||||
throw new NotSupportedException($"Min required git version is '{requiredVersion}', your git ('{gitPath}') version is '{gitVersion}'");
|
||||
}
|
||||
|
||||
return gitVersion >= requiredVersion;
|
||||
}
|
||||
|
||||
public bool EnsureGitLFSVersion(Version requiredVersion, bool throwOnNotMatch)
|
||||
{
|
||||
ArgUtil.NotNull(gitLfsPath, nameof(gitLfsPath));
|
||||
ArgUtil.NotNull(gitLfsVersion, nameof(gitLfsVersion));
|
||||
|
||||
if (gitLfsVersion < requiredVersion && throwOnNotMatch)
|
||||
{
|
||||
throw new NotSupportedException($"Min required git-lfs version is '{requiredVersion}', your git-lfs ('{gitLfsPath}') version is '{gitLfsVersion}'");
|
||||
}
|
||||
|
||||
return gitLfsVersion >= requiredVersion;
|
||||
}
|
||||
|
||||
public async Task LoadGitExecutionInfo(RunnerActionPluginExecutionContext context)
|
||||
{
|
||||
// Resolve the location of git.
|
||||
gitPath = WhichUtil.Which("git", require: true, trace: context);
|
||||
ArgUtil.File(gitPath, nameof(gitPath));
|
||||
|
||||
// Get the Git version.
|
||||
gitVersion = await GitVersion(context);
|
||||
ArgUtil.NotNull(gitVersion, nameof(gitVersion));
|
||||
context.Debug($"Detect git version: {gitVersion.ToString()}.");
|
||||
|
||||
// Resolve the location of git-lfs.
|
||||
// This should be best effort since checkout lfs objects is an option.
|
||||
// We will check and ensure git-lfs version later
|
||||
gitLfsPath = WhichUtil.Which("git-lfs", require: false, trace: context);
|
||||
|
||||
// Get the Git-LFS version if git-lfs exist in %PATH%.
|
||||
if (!string.IsNullOrEmpty(gitLfsPath))
|
||||
{
|
||||
gitLfsVersion = await GitLfsVersion(context);
|
||||
context.Debug($"Detect git-lfs version: '{gitLfsVersion?.ToString() ?? string.Empty}'.");
|
||||
}
|
||||
|
||||
// required 2.0, all git operation commandline args need min git version 2.0
|
||||
Version minRequiredGitVersion = new Version(2, 0);
|
||||
EnsureGitVersion(minRequiredGitVersion, throwOnNotMatch: true);
|
||||
|
||||
// suggest user upgrade to 2.9 for better git experience
|
||||
Version recommendGitVersion = new Version(2, 9);
|
||||
if (!EnsureGitVersion(recommendGitVersion, throwOnNotMatch: false))
|
||||
{
|
||||
context.Output($"To get a better Git experience, upgrade your Git to at least version '{recommendGitVersion}'. Your current Git version is '{gitVersion}'.");
|
||||
}
|
||||
|
||||
// Set the user agent.
|
||||
string gitHttpUserAgentEnv = $"git/{gitVersion.ToString()} (github-actions-runner-git/{BuildConstants.RunnerPackage.Version})";
|
||||
context.Debug($"Set git useragent to: {gitHttpUserAgentEnv}.");
|
||||
gitEnv["GIT_HTTP_USER_AGENT"] = gitHttpUserAgentEnv;
|
||||
}
|
||||
|
||||
// git init <LocalDir>
|
||||
public async Task<int> GitInit(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug($"Init git repository at: {repositoryPath}.");
|
||||
string repoRootEscapeSpace = StringUtil.Format(@"""{0}""", repositoryPath.Replace(@"""", @"\"""));
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "init", StringUtil.Format($"{repoRootEscapeSpace}"));
|
||||
}
|
||||
|
||||
// git fetch --tags --prune --progress --no-recurse-submodules [--depth=15] origin [+refs/pull/*:refs/remote/pull/*]
|
||||
public async Task<int> GitFetch(RunnerActionPluginExecutionContext context, string repositoryPath, string remoteName, int fetchDepth, List<string> refSpec, string additionalCommandLine, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Debug($"Fetch git repository at: {repositoryPath} remote: {remoteName}.");
|
||||
if (refSpec != null && refSpec.Count > 0)
|
||||
{
|
||||
refSpec = refSpec.Where(r => !string.IsNullOrEmpty(r)).ToList();
|
||||
}
|
||||
|
||||
// default options for git fetch.
|
||||
string options = StringUtil.Format($"--tags --prune --progress --no-recurse-submodules {remoteName} {string.Join(" ", refSpec)}");
|
||||
|
||||
// If shallow fetch add --depth arg
|
||||
// If the local repository is shallowed but there is no fetch depth provide for this build,
|
||||
// add --unshallow to convert the shallow repository to a complete repository
|
||||
if (fetchDepth > 0)
|
||||
{
|
||||
options = StringUtil.Format($"--tags --prune --progress --no-recurse-submodules --depth={fetchDepth} {remoteName} {string.Join(" ", refSpec)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
if (File.Exists(Path.Combine(repositoryPath, ".git", "shallow")))
|
||||
{
|
||||
options = StringUtil.Format($"--tags --prune --progress --no-recurse-submodules --unshallow {remoteName} {string.Join(" ", refSpec)}");
|
||||
}
|
||||
}
|
||||
|
||||
int retryCount = 0;
|
||||
int fetchExitCode = 0;
|
||||
while (retryCount < 3)
|
||||
{
|
||||
fetchExitCode = await ExecuteGitCommandAsync(context, repositoryPath, "fetch", options, additionalCommandLine, cancellationToken);
|
||||
if (fetchExitCode == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (++retryCount < 3)
|
||||
{
|
||||
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
|
||||
context.Warning($"Git fetch failed with exit code {fetchExitCode}, back off {backOff.TotalSeconds} seconds before retry.");
|
||||
await Task.Delay(backOff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fetchExitCode;
|
||||
}
|
||||
|
||||
// git fetch --no-tags --prune --progress --no-recurse-submodules [--depth=15] origin [+refs/pull/*:refs/remote/pull/*] [+refs/tags/1:refs/tags/1]
|
||||
public async Task<int> GitFetchNoTags(RunnerActionPluginExecutionContext context, string repositoryPath, string remoteName, int fetchDepth, List<string> refSpec, string additionalCommandLine, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Debug($"Fetch git repository at: {repositoryPath} remote: {remoteName}.");
|
||||
if (refSpec != null && refSpec.Count > 0)
|
||||
{
|
||||
refSpec = refSpec.Where(r => !string.IsNullOrEmpty(r)).ToList();
|
||||
}
|
||||
|
||||
string options;
|
||||
|
||||
// If shallow fetch add --depth arg
|
||||
// If the local repository is shallowed but there is no fetch depth provide for this build,
|
||||
// add --unshallow to convert the shallow repository to a complete repository
|
||||
if (fetchDepth > 0)
|
||||
{
|
||||
options = StringUtil.Format($"--no-tags --prune --progress --no-recurse-submodules --depth={fetchDepth} {remoteName} {string.Join(" ", refSpec)}");
|
||||
}
|
||||
else if (File.Exists(Path.Combine(repositoryPath, ".git", "shallow")))
|
||||
{
|
||||
options = StringUtil.Format($"--no-tags --prune --progress --no-recurse-submodules --unshallow {remoteName} {string.Join(" ", refSpec)}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// default options for git fetch.
|
||||
options = StringUtil.Format($"--no-tags --prune --progress --no-recurse-submodules {remoteName} {string.Join(" ", refSpec)}");
|
||||
}
|
||||
|
||||
int retryCount = 0;
|
||||
int fetchExitCode = 0;
|
||||
while (retryCount < 3)
|
||||
{
|
||||
fetchExitCode = await ExecuteGitCommandAsync(context, repositoryPath, "fetch", options, additionalCommandLine, cancellationToken);
|
||||
if (fetchExitCode == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (++retryCount < 3)
|
||||
{
|
||||
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
|
||||
context.Warning($"Git fetch failed with exit code {fetchExitCode}, back off {backOff.TotalSeconds} seconds before retry.");
|
||||
await Task.Delay(backOff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fetchExitCode;
|
||||
}
|
||||
|
||||
// git lfs fetch origin [ref]
|
||||
public async Task<int> GitLFSFetch(RunnerActionPluginExecutionContext context, string repositoryPath, string remoteName, string refSpec, string additionalCommandLine, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Debug($"Fetch LFS objects for git repository at: {repositoryPath} remote: {remoteName}.");
|
||||
|
||||
// default options for git lfs fetch.
|
||||
string options = StringUtil.Format($"fetch origin {refSpec}");
|
||||
|
||||
int retryCount = 0;
|
||||
int fetchExitCode = 0;
|
||||
while (retryCount < 3)
|
||||
{
|
||||
fetchExitCode = await ExecuteGitCommandAsync(context, repositoryPath, "lfs", options, additionalCommandLine, cancellationToken);
|
||||
if (fetchExitCode == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (++retryCount < 3)
|
||||
{
|
||||
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
|
||||
context.Warning($"Git lfs fetch failed with exit code {fetchExitCode}, back off {backOff.TotalSeconds} seconds before retry.");
|
||||
await Task.Delay(backOff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return fetchExitCode;
|
||||
}
|
||||
|
||||
// git lfs pull
|
||||
public async Task<int> GitLFSPull(RunnerActionPluginExecutionContext context, string repositoryPath, string additionalCommandLine, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Debug($"Download LFS objects for git repository at: {repositoryPath}.");
|
||||
|
||||
int retryCount = 0;
|
||||
int pullExitCode = 0;
|
||||
while (retryCount < 3)
|
||||
{
|
||||
pullExitCode = await ExecuteGitCommandAsync(context, repositoryPath, "lfs", "pull", additionalCommandLine, cancellationToken);
|
||||
if (pullExitCode == 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (++retryCount < 3)
|
||||
{
|
||||
var backOff = BackoffTimerHelper.GetRandomBackoff(TimeSpan.FromSeconds(1), TimeSpan.FromSeconds(10));
|
||||
context.Warning($"Git lfs pull failed with exit code {pullExitCode}, back off {backOff.TotalSeconds} seconds before retry.");
|
||||
await Task.Delay(backOff);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return pullExitCode;
|
||||
}
|
||||
|
||||
// git symbolic-ref -q <HEAD>
|
||||
public async Task<int> GitSymbolicRefHEAD(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug($"Check whether HEAD is detached HEAD.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "symbolic-ref", "-q HEAD");
|
||||
}
|
||||
|
||||
// git checkout -f --progress <commitId/branch>
|
||||
public async Task<int> GitCheckout(RunnerActionPluginExecutionContext context, string repositoryPath, string committishOrBranchSpec, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Debug($"Checkout {committishOrBranchSpec}.");
|
||||
|
||||
// Git 2.7 support report checkout progress to stderr during stdout/err redirect.
|
||||
string options;
|
||||
if (gitVersion >= new Version(2, 7))
|
||||
{
|
||||
options = StringUtil.Format("--progress --force {0}", committishOrBranchSpec);
|
||||
}
|
||||
else
|
||||
{
|
||||
options = StringUtil.Format("--force {0}", committishOrBranchSpec);
|
||||
}
|
||||
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "checkout", options, cancellationToken);
|
||||
}
|
||||
|
||||
// git checkout -B --progress branch remoteBranch
|
||||
public async Task<int> GitCheckoutB(RunnerActionPluginExecutionContext context, string repositoryPath, string newBranch, string startPoint, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Debug($"Checkout -B {newBranch} {startPoint}.");
|
||||
|
||||
// Git 2.7 support report checkout progress to stderr during stdout/err redirect.
|
||||
string options;
|
||||
if (gitVersion >= new Version(2, 7))
|
||||
{
|
||||
options = $"--progress --force -B {newBranch} {startPoint}";
|
||||
}
|
||||
else
|
||||
{
|
||||
options = $"--force -B {newBranch} {startPoint}";
|
||||
}
|
||||
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "checkout", options, cancellationToken);
|
||||
}
|
||||
|
||||
// git clean -ffdx
|
||||
public async Task<int> GitClean(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug($"Delete untracked files/folders for repository at {repositoryPath}.");
|
||||
|
||||
// Git 2.4 support git clean -ffdx.
|
||||
string options;
|
||||
if (gitVersion >= new Version(2, 4))
|
||||
{
|
||||
options = "-ffdx";
|
||||
}
|
||||
else
|
||||
{
|
||||
options = "-fdx";
|
||||
}
|
||||
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "clean", options);
|
||||
}
|
||||
|
||||
// git reset --hard <commit>
|
||||
public async Task<int> GitReset(RunnerActionPluginExecutionContext context, string repositoryPath, string commit = "HEAD")
|
||||
{
|
||||
context.Debug($"Undo any changes to tracked files in the working tree for repository at {repositoryPath}.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "reset", $"--hard {commit}");
|
||||
}
|
||||
|
||||
// get remote set-url <origin> <url>
|
||||
public async Task<int> GitRemoteAdd(RunnerActionPluginExecutionContext context, string repositoryPath, string remoteName, string remoteUrl)
|
||||
{
|
||||
context.Debug($"Add git remote: {remoteName} to url: {remoteUrl} for repository under: {repositoryPath}.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "remote", StringUtil.Format($"add {remoteName} {remoteUrl}"));
|
||||
}
|
||||
|
||||
// get remote set-url <origin> <url>
|
||||
public async Task<int> GitRemoteSetUrl(RunnerActionPluginExecutionContext context, string repositoryPath, string remoteName, string remoteUrl)
|
||||
{
|
||||
context.Debug($"Set git fetch url to: {remoteUrl} for remote: {remoteName}.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "remote", StringUtil.Format($"set-url {remoteName} {remoteUrl}"));
|
||||
}
|
||||
|
||||
// get remote set-url --push <origin> <url>
|
||||
public async Task<int> GitRemoteSetPushUrl(RunnerActionPluginExecutionContext context, string repositoryPath, string remoteName, string remoteUrl)
|
||||
{
|
||||
context.Debug($"Set git push url to: {remoteUrl} for remote: {remoteName}.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "remote", StringUtil.Format($"set-url --push {remoteName} {remoteUrl}"));
|
||||
}
|
||||
|
||||
// git submodule foreach git clean -ffdx
|
||||
public async Task<int> GitSubmoduleClean(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug($"Delete untracked files/folders for submodules at {repositoryPath}.");
|
||||
|
||||
// Git 2.4 support git clean -ffdx.
|
||||
string options;
|
||||
if (gitVersion >= new Version(2, 4))
|
||||
{
|
||||
options = "-ffdx";
|
||||
}
|
||||
else
|
||||
{
|
||||
options = "-fdx";
|
||||
}
|
||||
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "submodule", $"foreach git clean {options}");
|
||||
}
|
||||
|
||||
// git submodule foreach git reset --hard HEAD
|
||||
public async Task<int> GitSubmoduleReset(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug($"Undo any changes to tracked files in the working tree for submodules at {repositoryPath}.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "submodule", "foreach git reset --hard HEAD");
|
||||
}
|
||||
|
||||
// git submodule update --init --force [--depth=15] [--recursive]
|
||||
public async Task<int> GitSubmoduleUpdate(RunnerActionPluginExecutionContext context, string repositoryPath, int fetchDepth, string additionalCommandLine, bool recursive, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Debug("Update the registered git submodules.");
|
||||
string options = "update --init --force";
|
||||
if (fetchDepth > 0)
|
||||
{
|
||||
options = options + $" --depth={fetchDepth}";
|
||||
}
|
||||
if (recursive)
|
||||
{
|
||||
options = options + " --recursive";
|
||||
}
|
||||
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "submodule", options, additionalCommandLine, cancellationToken);
|
||||
}
|
||||
|
||||
// git submodule sync [--recursive]
|
||||
public async Task<int> GitSubmoduleSync(RunnerActionPluginExecutionContext context, string repositoryPath, bool recursive, CancellationToken cancellationToken)
|
||||
{
|
||||
context.Debug("Synchronizes submodules' remote URL configuration setting.");
|
||||
string options = "sync";
|
||||
if (recursive)
|
||||
{
|
||||
options = options + " --recursive";
|
||||
}
|
||||
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "submodule", options, cancellationToken);
|
||||
}
|
||||
|
||||
// git config --get remote.origin.url
|
||||
public async Task<Uri> GitGetFetchUrl(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug($"Inspect remote.origin.url for repository under {repositoryPath}");
|
||||
Uri fetchUrl = null;
|
||||
|
||||
List<string> outputStrings = new List<string>();
|
||||
int exitCode = await ExecuteGitCommandAsync(context, repositoryPath, "config", "--get remote.origin.url", outputStrings);
|
||||
|
||||
if (exitCode != 0)
|
||||
{
|
||||
context.Warning($"'git config --get remote.origin.url' failed with exit code: {exitCode}, output: '{string.Join(Environment.NewLine, outputStrings)}'");
|
||||
}
|
||||
else
|
||||
{
|
||||
// remove empty strings
|
||||
outputStrings = outputStrings.Where(o => !string.IsNullOrEmpty(o)).ToList();
|
||||
if (outputStrings.Count == 1 && !string.IsNullOrEmpty(outputStrings.First()))
|
||||
{
|
||||
string remoteFetchUrl = outputStrings.First();
|
||||
if (Uri.IsWellFormedUriString(remoteFetchUrl, UriKind.Absolute))
|
||||
{
|
||||
context.Debug($"Get remote origin fetch url from git config: {remoteFetchUrl}");
|
||||
fetchUrl = new Uri(remoteFetchUrl);
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Debug($"The Origin fetch url from git config: {remoteFetchUrl} is not a absolute well formed url.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Debug($"Unable capture git remote fetch uri from 'git config --get remote.origin.url' command's output, the command's output is not expected: {string.Join(Environment.NewLine, outputStrings)}.");
|
||||
}
|
||||
}
|
||||
|
||||
return fetchUrl;
|
||||
}
|
||||
|
||||
// git config <key> <value>
|
||||
public async Task<int> GitConfig(RunnerActionPluginExecutionContext context, string repositoryPath, string configKey, string configValue)
|
||||
{
|
||||
context.Debug($"Set git config {configKey} {configValue}");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "config", StringUtil.Format($"{configKey} {configValue}"));
|
||||
}
|
||||
|
||||
// git config --get-all <key>
|
||||
public async Task<bool> GitConfigExist(RunnerActionPluginExecutionContext context, string repositoryPath, string configKey)
|
||||
{
|
||||
// git config --get-all {configKey} will return 0 and print the value if the config exist.
|
||||
context.Debug($"Checking git config {configKey} exist or not");
|
||||
|
||||
// ignore any outputs by redirect them into a string list, since the output might contains secrets.
|
||||
List<string> outputStrings = new List<string>();
|
||||
int exitcode = await ExecuteGitCommandAsync(context, repositoryPath, "config", StringUtil.Format($"--get-all {configKey}"), outputStrings);
|
||||
|
||||
return exitcode == 0;
|
||||
}
|
||||
|
||||
// git config --unset-all <key>
|
||||
public async Task<int> GitConfigUnset(RunnerActionPluginExecutionContext context, string repositoryPath, string configKey)
|
||||
{
|
||||
context.Debug($"Unset git config --unset-all {configKey}");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "config", StringUtil.Format($"--unset-all {configKey}"));
|
||||
}
|
||||
|
||||
// git config gc.auto 0
|
||||
public async Task<int> GitDisableAutoGC(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug("Disable git auto garbage collection.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "config", "gc.auto 0");
|
||||
}
|
||||
|
||||
// git repack -adfl
|
||||
public async Task<int> GitRepack(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug("Compress .git directory.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "repack", "-adfl");
|
||||
}
|
||||
|
||||
// git prune
|
||||
public async Task<int> GitPrune(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug("Delete unreachable objects under .git directory.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "prune", "-v");
|
||||
}
|
||||
|
||||
// git count-objects -v -H
|
||||
public async Task<int> GitCountObjects(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug("Inspect .git directory.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "count-objects", "-v -H");
|
||||
}
|
||||
|
||||
// git lfs install --local
|
||||
public async Task<int> GitLFSInstall(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug("Ensure git-lfs installed.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "lfs", "install --local");
|
||||
}
|
||||
|
||||
// git lfs logs last
|
||||
public async Task<int> GitLFSLogs(RunnerActionPluginExecutionContext context, string repositoryPath)
|
||||
{
|
||||
context.Debug("Get git-lfs logs.");
|
||||
return await ExecuteGitCommandAsync(context, repositoryPath, "lfs", "logs last");
|
||||
}
|
||||
|
||||
// git version
|
||||
public async Task<Version> GitVersion(RunnerActionPluginExecutionContext context)
|
||||
{
|
||||
context.Debug("Get git version.");
|
||||
string runnerWorkspace = context.GetRunnerContext("workspace");
|
||||
ArgUtil.Directory(runnerWorkspace, "runnerWorkspace");
|
||||
Version version = null;
|
||||
List<string> outputStrings = new List<string>();
|
||||
int exitCode = await ExecuteGitCommandAsync(context, runnerWorkspace, "version", null, outputStrings);
|
||||
context.Output($"{string.Join(Environment.NewLine, outputStrings)}");
|
||||
if (exitCode == 0)
|
||||
{
|
||||
// remove any empty line.
|
||||
outputStrings = outputStrings.Where(o => !string.IsNullOrEmpty(o)).ToList();
|
||||
if (outputStrings.Count == 1 && !string.IsNullOrEmpty(outputStrings.First()))
|
||||
{
|
||||
string verString = outputStrings.First();
|
||||
// we interested about major.minor.patch version
|
||||
Regex verRegex = new Regex("\\d+\\.\\d+(\\.\\d+)?", RegexOptions.IgnoreCase);
|
||||
var matchResult = verRegex.Match(verString);
|
||||
if (matchResult.Success && !string.IsNullOrEmpty(matchResult.Value))
|
||||
{
|
||||
if (!Version.TryParse(matchResult.Value, out version))
|
||||
{
|
||||
version = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
// git lfs version
|
||||
public async Task<Version> GitLfsVersion(RunnerActionPluginExecutionContext context)
|
||||
{
|
||||
context.Debug("Get git-lfs version.");
|
||||
string runnerWorkspace = context.GetRunnerContext("workspace");
|
||||
ArgUtil.Directory(runnerWorkspace, "runnerWorkspace");
|
||||
Version version = null;
|
||||
List<string> outputStrings = new List<string>();
|
||||
int exitCode = await ExecuteGitCommandAsync(context, runnerWorkspace, "lfs version", null, outputStrings);
|
||||
context.Output($"{string.Join(Environment.NewLine, outputStrings)}");
|
||||
if (exitCode == 0)
|
||||
{
|
||||
// remove any empty line.
|
||||
outputStrings = outputStrings.Where(o => !string.IsNullOrEmpty(o)).ToList();
|
||||
if (outputStrings.Count == 1 && !string.IsNullOrEmpty(outputStrings.First()))
|
||||
{
|
||||
string verString = outputStrings.First();
|
||||
// we interested about major.minor.patch version
|
||||
Regex verRegex = new Regex("\\d+\\.\\d+(\\.\\d+)?", RegexOptions.IgnoreCase);
|
||||
var matchResult = verRegex.Match(verString);
|
||||
if (matchResult.Success && !string.IsNullOrEmpty(matchResult.Value))
|
||||
{
|
||||
if (!Version.TryParse(matchResult.Value, out version))
|
||||
{
|
||||
version = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return version;
|
||||
}
|
||||
|
||||
private async Task<int> ExecuteGitCommandAsync(RunnerActionPluginExecutionContext context, string repoRoot, string command, string options, CancellationToken cancellationToken = default(CancellationToken))
|
||||
{
|
||||
string arg = StringUtil.Format($"{command} {options}").Trim();
|
||||
context.Command($"git {arg}");
|
||||
|
||||
var processInvoker = new ProcessInvoker(context);
|
||||
processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
context.Output(message.Data);
|
||||
};
|
||||
|
||||
processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
context.Output(message.Data);
|
||||
};
|
||||
|
||||
return await processInvoker.ExecuteAsync(
|
||||
workingDirectory: repoRoot,
|
||||
fileName: gitPath,
|
||||
arguments: arg,
|
||||
environment: gitEnv,
|
||||
requireExitCodeZero: false,
|
||||
outputEncoding: s_encoding,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
|
||||
private async Task<int> ExecuteGitCommandAsync(RunnerActionPluginExecutionContext context, string repoRoot, string command, string options, IList<string> output)
|
||||
{
|
||||
string arg = StringUtil.Format($"{command} {options}").Trim();
|
||||
context.Command($"git {arg}");
|
||||
|
||||
if (output == null)
|
||||
{
|
||||
output = new List<string>();
|
||||
}
|
||||
|
||||
var processInvoker = new ProcessInvoker(context);
|
||||
processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
output.Add(message.Data);
|
||||
};
|
||||
|
||||
processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
context.Output(message.Data);
|
||||
};
|
||||
|
||||
return await processInvoker.ExecuteAsync(
|
||||
workingDirectory: repoRoot,
|
||||
fileName: gitPath,
|
||||
arguments: arg,
|
||||
environment: gitEnv,
|
||||
requireExitCodeZero: false,
|
||||
outputEncoding: s_encoding,
|
||||
cancellationToken: default(CancellationToken));
|
||||
}
|
||||
|
||||
private async Task<int> ExecuteGitCommandAsync(RunnerActionPluginExecutionContext context, string repoRoot, string command, string options, string additionalCommandLine, CancellationToken cancellationToken)
|
||||
{
|
||||
string arg = StringUtil.Format($"{additionalCommandLine} {command} {options}").Trim();
|
||||
context.Command($"git {arg}");
|
||||
|
||||
var processInvoker = new ProcessInvoker(context);
|
||||
processInvoker.OutputDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
context.Output(message.Data);
|
||||
};
|
||||
|
||||
processInvoker.ErrorDataReceived += delegate (object sender, ProcessDataReceivedEventArgs message)
|
||||
{
|
||||
context.Output(message.Data);
|
||||
};
|
||||
|
||||
return await processInvoker.ExecuteAsync(
|
||||
workingDirectory: repoRoot,
|
||||
fileName: gitPath,
|
||||
arguments: arg,
|
||||
environment: gitEnv,
|
||||
requireExitCodeZero: false,
|
||||
outputEncoding: s_encoding,
|
||||
cancellationToken: cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
703
src/Runner.Plugins/Repository/v1.0/GitSourceProvider.cs
Normal file
703
src/Runner.Plugins/Repository/v1.0/GitSourceProvider.cs
Normal file
@@ -0,0 +1,703 @@
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text;
|
||||
using System.Diagnostics;
|
||||
using GitHub.Runner.Sdk;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Services.WebApi;
|
||||
|
||||
namespace GitHub.Runner.Plugins.Repository.v1_0
|
||||
{
|
||||
public sealed class GitHubSourceProvider
|
||||
{
|
||||
// refs prefix
|
||||
private const string _refsPrefix = "refs/heads/";
|
||||
private const string _remoteRefsPrefix = "refs/remotes/origin/";
|
||||
private const string _pullRefsPrefix = "refs/pull/";
|
||||
private const string _remotePullRefsPrefix = "refs/remotes/pull/";
|
||||
|
||||
// min git version that support add extra auth header.
|
||||
private Version _minGitVersionSupportAuthHeader = new Version(2, 9);
|
||||
|
||||
#if OS_WINDOWS
|
||||
// min git version that support override sslBackend setting.
|
||||
private Version _minGitVersionSupportSSLBackendOverride = new Version(2, 14, 2);
|
||||
#endif
|
||||
|
||||
// min git-lfs version that support add extra auth header.
|
||||
private Version _minGitLfsVersionSupportAuthHeader = new Version(2, 1);
|
||||
|
||||
private void RequirementCheck(RunnerActionPluginExecutionContext executionContext, GitCliManager gitCommandManager, bool checkGitLfs)
|
||||
{
|
||||
// v2.9 git exist use auth header.
|
||||
gitCommandManager.EnsureGitVersion(_minGitVersionSupportAuthHeader, throwOnNotMatch: true);
|
||||
|
||||
#if OS_WINDOWS
|
||||
// check git version for SChannel SSLBackend (Windows Only)
|
||||
bool schannelSslBackend = StringUtil.ConvertToBoolean(executionContext.GetRunnerContext("gituseschannel"));
|
||||
if (schannelSslBackend)
|
||||
{
|
||||
gitCommandManager.EnsureGitVersion(_minGitVersionSupportSSLBackendOverride, throwOnNotMatch: true);
|
||||
}
|
||||
#endif
|
||||
if (checkGitLfs)
|
||||
{
|
||||
// v2.1 git-lfs exist use auth header.
|
||||
gitCommandManager.EnsureGitLFSVersion(_minGitLfsVersionSupportAuthHeader, throwOnNotMatch: true);
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateBasicAuthHeader(RunnerActionPluginExecutionContext executionContext, string accessToken)
|
||||
{
|
||||
// use basic auth header with username:password in base64encoding.
|
||||
string authHeader = $"x-access-token:{accessToken}";
|
||||
string base64encodedAuthHeader = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader));
|
||||
|
||||
// add base64 encoding auth header into secretMasker.
|
||||
executionContext.AddMask(base64encodedAuthHeader);
|
||||
return $"basic {base64encodedAuthHeader}";
|
||||
}
|
||||
|
||||
public async Task GetSourceAsync(
|
||||
RunnerActionPluginExecutionContext executionContext,
|
||||
string repositoryPath,
|
||||
string repoFullName,
|
||||
string sourceBranch,
|
||||
string sourceVersion,
|
||||
bool clean,
|
||||
string submoduleInput,
|
||||
int fetchDepth,
|
||||
bool gitLfsSupport,
|
||||
string accessToken,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Validate args.
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
Uri proxyUrlWithCred = null;
|
||||
string proxyUrlWithCredString = null;
|
||||
bool useSelfSignedCACert = false;
|
||||
bool useClientCert = false;
|
||||
string clientCertPrivateKeyAskPassFile = null;
|
||||
bool acceptUntrustedCerts = false;
|
||||
|
||||
executionContext.Output($"Syncing repository: {repoFullName}");
|
||||
Uri repositoryUrl = new Uri($"https://github.com/{repoFullName}");
|
||||
if (!repositoryUrl.IsAbsoluteUri)
|
||||
{
|
||||
throw new InvalidOperationException("Repository url need to be an absolute uri.");
|
||||
}
|
||||
|
||||
string targetPath = repositoryPath;
|
||||
|
||||
// input Submodules can be ['', true, false, recursive]
|
||||
// '' or false indicate don't checkout submodules
|
||||
// true indicate checkout top level submodules
|
||||
// recursive indicate checkout submodules recursively
|
||||
bool checkoutSubmodules = false;
|
||||
bool checkoutNestedSubmodules = false;
|
||||
if (!string.IsNullOrEmpty(submoduleInput))
|
||||
{
|
||||
if (string.Equals(submoduleInput, Pipelines.PipelineConstants.CheckoutTaskInputs.SubmodulesOptions.Recursive, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
checkoutSubmodules = true;
|
||||
checkoutNestedSubmodules = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
checkoutSubmodules = StringUtil.ConvertToBoolean(submoduleInput);
|
||||
}
|
||||
}
|
||||
|
||||
var runnerCert = executionContext.GetCertConfiguration();
|
||||
acceptUntrustedCerts = runnerCert?.SkipServerCertificateValidation ?? false;
|
||||
|
||||
executionContext.Debug($"repository url={repositoryUrl}");
|
||||
executionContext.Debug($"targetPath={targetPath}");
|
||||
executionContext.Debug($"sourceBranch={sourceBranch}");
|
||||
executionContext.Debug($"sourceVersion={sourceVersion}");
|
||||
executionContext.Debug($"clean={clean}");
|
||||
executionContext.Debug($"checkoutSubmodules={checkoutSubmodules}");
|
||||
executionContext.Debug($"checkoutNestedSubmodules={checkoutNestedSubmodules}");
|
||||
executionContext.Debug($"fetchDepth={fetchDepth}");
|
||||
executionContext.Debug($"gitLfsSupport={gitLfsSupport}");
|
||||
executionContext.Debug($"acceptUntrustedCerts={acceptUntrustedCerts}");
|
||||
|
||||
#if OS_WINDOWS
|
||||
bool schannelSslBackend = StringUtil.ConvertToBoolean(executionContext.GetRunnerContext("gituseschannel"));
|
||||
executionContext.Debug($"schannelSslBackend={schannelSslBackend}");
|
||||
#endif
|
||||
|
||||
// Initialize git command manager with additional environment variables.
|
||||
Dictionary<string, string> gitEnv = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Disable prompting for git credential manager
|
||||
gitEnv["GCM_INTERACTIVE"] = "Never";
|
||||
|
||||
// Git-lfs will try to pull down asset if any of the local/user/system setting exist.
|
||||
// If customer didn't enable `LFS` in their pipeline definition, we will use ENV to disable LFS fetch/checkout.
|
||||
if (!gitLfsSupport)
|
||||
{
|
||||
gitEnv["GIT_LFS_SKIP_SMUDGE"] = "1";
|
||||
}
|
||||
|
||||
// Add the public variables.
|
||||
foreach (var variable in executionContext.Variables)
|
||||
{
|
||||
// Add the variable using the formatted name.
|
||||
string formattedKey = (variable.Key ?? string.Empty).Replace('.', '_').Replace(' ', '_').ToUpperInvariant();
|
||||
gitEnv[formattedKey] = variable.Value?.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
GitCliManager gitCommandManager = new GitCliManager(gitEnv);
|
||||
await gitCommandManager.LoadGitExecutionInfo(executionContext);
|
||||
|
||||
// Make sure the build machine met all requirements for the git repository
|
||||
// For now, the requirement we have are:
|
||||
// 1. git version greater than 2.9 since we need to use auth header.
|
||||
// 2. git-lfs version greater than 2.1 since we need to use auth header.
|
||||
// 3. git version greater than 2.14.2 if use SChannel for SSL backend (Windows only)
|
||||
RequirementCheck(executionContext, gitCommandManager, gitLfsSupport);
|
||||
|
||||
// prepare credentail embedded urls
|
||||
var runnerProxy = executionContext.GetProxyConfiguration();
|
||||
if (runnerProxy != null && !string.IsNullOrEmpty(runnerProxy.ProxyAddress) && !runnerProxy.WebProxy.IsBypassed(repositoryUrl))
|
||||
{
|
||||
proxyUrlWithCred = UrlUtil.GetCredentialEmbeddedUrl(new Uri(runnerProxy.ProxyAddress), runnerProxy.ProxyUsername, runnerProxy.ProxyPassword);
|
||||
|
||||
// uri.absoluteuri will not contains port info if the scheme is http/https and the port is 80/443
|
||||
// however, git.exe always require you provide port info, if nothing passed in, it will use 1080 as default
|
||||
// as result, we need prefer the uri.originalstring when it's different than uri.absoluteuri.
|
||||
if (string.Equals(proxyUrlWithCred.AbsoluteUri, proxyUrlWithCred.OriginalString, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
proxyUrlWithCredString = proxyUrlWithCred.AbsoluteUri;
|
||||
}
|
||||
else
|
||||
{
|
||||
proxyUrlWithCredString = proxyUrlWithCred.OriginalString;
|
||||
}
|
||||
}
|
||||
|
||||
// prepare askpass for client cert private key, if the repository's endpoint url match the runner config url
|
||||
var systemConnection = executionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||
if (runnerCert != null && Uri.Compare(repositoryUrl, systemConnection.Url, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(runnerCert.CACertificateFile))
|
||||
{
|
||||
useSelfSignedCACert = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificateFile) &&
|
||||
!string.IsNullOrEmpty(runnerCert.ClientCertificatePrivateKeyFile))
|
||||
{
|
||||
useClientCert = true;
|
||||
|
||||
// prepare askpass for client cert password
|
||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificatePassword))
|
||||
{
|
||||
clientCertPrivateKeyAskPassFile = Path.Combine(executionContext.GetRunnerContext("temp"), $"{Guid.NewGuid()}.sh");
|
||||
List<string> askPass = new List<string>();
|
||||
askPass.Add("#!/bin/sh");
|
||||
askPass.Add($"echo \"{runnerCert.ClientCertificatePassword}\"");
|
||||
File.WriteAllLines(clientCertPrivateKeyAskPassFile, askPass);
|
||||
|
||||
#if !OS_WINDOWS
|
||||
string toolPath = WhichUtil.Which("chmod", true);
|
||||
string argLine = $"775 {clientCertPrivateKeyAskPassFile}";
|
||||
executionContext.Command($"chmod {argLine}");
|
||||
|
||||
var processInvoker = new ProcessInvoker(executionContext);
|
||||
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
executionContext.Output(args.Data);
|
||||
}
|
||||
};
|
||||
processInvoker.ErrorDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
executionContext.Output(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
string workingDirectory = executionContext.GetRunnerContext("workspace");
|
||||
await processInvoker.ExecuteAsync(workingDirectory, toolPath, argLine, null, true, CancellationToken.None);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check the current contents of the root folder to see if there is already a repo
|
||||
// If there is a repo, see if it matches the one we are expecting to be there based on the remote fetch url
|
||||
// if the repo is not what we expect, remove the folder
|
||||
if (!await IsRepositoryOriginUrlMatch(executionContext, gitCommandManager, targetPath, repositoryUrl))
|
||||
{
|
||||
// Delete source folder
|
||||
IOUtil.DeleteDirectory(targetPath, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// delete the index.lock file left by previous canceled build or any operation cause git.exe crash last time.
|
||||
string lockFile = Path.Combine(targetPath, ".git\\index.lock");
|
||||
if (File.Exists(lockFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(lockFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
executionContext.Debug($"Unable to delete the index.lock file: {lockFile}");
|
||||
executionContext.Debug(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// delete the shallow.lock file left by previous canceled build or any operation cause git.exe crash last time.
|
||||
string shallowLockFile = Path.Combine(targetPath, ".git\\shallow.lock");
|
||||
if (File.Exists(shallowLockFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(shallowLockFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
executionContext.Debug($"Unable to delete the shallow.lock file: {shallowLockFile}");
|
||||
executionContext.Debug(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// When repo.clean is selected for a git repo, execute git clean -ffdx and git reset --hard HEAD on the current repo.
|
||||
// This will help us save the time to reclone the entire repo.
|
||||
// If any git commands exit with non-zero return code or any exception happened during git.exe invoke, fall back to delete the repo folder.
|
||||
if (clean)
|
||||
{
|
||||
Boolean softCleanSucceed = true;
|
||||
|
||||
// git clean -ffdx
|
||||
int exitCode_clean = await gitCommandManager.GitClean(executionContext, targetPath);
|
||||
if (exitCode_clean != 0)
|
||||
{
|
||||
executionContext.Debug($"'git clean -ffdx' failed with exit code {exitCode_clean}, this normally caused by:\n 1) Path too long\n 2) Permission issue\n 3) File in use\nFor futher investigation, manually run 'git clean -ffdx' on repo root: {targetPath} after each build.");
|
||||
softCleanSucceed = false;
|
||||
}
|
||||
|
||||
// git reset --hard HEAD
|
||||
if (softCleanSucceed)
|
||||
{
|
||||
int exitCode_reset = await gitCommandManager.GitReset(executionContext, targetPath);
|
||||
if (exitCode_reset != 0)
|
||||
{
|
||||
executionContext.Debug($"'git reset --hard HEAD' failed with exit code {exitCode_reset}\nFor futher investigation, manually run 'git reset --hard HEAD' on repo root: {targetPath} after each build.");
|
||||
softCleanSucceed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// git clean -ffdx and git reset --hard HEAD for each submodule
|
||||
if (checkoutSubmodules)
|
||||
{
|
||||
if (softCleanSucceed)
|
||||
{
|
||||
int exitCode_submoduleclean = await gitCommandManager.GitSubmoduleClean(executionContext, targetPath);
|
||||
if (exitCode_submoduleclean != 0)
|
||||
{
|
||||
executionContext.Debug($"'git submodule foreach git clean -ffdx' failed with exit code {exitCode_submoduleclean}\nFor futher investigation, manually run 'git submodule foreach git clean -ffdx' on repo root: {targetPath} after each build.");
|
||||
softCleanSucceed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (softCleanSucceed)
|
||||
{
|
||||
int exitCode_submodulereset = await gitCommandManager.GitSubmoduleReset(executionContext, targetPath);
|
||||
if (exitCode_submodulereset != 0)
|
||||
{
|
||||
executionContext.Debug($"'git submodule foreach git reset --hard HEAD' failed with exit code {exitCode_submodulereset}\nFor futher investigation, manually run 'git submodule foreach git reset --hard HEAD' on repo root: {targetPath} after each build.");
|
||||
softCleanSucceed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!softCleanSucceed)
|
||||
{
|
||||
//fall back
|
||||
executionContext.Warning("Unable to run \"git clean -ffdx\" and \"git reset --hard HEAD\" successfully, delete source folder instead.");
|
||||
IOUtil.DeleteDirectory(targetPath, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the folder is missing, create it
|
||||
if (!Directory.Exists(targetPath))
|
||||
{
|
||||
Directory.CreateDirectory(targetPath);
|
||||
}
|
||||
|
||||
// if the folder contains a .git folder, it means the folder contains a git repo that matches the remote url and in a clean state.
|
||||
// we will run git fetch to update the repo.
|
||||
if (!Directory.Exists(Path.Combine(targetPath, ".git")))
|
||||
{
|
||||
// init git repository
|
||||
int exitCode_init = await gitCommandManager.GitInit(executionContext, targetPath);
|
||||
if (exitCode_init != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to use git.exe init repository under {targetPath}, 'git init' failed with exit code: {exitCode_init}");
|
||||
}
|
||||
|
||||
int exitCode_addremote = await gitCommandManager.GitRemoteAdd(executionContext, targetPath, "origin", repositoryUrl.AbsoluteUri);
|
||||
if (exitCode_addremote != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to use git.exe add remote 'origin', 'git remote add' failed with exit code: {exitCode_addremote}");
|
||||
}
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// disable git auto gc
|
||||
int exitCode_disableGC = await gitCommandManager.GitDisableAutoGC(executionContext, targetPath);
|
||||
if (exitCode_disableGC != 0)
|
||||
{
|
||||
executionContext.Warning("Unable turn off git auto garbage collection, git fetch operation may trigger auto garbage collection which will affect the performance of fetching.");
|
||||
}
|
||||
|
||||
// always remove any possible left extraheader setting from git config.
|
||||
if (await gitCommandManager.GitConfigExist(executionContext, targetPath, $"http.{repositoryUrl.AbsoluteUri}.extraheader"))
|
||||
{
|
||||
executionContext.Debug("Remove any extraheader setting from git config.");
|
||||
await RemoveGitConfig(executionContext, gitCommandManager, targetPath, $"http.{repositoryUrl.AbsoluteUri}.extraheader", string.Empty);
|
||||
}
|
||||
|
||||
// always remove any possible left proxy setting from git config, the proxy setting may contains credential
|
||||
if (await gitCommandManager.GitConfigExist(executionContext, targetPath, $"http.proxy"))
|
||||
{
|
||||
executionContext.Debug("Remove any proxy setting from git config.");
|
||||
await RemoveGitConfig(executionContext, gitCommandManager, targetPath, $"http.proxy", string.Empty);
|
||||
}
|
||||
|
||||
List<string> additionalFetchArgs = new List<string>();
|
||||
List<string> additionalLfsFetchArgs = new List<string>();
|
||||
|
||||
// add accessToken as basic auth header to handle auth challenge.
|
||||
if (!string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.extraheader=\"AUTHORIZATION: {GenerateBasicAuthHeader(executionContext, accessToken)}\"");
|
||||
}
|
||||
|
||||
// Prepare proxy config for fetch.
|
||||
if (runnerProxy != null && !string.IsNullOrEmpty(runnerProxy.ProxyAddress) && !runnerProxy.WebProxy.IsBypassed(repositoryUrl))
|
||||
{
|
||||
executionContext.Debug($"Config proxy server '{runnerProxy.ProxyAddress}' for git fetch.");
|
||||
ArgUtil.NotNullOrEmpty(proxyUrlWithCredString, nameof(proxyUrlWithCredString));
|
||||
additionalFetchArgs.Add($"-c http.proxy=\"{proxyUrlWithCredString}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.proxy=\"{proxyUrlWithCredString}\"");
|
||||
}
|
||||
|
||||
// Prepare ignore ssl cert error config for fetch.
|
||||
if (acceptUntrustedCerts)
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslVerify=false");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslVerify=false");
|
||||
}
|
||||
|
||||
// Prepare self-signed CA cert config for fetch from server.
|
||||
if (useSelfSignedCACert)
|
||||
{
|
||||
executionContext.Debug($"Use self-signed certificate '{runnerCert.CACertificateFile}' for git fetch.");
|
||||
additionalFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
}
|
||||
|
||||
// Prepare client cert config for fetch from server.
|
||||
if (useClientCert)
|
||||
{
|
||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git fetch.");
|
||||
|
||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
}
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
if (schannelSslBackend)
|
||||
{
|
||||
executionContext.Debug("Use SChannel SslBackend for git fetch.");
|
||||
additionalFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
additionalLfsFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
}
|
||||
#endif
|
||||
// Prepare gitlfs url for fetch and checkout
|
||||
if (gitLfsSupport)
|
||||
{
|
||||
// Initialize git lfs by execute 'git lfs install'
|
||||
executionContext.Debug("Setup the local Git hooks for Git LFS.");
|
||||
int exitCode_lfsInstall = await gitCommandManager.GitLFSInstall(executionContext, targetPath);
|
||||
if (exitCode_lfsInstall != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Git-lfs installation failed with exit code: {exitCode_lfsInstall}");
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
||||
additionalLfsFetchArgs.Add($"-c http.{authorityUrl}.extraheader=\"AUTHORIZATION: {GenerateBasicAuthHeader(executionContext, accessToken)}\"");
|
||||
}
|
||||
}
|
||||
|
||||
List<string> additionalFetchSpecs = new List<string>();
|
||||
additionalFetchSpecs.Add("+refs/heads/*:refs/remotes/origin/*");
|
||||
|
||||
if (IsPullRequest(sourceBranch))
|
||||
{
|
||||
additionalFetchSpecs.Add($"+{sourceBranch}:{GetRemoteRefName(sourceBranch)}");
|
||||
}
|
||||
|
||||
int exitCode_fetch = await gitCommandManager.GitFetch(executionContext, targetPath, "origin", fetchDepth, additionalFetchSpecs, string.Join(" ", additionalFetchArgs), cancellationToken);
|
||||
if (exitCode_fetch != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Git fetch failed with exit code: {exitCode_fetch}");
|
||||
}
|
||||
|
||||
// Checkout
|
||||
// sourceToBuild is used for checkout
|
||||
// if sourceBranch is a PR branch or sourceVersion is null, make sure branch name is a remote branch. we need checkout to detached head.
|
||||
// (change refs/heads to refs/remotes/origin, refs/pull to refs/remotes/pull, or leave it as it when the branch name doesn't contain refs/...)
|
||||
// if sourceVersion provide, just use that for checkout, since when you checkout a commit, it will end up in detached head.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
string sourcesToBuild;
|
||||
if (IsPullRequest(sourceBranch) || string.IsNullOrEmpty(sourceVersion))
|
||||
{
|
||||
sourcesToBuild = GetRemoteRefName(sourceBranch);
|
||||
}
|
||||
else
|
||||
{
|
||||
sourcesToBuild = sourceVersion;
|
||||
}
|
||||
|
||||
// fetch lfs object upfront, this will avoid fetch lfs object during checkout which cause checkout taking forever
|
||||
// since checkout will fetch lfs object 1 at a time, while git lfs fetch will fetch lfs object in parallel.
|
||||
if (gitLfsSupport)
|
||||
{
|
||||
int exitCode_lfsFetch = await gitCommandManager.GitLFSFetch(executionContext, targetPath, "origin", sourcesToBuild, string.Join(" ", additionalLfsFetchArgs), cancellationToken);
|
||||
if (exitCode_lfsFetch != 0)
|
||||
{
|
||||
// local repository is shallow repository, lfs fetch may fail due to lack of commits history.
|
||||
// this will happen when the checkout commit is older than tip -> fetchDepth
|
||||
if (fetchDepth > 0)
|
||||
{
|
||||
executionContext.Warning($"Git lfs fetch failed on shallow repository, this might because of git fetch with depth '{fetchDepth}' doesn't include the lfs fetch commit '{sourcesToBuild}'.");
|
||||
}
|
||||
|
||||
// git lfs fetch failed, get lfs log, the log is critical for debug.
|
||||
int exitCode_lfsLogs = await gitCommandManager.GitLFSLogs(executionContext, targetPath);
|
||||
throw new InvalidOperationException($"Git lfs fetch failed with exit code: {exitCode_lfsFetch}. Git lfs logs returned with exit code: {exitCode_lfsLogs}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, checkout the sourcesToBuild (if we didn't find a valid git object this will throw)
|
||||
int exitCode_checkout = await gitCommandManager.GitCheckout(executionContext, targetPath, sourcesToBuild, cancellationToken);
|
||||
if (exitCode_checkout != 0)
|
||||
{
|
||||
// local repository is shallow repository, checkout may fail due to lack of commits history.
|
||||
// this will happen when the checkout commit is older than tip -> fetchDepth
|
||||
if (fetchDepth > 0)
|
||||
{
|
||||
executionContext.Warning($"Git checkout failed on shallow repository, this might because of git fetch with depth '{fetchDepth}' doesn't include the checkout commit '{sourcesToBuild}'.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Git checkout failed with exit code: {exitCode_checkout}");
|
||||
}
|
||||
|
||||
// Submodule update
|
||||
if (checkoutSubmodules)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
int exitCode_submoduleSync = await gitCommandManager.GitSubmoduleSync(executionContext, targetPath, checkoutNestedSubmodules, cancellationToken);
|
||||
if (exitCode_submoduleSync != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Git submodule sync failed with exit code: {exitCode_submoduleSync}");
|
||||
}
|
||||
|
||||
List<string> additionalSubmoduleUpdateArgs = new List<string>();
|
||||
|
||||
if (!string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.extraheader=\"AUTHORIZATION: {GenerateBasicAuthHeader(executionContext, accessToken)}\"");
|
||||
}
|
||||
|
||||
// Prepare proxy config for submodule update.
|
||||
if (runnerProxy != null && !string.IsNullOrEmpty(runnerProxy.ProxyAddress) && !runnerProxy.WebProxy.IsBypassed(repositoryUrl))
|
||||
{
|
||||
executionContext.Debug($"Config proxy server '{runnerProxy.ProxyAddress}' for git submodule update.");
|
||||
ArgUtil.NotNullOrEmpty(proxyUrlWithCredString, nameof(proxyUrlWithCredString));
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.proxy=\"{proxyUrlWithCredString}\"");
|
||||
}
|
||||
|
||||
// Prepare ignore ssl cert error config for fetch.
|
||||
if (acceptUntrustedCerts)
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.sslVerify=false");
|
||||
}
|
||||
|
||||
// Prepare self-signed CA cert config for submodule update.
|
||||
if (useSelfSignedCACert)
|
||||
{
|
||||
executionContext.Debug($"Use self-signed CA certificate '{runnerCert.CACertificateFile}' for git submodule update.");
|
||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
}
|
||||
|
||||
// Prepare client cert config for submodule update.
|
||||
if (useClientCert)
|
||||
{
|
||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git submodule update.");
|
||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.{authorityUrl}.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
}
|
||||
}
|
||||
#if OS_WINDOWS
|
||||
if (schannelSslBackend)
|
||||
{
|
||||
executionContext.Debug("Use SChannel SslBackend for git submodule update.");
|
||||
additionalSubmoduleUpdateArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
}
|
||||
#endif
|
||||
|
||||
int exitCode_submoduleUpdate = await gitCommandManager.GitSubmoduleUpdate(executionContext, targetPath, fetchDepth, string.Join(" ", additionalSubmoduleUpdateArgs), checkoutNestedSubmodules, cancellationToken);
|
||||
if (exitCode_submoduleUpdate != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Git submodule update failed with exit code: {exitCode_submoduleUpdate}");
|
||||
}
|
||||
}
|
||||
|
||||
if (useClientCert && !string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
executionContext.Debug("Remove git.sslkey askpass file.");
|
||||
IOUtil.DeleteFile(clientCertPrivateKeyAskPassFile);
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<bool> IsRepositoryOriginUrlMatch(RunnerActionPluginExecutionContext context, GitCliManager gitCommandManager, string repositoryPath, Uri expectedRepositoryOriginUrl)
|
||||
{
|
||||
context.Debug($"Checking if the repo on {repositoryPath} matches the expected repository origin URL. expected Url: {expectedRepositoryOriginUrl.AbsoluteUri}");
|
||||
if (!Directory.Exists(Path.Combine(repositoryPath, ".git")))
|
||||
{
|
||||
// There is no repo directory
|
||||
context.Debug($"Repository is not found since '.git' directory does not exist under. {repositoryPath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Uri remoteUrl;
|
||||
remoteUrl = await gitCommandManager.GitGetFetchUrl(context, repositoryPath);
|
||||
|
||||
if (remoteUrl == null)
|
||||
{
|
||||
// origin fetch url not found.
|
||||
context.Debug("Repository remote origin fetch url is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
context.Debug($"Repository remote origin fetch url is {remoteUrl}");
|
||||
// compare the url passed in with the remote url found
|
||||
if (expectedRepositoryOriginUrl.Equals(remoteUrl))
|
||||
{
|
||||
context.Debug("URLs match.");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Debug($"The remote.origin.url of the repository under root folder '{repositoryPath}' doesn't matches source repository url.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveGitConfig(RunnerActionPluginExecutionContext executionContext, GitCliManager gitCommandManager, string targetPath, string configKey, string configValue)
|
||||
{
|
||||
int exitCode_configUnset = await gitCommandManager.GitConfigUnset(executionContext, targetPath, configKey);
|
||||
if (exitCode_configUnset != 0)
|
||||
{
|
||||
// if unable to use git.exe unset http.extraheader, http.proxy or core.askpass, modify git config file on disk. make sure we don't left credential.
|
||||
if (!string.IsNullOrEmpty(configValue))
|
||||
{
|
||||
executionContext.Warning("An unsuccessful attempt was made using git command line to remove \"http.extraheader\" from the git config. Attempting to modify the git config file directly to remove the credential.");
|
||||
string gitConfig = Path.Combine(targetPath, ".git/config");
|
||||
if (File.Exists(gitConfig))
|
||||
{
|
||||
string gitConfigContent = File.ReadAllText(Path.Combine(targetPath, ".git", "config"));
|
||||
if (gitConfigContent.Contains(configKey))
|
||||
{
|
||||
string setting = $"extraheader = {configValue}";
|
||||
gitConfigContent = Regex.Replace(gitConfigContent, setting, string.Empty, RegexOptions.IgnoreCase);
|
||||
|
||||
setting = $"proxy = {configValue}";
|
||||
gitConfigContent = Regex.Replace(gitConfigContent, setting, string.Empty, RegexOptions.IgnoreCase);
|
||||
|
||||
setting = $"askpass = {configValue}";
|
||||
gitConfigContent = Regex.Replace(gitConfigContent, setting, string.Empty, RegexOptions.IgnoreCase);
|
||||
|
||||
File.WriteAllText(gitConfig, gitConfigContent);
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
executionContext.Warning($"Unable to remove \"{configKey}\" from the git config. To remove the credential, execute \"git config --unset - all {configKey}\" from the repository root \"{targetPath}\".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPullRequest(string sourceBranch)
|
||||
{
|
||||
return !string.IsNullOrEmpty(sourceBranch) &&
|
||||
(sourceBranch.StartsWith(_pullRefsPrefix, StringComparison.OrdinalIgnoreCase) ||
|
||||
sourceBranch.StartsWith(_remotePullRefsPrefix, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private string GetRemoteRefName(string refName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(refName))
|
||||
{
|
||||
// If the refName is empty return the remote name for master
|
||||
refName = _remoteRefsPrefix + "master";
|
||||
}
|
||||
else if (refName.Equals("master", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// If the refName is master return the remote name for master
|
||||
refName = _remoteRefsPrefix + refName;
|
||||
}
|
||||
else if (refName.StartsWith(_refsPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// If the refName is refs/heads change it to the remote version of the name
|
||||
refName = _remoteRefsPrefix + refName.Substring(_refsPrefix.Length);
|
||||
}
|
||||
else if (refName.StartsWith(_pullRefsPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// If the refName is refs/pull change it to the remote version of the name
|
||||
refName = refName.Replace(_pullRefsPrefix, _remotePullRefsPrefix);
|
||||
}
|
||||
|
||||
return refName;
|
||||
}
|
||||
}
|
||||
}
|
||||
175
src/Runner.Plugins/Repository/v1.0/RepositoryPlugin.cs
Normal file
175
src/Runner.Plugins/Repository/v1.0/RepositoryPlugin.cs
Normal file
@@ -0,0 +1,175 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Sdk;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
using System.IO;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using System.Text.RegularExpressions;
|
||||
using GitHub.DistributedTask.Pipelines.Expressions;
|
||||
using System.Text;
|
||||
|
||||
namespace GitHub.Runner.Plugins.Repository.v1_0
|
||||
{
|
||||
public class CheckoutTask : IRunnerActionPlugin
|
||||
{
|
||||
private readonly Regex _validSha1 = new Regex(@"\b[0-9a-f]{40}\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, TimeSpan.FromSeconds(2));
|
||||
|
||||
public async Task RunAsync(RunnerActionPluginExecutionContext executionContext, CancellationToken token)
|
||||
{
|
||||
string runnerWorkspace = executionContext.GetRunnerContext("workspace");
|
||||
ArgUtil.Directory(runnerWorkspace, nameof(runnerWorkspace));
|
||||
string tempDirectory = executionContext.GetRunnerContext("temp");
|
||||
ArgUtil.Directory(tempDirectory, nameof(tempDirectory));
|
||||
|
||||
var repoFullName = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Repository);
|
||||
if (string.IsNullOrEmpty(repoFullName))
|
||||
{
|
||||
repoFullName = executionContext.GetGitHubContext("repository");
|
||||
}
|
||||
|
||||
var repoFullNameSplit = repoFullName.Split("/", StringSplitOptions.RemoveEmptyEntries);
|
||||
if (repoFullNameSplit.Length != 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(repoFullName);
|
||||
}
|
||||
|
||||
string expectRepoPath;
|
||||
var path = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Path);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
expectRepoPath = IOUtil.ResolvePath(runnerWorkspace, path);
|
||||
if (!expectRepoPath.StartsWith(runnerWorkspace.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar))
|
||||
{
|
||||
throw new ArgumentException($"Input path '{path}' should resolve to a directory under '{runnerWorkspace}', current resolved path '{expectRepoPath}'.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// When repository doesn't has path set, default to sources directory 1/repoName
|
||||
expectRepoPath = Path.Combine(runnerWorkspace, repoFullNameSplit[1]);
|
||||
}
|
||||
|
||||
var workspaceRepo = executionContext.GetGitHubContext("repository");
|
||||
// for self repository, we need to let the worker knows where it is after checkout.
|
||||
if (string.Equals(workspaceRepo, repoFullName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var workspaceRepoPath = executionContext.GetGitHubContext("workspace");
|
||||
|
||||
executionContext.Debug($"Repository requires to be placed at '{expectRepoPath}', current location is '{workspaceRepoPath}'");
|
||||
if (!string.Equals(workspaceRepoPath.Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), expectRepoPath.Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), IOUtil.FilePathStringComparison))
|
||||
{
|
||||
executionContext.Output($"Repository is current at '{workspaceRepoPath}', move to '{expectRepoPath}'.");
|
||||
var count = 1;
|
||||
var staging = Path.Combine(tempDirectory, $"_{count}");
|
||||
while (Directory.Exists(staging))
|
||||
{
|
||||
count++;
|
||||
staging = Path.Combine(tempDirectory, $"_{count}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
executionContext.Debug($"Move existing repository '{workspaceRepoPath}' to '{expectRepoPath}' via staging directory '{staging}'.");
|
||||
IOUtil.MoveDirectory(workspaceRepoPath, expectRepoPath, staging, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
executionContext.Debug("Catch exception during repository move.");
|
||||
executionContext.Debug(ex.ToString());
|
||||
executionContext.Warning("Unable move and reuse existing repository to required location.");
|
||||
IOUtil.DeleteDirectory(expectRepoPath, CancellationToken.None);
|
||||
}
|
||||
|
||||
executionContext.Output($"Repository will locate at '{expectRepoPath}'.");
|
||||
}
|
||||
|
||||
executionContext.Debug($"Update workspace repository location.");
|
||||
executionContext.SetRepositoryPath(repoFullName, expectRepoPath, true);
|
||||
}
|
||||
|
||||
string sourceBranch;
|
||||
string sourceVersion;
|
||||
string refInput = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Ref);
|
||||
if (string.IsNullOrEmpty(refInput))
|
||||
{
|
||||
sourceBranch = executionContext.GetGitHubContext("ref");
|
||||
sourceVersion = executionContext.GetGitHubContext("sha");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBranch = refInput;
|
||||
sourceVersion = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Version); // version get removed when checkout move to repo in the graph
|
||||
if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.SHA1))
|
||||
{
|
||||
sourceVersion = sourceBranch;
|
||||
|
||||
// If Ref is a SHA and the repo is self, we need to use github.ref as source branch since it might be refs/pull/*
|
||||
if (string.Equals(workspaceRepo, repoFullName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sourceBranch = executionContext.GetGitHubContext("ref");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBranch = "refs/heads/master";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool clean = StringUtil.ConvertToBoolean(executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Clean), true);
|
||||
string submoduleInput = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Submodules);
|
||||
|
||||
int fetchDepth = 0;
|
||||
if (!int.TryParse(executionContext.GetInput("fetch-depth"), out fetchDepth) || fetchDepth < 0)
|
||||
{
|
||||
fetchDepth = 0;
|
||||
}
|
||||
|
||||
bool gitLfsSupport = StringUtil.ConvertToBoolean(executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Lfs));
|
||||
string accessToken = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Token);
|
||||
if (string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
accessToken = executionContext.GetGitHubContext("token");
|
||||
}
|
||||
|
||||
// register problem matcher
|
||||
string problemMatcher = @"
|
||||
{
|
||||
""problemMatcher"": [
|
||||
{
|
||||
""owner"": ""checkout-git"",
|
||||
""pattern"": [
|
||||
{
|
||||
""regexp"": ""^fatal: (.*)$"",
|
||||
""message"": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}";
|
||||
string matcherFile = Path.Combine(tempDirectory, $"git_{Guid.NewGuid()}.json");
|
||||
File.WriteAllText(matcherFile, problemMatcher, new UTF8Encoding(false));
|
||||
executionContext.Output($"##[add-matcher]{matcherFile}");
|
||||
try
|
||||
{
|
||||
await new GitHubSourceProvider().GetSourceAsync(executionContext,
|
||||
expectRepoPath,
|
||||
repoFullName,
|
||||
sourceBranch,
|
||||
sourceVersion,
|
||||
clean,
|
||||
submoduleInput,
|
||||
fetchDepth,
|
||||
gitLfsSupport,
|
||||
accessToken,
|
||||
token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
executionContext.Output("##[remove-matcher owner=checkout-git]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
740
src/Runner.Plugins/Repository/v1.1/GitSourceProvider.cs
Normal file
740
src/Runner.Plugins/Repository/v1.1/GitSourceProvider.cs
Normal file
@@ -0,0 +1,740 @@
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.IO;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Text;
|
||||
using System.Diagnostics;
|
||||
using GitHub.Runner.Sdk;
|
||||
using System.Linq;
|
||||
using GitHub.DistributedTask.WebApi;
|
||||
using GitHub.Services.WebApi;
|
||||
|
||||
namespace GitHub.Runner.Plugins.Repository.v1_1
|
||||
{
|
||||
public sealed class GitHubSourceProvider
|
||||
{
|
||||
// refs prefix
|
||||
private const string _refsPrefix = "refs/heads/";
|
||||
private const string _remoteRefsPrefix = "refs/remotes/origin/";
|
||||
private const string _pullRefsPrefix = "refs/pull/";
|
||||
private const string _remotePullRefsPrefix = "refs/remotes/pull/";
|
||||
private const string _tagRefsPrefix = "refs/tags/";
|
||||
|
||||
// min git version that support add extra auth header.
|
||||
private Version _minGitVersionSupportAuthHeader = new Version(2, 9);
|
||||
|
||||
#if OS_WINDOWS
|
||||
// min git version that support override sslBackend setting.
|
||||
private Version _minGitVersionSupportSSLBackendOverride = new Version(2, 14, 2);
|
||||
#endif
|
||||
|
||||
// min git-lfs version that support add extra auth header.
|
||||
private Version _minGitLfsVersionSupportAuthHeader = new Version(2, 1);
|
||||
|
||||
public static string ProblemMatcher => @"
|
||||
{
|
||||
""problemMatcher"": [
|
||||
{
|
||||
""owner"": ""checkout-git"",
|
||||
""pattern"": [
|
||||
{
|
||||
""regexp"": ""^(fatal|error): (.*)$"",
|
||||
""message"": 2
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}";
|
||||
|
||||
public async Task GetSourceAsync(
|
||||
RunnerActionPluginExecutionContext executionContext,
|
||||
string repositoryPath,
|
||||
string repoFullName,
|
||||
string sourceBranch,
|
||||
string sourceVersion,
|
||||
bool clean,
|
||||
string submoduleInput,
|
||||
int fetchDepth,
|
||||
bool gitLfsSupport,
|
||||
string accessToken,
|
||||
CancellationToken cancellationToken)
|
||||
{
|
||||
// Validate args.
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
Dictionary<string, string> configModifications = new Dictionary<string, string>();
|
||||
Uri proxyUrlWithCred = null;
|
||||
string proxyUrlWithCredString = null;
|
||||
bool useSelfSignedCACert = false;
|
||||
bool useClientCert = false;
|
||||
string clientCertPrivateKeyAskPassFile = null;
|
||||
bool acceptUntrustedCerts = false;
|
||||
|
||||
executionContext.Output($"Syncing repository: {repoFullName}");
|
||||
Uri repositoryUrl = new Uri($"https://github.com/{repoFullName}");
|
||||
if (!repositoryUrl.IsAbsoluteUri)
|
||||
{
|
||||
throw new InvalidOperationException("Repository url need to be an absolute uri.");
|
||||
}
|
||||
|
||||
string targetPath = repositoryPath;
|
||||
|
||||
// input Submodules can be ['', true, false, recursive]
|
||||
// '' or false indicate don't checkout submodules
|
||||
// true indicate checkout top level submodules
|
||||
// recursive indicate checkout submodules recursively
|
||||
bool checkoutSubmodules = false;
|
||||
bool checkoutNestedSubmodules = false;
|
||||
if (!string.IsNullOrEmpty(submoduleInput))
|
||||
{
|
||||
if (string.Equals(submoduleInput, Pipelines.PipelineConstants.CheckoutTaskInputs.SubmodulesOptions.Recursive, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
checkoutSubmodules = true;
|
||||
checkoutNestedSubmodules = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
checkoutSubmodules = StringUtil.ConvertToBoolean(submoduleInput);
|
||||
}
|
||||
}
|
||||
|
||||
var runnerCert = executionContext.GetCertConfiguration();
|
||||
acceptUntrustedCerts = runnerCert?.SkipServerCertificateValidation ?? false;
|
||||
|
||||
executionContext.Debug($"repository url={repositoryUrl}");
|
||||
executionContext.Debug($"targetPath={targetPath}");
|
||||
executionContext.Debug($"sourceBranch={sourceBranch}");
|
||||
executionContext.Debug($"sourceVersion={sourceVersion}");
|
||||
executionContext.Debug($"clean={clean}");
|
||||
executionContext.Debug($"checkoutSubmodules={checkoutSubmodules}");
|
||||
executionContext.Debug($"checkoutNestedSubmodules={checkoutNestedSubmodules}");
|
||||
executionContext.Debug($"fetchDepth={fetchDepth}");
|
||||
executionContext.Debug($"gitLfsSupport={gitLfsSupport}");
|
||||
executionContext.Debug($"acceptUntrustedCerts={acceptUntrustedCerts}");
|
||||
|
||||
#if OS_WINDOWS
|
||||
bool schannelSslBackend = StringUtil.ConvertToBoolean(executionContext.GetRunnerContext("gituseschannel"));
|
||||
executionContext.Debug($"schannelSslBackend={schannelSslBackend}");
|
||||
#endif
|
||||
|
||||
// Initialize git command manager with additional environment variables.
|
||||
Dictionary<string, string> gitEnv = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
// Disable git prompt
|
||||
gitEnv["GIT_TERMINAL_PROMPT"] = "0";
|
||||
|
||||
// Disable prompting for git credential manager
|
||||
gitEnv["GCM_INTERACTIVE"] = "Never";
|
||||
|
||||
// Git-lfs will try to pull down asset if any of the local/user/system setting exist.
|
||||
// If customer didn't enable `LFS` in their pipeline definition, we will use ENV to disable LFS fetch/checkout.
|
||||
if (!gitLfsSupport)
|
||||
{
|
||||
gitEnv["GIT_LFS_SKIP_SMUDGE"] = "1";
|
||||
}
|
||||
|
||||
// Add the public variables.
|
||||
foreach (var variable in executionContext.Variables)
|
||||
{
|
||||
// Add the variable using the formatted name.
|
||||
string formattedKey = (variable.Key ?? string.Empty).Replace('.', '_').Replace(' ', '_').ToUpperInvariant();
|
||||
gitEnv[formattedKey] = variable.Value?.Value ?? string.Empty;
|
||||
}
|
||||
|
||||
GitCliManager gitCommandManager = new GitCliManager(gitEnv);
|
||||
await gitCommandManager.LoadGitExecutionInfo(executionContext);
|
||||
|
||||
// Make sure the build machine met all requirements for the git repository
|
||||
// For now, the requirement we have are:
|
||||
// 1. git version greater than 2.9 since we need to use auth header.
|
||||
// 2. git-lfs version greater than 2.1 since we need to use auth header.
|
||||
// 3. git version greater than 2.14.2 if use SChannel for SSL backend (Windows only)
|
||||
RequirementCheck(executionContext, gitCommandManager, gitLfsSupport);
|
||||
|
||||
// prepare credentail embedded urls
|
||||
var runnerProxy = executionContext.GetProxyConfiguration();
|
||||
if (runnerProxy != null && !string.IsNullOrEmpty(runnerProxy.ProxyAddress) && !runnerProxy.WebProxy.IsBypassed(repositoryUrl))
|
||||
{
|
||||
proxyUrlWithCred = UrlUtil.GetCredentialEmbeddedUrl(new Uri(runnerProxy.ProxyAddress), runnerProxy.ProxyUsername, runnerProxy.ProxyPassword);
|
||||
|
||||
// uri.absoluteuri will not contains port info if the scheme is http/https and the port is 80/443
|
||||
// however, git.exe always require you provide port info, if nothing passed in, it will use 1080 as default
|
||||
// as result, we need prefer the uri.originalstring over uri.absoluteuri.
|
||||
proxyUrlWithCredString = proxyUrlWithCred.OriginalString;
|
||||
}
|
||||
|
||||
// prepare askpass for client cert private key, if the repository's endpoint url match the runner config url
|
||||
var systemConnection = executionContext.Endpoints.Single(x => string.Equals(x.Name, WellKnownServiceEndpointNames.SystemVssConnection, StringComparison.OrdinalIgnoreCase));
|
||||
if (runnerCert != null && Uri.Compare(repositoryUrl, systemConnection.Url, UriComponents.SchemeAndServer, UriFormat.Unescaped, StringComparison.OrdinalIgnoreCase) == 0)
|
||||
{
|
||||
if (!string.IsNullOrEmpty(runnerCert.CACertificateFile))
|
||||
{
|
||||
useSelfSignedCACert = true;
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificateFile) &&
|
||||
!string.IsNullOrEmpty(runnerCert.ClientCertificatePrivateKeyFile))
|
||||
{
|
||||
useClientCert = true;
|
||||
|
||||
// prepare askpass for client cert password
|
||||
if (!string.IsNullOrEmpty(runnerCert.ClientCertificatePassword))
|
||||
{
|
||||
clientCertPrivateKeyAskPassFile = Path.Combine(executionContext.GetRunnerContext("temp"), $"{Guid.NewGuid()}.sh");
|
||||
List<string> askPass = new List<string>();
|
||||
askPass.Add("#!/bin/sh");
|
||||
askPass.Add($"echo \"{runnerCert.ClientCertificatePassword}\"");
|
||||
File.WriteAllLines(clientCertPrivateKeyAskPassFile, askPass);
|
||||
|
||||
#if !OS_WINDOWS
|
||||
string toolPath = WhichUtil.Which("chmod", true);
|
||||
string argLine = $"775 {clientCertPrivateKeyAskPassFile}";
|
||||
executionContext.Command($"chmod {argLine}");
|
||||
|
||||
var processInvoker = new ProcessInvoker(executionContext);
|
||||
processInvoker.OutputDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
executionContext.Output(args.Data);
|
||||
}
|
||||
};
|
||||
processInvoker.ErrorDataReceived += (object sender, ProcessDataReceivedEventArgs args) =>
|
||||
{
|
||||
if (!string.IsNullOrEmpty(args.Data))
|
||||
{
|
||||
executionContext.Output(args.Data);
|
||||
}
|
||||
};
|
||||
|
||||
string workingDirectory = executionContext.GetRunnerContext("workspace");
|
||||
await processInvoker.ExecuteAsync(workingDirectory, toolPath, argLine, null, true, CancellationToken.None);
|
||||
#endif
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check the current contents of the root folder to see if there is already a repo
|
||||
// If there is a repo, see if it matches the one we are expecting to be there based on the remote fetch url
|
||||
// if the repo is not what we expect, remove the folder
|
||||
if (!await IsRepositoryOriginUrlMatch(executionContext, gitCommandManager, targetPath, repositoryUrl))
|
||||
{
|
||||
// Delete source folder
|
||||
IOUtil.DeleteDirectory(targetPath, cancellationToken);
|
||||
}
|
||||
else
|
||||
{
|
||||
// delete the index.lock file left by previous canceled build or any operation cause git.exe crash last time.
|
||||
string lockFile = Path.Combine(targetPath, ".git\\index.lock");
|
||||
if (File.Exists(lockFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(lockFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
executionContext.Debug($"Unable to delete the index.lock file: {lockFile}");
|
||||
executionContext.Debug(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// delete the shallow.lock file left by previous canceled build or any operation cause git.exe crash last time.
|
||||
string shallowLockFile = Path.Combine(targetPath, ".git\\shallow.lock");
|
||||
if (File.Exists(shallowLockFile))
|
||||
{
|
||||
try
|
||||
{
|
||||
File.Delete(shallowLockFile);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
executionContext.Debug($"Unable to delete the shallow.lock file: {shallowLockFile}");
|
||||
executionContext.Debug(ex.ToString());
|
||||
}
|
||||
}
|
||||
|
||||
// When repo.clean is selected for a git repo, execute git clean -ffdx and git reset --hard HEAD on the current repo.
|
||||
// This will help us save the time to reclone the entire repo.
|
||||
// If any git commands exit with non-zero return code or any exception happened during git.exe invoke, fall back to delete the repo folder.
|
||||
if (clean)
|
||||
{
|
||||
Boolean softCleanSucceed = true;
|
||||
|
||||
// git clean -ffdx
|
||||
int exitCode_clean = await gitCommandManager.GitClean(executionContext, targetPath);
|
||||
if (exitCode_clean != 0)
|
||||
{
|
||||
executionContext.Debug($"'git clean -ffdx' failed with exit code {exitCode_clean}, this normally caused by:\n 1) Path too long\n 2) Permission issue\n 3) File in use\nFor futher investigation, manually run 'git clean -ffdx' on repo root: {targetPath} after each build.");
|
||||
softCleanSucceed = false;
|
||||
}
|
||||
|
||||
// git reset --hard HEAD
|
||||
if (softCleanSucceed)
|
||||
{
|
||||
int exitCode_reset = await gitCommandManager.GitReset(executionContext, targetPath);
|
||||
if (exitCode_reset != 0)
|
||||
{
|
||||
executionContext.Debug($"'git reset --hard HEAD' failed with exit code {exitCode_reset}\nFor futher investigation, manually run 'git reset --hard HEAD' on repo root: {targetPath} after each build.");
|
||||
softCleanSucceed = false;
|
||||
}
|
||||
}
|
||||
|
||||
// git clean -ffdx and git reset --hard HEAD for each submodule
|
||||
if (checkoutSubmodules)
|
||||
{
|
||||
if (softCleanSucceed)
|
||||
{
|
||||
int exitCode_submoduleclean = await gitCommandManager.GitSubmoduleClean(executionContext, targetPath);
|
||||
if (exitCode_submoduleclean != 0)
|
||||
{
|
||||
executionContext.Debug($"'git submodule foreach git clean -ffdx' failed with exit code {exitCode_submoduleclean}\nFor futher investigation, manually run 'git submodule foreach git clean -ffdx' on repo root: {targetPath} after each build.");
|
||||
softCleanSucceed = false;
|
||||
}
|
||||
}
|
||||
|
||||
if (softCleanSucceed)
|
||||
{
|
||||
int exitCode_submodulereset = await gitCommandManager.GitSubmoduleReset(executionContext, targetPath);
|
||||
if (exitCode_submodulereset != 0)
|
||||
{
|
||||
executionContext.Debug($"'git submodule foreach git reset --hard HEAD' failed with exit code {exitCode_submodulereset}\nFor futher investigation, manually run 'git submodule foreach git reset --hard HEAD' on repo root: {targetPath} after each build.");
|
||||
softCleanSucceed = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!softCleanSucceed)
|
||||
{
|
||||
//fall back
|
||||
executionContext.Warning("Unable to run \"git clean -ffdx\" and \"git reset --hard HEAD\" successfully, delete source folder instead.");
|
||||
IOUtil.DeleteDirectory(targetPath, cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// if the folder is missing, create it
|
||||
if (!Directory.Exists(targetPath))
|
||||
{
|
||||
Directory.CreateDirectory(targetPath);
|
||||
}
|
||||
|
||||
// if the folder contains a .git folder, it means the folder contains a git repo that matches the remote url and in a clean state.
|
||||
// we will run git fetch to update the repo.
|
||||
if (!Directory.Exists(Path.Combine(targetPath, ".git")))
|
||||
{
|
||||
// init git repository
|
||||
int exitCode_init = await gitCommandManager.GitInit(executionContext, targetPath);
|
||||
if (exitCode_init != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to use git.exe init repository under {targetPath}, 'git init' failed with exit code: {exitCode_init}");
|
||||
}
|
||||
|
||||
int exitCode_addremote = await gitCommandManager.GitRemoteAdd(executionContext, targetPath, "origin", repositoryUrl.AbsoluteUri);
|
||||
if (exitCode_addremote != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Unable to use git.exe add remote 'origin', 'git remote add' failed with exit code: {exitCode_addremote}");
|
||||
}
|
||||
}
|
||||
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
// disable git auto gc
|
||||
int exitCode_disableGC = await gitCommandManager.GitDisableAutoGC(executionContext, targetPath);
|
||||
if (exitCode_disableGC != 0)
|
||||
{
|
||||
executionContext.Warning("Unable turn off git auto garbage collection, git fetch operation may trigger auto garbage collection which will affect the performance of fetching.");
|
||||
}
|
||||
|
||||
// always remove any possible left extraheader setting from git config.
|
||||
if (await gitCommandManager.GitConfigExist(executionContext, targetPath, $"http.{repositoryUrl.AbsoluteUri}.extraheader"))
|
||||
{
|
||||
executionContext.Debug("Remove any extraheader setting from git config.");
|
||||
await RemoveGitConfig(executionContext, gitCommandManager, targetPath, $"http.{repositoryUrl.AbsoluteUri}.extraheader", string.Empty);
|
||||
}
|
||||
|
||||
// always remove any possible left proxy setting from git config, the proxy setting may contains credential
|
||||
if (await gitCommandManager.GitConfigExist(executionContext, targetPath, $"http.proxy"))
|
||||
{
|
||||
executionContext.Debug("Remove any proxy setting from git config.");
|
||||
await RemoveGitConfig(executionContext, gitCommandManager, targetPath, $"http.proxy", string.Empty);
|
||||
}
|
||||
|
||||
List<string> additionalFetchArgs = new List<string>();
|
||||
List<string> additionalLfsFetchArgs = new List<string>();
|
||||
|
||||
// Add http.https://github.com.extraheader=... to gitconfig
|
||||
// accessToken as basic auth header to handle any auth challenge from github.com
|
||||
string configKey = $"http.https://github.com/.extraheader";
|
||||
string configValue = $"\"AUTHORIZATION: {GenerateBasicAuthHeader(executionContext, accessToken)}\"";
|
||||
configModifications[configKey] = configValue.Trim('\"');
|
||||
int exitCode_config = await gitCommandManager.GitConfig(executionContext, targetPath, configKey, configValue);
|
||||
if (exitCode_config != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Git config failed with exit code: {exitCode_config}");
|
||||
}
|
||||
|
||||
// Prepare proxy config for fetch.
|
||||
if (runnerProxy != null && !string.IsNullOrEmpty(runnerProxy.ProxyAddress) && !runnerProxy.WebProxy.IsBypassed(repositoryUrl))
|
||||
{
|
||||
executionContext.Debug($"Config proxy server '{runnerProxy.ProxyAddress}' for git fetch.");
|
||||
ArgUtil.NotNullOrEmpty(proxyUrlWithCredString, nameof(proxyUrlWithCredString));
|
||||
additionalFetchArgs.Add($"-c http.proxy=\"{proxyUrlWithCredString}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.proxy=\"{proxyUrlWithCredString}\"");
|
||||
}
|
||||
|
||||
// Prepare ignore ssl cert error config for fetch.
|
||||
if (acceptUntrustedCerts)
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslVerify=false");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslVerify=false");
|
||||
}
|
||||
|
||||
// Prepare self-signed CA cert config for fetch from server.
|
||||
if (useSelfSignedCACert)
|
||||
{
|
||||
executionContext.Debug($"Use self-signed certificate '{runnerCert.CACertificateFile}' for git fetch.");
|
||||
additionalFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
}
|
||||
|
||||
// Prepare client cert config for fetch from server.
|
||||
if (useClientCert)
|
||||
{
|
||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git fetch.");
|
||||
|
||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
additionalFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
additionalLfsFetchArgs.Add($"-c http.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
}
|
||||
}
|
||||
|
||||
#if OS_WINDOWS
|
||||
if (schannelSslBackend)
|
||||
{
|
||||
executionContext.Debug("Use SChannel SslBackend for git fetch.");
|
||||
additionalFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
additionalLfsFetchArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
}
|
||||
#endif
|
||||
// Prepare gitlfs url for fetch and checkout
|
||||
if (gitLfsSupport)
|
||||
{
|
||||
// Initialize git lfs by execute 'git lfs install'
|
||||
executionContext.Debug("Setup the local Git hooks for Git LFS.");
|
||||
int exitCode_lfsInstall = await gitCommandManager.GitLFSInstall(executionContext, targetPath);
|
||||
if (exitCode_lfsInstall != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Git-lfs installation failed with exit code: {exitCode_lfsInstall}");
|
||||
}
|
||||
}
|
||||
|
||||
List<string> additionalFetchSpecs = new List<string>();
|
||||
additionalFetchSpecs.Add("+refs/heads/*:refs/remotes/origin/*");
|
||||
|
||||
if (IsPullRequest(sourceBranch))
|
||||
{
|
||||
additionalFetchSpecs.Add($"+{sourceBranch}:{GetRemoteRefName(sourceBranch)}");
|
||||
}
|
||||
|
||||
int exitCode_fetch = await gitCommandManager.GitFetch(executionContext, targetPath, "origin", fetchDepth, additionalFetchSpecs, string.Join(" ", additionalFetchArgs), cancellationToken);
|
||||
if (exitCode_fetch != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Git fetch failed with exit code: {exitCode_fetch}");
|
||||
}
|
||||
|
||||
// Checkout
|
||||
// sourceToBuild is used for checkout
|
||||
// if sourceBranch is a PR branch or sourceVersion is null, make sure branch name is a remote branch. we need checkout to detached head.
|
||||
// (change refs/heads to refs/remotes/origin, refs/pull to refs/remotes/pull, or leave it as it when the branch name doesn't contain refs/...)
|
||||
// if sourceVersion provide, just use that for checkout, since when you checkout a commit, it will end up in detached head.
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
string sourcesToBuild;
|
||||
if (IsPullRequest(sourceBranch) || string.IsNullOrEmpty(sourceVersion))
|
||||
{
|
||||
sourcesToBuild = GetRemoteRefName(sourceBranch);
|
||||
}
|
||||
else
|
||||
{
|
||||
sourcesToBuild = sourceVersion;
|
||||
}
|
||||
|
||||
// fetch lfs object upfront, this will avoid fetch lfs object during checkout which cause checkout taking forever
|
||||
// since checkout will fetch lfs object 1 at a time, while git lfs fetch will fetch lfs object in parallel.
|
||||
if (gitLfsSupport)
|
||||
{
|
||||
int exitCode_lfsFetch = await gitCommandManager.GitLFSFetch(executionContext, targetPath, "origin", sourcesToBuild, string.Join(" ", additionalLfsFetchArgs), cancellationToken);
|
||||
if (exitCode_lfsFetch != 0)
|
||||
{
|
||||
// local repository is shallow repository, lfs fetch may fail due to lack of commits history.
|
||||
// this will happen when the checkout commit is older than tip -> fetchDepth
|
||||
if (fetchDepth > 0)
|
||||
{
|
||||
executionContext.Warning($"Git lfs fetch failed on shallow repository, this might because of git fetch with depth '{fetchDepth}' doesn't include the lfs fetch commit '{sourcesToBuild}'.");
|
||||
}
|
||||
|
||||
// git lfs fetch failed, get lfs log, the log is critical for debug.
|
||||
int exitCode_lfsLogs = await gitCommandManager.GitLFSLogs(executionContext, targetPath);
|
||||
throw new InvalidOperationException($"Git lfs fetch failed with exit code: {exitCode_lfsFetch}. Git lfs logs returned with exit code: {exitCode_lfsLogs}.");
|
||||
}
|
||||
}
|
||||
|
||||
// Finally, checkout the sourcesToBuild (if we didn't find a valid git object this will throw)
|
||||
int exitCode_checkout = await gitCommandManager.GitCheckout(executionContext, targetPath, sourcesToBuild, cancellationToken);
|
||||
if (exitCode_checkout != 0)
|
||||
{
|
||||
// local repository is shallow repository, checkout may fail due to lack of commits history.
|
||||
// this will happen when the checkout commit is older than tip -> fetchDepth
|
||||
if (fetchDepth > 0)
|
||||
{
|
||||
executionContext.Warning($"Git checkout failed on shallow repository, this might because of git fetch with depth '{fetchDepth}' doesn't include the checkout commit '{sourcesToBuild}'.");
|
||||
}
|
||||
|
||||
throw new InvalidOperationException($"Git checkout failed with exit code: {exitCode_checkout}");
|
||||
}
|
||||
|
||||
// Submodule update
|
||||
if (checkoutSubmodules)
|
||||
{
|
||||
cancellationToken.ThrowIfCancellationRequested();
|
||||
|
||||
int exitCode_submoduleSync = await gitCommandManager.GitSubmoduleSync(executionContext, targetPath, checkoutNestedSubmodules, cancellationToken);
|
||||
if (exitCode_submoduleSync != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Git submodule sync failed with exit code: {exitCode_submoduleSync}");
|
||||
}
|
||||
|
||||
List<string> additionalSubmoduleUpdateArgs = new List<string>();
|
||||
|
||||
// Prepare proxy config for submodule update.
|
||||
if (runnerProxy != null && !string.IsNullOrEmpty(runnerProxy.ProxyAddress) && !runnerProxy.WebProxy.IsBypassed(repositoryUrl))
|
||||
{
|
||||
executionContext.Debug($"Config proxy server '{runnerProxy.ProxyAddress}' for git submodule update.");
|
||||
ArgUtil.NotNullOrEmpty(proxyUrlWithCredString, nameof(proxyUrlWithCredString));
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.proxy=\"{proxyUrlWithCredString}\"");
|
||||
}
|
||||
|
||||
// Prepare ignore ssl cert error config for fetch.
|
||||
if (acceptUntrustedCerts)
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.sslVerify=false");
|
||||
}
|
||||
|
||||
// Prepare self-signed CA cert config for submodule update.
|
||||
if (useSelfSignedCACert)
|
||||
{
|
||||
executionContext.Debug($"Use self-signed CA certificate '{runnerCert.CACertificateFile}' for git submodule update.");
|
||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcainfo=\"{runnerCert.CACertificateFile}\"");
|
||||
}
|
||||
|
||||
// Prepare client cert config for submodule update.
|
||||
if (useClientCert)
|
||||
{
|
||||
executionContext.Debug($"Use client certificate '{runnerCert.ClientCertificateFile}' for git submodule update.");
|
||||
string authorityUrl = repositoryUrl.AbsoluteUri.Replace(repositoryUrl.PathAndQuery, string.Empty);
|
||||
|
||||
if (!string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\" -c http.{authorityUrl}.sslCertPasswordProtected=true -c core.askpass=\"{clientCertPrivateKeyAskPassFile}\"");
|
||||
}
|
||||
else
|
||||
{
|
||||
additionalSubmoduleUpdateArgs.Add($"-c http.{authorityUrl}.sslcert=\"{runnerCert.ClientCertificateFile}\" -c http.{authorityUrl}.sslkey=\"{runnerCert.ClientCertificatePrivateKeyFile}\"");
|
||||
}
|
||||
}
|
||||
#if OS_WINDOWS
|
||||
if (schannelSslBackend)
|
||||
{
|
||||
executionContext.Debug("Use SChannel SslBackend for git submodule update.");
|
||||
additionalSubmoduleUpdateArgs.Add("-c http.sslbackend=\"schannel\"");
|
||||
}
|
||||
#endif
|
||||
|
||||
int exitCode_submoduleUpdate = await gitCommandManager.GitSubmoduleUpdate(executionContext, targetPath, fetchDepth, string.Join(" ", additionalSubmoduleUpdateArgs), checkoutNestedSubmodules, cancellationToken);
|
||||
if (exitCode_submoduleUpdate != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"Git submodule update failed with exit code: {exitCode_submoduleUpdate}");
|
||||
}
|
||||
}
|
||||
|
||||
if (useClientCert && !string.IsNullOrEmpty(clientCertPrivateKeyAskPassFile))
|
||||
{
|
||||
executionContext.Debug("Remove git.sslkey askpass file.");
|
||||
IOUtil.DeleteFile(clientCertPrivateKeyAskPassFile);
|
||||
}
|
||||
|
||||
// Set intra-task variable for post job cleanup
|
||||
executionContext.SetIntraActionState("repositoryPath", targetPath);
|
||||
executionContext.SetIntraActionState("modifiedgitconfig", JsonUtility.ToString(configModifications.Keys));
|
||||
foreach (var config in configModifications)
|
||||
{
|
||||
executionContext.SetIntraActionState(config.Key, config.Value);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task CleanupAsync(RunnerActionPluginExecutionContext executionContext)
|
||||
{
|
||||
ArgUtil.NotNull(executionContext, nameof(executionContext));
|
||||
var repositoryPath = Environment.GetEnvironmentVariable("STATE_repositoryPath");
|
||||
ArgUtil.NotNullOrEmpty(repositoryPath, nameof(repositoryPath));
|
||||
executionContext.Output($"Cleanup cached git credential from {repositoryPath}.");
|
||||
|
||||
// Initialize git command manager
|
||||
GitCliManager gitCommandManager = new GitCliManager();
|
||||
await gitCommandManager.LoadGitExecutionInfo(executionContext);
|
||||
|
||||
executionContext.Debug("Remove any extraheader and proxy setting from git config.");
|
||||
var configKeys = JsonUtility.FromString<List<string>>(Environment.GetEnvironmentVariable("STATE_modifiedgitconfig"));
|
||||
if (configKeys?.Count > 0)
|
||||
{
|
||||
foreach (var config in configKeys)
|
||||
{
|
||||
var configValue = Environment.GetEnvironmentVariable($"STATE_{config}");
|
||||
if (!string.IsNullOrEmpty(configValue))
|
||||
{
|
||||
await RemoveGitConfig(executionContext, gitCommandManager, repositoryPath, config, configValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void RequirementCheck(RunnerActionPluginExecutionContext executionContext, GitCliManager gitCommandManager, bool checkGitLfs)
|
||||
{
|
||||
// v2.9 git exist use auth header.
|
||||
gitCommandManager.EnsureGitVersion(_minGitVersionSupportAuthHeader, throwOnNotMatch: true);
|
||||
|
||||
#if OS_WINDOWS
|
||||
// check git version for SChannel SSLBackend (Windows Only)
|
||||
bool schannelSslBackend = StringUtil.ConvertToBoolean(executionContext.GetRunnerContext("gituseschannel"));
|
||||
if (schannelSslBackend)
|
||||
{
|
||||
gitCommandManager.EnsureGitVersion(_minGitVersionSupportSSLBackendOverride, throwOnNotMatch: true);
|
||||
}
|
||||
#endif
|
||||
if (checkGitLfs)
|
||||
{
|
||||
// v2.1 git-lfs exist use auth header.
|
||||
gitCommandManager.EnsureGitLFSVersion(_minGitLfsVersionSupportAuthHeader, throwOnNotMatch: true);
|
||||
}
|
||||
}
|
||||
|
||||
private string GenerateBasicAuthHeader(RunnerActionPluginExecutionContext executionContext, string accessToken)
|
||||
{
|
||||
// use basic auth header with username:password in base64encoding.
|
||||
string authHeader = $"x-access-token:{accessToken}";
|
||||
string base64encodedAuthHeader = Convert.ToBase64String(Encoding.UTF8.GetBytes(authHeader));
|
||||
|
||||
// add base64 encoding auth header into secretMasker.
|
||||
executionContext.AddMask(base64encodedAuthHeader);
|
||||
return $"basic {base64encodedAuthHeader}";
|
||||
}
|
||||
|
||||
private async Task<bool> IsRepositoryOriginUrlMatch(RunnerActionPluginExecutionContext context, GitCliManager gitCommandManager, string repositoryPath, Uri expectedRepositoryOriginUrl)
|
||||
{
|
||||
context.Debug($"Checking if the repo on {repositoryPath} matches the expected repository origin URL. expected Url: {expectedRepositoryOriginUrl.AbsoluteUri}");
|
||||
if (!Directory.Exists(Path.Combine(repositoryPath, ".git")))
|
||||
{
|
||||
// There is no repo directory
|
||||
context.Debug($"Repository is not found since '.git' directory does not exist under. {repositoryPath}");
|
||||
return false;
|
||||
}
|
||||
|
||||
Uri remoteUrl;
|
||||
remoteUrl = await gitCommandManager.GitGetFetchUrl(context, repositoryPath);
|
||||
|
||||
if (remoteUrl == null)
|
||||
{
|
||||
// origin fetch url not found.
|
||||
context.Debug("Repository remote origin fetch url is empty.");
|
||||
return false;
|
||||
}
|
||||
|
||||
context.Debug($"Repository remote origin fetch url is {remoteUrl}");
|
||||
// compare the url passed in with the remote url found
|
||||
if (expectedRepositoryOriginUrl.Equals(remoteUrl))
|
||||
{
|
||||
context.Debug("URLs match.");
|
||||
return true;
|
||||
}
|
||||
else
|
||||
{
|
||||
context.Debug($"The remote.origin.url of the repository under root folder '{repositoryPath}' doesn't matches source repository url.");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task RemoveGitConfig(RunnerActionPluginExecutionContext executionContext, GitCliManager gitCommandManager, string targetPath, string configKey, string configValue)
|
||||
{
|
||||
int exitCode_configUnset = await gitCommandManager.GitConfigUnset(executionContext, targetPath, configKey);
|
||||
if (exitCode_configUnset != 0)
|
||||
{
|
||||
// if unable to use git.exe unset http.extraheader, http.proxy or core.askpass, modify git config file on disk. make sure we don't left credential.
|
||||
if (!string.IsNullOrEmpty(configValue))
|
||||
{
|
||||
executionContext.Warning("An unsuccessful attempt was made using git command line to remove \"http.extraheader\" from the git config. Attempting to modify the git config file directly to remove the credential.");
|
||||
string gitConfig = Path.Combine(targetPath, ".git/config");
|
||||
if (File.Exists(gitConfig))
|
||||
{
|
||||
List<string> safeGitConfig = new List<string>();
|
||||
var gitConfigContents = File.ReadAllLines(gitConfig);
|
||||
foreach (var line in gitConfigContents)
|
||||
{
|
||||
if (!line.Contains(configValue))
|
||||
{
|
||||
safeGitConfig.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
File.WriteAllLines(gitConfig, safeGitConfig);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
executionContext.Warning($"Unable to remove \"{configKey}\" from the git config. To remove the credential, execute \"git config --unset - all {configKey}\" from the repository root \"{targetPath}\".");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private bool IsPullRequest(string sourceBranch)
|
||||
{
|
||||
return !string.IsNullOrEmpty(sourceBranch) &&
|
||||
(sourceBranch.StartsWith(_pullRefsPrefix, StringComparison.OrdinalIgnoreCase) ||
|
||||
sourceBranch.StartsWith(_remotePullRefsPrefix, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
private string GetRemoteRefName(string refName)
|
||||
{
|
||||
if (string.IsNullOrEmpty(refName))
|
||||
{
|
||||
// If the refName is empty return the remote name for master
|
||||
refName = _remoteRefsPrefix + "master";
|
||||
}
|
||||
else if (refName.Equals("master", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// If the refName is master return the remote name for master
|
||||
refName = _remoteRefsPrefix + refName;
|
||||
}
|
||||
else if (refName.StartsWith(_refsPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// If the refName is refs/heads change it to the remote version of the name
|
||||
refName = _remoteRefsPrefix + refName.Substring(_refsPrefix.Length);
|
||||
}
|
||||
else if (refName.StartsWith(_pullRefsPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// If the refName is refs/pull change it to the remote version of the name
|
||||
refName = refName.Replace(_pullRefsPrefix, _remotePullRefsPrefix);
|
||||
}
|
||||
|
||||
return refName;
|
||||
}
|
||||
}
|
||||
}
|
||||
180
src/Runner.Plugins/Repository/v1.1/RepositoryPlugin.cs
Normal file
180
src/Runner.Plugins/Repository/v1.1/RepositoryPlugin.cs
Normal file
@@ -0,0 +1,180 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using GitHub.Runner.Sdk;
|
||||
using Pipelines = GitHub.DistributedTask.Pipelines;
|
||||
using System.IO;
|
||||
using GitHub.DistributedTask.Pipelines.ContextData;
|
||||
using System.Text.RegularExpressions;
|
||||
using GitHub.DistributedTask.Pipelines.Expressions;
|
||||
using System.Text;
|
||||
|
||||
namespace GitHub.Runner.Plugins.Repository.v1_1
|
||||
{
|
||||
public class CheckoutTask : IRunnerActionPlugin
|
||||
{
|
||||
public async Task RunAsync(RunnerActionPluginExecutionContext executionContext, CancellationToken token)
|
||||
{
|
||||
string runnerWorkspace = executionContext.GetRunnerContext("workspace");
|
||||
ArgUtil.Directory(runnerWorkspace, nameof(runnerWorkspace));
|
||||
string tempDirectory = executionContext.GetRunnerContext("temp");
|
||||
ArgUtil.Directory(tempDirectory, nameof(tempDirectory));
|
||||
|
||||
var repoFullName = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Repository);
|
||||
if (string.IsNullOrEmpty(repoFullName))
|
||||
{
|
||||
repoFullName = executionContext.GetGitHubContext("repository");
|
||||
}
|
||||
|
||||
var repoFullNameSplit = repoFullName.Split("/", StringSplitOptions.RemoveEmptyEntries);
|
||||
if (repoFullNameSplit.Length != 2)
|
||||
{
|
||||
throw new ArgumentOutOfRangeException(repoFullName);
|
||||
}
|
||||
|
||||
string expectRepoPath;
|
||||
var path = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Path);
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
expectRepoPath = IOUtil.ResolvePath(runnerWorkspace, path);
|
||||
if (!expectRepoPath.StartsWith(runnerWorkspace.Replace(Path.AltDirectorySeparatorChar, Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar))
|
||||
{
|
||||
throw new ArgumentException($"Input path '{path}' should resolve to a directory under '{runnerWorkspace}', current resolved path '{expectRepoPath}'.");
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// When repository doesn't has path set, default to sources directory 1/repoName
|
||||
expectRepoPath = Path.Combine(runnerWorkspace, repoFullNameSplit[1]);
|
||||
}
|
||||
|
||||
var workspaceRepo = executionContext.GetGitHubContext("repository");
|
||||
// for self repository, we need to let the worker knows where it is after checkout.
|
||||
if (string.Equals(workspaceRepo, repoFullName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var workspaceRepoPath = executionContext.GetGitHubContext("workspace");
|
||||
|
||||
executionContext.Debug($"Repository requires to be placed at '{expectRepoPath}', current location is '{workspaceRepoPath}'");
|
||||
if (!string.Equals(workspaceRepoPath.Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), expectRepoPath.Trim(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar), IOUtil.FilePathStringComparison))
|
||||
{
|
||||
executionContext.Output($"Repository is current at '{workspaceRepoPath}', move to '{expectRepoPath}'.");
|
||||
var count = 1;
|
||||
var staging = Path.Combine(tempDirectory, $"_{count}");
|
||||
while (Directory.Exists(staging))
|
||||
{
|
||||
count++;
|
||||
staging = Path.Combine(tempDirectory, $"_{count}");
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
executionContext.Debug($"Move existing repository '{workspaceRepoPath}' to '{expectRepoPath}' via staging directory '{staging}'.");
|
||||
IOUtil.MoveDirectory(workspaceRepoPath, expectRepoPath, staging, CancellationToken.None);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
executionContext.Debug("Catch exception during repository move.");
|
||||
executionContext.Debug(ex.ToString());
|
||||
executionContext.Warning("Unable move and reuse existing repository to required location.");
|
||||
IOUtil.DeleteDirectory(expectRepoPath, CancellationToken.None);
|
||||
}
|
||||
|
||||
executionContext.Output($"Repository will locate at '{expectRepoPath}'.");
|
||||
}
|
||||
|
||||
executionContext.Debug($"Update workspace repository location.");
|
||||
executionContext.SetRepositoryPath(repoFullName, expectRepoPath, true);
|
||||
}
|
||||
|
||||
string sourceBranch;
|
||||
string sourceVersion;
|
||||
string refInput = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Ref);
|
||||
if (string.IsNullOrEmpty(refInput))
|
||||
{
|
||||
sourceBranch = executionContext.GetGitHubContext("ref");
|
||||
sourceVersion = executionContext.GetGitHubContext("sha");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBranch = refInput;
|
||||
sourceVersion = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Version); // version get removed when checkout move to repo in the graph
|
||||
if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.SHA1))
|
||||
{
|
||||
sourceVersion = sourceBranch;
|
||||
// If Ref is a SHA and the repo is self, we need to use github.ref as source branch since it might be refs/pull/*
|
||||
if (string.Equals(workspaceRepo, repoFullName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
sourceBranch = executionContext.GetGitHubContext("ref");
|
||||
}
|
||||
else
|
||||
{
|
||||
sourceBranch = "refs/heads/master";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool clean = StringUtil.ConvertToBoolean(executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Clean), true);
|
||||
string submoduleInput = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Submodules);
|
||||
|
||||
int fetchDepth = 0;
|
||||
if (!int.TryParse(executionContext.GetInput("fetch-depth"), out fetchDepth) || fetchDepth < 0)
|
||||
{
|
||||
fetchDepth = 0;
|
||||
}
|
||||
|
||||
bool gitLfsSupport = StringUtil.ConvertToBoolean(executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Lfs));
|
||||
string accessToken = executionContext.GetInput(Pipelines.PipelineConstants.CheckoutTaskInputs.Token);
|
||||
if (string.IsNullOrEmpty(accessToken))
|
||||
{
|
||||
accessToken = executionContext.GetGitHubContext("token");
|
||||
}
|
||||
|
||||
// register problem matcher
|
||||
string matcherFile = Path.Combine(tempDirectory, $"git_{Guid.NewGuid()}.json");
|
||||
File.WriteAllText(matcherFile, GitHubSourceProvider.ProblemMatcher, new UTF8Encoding(false));
|
||||
executionContext.Output($"##[add-matcher]{matcherFile}");
|
||||
try
|
||||
{
|
||||
await new GitHubSourceProvider().GetSourceAsync(executionContext,
|
||||
expectRepoPath,
|
||||
repoFullName,
|
||||
sourceBranch,
|
||||
sourceVersion,
|
||||
clean,
|
||||
submoduleInput,
|
||||
fetchDepth,
|
||||
gitLfsSupport,
|
||||
accessToken,
|
||||
token);
|
||||
}
|
||||
finally
|
||||
{
|
||||
executionContext.Output("##[remove-matcher owner=checkout-git]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class CleanupTask : IRunnerActionPlugin
|
||||
{
|
||||
public async Task RunAsync(RunnerActionPluginExecutionContext executionContext, CancellationToken token)
|
||||
{
|
||||
string tempDirectory = executionContext.GetRunnerContext("temp");
|
||||
ArgUtil.Directory(tempDirectory, nameof(tempDirectory));
|
||||
|
||||
// register problem matcher
|
||||
string matcherFile = Path.Combine(tempDirectory, $"git_{Guid.NewGuid()}.json");
|
||||
File.WriteAllText(matcherFile, GitHubSourceProvider.ProblemMatcher, new UTF8Encoding(false));
|
||||
executionContext.Output($"##[add-matcher]{matcherFile}");
|
||||
try
|
||||
{
|
||||
await new GitHubSourceProvider().CleanupAsync(executionContext);
|
||||
}
|
||||
finally
|
||||
{
|
||||
executionContext.Output("##[remove-matcher owner=checkout-git]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
60
src/Runner.Plugins/Runner.Plugins.csproj
Normal file
60
src/Runner.Plugins/Runner.Plugins.csproj
Normal file
@@ -0,0 +1,60 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netcoreapp2.2</TargetFramework>
|
||||
<OutputType>Library</OutputType>
|
||||
<RuntimeIdentifiers>win-x64;win-x86;linux-x64;linux-arm;rhel.6-x64;osx-x64</RuntimeIdentifiers>
|
||||
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
|
||||
<AssetTargetFallback>portable-net45+win8</AssetTargetFallback>
|
||||
<NoWarn>NU1701;NU1603</NoWarn>
|
||||
<Version>$(Version)</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Sdk\Sdk.csproj" />
|
||||
<ProjectReference Include="..\Runner.Sdk\Runner.Sdk.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup Condition=" '$(Configuration)' == 'Debug' ">
|
||||
<DebugType>portable</DebugType>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true' AND '$(PackageRuntime)' == 'win-x64'">
|
||||
<DefineConstants>OS_WINDOWS;X64;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true' AND '$(PackageRuntime)' == 'win-x86'">
|
||||
<DefineConstants>OS_WINDOWS;X86;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true' AND '$(Configuration)' == 'Debug' AND '$(PackageRuntime)' == 'win-x64'">
|
||||
<DefineConstants>OS_WINDOWS;X64;DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Windows)))' == 'true' AND '$(Configuration)' == 'Debug' AND '$(PackageRuntime)' == 'win-x86'">
|
||||
<DefineConstants>OS_WINDOWS;X86;DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true'">
|
||||
<DefineConstants>OS_OSX;X64;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::OSX)))' == 'true' AND '$(Configuration)' == 'Debug'">
|
||||
<DefineConstants>OS_OSX;DEBUG;X64;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(PackageRuntime)' == 'linux-x64'">
|
||||
<DefineConstants>OS_LINUX;X64;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(PackageRuntime)' == 'rhel.6-x64'">
|
||||
<DefineConstants>OS_LINUX;OS_RHEL6;X64;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(PackageRuntime)' == 'linux-arm'">
|
||||
<DefineConstants>OS_LINUX;ARM;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(Configuration)' == 'Debug' AND '$(PackageRuntime)' == 'linux-x64'">
|
||||
<DefineConstants>OS_LINUX;X64;DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(Configuration)' == 'Debug' AND '$(PackageRuntime)' == 'rhel.6-x64'">
|
||||
<DefineConstants>OS_LINUX;OS_RHEL6;X64;DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition="'$([System.Runtime.InteropServices.RuntimeInformation]::IsOSPlatform($([System.Runtime.InteropServices.OSPlatform]::Linux)))' == 'true' AND '$(Configuration)' == 'Debug' AND '$(PackageRuntime)' == 'linux-arm'">
|
||||
<DefineConstants>OS_LINUX;ARM;DEBUG;TRACE</DefineConstants>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
Reference in New Issue
Block a user