WIP publish parallelization + deltas

This commit is contained in:
Caelan Sayler
2024-12-22 21:44:23 +00:00
committed by Caelan
parent 75a0e5610e
commit 5c8c066217
11 changed files with 329 additions and 100 deletions

View File

@@ -0,0 +1,4 @@
@echo off
setlocal enabledelayedexpansion
cd %~dp0..
%~dp0..\..\..\build\Debug\net8.0\vpk publish -o releases

View File

@@ -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<T> RunTask<T>(string name, Func<Action<int>, Task<T>> fn)
{
try {
return await fn(x => { }).ConfigureAwait(false);
} catch (Exception ex) {
this.LogError(ex, "Error running task {0}", name);
throw;
}
}

View File

@@ -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<bool> 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;
}
}

View File

@@ -3,4 +3,5 @@
public interface IFancyConsoleProgress
{
Task RunTask(string name, Func<Action<int>, Task> fn);
Task<T> RunTask<T>(string name, Func<Action<int>, Task<T>> fn);
}

View File

@@ -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<bool> LoginAsync(VelopackLoginOptions? options, bool suppressOutput, CancellationToken cancellationToken);
Task LogoutAsync(VelopackServiceOptions? options, CancellationToken cancellationToken);
Task<Profile?> 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<int>? 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<bool> 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<Profile>(endpoint, cancellationToken);
var client = GetHttpClient();
return await client.GetFromJsonAsync<Profile>(endpoint, cancellationToken);
}
public async Task<string> 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<string> 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<Task>();
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<int> 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<ReleaseGroup> 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<ReleaseGroup>(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<int> 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<ReleaseGroup>(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<AuthConfiguration> 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<AuthConfiguration>(endpoint, cancellationToken);
var client = GetHttpClient();
var authConfig = await client.GetFromJsonAsync<AuthConfiguration>(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<AuthenticationResult?> AcquireInteractiveAsync(IPublicClientApplication pca, AuthConfiguration authConfiguration, CancellationToken cancellationToken)
private static async Task<AuthenticationResult?> 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<AuthenticationResult?> 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<string, string>("vpk.client-id", authConfiguration.ClientId ?? ""),
new KeyValuePair<string, string>("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<string, string>("vpk.client-id", authConfiguration.ClientId ?? ""),
new KeyValuePair<string, string>("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,
}
}
}

View File

@@ -19,6 +19,8 @@
<PackageReference Include="Microsoft.Identity.Client" Version="4.66.2" />
<PackageReference Include="Microsoft.Identity.Client.Broker" Version="4.66.2" />
<PackageReference Include="Microsoft.Identity.Client.Extensions.Msal" Version="4.66.2" />
<PackageReference Include="Microsoft.Extensions.Http" Version="9.0.0" />
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="6.0.0" Aliases="HttpFormatting" />
<PackageReference Include="NuGet.Protocol" Version="6.12.1" />
</ItemGroup>

View File

@@ -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<string>(v => ReleaseDirectory = v, "--releaseDir")
AddOption<string>(v => ReleaseDirectory = v, "-o", "--outputDir")
.SetDescription("The directory containing the Velopack release files.")
.SetArgumentHelpName("DIR")
.SetRequired();
AddOption<string>(v => Channel = v, "-c", "--channel")
.SetDescription("The channel for the release");
.SetArgumentHelpName("NAME")
.SetDescription("The channel used for the release.");
AddOption<bool>(v => NoWaitForLive = v, "--noWaitForLive")
.SetDescription("Skip waiting for the release to finish processing and go live.");
}
}
}

View File

@@ -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);
}
}

View File

@@ -10,4 +10,6 @@ public sealed class PublishOptions : VelopackServiceOptions
public string ReleaseDirectory { get; set; } = "";
public string? Channel { get; set; }
public bool NoWaitForLive { get; set; }
}

View File

@@ -59,5 +59,13 @@ public class BasicConsole : IFancyConsole
await Task.Run(() => fn(_ => { }));
_logger.Info("Complete: " + name);
}
public async Task<T> RunTask<T>(string name, Func<Action<int>, Task<T>> fn)
{
_logger.Info("Starting: " + name);
var result = await Task.Run(() => fn(_ => { }));
_logger.Info("Complete: " + name);
return result;
}
}
}

View File

@@ -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<Action<int>, Task> fn)
{
await RunTask(
name,
async (progress) => {
await fn(progress);
return true;
});
}
public async Task<T> RunTask<T>(string name, Func<Action<int>, Task<T>> 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;
}
}
}
}