Fix signature change with profile endpoint

Fixing up the publish command to work like the MSBuild task

This corrects a lot of drift that had occurred between the CLI commands and the MSBuild task.
This commit is contained in:
Kevin Bost
2024-05-13 23:58:54 -07:00
parent d702cc0510
commit b3c425436a
13 changed files with 145 additions and 185 deletions

View File

@@ -1,6 +1,4 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
@@ -13,7 +11,6 @@ internal class HmacAuthHttpClientHandler : HttpClientHandler
{
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
Debugger.Launch();
if (request.Headers.Authorization?.Scheme == HmacHelper.HmacScheme &&
request.Headers.Authorization.Parameter is { } authParameter &&
authParameter.Split(':') is var keyParts &&

View File

@@ -1,12 +1,7 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http;
using System.Threading.Tasks;
using Microsoft.Build.Framework;
using Microsoft.Extensions.Logging;
using NuGet.Versioning;
using Velopack.Packaging;
using Velopack.Packaging.Flow;
namespace Velopack.Build;
@@ -22,14 +17,12 @@ public class PublishTask : MSBuildAsyncTask
public string? Channel { get; set; }
public string? Version { get; set; }
public string? ApiKey { get; set; }
protected override async Task<bool> ExecuteAsync()
{
//System.Diagnostics.Debugger.Launch();
VelopackFlowServiceClient client = new(HttpClient, Logger);
IVelopackFlowServiceClient client = new VelopackFlowServiceClient(HttpClient, Logger);
if (!await client.LoginAsync(new() {
AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false,
@@ -40,61 +33,9 @@ public class PublishTask : MSBuildAsyncTask
return true;
}
Channel ??= ReleaseEntryHelper.GetDefaultChannel(VelopackRuntimeInfo.SystemOs);
ReleaseEntryHelper helper = new(ReleaseDirectory, Channel, Logger);
var latestAssets = helper.GetLatestAssets().ToList();
await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, ServiceUrl)
.ConfigureAwait(false);
List<string> installers = [];
List<string> files = latestAssets.Select(x => x.FileName).ToList();
string? packageId = null;
SemanticVersion? version = null;
if (latestAssets.Count > 0) {
packageId = latestAssets[0].PackageId;
version = latestAssets[0].Version;
if (VelopackRuntimeInfo.IsWindows || VelopackRuntimeInfo.IsOSX) {
var setupName = ReleaseEntryHelper.GetSuggestedSetupName(packageId, Channel);
if (File.Exists(Path.Combine(ReleaseDirectory, setupName))) {
installers.Add(setupName);
}
}
var portableName = ReleaseEntryHelper.GetSuggestedPortableName(packageId, Channel);
if (File.Exists(Path.Combine(ReleaseDirectory, portableName))) {
installers.Add(portableName);
}
}
Logger.LogInformation("Preparing to upload {AssetCount} assets to Velopack ({ServiceUrl})", latestAssets.Count + installers.Count, ServiceUrl);
foreach (var assetFileName in files) {
var latestPath = Path.Combine(ReleaseDirectory, assetFileName);
using var fileStream = File.OpenRead(latestPath);
var options = new UploadOptions(fileStream, assetFileName, Channel) {
VelopackBaseUrl = ServiceUrl
};
await client.UploadReleaseAssetAsync(options).ConfigureAwait(false);
Logger.LogInformation("Uploaded {FileName} to Velopack", assetFileName);
}
foreach (var installerFile in installers) {
var latestPath = Path.Combine(ReleaseDirectory, installerFile);
using var fileStream = File.OpenRead(latestPath);
var options = new UploadInstallerOptions(packageId!, version!, fileStream, installerFile, Channel) {
VelopackBaseUrl = ServiceUrl
};
await client.UploadInstallerAssetAsync(options).ConfigureAwait(false);
Logger.LogInformation("Uploaded {FileName} installer to Velopack", installerFile);
}
return true;
}
}

View File

@@ -1,62 +0,0 @@
using Microsoft.Extensions.Logging;
using Velopack.NuGet;
using Velopack.Packaging;
using Velopack.Packaging.Flow;
using Velopack.Sources;
namespace Velopack.Deployment;
public class VelopackFlowDownloadOptions : RepositoryOptions
{
public string Version { get; set; }
}
public class VelopackFlowUploadOptions : VelopackFlowDownloadOptions
{
}
public class VelopackFlowRepository : SourceRepository<VelopackFlowDownloadOptions, VelopackFlowUpdateSource>, IRepositoryCanUpload<VelopackFlowUploadOptions>
{
private static HttpClient Client { get; } = new HttpClient {
BaseAddress = new Uri(VelopackServiceOptions.DefaultBaseUrl)
};
public VelopackFlowRepository(ILogger logger)
: base(logger)
{ }
public override VelopackFlowUpdateSource CreateSource(VelopackFlowDownloadOptions options)
{
return new VelopackFlowUpdateSource();
}
public async Task UploadMissingAssetsAsync(VelopackFlowUploadOptions options)
{
var helper = new ReleaseEntryHelper(options.ReleaseDir.FullName, options.Channel, Log);
var latest = helper.GetLatestAssets().ToList();
Log.Info($"Preparing to upload {latest.Count} assets to Velopack");
foreach (var asset in latest) {
var latestPath = Path.Combine(options.ReleaseDir.FullName, asset.FileName);
ZipPackage zipPackage = new(latestPath);
var semVer = options.Version ?? asset.Version.ToString();
using var formData = new MultipartFormDataContent
{
{ new StringContent(options.Channel), "Channel" },
};
using var fileStream = File.OpenRead(latestPath);
using var fileContent = new StreamContent(fileStream);
formData.Add(fileContent, "File", asset.FileName);
var response = await Client.PostAsync("api/v1/upload", formData);
response.EnsureSuccessStatusCode();
Log.Info($" Uploaded {asset.FileName} to Velopack");
}
}
}

View File

@@ -3,6 +3,12 @@
#nullable enable
public class Profile
{
public string? Name { get; set; }
public string? Id { get; set; }
public string? DisplayName { get; set; }
public string? Email { get; set; }
public string? GetDisplayName()
{
return DisplayName ?? Email ?? "<unknown>";
}
}

View File

@@ -1,8 +1,7 @@
using Microsoft.Identity.Client;
using Microsoft.Identity.Client.Extensions.Msal;
using Velopack.Packaging.Abstractions;
using NuGet.Versioning;
using Microsoft.Extensions.Logging;
#if NET6_0_OR_GREATER
using System.Net.Http.Json;
@@ -19,10 +18,11 @@ public interface IVelopackFlowServiceClient
Task LogoutAsync(VelopackServiceOptions? options = null);
Task<Profile?> GetProfileAsync(VelopackServiceOptions? options = null);
Task UploadReleaseAssetAsync(UploadOptions options);
Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl);
}
public class VelopackFlowServiceClient(HttpClient HttpClient, IConsole Console) : IVelopackFlowServiceClient
public class VelopackFlowServiceClient(HttpClient HttpClient, ILogger Logger) : IVelopackFlowServiceClient
{
private static readonly string[] Scopes = ["openid", "offline_access"];
@@ -33,7 +33,7 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, IConsole Console)
public async Task<bool> LoginAsync(VelopackLoginOptions? options = null)
{
options ??= new VelopackLoginOptions();
Console.WriteLine($"Preparing to login to Velopack ({options.VelopackBaseUrl})");
Logger.LogInformation("Preparing to login to Velopack ({ServiceUrl})", options.VelopackBaseUrl);
var authConfiguration = await GetAuthConfigurationAsync(options);
@@ -42,7 +42,7 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, IConsole Console)
if (!string.IsNullOrWhiteSpace(options.ApiKey)) {
HttpClient.DefaultRequestHeaders.Authorization = new(HmacHelper.HmacScheme, options.ApiKey);
var profile = await GetProfileAsync(options);
Console.WriteLine($"{profile?.Name} logged into Velopack with API key");
Logger.LogInformation("{UserName} logged into Velopack with API key", profile?.GetDisplayName());
return true;
} else {
AuthenticationResult? rv = null;
@@ -60,10 +60,10 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, IConsole Console)
HttpClient.DefaultRequestHeaders.Authorization = new("Bearer", rv.IdToken ?? rv.AccessToken);
var profile = await GetProfileAsync(options);
Console.WriteLine($"{profile?.Name} logged into Velopack");
Logger.LogInformation("{UserName} logged into Velopack", profile?.GetDisplayName());
return true;
} else {
Console.WriteLine("Failed to login to Velopack");
Logger.LogError("Failed to login to Velopack");
return false;
}
}
@@ -78,9 +78,9 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, IConsole Console)
// clear the cache
while ((await pca.GetAccountsAsync()).FirstOrDefault() is { } account) {
await pca.RemoveAsync(account);
Console.WriteLine($"Logged out of {account.Username}");
Logger.LogInformation("Logged out of {Username}", account.Username);
}
Console.WriteLine("Cleared saved login(s) for Velopack");
Logger.LogInformation("Cleared saved login(s) for Velopack");
}
public async Task<Profile?> GetProfileAsync(VelopackServiceOptions? options = null)
@@ -91,7 +91,65 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, IConsole Console)
return await HttpClient.GetFromJsonAsync<Profile>(endpoint);
}
public async Task UploadReleaseAssetAsync(UploadOptions options)
public async Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory, string? serviceUrl)
{
channel ??= ReleaseEntryHelper.GetDefaultChannel(VelopackRuntimeInfo.SystemOs);
ReleaseEntryHelper helper = new(releaseDirectory, channel, Logger);
var latestAssets = helper.GetLatestAssets().ToList();
List<string> installers = [];
List<string> files = latestAssets.Select(x => x.FileName).ToList();
string? packageId = null;
SemanticVersion? version = null;
if (latestAssets.Count > 0) {
packageId = latestAssets[0].PackageId;
version = latestAssets[0].Version;
if (VelopackRuntimeInfo.IsWindows || VelopackRuntimeInfo.IsOSX) {
var setupName = ReleaseEntryHelper.GetSuggestedSetupName(packageId, channel);
if (File.Exists(Path.Combine(releaseDirectory, setupName))) {
installers.Add(setupName);
}
}
var portableName = ReleaseEntryHelper.GetSuggestedPortableName(packageId, channel);
if (File.Exists(Path.Combine(releaseDirectory, portableName))) {
installers.Add(portableName);
}
}
Logger.LogInformation("Preparing to upload {AssetCount} assets to Velopack ({ServiceUrl})", latestAssets.Count + installers.Count, serviceUrl);
foreach (var assetFileName in files) {
var latestPath = Path.Combine(releaseDirectory, assetFileName);
using var fileStream = File.OpenRead(latestPath);
var options = new UploadOptions(fileStream, assetFileName, channel) {
VelopackBaseUrl = serviceUrl
};
await UploadReleaseAssetAsync(options).ConfigureAwait(false);
Logger.LogInformation("Uploaded {FileName} to Velopack", assetFileName);
}
foreach (var installerFile in installers) {
var latestPath = Path.Combine(releaseDirectory, installerFile);
using var fileStream = File.OpenRead(latestPath);
var options = new UploadInstallerOptions(packageId!, version!, fileStream, installerFile, channel) {
VelopackBaseUrl = serviceUrl
};
await UploadInstallerAssetAsync(options).ConfigureAwait(false);
Logger.LogInformation("Uploaded {FileName} installer to Velopack", installerFile);
}
}
private async Task UploadReleaseAssetAsync(UploadOptions options)
{
AssertAuthenticated();
@@ -110,7 +168,7 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, IConsole Console)
response.EnsureSuccessStatusCode();
}
public async Task UploadInstallerAssetAsync(UploadInstallerOptions options)
private async Task UploadInstallerAssetAsync(UploadInstallerOptions options)
{
AssertAuthenticated();
@@ -207,11 +265,11 @@ public class VelopackFlowServiceClient(HttpClient HttpClient, IConsole Console)
// * The timeout specified by the server for the lifetime of this code (typically ~15 minutes) has been reached
// * The developing application calls the Cancel() method on a CancellationToken sent into the method.
// If this occurs, an OperationCanceledException will be thrown (see catch below for more details).
Console.WriteLine(deviceCodeResult.Message);
Logger.LogInformation(deviceCodeResult.Message);
return Task.FromResult(0);
}).ExecuteAsync();
Console.WriteLine(result.Account.Username);
Logger.LogInformation(result.Account.Username);
return result;
} catch (MsalException) {
}

