diff --git a/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsCommandsFixture.cs b/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsCommandsFixture.cs index 73734e3126..9c28bd8e6b 100644 --- a/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsCommandsFixture.cs +++ b/src/Cake.Common.Tests/Fixtures/Build/GitHubActionsCommandsFixture.cs @@ -7,6 +7,7 @@ using Cake.Common.Build.GitHubActions.Commands; using Cake.Common.Build.GitHubActions.Data; using Cake.Core; +using Cake.Core.IO; using Cake.Testing; using NSubstitute; @@ -16,6 +17,8 @@ internal sealed class GitHubActionsCommandsFixture : HttpMessageHandler { private const string ApiVersion = "6.0-preview"; private const string AcceptHeader = "application/json; api-version=" + ApiVersion; + private const string AcceptGzip = "application/octet-stream; api-version=" + ApiVersion; + private const string AcceptEncodingGzip = "gzip"; private const string CreateArtifactUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + "_apis/pipelines/workflows/34058136/artifacts?api-version=" + ApiVersion + "&artifactName=artifact"; private const string CreateArtifactsUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + @@ -54,6 +57,54 @@ internal sealed class GitHubActionsCommandsFixture : HttpMessageHandler private const string PutDirectoryFolderBFolderCUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + "_apis/resources/Containers/942031?itemPath=artifacts%2Ffolder_b%2Ffolder_c%2Fartifact.txt"; + private const string GetArtifactResourceUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + + "_apis/pipelines/workflows/34058136/artifacts?api-version=6.0-preview&artifactName=artifact"; + private const string FileContainerResourceUrl = GitHubActionsInfoFixture.ActionRuntimeUrl + @"_apis/resources/Containers/4794789"; + private const string GetArtifactResourceResponse = @"{ + ""count"": 1, + ""value"": [ + { + ""containerId"": 4794789, + ""size"": 4, + ""signedContent"": null, + ""fileContainerResourceUrl"": """ + FileContainerResourceUrl + @""", + ""type"": ""actions_storage"", + ""name"": ""artifact"", + ""url"": """ + GitHubActionsInfoFixture.ActionRuntimeUrl + @"_apis/pipelines/1/runs/7/artifacts?artifactName=artifact"", + ""expiresOn"": ""2022-03-16T08:22:01.5699067Z"", + ""items"": null + } + ] +}"; + + private const string GetContainerItemResourcesUrl = FileContainerResourceUrl + "?itemPath=artifact"; + private const string GetContainerItemResourcesResponse = @"{ + ""count"": 1, + ""value"": [ + { + ""containerId"": 4794789, + ""scopeIdentifier"": ""00000000-0000-0000-0000-000000000000"", + ""path"": ""artifact/test.txt"", + ""itemType"": ""file"", + ""status"": ""created"", + ""fileLength"": 4, + ""fileEncoding"": 1, + ""fileType"": 1, + ""dateCreated"": ""2021-12-16T09:05:18.803Z"", + ""dateLastModified"": ""2021-12-16T09:05:18.907Z"", + ""createdBy"": ""2daeb16b-86ae-4e46-ba89-92a8aa076e52"", + ""lastModifiedBy"": ""2daeb16b-86ae-4e46-ba89-92a8aa076e52"", + ""itemLocation"": """ + GetContainerItemResourcesUrl + @"%2Ftest.txt&metadata=True"", + ""contentLocation"": """ + GetContainerItemResourcesUrl + @"%2Ftest.txt"", + ""fileId"": 1407, + ""contentId"": """" + } + ] +}"; + + private const string DownloadItemResourceUrl = GetContainerItemResourcesUrl + "%2Ftest.txt"; + private const string DownloadItemResourceResponse = "Cake"; + private GitHubActionsInfoFixture GitHubActionsInfoFixture { get; } private ICakeEnvironment Environment { get; } public FakeFileSystem FileSystem { get; } @@ -71,6 +122,12 @@ public GitHubActionsCommands CreateGitHubActionsCommands() return new GitHubActionsCommands(Environment, FileSystem, GitHubActionsInfoFixture.CreateEnvironmentInfo(), CreateClient); } + public GitHubActionsCommandsFixture WithWorkingDirectory(DirectoryPath workingDirectory) + { + Environment.WorkingDirectory = workingDirectory; + return this; + } + public GitHubActionsCommandsFixture WithNoGitHubEnv() { Environment.GetEnvironmentVariable("GITHUB_ENV").Returns(null as string); @@ -95,18 +152,26 @@ protected override async Task SendAsync(HttpRequestMessage }; } - if (!request.Headers.TryGetValues("Accept", out var values) || !values.Contains(AcceptHeader)) + if ( + !request.Headers.TryGetValues("Accept", out var values) + || !values.Contains(AcceptHeader)) { - return new HttpResponseMessage + if (request.RequestUri.AbsoluteUri != DownloadItemResourceUrl + || !values.Contains(AcceptGzip) + || !request.Headers.TryGetValues("Accept-Encoding", out var encodingValues) + || !encodingValues.Contains(AcceptEncodingGzip)) { - StatusCode = HttpStatusCode.BadRequest - }; + return new HttpResponseMessage + { + StatusCode = HttpStatusCode.BadRequest + }; + } } switch (request) { #pragma warning disable SA1013 - // FilePath + // Create Artifact FilePath case { RequestUri: { AbsoluteUri: CreateArtifactUrl }, @@ -116,7 +181,7 @@ protected override async Task SendAsync(HttpRequestMessage return Ok(new StringContent(CreateArtifactResponse)); } - // DirectoryPath + // Create Artifact DirectoryPath case { RequestUri: { AbsoluteUri: CreateArtifactsUrl }, @@ -126,7 +191,37 @@ protected override async Task SendAsync(HttpRequestMessage return Ok(new StringContent(CreateArtifactsResponse)); } - // FilePath + // Download Artifact - Get Artifact Container Resource + case + { + RequestUri: { AbsoluteUri: GetArtifactResourceUrl }, + Method: { Method: "GET" } + }: + { + return Ok(new StringContent(GetArtifactResourceResponse)); + } + + // Download Artifact - Get Artifact Container Item Resource + case + { + RequestUri: { AbsoluteUri: GetContainerItemResourcesUrl }, + Method: { Method: "GET" } + }: + { + return Ok(new StringContent(GetContainerItemResourcesResponse)); + } + + // Download Artifact - DownloadItemResource + case + { + RequestUri: { AbsoluteUri: DownloadItemResourceUrl }, + Method: { Method: "GET" } + }: + { + return Ok(new StringContent(DownloadItemResourceResponse)); + } + + // Put FilePath case { RequestUri: { AbsoluteUri: PutFileUrl }, @@ -138,7 +233,7 @@ protected override async Task SendAsync(HttpRequestMessage Method: { Method: "PATCH" }, }: - // DirectoryPath + // Put DirectoryPath case { RequestUri: { AbsoluteUri: PutDirectoryRootUrl }, diff --git a/src/Cake.Common.Tests/Unit/Build/GitHubActions/Commands/GitHubActionsCommandsTests.cs b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Commands/GitHubActionsCommandsTests.cs index 70e5c6b1f4..6661c123de 100644 --- a/src/Cake.Common.Tests/Unit/Build/GitHubActions/Commands/GitHubActionsCommandsTests.cs +++ b/src/Cake.Common.Tests/Unit/Build/GitHubActions/Commands/GitHubActionsCommandsTests.cs @@ -233,20 +233,24 @@ public async Task Should_Throw_If_File_Missing() AssertEx.IsExceptionWithMessage(result, "Artifact file not found."); } - [Fact] - public async Task Should_Upload() + [Theory] + [InlineData("/", "/artifacts/artifact.txt")] + [InlineData("/artifacts", "artifact.txt")] + public async Task Should_Upload(string workingDirectory, string testPath) { // Given - var gitHubActionsCommandsFixture = new GitHubActionsCommandsFixture(); + var gitHubActionsCommandsFixture = new GitHubActionsCommandsFixture() + .WithWorkingDirectory(workingDirectory); + var testFilePath = FilePath.FromString(testPath); var artifactName = "artifact"; - var file = gitHubActionsCommandsFixture + gitHubActionsCommandsFixture .FileSystem .CreateFile("/artifacts/artifact.txt") .SetContent(artifactName); var commands = gitHubActionsCommandsFixture.CreateGitHubActionsCommands(); // When - await commands.UploadArtifact(file.Path, artifactName); + await commands.UploadArtifact(testFilePath, artifactName); } } @@ -295,13 +299,17 @@ public async Task Should_Throw_If_Directory_Missing() AssertEx.IsExceptionWithMessage(result, "Artifact directory /artifacts not found."); } - [Fact] - public async Task Should_Upload() + [Theory] + [InlineData("/", "/src/artifacts")] + [InlineData("/src", "artifacts")] + public async Task Should_Upload(string workingDirectory, string testPath) { // Given - var gitHubActionsCommandsFixture = new GitHubActionsCommandsFixture(); + var gitHubActionsCommandsFixture = new GitHubActionsCommandsFixture() + .WithWorkingDirectory(workingDirectory); + var testDirectoryPath = DirectoryPath.FromString(testPath); var artifactName = "artifacts"; - var directory = DirectoryPath.FromString("/artifacts"); + var directory = DirectoryPath.FromString("/src/artifacts"); gitHubActionsCommandsFixture .FileSystem @@ -326,9 +334,85 @@ public async Task Should_Upload() var commands = gitHubActionsCommandsFixture.CreateGitHubActionsCommands(); // When - await commands.UploadArtifact(directory, artifactName); + await commands.UploadArtifact(testDirectoryPath, artifactName); } } } + + public sealed class TheDownloadArtifactMethod + { + [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.DownloadArtifact(null, path)); + + // Then + AssertEx.IsArgumentNullException(result, "artifactName"); + } + + [Fact] + public async Task Should_Throw_If_Path_Is_Null() + { + // Given + var commands = new GitHubActionsCommandsFixture().CreateGitHubActionsCommands(); + var artifactName = "artifactName"; + + // When + var result = await Record.ExceptionAsync(() => commands.DownloadArtifact(artifactName, null)); + + // Then + AssertEx.IsArgumentNullException(result, "path"); + } + + [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.DownloadArtifact(artifactName, path)); + + // Then + AssertEx.IsExceptionWithMessage(result, "Local directory /artifacts not found."); + } + + [Theory] + [InlineData("/", "/src/artifacts")] + [InlineData("/src", "artifacts")] + public async Task Should_Download(string workingDirectory, string testPath) + { + // Given + var gitHubActionsCommandsFixture = new GitHubActionsCommandsFixture() + .WithWorkingDirectory(workingDirectory); + var testDirectoryPath = DirectoryPath.FromString(testPath); + var artifactName = "artifact"; + var directory = DirectoryPath.FromString("/src/artifacts"); + var filePath = directory.CombineWithFilePath("test.txt"); + + gitHubActionsCommandsFixture + .FileSystem + .CreateDirectory(directory); + + var commands = gitHubActionsCommandsFixture.CreateGitHubActionsCommands(); + + // When + await commands.DownloadArtifact(artifactName, testDirectoryPath); + var file = gitHubActionsCommandsFixture + .FileSystem + .GetFile(filePath); + + // Then + Assert.True(file.Exists, $"{filePath.FullPath} doesn't exist."); + Assert.Equal("Cake", file.GetTextContent()); + } + } } } \ No newline at end of file diff --git a/src/Cake.Common/Build/GitHubActions/Commands/ContainerItem.cs b/src/Cake.Common/Build/GitHubActions/Commands/ContainerItem.cs new file mode 100644 index 0000000000..f9fd045659 --- /dev/null +++ b/src/Cake.Common/Build/GitHubActions/Commands/ContainerItem.cs @@ -0,0 +1,33 @@ +// 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.Text.Json.Serialization; + +namespace Cake.Common.Build.GitHubActions.Commands +{ + internal sealed class ContainerItem + { + [JsonPropertyName("containerId")] + public long ContainerId { get; set; } + + [JsonPropertyName("size")] + public long Size { 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; } + + [JsonPropertyName("expiresOn")] + public DateTimeOffset ExpiresOn { get; set; } + } +} diff --git a/src/Cake.Common/Build/GitHubActions/Commands/ContainerItemResource.cs b/src/Cake.Common/Build/GitHubActions/Commands/ContainerItemResource.cs new file mode 100644 index 0000000000..8065fc0a31 --- /dev/null +++ b/src/Cake.Common/Build/GitHubActions/Commands/ContainerItemResource.cs @@ -0,0 +1,60 @@ +// 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.Text.Json.Serialization; + +namespace Cake.Common.Build.GitHubActions.Commands +{ + internal class ContainerItemResource + { + [JsonPropertyName("containerId")] + public long ContainerId { get; set; } + + [JsonPropertyName("scopeIdentifier")] + public Guid ScopeIdentifier { get; set; } + + [JsonPropertyName("path")] + public string Path { get; set; } + + [JsonPropertyName("itemType")] + public string ItemType { get; set; } + + [JsonPropertyName("status")] + public string Status { get; set; } + + [JsonPropertyName("dateCreated")] + public DateTimeOffset DateCreated { get; set; } + + [JsonPropertyName("dateLastModified")] + public DateTimeOffset DateLastModified { get; set; } + + [JsonPropertyName("createdBy")] + public Guid CreatedBy { get; set; } + + [JsonPropertyName("lastModifiedBy")] + public Guid LastModifiedBy { get; set; } + + [JsonPropertyName("itemLocation")] + public string ItemLocation { get; set; } + + [JsonPropertyName("contentLocation")] + public string ContentLocation { get; set; } + + [JsonPropertyName("contentId")] + public string ContentId { get; set; } + + [JsonPropertyName("fileLength")] + public long? FileLength { get; set; } + + [JsonPropertyName("fileEncoding")] + public long? FileEncoding { get; set; } + + [JsonPropertyName("fileType")] + public long? FileType { get; set; } + + [JsonPropertyName("fileId")] + public long? FileId { get; set; } + } +} diff --git a/src/Cake.Common/Build/GitHubActions/Commands/GitHubActionsCommands.cs b/src/Cake.Common/Build/GitHubActions/Commands/GitHubActionsCommands.cs index dc46a208fa..8eba25d7d2 100644 --- a/src/Cake.Common/Build/GitHubActions/Commands/GitHubActionsCommands.cs +++ b/src/Cake.Common/Build/GitHubActions/Commands/GitHubActionsCommands.cs @@ -12,7 +12,6 @@ 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 { @@ -22,8 +21,10 @@ namespace Cake.Common.Build.GitHubActions.Commands public sealed class GitHubActionsCommands { private const string ApiVersion = "6.0-preview"; - private const string AcceptHeader = "application/json;api-version=" + ApiVersion; + private const string AcceptHeader = "application/json; api-version=" + ApiVersion; private const string ContentTypeHeader = "application/json"; + private const string AcceptGzip = "application/octet-stream; api-version=" + ApiVersion; + private const string AcceptEncodingGzip = "gzip"; private readonly ICakeEnvironment _environment; private readonly IFileSystem _fileSystem; @@ -110,16 +111,14 @@ public void SetEnvironmentVariable(string key, string value) /// A representing the asynchronous operation. public async Task UploadArtifact(FilePath path, string artifactName) { - ValidateUploadArtifactParameters(path, artifactName); - - var file = _fileSystem.GetFile(path); + var file = _fileSystem.GetFile(ValidateArtifactParameters(path, artifactName)); if (!file.Exists) { - throw new FileNotFoundException("Artifact file not found.", path.FullPath); + throw new FileNotFoundException("Artifact file not found.", file.Path.FullPath); } - await CreateAndUploadArtifactFiles(artifactName, path.GetDirectory(), file); + await CreateAndUploadArtifactFiles(artifactName, file.Path.GetDirectory(), file); } /// @@ -130,23 +129,159 @@ public async Task UploadArtifact(FilePath path, string artifactName) /// A representing the asynchronous operation. public async Task UploadArtifact(DirectoryPath path, string artifactName) { - ValidateUploadArtifactParameters(path, artifactName); - - var directory = _fileSystem.GetDirectory(path); + var directory = _fileSystem.GetDirectory(ValidateArtifactParameters(path, artifactName)); if (!directory.Exists) { - throw new DirectoryNotFoundException(FormattableString.Invariant($"Artifact directory {path.FullPath} not found.")); + throw new DirectoryNotFoundException(FormattableString.Invariant($"Artifact directory {directory.Path.FullPath} not found.")); } var files = directory .GetFiles("*", SearchScope.Recursive) .ToArray(); - await CreateAndUploadArtifactFiles(artifactName, path, files); + await CreateAndUploadArtifactFiles(artifactName, directory.Path, files); + } + + /// + /// Download remote artifact container into local directory. + /// + /// The artifact name. + /// Path to the local directory. + /// A representing the asynchronous operation. + public async Task DownloadArtifact(string artifactName, DirectoryPath path) + { + var directory = _fileSystem.GetDirectory(ValidateArtifactParameters(path, artifactName)); + + if (!directory.Exists) + { + throw new DirectoryNotFoundException(FormattableString.Invariant($"Local directory {directory.Path.FullPath} not found.")); + } + + var client = GetRuntimeHttpClient(); + + var artifactResourceUrl = await GetArtifactResourceUrl(client, artifactName); + + var containerItemResources = await GetContainerItemResources( + client, + directory.Path, + artifactName, + artifactResourceUrl); + + await DownloadItemResources(client, containerItemResources); + } + + private async Task DownloadItemResources(HttpClient client, (FilePath FilePath, string ContentLocation, long FileLength)[] containerItemResourceContent) + { + foreach (var (filePath, contentLocation, fileLength) in containerItemResourceContent) + { + await DownloadItemResource(client, filePath, contentLocation, fileLength); + } + } + + private async Task DownloadItemResource( + HttpClient client, + FilePath filePath, + string contentLocation, + long fileLength) + { + var contentDirectory = _fileSystem.GetDirectory(filePath.GetDirectory()); + + if (!contentDirectory.Exists) + { + contentDirectory.Create(); + } + + var contentFile = _fileSystem.GetFile(filePath); + + using var contentFileStream = contentFile.OpenWrite(); + + if (fileLength == 0) + { + return; + } + + using var contentResponse = await client.SendAsync( + new HttpRequestMessage( + HttpMethod.Get, + contentLocation) + { + Headers = + { + Accept = { MediaTypeWithQualityHeaderValue.Parse(AcceptGzip) }, + AcceptEncoding = { StringWithQualityHeaderValue.Parse(AcceptEncodingGzip) } + } + }); + + contentResponse.EnsureSuccessStatusCode(); + + using var contentResponseStream = await contentResponse.Content.ReadAsStreamAsync(); + + await contentResponseStream.CopyToAsync(contentFileStream); } - private void ValidateUploadArtifactParameters(Path path, string artifactName) + private async Task<(FilePath LocalPath, string ContentLocation, long FileLength)[]> GetContainerItemResources( + HttpClient client, + DirectoryPath path, + string artifactName, + string artifactResourceUrl) + { + using var resourceResponse = await client.GetAsync(artifactResourceUrl); + + resourceResponse.EnsureSuccessStatusCode(); + + using var resourceResponseStream = await resourceResponse.Content.ReadAsStreamAsync(); + + var containerItemResource = await JsonSerializer.DeserializeAsync>(resourceResponseStream); + + int artifactNameLength = artifactName.Length; + int relativePathStart = artifactNameLength + 1; + var containerItemResourceContent = + containerItemResource + ?.Value + .Where(content => content?.Path is string path + && path.Length > relativePathStart + && path[artifactNameLength] is char separator + && (separator == '/' || separator == '\\') + && path.StartsWith(artifactName) + && content.ItemType?.ToLowerInvariant() == "file") + .Select(content => (path.CombineWithFilePath(content.Path[relativePathStart..]), + content.ContentLocation, + content.FileLength ?? 0)) + .ToArray(); + + if (containerItemResourceContent == null || containerItemResourceContent.Length == 0) + { + throw new Exception($"Artifact \"{artifactName}\" content not found."); + } + + return containerItemResourceContent; + } + + private async Task GetArtifactResourceUrl(HttpClient client, string artifactName) + { + var artifactUrl = GetArtifactUrl(artifactName); + + using var containerResponse = await client.GetAsync(artifactUrl); + + containerResponse.EnsureSuccessStatusCode(); + + using var containerResponseStream = await containerResponse.Content.ReadAsStreamAsync(); + + var containerItems = await JsonSerializer.DeserializeAsync>(containerResponseStream); + + var artifactsLookup = (containerItems?.Value ?? Array.Empty()) + .Where(item => !string.IsNullOrWhiteSpace(item.FileContainerResourceUrl) + && !string.IsNullOrWhiteSpace(item.Name)) + .ToLookup( + key => key.Name, + item => string.Concat(item.FileContainerResourceUrl, "?itemPath=", Uri.EscapeDataString(item.Name))); + + return artifactsLookup[artifactName].FirstOrDefault() + ?? throw new Exception($"Artifact \"{artifactName}\" not found."); + } + + private T ValidateArtifactParameters(T path, string artifactName) where T : IPath { if (path is null) { @@ -172,6 +307,8 @@ private void ValidateUploadArtifactParameters(Path path, string artifactName) { throw new CakeException("GitHub Actions Workflow RunId missing."); } + + return path.MakeAbsolute(_environment); } private async Task CreateAndUploadArtifactFiles( @@ -179,14 +316,7 @@ private async Task CreateAndUploadArtifactFiles( 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 artifactUrl = GetArtifactUrl(artifactName); var client = GetRuntimeHttpClient(); @@ -203,6 +333,18 @@ private async Task CreateAndUploadArtifactFiles( await FinalizeArtifact(client, artifactUrl, totalFileSize); } + private string GetArtifactUrl(string artifactName) + { + return string.Concat( + _actionsEnvironment.Runtime.Url, + "_apis/pipelines/workflows/", + _actionsEnvironment.Workflow.RunId, + "/artifacts?api-version=", + ApiVersion, + "&artifactName=", + Uri.EscapeDataString(artifactName)); + } + private HttpClient GetRuntimeHttpClient([System.Runtime.CompilerServices.CallerMemberName] string memberName = null) { var client = _createHttpClient(memberName); diff --git a/src/Cake.Common/Build/GitHubActions/Commands/Values.cs b/src/Cake.Common/Build/GitHubActions/Commands/Values.cs new file mode 100644 index 0000000000..73c44ad61f --- /dev/null +++ b/src/Cake.Common/Build/GitHubActions/Commands/Values.cs @@ -0,0 +1,17 @@ +// 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 Values + { + [JsonPropertyName("count")] + public long Count { get; set; } + + [JsonPropertyName("value")] + public T[] Value { get; set; } + } +} diff --git a/src/Cake.Core/IO/DirectoryPath.cs b/src/Cake.Core/IO/DirectoryPath.cs index 5aab01d7ca..884661d97d 100644 --- a/src/Cake.Core/IO/DirectoryPath.cs +++ b/src/Cake.Core/IO/DirectoryPath.cs @@ -14,7 +14,7 @@ namespace Cake.Core.IO /// Represents a directory path. /// [TypeConverter(typeof(DirectoryPathConverter))] - public sealed class DirectoryPath : Path, IEquatable, IComparer + public sealed class DirectoryPath : Path, IEquatable, IComparer, IPath { /// /// Initializes a new instance of the class. diff --git a/src/Cake.Core/IO/FilePath.cs b/src/Cake.Core/IO/FilePath.cs index 753f81e398..d4fac1767f 100644 --- a/src/Cake.Core/IO/FilePath.cs +++ b/src/Cake.Core/IO/FilePath.cs @@ -13,7 +13,7 @@ namespace Cake.Core.IO /// Represents a file path. /// [TypeConverter(typeof(FilePathConverter))] - public sealed class FilePath : Path, IEquatable, IComparer + public sealed class FilePath : Path, IEquatable, IComparer, IPath { /// /// Gets a value indicating whether this path has a file extension. diff --git a/src/Cake.Core/IO/IPath.cs b/src/Cake.Core/IO/IPath.cs new file mode 100644 index 0000000000..a20176e58f --- /dev/null +++ b/src/Cake.Core/IO/IPath.cs @@ -0,0 +1,81 @@ +// 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.Core.IO +{ + /// + /// Represents a path. + /// + /// The path type. + public interface IPath + { + /// + /// Gets the full path. + /// + /// The full path. + string FullPath { get; } + + /// + /// Gets a value indicating whether or not this path is relative. + /// + /// + /// true if this path is relative; otherwise, false. + /// + bool IsRelative { get; } + + /// + /// Gets a value indicating whether or not this path is an UNC path. + /// + /// + /// true if this path is an UNC path; otherwise, false. + /// + bool IsUNC { get; } + + /// + /// Gets the separator this path was normalized with. + /// + char Separator { get; } + + /// + /// Gets the segments making up the path. + /// + /// The segments making up the path. + string[] Segments { get; } + + /// + /// Returns a that represents this path. + /// + /// + /// A that represents this instance. + /// + string ToString(); + + /// + /// Makes the {T} path absolute (if relative) using the current working directory. + /// + /// The environment. + /// An absolute path. + T MakeAbsolute(ICakeEnvironment environment); + + /// + /// Makes the path absolute (if relative) using the specified directory path. + /// + /// The path. + /// An absolute path. + T MakeAbsolute(DirectoryPath path); + + /// + /// Collapses a path containing ellipses. + /// + /// A collapsed path. + T Collapse(); + + /// + /// Get the relative path to another path. + /// + /// The target path. + /// A path. + public T GetRelativePath(T to); + } +} \ 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 index 3afb6799f4..2b5f7f5661 100644 --- a/tests/integration/Cake.Common/Build/GitHubActions/GitHubActionsProvider.cake +++ b/tests/integration/Cake.Common/Build/GitHubActions/GitHubActionsProvider.cake @@ -1,4 +1,6 @@ #load "./../../../utilities/xunit.cake" +#load "./../../../utilities/paths.cake" +#load "./../../../utilities/io.cake" Task("Cake.Common.Build.GitHubActionsProvider.Provider") .Does(() => { @@ -6,12 +8,9 @@ Task("Cake.Common.Build.GitHubActionsProvider.Provider") }); Task("Cake.Common.Build.GitHubActionsProvider.Commands.AddPath") - .Does(() => { - // Given - FilePath path = typeof(ICakeContext).GetTypeInfo().Assembly.Location; - + .Does(async data => { // When - GitHubActions.Commands.AddPath(path.GetDirectory()); + GitHubActions.Commands.AddPath(data.AssemblyPath.GetDirectory()); }); @@ -29,23 +28,32 @@ Task("Cake.Common.Build.GitHubActionsProvider.Commands.SetEnvironmentVariable") }); 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}"; - + .Does(async data => { // When - await GitHubActions.Commands.UploadArtifact(path, artifactName); + await GitHubActions.Commands.UploadArtifact(data.AssemblyPath, data.FileArtifactName); }); Task("Cake.Common.Build.GitHubActionsProvider.Commands.UploadArtifact.Directory") - .Does(async () => { + .Does(async data => { + // When + await GitHubActions.Commands.UploadArtifact(data.AssemblyPath.GetDirectory(), data.DirectoryArtifactName); +}); + +Task("Cake.Common.Build.GitHubActionsProvider.Commands.DownloadArtifact") + .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Commands.UploadArtifact.File") + .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Commands.UploadArtifact.Directory") + .Does(async data => { // Given - FilePath path = typeof(ICakeContext).GetTypeInfo().Assembly.Location; - string artifactName = $"Directory_{GitHubActions.Environment.Runner.ImageOS ?? GitHubActions.Environment.Runner.OS}_{Context.Environment.Runtime.BuiltFramework.Identifier}_{Context.Environment.Runtime.BuiltFramework.Version}"; + var targetPath = Paths.Temp.Combine("./Cake.Common.Build.GitHubActionsProvider.Commands.DownloadArtifact"); + EnsureDirectoryExists(targetPath); + var targetArtifactPath = targetPath.CombineWithFilePath(data.AssemblyPath.GetFilename()); // When - await GitHubActions.Commands.UploadArtifact(path.GetDirectory(), artifactName); + await GitHubActions.Commands.DownloadArtifact(data.FileArtifactName, targetPath); + + // Then + Assert.True(System.IO.File.Exists(targetArtifactPath.FullPath), $"{targetArtifactPath.FullPath} Missing"); + Assert.True(FileHashEquals(data.AssemblyPath, targetArtifactPath), $"{data.AssemblyPath.FullPath}=={targetArtifactPath.FullPath}"); }); Task("Cake.Common.Build.GitHubActionsProvider.Environment.Runner.Architecture") @@ -88,8 +96,21 @@ if (GitHubActions.IsRunningOnGitHubActions) if (GitHubActions.Environment.Runtime.IsRuntimeAvailable) { + Setup(context => new GitHubActionsData { + AssemblyPath = typeof(ICakeContext).GetTypeInfo().Assembly.Location, + FileArtifactName = $"File_{GitHubActions.Environment.Runner.ImageOS ?? GitHubActions.Environment.Runner.OS}_{Context.Environment.Runtime.BuiltFramework.Identifier}_{Context.Environment.Runtime.BuiltFramework.Version}", + DirectoryArtifactName = $"Directory_{GitHubActions.Environment.Runner.ImageOS ?? GitHubActions.Environment.Runner.OS}_{Context.Environment.Runtime.BuiltFramework.Identifier}_{Context.Environment.Runtime.BuiltFramework.Version}" + }); + gitHubActionsProviderTask - .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Commands.UploadArtifact.File"); - gitHubActionsProviderTask - .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Commands.UploadArtifact.Directory"); + .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Commands.UploadArtifact.File") + .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Commands.UploadArtifact.Directory") + .IsDependentOn("Cake.Common.Build.GitHubActionsProvider.Commands.DownloadArtifact"); +} + +public class GitHubActionsData +{ + public FilePath AssemblyPath { get; set; } + public string FileArtifactName { get; set; } + public string DirectoryArtifactName { get; set; } } \ No newline at end of file