Allow github upload to merge with existing releases

This commit is contained in:
Caelan Sayler
2024-01-14 13:54:11 +00:00
parent 475129e7fe
commit 91c8bdce9f
5 changed files with 253 additions and 83 deletions

View File

@@ -2,6 +2,7 @@
using Octokit;
using Velopack.NuGet;
using Velopack.Packaging;
using Velopack.Packaging.Exceptions;
using Velopack.Sources;
namespace Velopack.Deployment;
@@ -20,6 +21,10 @@ public class GitHubUploadOptions : GitHubDownloadOptions
public bool Publish { get; set; }
public string ReleaseName { get; set; }
public string TagName { get; set; }
public bool Merge { get; set; }
}
public class GitHubRepository : SourceRepository<GitHubDownloadOptions, GithubSource>, IRepositoryCanUpload<GitHubUploadOptions>
@@ -54,7 +59,8 @@ public class GitHubRepository : SourceRepository<GitHubDownloadOptions, GithubSo
var latest = helper.GetLatestFullRelease(options.Channel);
var latestPath = Path.Combine(options.ReleaseDir.FullName, latest.OriginalFilename);
var releaseNotes = new ZipPackage(latestPath).ReleaseNotes;
var semVer = latest.Version;
var semVer = options.TagName ?? latest.Version.ToString();
var releaseName = string.IsNullOrWhiteSpace(options.ReleaseName) ? semVer.ToString() : options.ReleaseName;
Log.Info($"Preparing to upload {assets.Files.Count} assets to GitHub");
@@ -62,22 +68,46 @@ public class GitHubRepository : SourceRepository<GitHubDownloadOptions, GithubSo
Credentials = new Credentials(options.Token)
};
var newReleaseReq = new NewRelease(semVer.ToString()) {
Body = releaseNotes,
Draft = true,
Prerelease = options.Prerelease,
Name = string.IsNullOrWhiteSpace(options.ReleaseName) ? semVer.ToString() : options.ReleaseName,
};
Log.Info($"Creating draft release titled '{newReleaseReq.Name}'");
var existingReleases = await client.Repository.Release.GetAll(repoOwner, repoName);
if (existingReleases.Any(r => r.TagName == semVer.ToString())) {
throw new Exception($"There is already an existing release tagged '{semVer}'. Please delete this release or provide a new version number / release name.");
if (!options.Merge) {
if (existingReleases.Any(r => r.TagName == semVer.ToString())) {
throw new UserInfoException($"There is already an existing release tagged '{semVer}'. Please delete this release or provide a new version number.");
}
if (existingReleases.Any(r => r.Name == releaseName)) {
throw new UserInfoException($"There is already an existing release named '{releaseName}'. Please delete this release or provide a new release name.");
}
}
// create github release
var release = await client.Repository.Release.Create(repoOwner, repoName, newReleaseReq);
// create or retrieve github release
var release = existingReleases.FirstOrDefault(r => r.TagName == semVer.ToString())
?? existingReleases.FirstOrDefault(r => r.Name == releaseName);
if (release != null) {
if (release.TagName != semVer.ToString())
throw new UserInfoException($"Found existing release matched by name ({release.Name} [{release.TagName}]), but tag name does not match ({semVer}).");
Log.Info($"Found existing release ({release.Name} [{release.TagName}]). Merge flag is enabled.");
} else {
var newReleaseReq = new NewRelease(semVer.ToString()) {
Body = releaseNotes,
Draft = true,
Prerelease = options.Prerelease,
Name = string.IsNullOrWhiteSpace(options.ReleaseName) ? semVer.ToString() : options.ReleaseName,
};
Log.Info($"Creating draft release titled '{newReleaseReq.Name}'");
release = await client.Repository.Release.Create(repoOwner, repoName, newReleaseReq);
}
// check if there is an existing releasesFile to merge
var releaseAsset = release.Assets.FirstOrDefault(a => a.Name == assets.ReleasesFileName);
if (releaseAsset != null) {
throw new UserInfoException($"There is already a release asset named '{assets.ReleasesFileName}', and merging release files is not supported.");
//Log.Info($"Will merge with existing remote releases file ({releaseAsset.Name}).");
//var dl = Utility.CreateDefaultDownloader();
//var releasesString = await dl.DownloadString(releaseAsset.BrowserDownloadUrl);
//var remoteReleases = ReleaseEntry.ParseReleaseFile(releasesString).ToArray();
//assets.Releases.AddRange(remoteReleases);
//Log.Info($"There will be {assets.Releases.Count} in final release file ({remoteReleases.Length} added).");
}
// upload all assets (incl packages)
foreach (var a in assets.Files) {
@@ -92,10 +122,14 @@ public class GitHubRepository : SourceRepository<GitHubDownloadOptions, GithubSo
// convert draft to full release
if (options.Publish) {
Log.Info("Converting draft to full published release.");
var upd = release.ToUpdate();
upd.Draft = false;
release = await client.Repository.Release.Edit(repoOwner, repoName, release.Id, upd);
if (release.Draft) {
Log.Info("Converting draft to full published release.");
var upd = release.ToUpdate();
upd.Draft = false;
release = await client.Repository.Release.Edit(repoOwner, repoName, release.Id, upd);
} else {
Log.Info("Skipping publish, release is already not a draft.");
}
}
}

View File

@@ -171,16 +171,16 @@ namespace Velopack.Packaging
var suffix = GetPkgSuffix(os, channel);
if (!_releases.ContainsKey(channel))
throw new ArgumentException("No releases found for channel: " + channel);
throw new UserInfoException("No releases found for channel: " + channel);
ret.ReleasesFileName = SourceBase.GetReleasesFileNameImpl(channel);
var relPath = GetReleasePath(channel);
if (!File.Exists(relPath))
throw new FileNotFoundException("Could not find RELEASES file for channel: " + channel, relPath);
throw new UserInfoException($"Could not find RELEASES file for channel {channel} at {relPath}");
ReleaseEntry latest = GetLatestFullRelease(channel);
if (latest == null) {
throw new ArgumentException("No full releases found for channel: " + channel);
throw new UserInfoException("No full releases found for channel: " + channel);
} else {
_logger.Info("Latest local release: " + latest.OriginalFilename);
}

View File

@@ -6,8 +6,12 @@ public class GitHubUploadCommand : GitHubBaseCommand
public string ReleaseName { get; private set; }
public string TagName { get; private set; }
public bool Pre { get; private set; }
public bool Merge { get; private set; }
public GitHubUploadCommand()
: base("github", "Upload releases to a GitHub repository.")
{
@@ -17,8 +21,15 @@ public class GitHubUploadCommand : GitHubBaseCommand
AddOption<bool>((v) => Pre = v, "--pre")
.SetDescription("Create as pre-release instead of stable.");
AddOption<bool>((v) => Merge = v, "--merge")
.SetDescription("Allow merging this upload with an existing release.");
AddOption<string>((v) => ReleaseName = v, "--releaseName")
.SetDescription("A custom name for created release.")
.SetDescription("A custom name for the release.")
.SetArgumentHelpName("NAME");
AddOption<string>((v) => TagName = v, "--tag")
.SetDescription("A custom tag for the release.")
.SetArgumentHelpName("NAME");
ReleaseDirectoryOption.SetRequired();

View File

@@ -1,4 +1,4 @@
using System.Runtime.Versioning;
using System.Runtime.Versioning;
using Velopack.Deployment;
using Velopack.Packaging.Commands;
using Velopack.Packaging.Unix.Commands;
@@ -156,6 +156,8 @@ public class EmbeddedRunner : ICommandRunner
Publish = command.Publish,
ReleaseName = command.ReleaseName,
Channel = command.Channel,
Merge = command.Merge,
TagName = command.TagName,
};
return new GitHubRepository(_logger).UploadMissingAssetsAsync(options);
}