View File

@@ -12,4 +12,4 @@ public class LoginCommandRunner(IVelopackFlowServiceClient Client) : ICommand<Lo
VelopackBaseUrl = options.VelopackBaseUrl
});
}
}
}

View File

@@ -0,0 +1,21 @@
namespace Velopack.Vpk.Commands.Flow;
#nullable enable
public class PublishCommand : VelopackServiceCommand
{
public string ReleaseDirectory { get; set; } = "";
public string? Channel { get; set; }
public PublishCommand()
: base("publish", "Uploads a release to Velopack's hosted service")
{
AddOption<string>(v => ReleaseDirectory = v, "--releaseDir")
.SetDescription("The directory containing the Velopack release files.")
.SetRequired();
AddOption<string>(v => Channel = v, "-c", "--channel")
.SetDescription("The channel for the release");
}
}

View File

@@ -0,0 +1,22 @@
using Velopack.Packaging.Abstractions;
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
public class PublishCommandRunner(IVelopackFlowServiceClient Client) : ICommand<PublishOptions>
{
public async Task Run(PublishOptions options)
{
if (!await Client.LoginAsync(new VelopackLoginOptions() {
AllowCacheCredentials = true,
AllowDeviceCodeFlow = false,
AllowInteractiveLogin = false,
ApiKey = options.ApiKey,
VelopackBaseUrl = options.VelopackBaseUrl
})) {
return;
}
await Client.UploadLatestReleaseAssetsAsync(options.Channel, options.ReleaseDirectory, options.VelopackBaseUrl);
}
}

