From 5c8c06621772e17e84acb018d35cce41409b8c22 Mon Sep 17 00:00:00 2001 From: Caelan Sayler Date: Sun, 22 Dec 2024 21:44:23 +0000 Subject: [PATCH] WIP publish parallelization + deltas --- .../CSharpAvalonia/dev-scripts/publish.bat | 4 + src/vpk/Velopack.Build/MSBuildLogger.cs | 12 + src/vpk/Velopack.Build/PublishTask.cs | 41 ++- .../Abstractions/IFancyConsoleProgress.cs | 1 + .../Flow/VelopackFlowServiceClient.cs | 324 ++++++++++++++---- .../Velopack.Packaging.csproj | 2 + .../Commands/Flow/PublishCommand.cs | 12 +- .../Commands/Flow/PublishCommandRunner.cs | 2 +- .../Commands/Flow/PublishOptions.cs | 2 + src/vpk/Velopack.Vpk/Logging/BasicConsole.cs | 8 + .../Velopack.Vpk/Logging/SpectreConsole.cs | 21 +- 11 files changed, 329 insertions(+), 100 deletions(-) create mode 100644 samples/CSharpAvalonia/dev-scripts/publish.bat diff --git a/samples/CSharpAvalonia/dev-scripts/publish.bat b/samples/CSharpAvalonia/dev-scripts/publish.bat new file mode 100644 index 00000000..f5703699 --- /dev/null +++ b/samples/CSharpAvalonia/dev-scripts/publish.bat @@ -0,0 +1,4 @@ +@echo off +setlocal enabledelayedexpansion +cd %~dp0.. +%~dp0..\..\..\build\Debug\net8.0\vpk publish -o releases \ No newline at end of file diff --git a/src/vpk/Velopack.Build/MSBuildLogger.cs b/src/vpk/Velopack.Build/MSBuildLogger.cs index 1f5bd538..fdda6799 100644 --- a/src/vpk/Velopack.Build/MSBuildLogger.cs +++ b/src/vpk/Velopack.Build/MSBuildLogger.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Threading.Tasks; using Microsoft.Build.Framework; using Microsoft.Build.Utilities; using Microsoft.Extensions.Logging; @@ -29,6 +30,17 @@ public class MSBuildLogger(TaskLoggingHelper loggingHelper) : ILogger, IFancyCon await fn(x => { }).ConfigureAwait(false); } catch (Exception ex) { this.LogError(ex, "Error running task {0}", name); + throw; + } + } + + public async Task RunTask(string name, Func, Task> fn) + { + try { + return await fn(x => { }).ConfigureAwait(false); + } catch (Exception ex) { + this.LogError(ex, "Error running task {0}", name); + throw; } } diff --git a/src/vpk/Velopack.Build/PublishTask.cs b/src/vpk/Velopack.Build/PublishTask.cs index 13537235..08d11476 100644 --- a/src/vpk/Velopack.Build/PublishTask.cs +++ b/src/vpk/Velopack.Build/PublishTask.cs @@ -24,27 +24,30 @@ public class PublishTask : MSBuildAsyncTask public string? Channel { get; set; } public string? ApiKey { get; set; } + + public bool NoWaitForLive { get; set; } protected override async Task ExecuteAsync(CancellationToken cancellationToken) { - //System.Diagnostics.Debugger.Launch(); - IVelopackFlowServiceClient client = new VelopackFlowServiceClient(HttpClient, Logger); - if (!await client.LoginAsync(new() { - AllowDeviceCodeFlow = false, - AllowInteractiveLogin = false, - VelopackBaseUrl = ServiceUrl, - ApiKey = ApiKey - }, false, cancellationToken).ConfigureAwait(false)) { - Logger.LogWarning("Not logged into Velopack Flow service, skipping publish. Please run vpk login."); - return true; - } - - // todo: currently it's not possible to cross-compile for different OSes using Velopack.Build - var targetOs = VelopackRuntimeInfo.SystemOs; - - await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, ServiceUrl, targetOs, cancellationToken) - .ConfigureAwait(false); - - return true; + throw new NotImplementedException(); + // //System.Diagnostics.Debugger.Launch(); + // IVelopackFlowServiceClient client = new VelopackFlowServiceClient(HttpClient, Logger, Logger); + // if (!await client.LoginAsync(new() { + // AllowDeviceCodeFlow = false, + // AllowInteractiveLogin = false, + // VelopackBaseUrl = ServiceUrl, + // ApiKey = ApiKey + // }, false, cancellationToken).ConfigureAwait(false)) { + // Logger.LogWarning("Not logged into Velopack Flow service, skipping publish. Please run vpk login."); + // return true; + // } + // + // // todo: currently it's not possible to cross-compile for different OSes using Velopack.Build + // var targetOs = VelopackRuntimeInfo.SystemOs; + // + // await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, ServiceUrl, targetOs, NoWaitForLive, cancellationToken) + // .ConfigureAwait(false); + // + // return true; } } diff --git a/src/vpk/Velopack.Packaging/Abstractions/IFancyConsoleProgress.cs b/src/vpk/Velopack.Packaging/Abstractions/IFancyConsoleProgress.cs index cf89767b..b196ffba 100644 --- a/src/vpk/Velopack.Packaging/Abstractions/IFancyConsoleProgress.cs +++ b/src/vpk/Velopack.Packaging/Abstractions/IFancyConsoleProgress.cs @@ -3,4 +3,5 @@ public interface IFancyConsoleProgress { Task RunTask(string name, Func, Task> fn); + Task RunTask(string name, Func, Task> fn); } diff --git a/src/vpk/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs b/src/vpk/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs index 0bf00e1e..d25a68d6 100644 --- a/src/vpk/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs +++ b/src/vpk/Velopack.Packaging/Flow/VelopackFlowServiceClient.cs @@ -1,11 +1,17 @@ -using Microsoft.Identity.Client; +extern alias HttpFormatting; +using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; using NuGet.Versioning; using Microsoft.Extensions.Logging; using System.Text; +using System.Net.Http.Headers; +using Velopack.NuGet; +using Velopack.Packaging.Abstractions; +using Velopack.Util; #if NET6_0_OR_GREATER using System.Net.Http.Json; + #else using System.Net.Http; #endif @@ -16,6 +22,7 @@ namespace Velopack.Packaging.Flow; public interface IVelopackFlowServiceClient { Task LoginAsync(VelopackLoginOptions? options, bool suppressOutput, CancellationToken cancellationToken); + Task LogoutAsync(VelopackServiceOptions? options, CancellationToken cancellationToken); Task GetProfileAsync(VelopackServiceOptions? options, CancellationToken cancellationToken); @@ -24,17 +31,43 @@ public interface IVelopackFlowServiceClient string method, string? body, CancellationToken cancellationToken); - Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, RuntimeOs os, CancellationToken cancellationToken); + + Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, RuntimeOs os, + bool noWaitForLive, CancellationToken cancellationToken); } -public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : IVelopackFlowServiceClient +public class VelopackFlowServiceClient( + IHttpMessageHandlerFactory HttpMessageHandlerFactory, + ILogger Logger, + IFancyConsole Console) : IVelopackFlowServiceClient { private static readonly string[] Scopes = ["openid", "offline_access"]; - public bool HasAuthentication => HttpClient.DefaultRequestHeaders.Authorization is not null; + private AuthenticationHeaderValue Authorization = null; private AuthConfiguration? AuthConfiguration { get; set; } + private HttpClient GetHttpClient(Action? progress = null) + { + HttpMessageHandler primaryHandler = HttpMessageHandlerFactory.CreateHandler("flow"); + + if (progress != null) { + var ph = new HttpFormatting::System.Net.Http.Handlers.ProgressMessageHandler(primaryHandler); + ph.HttpSendProgress += (_, args) => { + progress(args.ProgressPercentage); + // Console.WriteLine($"upload progress: {((double)args.BytesTransferred / args.TotalBytes) * 100.0}"); + }; + ph.HttpReceiveProgress += (_, args) => { + progress(args.ProgressPercentage); + }; + primaryHandler = ph; + } + + var client = new HttpClient(primaryHandler); + client.DefaultRequestHeaders.Authorization = Authorization; + return client; + } + public async Task LoginAsync(VelopackLoginOptions? options, bool suppressOutput, CancellationToken cancellationToken) { options ??= new VelopackLoginOptions(); @@ -43,41 +76,39 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : } var authConfiguration = await GetAuthConfigurationAsync(options, cancellationToken); - var pca = await BuildPublicApplicationAsync(authConfiguration); if (!string.IsNullOrWhiteSpace(options.ApiKey)) { - HttpClient.DefaultRequestHeaders.Authorization = new(HmacHelper.HmacScheme, options.ApiKey); - var profile = await GetProfileAsync(options, cancellationToken); - if (!suppressOutput) { - Logger.LogInformation("{UserName} logged into Velopack with API key", profile?.GetDisplayName()); - } - return true; + Authorization = new(HmacHelper.HmacScheme, options.ApiKey); } else { AuthenticationResult? rv = null; if (options.AllowCacheCredentials) { rv = await AcquireSilentlyAsync(pca, cancellationToken); } + if (rv is null && options.AllowInteractiveLogin) { rv = await AcquireInteractiveAsync(pca, authConfiguration, cancellationToken); } + if (rv is null && options.AllowDeviceCodeFlow) { rv = await AcquireByDeviceCodeAsync(pca, cancellationToken); } - if (rv != null) { - HttpClient.DefaultRequestHeaders.Authorization = new("Bearer", rv.IdToken ?? rv.AccessToken); - var profile = await GetProfileAsync(options, cancellationToken); - - if (!suppressOutput) { - Logger.LogInformation("{UserName} logged into Velopack", profile?.GetDisplayName()); - } - return true; - } else { + if (rv is null) { Logger.LogError("Failed to login to Velopack"); return false; } + + Authorization = new("Bearer", rv.IdToken ?? rv.AccessToken); } + + var profile = await GetProfileAsync(options, cancellationToken); + + if (!suppressOutput) { + Logger.LogInformation("{UserName} logged into Velopack", profile?.GetDisplayName()); + } + + return true; } public async Task LogoutAsync(VelopackServiceOptions? options, CancellationToken cancellationToken) @@ -91,6 +122,7 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : await pca.RemoveAsync(account); Logger.LogInformation("Logged out of {Username}", account.Username); } + Logger.LogInformation("Cleared saved login(s) for Velopack"); } @@ -99,7 +131,8 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : AssertAuthenticated(); var endpoint = GetEndpoint("v1/user/profile", options?.VelopackBaseUrl); - return await HttpClient.GetFromJsonAsync(endpoint, cancellationToken); + var client = GetHttpClient(); + return await client.GetFromJsonAsync(endpoint, cancellationToken); } public async Task InvokeEndpointAsync( @@ -116,7 +149,9 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : if (body is not null) { request.Content = new StringContent(body, Encoding.UTF8, "application/json"); } - HttpResponseMessage response = await HttpClient.SendAsync(request, cancellationToken); + + var client = GetHttpClient(); + HttpResponseMessage response = await client.SendAsync(request, cancellationToken); #if NET6_0_OR_GREATER string responseBody = await response.Content.ReadAsStringAsync(cancellationToken); @@ -127,18 +162,19 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : if (response.IsSuccessStatusCode) { return responseBody; } else { - throw new InvalidOperationException($"Failed to invoke endpoint {endpointUri} with status code {response.StatusCode}{Environment.NewLine}{responseBody}"); + throw new InvalidOperationException( + $"Failed to invoke endpoint {endpointUri} with status code {response.StatusCode}{Environment.NewLine}{responseBody}"); } } public async Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl, - RuntimeOs os, CancellationToken cancellationToken) + RuntimeOs os, bool noWaitForLive, CancellationToken cancellationToken) { AssertAuthenticated(); channel ??= ReleaseEntryHelper.GetDefaultChannel(os); ReleaseEntryHelper helper = new(releaseDirectory, channel, Logger, os); - var latestAssets = helper.GetLatestAssets().ToList(); + var latestAssets = helper.GetLatestAssets().Where(f => f.Type != VelopackAssetType.Delta).ToList(); List installers = []; @@ -166,6 +202,7 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : Logger.LogError("No package ID found in release directory {ReleaseDirectory}", releaseDirectory); return; } + if (version is null) { Logger.LogError("No version found in release directory {ReleaseDirectory}", releaseDirectory); return; @@ -173,21 +210,151 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : Logger.LogInformation("Uploading {AssetCount} assets to Velopack Flow ({ServiceUrl})", latestAssets.Count + installers.Count, serviceUrl); - ReleaseGroup releaseGroup = await CreateReleaseGroupAsync(packageId, version, channel, serviceUrl, cancellationToken); + await Console.ExecuteProgressAsync( + async (progress) => { + ReleaseGroup releaseGroup = await progress.RunTask( + "Preparing to upload assets", + async (report) => { + report(-1); + var result = await CreateReleaseGroupAsync(packageId, version, channel, serviceUrl, cancellationToken); + report(100); + return result; + }); - foreach (var assetFileName in files) { - await UploadReleaseAssetAsync(releaseDirectory, assetFileName, serviceUrl, releaseGroup.Id, FileType.Release, cancellationToken).ConfigureAwait(false); + var backgroundTasks = new List(); + foreach (var assetFileName in files) { + backgroundTasks.Add( + progress.RunTask( + $"Uploading {Path.GetFileName(assetFileName)}", + async (report) => { + await UploadReleaseAssetAsync( + releaseDirectory, + assetFileName, + serviceUrl, + releaseGroup.Id, + FileType.Release, + report, + cancellationToken); + }) + ); + } - Logger.LogInformation("Uploaded {FileName} to Velopack Flow", assetFileName); + using var _1 = TempUtil.GetTempDirectory(out var deltaGenTempDir); + var prevVersion = Path.Combine(deltaGenTempDir, "prev.nupkg"); + + var prevZip = await progress.RunTask( + $"Downloading delta base for {version}", + async (report) => { + await DownloadLatestRelease(packageId, channel, serviceUrl, prevVersion, report, cancellationToken); + return new ZipPackage(prevVersion); + }); + + if (prevZip.Version >= version) { + throw new InvalidOperationException( + $"Latest version in channel {channel} is greater than or equal to local (remote={prevZip.Version}, local={version})"); + } + + var suggestedDeltaName = ReleaseEntryHelper.GetSuggestedReleaseName(packageId, version.ToFullString(), channel, true, RuntimeOs.Unknown); + var deltaPath = Path.Combine(releaseDirectory, suggestedDeltaName); + + await progress.RunTask( + $"Building delta {prevZip.Version} -> {version}", + (report) => { + var delta = new DeltaPackageBuilder(Logger); + var pOld = new ReleasePackage(prevVersion); + var pNew = new ReleasePackage(releaseDirectory); + delta.CreateDeltaPackage(pOld, pNew, deltaPath, DeltaMode.BestSpeed, report); + return Task.CompletedTask; + }); + + backgroundTasks.Add( + progress.RunTask( + $"Uploading {Path.GetFileName(deltaPath)}", + async (report) => { + await UploadReleaseAssetAsync( + releaseDirectory, + deltaPath, + serviceUrl, + releaseGroup.Id, + FileType.Release, + report, + cancellationToken); + }) + ); + + await Task.WhenAll(backgroundTasks); + + var publishedGroup = await progress.RunTask( + $"Publishing release {version}", + async (report) => { + report(-1); + var result = await PublishReleaseGroupAsync(releaseGroup, serviceUrl, cancellationToken); + report(100); + return result; + }); + + if (!noWaitForLive) { + await progress.RunTask( + "Waiting for release to be live", + async (report) => { + report(-1); + await WaitUntilReleaseGroupLive(publishedGroup.Id, serviceUrl, cancellationToken); + }); + } + }); + + // ReleaseGroup releaseGroup = await CreateReleaseGroupAsync(packageId, version, channel, serviceUrl, cancellationToken); + // + // foreach (var assetFileName in files) { + // await UploadReleaseAssetAsync(releaseDirectory, assetFileName, serviceUrl, releaseGroup.Id, FileType.Release, cancellationToken) + // .ConfigureAwait(false); + // + // Logger.LogInformation("Uploaded {FileName} to Velopack Flow", assetFileName); + // } + // + // foreach (var installerFile in installers) { + // await UploadReleaseAssetAsync(releaseDirectory, installerFile, serviceUrl, releaseGroup.Id, FileType.Installer, cancellationToken) + // .ConfigureAwait(false); + // + // Logger.LogInformation("Uploaded {FileName} installer to Velopack Flow", installerFile); + // } + // + // await PublishReleaseGroupAsync(releaseGroup, serviceUrl, cancellationToken); + + // TODO wait for published + } + + private async Task DownloadLatestRelease(string packageId, string channel, string? velopackBaseUrl, string localPath, + Action progress, CancellationToken cancellationToken) + { + var client = GetHttpClient(progress); + var endpoint = GetEndpoint($"v1/download/{packageId}/{channel}", velopackBaseUrl); + + using var fs = File.Create(localPath); + + var response = await client.GetAsync(endpoint, cancellationToken); + response.EnsureSuccessStatusCode(); + +#if NET6_0_OR_GREATER + await response.Content.CopyToAsync(fs, cancellationToken); +#else + await response.Content.CopyToAsync(fs); +#endif + } + + + private async Task WaitUntilReleaseGroupLive(Guid releaseGroupId, string? velopackBaseUrl, CancellationToken cancellationToken) + { + var client = GetHttpClient(); + var endpoint = GetEndpoint($"v1/releaseGroups/{releaseGroupId}", velopackBaseUrl); + + for (int i = 0; i < 120; i++) { + var response = await client.GetAsync(endpoint, cancellationToken); + response.EnsureSuccessStatusCode(); + var content = await response.Content.ReadAsStringAsync(cancellationToken); + Console.WriteLine(content); + await Task.Delay(1000, cancellationToken); } - - foreach (var installerFile in installers) { - await UploadReleaseAssetAsync(releaseDirectory, installerFile, serviceUrl, releaseGroup.Id, FileType.Installer, cancellationToken).ConfigureAwait(false); - - Logger.LogInformation("Uploaded {FileName} installer to Velopack Flow", installerFile); - } - - await PublishReleaseGroupAsync(releaseGroup, serviceUrl, cancellationToken); } private async Task CreateReleaseGroupAsync( @@ -200,23 +367,25 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : Version = version.ToNormalizedString() }; + var client = GetHttpClient(); var endpoint = GetEndpoint("v1/releaseGroups/create", velopackBaseUrl); - var response = await HttpClient.PostAsJsonAsync(endpoint, request, cancellationToken); + var response = await client.PostAsJsonAsync(endpoint, request, cancellationToken); if (!response.IsSuccessStatusCode) { string content = await response.Content.ReadAsStringAsync(cancellationToken); - throw new InvalidOperationException($"Failed to create release group with version {version.ToNormalizedString()}{Environment.NewLine}Response status code: {response.StatusCode}{Environment.NewLine}{content}"); + throw new InvalidOperationException( + $"Failed to create release group with version {version.ToNormalizedString()}" + + $"{Environment.NewLine}Response status code: {response.StatusCode}{Environment.NewLine}{content}"); } return await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken) - ?? throw new InvalidOperationException($"Failed to create release group with version {version.ToNormalizedString()}"); + ?? throw new InvalidOperationException($"Failed to create release group with version {version.ToNormalizedString()}"); } - private async Task UploadReleaseAssetAsync(string releaseDirectory, string fileName, - string? serviceUrl, Guid releaseGroupId, FileType fileType, CancellationToken cancellationToken) + private async Task UploadReleaseAssetAsync(string releaseDirectory, string fileName, string? velopackBaseUrl, Guid releaseGroupId, + FileType fileType, Action progress, CancellationToken cancellationToken) { - using var formData = new MultipartFormDataContent - { + using var formData = new MultipartFormDataContent { { new StringContent(releaseGroupId.ToString()), "ReleaseGroupId" }, { new StringContent(fileType.ToString()), "FileType" } }; @@ -228,9 +397,10 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : using var fileContent = new StreamContent(fileStream); formData.Add(fileContent, "File", fileName); - var endpoint = GetEndpoint("v1/releases/upload", serviceUrl); + var endpoint = GetEndpoint("v1/releases/upload", velopackBaseUrl); - var response = await HttpClient.PostAsync(endpoint, formData, cancellationToken); + var client = GetHttpClient(progress); + var response = await client.PostAsync(endpoint, formData, cancellationToken); response.EnsureSuccessStatusCode(); } @@ -241,16 +411,18 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : State = ReleaseGroupState.Published }; + var client = GetHttpClient(); var endpoint = GetEndpoint($"v1/releaseGroups/{releaseGroup.Id}", velopackBaseUrl); - var response = await HttpClient.PutAsJsonAsync(endpoint, request, cancellationToken); + var response = await client.PutAsJsonAsync(endpoint, request, cancellationToken); if (!response.IsSuccessStatusCode) { string content = await response.Content.ReadAsStringAsync(cancellationToken); - throw new InvalidOperationException($"Failed to publish release group with id {releaseGroup.Id}.{Environment.NewLine}Response status code: {response.StatusCode}{Environment.NewLine}{content}"); + throw new InvalidOperationException( + $"Failed to publish release group with id {releaseGroup.Id}.{Environment.NewLine}Response status code: {response.StatusCode}{Environment.NewLine}{content}"); } return await response.Content.ReadFromJsonAsync(cancellationToken: cancellationToken) - ?? throw new InvalidOperationException($"Failed to publish release group with id {releaseGroup.Id}"); + ?? throw new InvalidOperationException($"Failed to publish release group with id {releaseGroup.Id}"); } private async Task GetAuthConfigurationAsync(VelopackServiceOptions? options, CancellationToken cancellationToken) @@ -260,7 +432,8 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : var endpoint = GetEndpoint("v1/auth/config", options); - var authConfig = await HttpClient.GetFromJsonAsync(endpoint, cancellationToken); + var client = GetHttpClient(); + var authConfig = await client.GetFromJsonAsync(endpoint, cancellationToken); if (authConfig is null) throw new Exception("Failed to get auth configuration."); if (authConfig.B2CAuthority is null) @@ -285,7 +458,7 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : private void AssertAuthenticated() { - if (!HasAuthentication) { + if (Authorization is null) { throw new InvalidOperationException($"{nameof(VelopackFlowServiceClient)} has not been authenticated, call {nameof(LoginAsync)} first."); } } @@ -303,24 +476,28 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : // No token found in the cache or Azure AD insists that a form interactive auth is required (e.g. the tenant admin turned on MFA) } } + return null; } - private static async Task AcquireInteractiveAsync(IPublicClientApplication pca, AuthConfiguration authConfiguration, CancellationToken cancellationToken) + private static async Task AcquireInteractiveAsync(IPublicClientApplication pca, AuthConfiguration authConfiguration, + CancellationToken cancellationToken) { try { return await pca.AcquireTokenInteractive(Scopes) - .WithB2CAuthority(authConfiguration.B2CAuthority) - .ExecuteAsync(cancellationToken); + .WithB2CAuthority(authConfiguration.B2CAuthority) + .ExecuteAsync(cancellationToken); } catch (MsalException) { } + return null; } private async Task AcquireByDeviceCodeAsync(IPublicClientApplication pca, CancellationToken cancellationToken) { try { - var result = await pca.AcquireTokenWithDeviceCode(Scopes, + var result = await pca.AcquireTokenWithDeviceCode( + Scopes, deviceCodeResult => { // This will print the message on the logger which tells the user where to go sign-in using // a separate browser and the code to enter once they sign in. @@ -340,6 +517,7 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : return result; } catch (MsalException) { } + return null; } @@ -350,29 +528,29 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : var vpkPath = Path.Combine(userPath, ".vpk"); var storageProperties = - new StorageCreationPropertiesBuilder("creds.bin", vpkPath) - .WithLinuxKeyring( - schemaName: "com.velopack.app", - collection: "default", - secretLabel: "Credentials for Velopack", - new KeyValuePair("vpk.client-id", authConfiguration.ClientId ?? ""), - new KeyValuePair("vpk.version", "v1") - ) - .WithMacKeyChain( - serviceName: "velopack", - accountName: "vpk") - .Build(); + new StorageCreationPropertiesBuilder("creds.bin", vpkPath) + .WithLinuxKeyring( + schemaName: "com.velopack.app", + collection: "default", + secretLabel: "Credentials for Velopack", + new KeyValuePair("vpk.client-id", authConfiguration.ClientId ?? ""), + new KeyValuePair("vpk.version", "v1") + ) + .WithMacKeyChain( + serviceName: "velopack", + accountName: "vpk") + .Build(); var cacheHelper = await MsalCacheHelper.CreateAsync(storageProperties); var pca = PublicClientApplicationBuilder - .Create(authConfiguration.ClientId) - .WithB2CAuthority(authConfiguration.B2CAuthority) - .WithRedirectUri(authConfiguration.RedirectUri) + .Create(authConfiguration.ClientId) + .WithB2CAuthority(authConfiguration.B2CAuthority) + .WithRedirectUri(authConfiguration.RedirectUri) #if DEBUG .WithLogging((Microsoft.Identity.Client.LogLevel level, string message, bool containsPii) => System.Console.WriteLine($"[{level}]: {message}"), enablePiiLogging: true, enableDefaultPlatformLogging: true) #endif - .WithClientName("velopack") - .Build(); + .WithClientName("velopack") + .Build(); cacheHelper.RegisterCache(pca.UserTokenCache); return pca; @@ -384,4 +562,4 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : Release, Installer, } -} +} \ No newline at end of file diff --git a/src/vpk/Velopack.Packaging/Velopack.Packaging.csproj b/src/vpk/Velopack.Packaging/Velopack.Packaging.csproj index f3345fd1..5f68c98e 100644 --- a/src/vpk/Velopack.Packaging/Velopack.Packaging.csproj +++ b/src/vpk/Velopack.Packaging/Velopack.Packaging.csproj @@ -19,6 +19,8 @@ + + diff --git a/src/vpk/Velopack.Vpk/Commands/Flow/PublishCommand.cs b/src/vpk/Velopack.Vpk/Commands/Flow/PublishCommand.cs index 8321686c..c774996f 100644 --- a/src/vpk/Velopack.Vpk/Commands/Flow/PublishCommand.cs +++ b/src/vpk/Velopack.Vpk/Commands/Flow/PublishCommand.cs @@ -7,15 +7,21 @@ public class PublishCommand : VelopackServiceCommand public string? Channel { get; set; } + public bool NoWaitForLive { get; set; } public PublishCommand() : base("publish", "Uploads a release to Velopack's hosted service") { - AddOption(v => ReleaseDirectory = v, "--releaseDir") + AddOption(v => ReleaseDirectory = v, "-o", "--outputDir") .SetDescription("The directory containing the Velopack release files.") + .SetArgumentHelpName("DIR") .SetRequired(); AddOption(v => Channel = v, "-c", "--channel") - .SetDescription("The channel for the release"); + .SetArgumentHelpName("NAME") + .SetDescription("The channel used for the release."); + + AddOption(v => NoWaitForLive = v, "--noWaitForLive") + .SetDescription("Skip waiting for the release to finish processing and go live."); } -} +} \ No newline at end of file diff --git a/src/vpk/Velopack.Vpk/Commands/Flow/PublishCommandRunner.cs b/src/vpk/Velopack.Vpk/Commands/Flow/PublishCommandRunner.cs index 1ee02acb..f58a424e 100644 --- a/src/vpk/Velopack.Vpk/Commands/Flow/PublishCommandRunner.cs +++ b/src/vpk/Velopack.Vpk/Commands/Flow/PublishCommandRunner.cs @@ -20,6 +20,6 @@ public class PublishCommandRunner(IVelopackFlowServiceClient Client) : ICommand< } await Client.UploadLatestReleaseAssetsAsync(options.Channel, options.ReleaseDirectory, - options.VelopackBaseUrl, options.TargetOs, token); + options.VelopackBaseUrl, options.TargetOs, options.NoWaitForLive, token); } } diff --git a/src/vpk/Velopack.Vpk/Commands/Flow/PublishOptions.cs b/src/vpk/Velopack.Vpk/Commands/Flow/PublishOptions.cs index 8e051320..061b5cab 100644 --- a/src/vpk/Velopack.Vpk/Commands/Flow/PublishOptions.cs +++ b/src/vpk/Velopack.Vpk/Commands/Flow/PublishOptions.cs @@ -10,4 +10,6 @@ public sealed class PublishOptions : VelopackServiceOptions public string ReleaseDirectory { get; set; } = ""; public string? Channel { get; set; } + + public bool NoWaitForLive { get; set; } } \ No newline at end of file diff --git a/src/vpk/Velopack.Vpk/Logging/BasicConsole.cs b/src/vpk/Velopack.Vpk/Logging/BasicConsole.cs index 8ff8c262..87662b93 100644 --- a/src/vpk/Velopack.Vpk/Logging/BasicConsole.cs +++ b/src/vpk/Velopack.Vpk/Logging/BasicConsole.cs @@ -59,5 +59,13 @@ public class BasicConsole : IFancyConsole await Task.Run(() => fn(_ => { })); _logger.Info("Complete: " + name); } + + public async Task RunTask(string name, Func, Task> fn) + { + _logger.Info("Starting: " + name); + var result = await Task.Run(() => fn(_ => { })); + _logger.Info("Complete: " + name); + return result; + } } } diff --git a/src/vpk/Velopack.Vpk/Logging/SpectreConsole.cs b/src/vpk/Velopack.Vpk/Logging/SpectreConsole.cs index 68ed47ef..73cd261c 100644 --- a/src/vpk/Velopack.Vpk/Logging/SpectreConsole.cs +++ b/src/vpk/Velopack.Vpk/Logging/SpectreConsole.cs @@ -28,13 +28,14 @@ public class SpectreConsole : IFancyConsole .AutoRefresh(true) .AutoClear(false) .HideCompleted(false) - .Columns(new ProgressColumn[] { + .Columns( + new ProgressColumn[] { new SpinnerColumn(), new TaskDescriptionColumn(), new ProgressBarColumn(), new PercentageColumn(), new ElapsedTimeColumn(), - }) + }) .StartAsync(async ctx => await action(new Progress(logger, ctx))); logger.Info($"[bold]Finished in {DateTime.UtcNow - start}.[/]"); } @@ -86,6 +87,7 @@ public class SpectreConsole : IFancyConsole for (int i = 0; i < numColumns; i++) { table.AddColumn($"Column {i}"); } + table.HideHeaders(); } @@ -110,6 +112,16 @@ public class SpectreConsole : IFancyConsole } public async Task RunTask(string name, Func, Task> fn) + { + await RunTask( + name, + async (progress) => { + await fn(progress); + return true; + }); + } + + public async Task RunTask(string name, Func, Task> fn) { _logger.Log(LogLevel.Debug, "Starting: " + name); @@ -126,11 +138,12 @@ public class SpectreConsole : IFancyConsole } } - await Task.Run(() => fn(progress)).ConfigureAwait(false); + var result = await Task.Run(() => fn(progress)).ConfigureAwait(false); task.IsIndeterminate = false; task.StopTask(); _logger.Log(LogLevel.Debug, $"[bold]Complete: {name}[/]"); + return result; } } -} +} \ No newline at end of file