Implementing Flow tiered rollout in C#

This add support for setting the tiered rollout percentage in vpk
This commit is contained in:
Kevin Bost
2025-02-13 17:30:38 -08:00
committed by Caelan
parent 588d287ae7
commit e51a3505b6
15 changed files with 153 additions and 54 deletions

View File

@@ -154,7 +154,7 @@ namespace Velopack.Locators
{
if (PackagesDir == null) return null;
var stagedUserIdFile = Path.Combine(PackagesDir, ".betaId");
var ret = default(Guid);
Guid ret;
if (File.Exists(stagedUserIdFile)) {
try {
@@ -170,13 +170,9 @@ namespace Velopack.Locators
Log.Warn($"No userId could not be parsed from '{stagedUserIdFile}', creating a new one.");
}
var prng = new Random();
var buf = new byte[4096];
prng.NextBytes(buf);
ret = GuidUtil.CreateGuidFromHash(buf);
ret = Guid.NewGuid();
try {
File.WriteAllText(stagedUserIdFile, ret.ToString(), Encoding.UTF8);
File.WriteAllText(stagedUserIdFile, ret.ToString("N"), Encoding.UTF8);
Log.Info($"Generated new staging userId: {ret}");
return ret;
} catch (Exception ex) {

View File

@@ -32,8 +32,14 @@ namespace Velopack.Sources
public async Task<VelopackAssetFeed> GetReleaseFeed(ILogger logger, string channel, Guid? stagingId = null,
VelopackAsset? latestLocalRelease = null)
{
string? packageId = latestLocalRelease?.PackageId ?? VelopackLocator.GetDefault(logger).AppId;
if (string.IsNullOrWhiteSpace(packageId)) {
//Without a package id, we can't get a feed.
return new VelopackAssetFeed();
}
Uri baseUri = new(BaseUri, $"v1.0/manifest/");
var uri = HttpUtil.AppendPathToUri(baseUri, CoreUtil.GetVeloReleaseIndexName(channel));
Uri uri = HttpUtil.AppendPathToUri(baseUri, $"{packageId}/{channel}");
var args = new Dictionary<string, string>();
if (VelopackRuntimeInfo.SystemArch != RuntimeCpu.Unknown) {
@@ -46,10 +52,11 @@ namespace Velopack.Sources
}
if (latestLocalRelease != null) {
args.Add("id", latestLocalRelease.PackageId);
args.Add("localVersion", latestLocalRelease.Version.ToString());
} else {
args.Add("id", VelopackLocator.GetDefault(logger).AppId ?? "");
}
if (stagingId != null) {
args.Add("stagingId", stagingId.Value.ToString());
}
var uriAndQuery = HttpUtil.AddQueryParamsToUri(uri, args);

View File

@@ -122,11 +122,11 @@ namespace Velopack
{
EnsureInstalled();
var installedVer = CurrentVersion!;
var betaId = Locator.GetOrCreateStagedUserId();
var stagedUserId = Locator.GetOrCreateStagedUserId();
var latestLocalFull = Locator.GetLatestLocalFullPackage();
Log.Debug("Retrieving latest release feed.");
var feedObj = await Source.GetReleaseFeed(Log, Channel, betaId, latestLocalFull).ConfigureAwait(false);
var feedObj = await Source.GetReleaseFeed(Log, Channel, stagedUserId, latestLocalFull).ConfigureAwait(false);
var feed = feedObj.Assets;
var latestRemoteFull = feed.Where(r => r.Type == VelopackAssetType.Full).MaxByPolyfill(x => x.Version).FirstOrDefault();

View File

@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt
// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt
using System;
using System.Collections.Concurrent;
@@ -104,7 +104,7 @@ namespace Velopack.Util
return ExtremaBy(source, keySelector, (key, minValue) => comparer.Compare(key, minValue));
}
private static IList<TSource> ExtremaBy<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TKey, TKey, int> compare)
private static List<TSource> ExtremaBy<TSource, TKey>(IEnumerable<TSource> source, Func<TSource, TKey> keySelector, Func<TKey, TKey, int> compare)
{
var result = new List<TSource>();

View File

@@ -53,7 +53,7 @@ public class PublishTask : MSBuildAsyncTask
// todo: currently it's not possible to cross-compile for different OSes using Velopack.Build
var targetOs = VelopackRuntimeInfo.SystemOs;
await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, targetOs, WaitForLive, cancellationToken)
await client.UploadLatestReleaseAssetsAsync(Channel, ReleaseDirectory, targetOs, WaitForLive, 100, cancellationToken)
.ConfigureAwait(false);
return true;

View File

@@ -24,6 +24,7 @@ public class PublishCommandRunner(ILogger logger, IFancyConsole console) : IComm
options.ReleaseDirectory,
options.TargetOs,
options.WaitForLive,
options.TieredRolloutPercentage,
token);
}
}

