diff --git a/src/Runner.Plugins/Repository/v1.0/RepositoryPlugin.cs b/src/Runner.Plugins/Repository/v1.0/RepositoryPlugin.cs index fd78da849..6e91349bd 100644 --- a/src/Runner.Plugins/Repository/v1.0/RepositoryPlugin.cs +++ b/src/Runner.Plugins/Repository/v1.0/RepositoryPlugin.cs @@ -12,8 +12,6 @@ namespace GitHub.Runner.Plugins.Repository.v1_0 { public class CheckoutTask : IRunnerActionPlugin { - private readonly Regex _validSha1 = new(@"\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"); @@ -99,7 +97,7 @@ namespace GitHub.Runner.Plugins.Repository.v1_0 { 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)) + if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.CommitHash)) { sourceVersion = sourceBranch; diff --git a/src/Runner.Plugins/Repository/v1.1/RepositoryPlugin.cs b/src/Runner.Plugins/Repository/v1.1/RepositoryPlugin.cs index e314ecdc7..96fcde60d 100644 --- a/src/Runner.Plugins/Repository/v1.1/RepositoryPlugin.cs +++ b/src/Runner.Plugins/Repository/v1.1/RepositoryPlugin.cs @@ -96,7 +96,7 @@ namespace GitHub.Runner.Plugins.Repository.v1_1 { 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)) + if (string.IsNullOrEmpty(sourceVersion) && RegexUtility.IsMatch(sourceBranch, WellKnownRegularExpressions.CommitHash)) { 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/* diff --git a/src/Sdk/DTPipelines/Pipelines/Expressions/WellKnownRegularExpressions.cs b/src/Sdk/DTPipelines/Pipelines/Expressions/WellKnownRegularExpressions.cs index 72899fb42..d4d3c75a6 100644 --- a/src/Sdk/DTPipelines/Pipelines/Expressions/WellKnownRegularExpressions.cs +++ b/src/Sdk/DTPipelines/Pipelines/Expressions/WellKnownRegularExpressions.cs @@ -8,6 +8,7 @@ namespace GitHub.DistributedTask.Pipelines.Expressions public const String Email = nameof(Email); public const String IPv4Address = nameof(IPv4Address); public const String SHA1 = nameof(SHA1); + public const String CommitHash = nameof(CommitHash); public const String Url = nameof(Url); /// @@ -24,7 +25,8 @@ namespace GitHub.DistributedTask.Pipelines.Expressions case IPv4Address: return s_validIPv4Address; case SHA1: - return s_validSha1; + case CommitHash: + return s_validCommitHash; case Url: return s_validUrl; default: @@ -46,9 +48,9 @@ namespace GitHub.DistributedTask.Pipelines.Expressions ) ); - // 40 hex characters - private static readonly Lazy s_validSha1 = new Lazy(() => new Regex( - @"\b[0-9a-f]{40}\b", + // 40 or 64 hex characters (SHA-1 or SHA-256 commit hash) + private static readonly Lazy s_validCommitHash = new Lazy(() => new Regex( + @"\b(?:[0-9a-f]{40}|[0-9a-f]{64})\b", RegexOptions.IgnoreCase | RegexOptions.CultureInvariant | RegexOptions.Compiled, RegexUtility.GetRegexTimeOut() ) ); diff --git a/src/Test/L0/ConstantGenerationL0.cs b/src/Test/L0/ConstantGenerationL0.cs index f3c1b8f9e..4ff2dbb30 100644 --- a/src/Test/L0/ConstantGenerationL0.cs +++ b/src/Test/L0/ConstantGenerationL0.cs @@ -24,7 +24,10 @@ namespace GitHub.Runner.Common.Tests "osx-arm64" }; - Assert.Equal(40, BuildConstants.Source.CommitHash.Length); + Assert.True( + BuildConstants.Source.CommitHash.Length == 40 || BuildConstants.Source.CommitHash.Length == 64, + "CommitHash should be a 40-char SHA-1 or 64-char SHA-256 hex string"); + Assert.Matches("^[0-9a-f]+$", BuildConstants.Source.CommitHash); Assert.True(validPackageNames.Contains(BuildConstants.RunnerPackage.PackageName), $"PackageName should be one of the following '{string.Join(", ", validPackageNames)}', current PackageName is '{BuildConstants.RunnerPackage.PackageName}'"); } } diff --git a/src/Test/L0/Listener/RunnerL0.cs b/src/Test/L0/Listener/RunnerL0.cs index c88bc8274..b9aa4c880 100644 --- a/src/Test/L0/Listener/RunnerL0.cs +++ b/src/Test/L0/Listener/RunnerL0.cs @@ -14,7 +14,7 @@ using Pipelines = GitHub.DistributedTask.Pipelines; namespace GitHub.Runner.Common.Tests.Listener { - public sealed class RunnerL0 + public sealed class RunnerL0 : IDisposable { private Mock _configurationManager; private Mock _jobNotification; @@ -29,6 +29,7 @@ namespace GitHub.Runner.Common.Tests.Listener private Mock _credentialManager; private Mock _actionsRunServer; private Mock _runServer; + private readonly string _returnJobResultForHosted; public RunnerL0() { @@ -45,6 +46,14 @@ namespace GitHub.Runner.Common.Tests.Listener _credentialManager = new Mock(); _actionsRunServer = new Mock(); _runServer = new Mock(); + + _returnJobResultForHosted = Environment.GetEnvironmentVariable("ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED"); + Environment.SetEnvironmentVariable("ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED", null); + } + + public void Dispose() + { + Environment.SetEnvironmentVariable("ACTIONS_RUNNER_RETURN_JOB_RESULT_FOR_HOSTED", _returnJobResultForHosted); } private Pipelines.AgentJobRequestMessage CreateJobRequestMessage(string jobName) diff --git a/src/Test/L0/Sdk/WellKnownRegularExpressionsL0.cs b/src/Test/L0/Sdk/WellKnownRegularExpressionsL0.cs new file mode 100644 index 000000000..5b85a3f0c --- /dev/null +++ b/src/Test/L0/Sdk/WellKnownRegularExpressionsL0.cs @@ -0,0 +1,100 @@ +using GitHub.DistributedTask.Pipelines.Expressions; +using Xunit; + +namespace GitHub.Runner.Common.Tests.Sdk +{ + public sealed class WellKnownRegularExpressionsL0 + { + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Sdk")] + public void SHA1_Key_Returns_CommitHash_Regex() + { + var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.SHA1); + + Assert.NotNull(regex); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Sdk")] + public void CommitHash_Key_Returns_CommitHash_Regex() + { + var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash); + + Assert.NotNull(regex); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Sdk")] + public void SHA1_And_CommitHash_Return_Same_Regex() + { + var sha1Regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.SHA1); + var commitHashRegex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash); + + Assert.Same(sha1Regex, commitHashRegex); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Sdk")] + public void Matches_40_Char_Hex() + { + var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash); + + Assert.Matches(regex.Value, new string('a', 40)); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Sdk")] + public void Matches_64_Char_Hex() + { + var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash); + + Assert.Matches(regex.Value, new string('a', 64)); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Sdk")] + public void Does_Not_Match_63_Char_Hex() + { + var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash); + + Assert.DoesNotMatch(regex.Value, new string('a', 63)); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Sdk")] + public void Does_Not_Match_65_Char_Hex() + { + var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash); + + Assert.DoesNotMatch(regex.Value, new string('a', 65)); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Sdk")] + public void Matches_Mixed_Case_64_Char() + { + var regex = WellKnownRegularExpressions.GetRegex(WellKnownRegularExpressions.CommitHash); + var value = new string('A', 32) + new string('b', 32); + + Assert.Matches(regex.Value, value); + } + + [Fact] + [Trait("Level", "L0")] + [Trait("Category", "Sdk")] + public void Unknown_Key_Returns_Null() + { + var regex = WellKnownRegularExpressions.GetRegex("UnknownType"); + + Assert.Null(regex); + } + } +} diff --git a/src/Test/L0/Worker/ActionManagerL0.cs b/src/Test/L0/Worker/ActionManagerL0.cs index 7ed5219a1..c612ac9d0 100644 --- a/src/Test/L0/Worker/ActionManagerL0.cs +++ b/src/Test/L0/Worker/ActionManagerL0.cs @@ -25,6 +25,7 @@ namespace GitHub.Runner.Common.Tests.Worker public sealed class ActionManagerL0 { private const string TestDataFolderName = "TestData"; + private const string Sha256 = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"; private CancellationTokenSource _ecTokenSource; private Mock _configurationStore; private Mock _dockerManager; @@ -334,7 +335,7 @@ runs: await File.WriteAllTextAsync(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions-download-artifact", "action.yml"), Content); #if OS_WINDOWS - ZipFile.CreateFromDirectory(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions-download-artifact"), Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "action_cache", "actions_download-artifact", "master-sha.zip"), CompressionLevel.Fastest, true); + ZipFile.CreateFromDirectory(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions-download-artifact"), Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "action_cache", "actions_download-artifact", $"{Sha256}.zip"), CompressionLevel.Fastest, true); #else string tar = WhichUtil.Which("tar", require: true, trace: _hc.GetTrace()); @@ -360,7 +361,7 @@ runs: string cwd = Path.GetDirectoryName(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions-download-artifact")); string inputDirectory = Path.GetFileName(Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions-download-artifact")); - string archiveFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "action_cache", "actions_download-artifact", "master-sha.tar.gz"); + string archiveFile = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "action_cache", "actions_download-artifact", $"{Sha256}.tar.gz"); int exitCode = await processInvoker.ExecuteAsync(_hc.GetDirectory(WellKnownDirectory.Bin), tar, $"-czf \"{archiveFile}\" -C \"{cwd}\" \"{inputDirectory}\"", null, CancellationToken.None); if (exitCode != 0) { @@ -368,6 +369,8 @@ runs: } } #endif + MockResolvedSha("actions/download-artifact", "master", Sha256); + var actionId = Guid.NewGuid(); var actions = new List { @@ -516,9 +519,10 @@ runs: string actionsArchive = Path.Combine(_hc.GetDirectory(WellKnownDirectory.Temp), "actions_archive", "action_checkout"); Directory.CreateDirectory(actionsArchive); - Directory.CreateDirectory(Path.Combine(actionsArchive, "actions_checkout", "master-sha")); - Directory.CreateDirectory(Path.Combine(actionsArchive, "actions_checkout", "master-sha", "content")); - await File.WriteAllTextAsync(Path.Combine(actionsArchive, "actions_checkout", "master-sha", "content", "action.yml"), Content); + Directory.CreateDirectory(Path.Combine(actionsArchive, "actions_checkout", Sha256)); + Directory.CreateDirectory(Path.Combine(actionsArchive, "actions_checkout", Sha256, "content")); + await File.WriteAllTextAsync(Path.Combine(actionsArchive, "actions_checkout", Sha256, "content", "action.yml"), Content); + MockResolvedSha("actions/checkout", "master", Sha256); Environment.SetEnvironmentVariable(Constants.Variables.Agent.ActionArchiveCacheDirectory, actionsArchive); //Act @@ -3149,6 +3153,51 @@ runs: #endif } + private void MockResolvedSha(string nameWithOwner, string reference, string resolvedSha) + { + _jobServer.Setup(x => x.ResolveActionDownloadInfoAsync(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny(), It.Is(actions => actions.Actions.Any(action => action.NameWithOwner == nameWithOwner && action.Ref == reference)), It.IsAny())) + .Returns((Guid scopeIdentifier, string hubName, Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken) => + { + var result = new ActionDownloadInfoCollection { Actions = new Dictionary() }; + foreach (var action in actions.Actions) + { + var key = $"{action.NameWithOwner}@{action.Ref}"; + result.Actions[key] = new ActionDownloadInfo + { + NameWithOwner = action.NameWithOwner, + Ref = action.Ref, + ResolvedNameWithOwner = action.NameWithOwner, + ResolvedSha = resolvedSha, + TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}", + ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}", + }; + } + + return Task.FromResult(result); + }); + + _launchServer.Setup(x => x.ResolveActionsDownloadInfoAsync(It.IsAny(), It.IsAny(), It.Is(actions => actions.Actions.Any(action => action.NameWithOwner == nameWithOwner && action.Ref == reference)), It.IsAny(), It.IsAny())) + .Returns((Guid planId, Guid jobId, ActionReferenceList actions, CancellationToken cancellationToken, bool displayHelpfulActionsDownloadErrors) => + { + var result = new ActionDownloadInfoCollection { Actions = new Dictionary() }; + foreach (var action in actions.Actions) + { + var key = $"{action.NameWithOwner}@{action.Ref}"; + result.Actions[key] = new ActionDownloadInfo + { + NameWithOwner = action.NameWithOwner, + Ref = action.Ref, + ResolvedNameWithOwner = action.NameWithOwner, + ResolvedSha = resolvedSha, + TarballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/tarball/{action.Ref}", + ZipballUrl = $"https://api.github.com/repos/{action.NameWithOwner}/zipball/{action.Ref}", + }; + } + + return Task.FromResult(result); + }); + } + private void Setup([CallerMemberName] string name = "", bool enableComposite = true) { _ecTokenSource?.Dispose();