View File

@@ -1,16 +1,17 @@
using System.Diagnostics;
using System.Diagnostics;
using Velopack.Deployment;
using Velopack.Packaging.Unix.Commands;
using Velopack.Packaging.Windows.Commands;
using Velopack.Sources;
using Octokit;
using Velopack.Packaging.Exceptions;
namespace Velopack.Packaging.Tests
{
public class DeploymentTests
{
public readonly string GITHUB_TOKEN = Environment.GetEnvironmentVariable("VELOPACK_GITHUB_TEST_TOKEN");
public readonly string GITHUB_REPOURL = "https://github.com/caesay/VelopackGithubUpdateTest";
public readonly static string GITHUB_TOKEN = Environment.GetEnvironmentVariable("VELOPACK_GITHUB_TEST_TOKEN");
public readonly static string GITHUB_REPOURL = "https://github.com/caesay/VelopackGithubUpdateTest";
private readonly ITestOutputHelper _output;
@@ -19,32 +20,114 @@ namespace Velopack.Packaging.Tests
_output = output;
}
[Fact]
[SkippableFact]
public void WillRefuseToUploadMultipleWithoutMergeArg()
{
Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set.");
using var logger = _output.BuildLoggerFor<DeploymentTests>();
using var _1 = Utility.GetTempDirectory(out var releaseDir);
using var _2 = Utility.GetTempDirectory(out var releaseDir2);
using var ghvar = GitHubReleaseTest.Create("nomerge", logger);
var id = "GithubUpdateTest";
PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir, logger);
var gh = new GitHubRepository(logger);
var options = new GitHubUploadOptions {
ReleaseName = ghvar.ReleaseName,
ReleaseDir = new DirectoryInfo(releaseDir),
RepoUrl = GITHUB_REPOURL,
Token = GITHUB_TOKEN,
Prerelease = false,
Publish = true,
};
gh.UploadMissingAssetsAsync(options).GetAwaiterResult();
PackTestApp(id, $"0.0.2-{ghvar.UniqueSuffix}", "t1", releaseDir2, logger);
options.ReleaseDir = new DirectoryInfo(releaseDir2);
Assert.ThrowsAny<UserInfoException>(() => gh.UploadMissingAssetsAsync(options).GetAwaiterResult());
}
[SkippableFact]
public void WillNotMergeMixmatchedTag()
{
Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set.");
using var logger = _output.BuildLoggerFor<DeploymentTests>();
using var _1 = Utility.GetTempDirectory(out var releaseDir);
using var _2 = Utility.GetTempDirectory(out var releaseDir2);
using var ghvar = GitHubReleaseTest.Create("mixmatched", logger);
var id = "GithubUpdateTest";
PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir, logger);
var gh = new GitHubRepository(logger);
var options = new GitHubUploadOptions {
ReleaseName = ghvar.ReleaseName,
ReleaseDir = new DirectoryInfo(releaseDir),
RepoUrl = GITHUB_REPOURL,
Token = GITHUB_TOKEN,
Prerelease = false,
Publish = true,
Merge = true,
};
gh.UploadMissingAssetsAsync(options).GetAwaiterResult();
PackTestApp(id, $"0.0.2-{ghvar.UniqueSuffix}", "t1", releaseDir2, logger);
options.ReleaseDir = new DirectoryInfo(releaseDir2);
Assert.ThrowsAny<UserInfoException>(() => gh.UploadMissingAssetsAsync(options).GetAwaiterResult());
}
[SkippableFact]
public void WillMergeGithubReleases()
{
Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set.");
using var logger = _output.BuildLoggerFor<DeploymentTests>();
using var _1 = Utility.GetTempDirectory(out var releaseDir);
using var _2 = Utility.GetTempDirectory(out var releaseDir2);
using var ghvar = GitHubReleaseTest.Create("yesmerge", logger);
var id = "GithubUpdateTest";
PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir, logger);
var gh = new GitHubRepository(logger);
var options = new GitHubUploadOptions {
ReleaseName = ghvar.ReleaseName,
ReleaseDir = new DirectoryInfo(releaseDir),
RepoUrl = GITHUB_REPOURL,
Token = GITHUB_TOKEN,
TagName = $"0.0.1-{ghvar.UniqueSuffix}",
Prerelease = false,
Publish = true,
Merge = true,
};
gh.UploadMissingAssetsAsync(options).GetAwaiterResult();
PackTestApp(id, $"0.0.1-{ghvar.UniqueSuffix}", "t1", releaseDir2, logger, channel: "experimental");
options.ReleaseDir = new DirectoryInfo(releaseDir2);
options.Channel = "experimental";
gh.UploadMissingAssetsAsync(options).GetAwaiterResult();
}
[SkippableFact]
public void CanDeployAndUpdateFromGithub()
{
Skip.If(String.IsNullOrWhiteSpace(GITHUB_TOKEN), "VELOPACK_GITHUB_TEST_TOKEN is not set.");
using var logger = _output.BuildLoggerFor<DeploymentTests>();
var id = "GithubUpdateTest";
var ci = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
using var _1 = Utility.GetTempDirectory(out var releaseDir);
var uniqueSuffix = (ci ? "ci-" : "local-") + VelopackRuntimeInfo.SystemOs.GetOsShortName();
var releaseName = $"{VelopackRuntimeInfo.VelopackNugetVersion}-{uniqueSuffix}";
// delete release if already exists
var client = new GitHubClient(new ProductHeaderValue("Velopack")) {
Credentials = new Credentials(GITHUB_TOKEN)
};
var (repoOwner, repoName) = GitHubRepository.GetOwnerAndRepo(GITHUB_REPOURL);
var existingRelease = client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().SingleOrDefault(s => s.Name == releaseName);
if (existingRelease != null) {
client.Repository.Release.Delete(repoOwner, repoName, existingRelease.Id).GetAwaiterResult();
logger.Info($"Deleted existing release '{releaseName}'");
}
using var ghvar = GitHubReleaseTest.Create("integration", logger);
var releaseName = ghvar.ReleaseName;
var uniqueSuffix = ghvar.UniqueSuffix;
var client = ghvar.Client;
// create releases
var notesPath = Path.Combine(releaseDir, "NOTES");
var notesContent = $"""
# Release {releaseName}
CI: {ci}
This is just a _test_!
""";
File.WriteAllText(notesPath, notesContent);
@@ -56,56 +139,93 @@ This is just a _test_!
PackTestApp(id, $"0.0.1-{uniqueSuffix}", "t1", releaseDir, logger, notesPath);
PackTestApp(id, newVer, "t2", releaseDir, logger, notesPath);
try {
// deploy
var gh = new GitHubRepository(logger);
var options = new GitHubUploadOptions {
ReleaseName = releaseName,
ReleaseDir = new DirectoryInfo(releaseDir),
RepoUrl = GITHUB_REPOURL,
Token = GITHUB_TOKEN,
Prerelease = false,
Publish = true,
// deploy
var gh = new GitHubRepository(logger);
var options = new GitHubUploadOptions {
ReleaseName = releaseName,
ReleaseDir = new DirectoryInfo(releaseDir),
RepoUrl = GITHUB_REPOURL,
Token = GITHUB_TOKEN,
Prerelease = false,
Publish = true,
};
gh.UploadMissingAssetsAsync(options).GetAwaiterResult();
// check
var newRelease = client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().Single(s => s.Name == releaseName);
Assert.False(newRelease.Draft);
Assert.Equal(notesContent.Trim().ReplaceLineEndings("\n"), newRelease.Body.Trim());
// update
var source = new GithubSource(GITHUB_REPOURL, GITHUB_TOKEN, false, logger: logger);
var releases = source.GetReleaseFeed().GetAwaiterResult();
var ghrel = releases.Select(r => (GithubReleaseEntry) r).ToArray();
Assert.Equal(2, ghrel.Length);
foreach (var r in ghrel) {
Assert.Equal(releaseName, r.Release.Name);
Assert.Equal(id, r.PackageId);
Assert.Equal(newVer, r.Version.ToNormalizedString());
}
using var _2 = Utility.GetTempDirectory(out var releaseDirNew);
gh.DownloadLatestFullPackageAsync(new GitHubDownloadOptions {
Token = GITHUB_TOKEN,
RepoUrl = GITHUB_REPOURL,
ReleaseDir = new DirectoryInfo(releaseDirNew),
}).GetAwaiterResult();
var filename = $"{id}-{newVer}-{VelopackRuntimeInfo.SystemOs.GetOsShortName()}-full.nupkg";
Assert.True(File.Exists(Path.Combine(releaseDirNew, filename)));
}
private class GitHubReleaseTest : IDisposable
{
public string ReleaseName { get; }
public string UniqueSuffix { get; }
public GitHubClient Client { get; }
public ILogger Logger { get; }
public GitHubReleaseTest(string releaseName, string uniqueSuffix, GitHubClient client, ILogger logger)
{
ReleaseName = releaseName;
UniqueSuffix = uniqueSuffix;
Client = client;
Logger = logger;
}
public static GitHubReleaseTest Create(string method, ILogger logger)
{
var ci = !String.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"));
var uniqueSuffix = (ci ? "ci-" : "local-") + VelopackRuntimeInfo.SystemOs.GetOsShortName();
var releaseName = $"{VelopackRuntimeInfo.VelopackNugetVersion}-{uniqueSuffix}-{method}";
var (repoOwner, repoName) = GitHubRepository.GetOwnerAndRepo(GITHUB_REPOURL);
// delete release if already exists
var client = new GitHubClient(new ProductHeaderValue("Velopack")) {
Credentials = new Credentials(GITHUB_TOKEN)
};
gh.UploadMissingAssetsAsync(options).GetAwaiterResult();
// check
var newRelease = client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().Single(s => s.Name == releaseName);
Assert.False(newRelease.Draft);
Assert.Equal(notesContent.Trim().ReplaceLineEndings("\n"), newRelease.Body.Trim());
// update
var source = new GithubSource(GITHUB_REPOURL, GITHUB_TOKEN, false, logger: logger);
var releases = source.GetReleaseFeed().GetAwaiterResult();
var ghrel = releases.Select(r => (GithubReleaseEntry) r).ToArray();
Assert.Equal(2, ghrel.Length);
foreach (var r in ghrel) {
Assert.Equal(releaseName, r.Release.Name);
Assert.Equal(id, r.PackageId);
Assert.Equal(newVer, r.Version.ToNormalizedString());
var existingRelease = client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().SingleOrDefault(s => s.Name == releaseName);
if (existingRelease != null) {
client.Repository.Release.Delete(repoOwner, repoName, existingRelease.Id).GetAwaiterResult();
logger.Info("Deleted existing release: " + releaseName);
}
return new GitHubReleaseTest(releaseName, uniqueSuffix, client, logger);
}
using var _2 = Utility.GetTempDirectory(out var releaseDirNew);
gh.DownloadLatestFullPackageAsync(new GitHubDownloadOptions {
Token = GITHUB_TOKEN,
RepoUrl = GITHUB_REPOURL,
ReleaseDir = new DirectoryInfo(releaseDirNew),
}).GetAwaiterResult();
var filename = $"{id}-{newVer}-{VelopackRuntimeInfo.SystemOs.GetOsShortName()}-full.nupkg";
Assert.True(File.Exists(Path.Combine(releaseDirNew, filename)));
} finally {
// clean up
var finalRelease = client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().SingleOrDefault(s => s.Name == releaseName);
public void Dispose()
{
var (repoOwner, repoName) = GitHubRepository.GetOwnerAndRepo(GITHUB_REPOURL);
var finalRelease = Client.Repository.Release.GetAll(repoOwner, repoName).GetAwaiterResult().SingleOrDefault(s => s.Name == ReleaseName);
if (finalRelease != null) {
client.Repository.Release.Delete(repoOwner, repoName, finalRelease.Id).GetAwaiterResult();
logger.Info($"Deleted final release '{releaseName}'");
Client.Repository.Release.Delete(repoOwner, repoName, finalRelease.Id).GetAwaiterResult();
Logger.Info($"Deleted final release '{ReleaseName}'");
}
}
}
private void PackTestApp(string id, string version, string testString, string releaseDir, ILogger logger, string releaseNotes)
private void PackTestApp(string id, string version, string testString, string releaseDir, ILogger logger,
string releaseNotes = null, string channel = null)
{
var projDir = PathHelper.GetTestRootPath("TestApp");
var testStringFile = Path.Combine(projDir, "Const.cs");
@@ -137,6 +257,7 @@ This is just a _test_!
PackVersion = version,
PackDirectory = Path.Combine(projDir, "publish"),
ReleaseNotes = releaseNotes,
Channel = channel,
};
var runner = new WindowsPackCommandRunner(logger);
runner.Run(options).GetAwaiterResult();
@@ -150,6 +271,7 @@ This is just a _test_!
PackVersion = version,
PackDirectory = Path.Combine(projDir, "publish"),
ReleaseNotes = releaseNotes,
Channel = channel,
};
var runner = new OsxPackCommandRunner(logger);
runner.Run(options).GetAwaiterResult();
@@ -163,6 +285,7 @@ This is just a _test_!
PackVersion = version,
PackDirectory = Path.Combine(projDir, "publish"),
ReleaseNotes = releaseNotes,
Channel = channel,
};
var runner = new LinuxPackCommandRunner(logger);
runner.Run(options).GetAwaiterResult();