diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5df9ccc025..c3a1ec2b98 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -41,3 +41,10 @@ jobs: with: target: Run-Integration-Tests cake-version: tool-manifest + + - name: Validate Integration Tests + uses: cake-build/cake-action@master + with: + script-path: tests/integration/Cake.Common/Build/GitHubActions/ValidateGitHubActionsProvider.cake + cake-version: tool-manifest + diff --git a/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsCommandsFixture.cs b/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsCommandsFixture.cs new file mode 100644 index 0000000000..8332925d99 --- /dev/null +++ b/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsCommandsFixture.cs @@ -0,0 +1,193 @@ +using System; +using System.Linq; +using System.Net; +using System.Net.Http; +using System.Threading; +using System.Threading.Tasks; +using Cake.Common.Build.GitHubActions.Commands; +using Cake.Common.Build.GitHubActions.Data; +using Cake.Core; +using Cake.Testing; +using NSubstitute; + +namespace Cake.Common.Tests.Fixtures.Build +{ + internal sealed class GitHubActionsCommandsFixture : HttpMessageHandler + { + private const string ApiVersion = "6.0-preview"; + private const string AcceptHeader = "application/json; api-version=" + ApiVersion; + private const string CreateArtifactUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + + "_apis/pipelines/workflows/34058136/artifacts?api-version=" + ApiVersion + "&artifactName=artifact"; + private const string CreateArtifactsUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + + "_apis/pipelines/workflows/34058136/artifacts?api-version=" + ApiVersion + "&artifactName=artifacts"; + private const string PutFileUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + + "_apis/resources/Containers/942031?itemPath=artifact%2Fartifact.txt"; + private const string CreateArtifactResponse = @"{ + ""containerId"": 942031, + ""size"": -1, + ""signedContent"": null, + ""fileContainerResourceUrl"": """ + GitHubActionsInfoFixture.ActionRuntimeUrl + @"_apis/resources/Containers/942031"", + ""type"": ""actions_storage"", + ""name"": ""artifact"", + ""url"": """ + GitHubActionsInfoFixture.ActionRuntimeUrl + @"_apis/pipelines/1/runs/7/artifacts?artifactName=artifact"", + ""expiresOn"": ""2021-12-14T18:43:29.7431144Z"", + ""items"": null +}"; + private const string CreateArtifactsResponse = @"{ + ""containerId"": 942031, + ""size"": -1, + ""signedContent"": null, + ""fileContainerResourceUrl"": """ + GitHubActionsInfoFixture.ActionRuntimeUrl + @"_apis/resources/Containers/942031"", + ""type"": ""actions_storage"", + ""name"": ""artifact"", + ""url"": """ + GitHubActionsInfoFixture.ActionRuntimeUrl + @"_apis/pipelines/1/runs/7/artifacts?artifactName=artifacts"", + ""expiresOn"": ""2021-12-14T18:43:29.7431144Z"", + ""items"": null +}"; + + private const string PutDirectoryRootUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + + "_apis/resources/Containers/942031?itemPath=artifacts%2Fartifact.txt"; + private const string PutDirectoryFolderAUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + + "_apis/resources/Containers/942031?itemPath=artifacts%2Ffolder_a%2Fartifact.txt"; + private const string PutDirectoryFolderBUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + + "_apis/resources/Containers/942031?itemPath=artifacts%2Ffolder_b%2Fartifact.txt"; + private const string PutDirectoryFolderBFolderCUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + + "_apis/resources/Containers/942031?itemPath=artifacts%2Ffolder_b%2Ffolder_c%2Fartifact.txt"; + + private GitHubActionsInfoFixture GitHubActionsInfoFixture { get; } + private ICakeEnvironment Environment { get; } + public FakeFileSystem FileSystem { get; } + + public GitHubActionsCommandsFixture() + { + GitHubActionsInfoFixture = new GitHubActionsInfoFixture(); + FileSystem = new FakeFileSystem(GitHubActionsInfoFixture.Environment); + FileSystem.CreateDirectory("/opt"); + Environment = GitHubActionsInfoFixture.Environment; + } + + public GitHubActionsCommands CreateGitHubActionsCommands() + { + return new GitHubActionsCommands(Environment, FileSystem, GitHubActionsInfoFixture.CreateEnvironmentInfo(), CreateClient); + } + + public GitHubActionsCommandsFixture WithNoGitubEnv() + { + Environment.GetEnvironmentVariable("GITHUB_ENV").Returns(null as string); + return this; + } + + public GitHubActionsCommandsFixture WithNoGitubPath() + { + Environment.GetEnvironmentVariable("GITHUB_PATH").Returns(null as string); + return this; + } + + private HttpClient CreateClient(string name) => new HttpClient(this); + + protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) + { + if (request.Headers.Authorization is null || request.Headers.Authorization.Scheme != "Bearer" || request.Headers.Authorization.Parameter != GitHubActionsInfoFixture.ActionRuntimeToken) + { + return new HttpResponseMessage + { + StatusCode = HttpStatusCode.Unauthorized + }; + } + + if (!request.Headers.TryGetValues("Accept", out var values) || !values.Contains(AcceptHeader)) + { + return new HttpResponseMessage + { + StatusCode = HttpStatusCode.BadRequest + }; + } + + switch (request) + { +#pragma warning disable SA1013 + // FilePath + case + { + RequestUri: { AbsoluteUri: CreateArtifactUrl }, + Method: { Method: "POST" }, + }: + { + return Ok(new StringContent(CreateArtifactResponse)); + } + + // DirectoryPath + case + { + RequestUri: { AbsoluteUri: CreateArtifactsUrl }, + Method: { Method: "POST" }, + }: + { + return Ok(new StringContent(CreateArtifactsResponse)); + } + + // FilePath + case + { + RequestUri: { AbsoluteUri: PutFileUrl }, + Method: { Method: "PUT" } + }: + case + { + RequestUri: { AbsoluteUri: CreateArtifactUrl }, + Method: { Method: "PATCH" }, + }: + + // DirectoryPath + case + { + RequestUri: { AbsoluteUri: PutDirectoryRootUrl }, + Method: { Method: "PUT" } + }: + case + { + RequestUri: { AbsoluteUri: PutDirectoryFolderAUrl }, + Method: { Method: "PUT" } + }: + case + { + RequestUri: { AbsoluteUri: PutDirectoryFolderBUrl }, + Method: { Method: "PUT" } + }: + case + { + RequestUri: { AbsoluteUri: PutDirectoryFolderBFolderCUrl }, + Method: { Method: "PUT" } + }: + case + { + RequestUri: { AbsoluteUri: CreateArtifactsUrl }, + Method: { Method: "PATCH" }, + }: + { + return Ok(); + } +#pragma warning restore SA1013 + + default: + { + await Task.Delay(1, cancellationToken); + return new HttpResponseMessage + { + StatusCode = HttpStatusCode.NotFound + }; + } + } + } + + private static HttpResponseMessage Ok(HttpContent content = null) + { + return new HttpResponseMessage + { + StatusCode = HttpStatusCode.OK, + ReasonPhrase = "OK", + Content = content + }; + } + } +} \ No newline at end of file diff --git a/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsFixture.cs b/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsFixture.cs index 42da53ad35..02ae8ab82f 100644 --- a/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsFixture.cs +++ b/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsFixture.cs @@ -4,18 +4,23 @@ using Cake.Common.Build.GitHubActions; using Cake.Core; +using Cake.Core.IO; +using Cake.Testing; using NSubstitute; namespace Cake.Common.Tests.Fixtures.Build { internal sealed class GitHubActionsFixture { - public ICakeEnvironment Environment { get; set; } + public ICakeEnvironment Environment { get; } + public IFileSystem FileSystem { get; } public GitHubActionsFixture() { Environment = Substitute.For(); Environment.GetEnvironmentVariable("GITHUB_ACTIONS").Returns((string)null); + Environment.WorkingDirectory.Returns("/home/cake"); + FileSystem = new FakeFileSystem(Environment); } public void IsRunningOnGitHubActions() @@ -25,7 +30,7 @@ public void IsRunningOnGitHubActions() public GitHubActionsProvider CreateGitHubActionsService() { - return new GitHubActionsProvider(Environment); + return new GitHubActionsProvider(Environment, FileSystem); } } } diff --git a/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsInfoFixture.cs b/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsInfoFixture.cs index 81cc6acf02..5df13154e8 100644 --- a/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsInfoFixture.cs +++ b/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsInfoFixture.cs @@ -10,7 +10,9 @@ namespace Cake.Common.Tests.Fixtures.Build { internal sealed class GitHubActionsInfoFixture { - public ICakeEnvironment Environment { get; set; } + public const string ActionRuntimeToken = "zht1j5NeW2T5ZsOxncX4CUEiWYhD4ZRwoDghkARk"; + public const string ActionRuntimeUrl = "https://pipelines.actions.githubusercontent.com/ip0FyYnZXxdEOcOwPHkRsZJd2x6G5XoT486UsAb0/"; + public ICakeEnvironment Environment { get; } public GitHubActionsInfoFixture() { @@ -19,12 +21,14 @@ public GitHubActionsInfoFixture() Environment.GetEnvironmentVariable("GITHUB_ACTIONS").Returns("true"); Environment.GetEnvironmentVariable("HOME").Returns("/home/runner"); + Environment.GetEnvironmentVariable("RUNNER_NAME").Returns("RunnerName"); Environment.GetEnvironmentVariable("RUNNER_OS").Returns("Linux"); Environment.GetEnvironmentVariable("RUNNER_TEMP").Returns("/home/runner/work/_temp"); Environment.GetEnvironmentVariable("RUNNER_TOOL_CACHE").Returns("/opt/hostedtoolcache"); Environment.GetEnvironmentVariable("RUNNER_WORKSPACE").Returns("/home/runner/work/cake"); Environment.GetEnvironmentVariable("GITHUB_ACTION").Returns("run1"); + Environment.GetEnvironmentVariable("GITHUB_ACTION_PATH").Returns("/path/to/action"); Environment.GetEnvironmentVariable("GITHUB_ACTOR").Returns("dependabot"); Environment.GetEnvironmentVariable("GITHUB_API_URL").Returns("https://api.github.com"); Environment.GetEnvironmentVariable("GITHUB_BASE_REF").Returns("master"); @@ -42,6 +46,12 @@ public GitHubActionsInfoFixture() Environment.GetEnvironmentVariable("GITHUB_SHA").Returns("d1e4f990f57349334368c8253382abc63be02d73"); Environment.GetEnvironmentVariable("GITHUB_WORKFLOW").Returns("Build"); Environment.GetEnvironmentVariable("GITHUB_WORKSPACE").Returns("/home/runner/work/cake/cake"); + + Environment.GetEnvironmentVariable("ACTIONS_RUNTIME_TOKEN").Returns(ActionRuntimeToken); + Environment.GetEnvironmentVariable("ACTIONS_RUNTIME_URL").Returns(ActionRuntimeUrl); + Environment.GetEnvironmentVariable("GITHUB_ENV").Returns("/opt/github.env"); + Environment.GetEnvironmentVariable("GITHUB_PATH").Returns("/opt/github.path"); + Environment.WorkingDirectory.Returns("/home/runner/work/cake/cake"); } public GitHubActionsRunnerInfo CreateRunnerInfo() @@ -63,5 +73,10 @@ public GitHubActionsEnvironmentInfo CreateEnvironmentInfo() { return new GitHubActionsEnvironmentInfo(Environment); } + + public GitHubActionsRuntimeInfo CreateRuntimeInfo() + { + return new GitHubActionsRuntimeInfo(Environment); + } } } diff --git a/src/Cake.Common.Tests/Unit/Build/GitHubActions/Commands/GitHubActionsCommandsTests.cs b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Commands/GitHubActionsCommandsTests.cs new file mode 100644 index 0000000000..c95ed46f26 --- /dev/null +++ b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Commands/GitHubActionsCommandsTests.cs @@ -0,0 +1,334 @@ +using System.IO; +using System.Threading.Tasks; +using Cake.Common.Build.GitHubActions.Commands; +using Cake.Common.Tests.Fixtures.Build; +using Cake.Core; +using Cake.Core.IO; +using Cake.Testing; +using NSubstitute; +using Xunit; + +namespace Cake.Common.Tests.Unit.Build.GitHubActions.Commands +{ + public sealed class GitHubActionsCommandsTests + { + public sealed class TheConstructor + { + [Fact] + public void Should_Throw_If_Environment_Is_Null() + { + // Given, When + var result = Record.Exception(() => new GitHubActionsCommands(null, null, null, null)); + + // Then + AssertEx.IsArgumentNullException(result, "environment"); + } + + [Fact] + public void Should_Throw_If_FileSystem_Is_Null() + { + // Given + var environment = Substitute.For(); + + // When + var result = Record.Exception(() => new GitHubActionsCommands(environment, null, null, null)); + + // Then + AssertEx.IsArgumentNullException(result, "fileSystem"); + } + + [Fact] + public void Should_Throw_If_ActionsEnvironment_Is_Null() + { + // Given + var environment = Substitute.For(); + var filesystem = Substitute.For(); + + // When + var result = Record.Exception(() => new GitHubActionsCommands(environment, filesystem, null, null)); + + // Then + AssertEx.IsArgumentNullException(result, "actionsEnvironment"); + } + + [Fact] + public void Should_Throw_If_CreateHttpClient_Is_Null() + { + // Given + var environment = Substitute.For(); + var filesystem = Substitute.For(); + var actionsEnvironment = new GitHubActionsInfoFixture().CreateEnvironmentInfo(); + + // When + var result = Record.Exception(() => new GitHubActionsCommands(environment, filesystem, actionsEnvironment, null)); + + // Then + AssertEx.IsArgumentNullException(result, "createHttpClient"); + } + } + + public sealed class TheAddPathMethod + { + [Fact] + public void Should_Throw_If_Path_Is_Null() + { + // Given + var commands = new GitHubActionsCommandsFixture().CreateGitHubActionsCommands(); + + // When + var result = Record.Exception(() => commands.AddPath(null)); + + // Then + AssertEx.IsArgumentNullException(result, "path"); + } + + [Fact] + public void Should_Throw_If_SystemPath_Is_Null() + { + // Given + var commands = new GitHubActionsCommandsFixture() + .WithNoGitubPath() + .CreateGitHubActionsCommands(); + + var path = "/temp/dev/bin"; + + // When + var result = Record.Exception(() => commands.AddPath(path)); + + // Then + AssertEx.IsCakeException(result, "GitHub Actions Runtime SystemPath missing."); + } + + [Fact] + public void Should_AddPath() + { + // Given + var gitHubActionsCommandsFixture = new GitHubActionsCommandsFixture(); + var commands = gitHubActionsCommandsFixture.CreateGitHubActionsCommands(); + var path = "/temp/dev/bin"; + + // When + commands.AddPath(path); + + // Then + Assert.Equal( + (path + System.Environment.NewLine).NormalizeLineEndings(), + gitHubActionsCommandsFixture.FileSystem.GetFile("/opt/github.path").GetTextContent().NormalizeLineEndings()); + } + } + + public sealed class TheSetEnvironmentVariableMethod + { + [Fact] + public void Should_Throw_If_Key_Is_Null() + { + // Given + var commands = new GitHubActionsCommandsFixture().CreateGitHubActionsCommands(); + + // When + var result = Record.Exception(() => commands.SetEnvironmentVariable(null, null)); + + // Then + AssertEx.IsArgumentNullException(result, "key"); + } + + [Fact] + public void Should_Throw_If_Value_Is_Null() + { + // Given + var commands = new GitHubActionsCommandsFixture().CreateGitHubActionsCommands(); + var key = "Key"; + + // When + var result = Record.Exception(() => commands.SetEnvironmentVariable(key, null)); + + // Then + AssertEx.IsArgumentNullException(result, "value"); + } + + [Fact] + public void Should_Throw_If_EnvPath_Is_Null() + { + // Given + var commands = new GitHubActionsCommandsFixture() + .WithNoGitubEnv() + .CreateGitHubActionsCommands(); + + var key = "Key"; + var value = "Value"; + + // When + var result = Record.Exception(() => commands.SetEnvironmentVariable(key, value)); + + // Then + AssertEx.IsCakeException(result, "GitHub Actions Runtime EnvPath missing."); + } + + [Fact] + public void Should_SetEnvironmentVariable() + { + // Given + var gitHubActionsCommandsFixture = new GitHubActionsCommandsFixture(); + var commands = gitHubActionsCommandsFixture.CreateGitHubActionsCommands(); + var key = "Key"; + var value = "Value"; + + // When + commands.SetEnvironmentVariable(key, value); + + // Then + Assert.Equal( + @"Key< commands.UploadArtifact(path, null)); + + // Then + AssertEx.IsArgumentNullException(result, "path"); + } + + [Fact] + public async Task Should_Throw_If_ArtifactName_Is_Null() + { + // Given + var commands = new GitHubActionsCommandsFixture().CreateGitHubActionsCommands(); + var path = FilePath.FromString("/artifacts/artifact.zip"); + + // When + var result = await Record.ExceptionAsync(() => commands.UploadArtifact(path, null)); + + // Then + AssertEx.IsArgumentNullException(result, "artifactName"); + } + + [Fact] + public async Task Should_Throw_If_File_Missing() + { + // Given + var commands = new GitHubActionsCommandsFixture().CreateGitHubActionsCommands(); + var path = FilePath.FromString("/artifacts/artifact.zip"); + var artifactName = "artifact"; + + // When + var result = await Record.ExceptionAsync(() => commands.UploadArtifact(path, artifactName)); + + // Then + AssertEx.IsExceptionWithMessage(result, "Artifact file not found."); + } + + [Fact] + public async Task Should_Upload() + { + // Given + var gitHubActionsCommandsFixture = new GitHubActionsCommandsFixture(); + var artifactName = "artifact"; + var file = gitHubActionsCommandsFixture + .FileSystem + .CreateFile("/artifacts/artifact.txt") + .SetContent(artifactName); + var commands = gitHubActionsCommandsFixture.CreateGitHubActionsCommands(); + + // When + await commands.UploadArtifact(file.Path, artifactName); + } + } + + public sealed class Directory + { + [Fact] + public async Task Should_Throw_If_Path_Is_Null() + { + // Given + var commands = new GitHubActionsCommandsFixture().CreateGitHubActionsCommands(); + DirectoryPath path = null; + + // When + var result = await Record.ExceptionAsync(() => commands.UploadArtifact(path, null)); + + // Then + AssertEx.IsArgumentNullException(result, "path"); + } + + [Fact] + public async Task Should_Throw_If_ArtifactName_Is_Null() + { + // Given + var commands = new GitHubActionsCommandsFixture().CreateGitHubActionsCommands(); + var path = DirectoryPath.FromString("/artifacts"); + + // When + var result = await Record.ExceptionAsync(() => commands.UploadArtifact(path, null)); + + // Then + AssertEx.IsArgumentNullException(result, "artifactName"); + } + + [Fact] + public async Task Should_Throw_If_Directory_Missing() + { + // Given + var commands = new GitHubActionsCommandsFixture().CreateGitHubActionsCommands(); + var path = DirectoryPath.FromString("/artifacts"); + var artifactName = "artifact"; + + // When + var result = await Record.ExceptionAsync(() => commands.UploadArtifact(path, artifactName)); + + // Then + AssertEx.IsExceptionWithMessage(result, "Artifact directory /artifacts not found."); + } + + [Fact] + public async Task Should_Upload() + { + // Given + var gitHubActionsCommandsFixture = new GitHubActionsCommandsFixture(); + var artifactName = "artifacts"; + var directory = DirectoryPath.FromString("/artifacts"); + + gitHubActionsCommandsFixture + .FileSystem + .CreateFile(directory.CombineWithFilePath("artifact.txt")) + .SetContent(artifactName); + + gitHubActionsCommandsFixture + .FileSystem + .CreateFile(directory.Combine("folder_a").CombineWithFilePath("artifact.txt")) + .SetContent(artifactName); + + gitHubActionsCommandsFixture + .FileSystem + .CreateFile(directory.Combine("folder_b").CombineWithFilePath("artifact.txt")) + .SetContent(artifactName); + + gitHubActionsCommandsFixture + .FileSystem + .CreateFile(directory.Combine("folder_b").Combine("folder_c").CombineWithFilePath("artifact.txt")) + .SetContent(artifactName); + + var commands = gitHubActionsCommandsFixture.CreateGitHubActionsCommands(); + + // When + await commands.UploadArtifact(directory, artifactName); + } + } + } + } +} \ No newline at end of file diff --git a/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsEnvironmentInfoTests.cs b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsEnvironmentInfoTests.cs index f147ee0d65..27c6815b0a 100644 --- a/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsEnvironmentInfoTests.cs +++ b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsEnvironmentInfoTests.cs @@ -18,7 +18,7 @@ public void Should_Return_Correct_Values() var info = new GitHubActionsInfoFixture().CreateEnvironmentInfo(); // When - var result = info.Home; + var result = info.Home.FullPath; // Then Assert.Equal("/home/runner", result); diff --git a/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsRunnerInfoTests.cs b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsRunnerInfoTests.cs index 236c8e3a81..85ae645a23 100644 --- a/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsRunnerInfoTests.cs +++ b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsRunnerInfoTests.cs @@ -9,6 +9,22 @@ namespace Cake.Common.Tests.Unit.Build.GitHubActions.Data { public sealed class GitHubActionsRunnerInfoTests { + public sealed class TheNameProperty + { + [Fact] + public void Should_Return_Correct_Value() + { + // Given + var info = new GitHubActionsInfoFixture().CreateRunnerInfo(); + + // When + var result = info.Name; + + // Then + Assert.Equal("RunnerName", result); + } + } + // ReSharper disable once InconsistentNaming public sealed class TheOSProperty { @@ -35,7 +51,7 @@ public void Should_Return_Correct_Value() var info = new GitHubActionsInfoFixture().CreateRunnerInfo(); // When - var result = info.Temp; + var result = info.Temp.FullPath; // Then Assert.Equal("/home/runner/work/_temp", result); @@ -51,7 +67,7 @@ public void Should_Return_Correct_Value() var info = new GitHubActionsInfoFixture().CreateRunnerInfo(); // When - var result = info.ToolCache; + var result = info.ToolCache.FullPath; // Then Assert.Equal("/opt/hostedtoolcache", result); @@ -67,7 +83,7 @@ public void Should_Return_Correct_Value() var info = new GitHubActionsInfoFixture().CreateRunnerInfo(); // When - var result = info.Workspace; + var result = info.Workspace.FullPath; // Then Assert.Equal("/home/runner/work/cake", result); diff --git a/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsRuntimeInfoTests.cs b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsRuntimeInfoTests.cs new file mode 100644 index 0000000000..f9a966960f --- /dev/null +++ b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsRuntimeInfoTests.cs @@ -0,0 +1,88 @@ +using Cake.Common.Tests.Fixtures.Build; +using Xunit; + +namespace Cake.Common.Tests.Unit.Build.GitHubActions.Data +{ + public sealed class GitHubActionsRuntimeInfoTests + { + public sealed class TheIsRuntimeAvailableProperty + { + [Fact] + public void Should_Return_Correct_Value() + { + // Given + var info = new GitHubActionsInfoFixture().CreateRuntimeInfo(); + + // When + var result = info.IsRuntimeAvailable; + + // Then + Assert.Equal(true, result); + } + } + + public sealed class TheTokenProperty + { + [Fact] + public void Should_Return_Correct_Value() + { + // Given + var info = new GitHubActionsInfoFixture().CreateRuntimeInfo(); + + // When + var result = info.Token; + + // Then + Assert.Equal("zht1j5NeW2T5ZsOxncX4CUEiWYhD4ZRwoDghkARk", result); + } + } + + public sealed class TheUrlProperty + { + [Fact] + public void Should_Return_Correct_Value() + { + // Given + var info = new GitHubActionsInfoFixture().CreateRuntimeInfo(); + + // When + var result = info.Url; + + // Then + Assert.Equal("https://pipelines.actions.githubusercontent.com/ip0FyYnZXxdEOcOwPHkRsZJd2x6G5XoT486UsAb0/", result); + } + } + + public sealed class TheEnvPathProperty + { + [Fact] + public void Should_Return_Correct_Value() + { + // Given + var info = new GitHubActionsInfoFixture().CreateRuntimeInfo(); + + // When + var result = info.EnvPath.FullPath; + + // Then + Assert.Equal("/opt/github.env", result); + } + } + + public sealed class TheSystemPathProperty + { + [Fact] + public void Should_Return_Correct_Value() + { + // Given + var info = new GitHubActionsInfoFixture().CreateRuntimeInfo(); + + // When + var result = info.SystemPath.FullPath; + + // Then + Assert.Equal("/opt/github.path", result); + } + } + } +} \ No newline at end of file diff --git a/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsWorkflowInfoTests.cs b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsWorkflowInfoTests.cs index 49d47dce2e..55924d6dad 100644 --- a/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsWorkflowInfoTests.cs +++ b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Data/GitHubActionsWorkflowInfoTests.cs @@ -25,6 +25,22 @@ public void Should_Return_Correct_Value() } } + public sealed class TheActionPathProperty + { + [Fact] + public void Should_Return_Correct_Value() + { + // Given + var info = new GitHubActionsInfoFixture().CreateWorkflowInfo(); + + // When + var result = info.ActionPath.FullPath; + + // Then + Assert.Equal("/path/to/action", result); + } + } + public sealed class TheActorProperty { [Fact] @@ -98,7 +114,7 @@ public void Should_Return_Correct_Value() var info = new GitHubActionsInfoFixture().CreateWorkflowInfo(); // When - var result = info.EventPath; + var result = info.EventPath.FullPath; // Then Assert.Equal("/home/runner/work/_temp/_github_workflow/event.json", result); @@ -290,7 +306,7 @@ public void Should_Return_Correct_Value() var info = new GitHubActionsInfoFixture().CreateWorkflowInfo(); // When - var result = info.Workspace; + var result = info.Workspace.FullPath; // Then Assert.Equal("/home/runner/work/cake/cake", result); diff --git a/src/Cake.Common.Tests/Unit/Build/GitHubActions/GitHubActionsProviderTests.cs b/src/Cake.Common.Tests/Unit/Build/GitHubActions/GitHubActionsProviderTests.cs index 1c19960527..cb5e387e2e 100644 --- a/src/Cake.Common.Tests/Unit/Build/GitHubActions/GitHubActionsProviderTests.cs +++ b/src/Cake.Common.Tests/Unit/Build/GitHubActions/GitHubActionsProviderTests.cs @@ -4,6 +4,8 @@ using Cake.Common.Build.GitHubActions; using Cake.Common.Tests.Fixtures.Build; +using Cake.Core; +using NSubstitute; using Xunit; namespace Cake.Common.Tests.Unit.Build.GitHubActions @@ -16,11 +18,24 @@ public sealed class TheConstructor public void Should_Throw_If_Environment_Is_Null() { // Given, When - var result = Record.Exception(() => new GitHubActionsProvider(null)); + var result = Record.Exception(() => new GitHubActionsProvider(null, null)); // Then AssertEx.IsArgumentNullException(result, "environment"); } + + [Fact] + public void Should_Throw_If_FileSystem_Is_Null() + { + // Given + var environment = Substitute.For(); + + // When + var result = Record.Exception(() => new GitHubActionsProvider(environment, null)); + + // Then + AssertEx.IsArgumentNullException(result, "fileSystem"); + } } public sealed class TheIsRunningOnGitHubActionsProperty diff --git a/src/Cake.Common/Build/BuildSystemAliases.cs b/src/Cake.Common/Build/BuildSystemAliases.cs index a658d0805a..197c7fc76f 100644 --- a/src/Cake.Common/Build/BuildSystemAliases.cs +++ b/src/Cake.Common/Build/BuildSystemAliases.cs @@ -57,7 +57,7 @@ public static BuildSystem BuildSystem(this ICakeContext context) var bitbucketPipelinesProvider = new BitbucketPipelinesProvider(context.Environment); var goCDProvider = new GoCDProvider(context.Environment, context.Log); var gitLabCIProvider = new GitLabCIProvider(context.Environment); - var gitHubActionsProvider = new GitHubActionsProvider(context.Environment); + var gitHubActionsProvider = new GitHubActionsProvider(context.Environment, context.FileSystem); var azurePipelinesProvider = new AzurePipelinesProvider(context.Environment, new BuildSystemServiceMessageWriter()); return new BuildSystem(appVeyorProvider, teamCityProvider, myGetProvider, bambooProvider, continuaCIProvider, jenkinsProvider, bitriseProvider, travisCIProvider, bitbucketPipelinesProvider, goCDProvider, gitLabCIProvider, gitHubActionsProvider, azurePipelinesProvider); diff --git a/src/Cake.Common/Build/GitHubActions/Commands/ArtifactResponse.cs b/src/Cake.Common/Build/GitHubActions/Commands/ArtifactResponse.cs new file mode 100644 index 0000000000..7fc9ddd6c7 --- /dev/null +++ b/src/Cake.Common/Build/GitHubActions/Commands/ArtifactResponse.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text.Json.Serialization; + +namespace Cake.Common.Build.GitHubActions.Commands +{ + internal sealed class ArtifactResponse + { + [JsonPropertyName("containerId")] + public long ContainerId { get; set; } + + [JsonPropertyName("size")] + public long Size { get; set; } + + [JsonPropertyName("signedContent")] + public string SignedContent { get; set; } + + [JsonPropertyName("fileContainerResourceUrl")] + public string FileContainerResourceUrl { get; set; } + + [JsonPropertyName("type")] + public string Type { get; set; } + + [JsonPropertyName("name")] + public string Name { get; set; } + + [JsonPropertyName("url")] + public string Url { get; set; } + } +} diff --git a/src/Cake.Common/Build/GitHubActions/Commands/CreateArtifactParameters.cs b/src/Cake.Common/Build/GitHubActions/Commands/CreateArtifactParameters.cs new file mode 100644 index 0000000000..9930c25bbe --- /dev/null +++ b/src/Cake.Common/Build/GitHubActions/Commands/CreateArtifactParameters.cs @@ -0,0 +1,19 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Cake.Common.Build.GitHubActions.Commands +{ + internal sealed class CreateArtifactParameters + { + public string Name { get; set; } + + public string Type { get; set; } + + public CreateArtifactParameters(string name) + { + Name = name; + Type = "actions_storage"; + } + } +} diff --git a/src/Cake.Common/Build/GitHubActions/Commands/GitHubActionsCommands.cs b/src/Cake.Common/Build/GitHubActions/Commands/GitHubActionsCommands.cs new file mode 100644 index 0000000000..dc46a208fa --- /dev/null +++ b/src/Cake.Common/Build/GitHubActions/Commands/GitHubActionsCommands.cs @@ -0,0 +1,283 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Net.Http.Headers; +using System.Text.Json; +using System.Threading.Tasks; +using Cake.Common.Build.GitHubActions.Data; +using Cake.Core; +using Cake.Core.IO; +using Path = Cake.Core.IO.Path; + +namespace Cake.Common.Build.GitHubActions.Commands +{ + /// + /// Provides GitHub Actions commands for a current build. + /// + public sealed class GitHubActionsCommands + { + private const string ApiVersion = "6.0-preview"; + private const string AcceptHeader = "application/json;api-version=" + ApiVersion; + private const string ContentTypeHeader = "application/json"; + + private readonly ICakeEnvironment _environment; + private readonly IFileSystem _fileSystem; + private readonly GitHubActionsEnvironmentInfo _actionsEnvironment; + private readonly Func _createHttpClient; + + /// + /// Initializes a new instance of the class. + /// + /// The environment. + /// The actions environment. + /// The file system. + /// The http client factory. + public GitHubActionsCommands( + ICakeEnvironment environment, + IFileSystem fileSystem, + GitHubActionsEnvironmentInfo actionsEnvironment, + Func createHttpClient) + { + _environment = environment ?? throw new ArgumentNullException(nameof(environment)); + _fileSystem = fileSystem ?? throw new ArgumentNullException(nameof(fileSystem)); + _actionsEnvironment = actionsEnvironment ?? throw new ArgumentNullException(nameof(actionsEnvironment)); + _createHttpClient = createHttpClient ?? throw new ArgumentNullException(nameof(createHttpClient)); + } + + /// + /// Prepends a directory to the system PATH variable and automatically makes it available to all subsequent actions in the current job. + /// + /// The directory path. + public void AddPath(DirectoryPath path) + { + if (path is null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (_actionsEnvironment.Runtime.SystemPath == null) + { + throw new CakeException("GitHub Actions Runtime SystemPath missing."); + } + + var file = _fileSystem.GetFile(_actionsEnvironment.Runtime.SystemPath); + using var stream = file.Open(FileMode.Append, FileAccess.Write, FileShare.None); + using var writer = new StreamWriter(stream); + writer.WriteLine(path.MakeAbsolute(_environment).FullPath); + } + + /// + /// Creates or updates an environment variable for any steps running next in a job. + /// + /// The key. + /// The Value. + public void SetEnvironmentVariable(string key, string value) + { + if (string.IsNullOrEmpty(key)) + { + throw new ArgumentNullException(nameof(key)); + } + + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (_actionsEnvironment.Runtime.EnvPath == null) + { + throw new CakeException("GitHub Actions Runtime EnvPath missing."); + } + + var file = _fileSystem.GetFile(_actionsEnvironment.Runtime.EnvPath); + using var stream = file.Open(FileMode.Append, FileAccess.Write, FileShare.None); + using var writer = new StreamWriter(stream); + writer.Write(key); + writer.WriteLine("< + /// Upload local file into a file container folder, and create an artifact. + /// + /// Path to the local file. + /// The artifact name. + /// A representing the asynchronous operation. + public async Task UploadArtifact(FilePath path, string artifactName) + { + ValidateUploadArtifactParameters(path, artifactName); + + var file = _fileSystem.GetFile(path); + + if (!file.Exists) + { + throw new FileNotFoundException("Artifact file not found.", path.FullPath); + } + + await CreateAndUploadArtifactFiles(artifactName, path.GetDirectory(), file); + } + + /// + /// Upload local directory files into a file container folder, and create an artifact. + /// + /// Path to the local directory. + /// The artifact name. + /// A representing the asynchronous operation. + public async Task UploadArtifact(DirectoryPath path, string artifactName) + { + ValidateUploadArtifactParameters(path, artifactName); + + var directory = _fileSystem.GetDirectory(path); + + if (!directory.Exists) + { + throw new DirectoryNotFoundException(FormattableString.Invariant($"Artifact directory {path.FullPath} not found.")); + } + + var files = directory + .GetFiles("*", SearchScope.Recursive) + .ToArray(); + + await CreateAndUploadArtifactFiles(artifactName, path, files); + } + + private void ValidateUploadArtifactParameters(Path path, string artifactName) + { + if (path is null) + { + throw new ArgumentNullException(nameof(path)); + } + + if (string.IsNullOrWhiteSpace(artifactName)) + { + throw new ArgumentNullException(nameof(artifactName)); + } + + if (string.IsNullOrWhiteSpace(_actionsEnvironment.Runtime.Token)) + { + throw new CakeException("GitHub Actions Runtime Token missing."); + } + + if (string.IsNullOrWhiteSpace(_actionsEnvironment.Runtime.Url)) + { + throw new CakeException("GitHub Actions Runtime Url missing."); + } + + if (string.IsNullOrWhiteSpace(_actionsEnvironment.Workflow.RunId)) + { + throw new CakeException("GitHub Actions Workflow RunId missing."); + } + } + + private async Task CreateAndUploadArtifactFiles( + string artifactName, + DirectoryPath rootPath, + params IFile[] files) + { + var artifactUrl = string.Concat( + _actionsEnvironment.Runtime.Url, + "_apis/pipelines/workflows/", + _actionsEnvironment.Workflow.RunId, + "/artifacts?api-version=", + ApiVersion, + "&artifactName=", + Uri.EscapeDataString(artifactName)); + + var client = GetRuntimeHttpClient(); + + var artifactResponse = await CreateArtifact(artifactName, client, artifactUrl); + + long totalFileSize = 0L; + foreach (var file in files) + { + using var artifactStream = file.OpenRead(); + await UploadFile(rootPath, artifactName, artifactResponse, client, artifactStream, file); + totalFileSize += file.Length; + } + + await FinalizeArtifact(client, artifactUrl, totalFileSize); + } + + private HttpClient GetRuntimeHttpClient([System.Runtime.CompilerServices.CallerMemberName] string memberName = null) + { + var client = _createHttpClient(memberName); + client.DefaultRequestHeaders.Accept.Add(MediaTypeWithQualityHeaderValue.Parse(AcceptHeader)); + client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", _actionsEnvironment.Runtime.Token); + return client; + } + + private static async Task FinalizeArtifact(HttpClient client, string artifactUrl, long totalSize) + { + var jsonData = JsonSerializer.SerializeToUtf8Bytes(new PatchArtifactSize(totalSize)); + + var patchResponse = await client.SendAsync( + new HttpRequestMessage( + new HttpMethod("PATCH"), + artifactUrl) + { + Content = new ByteArrayContent(jsonData) + { + Headers = { ContentType = MediaTypeHeaderValue.Parse(ContentTypeHeader) } + } + }); + + patchResponse.EnsureSuccessStatusCode(); + } + + private static async Task UploadFile(DirectoryPath rootPath, string artifactName, ArtifactResponse artifactResponse, HttpClient client, Stream artifactStream, IFile file) + { + var itemPath = string.Concat( + artifactName, + "/", + rootPath.GetRelativePath(file.Path).FullPath); + + var putFileUrl = string.Concat( + artifactResponse?.FileContainerResourceUrl ?? throw new ArgumentNullException("FileContainerResourceUrl"), + $"?itemPath={Uri.EscapeDataString(itemPath)}"); + + var putResponse = await client.PutAsync( + putFileUrl, + new StreamContent(artifactStream) + { + Headers = + { + ContentType = MediaTypeHeaderValue.Parse("application/octet-stream"), + ContentLength = file.Length, + ContentRange = new ContentRangeHeaderValue(0, file.Length - 1L, file.Length) + } + }); + + if (!putResponse.IsSuccessStatusCode) + { + throw new CakeException( + FormattableString.Invariant($"Put artifact file {itemPath} failed."), + new HttpRequestException( + FormattableString.Invariant($"Response status code does not indicate success: {putResponse.StatusCode:d} ({putResponse.ReasonPhrase})."))); + } + } + + private static async Task CreateArtifact(string artifactName, HttpClient client, string artifactUrl) + { + var jsonData = JsonSerializer.SerializeToUtf8Bytes(new CreateArtifactParameters(artifactName)); + var response = await client.PostAsync( + artifactUrl, + new ByteArrayContent(jsonData) + { + Headers = { ContentType = MediaTypeHeaderValue.Parse(ContentTypeHeader) } + }); + + response.EnsureSuccessStatusCode(); + + using var responseStream = await response.Content.ReadAsStreamAsync(); + var artifactResponse = await JsonSerializer.DeserializeAsync(responseStream) + ?? throw new CakeException("Failed to parse ArtifactResponse"); + + return artifactResponse; + } + } +} diff --git a/src/Cake.Common/Build/GitHubActions/Commands/PatchArtifactSize.cs b/src/Cake.Common/Build/GitHubActions/Commands/PatchArtifactSize.cs new file mode 100644 index 0000000000..f70e245251 --- /dev/null +++ b/src/Cake.Common/Build/GitHubActions/Commands/PatchArtifactSize.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +namespace Cake.Common.Build.GitHubActions.Commands +{ + internal sealed class PatchArtifactSize + { + public long Size { get; set; } + + public PatchArtifactSize(long size) + { + Size = size; + } + } +} \ No newline at end of file diff --git a/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsEnvironmentInfo.cs b/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsEnvironmentInfo.cs index e5c4114ca3..3c363cd254 100644 --- a/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsEnvironmentInfo.cs +++ b/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsEnvironmentInfo.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Cake.Core; +using Cake.Core.IO; namespace Cake.Common.Build.GitHubActions.Data { @@ -21,6 +22,7 @@ public GitHubActionsEnvironmentInfo(ICakeEnvironment environment) Runner = new GitHubActionsRunnerInfo(environment); Workflow = new GitHubActionsWorkflowInfo(environment); PullRequest = new GitHubActionsPullRequestInfo(environment); + Runtime = new GitHubActionsRuntimeInfo(environment); } /// @@ -61,7 +63,7 @@ public GitHubActionsEnvironmentInfo(ICakeEnvironment environment) /// } /// /// - public string Home => GetEnvironmentString("HOME"); + public DirectoryPath Home => GetEnvironmentDirectoryPath("HOME"); /// /// Gets GitHub Actions runner information. @@ -201,5 +203,25 @@ public GitHubActionsEnvironmentInfo(ICakeEnvironment environment) /// /// public GitHubActionsPullRequestInfo PullRequest { get; } + + /// + /// Gets GitHub Actions runtime information. + /// + /// + /// The GitHub Actions runtime information. + /// + /// Via BuildSystem. + /// + /// + /// // TODO + /// + /// + /// Via GitHubActions. + /// + /// + /// // TODO + /// + /// + public GitHubActionsRuntimeInfo Runtime { get; } } } diff --git a/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsRunnerInfo.cs b/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsRunnerInfo.cs index 1bb31db6dc..24431384d2 100644 --- a/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsRunnerInfo.cs +++ b/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsRunnerInfo.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Cake.Core; +using Cake.Core.IO; namespace Cake.Common.Build.GitHubActions.Data { @@ -20,6 +21,14 @@ public GitHubActionsRunnerInfo(ICakeEnvironment environment) { } + /// + /// Gets the name of the runner executing the job. + /// + /// + /// The name of the runner executing the job. + /// + public string Name => GetEnvironmentString("RUNNER_NAME"); + /// /// Gets the operating system of the runner executing the job. /// @@ -36,7 +45,7 @@ public GitHubActionsRunnerInfo(ICakeEnvironment environment) /// The path of the temporary directory for the runner. /// This directory is guaranteed to be empty at the start of each job, even on self-hosted runners. /// - public string Temp => GetEnvironmentString("RUNNER_TEMP"); + public DirectoryPath Temp => GetEnvironmentDirectoryPath("RUNNER_TEMP"); /// /// Gets the path of the directory containing some of the pre-installed tools for GitHub-hosted runners. @@ -44,7 +53,7 @@ public GitHubActionsRunnerInfo(ICakeEnvironment environment) /// /// The path of the directory containing some of the pre-installed tools for GitHub-hosted runners. /// - public string ToolCache => GetEnvironmentString("RUNNER_TOOL_CACHE"); + public DirectoryPath ToolCache => GetEnvironmentDirectoryPath("RUNNER_TOOL_CACHE"); /// /// Gets the runner workspace directory path. @@ -52,6 +61,6 @@ public GitHubActionsRunnerInfo(ICakeEnvironment environment) /// /// The runner workspace directory path. /// - public string Workspace => GetEnvironmentString("RUNNER_WORKSPACE"); + public DirectoryPath Workspace => GetEnvironmentDirectoryPath("RUNNER_WORKSPACE"); } } diff --git a/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsRuntimeInfo.cs b/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsRuntimeInfo.cs new file mode 100644 index 0000000000..a09f0393d3 --- /dev/null +++ b/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsRuntimeInfo.cs @@ -0,0 +1,65 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using Cake.Core; +using Cake.Core.IO; + +namespace Cake.Common.Build.GitHubActions.Data +{ + /// + /// Provides GitHub Actions runtime information for the current build. + /// + public sealed class GitHubActionsRuntimeInfo : GitHubActionsInfo + { + /// + /// Initializes a new instance of the class. + /// + /// The environment. + public GitHubActionsRuntimeInfo(ICakeEnvironment environment) + : base(environment) + { + } + + /// + /// Gets a value indicating whether the GitHub Actions Runtime is available for the current build. + /// + /// + /// true if the GitHub Actions Runtime is available for the current build. + /// + public bool IsRuntimeAvailable + => !string.IsNullOrWhiteSpace(Token) && !string.IsNullOrWhiteSpace(Url); + + /// + /// Gets the current runtime API authorization token. + /// + /// + /// The current runtime API authorization token. + /// + public string Token => GetEnvironmentString("ACTIONS_RUNTIME_TOKEN"); + + /// + /// Gets the current runtime API endpoint url. + /// + /// + /// The current runtime API endpoint url. + /// + public string Url => GetEnvironmentString("ACTIONS_RUNTIME_URL"); + + /// + /// Gets the path to environment file to set an environment variable that the following steps in a job can use. + /// + /// + /// The path to environment file to set an environment variable that the following steps in a job can use. + /// + public FilePath EnvPath => GetEnvironmentFilePath("GITHUB_ENV"); + + /// + /// Gets the path to path file to add a path to system path that the following steps in a job can use. + /// + /// + /// The path to path file to add a path to system path that the following steps in a job can use. + /// + public FilePath SystemPath => GetEnvironmentFilePath("GITHUB_PATH"); + } +} \ No newline at end of file diff --git a/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsWorkflowInfo.cs b/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsWorkflowInfo.cs index 10f371048a..b01c2ecfdd 100644 --- a/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsWorkflowInfo.cs +++ b/src/Cake.Common/Build/GitHubActions/Data/GitHubActionsWorkflowInfo.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Cake.Core; +using Cake.Core.IO; namespace Cake.Common.Build.GitHubActions.Data { @@ -28,6 +29,14 @@ public GitHubActionsWorkflowInfo(ICakeEnvironment environment) /// public string Action => GetEnvironmentString("GITHUB_ACTION"); + /// + /// Gets the path where your action is located. You can use this path to access files located in the same repository as your action. This variable is only supported in composite run steps actions. + /// + /// + /// The path where your action is located. You can use this path to access files located in the same repository as your action. This variable is only supported in composite run steps actions. + /// + public DirectoryPath ActionPath => GetEnvironmentDirectoryPath("GITHUB_ACTION_PATH"); + /// /// Gets the name of the person or app that initiated the workflow. /// @@ -66,7 +75,7 @@ public GitHubActionsWorkflowInfo(ICakeEnvironment environment) /// /// The path of the file with the complete webhook event payload. /// - public string EventPath => GetEnvironmentString("GITHUB_EVENT_PATH"); + public FilePath EventPath => GetEnvironmentFilePath("GITHUB_EVENT_PATH"); /// /// Gets the GraphQL API URL. @@ -162,6 +171,6 @@ public GitHubActionsWorkflowInfo(ICakeEnvironment environment) /// /// The GitHub workspace directory path. /// - public string Workspace => GetEnvironmentString("GITHUB_WORKSPACE"); + public DirectoryPath Workspace => GetEnvironmentDirectoryPath("GITHUB_WORKSPACE"); } } diff --git a/src/Cake.Common/Build/GitHubActions/GitHubActionsInfo.cs b/src/Cake.Common/Build/GitHubActions/GitHubActionsInfo.cs index 5bb2fd1d08..1c0ce6f64a 100644 --- a/src/Cake.Common/Build/GitHubActions/GitHubActionsInfo.cs +++ b/src/Cake.Common/Build/GitHubActions/GitHubActionsInfo.cs @@ -4,6 +4,7 @@ using System; using Cake.Core; +using Cake.Core.IO; namespace Cake.Common.Build.GitHubActions { @@ -24,7 +25,7 @@ protected GitHubActionsInfo(ICakeEnvironment environment) } /// - /// Gets an environment variable as a . + /// Gets an environment variable as a . /// /// The environment variable name. /// The environment variable. @@ -33,6 +34,30 @@ protected string GetEnvironmentString(string variable) return _environment.GetEnvironmentVariable(variable) ?? string.Empty; } + /// + /// Gets an environment variable as a . + /// + /// The environment variable name. + /// The environment variable. + protected DirectoryPath GetEnvironmentDirectoryPath(string variable) + { + return _environment.GetEnvironmentVariable(variable) is string value + ? DirectoryPath.FromString(value) + : null; + } + + /// + /// Gets an environment variable as a . + /// + /// The environment variable name. + /// The environment variable. + protected FilePath GetEnvironmentFilePath(string variable) + { + return _environment.GetEnvironmentVariable(variable) is string value + ? FilePath.FromString(value) + : null; + } + /// /// Gets an environment variable as a . /// diff --git a/src/Cake.Common/Build/GitHubActions/GitHubActionsProvider.cs b/src/Cake.Common/Build/GitHubActions/GitHubActionsProvider.cs index 69ca235b5e..f8b600c8e3 100644 --- a/src/Cake.Common/Build/GitHubActions/GitHubActionsProvider.cs +++ b/src/Cake.Common/Build/GitHubActions/GitHubActionsProvider.cs @@ -3,8 +3,10 @@ // See the LICENSE file in the project root for more information. using System; +using Cake.Common.Build.GitHubActions.Commands; using Cake.Common.Build.GitHubActions.Data; using Cake.Core; +using Cake.Core.IO; namespace Cake.Common.Build.GitHubActions { @@ -19,10 +21,12 @@ public class GitHubActionsProvider : IGitHubActionsProvider /// Initializes a new instance of the class. /// /// The environment. - public GitHubActionsProvider(ICakeEnvironment environment) + /// The file system. + public GitHubActionsProvider(ICakeEnvironment environment, IFileSystem fileSystem) { _environment = environment ?? throw new ArgumentNullException(nameof(environment)); Environment = new GitHubActionsEnvironmentInfo(environment); + Commands = new GitHubActionsCommands(environment, fileSystem, Environment, _ => new System.Net.Http.HttpClient()); } /// @@ -30,5 +34,8 @@ public GitHubActionsProvider(ICakeEnvironment environment) /// public GitHubActionsEnvironmentInfo Environment { get; } + + /// + public GitHubActionsCommands Commands { get; } } } diff --git a/src/Cake.Common/Build/GitHubActions/IGitHubActionsProvider.cs b/src/Cake.Common/Build/GitHubActions/IGitHubActionsProvider.cs index edb4c446fd..32ae3fb917 100644 --- a/src/Cake.Common/Build/GitHubActions/IGitHubActionsProvider.cs +++ b/src/Cake.Common/Build/GitHubActions/IGitHubActionsProvider.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. +using Cake.Common.Build.GitHubActions.Commands; using Cake.Common.Build.GitHubActions.Data; namespace Cake.Common.Build.GitHubActions @@ -26,5 +27,13 @@ public interface IGitHubActionsProvider /// The GitHub Actions environment. /// GitHubActionsEnvironmentInfo Environment { get; } + + /// + /// Gets the GitHub Actions commands. + /// + /// + /// The GitHub Actions commands. + /// + public GitHubActionsCommands Commands { get; } } } diff --git a/tests/integration/Cake.Common/Build/BuildSystemAliases.cake b/tests/integration/Cake.Common/Build/BuildSystemAliases.cake new file mode 100644 index 0000000000..aeceb7d274 --- /dev/null +++ b/tests/integration/Cake.Common/Build/BuildSystemAliases.cake @@ -0,0 +1,13 @@ +#load "GitHubActions/GitHubActionsProvider.cake" + +Task("Cake.Common.Build.BuildSystemAliases.BuildProvider") + .DoesForEach( + () => Enum.GetValues(typeof(BuildProvider)).OfType(), + item => { + Information("{0}: {1}", item, (BuildSystem.Provider & item) == item); + }); + + +Task("Cake.Common.Build.BuildSystemAliases") + .IsDependentOn("Cake.Common.Build.BuildSystemAliases.BuildProvider") + .IsDependentOn("Cake.Common.Build.GitHubActionsProvider"); \ No newline at end of file diff --git a/tests/integration/Cake.Common/Build/GitHubActions/GitHubActionsProvider.cake b/tests/integration/Cake.Common/Build/GitHubActions/GitHubActionsProvider.cake new file mode 100644 index 0000000000..df76358646 --- /dev/null +++ b/tests/integration/Cake.Common/Build/GitHubActions/GitHubActionsProvider.cake @@ -0,0 +1,68 @@ +#load "./../../../utilities/xunit.cake" + +Task("Cake.Common.Build.GitHubActionsProvider.Provider") + .Does(() => { + Assert.Equal(BuildProvider.GitHubActions, BuildSystem.Provider); +}); + +Task("Cake.Common.Build.GitHubActionsProvider.Commands.AddPath") + .Does(() => { + // Given + FilePath path = typeof(ICakeContext).GetTypeInfo().Assembly.Location; + + // When + GitHubActions.Commands.AddPath(path.GetDirectory()); +}); + + +Task("Cake.Common.Build.GitHubActionsProvider.Commands.SetEnvironmentVariable") + .Does(() => { + // Given + string key = $"CAKE_{GitHubActions.Environment.Runner.OS}_{Context.Environment.Runtime.BuiltFramework.Identifier}_{Context.Environment.Runtime.BuiltFramework.Version}_VERSION" + .Replace(".", "_") + .Replace("__", "_") + .ToUpper(), + value = Context.Environment.Runtime.CakeVersion.ToString(3); + + // When + GitHubActions.Commands.SetEnvironmentVariable(key, value); +}); + +Task("Cake.Common.Build.GitHubActionsProvider.Commands.UploadArtifact.File") + .Does(async () => { + // Given + FilePath path = typeof(ICakeContext).GetTypeInfo().Assembly.Location; + string artifactName = $"File_{GitHubActions.Environment.Runner.OS}_{Context.Environment.Runtime.BuiltFramework.Identifier}_{Context.Environment.Runtime.BuiltFramework.Version}"; + + // When + await GitHubActions.Commands.UploadArtifact(path, artifactName); +}); + +Task("Cake.Common.Build.GitHubActionsProvider.Commands.UploadArtifact.Directory") + .Does(async () => { + // Given + FilePath path = typeof(ICakeContext).GetTypeInfo().Assembly.Location; + string artifactName = $"Directory_{GitHubActions.Environment.Runner.OS}_{Context.Environment.Runtime.BuiltFramework.Identifier}_{Context.Environment.Runtime.BuiltFramework.Version}"; + + // When + await GitHubActions.Commands.UploadArtifact(path.GetDirectory(), artifactName); +}); + +var gitHubActionsProviderTask = Task("Cake.Common.Build.GitHubActionsProvider"); + +if (GitHubActions.IsRunningOnGitHubActions) +{ + gitHubActionsProviderTask + .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Provider") + .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Commands.AddPath") + .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Commands.SetEnvironmentVariable"); + +} + +if (GitHubActions.Environment.Runtime.IsRuntimeAvailable) +{ + gitHubActionsProviderTask + .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Commands.UploadArtifact.File"); + gitHubActionsProviderTask + .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Commands.UploadArtifact.Directory"); +} \ No newline at end of file diff --git a/tests/integration/Cake.Common/Build/GitHubActions/ValidateGitHubActionsProvider.cake b/tests/integration/Cake.Common/Build/GitHubActions/ValidateGitHubActionsProvider.cake new file mode 100644 index 0000000000..852ae63fe6 --- /dev/null +++ b/tests/integration/Cake.Common/Build/GitHubActions/ValidateGitHubActionsProvider.cake @@ -0,0 +1,39 @@ +#load "./../../../utilities/xunit.cake" + +public record BuildData(string GitVersion, string Path, string OS); + +Setup( + context => new BuildData( + EnvironmentVariable("GitVersion_MajorMinorPatch") ?? throw new ArgumentNullException("Missing GitVersion Variable.", "GitVersion_MajorMinorPatch"), + EnvironmentVariable("PATH") ?? throw new ArgumentNullException("Missing PATH varable.", "PATH"), + GitHubActions.Environment.Runner.OS.ToUpper() + ) +); + + +Task("ValidateEnvironment") + .DoesForEach( + data => new [] { + $"CAKE_{data.OS}_NETCOREAPP_3_1_VERSION", + $"CAKE_{data.OS}_NETCOREAPP_5_0_VERSION", + $"CAKE_{data.OS}_NETCOREAPP_6_0_VERSION" + }, + (data, envKey) => Assert.Equal(data.GitVersion, EnvironmentVariable(envKey)) + ); + +Task("ValidatePath") + .DoesForEach( + new [] { + "Cake\\WTool\\Wtools\\Wnet6\\W0", + "Cake\\WTool\\Wtools\\Wnet5\\W0", + "Cake\\WTool\\Wtools\\Wnetcoreapp3\\W1" + }, + (data, path) => Assert.Matches(path, data.Path) + ); + +Task("Default") + .IsDependentOn("ValidateEnvironment") + .IsDependentOn("ValidatePath"); + + +RunTarget(Argument("target", "Default")); \ No newline at end of file diff --git a/tests/integration/build.cake b/tests/integration/build.cake index 7e9b8003af..6c93a351e7 100644 --- a/tests/integration/build.cake +++ b/tests/integration/build.cake @@ -7,6 +7,7 @@ #load "setup.cake" #load "teardown.cake" #load "./Cake.Common/ArgumentAliases.cake" +#load "./Cake.Common/Build/BuildSystemAliases.cake" #load "./Cake.Common/EnvironmentAliases.cake" #load "./Cake.Common/Diagnostics/LoggingAliases.cake" #load "./Cake.Common/IO/DirectoryAliases.cake" @@ -65,6 +66,7 @@ Task("Cake.Core") Task("Cake.Common") .IsDependentOn("Cake.Common.ArgumentAliases") + .IsDependentOn("Cake.Common.Build.BuildSystemAliases") .IsDependentOn("Cake.Common.EnvironmentAliases") .IsDependentOn("Cake.Common.Diagnostics.LoggingAliases") .IsDependentOn("Cake.Common.IO.DirectoryAliases")