View File

@@ -0,0 +1,11 @@
using Velopack.Packaging.Flow;
namespace Velopack.Vpk.Commands.Flow;
#nullable enable
public sealed class PublishOptions : VelopackServiceOptions
{
public string ReleaseDirectory { get; set; } = "";
public string? Channel { get; set; }
}

View File

@@ -1,21 +0,0 @@
namespace Velopack.Vpk.Commands.Flow;
#nullable enable
public abstract class VelopackBaseCommand : OutputCommand
{
public string? TeamName { get; private set; }
public string? ProjectName { get; private set; }
protected VelopackBaseCommand(string name, string description)
: base(name, description)
{
AddOption<string>((v) => TeamName = v, "--team-name", "-t")
.SetDescription("The name of the team")
.SetRequired();
AddOption<string>((v) => ProjectName = v, "--project-name", "-p")
.SetDescription("The name of the project")
.SetRequired();
}
}

View File

@@ -1,13 +0,0 @@
namespace Velopack.Vpk.Commands.Flow;
#nullable enable
public class VelopackPublishCommand : VelopackBaseCommand
{
public string? Version { get; set; }
public VelopackPublishCommand()
: base("publish", "Uploads a release to Velopack's hosted service")
{
}
}

View File

@@ -33,7 +33,7 @@ public static partial class OptionMapper
public static partial DeltaPatchOptions ToOptions(this DeltaPatchCommand cmd);
public static partial LoginOptions ToOptions(this LoginCommand cmd);
public static partial LogoutOptions ToOptions(this LogoutCommand cmd);
public static partial VelopackFlowUploadOptions ToOptions(this VelopackPublishCommand cmd);
public static partial PublishOptions ToOptions(this PublishCommand cmd);
private static DirectoryInfo StringToDirectoryInfo(string t)
{

View File

@@ -103,14 +103,14 @@ public class Program
deltaCommand.AddCommand<DeltaPatchCommand, DeltaPatchCommandRunner, DeltaPatchOptions>(provider);
rootCommand.Add(deltaCommand);
#if DEBUG
rootCommand.AddCommand<LoginCommand, LoginCommandRunner, LoginOptions>(provider);
rootCommand.AddCommand<LogoutCommand, LogoutCommandRunner, LogoutOptions>(provider);
rootCommand.AddRepositoryUpload<VelopackPublishCommand, VelopackFlowRepository, VelopackFlowUploadOptions>(provider);
#endif
HideCommand(rootCommand.AddCommand<LoginCommand, LoginCommandRunner, LoginOptions>(provider));
HideCommand(rootCommand.AddCommand<LogoutCommand, LogoutCommandRunner, LogoutOptions>(provider));
HideCommand(rootCommand.AddCommand<PublishCommand, PublishCommandRunner, PublishOptions>(provider));
var cli = new CliConfiguration(rootCommand);
return await cli.InvokeAsync(args);
static void HideCommand(CliCommand command) => command.Hidden = true;
}
private static void SetupConfig(IHostApplicationBuilder builder)