View File

@@ -9,4 +9,6 @@ public sealed class PublishOptions : VelopackFlowServiceOptions
public string? Channel { get; set; }
public bool WaitForLive { get; set; }
public int TieredRolloutPercentage { get; set; }
}

View File

@@ -1589,14 +1589,14 @@ namespace Velopack.Flow
}
/// <exception cref="ApiException">A server side error occurred.</exception>
public virtual System.Threading.Tasks.Task<System.Collections.Generic.ICollection<VelopackAsset>> GetManifestAsync(string packageId, string channel)
public virtual System.Threading.Tasks.Task<System.Collections.Generic.ICollection<VelopackAsset>> GetManifestAsync(string packageId, string channel, string id, string stagingId, string arch, string os, string rid, string localVersion)
{
return GetManifestAsync(packageId, channel, System.Threading.CancellationToken.None);
return GetManifestAsync(packageId, channel, id, stagingId, arch, os, rid, localVersion, System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <exception cref="ApiException">A server side error occurred.</exception>
public virtual async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<VelopackAsset>> GetManifestAsync(string packageId, string channel, System.Threading.CancellationToken cancellationToken)
public virtual async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<VelopackAsset>> GetManifestAsync(string packageId, string channel, string id, string stagingId, string arch, string os, string rid, string localVersion, System.Threading.CancellationToken cancellationToken)
{
if (packageId == null)
throw new System.ArgumentNullException("packageId");
@@ -1620,6 +1620,32 @@ namespace Velopack.Flow
urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(packageId, System.Globalization.CultureInfo.InvariantCulture)));
urlBuilder_.Append('/');
urlBuilder_.Append(System.Uri.EscapeDataString(ConvertToString(channel, System.Globalization.CultureInfo.InvariantCulture)));
urlBuilder_.Append('?');
if (id != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("Id")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
if (stagingId != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("StagingId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(stagingId, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
if (arch != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("Arch")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(arch, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
if (os != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("Os")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(os, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
if (rid != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("Rid")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(rid, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
if (localVersion != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("LocalVersion")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(localVersion, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
urlBuilder_.Length--;
PrepareRequest(client_, request_, urlBuilder_);
@@ -1684,14 +1710,14 @@ namespace Velopack.Flow
}
/// <exception cref="ApiException">A server side error occurred.</exception>
public virtual System.Threading.Tasks.Task<System.Collections.Generic.ICollection<VelopackAsset>> GetChannelManifestAsync(string channel, string id, string arch, string os, string rid, string localVersion)
public virtual System.Threading.Tasks.Task<System.Collections.Generic.ICollection<VelopackAsset>> GetChannelManifestAsync(string channel, string id, string stagingId, string arch, string os, string rid, string localVersion)
{
return GetChannelManifestAsync(channel, id, arch, os, rid, localVersion, System.Threading.CancellationToken.None);
return GetChannelManifestAsync(channel, id, stagingId, arch, os, rid, localVersion, System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <exception cref="ApiException">A server side error occurred.</exception>
public virtual async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<VelopackAsset>> GetChannelManifestAsync(string channel, string id, string arch, string os, string rid, string localVersion, System.Threading.CancellationToken cancellationToken)
public virtual async System.Threading.Tasks.Task<System.Collections.Generic.ICollection<VelopackAsset>> GetChannelManifestAsync(string channel, string id, string stagingId, string arch, string os, string rid, string localVersion, System.Threading.CancellationToken cancellationToken)
{
if (channel == null)
throw new System.ArgumentNullException("channel");
@@ -1716,6 +1742,10 @@ namespace Velopack.Flow
{
urlBuilder_.Append(System.Uri.EscapeDataString("Id")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(id, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
if (stagingId != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("StagingId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(stagingId, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
if (arch != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("Arch")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(arch, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
@@ -3841,14 +3871,14 @@ namespace Velopack.Flow
}
/// <exception cref="ApiException">A server side error occurred.</exception>
public virtual System.Threading.Tasks.Task<ReleaseGroupListResponse> ListReleaseGroupsAsync(System.Guid projectId)
public virtual System.Threading.Tasks.Task<ReleaseGroupListResponse> ListReleaseGroupsAsync(System.Guid projectId, string channelId)
{
return ListReleaseGroupsAsync(projectId, System.Threading.CancellationToken.None);
return ListReleaseGroupsAsync(projectId, channelId, System.Threading.CancellationToken.None);
}
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
/// <exception cref="ApiException">A server side error occurred.</exception>
public virtual async System.Threading.Tasks.Task<ReleaseGroupListResponse> ListReleaseGroupsAsync(System.Guid projectId, System.Threading.CancellationToken cancellationToken)
public virtual async System.Threading.Tasks.Task<ReleaseGroupListResponse> ListReleaseGroupsAsync(System.Guid projectId, string channelId, System.Threading.CancellationToken cancellationToken)
{
if (projectId == null)
throw new System.ArgumentNullException("projectId");
@@ -3868,6 +3898,10 @@ namespace Velopack.Flow
urlBuilder_.Append("v1/releaseGroups/list");
urlBuilder_.Append('?');
urlBuilder_.Append(System.Uri.EscapeDataString("projectId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(projectId, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
if (channelId != null)
{
urlBuilder_.Append(System.Uri.EscapeDataString("channelId")).Append('=').Append(System.Uri.EscapeDataString(ConvertToString(channelId, System.Globalization.CultureInfo.InvariantCulture))).Append('&');
}
urlBuilder_.Length--;
PrepareRequest(client_, request_, urlBuilder_);
@@ -4094,6 +4128,12 @@ namespace Velopack.Flow
throw new ApiException("A server side error occurred.", status_, responseText_, headers_, null);
}
else
if (status_ == 400)
{
string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("A server side error occurred.", status_, responseText_, headers_, null);
}
else
if (status_ == 401)
{
string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
@@ -4269,6 +4309,12 @@ namespace Velopack.Flow
return objectResponse_.Object;
}
else
if (status_ == 400)
{
string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
throw new ApiException("A server side error occurred.", status_, responseText_, headers_, null);
}
else
if (status_ == 401)
{
string responseText_ = ( response_.Content == null ) ? string.Empty : await response_.Content.ReadAsStringAsync().ConfigureAwait(false);
@@ -4891,6 +4937,9 @@ namespace Velopack.Flow
[Newtonsoft.Json.JsonProperty("version", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string Version { get; set; }
[Newtonsoft.Json.JsonProperty("type", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public VelopackAssetType Type { get; set; }
[Newtonsoft.Json.JsonProperty("size", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public long Size { get; set; }
@@ -4911,6 +4960,24 @@ namespace Velopack.Flow
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
public enum VelopackAssetType
{
Unknown = 0,
Full = 1,
Delta = 2,
Portable = 3,
Setup = 4,
MsiDeploymentTool = 5,
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
public partial class ReleaseAssetListResponse
{
@@ -5265,6 +5332,12 @@ namespace Velopack.Flow
[Newtonsoft.Json.JsonProperty("channelName", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string ChannelName { get; set; }
[Newtonsoft.Json.JsonProperty("packageId", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string PackageId { get; set; }
[Newtonsoft.Json.JsonProperty("tieredRolloutPercentage", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public double TieredRolloutPercentage { get; set; }
[Newtonsoft.Json.JsonProperty("createdAt", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public System.DateTimeOffset CreatedAt { get; set; }
@@ -5406,6 +5479,10 @@ namespace Velopack.Flow
[Newtonsoft.Json.JsonProperty("channelIdentifier", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
public string ChannelIdentifier { get; set; }
[Newtonsoft.Json.JsonProperty("tieredRolloutPercentage", Required = Newtonsoft.Json.Required.DisallowNull, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
[System.ComponentModel.DataAnnotations.Range(0.0D, 1.0D)]
public double TieredRolloutPercentage { get; set; }
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]
@@ -5421,6 +5498,10 @@ namespace Velopack.Flow
[Newtonsoft.Json.JsonConverter(typeof(Newtonsoft.Json.Converters.StringEnumConverter))]
public ReleaseGroupPublishState? State { get; set; }
[Newtonsoft.Json.JsonProperty("tieredRolloutPercentage", Required = Newtonsoft.Json.Required.Default, NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore)]
[System.ComponentModel.DataAnnotations.Range(0.0D, 1.0D)]
public double? TieredRolloutPercentage { get; set; }
}
[System.CodeDom.Compiler.GeneratedCode("NJsonSchema", "14.2.0.0 (NJsonSchema v11.1.0.0 (Newtonsoft.Json v13.0.0.0))")]

View File

@@ -38,19 +38,15 @@ public static class FlowApiExtensions
return null;
}
public static FileType ToFileType(this VelopackAssetType type)
public static FileType ToFileType(this Velopack.VelopackAssetType type)
{
switch (type) {
case VelopackAssetType.Full:
return FileType.Full;
case VelopackAssetType.Delta:
return FileType.Delta;
case VelopackAssetType.Portable:
return FileType.Portable;
case VelopackAssetType.Installer:
return FileType.Setup;
default:
throw new ArgumentOutOfRangeException(nameof(type), type, null);
}
return type switch {
Velopack.VelopackAssetType.Full => FileType.Full,
Velopack.VelopackAssetType.Delta => FileType.Delta,
Velopack.VelopackAssetType.Portable => FileType.Portable,
Velopack.VelopackAssetType.Installer => FileType.Setup,
//TODO: MSI Deployment Tool?
_ => throw new ArgumentOutOfRangeException(nameof(type), type, null),
};
}
}

View File

@@ -125,13 +125,13 @@ public class VelopackFlowServiceClient(
}
public async Task UploadLatestReleaseAssetsAsync(string? channel, string releaseDirectory,
RuntimeOs os, bool waitForLive, CancellationToken cancellationToken)
RuntimeOs os, bool waitForLive, int tieredRolloutPercentage, CancellationToken cancellationToken)
{
AssertAuthenticated();
channel ??= DefaultName.GetDefaultChannel(os);
BuildAssets assets = BuildAssets.Read(releaseDirectory, channel);
var fullAsset = assets.GetReleaseEntries().SingleOrDefault(a => a.Type == VelopackAssetType.Full);
var fullAsset = assets.GetReleaseEntries().SingleOrDefault(a => a.Type == Velopack.VelopackAssetType.Full);
if (fullAsset is null) {
Logger.LogError("No full asset found in release directory {ReleaseDirectory} (or it's missing from assets file)", releaseDirectory);
@@ -143,7 +143,7 @@ public class VelopackFlowServiceClient(
var version = fullAsset.Version;
var filesToUpload = assets.GetAssets()
.Where(p => p.Type is not VelopackAssetType.Delta)
.Where(p => p.Type is not Velopack.VelopackAssetType.Delta)
.Select(p => (p.Path, p.Type.ToFileType()))
.ToArray();
@@ -160,7 +160,7 @@ public class VelopackFlowServiceClient(
report(-1);
await CreateChannelIfNotExists(client, packageId, channel, cancellationToken);
report(50);
var result = await CreateReleaseGroupAsync(client, packageId, version, channel, cancellationToken);
var result = await CreateReleaseGroupAsync(client, packageId, version, channel, tieredRolloutPercentage / 100.0, cancellationToken);
Logger.LogInformation("Created release {Version} ({ReleaseGroupId})", version, result.Id);
report(100);
return result;
@@ -316,12 +316,13 @@ public class VelopackFlowServiceClient(
}
private static async Task<ReleaseGroup> CreateReleaseGroupAsync(FlowApi client, string packageId, SemanticVersion version, string channel,
CancellationToken cancellationToken)
double tieredRolloutPercentage, CancellationToken cancellationToken)
{
CreateReleaseGroupRequest request = new() {
ChannelIdentifier = channel,
PackageId = packageId,
Version = version.ToNormalizedString()
Version = version.ToNormalizedString(),
TieredRolloutPercentage = tieredRolloutPercentage
};
return await client.CreateReleaseGroupAsync(request, cancellationToken);

View File

@@ -9,6 +9,8 @@ public class PublishCommand : VelopackServiceCommand
public bool WaitForLive { get; set; }
public int TieredRolloutPercentage { get; set; }
public PublishCommand()
: base("publish", "Uploads a release to Velopack's hosted service")
{
@@ -23,5 +25,10 @@ public class PublishCommand : VelopackServiceCommand
AddOption<bool>(v => WaitForLive = v, "--waitForLive")
.SetDescription("Wait for the release to finish processing and go live.");
AddOption<int>(v => TieredRolloutPercentage = v, "--tieredRolloutPercentage")
.SetDescription("Set the starting percentage for this release when using a tiered rollout. Range 0 to 100")
.SetDefault(100)
.SetValidRange(0, 100);
}
}

View File

@@ -53,6 +53,12 @@ internal static class SystemCommandLineExtensions
return option;
}
public static CliOption<int> SetValidRange(this CliOption<int> option, int minimum, int maximum)
{
option.Validators.Add(x => Validate.MustBeBetween(x, minimum, maximum));
return option;
}
public static CliOption<T> SetArgumentHelpName<T>(this CliOption<T> option, string argumentHelpName)
{
option.HelpName = argumentHelpName;

View File

@@ -1,14 +1,15 @@
using System.Text;
using Velopack.Sources;
namespace Velopack.Tests;
public class FakeDownloader : Sources.IFileDownloader
public class FakeDownloader : IFileDownloader
{
public string LastUrl { get; private set; }
public string LastLocalFile { get; private set; }
public string LastAuthHeader { get; private set; }
public string LastAcceptHeader { get; private set; }
public byte[] MockedResponseBytes { get; set; } = new byte[0];
public byte[] MockedResponseBytes { get; set; } = [];
public bool WriteMockLocalFile { get; set; } = false;
public Task<byte[]> DownloadBytes(string url, string auth, string acc, double timeout = 30)

View File

@@ -7,7 +7,7 @@ using Velopack.Util;
namespace Velopack.Tests.TestHelpers;
internal class FakeFixtureRepository : Sources.IFileDownloader
internal class FakeFixtureRepository : IFileDownloader
{
private readonly string _pkgId;
private readonly IEnumerable<ReleaseEntry> _releases;

View File

@@ -19,29 +19,30 @@
</Choose>
<ItemGroup>
<Compile Include="..\..\src\vpk\Velopack.Core\SimpleJson.cs" Link="SimpleJson.cs"/>
<Compile Include="..\..\src\vpk\Velopack.Core\SimpleJson.cs" Link="SimpleJson.cs" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="System.IO.Packaging" Version="9.0.2"/>
<PackageReference Include="System.IO.Packaging" Version="9.0.1" />
</ItemGroup>
<ItemGroup Condition=" $(TargetFramework.StartsWith('net4')) ">
<Reference Include="System.Web"/>
<Reference Include="System.Net.Http"/>
<Reference Include="System.IO.Compression"/>
<Reference Include="System.IO.Compression.FileSystem"/>
<Reference Include="System.Web" />
<Reference Include="System.Net.Http" />
<Reference Include="System.IO.Compression" />
<Reference Include="System.IO.Compression.FileSystem" />
</ItemGroup>
<Choose>
<When Condition="'$(TargetFramework)' == 'net6.0'">
<ItemGroup>
<ProjectReference Include="..\..\src\lib-csharp\Velopack.csproj" SetTargetFramework="TargetFramework=netstandard2.0"/>
<ProjectReference Include="..\..\src\lib-csharp\Velopack.csproj" SetTargetFramework="TargetFramework=netstandard2.0" />
</ItemGroup>
</When>
<Otherwise>
<ItemGroup>
<ProjectReference Include="..\..\src\lib-csharp\Velopack.csproj"/>
<ProjectReference Include="..\..\src\lib-csharp\Velopack.csproj" />
</ItemGroup>
</Otherwise>
</Choose>