mirror of
https://github.com/velopack/velopack.git
synced 2025-10-25 15:19:22 +00:00
Refactor channels, deployment repositories
This commit is contained in:
@@ -1,12 +1,12 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Octokit;
|
||||
using Velopack.Packaging;
|
||||
using Velopack.Sources;
|
||||
|
||||
namespace Velopack.Deployment;
|
||||
|
||||
public class GitHubOptions
|
||||
public class GitHubOptions : RepositoryOptions
|
||||
{
|
||||
public DirectoryInfo ReleaseDir { get; set; }
|
||||
public string RepoUrl { get; set; }
|
||||
|
||||
public string Token { get; set; }
|
||||
@@ -15,7 +15,6 @@ public class GitHubOptions
|
||||
public class GitHubDownloadOptions : GitHubOptions
|
||||
{
|
||||
public bool Pre { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class GitHubUploadOptions : GitHubOptions
|
||||
@@ -23,66 +22,21 @@ public class GitHubUploadOptions : GitHubOptions
|
||||
public bool Publish { get; set; }
|
||||
|
||||
public string ReleaseName { get; set; }
|
||||
|
||||
}
|
||||
|
||||
public class GitHubRepository
|
||||
public class GitHubRepository : SourceRepository<GitHubDownloadOptions, GithubSource>, IRepositoryCanUpload<GitHubUploadOptions>
|
||||
{
|
||||
private readonly ILogger _log;
|
||||
|
||||
public GitHubRepository(ILogger logger)
|
||||
public GitHubRepository(ILogger logger) : base(logger)
|
||||
{
|
||||
_log = logger;
|
||||
}
|
||||
|
||||
public async Task DownloadRecentPackages(GitHubDownloadOptions options)
|
||||
public override GithubSource CreateSource(GitHubDownloadOptions options)
|
||||
{
|
||||
var releaseDirectoryInfo = options.ReleaseDir;
|
||||
if (String.IsNullOrWhiteSpace(options.Token))
|
||||
_log.Warn("No GitHub access token provided. Unauthenticated requests will be limited to 60 per hour.");
|
||||
|
||||
_log.Info("Fetching RELEASES...");
|
||||
var source = new GithubSource(options.RepoUrl, options.Token, options.Pre);
|
||||
var latestReleaseEntries = await source.GetReleaseFeed();
|
||||
|
||||
if (latestReleaseEntries == null || latestReleaseEntries.Length == 0) {
|
||||
_log.Warn("No github release or assets found.");
|
||||
return;
|
||||
return new GithubSource(options.RepoUrl, options.Token, options.Pre, options.Channel, logger: Log);
|
||||
}
|
||||
|
||||
_log.Info($"Found {latestReleaseEntries.Length} assets in latest RELEASES file.");
|
||||
|
||||
var releasesToDownload = latestReleaseEntries
|
||||
.Where(x => !x.IsDelta)
|
||||
.OrderByDescending(x => x.Version)
|
||||
.Take(1)
|
||||
.Select(x => new {
|
||||
Obj = x,
|
||||
LocalPath = Path.Combine(releaseDirectoryInfo.FullName, x.OriginalFilename),
|
||||
Filename = x.OriginalFilename,
|
||||
});
|
||||
|
||||
foreach (var entry in releasesToDownload) {
|
||||
if (File.Exists(entry.LocalPath)) {
|
||||
_log.Warn($"File '{entry.Filename}' exists on disk, skipping download.");
|
||||
continue;
|
||||
}
|
||||
|
||||
_log.Info($"Downloading {entry.Filename}...");
|
||||
await source.DownloadReleaseEntry(entry.Obj, entry.LocalPath, (p) => { });
|
||||
}
|
||||
|
||||
ReleaseEntry.BuildReleasesFile(releaseDirectoryInfo.FullName);
|
||||
_log.Info("Done.");
|
||||
}
|
||||
|
||||
public async Task UploadMissingPackages(GitHubUploadOptions options)
|
||||
public async Task UploadMissingAssetsAsync(GitHubUploadOptions options)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(options.Token))
|
||||
throw new InvalidOperationException("Must provide access token to create a GitHub release.");
|
||||
|
||||
var releaseDirectoryInfo = options.ReleaseDir;
|
||||
|
||||
var repoUri = new Uri(options.RepoUrl);
|
||||
var repoParts = repoUri.AbsolutePath.Trim('/').Split('/');
|
||||
if (repoParts.Length != 2)
|
||||
@@ -91,85 +45,48 @@ public class GitHubRepository
|
||||
var repoOwner = repoParts[0];
|
||||
var repoName = repoParts[1];
|
||||
|
||||
var helper = new ReleaseEntryHelper(options.ReleaseDir.FullName, Log);
|
||||
var assets = helper.GetUploadAssets(options.Channel, ReleaseEntryHelper.AssetsMode.OnlyLatest);
|
||||
var latest = helper.GetLatestFullRelease(options.Channel);
|
||||
var semVer = latest.Version;
|
||||
|
||||
Log.Info($"Preparing to upload {assets.Files.Count} assets to GitHub");
|
||||
|
||||
var client = new GitHubClient(new ProductHeaderValue("Velopack")) {
|
||||
Credentials = new Credentials(options.Token)
|
||||
};
|
||||
|
||||
var releasesPath = Path.Combine(releaseDirectoryInfo.FullName, "RELEASES");
|
||||
if (!File.Exists(releasesPath))
|
||||
ReleaseEntry.BuildReleasesFile(releaseDirectoryInfo.FullName);
|
||||
|
||||
var releases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesPath)).ToArray();
|
||||
if (releases.Length == 0)
|
||||
throw new Exception("There are no nupkg's in the releases directory to upload");
|
||||
|
||||
var ver = Enumerable.MaxBy(releases, x => x.Version);
|
||||
if (ver == null)
|
||||
throw new Exception("There are no nupkg's in the releases directory to upload");
|
||||
var semVer = ver.Version;
|
||||
|
||||
_log.Info($"Preparing to upload latest local release to GitHub");
|
||||
|
||||
var newReleaseReq = new NewRelease(semVer.ToString()) {
|
||||
//Body = ver.GetReleaseNotes(releaseDirectoryInfo.FullName, ReleaseNotesFormat.Markdown),
|
||||
Draft = true,
|
||||
Prerelease = semVer.HasMetadata || semVer.IsPrerelease,
|
||||
Name = string.IsNullOrWhiteSpace(options.ReleaseName)
|
||||
? semVer.ToString()
|
||||
: options.ReleaseName,
|
||||
Name = string.IsNullOrWhiteSpace(options.ReleaseName) ? semVer.ToString() : options.ReleaseName,
|
||||
};
|
||||
|
||||
_log.Info($"Creating draft release titled '{semVer.ToString()}'");
|
||||
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 choose a new version number.");
|
||||
throw new Exception($"There is already an existing release tagged '{semVer}'. Please delete this release or provide a new version number / release name.");
|
||||
}
|
||||
|
||||
// create github release
|
||||
var release = await client.Repository.Release.Create(repoOwner, repoName, newReleaseReq);
|
||||
|
||||
// locate files to upload
|
||||
var files = releaseDirectoryInfo.GetFiles("*", SearchOption.TopDirectoryOnly);
|
||||
var msiFile = files.SingleOrDefault(f => f.FullName.EndsWith(".msi", StringComparison.InvariantCultureIgnoreCase));
|
||||
var setupFile = files.Where(f => f.FullName.EndsWith("Setup.exe", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ContextualSingle("release directory", "Setup.exe file");
|
||||
// upload all assets (incl packages)
|
||||
foreach (var a in assets.Files) {
|
||||
await RetryAsync(() => UploadFileAsAsset(client, release, a.FullName), $"Uploading asset '{a.Name}'..");
|
||||
}
|
||||
|
||||
var releasesToUpload = releases.Where(x => x.Version == semVer).ToArray();
|
||||
MemoryStream releasesFileToUpload = new MemoryStream();
|
||||
ReleaseEntry.WriteReleaseFile(releasesToUpload, releasesFileToUpload);
|
||||
ReleaseEntry.WriteReleaseFile(assets.Releases, releasesFileToUpload);
|
||||
var releasesBytes = releasesFileToUpload.ToArray();
|
||||
|
||||
// upload nupkg's
|
||||
foreach (var r in releasesToUpload) {
|
||||
var path = Path.Combine(releaseDirectoryInfo.FullName, r.OriginalFilename);
|
||||
await UploadFileAsAsset(client, release, path);
|
||||
}
|
||||
|
||||
// other files
|
||||
await UploadFileAsAsset(client, release, setupFile.FullName);
|
||||
if (msiFile != null) await UploadFileAsAsset(client, release, msiFile.FullName);
|
||||
|
||||
// RELEASES
|
||||
_log.Info($"Uploading RELEASES");
|
||||
var data = new ReleaseAssetUpload("RELEASES", "application/octet-stream", new MemoryStream(releasesBytes), TimeSpan.FromMinutes(1));
|
||||
var data = new ReleaseAssetUpload(assets.ReleasesFileName, "application/octet-stream", new MemoryStream(releasesBytes), TimeSpan.FromMinutes(1));
|
||||
await client.Repository.Release.UploadAsset(release, data, CancellationToken.None);
|
||||
|
||||
_log.Info($"Done creating draft GitHub release.");
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
_log.Info("Release URL: " + release.HtmlUrl);
|
||||
}
|
||||
|
||||
private async Task UploadFileAsAsset(GitHubClient client, Release release, string filePath)
|
||||
{
|
||||
_log.Info($"Uploading asset '{Path.GetFileName(filePath)}'");
|
||||
using var stream = File.OpenRead(filePath);
|
||||
var data = new ReleaseAssetUpload(Path.GetFileName(filePath), "application/octet-stream", stream, TimeSpan.FromMinutes(30));
|
||||
await client.Repository.Release.UploadAsset(release, data, CancellationToken.None);
|
||||
|
||||
21
src/Velopack.Deployment/HttpRepository.cs
Normal file
21
src/Velopack.Deployment/HttpRepository.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Velopack.Sources;
|
||||
|
||||
namespace Velopack.Deployment;
|
||||
|
||||
public class HttpDownloadOptions : RepositoryOptions
|
||||
{
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
public class HttpRepository : SourceRepository<HttpDownloadOptions, SimpleWebSource>
|
||||
{
|
||||
public HttpRepository(ILogger logger)
|
||||
: base(logger)
|
||||
{ }
|
||||
|
||||
public override SimpleWebSource CreateSource(HttpDownloadOptions options)
|
||||
{
|
||||
return new SimpleWebSource(options.Url, options.Channel, logger: Log);
|
||||
}
|
||||
}
|
||||
@@ -5,13 +5,13 @@ using Amazon;
|
||||
using Amazon.S3;
|
||||
using Amazon.S3.Model;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Velopack.Packaging;
|
||||
using Velopack.Sources;
|
||||
|
||||
namespace Velopack.Deployment;
|
||||
|
||||
public class S3Options
|
||||
public class S3DownloadOptions : RepositoryOptions
|
||||
{
|
||||
public DirectoryInfo ReleaseDir { get; set; }
|
||||
|
||||
public string KeyId { get; set; }
|
||||
|
||||
public string Secret { get; set; }
|
||||
@@ -23,27 +23,86 @@ public class S3Options
|
||||
public string Endpoint { get; set; }
|
||||
|
||||
public string Bucket { get; set; }
|
||||
|
||||
public string PathPrefix { get; set; }
|
||||
}
|
||||
|
||||
public class S3UploadOptions : S3Options
|
||||
public class S3UploadOptions : S3DownloadOptions
|
||||
{
|
||||
public bool Overwrite { get; set; }
|
||||
|
||||
public int KeepMaxReleases { get; set; }
|
||||
}
|
||||
|
||||
public class S3Repository
|
||||
public class S3Repository : DownRepository<S3DownloadOptions>, IRepositoryCanUpload<S3UploadOptions>
|
||||
{
|
||||
private readonly ILogger Log;
|
||||
|
||||
public S3Repository(ILogger logger)
|
||||
public S3Repository(ILogger logger) : base(logger)
|
||||
{
|
||||
Log = logger;
|
||||
}
|
||||
|
||||
private static AmazonS3Client GetS3Client(S3Options options)
|
||||
public async Task UploadMissingAssetsAsync(S3UploadOptions options)
|
||||
{
|
||||
var assets = new ReleaseEntryHelper(options.ReleaseDir.FullName, Log)
|
||||
.GetUploadAssets(options.Channel, ReleaseEntryHelper.AssetsMode.AllPackages);
|
||||
|
||||
var releasesName = SourceBase.GetReleasesFileNameImpl(options.Channel);
|
||||
var remoteReleases = await GetReleasesAsync(options);
|
||||
var client = GetS3Client(options);
|
||||
|
||||
Log.Info($"There are {remoteReleases.Length} assets in remote RELEASES file.");
|
||||
|
||||
var matchCount = assets.Releases.Count(r => remoteReleases.Any(remote => remote.OriginalFilename.Equals(r.OriginalFilename)));
|
||||
Log.Info($"There are {assets.Releases} local releases ({assets.Releases.Count - matchCount} new, {matchCount} matching a remote filename).");
|
||||
|
||||
// merge local release entries with remote ones
|
||||
// will preserve the local entries because they appear first
|
||||
var releaseEntries = assets.Releases
|
||||
.Concat(remoteReleases)
|
||||
.DistinctBy(r => r.OriginalFilename)
|
||||
.OrderBy(k => k.Version)
|
||||
.ThenBy(k => !k.IsDelta)
|
||||
.ToArray();
|
||||
|
||||
Log.Info($"{releaseEntries.Length} merged releases.");
|
||||
|
||||
foreach (var asset in assets.Files) {
|
||||
await UploadFile(client, options.Bucket, asset.Name, asset, options.Overwrite);
|
||||
}
|
||||
|
||||
using var _1 = Utility.GetTempFileName(out var tmpReleases);
|
||||
ReleaseEntry.WriteReleaseFile(releaseEntries, tmpReleases);
|
||||
await UploadFile(client, options.Bucket, releasesName, new FileInfo(tmpReleases), true);
|
||||
Log.Info("Done.");
|
||||
}
|
||||
|
||||
protected override async Task<ReleaseEntry[]> GetReleasesAsync(S3DownloadOptions options)
|
||||
{
|
||||
var releasesName = SourceBase.GetReleasesFileNameImpl(options.Channel);
|
||||
var client = GetS3Client(options);
|
||||
|
||||
var ms = new MemoryStream();
|
||||
|
||||
try {
|
||||
await RetryAsync(async () => {
|
||||
using (var obj = await client.GetObjectAsync(options.Bucket, releasesName))
|
||||
using (var stream = obj.ResponseStream) {
|
||||
await stream.CopyToAsync(ms);
|
||||
}
|
||||
}, $"Fetching {releasesName}...");
|
||||
} catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) {
|
||||
return new ReleaseEntry[0];
|
||||
}
|
||||
|
||||
return ReleaseEntry.ParseReleaseFile(Encoding.UTF8.GetString(ms.ToArray())).ToArray();
|
||||
}
|
||||
|
||||
protected override async Task SaveEntryToFileAsync(S3DownloadOptions options, ReleaseEntry entry, string filePath)
|
||||
{
|
||||
var client = GetS3Client(options);
|
||||
await RetryAsync(async () => {
|
||||
using (var obj = await client.GetObjectAsync(options.Bucket, entry.OriginalFilename)) {
|
||||
await obj.WriteResponseStreamToFileAsync(filePath, false, CancellationToken.None);
|
||||
}
|
||||
}, $"Downloading {entry.OriginalFilename}...");
|
||||
}
|
||||
|
||||
private static AmazonS3Client GetS3Client(S3DownloadOptions options)
|
||||
{
|
||||
if (options.Region != null) {
|
||||
var r = RegionEndpoint.GetBySystemName(options.Region);
|
||||
@@ -59,131 +118,13 @@ public class S3Repository
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetPrefix(S3Options options)
|
||||
private async Task UploadFile(AmazonS3Client client, string bucket, string key, FileInfo f, bool overwriteRemote)
|
||||
{
|
||||
var prefix = options.PathPrefix?.Replace('\\', '/') ?? "";
|
||||
if (!String.IsNullOrWhiteSpace(prefix) && !prefix.EndsWith("/")) prefix += "/";
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public async Task DownloadRecentPackages(S3Options options)
|
||||
{
|
||||
var _client = GetS3Client(options);
|
||||
var _prefix = GetPrefix(options);
|
||||
var releasesDir = options.ReleaseDir;
|
||||
var releasesPath = Path.Combine(releasesDir.FullName, "RELEASES");
|
||||
|
||||
Log.Info($"Downloading latest release to '{releasesDir.FullName}' from S3 bucket '{options.Bucket}'"
|
||||
+ (String.IsNullOrWhiteSpace(_prefix) ? "" : " with prefix '" + _prefix + "'"));
|
||||
|
||||
try {
|
||||
Log.Info("Downloading RELEASES");
|
||||
using (var obj = await _client.GetObjectAsync(options.Bucket, _prefix + "RELEASES"))
|
||||
await obj.WriteResponseStreamToFileAsync(releasesPath, false, CancellationToken.None);
|
||||
} catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) {
|
||||
Log.Warn("RELEASES file not found. No releases to download.");
|
||||
return;
|
||||
}
|
||||
|
||||
var releasesToDownload = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesPath))
|
||||
.Where(x => !x.IsDelta)
|
||||
.OrderByDescending(x => x.Version)
|
||||
.Take(1)
|
||||
.Select(x => new {
|
||||
LocalPath = Path.Combine(releasesDir.FullName, x.OriginalFilename),
|
||||
Filename = x.OriginalFilename,
|
||||
});
|
||||
|
||||
foreach (var releaseToDownload in releasesToDownload) {
|
||||
Log.Info("Downloading " + releaseToDownload.Filename);
|
||||
using (var pkgobj = await _client.GetObjectAsync(options.Bucket, _prefix + releaseToDownload.Filename))
|
||||
await pkgobj.WriteResponseStreamToFileAsync(releaseToDownload.LocalPath, false, CancellationToken.None);
|
||||
}
|
||||
}
|
||||
|
||||
public async Task UploadMissingPackages(S3UploadOptions options)
|
||||
{
|
||||
var _client = GetS3Client(options);
|
||||
var _prefix = GetPrefix(options);
|
||||
var releasesDir = options.ReleaseDir;
|
||||
|
||||
Log.Info($"Uploading releases from '{releasesDir.FullName}' to S3 bucket '{options.Bucket}'"
|
||||
+ (String.IsNullOrWhiteSpace(_prefix) ? "" : " with prefix '" + _prefix + "'"));
|
||||
|
||||
// locate files to upload
|
||||
var files = releasesDir.GetFiles("*", SearchOption.TopDirectoryOnly);
|
||||
var msiFile = files.Where(f => f.FullName.EndsWith(".msi", StringComparison.InvariantCultureIgnoreCase)).SingleOrDefault();
|
||||
var setupFile = files.Where(f => f.FullName.EndsWith("Setup.exe", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ContextualSingle("release directory", "Setup.exe file");
|
||||
var releasesFile = files.Where(f => f.Name.Equals("RELEASES", StringComparison.InvariantCultureIgnoreCase))
|
||||
.ContextualSingle("release directory", "RELEASES file");
|
||||
var nupkgFiles = files.Where(f => f.FullName.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase)).ToArray();
|
||||
|
||||
// we will merge the remote RELEASES file with the local one
|
||||
string remoteReleasesContent = null;
|
||||
try {
|
||||
Log.Info("Downloading remote RELEASES file");
|
||||
using (var obj = await _client.GetObjectAsync(options.Bucket, _prefix + "RELEASES"))
|
||||
using (var sr = new StreamReader(obj.ResponseStream, Encoding.UTF8, true))
|
||||
remoteReleasesContent = await sr.ReadToEndAsync();
|
||||
Log.Info("Merging remote and local RELEASES files");
|
||||
} catch (AmazonS3Exception ex) when (ex.StatusCode == HttpStatusCode.NotFound) {
|
||||
Log.Warn("No remote RELEASES found.");
|
||||
}
|
||||
|
||||
var localReleases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesFile.FullName));
|
||||
var remoteReleases = ReleaseEntry.ParseReleaseFile(remoteReleasesContent);
|
||||
|
||||
// apply retention policy. count '-full' versions only, then also remove corresponding delta packages
|
||||
var releaseEntries = localReleases
|
||||
.Concat(remoteReleases)
|
||||
.DistinctBy(r => r.OriginalFilename) // will preserve the local entries because they appear first
|
||||
.OrderBy(k => k.Version)
|
||||
.ThenBy(k => !k.IsDelta)
|
||||
.ToArray();
|
||||
|
||||
if (releaseEntries.Length == 0) {
|
||||
Log.Warn("No releases found.");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!releaseEntries.All(f => f.PackageId == releaseEntries.First().PackageId)) {
|
||||
throw new Exception("There are mix-matched package Id's in local/remote RELEASES file. " +
|
||||
"Please fix the release files manually so there is only one consistent package Id present.");
|
||||
}
|
||||
|
||||
var fullCount = releaseEntries.Where(r => !r.IsDelta).Count();
|
||||
if (options.KeepMaxReleases > 0 && fullCount > options.KeepMaxReleases) {
|
||||
Log.Info($"Retention Policy: {fullCount - options.KeepMaxReleases} releases will be removed from RELEASES file.");
|
||||
|
||||
var fullReleases = releaseEntries
|
||||
.OrderByDescending(k => k.Version)
|
||||
.Where(k => !k.IsDelta)
|
||||
.Take(options.KeepMaxReleases)
|
||||
.ToArray();
|
||||
|
||||
var deltaReleases = releaseEntries
|
||||
.Where(k => k.IsDelta)
|
||||
.Where(k => fullReleases.Any(f => f.Version == k.Version))
|
||||
.Where(k => k.Version != fullReleases.Last().Version) // ignore delta packages for the oldest full package
|
||||
.ToArray();
|
||||
|
||||
Log.Info($"Total number of packages in remote after retention: {fullReleases.Length} full, {deltaReleases.Length} delta.");
|
||||
fullCount = fullReleases.Length;
|
||||
ReleaseEntry.WriteReleaseFile(fullReleases.Concat(deltaReleases), releasesFile.FullName);
|
||||
} else {
|
||||
Log.Info($"There are currently {fullCount} full releases in RELEASES file.");
|
||||
ReleaseEntry.WriteReleaseFile(releaseEntries, releasesFile.FullName);
|
||||
}
|
||||
|
||||
async Task UploadFile(FileInfo f, bool overwriteRemote)
|
||||
{
|
||||
string key = _prefix + f.Name;
|
||||
string deleteOldVersionId = null;
|
||||
|
||||
// try to detect an existing remote file of the same name
|
||||
try {
|
||||
var metadata = await _client.GetObjectMetadataAsync(options.Bucket, key);
|
||||
var metadata = await client.GetObjectMetadataAsync(bucket, key);
|
||||
var md5 = GetFileMD5Checksum(f.FullName);
|
||||
var stored = metadata?.ETag?.Trim().Trim('"');
|
||||
|
||||
@@ -205,127 +146,18 @@ public class S3Repository
|
||||
}
|
||||
|
||||
var req = new PutObjectRequest {
|
||||
BucketName = options.Bucket,
|
||||
BucketName = bucket,
|
||||
FilePath = f.FullName,
|
||||
Key = key,
|
||||
};
|
||||
|
||||
await RetryAsync(() => _client.PutObjectAsync(req), "Uploading " + f.Name);
|
||||
await RetryAsync(() => client.PutObjectAsync(req), "Uploading " + f.Name);
|
||||
|
||||
if (deleteOldVersionId != null) {
|
||||
await RetryAsync(() => _client.DeleteObjectAsync(options.Bucket, key, deleteOldVersionId),
|
||||
"Removing old version of " + f.Name,
|
||||
throwIfFail: false);
|
||||
}
|
||||
}
|
||||
|
||||
// we need to upload things in a certain order. If we upload 'RELEASES' first, for example, a client
|
||||
// might try to request a nupkg that does not yet exist.
|
||||
|
||||
// upload nupkg's first
|
||||
foreach (var f in nupkgFiles) {
|
||||
if (!releaseEntries.Any(r => r.OriginalFilename.Equals(f.Name, StringComparison.InvariantCultureIgnoreCase))) {
|
||||
Log.Warn($"Upload file '{f.Name}' skipped (not in RELEASES file)");
|
||||
continue;
|
||||
}
|
||||
|
||||
await UploadFile(f, options.Overwrite);
|
||||
}
|
||||
|
||||
// next upload setup files
|
||||
await UploadFile(setupFile, true);
|
||||
if (msiFile != null) await UploadFile(msiFile, true);
|
||||
|
||||
// upload RELEASES
|
||||
await UploadFile(releasesFile, true);
|
||||
|
||||
// ignore dead package cleanup if there is no retention policy
|
||||
if (options.KeepMaxReleases > 0) {
|
||||
// remove any dead packages (not in RELEASES) as they are undiscoverable anyway
|
||||
Log.Info("Searching for remote dead packages (not in RELEASES file)");
|
||||
|
||||
var objects = await ListBucketContentsAsync(_client, options.Bucket, _prefix).ToArrayAsync();
|
||||
|
||||
var deadObjectQuery =
|
||||
from o in objects
|
||||
let key = o.Key
|
||||
where key.EndsWith(".nupkg", StringComparison.InvariantCultureIgnoreCase)
|
||||
where key.StartsWith(_prefix, StringComparison.InvariantCultureIgnoreCase)
|
||||
let fileName = key.Substring(_prefix.Length)
|
||||
where !fileName.Contains('/') // filters out objects in folders if _prefix is empty
|
||||
where !releaseEntries.Any(r => r.OriginalFilename.Equals(fileName, StringComparison.InvariantCultureIgnoreCase))
|
||||
orderby o.LastModified ascending
|
||||
select new { key, fileName, versionId = o.VersionId };
|
||||
|
||||
var deadObj = deadObjectQuery.ToArray();
|
||||
|
||||
Log.Info($"Found {deadObj.Length} dead packages.");
|
||||
foreach (var s3obj in deadObj) {
|
||||
var req = new DeleteObjectRequest { BucketName = options.Bucket, Key = s3obj.key, VersionId = s3obj.versionId };
|
||||
await RetryAsync(() => _client.DeleteObjectAsync(req), "Deleting dead package: " + s3obj, throwIfFail: false);
|
||||
}
|
||||
}
|
||||
|
||||
Log.Info("Done");
|
||||
|
||||
var regionEndpoint = RegionEndpoint.GetBySystemName(options.Region);
|
||||
#pragma warning disable CS0618 // Type or member is obsolete
|
||||
var endpointHost = options.Endpoint ?? regionEndpoint.GetEndpointForService("s3").Hostname;
|
||||
#pragma warning restore CS0618 // Type or member is obsolete
|
||||
|
||||
if (Regex.IsMatch(endpointHost, @"^https?:\/\/", RegexOptions.IgnoreCase)) {
|
||||
endpointHost = new Uri(endpointHost, UriKind.Absolute).Host;
|
||||
}
|
||||
|
||||
var baseurl = $"https://{options.Bucket}.{endpointHost}/{_prefix}";
|
||||
Log.Info($"Bucket URL: {baseurl}");
|
||||
Log.Info($"Setup URL: {baseurl}{setupFile.Name}");
|
||||
}
|
||||
|
||||
private static async IAsyncEnumerable<S3ObjectVersion> ListBucketContentsAsync(IAmazonS3 client, string bucketName, string prefix)
|
||||
{
|
||||
var request = new ListVersionsRequest {
|
||||
BucketName = bucketName,
|
||||
MaxKeys = 100,
|
||||
Prefix = prefix,
|
||||
};
|
||||
|
||||
ListVersionsResponse response;
|
||||
do {
|
||||
response = await client.ListVersionsAsync(request);
|
||||
foreach (var obj in response.Versions) {
|
||||
yield return obj;
|
||||
}
|
||||
|
||||
// If the response is truncated, set the request ContinuationToken
|
||||
// from the NextContinuationToken property of the response.
|
||||
request.KeyMarker = response.NextKeyMarker;
|
||||
request.VersionIdMarker = response.NextVersionIdMarker;
|
||||
} while (response.IsTruncated);
|
||||
}
|
||||
|
||||
private async Task RetryAsync(Func<Task> block, string message, bool throwIfFail = true, bool showMessageFirst = true)
|
||||
{
|
||||
int ctry = 0;
|
||||
while (true) {
|
||||
try {
|
||||
if (showMessageFirst || ctry > 0)
|
||||
Log.Info((ctry > 0 ? $"(retry {ctry}) " : "") + message);
|
||||
await block().ConfigureAwait(false);
|
||||
return;
|
||||
} catch (Exception ex) {
|
||||
if (ctry++ > 2) {
|
||||
if (throwIfFail) {
|
||||
throw;
|
||||
} else {
|
||||
Log.Error("Error: " + ex.Message + ", will not try again.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
Log.Error($"Error: {ex.Message}, retrying in 1 second.");
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
}
|
||||
await RetryAsync(() => client.DeleteObjectAsync(bucket, key, deleteOldVersionId),
|
||||
"Removing old version of " + f.Name);
|
||||
} catch { }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,85 +0,0 @@
|
||||
using System.Reflection;
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace Velopack.Deployment;
|
||||
|
||||
public class HttpDownloadOptions
|
||||
{
|
||||
public DirectoryInfo ReleaseDir { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
public class SimpleWebRepository
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
|
||||
public SimpleWebRepository(ILogger logger)
|
||||
{
|
||||
_logger = logger;
|
||||
}
|
||||
|
||||
public async Task DownloadRecentPackages(HttpDownloadOptions options)
|
||||
{
|
||||
var uri = new Uri(options.Url);
|
||||
var releasesDir = options.ReleaseDir;
|
||||
var releasesUri = Utility.AppendPathToUri(uri, "RELEASES");
|
||||
var releasesIndex = await retryAsync(3, () => downloadReleasesIndex(releasesUri));
|
||||
|
||||
File.WriteAllText(Path.Combine(releasesDir.FullName, "RELEASES"), releasesIndex);
|
||||
|
||||
var releasesToDownload = ReleaseEntry.ParseReleaseFile(releasesIndex)
|
||||
.Where(x => !x.IsDelta)
|
||||
.OrderByDescending(x => x.Version)
|
||||
.Take(1)
|
||||
.Select(x => new {
|
||||
LocalPath = Path.Combine(releasesDir.FullName, x.OriginalFilename),
|
||||
RemoteUrl = new Uri(Utility.EnsureTrailingSlash(uri), x.BaseUrl + x.OriginalFilename + x.Query)
|
||||
});
|
||||
|
||||
foreach (var releaseToDownload in releasesToDownload) {
|
||||
await retryAsync(3, () => downloadRelease(releaseToDownload.LocalPath, releaseToDownload.RemoteUrl));
|
||||
}
|
||||
}
|
||||
|
||||
async Task<string> downloadReleasesIndex(Uri uri)
|
||||
{
|
||||
_logger.Info($"Trying to download RELEASES index from {uri}");
|
||||
|
||||
var userAgent = new System.Net.Http.Headers.ProductInfoHeaderValue("Velopack", Assembly.GetExecutingAssembly().GetName().Version.ToString());
|
||||
using (HttpClient client = new HttpClient()) {
|
||||
client.DefaultRequestHeaders.UserAgent.Add(userAgent);
|
||||
return await client.GetStringAsync(uri);
|
||||
}
|
||||
}
|
||||
|
||||
async Task downloadRelease(string localPath, Uri remoteUrl)
|
||||
{
|
||||
if (File.Exists(localPath)) {
|
||||
File.Delete(localPath);
|
||||
}
|
||||
|
||||
_logger.Info($"Downloading release from {remoteUrl}");
|
||||
var wc = Utility.CreateDefaultDownloader();
|
||||
await wc.DownloadFile(remoteUrl.ToString(), localPath, null);
|
||||
}
|
||||
|
||||
static async Task<T> retryAsync<T>(int count, Func<Task<T>> block)
|
||||
{
|
||||
int retryCount = count;
|
||||
|
||||
retry:
|
||||
try {
|
||||
return await block();
|
||||
} catch (Exception) {
|
||||
retryCount--;
|
||||
if (retryCount >= 0) goto retry;
|
||||
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
static async Task retryAsync(int count, Func<Task> block)
|
||||
{
|
||||
await retryAsync(count, async () => { await block(); return false; });
|
||||
}
|
||||
}
|
||||
@@ -6,10 +6,6 @@
|
||||
<NoWarn>$(NoWarn);CA2007;CS8002</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Velopack\Velopack.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AWSSDK.S3" Version="3.7.305" />
|
||||
<PackageReference Include="Octokit" Version="9.0.0" />
|
||||
@@ -17,4 +13,8 @@
|
||||
<PackageReference Include="NuGet.Commands" Version="6.8.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Velopack.Packaging\Velopack.Packaging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
145
src/Velopack.Deployment/_Repository.cs
Normal file
145
src/Velopack.Deployment/_Repository.cs
Normal file
@@ -0,0 +1,145 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Velopack.Sources;
|
||||
using Velopack.Packaging;
|
||||
|
||||
namespace Velopack.Deployment;
|
||||
|
||||
public class RepositoryOptions
|
||||
{
|
||||
public string Channel { get; set; }
|
||||
|
||||
public DirectoryInfo ReleaseDir { get; set; }
|
||||
}
|
||||
|
||||
public interface IRepositoryCanUpload<TUp> where TUp : RepositoryOptions
|
||||
{
|
||||
Task UploadMissingAssetsAsync(TUp options);
|
||||
}
|
||||
|
||||
public interface IRepositoryCanDownload<TDown> where TDown : RepositoryOptions
|
||||
{
|
||||
Task DownloadLatestFullPackageAsync(TDown options);
|
||||
}
|
||||
|
||||
public abstract class SourceRepository<TDown, TSource> : DownRepository<TDown>
|
||||
where TDown : RepositoryOptions
|
||||
where TSource : IUpdateSource
|
||||
{
|
||||
public SourceRepository(ILogger logger)
|
||||
: base(logger)
|
||||
{ }
|
||||
|
||||
protected override Task<ReleaseEntry[]> GetReleasesAsync(TDown options)
|
||||
{
|
||||
var source = CreateSource(options);
|
||||
return source.GetReleaseFeed();
|
||||
}
|
||||
|
||||
protected override Task SaveEntryToFileAsync(TDown options, ReleaseEntry entry, string filePath)
|
||||
{
|
||||
var source = CreateSource(options);
|
||||
return source.DownloadReleaseEntry(entry, filePath, (i) => { });
|
||||
}
|
||||
|
||||
public abstract TSource CreateSource(TDown options);
|
||||
}
|
||||
|
||||
public abstract class DownRepository<TDown> : IRepositoryCanDownload<TDown>
|
||||
where TDown : RepositoryOptions
|
||||
{
|
||||
protected ILogger Log { get; }
|
||||
|
||||
public DownRepository(ILogger logger)
|
||||
{
|
||||
Log = logger;
|
||||
}
|
||||
|
||||
public virtual async Task DownloadLatestFullPackageAsync(TDown options)
|
||||
{
|
||||
var releasesName = SourceBase.GetReleasesFileNameImpl(options.Channel);
|
||||
|
||||
ReleaseEntry[] releases = await RetryAsyncRet(() => GetReleasesAsync(options), $"Fetching {releasesName}...");
|
||||
|
||||
Log.Info($"Found {releases.Length} release in remote file");
|
||||
|
||||
var latest = releases.Where(r => !r.IsDelta).OrderByDescending(r => r.Version).FirstOrDefault();
|
||||
if (latest == null) {
|
||||
Log.Warn("No full / applicible release was found to download. Aborting.");
|
||||
return;
|
||||
}
|
||||
|
||||
var path = Path.Combine(options.ReleaseDir.FullName, latest.OriginalFilename);
|
||||
var incomplete = Path.Combine(options.ReleaseDir.FullName, latest.OriginalFilename + ".incomplete");
|
||||
|
||||
if (File.Exists(path)) {
|
||||
Log.Warn($"File '{path}' already exists on disk. Verifying checksum...");
|
||||
var hash = Utility.CalculateFileSHA1(path);
|
||||
if (hash == latest.SHA1) {
|
||||
Log.Info("Checksum matches. Finished.");
|
||||
return;
|
||||
} else {
|
||||
Log.Info($"Checksum mismatch, re-downloading...");
|
||||
}
|
||||
}
|
||||
|
||||
await RetryAsync(() => SaveEntryToFileAsync(options, latest, incomplete), $"Downloading {latest.OriginalFilename}...");
|
||||
|
||||
Log.Info("Verifying checksum...");
|
||||
var newHash = Utility.CalculateFileSHA1(incomplete);
|
||||
if (newHash != latest.SHA1) {
|
||||
Log.Error($"Checksum mismatch, expected {latest.SHA1}, got {newHash}");
|
||||
return;
|
||||
}
|
||||
|
||||
File.Move(incomplete, path, true);
|
||||
Log.Info("Finished.");
|
||||
}
|
||||
|
||||
protected abstract Task<ReleaseEntry[]> GetReleasesAsync(TDown options);
|
||||
|
||||
protected abstract Task SaveEntryToFileAsync(TDown options, ReleaseEntry entry, string filePath);
|
||||
|
||||
protected async Task<T> RetryAsyncRet<T>(Func<Task<T>> block, string message, int maxRetries = 3)
|
||||
{
|
||||
int ctry = 0;
|
||||
while (true) {
|
||||
try {
|
||||
Log.Info((ctry > 0 ? $"(retry {ctry}) " : "") + message);
|
||||
return await block().ConfigureAwait(false);
|
||||
} catch (Exception ex) {
|
||||
if (ctry++ > maxRetries) {
|
||||
Log.Error(ex.Message + ", will not try again.");
|
||||
throw;
|
||||
}
|
||||
|
||||
Log.Error($"{ex.Message}, retrying in 1 second.");
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected async Task RetryAsync(Func<Task> block, string message, int maxRetries = 3)
|
||||
{
|
||||
int ctry = 0;
|
||||
while (true) {
|
||||
try {
|
||||
Log.Info((ctry > 0 ? $"(retry {ctry}) " : "") + message);
|
||||
await block().ConfigureAwait(false);
|
||||
return;
|
||||
} catch (Exception ex) {
|
||||
if (ctry++ > maxRetries) {
|
||||
Log.Error(ex.Message + ", will not try again.");
|
||||
throw;
|
||||
}
|
||||
|
||||
Log.Error($"{ex.Message}, retrying in 1 second.");
|
||||
await Task.Delay(1000).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -16,12 +16,15 @@ public class OsxPackCommandRunner
|
||||
|
||||
public void Releasify(OsxPackOptions options)
|
||||
{
|
||||
if (options.TargetRuntime.BaseRID != RuntimeOs.OSX)
|
||||
throw new ArgumentException("Target runtime must be OSX.", nameof(options.TargetRuntime));
|
||||
|
||||
var releaseDir = options.ReleaseDir;
|
||||
var channel = options.Channel?.ToLower() ?? "osx"; // default channel for mac packages.
|
||||
var channel = options.Channel?.ToLower() ?? ReleaseEntryHelper.GetDefaultChannel(RuntimeOs.OSX);
|
||||
|
||||
var helper = new HelperExe(_logger);
|
||||
var entryHelper = new ReleaseEntryHelper(releaseDir.FullName, _logger);
|
||||
entryHelper.ValidateEntriesForPackaging(SemanticVersion.Parse(options.PackVersion), channel);
|
||||
entryHelper.ValidateChannelForPackaging(SemanticVersion.Parse(options.PackVersion), channel, options.TargetRuntime);
|
||||
|
||||
bool deleteAppBundle = false;
|
||||
string appBundlePath = options.PackDirectory;
|
||||
@@ -39,6 +42,11 @@ public class OsxPackCommandRunner
|
||||
var packAuthors = options.PackAuthors;
|
||||
var packVersion = options.PackVersion;
|
||||
|
||||
var suffix = ReleaseEntryHelper.GetPkgSuffix(RuntimeOs.OSX, channel);
|
||||
if (!String.IsNullOrWhiteSpace(suffix)) {
|
||||
options.PackVersion += suffix;
|
||||
}
|
||||
|
||||
_logger.Info("Adding Squirrel resources to bundle.");
|
||||
var nuspecText = NugetConsole.CreateNuspec(
|
||||
packId, packTitle, packAuthors, packVersion, options.ReleaseNotes, options.IncludePdb);
|
||||
@@ -48,7 +56,7 @@ public class OsxPackCommandRunner
|
||||
File.WriteAllText(nuspecPath, nuspecText);
|
||||
File.Copy(helper.UpdateMacPath, Path.Combine(structure.MacosDirectory, "UpdateMac"), true);
|
||||
|
||||
var zipPath = Path.Combine(releaseDir.FullName, $"{options.PackId}-[{options.TargetRuntime.ToDisplay(RidDisplayType.NoVersion)}]-Portable.zip");
|
||||
var zipPath = entryHelper.GetSuggestedPortablePath(packId, channel, options.TargetRuntime);
|
||||
if (File.Exists(zipPath)) File.Delete(zipPath);
|
||||
|
||||
// code signing all mach-o binaries
|
||||
@@ -92,7 +100,7 @@ public class OsxPackCommandRunner
|
||||
|
||||
// create installer package, sign and notarize
|
||||
if (!options.NoPackage) {
|
||||
var pkgPath = Path.Combine(releaseDir.FullName, $"{packId}-[{options.TargetRuntime.ToDisplay(RidDisplayType.NoVersion)}]-Setup.pkg");
|
||||
var pkgPath = entryHelper.GetSuggestedSetupPath(packId, channel, options.TargetRuntime);
|
||||
|
||||
Dictionary<string, string> pkgContent = new() {
|
||||
{"welcome", options.PackageWelcome },
|
||||
|
||||
@@ -16,9 +16,6 @@ public class WindowsPackCommandRunner
|
||||
|
||||
public void Pack(WindowsPackOptions options)
|
||||
{
|
||||
if (options.EntryExecutableName == null)
|
||||
options.EntryExecutableName = options.PackId + ".exe";
|
||||
|
||||
using (Utility.GetTempDirectory(out var tmp)) {
|
||||
var nupkgPath = new NugetConsole(_logger).CreatePackageFromOptions(tmp, options);
|
||||
options.Package = nupkgPath;
|
||||
|
||||
@@ -18,11 +18,14 @@ public class WindowsReleasifyCommandRunner
|
||||
|
||||
public void Releasify(WindowsReleasifyOptions options)
|
||||
{
|
||||
if (options.TargetRuntime?.BaseRID != RuntimeOs.Windows)
|
||||
throw new ArgumentException("Target runtime must be Windows.", nameof(options.TargetRuntime));
|
||||
|
||||
var targetDir = options.ReleaseDir.FullName;
|
||||
var package = options.Package;
|
||||
var backgroundGif = options.SplashImage;
|
||||
var setupIcon = options.Icon;
|
||||
var channel = options.Channel?.ToLower();
|
||||
var channel = options.Channel?.ToLower() ?? ReleaseEntryHelper.GetDefaultChannel(RuntimeOs.Windows);
|
||||
|
||||
// normalize and validate that the provided frameworks are supported
|
||||
IEnumerable<Runtimes.RuntimeInfo> requiredFrameworks = Enumerable.Empty<Runtimes.RuntimeInfo>();
|
||||
@@ -57,7 +60,7 @@ public class WindowsReleasifyCommandRunner
|
||||
var rp = new ReleasePackageBuilder(_logger, fileToProcess);
|
||||
|
||||
var entryHelper = new ReleaseEntryHelper(targetDir, _logger);
|
||||
entryHelper.ValidateEntriesForPackaging(rp.Version, channel);
|
||||
entryHelper.ValidateChannelForPackaging(rp.Version, channel, options.TargetRuntime);
|
||||
|
||||
rp.CreateReleasePackage(contentsPostProcessHook: (pkgPath, zpkg) => {
|
||||
var nuspecPath = Directory.GetFiles(pkgPath, "*.nuspec", SearchOption.TopDirectoryOnly)
|
||||
@@ -65,14 +68,15 @@ public class WindowsReleasifyCommandRunner
|
||||
var libDir = Directory.GetDirectories(Path.Combine(pkgPath, "lib"))
|
||||
.ContextualSingle("package", "'lib' folder");
|
||||
|
||||
var mainExe = Path.Combine(libDir, options.EntryExecutableName);
|
||||
var mainExeName = options.EntryExecutableName ?? zpkg.Id + ".exe";
|
||||
var mainExe = Path.Combine(libDir, mainExeName);
|
||||
if (!File.Exists(mainExe))
|
||||
throw new ArgumentException($"--exeName '{options.EntryExecutableName}' does not exist in package. Searched at: '{mainExe}'");
|
||||
throw new ArgumentException($"--exeName '{mainExeName}' does not exist in package. Searched at: '{mainExe}'");
|
||||
|
||||
try {
|
||||
var psi = new ProcessStartInfo(mainExe);
|
||||
psi.AppendArgumentListSafe(new[] { "--veloapp-version" }, out var _);
|
||||
var output = psi.Output(5000).GetAwaiterResult();
|
||||
var output = psi.Output(3000);
|
||||
if (String.IsNullOrWhiteSpace(output)) {
|
||||
throw new Exception("Process exited with no output.");
|
||||
}
|
||||
@@ -102,7 +106,10 @@ public class WindowsReleasifyCommandRunner
|
||||
"Please publish your application to a folder without ClickOnce.");
|
||||
}
|
||||
|
||||
NuspecManifest.SetMetadata(nuspecPath, options.EntryExecutableName, requiredFrameworks.Select(r => r.Id), options.TargetRuntime);
|
||||
var versionSuffix = ReleaseEntryHelper.GetPkgSuffix(RuntimeOs.Windows, options.Channel);
|
||||
var versionOverride = String.IsNullOrWhiteSpace(versionSuffix)
|
||||
? zpkg.Version : SemanticVersion.Parse(zpkg.Version.ToFullString() + versionSuffix);
|
||||
NuspecManifest.SetMetadata(nuspecPath, options.EntryExecutableName, requiredFrameworks.Select(r => r.Id), options.TargetRuntime, versionOverride.ToFullString());
|
||||
|
||||
// copy Update.exe into package, so it can also be updated in both full/delta packages
|
||||
// and do it before signing so that Update.exe will also be signed. It is renamed to
|
||||
@@ -121,7 +128,7 @@ public class WindowsReleasifyCommandRunner
|
||||
if (setupIcon != null) File.Copy(setupIcon, Path.Combine(pkgPath, "setup.ico"), true);
|
||||
if (backgroundGif != null) File.Copy(backgroundGif, Path.Combine(pkgPath, "splashimage" + Path.GetExtension(backgroundGif)));
|
||||
|
||||
var releaseName = new ReleaseEntryName(spec.Id, spec.Version, false, options.TargetRuntime);
|
||||
var releaseName = new ReleaseEntryName(spec.Id, versionOverride, false, options.TargetRuntime);
|
||||
return Path.Combine(targetDir, releaseName.ToFileName());
|
||||
});
|
||||
|
||||
@@ -141,7 +148,7 @@ public class WindowsReleasifyCommandRunner
|
||||
entryHelper.SaveReleasesFiles();
|
||||
|
||||
var bundledzp = new ZipPackage(package);
|
||||
var targetSetupExe = Path.Combine(targetDir, $"{bundledzp.Id}-[{options.TargetRuntime.ToDisplay(RidDisplayType.NoVersion)}]-Setup.exe");
|
||||
var targetSetupExe = entryHelper.GetSuggestedSetupPath(bundledzp.Id, channel, options.TargetRuntime);
|
||||
File.Copy(helper.SetupPath, targetSetupExe, true);
|
||||
|
||||
if (VelopackRuntimeInfo.IsWindows) {
|
||||
|
||||
@@ -6,6 +6,7 @@ using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using NuGet.Versioning;
|
||||
using Velopack.Sources;
|
||||
|
||||
namespace Velopack.Packaging
|
||||
{
|
||||
@@ -15,7 +16,7 @@ namespace Velopack.Packaging
|
||||
private readonly ILogger _logger;
|
||||
private Dictionary<string, List<ReleaseEntry>> _releases;
|
||||
|
||||
public const string DEFAULT_CHANNEL = "default";
|
||||
private const string BLANK_CHANNEL = "default";
|
||||
|
||||
public ReleaseEntryHelper(string outputDir, ILogger logger)
|
||||
{
|
||||
@@ -24,7 +25,7 @@ namespace Velopack.Packaging
|
||||
_releases = new Dictionary<string, List<ReleaseEntry>>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (var releaseFile in Directory.EnumerateFiles(outputDir, "RELEASES*")) {
|
||||
var fn = Path.GetFileName(releaseFile);
|
||||
var channel = fn.StartsWith("RELEASES-", StringComparison.OrdinalIgnoreCase) ? fn.Substring(9) : DEFAULT_CHANNEL;
|
||||
var channel = fn.StartsWith("RELEASES-", StringComparison.OrdinalIgnoreCase) ? fn.Substring(9) : BLANK_CHANNEL;
|
||||
var releases = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releaseFile)).ToList();
|
||||
_logger.Info($"Loaded {releases.Count} entries from: {releaseFile}");
|
||||
// this allows us to collapse RELEASES files with the same channel but different case on file systems
|
||||
@@ -37,14 +38,12 @@ namespace Velopack.Packaging
|
||||
}
|
||||
}
|
||||
|
||||
public void ValidateEntriesForPackaging(SemanticVersion version, string channel)
|
||||
public void ValidateChannelForPackaging(SemanticVersion version, string channel, RID rid)
|
||||
{
|
||||
if (!_releases.ContainsKey(channel) || !_releases[channel].Any())
|
||||
return;
|
||||
|
||||
RID rid = null;
|
||||
foreach (var release in _releases[channel]) {
|
||||
if (rid == null) rid = release.Rid;
|
||||
if (release.Rid != rid) {
|
||||
throw new ArgumentException("All releases in a channel must have the same RID. Please correct RELEASES file or change channel name: " + GetReleasePath(channel));
|
||||
}
|
||||
@@ -60,7 +59,7 @@ namespace Velopack.Packaging
|
||||
if (releases == null || !releases.Any()) return null;
|
||||
var entry = releases
|
||||
.Where(x => x.IsDelta == false)
|
||||
.Where(x => x.Version < version)
|
||||
.Where(x => VersionComparer.Version.Compare(x.Version, version) < 0)
|
||||
.OrderByDescending(x => x.Version)
|
||||
.FirstOrDefault();
|
||||
if (entry == null) return null;
|
||||
@@ -68,6 +67,13 @@ namespace Velopack.Packaging
|
||||
return new ReleasePackageBuilder(_logger, file, true);
|
||||
}
|
||||
|
||||
public ReleaseEntry GetLatestFullRelease(string channel)
|
||||
{
|
||||
var releases = _releases.ContainsKey(channel) ? _releases[channel] : null;
|
||||
if (releases == null || !releases.Any()) return null;
|
||||
return releases.Where(z => !z.IsDelta).MaxBy(z => z.Version).First();
|
||||
}
|
||||
|
||||
public void AddRemoteReleaseEntries(IEnumerable<ReleaseEntry> entries, string channel)
|
||||
{
|
||||
if (!_releases.ContainsKey(channel))
|
||||
@@ -110,49 +116,105 @@ namespace Velopack.Packaging
|
||||
}
|
||||
}
|
||||
|
||||
private string GetReleasePath(string channel)
|
||||
public static string GetPkgSuffix(RuntimeOs os, string channel)
|
||||
{
|
||||
return Path.Combine(_outputDir, channel == DEFAULT_CHANNEL ? "RELEASES" : $"RELEASES-{channel.ToLower()}");
|
||||
if (channel == null) return "";
|
||||
if (channel == BLANK_CHANNEL) return "";
|
||||
if (channel == "osx" && os == RuntimeOs.OSX) return "";
|
||||
return "-" + channel.ToLower();
|
||||
}
|
||||
|
||||
public IEnumerable<FileInfo> GetUploadAssets()
|
||||
public static string GetDefaultChannel(RuntimeOs os)
|
||||
{
|
||||
foreach (var rel in Directory.EnumerateFiles(_outputDir, "*.nupkg")) {
|
||||
if (_releases.Any(kvp => kvp.Value.Any(x => Path.GetFileName(rel).Equals(x.OriginalFilename, StringComparison.OrdinalIgnoreCase)))) {
|
||||
yield return new FileInfo(rel);
|
||||
if (os == RuntimeOs.Windows) return BLANK_CHANNEL;
|
||||
if (os == RuntimeOs.OSX) return "osx";
|
||||
throw new NotSupportedException("Unsupported OS: " + os);
|
||||
}
|
||||
|
||||
public string GetReleasePath(string channel)
|
||||
{
|
||||
return Path.Combine(_outputDir, SourceBase.GetReleasesFileNameImpl(channel));
|
||||
}
|
||||
|
||||
public enum AssetsMode
|
||||
{
|
||||
AllPackages,
|
||||
OnlyLatest,
|
||||
}
|
||||
|
||||
public class AssetUploadInfo
|
||||
{
|
||||
public List<FileInfo> Files { get; } = new List<FileInfo>();
|
||||
|
||||
public List<ReleaseEntry> Releases { get; } = new List<ReleaseEntry>();
|
||||
|
||||
public string ReleasesFileName { get; set; }
|
||||
}
|
||||
|
||||
public AssetUploadInfo GetUploadAssets(string channel, AssetsMode mode)
|
||||
{
|
||||
var ret = new AssetUploadInfo();
|
||||
var os = VelopackRuntimeInfo.SystemOs;
|
||||
channel ??= GetDefaultChannel(os);
|
||||
var suffix = GetPkgSuffix(os, channel);
|
||||
|
||||
if (!_releases.ContainsKey(channel))
|
||||
throw new ArgumentException("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);
|
||||
|
||||
ReleaseEntry latest = GetLatestFullRelease(channel);
|
||||
if (latest == null) {
|
||||
throw new ArgumentException("No full releases found for channel: " + channel);
|
||||
} else {
|
||||
_logger.Warn($"Asset '{rel}' is not in any RELEASES file, it will be ignored.");
|
||||
_logger.Info("Latest local release: " + latest.OriginalFilename);
|
||||
}
|
||||
|
||||
foreach (var rel in Directory.EnumerateFiles(_outputDir, "*.nupkg")) {
|
||||
var entry = _releases[channel].FirstOrDefault(x => Path.GetFileName(rel).Equals(x.OriginalFilename, StringComparison.OrdinalIgnoreCase));
|
||||
if (entry != null) {
|
||||
if (mode != AssetsMode.OnlyLatest || latest.Version == entry.Version) {
|
||||
_logger.Info($"Discovered asset: {rel}");
|
||||
ret.Files.Add(new FileInfo(rel));
|
||||
ret.Releases.Add(entry);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
foreach (var rel in Directory.EnumerateFiles(_outputDir, "*-Portable.zip")) {
|
||||
yield return new FileInfo(rel);
|
||||
foreach (var rel in Directory.EnumerateFiles(_outputDir, $"*{suffix}-Portable.zip")) {
|
||||
_logger.Info($"Discovered asset: {rel}");
|
||||
ret.Files.Add(new FileInfo(rel));
|
||||
}
|
||||
|
||||
foreach (var rel in Directory.EnumerateFiles(_outputDir, "*-Setup.exe")) {
|
||||
yield return new FileInfo(rel);
|
||||
foreach (var rel in Directory.EnumerateFiles(_outputDir, $"*{suffix}-Setup.exe")) {
|
||||
_logger.Info($"Discovered asset: {rel}");
|
||||
ret.Files.Add(new FileInfo(rel));
|
||||
}
|
||||
|
||||
foreach (var rel in Directory.EnumerateFiles(_outputDir, "*-Setup.pkg")) {
|
||||
yield return new FileInfo(rel);
|
||||
foreach (var rel in Directory.EnumerateFiles(_outputDir, $"*{suffix}-Setup.pkg")) {
|
||||
_logger.Info($"Discovered asset: {rel}");
|
||||
ret.Files.Add(new FileInfo(rel));
|
||||
}
|
||||
|
||||
foreach (var rel in Directory.EnumerateFiles(_outputDir, "RELEASES*")) {
|
||||
yield return new FileInfo(rel);
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
public string GetSuggestedPortablePath(string id, RID rid)
|
||||
public string GetSuggestedPortablePath(string id, string channel, RID rid)
|
||||
{
|
||||
return Path.Combine(_outputDir, $"{id}-[{rid.ToDisplay(RidDisplayType.NoVersion)}]-Portable.zip");
|
||||
var suffix = GetPkgSuffix(rid.BaseRID, channel);
|
||||
return Path.Combine(_outputDir, $"{id}-[{rid.ToDisplay(RidDisplayType.NoVersion)}]{suffix}-Portable.zip");
|
||||
}
|
||||
|
||||
public string GetSuggestedSetupPath(string id, RID rid)
|
||||
public string GetSuggestedSetupPath(string id, string channel, RID rid)
|
||||
{
|
||||
var suffix = GetPkgSuffix(rid.BaseRID, channel);
|
||||
if (rid.BaseRID == RuntimeOs.Windows)
|
||||
return Path.Combine(_outputDir, $"{id}-[{rid.ToDisplay(RidDisplayType.NoVersion)}]-Setup.exe");
|
||||
return Path.Combine(_outputDir, $"{id}-[{rid.ToDisplay(RidDisplayType.NoVersion)}]{suffix}-Setup.exe");
|
||||
else if (rid.BaseRID == RuntimeOs.OSX)
|
||||
return Path.Combine(_outputDir, $"{id}-[{rid.ToDisplay(RidDisplayType.NoVersion)}]-Setup.pkg");
|
||||
return Path.Combine(_outputDir, $"{id}-[{rid.ToDisplay(RidDisplayType.NoVersion)}]{suffix}-Setup.pkg");
|
||||
else
|
||||
throw new NotSupportedException("RID not supported: " + rid);
|
||||
}
|
||||
|
||||
@@ -26,8 +26,6 @@ public class OsxPackCommand : OsxBundleCommand
|
||||
|
||||
public string NotaryProfile { get; private set; }
|
||||
|
||||
public string Channel { get; private set; }
|
||||
|
||||
public OsxPackCommand()
|
||||
: base("pack", "Converts application files into a release and installer.")
|
||||
{
|
||||
@@ -40,12 +38,6 @@ public class OsxPackCommand : OsxBundleCommand
|
||||
.SetDefault(DeltaMode.BestSpeed)
|
||||
.SetDescription("Set the delta generation mode.");
|
||||
|
||||
AddOption<string>((v) => Channel = v, "-c", "--channel")
|
||||
.SetDescription("Release channel to use when creating the package.")
|
||||
.SetDefault("osx")
|
||||
.RequiresValidNuGetId()
|
||||
.SetArgumentHelpName("NAME");
|
||||
|
||||
AddOption<bool>((v) => NoPackage = v, "--noPkg")
|
||||
.SetDescription("Skip generating a .pkg installer.");
|
||||
|
||||
|
||||
@@ -15,8 +15,6 @@ public class S3BaseCommand : OutputCommand
|
||||
|
||||
public string Bucket { get; private set; }
|
||||
|
||||
public string PathPrefix { get; private set; }
|
||||
|
||||
protected S3BaseCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
@@ -50,10 +48,6 @@ public class S3BaseCommand : OutputCommand
|
||||
.SetDescription("Name of the S3 bucket.")
|
||||
.SetArgumentHelpName("NAME")
|
||||
.SetRequired();
|
||||
|
||||
AddOption<string>((v) => PathPrefix = v, "--pathPrefix")
|
||||
.SetDescription("A sub-folder used for files in the bucket, for creating release channels (eg. 'stable' or 'dev').")
|
||||
.SetArgumentHelpName("PREFIX");
|
||||
}
|
||||
|
||||
private static void MustBeValidAwsRegion(OptionResult result)
|
||||
|
||||
@@ -5,18 +5,12 @@ public class S3UploadCommand : S3BaseCommand
|
||||
{
|
||||
public bool Overwrite { get; private set; }
|
||||
|
||||
public int KeepMaxReleases { get; private set; }
|
||||
|
||||
public S3UploadCommand()
|
||||
: base("s3", "Upload releases to a S3 bucket.")
|
||||
{
|
||||
AddOption<bool>((v) => Overwrite = v, "--overwrite")
|
||||
.SetDescription("Replace remote files if local files have changed.");
|
||||
|
||||
AddOption<int>((v) => KeepMaxReleases = v, "--keepMaxReleases")
|
||||
.SetDescription("Apply a retention policy which keeps only the specified number of old versions in remote source.")
|
||||
.SetArgumentHelpName("NUMBER");
|
||||
|
||||
ReleaseDirectoryOption.SetRequired();
|
||||
ReleaseDirectoryOption.MustNotBeEmpty();
|
||||
}
|
||||
|
||||
@@ -16,8 +16,6 @@ public class WindowsReleasifyCommand : WindowsSigningCommand
|
||||
|
||||
public string EntryExecutableName { get; private set; }
|
||||
|
||||
public string Channel { get; private set; }
|
||||
|
||||
public WindowsReleasifyCommand()
|
||||
: this("releasify", "Take an existing nuget package and convert it into a release.")
|
||||
{
|
||||
@@ -60,11 +58,5 @@ public class WindowsReleasifyCommand : WindowsSigningCommand
|
||||
.SetDescription("The file name of the main/entry executable.")
|
||||
.SetArgumentHelpName("NAME")
|
||||
.RequiresExtension(".exe");
|
||||
|
||||
AddOption<string>((v) => Channel = v, "-c", "--channel")
|
||||
.SetDescription("Release channel to use when creating the package.")
|
||||
.SetDefault(ReleaseEntryHelper.DEFAULT_CHANNEL)
|
||||
.RequiresValidNuGetId()
|
||||
.SetArgumentHelpName("NAME");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,15 +3,20 @@ using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Velopack.Packaging;
|
||||
|
||||
namespace Velopack.Vpk.Commands
|
||||
{
|
||||
public class OutputCommand : BaseCommand
|
||||
public abstract class OutputCommand : BaseCommand
|
||||
{
|
||||
public string ReleaseDirectory { get; private set; }
|
||||
|
||||
public string Channel { get; private set; }
|
||||
|
||||
protected CliOption<DirectoryInfo> ReleaseDirectoryOption { get; private set; }
|
||||
|
||||
protected CliOption<string> ChannelOption { get; private set; }
|
||||
|
||||
protected OutputCommand(string name, string description)
|
||||
: base(name, description)
|
||||
{
|
||||
@@ -19,6 +24,12 @@ namespace Velopack.Vpk.Commands
|
||||
.SetDescription("Output directory for created packages.")
|
||||
.SetArgumentHelpName("DIR")
|
||||
.SetDefault(new DirectoryInfo(".\\Releases"));
|
||||
|
||||
ChannelOption = AddOption<string>((v) => Channel = v, "-c", "--channel")
|
||||
.SetDescription("The channel to use for this release.")
|
||||
.RequiresValidNuGetId()
|
||||
.SetArgumentHelpName("NAME")
|
||||
.SetDefault(ReleaseEntryHelper.GetDefaultChannel(VelopackRuntimeInfo.SystemOs));
|
||||
}
|
||||
|
||||
public DirectoryInfo GetReleaseDirectory()
|
||||
|
||||
@@ -6,24 +6,19 @@ using System.Threading.Tasks;
|
||||
|
||||
namespace Velopack.Vpk.Commands
|
||||
{
|
||||
public class PlatformCommand : OutputCommand
|
||||
public abstract class PlatformCommand : OutputCommand
|
||||
{
|
||||
public string TargetRuntime { get; set; }
|
||||
|
||||
//public FileSystemInfo SolutionDir { get; set; }
|
||||
protected CliOption<string> TargetRuntimeOption { get; private set; }
|
||||
|
||||
protected PlatformCommand(string name, string description) : base(name, description)
|
||||
{
|
||||
TargetRuntime = VelopackRuntimeInfo.SystemOs.GetOsShortName();
|
||||
|
||||
AddOption<string>((v) => TargetRuntime = v, "-r", "--runtime")
|
||||
TargetRuntimeOption = AddOption<string>((v) => TargetRuntime = v, "-r", "--runtime")
|
||||
.SetDescription("The target runtime to build packages for.")
|
||||
.SetArgumentHelpName("RID")
|
||||
.SetDefault(VelopackRuntimeInfo.SystemOs.GetOsShortName())
|
||||
.MustBeSupportedRid();
|
||||
|
||||
//AddOption<FileSystemInfo>((v) => SolutionDir = v, "--sln")
|
||||
// .SetDescription("Explicit path to project solution (.sln)")
|
||||
// .AcceptExistingOnly();
|
||||
}
|
||||
|
||||
public RID GetRid() => RID.Parse(TargetRuntime ?? VelopackRuntimeInfo.SystemOs.GetOsShortName());
|
||||
|
||||
@@ -122,8 +122,9 @@ public class EmbeddedRunner : ICommandRunner
|
||||
ReleaseDir = command.GetReleaseDirectory(),
|
||||
RepoUrl = command.RepoUrl,
|
||||
Token = command.Token,
|
||||
Channel = command.Channel,
|
||||
};
|
||||
return new GitHubRepository(_logger).DownloadRecentPackages(options);
|
||||
return new GitHubRepository(_logger).DownloadLatestFullPackageAsync(options);
|
||||
}
|
||||
|
||||
public virtual Task ExecuteGithubUpload(GitHubUploadCommand command)
|
||||
@@ -134,8 +135,9 @@ public class EmbeddedRunner : ICommandRunner
|
||||
Token = command.Token,
|
||||
Publish = command.Publish,
|
||||
ReleaseName = command.ReleaseName,
|
||||
Channel = command.Channel,
|
||||
};
|
||||
return new GitHubRepository(_logger).UploadMissingPackages(options);
|
||||
return new GitHubRepository(_logger).UploadMissingAssetsAsync(options);
|
||||
}
|
||||
|
||||
public virtual Task ExecuteHttpDownload(HttpDownloadCommand command)
|
||||
@@ -143,23 +145,24 @@ public class EmbeddedRunner : ICommandRunner
|
||||
var options = new HttpDownloadOptions {
|
||||
ReleaseDir = command.GetReleaseDirectory(),
|
||||
Url = command.Url,
|
||||
Channel = command.Channel,
|
||||
};
|
||||
return new SimpleWebRepository(_logger).DownloadRecentPackages(options);
|
||||
return new HttpRepository(_logger).DownloadLatestFullPackageAsync(options);
|
||||
}
|
||||
|
||||
public virtual Task ExecuteS3Download(S3DownloadCommand command)
|
||||
{
|
||||
var options = new S3Options {
|
||||
var options = new S3DownloadOptions {
|
||||
Bucket = command.Bucket,
|
||||
Endpoint = command.Endpoint,
|
||||
Session = command.Session,
|
||||
KeyId = command.KeyId,
|
||||
PathPrefix = command.PathPrefix,
|
||||
Region = command.Region,
|
||||
ReleaseDir = command.GetReleaseDirectory(),
|
||||
Session = command.Session,
|
||||
Secret = command.Secret,
|
||||
Channel = command.Channel,
|
||||
};
|
||||
return new S3Repository(_logger).DownloadRecentPackages(options);
|
||||
return new S3Repository(_logger).DownloadLatestFullPackageAsync(options);
|
||||
}
|
||||
|
||||
public virtual Task ExecuteS3Upload(S3UploadCommand command)
|
||||
@@ -168,14 +171,14 @@ public class EmbeddedRunner : ICommandRunner
|
||||
Bucket = command.Bucket,
|
||||
Endpoint = command.Endpoint,
|
||||
KeyId = command.KeyId,
|
||||
PathPrefix = command.PathPrefix,
|
||||
Session = command.Session,
|
||||
Region = command.Region,
|
||||
ReleaseDir = command.GetReleaseDirectory(),
|
||||
Secret = command.Secret,
|
||||
KeepMaxReleases = command.KeepMaxReleases,
|
||||
Overwrite = command.Overwrite,
|
||||
Channel = command.Channel,
|
||||
};
|
||||
return new S3Repository(_logger).UploadMissingPackages(options);
|
||||
return new S3Repository(_logger).UploadMissingAssetsAsync(options);
|
||||
}
|
||||
|
||||
public virtual Task ExecuteDeltaGen(DeltaGenCommand command)
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace Velopack
|
||||
AppendArgumentsTo(sb, args);
|
||||
debug = sb.ToString();
|
||||
}
|
||||
|
||||
#else
|
||||
public static void AppendArgumentListSafe(this ProcessStartInfo psi, IEnumerable<string> args, out string debug)
|
||||
{
|
||||
@@ -59,57 +58,68 @@ namespace Velopack
|
||||
return p;
|
||||
}
|
||||
|
||||
public static Task<string> Output(this ProcessStartInfo psi, int timeoutMs)
|
||||
public static string Output(this ProcessStartInfo psi, int timeoutMs)
|
||||
{
|
||||
TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
|
||||
|
||||
psi.RedirectStandardOutput = true;
|
||||
psi.RedirectStandardError = true;
|
||||
psi.UseShellExecute = false;
|
||||
|
||||
bool killed = false;
|
||||
|
||||
var sb = new StringBuilder();
|
||||
var p = new Process();
|
||||
p.StartInfo = psi;
|
||||
p.EnableRaisingEvents = true;
|
||||
|
||||
p.Exited += (o, e) => {
|
||||
if (killed) return;
|
||||
if (p.ExitCode != 0) {
|
||||
tcs.SetException(new Exception($"Process exited with code {p.ExitCode}."));
|
||||
} else {
|
||||
tcs.SetResult(sb.ToString());
|
||||
}
|
||||
};
|
||||
|
||||
p.ErrorDataReceived += (o, e) => {
|
||||
if (killed) return;
|
||||
if (e.Data != null) {
|
||||
sb.AppendLine(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
p.OutputDataReceived += (o, e) => {
|
||||
if (killed) return;
|
||||
if (e.Data != null) {
|
||||
sb.AppendLine(e.Data);
|
||||
}
|
||||
};
|
||||
|
||||
p.Start();
|
||||
p.BeginErrorReadLine();
|
||||
p.BeginOutputReadLine();
|
||||
|
||||
Task.Delay(timeoutMs).ContinueWith(t => {
|
||||
killed = true;
|
||||
if (!tcs.Task.IsCompleted) {
|
||||
tcs.SetException(new TimeoutException($"Process timed out after {timeoutMs}ms."));
|
||||
var p = Process.Start(psi);
|
||||
if (!p.WaitForExit(timeoutMs)) {
|
||||
p.Kill();
|
||||
throw new TimeoutException("Process did not exit within alloted time.");
|
||||
}
|
||||
});
|
||||
|
||||
return tcs.Task;
|
||||
return p.StandardOutput.ReadToEnd().Trim();
|
||||
|
||||
//TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();
|
||||
|
||||
//psi.RedirectStandardOutput = true;
|
||||
//psi.RedirectStandardError = true;
|
||||
//psi.UseShellExecute = false;
|
||||
|
||||
//bool killed = false;
|
||||
|
||||
//var sb = new StringBuilder();
|
||||
//var p = new Process();
|
||||
//p.StartInfo = psi;
|
||||
//p.EnableRaisingEvents = true;
|
||||
|
||||
//p.Exited += (o, e) => {
|
||||
// if (killed) return;
|
||||
// if (p.ExitCode != 0) {
|
||||
// tcs.SetException(new Exception($"Process exited with code {p.ExitCode}."));
|
||||
// } else {
|
||||
// tcs.SetResult(sb.ToString());
|
||||
// }
|
||||
//};
|
||||
|
||||
//p.ErrorDataReceived += (o, e) => {
|
||||
// if (killed) return;
|
||||
// if (e.Data != null) {
|
||||
// sb.AppendLine(e.Data);
|
||||
// }
|
||||
//};
|
||||
|
||||
//p.OutputDataReceived += (o, e) => {
|
||||
// if (killed) return;
|
||||
// if (e.Data != null) {
|
||||
// sb.AppendLine(e.Data);
|
||||
// }
|
||||
//};
|
||||
|
||||
//p.Start();
|
||||
//p.BeginErrorReadLine();
|
||||
//p.BeginOutputReadLine();
|
||||
|
||||
//Task.Delay(timeoutMs).ContinueWith(t => {
|
||||
// killed = true;
|
||||
// if (!tcs.Task.IsCompleted) {
|
||||
// tcs.SetException(new TimeoutException($"Process timed out after {timeoutMs}ms."));
|
||||
// p.Kill();
|
||||
// }
|
||||
//});
|
||||
|
||||
//return tcs.Task;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ namespace Velopack.NuGet
|
||||
}
|
||||
}
|
||||
|
||||
public static void SetMetadata(string nuspecPath, string mainExe, IEnumerable<string> runtimes, RID rid)
|
||||
public static void SetMetadata(string nuspecPath, string mainExe, IEnumerable<string> runtimes, RID rid, string version)
|
||||
{
|
||||
Dictionary<string, string> toSet = new();
|
||||
|
||||
@@ -78,9 +78,12 @@ namespace Velopack.NuGet
|
||||
toSet.Add("machineArchitecture", rid.Architecture.ToString());
|
||||
}
|
||||
|
||||
if (!String.IsNullOrEmpty(mainExe))
|
||||
if (!String.IsNullOrWhiteSpace(mainExe))
|
||||
toSet.Add("mainExe", mainExe);
|
||||
|
||||
if (!String.IsNullOrWhiteSpace(version))
|
||||
toSet.Add("version", version);
|
||||
|
||||
if (!toSet.Any())
|
||||
return;
|
||||
|
||||
|
||||
@@ -137,6 +137,9 @@ namespace Velopack.Sources
|
||||
AccessToken = accessToken;
|
||||
Prerelease = prerelease;
|
||||
Downloader = downloader ?? Utility.CreateDefaultDownloader();
|
||||
|
||||
if (String.IsNullOrWhiteSpace(AccessToken))
|
||||
logger?.Warn("No GitHub access token provided. Unauthenticated requests will be limited to 60 per hour.");
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
@@ -26,11 +26,15 @@ namespace Velopack.Sources
|
||||
/// <summary> Get the RELEASES file name for the specified Channel </summary>
|
||||
protected virtual string GetReleasesFileName()
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(Channel)) {
|
||||
return VelopackRuntimeInfo.IsOSX ? "RELEASES-osx" : "RELEASES";
|
||||
return GetReleasesFileNameImpl(Channel);
|
||||
}
|
||||
|
||||
return $"RELEASES-{Channel.ToLower()}";
|
||||
internal static string GetReleasesFileNameImpl(string channel)
|
||||
{
|
||||
if (String.IsNullOrWhiteSpace(channel) || channel == "default") {
|
||||
return VelopackRuntimeInfo.IsOSX ? "RELEASES-osx" : "RELEASES";
|
||||
}
|
||||
return $"RELEASES-{channel.ToLower()}";
|
||||
}
|
||||
|
||||
/// <inheritdoc/>
|
||||
|
||||
@@ -73,16 +73,16 @@ public abstract class S3CommandTests<T> : BaseCommandTests<T>
|
||||
Assert.StartsWith("Cannot use '--region' and '--endpoint' options together", parseResult.Errors[0].Message);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void PathPrefix_WithPath_ParsesValue()
|
||||
{
|
||||
S3BaseCommand command = new T();
|
||||
//[Fact]
|
||||
//public void PathPrefix_WithPath_ParsesValue()
|
||||
//{
|
||||
// S3BaseCommand command = new T();
|
||||
|
||||
string cli = GetRequiredDefaultOptions() + $"--pathPrefix \"sub-folder\"";
|
||||
ParseResult parseResult = command.ParseAndApply(cli);
|
||||
// string cli = GetRequiredDefaultOptions() + $"--pathPrefix \"sub-folder\"";
|
||||
// ParseResult parseResult = command.ParseAndApply(cli);
|
||||
|
||||
Assert.Equal("sub-folder", command.PathPrefix);
|
||||
}
|
||||
// Assert.Equal("sub-folder", command.PathPrefix);
|
||||
//}
|
||||
|
||||
protected override string GetRequiredDefaultOptions()
|
||||
{
|
||||
@@ -108,14 +108,14 @@ public class S3UploadCommandTests : S3CommandTests<S3UploadCommand>
|
||||
Assert.True(command.Overwrite);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void KeepMaxReleases_WithNumber_ParsesValue()
|
||||
{
|
||||
var command = new S3UploadCommand();
|
||||
//[Fact]
|
||||
//public void KeepMaxReleases_WithNumber_ParsesValue()
|
||||
//{
|
||||
// var command = new S3UploadCommand();
|
||||
|
||||
string cli = GetRequiredDefaultOptions() + "--keepMaxReleases 42";
|
||||
ParseResult parseResult = command.ParseAndApply(cli);
|
||||
// string cli = GetRequiredDefaultOptions() + "--keepMaxReleases 42";
|
||||
// ParseResult parseResult = command.ParseAndApply(cli);
|
||||
|
||||
Assert.Equal(42, command.KeepMaxReleases);
|
||||
}
|
||||
// Assert.Equal(42, command.KeepMaxReleases);
|
||||
//}
|
||||
}
|
||||
|
||||
@@ -58,18 +58,19 @@ public class WindowsPackTests
|
||||
PackTitle = "Test Squirrel App",
|
||||
PackDirectory = tmpOutput,
|
||||
IncludePdb = true,
|
||||
Channel = "asd123"
|
||||
};
|
||||
|
||||
var runner = new WindowsPackCommandRunner(logger);
|
||||
runner.Pack(options);
|
||||
|
||||
var nupkgPath = Path.Combine(tmpReleaseDir, $"{id}-{version}-win-x64-full.nupkg");
|
||||
var nupkgPath = Path.Combine(tmpReleaseDir, $"{id}-{version}-asd123-win-x64-full.nupkg");
|
||||
Assert.True(File.Exists(nupkgPath));
|
||||
|
||||
var setupPath = Path.Combine(tmpReleaseDir, $"{id}-Setup-[win-x64].exe");
|
||||
var setupPath = Path.Combine(tmpReleaseDir, $"{id}-[win-x64]-asd123-Setup.exe");
|
||||
Assert.True(File.Exists(setupPath));
|
||||
|
||||
var releasesPath = Path.Combine(tmpReleaseDir, $"RELEASES");
|
||||
var releasesPath = Path.Combine(tmpReleaseDir, $"RELEASES-asd123");
|
||||
Assert.True(File.Exists(releasesPath));
|
||||
|
||||
EasyZip.ExtractZipToDirectory(logger, nupkgPath, unzipDir);
|
||||
@@ -80,7 +81,7 @@ public class WindowsPackTests
|
||||
var xml = XDocument.Load(nuspecPath);
|
||||
|
||||
Assert.Equal(id, xml.Root.ElementsNoNamespace("metadata").Single().ElementsNoNamespace("id").Single().Value);
|
||||
Assert.Equal(version, xml.Root.ElementsNoNamespace("metadata").Single().ElementsNoNamespace("version").Single().Value);
|
||||
Assert.Equal(version + "-asd123", xml.Root.ElementsNoNamespace("metadata").Single().ElementsNoNamespace("version").Single().Value);
|
||||
Assert.Equal(exe, xml.Root.ElementsNoNamespace("metadata").Single().ElementsNoNamespace("mainExe").Single().Value);
|
||||
Assert.Equal("Test Squirrel App", xml.Root.ElementsNoNamespace("metadata").Single().ElementsNoNamespace("title").Single().Value);
|
||||
Assert.Equal("author", xml.Root.ElementsNoNamespace("metadata").Single().ElementsNoNamespace("authors").Single().Value);
|
||||
@@ -95,7 +96,7 @@ public class WindowsPackTests
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
public void PackBuildMultipleChannels()
|
||||
public void PackBuildMultipleChannelsSameRid()
|
||||
{
|
||||
Skip.IfNot(VelopackRuntimeInfo.IsWindows);
|
||||
|
||||
@@ -122,42 +123,76 @@ public class WindowsPackTests
|
||||
PackAuthors = "author",
|
||||
PackTitle = "Test Squirrel App",
|
||||
PackDirectory = tmpOutput,
|
||||
Channel = "hello",
|
||||
IncludePdb = true,
|
||||
};
|
||||
|
||||
var runner = new WindowsPackCommandRunner(logger);
|
||||
runner.Pack(options);
|
||||
|
||||
options.TargetRuntime = RID.Parse("win10.0.19043-x86");
|
||||
options.Channel = "hello2";
|
||||
runner.Pack(options);
|
||||
|
||||
var nupkgPath1 = Path.Combine(tmpReleaseDir, $"{id}-{version}-win-x64-full.nupkg");
|
||||
Assert.True(File.Exists(nupkgPath1));
|
||||
|
||||
var setupPath1 = Path.Combine(tmpReleaseDir, $"{id}-Setup-[win-x64].exe");
|
||||
var setupPath1 = Path.Combine(tmpReleaseDir, $"{id}-[win-x64]-Setup.exe");
|
||||
Assert.True(File.Exists(setupPath1));
|
||||
|
||||
var releasesPath1 = Path.Combine(tmpReleaseDir, $"RELEASES-hello");
|
||||
var releasesPath1 = Path.Combine(tmpReleaseDir, $"RELEASES");
|
||||
Assert.True(File.Exists(releasesPath1));
|
||||
|
||||
var nupkgPath2 = Path.Combine(tmpReleaseDir, $"{id}-{version}-win-x86-full.nupkg");
|
||||
Assert.True(File.Exists(nupkgPath2));
|
||||
|
||||
var setupPath2 = Path.Combine(tmpReleaseDir, $"{id}-Setup-[win-x86].exe");
|
||||
Assert.True(File.Exists(setupPath2));
|
||||
|
||||
var releasesPath2 = Path.Combine(tmpReleaseDir, $"RELEASES-hello2");
|
||||
Assert.True(File.Exists(releasesPath2));
|
||||
|
||||
var rel1 = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesPath1, Encoding.UTF8));
|
||||
Assert.Equal(1, rel1.Count());
|
||||
Assert.True(rel1.Single().Rid == RID.Parse("win-x64"));
|
||||
|
||||
options.Channel = "hello";
|
||||
runner.Pack(options);
|
||||
|
||||
var nupkgPath2 = Path.Combine(tmpReleaseDir, $"{id}-{version}-hello-win-x64-full.nupkg");
|
||||
Assert.True(File.Exists(nupkgPath2));
|
||||
|
||||
var setupPath2 = Path.Combine(tmpReleaseDir, $"{id}-[win-x64]-hello-Setup.exe");
|
||||
Assert.True(File.Exists(setupPath2));
|
||||
|
||||
var releasesPath2 = Path.Combine(tmpReleaseDir, $"RELEASES-hello");
|
||||
Assert.True(File.Exists(releasesPath2));
|
||||
|
||||
rel1 = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesPath1, Encoding.UTF8));
|
||||
Assert.Equal(1, rel1.Count());
|
||||
|
||||
var rel2 = ReleaseEntry.ParseReleaseFile(File.ReadAllText(releasesPath2, Encoding.UTF8));
|
||||
Assert.Equal(1, rel2.Count());
|
||||
Assert.True(rel2.Single().Rid == RID.Parse("win-x86"));
|
||||
}
|
||||
|
||||
|
||||
[SkippableFact]
|
||||
public void PackBuildRefuseSameVersion()
|
||||
{
|
||||
Skip.IfNot(VelopackRuntimeInfo.IsWindows);
|
||||
|
||||
using var logger = _output.BuildLoggerFor<WindowsPackTests>();
|
||||
|
||||
using var _1 = Utility.GetTempDirectory(out var tmpOutput);
|
||||
using var _2 = Utility.GetTempDirectory(out var tmpReleaseDir);
|
||||
|
||||
var exe = "testawareapp.exe";
|
||||
var pdb = Path.ChangeExtension(exe, ".pdb");
|
||||
var id = "Test.Squirrel-App";
|
||||
var version = "1.0.0";
|
||||
|
||||
File.Copy(HelperFile.FindTestFile(exe), Path.Combine(tmpOutput, exe));
|
||||
File.Copy(HelperFile.FindTestFile(pdb), Path.Combine(tmpOutput, pdb));
|
||||
|
||||
var options = new WindowsPackOptions {
|
||||
EntryExecutableName = exe,
|
||||
ReleaseDir = new DirectoryInfo(tmpReleaseDir),
|
||||
PackId = id,
|
||||
PackVersion = version,
|
||||
PackDirectory = tmpOutput,
|
||||
TargetRuntime = RID.Parse("win"),
|
||||
};
|
||||
|
||||
var runner = new WindowsPackCommandRunner(logger);
|
||||
runner.Pack(options);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => runner.Pack(options));
|
||||
}
|
||||
|
||||
[SkippableFact]
|
||||
@@ -261,7 +296,7 @@ public class WindowsPackTests
|
||||
var runner = new WindowsPackCommandRunner(logger);
|
||||
runner.Pack(options);
|
||||
|
||||
var setupPath1 = Path.Combine(tmpReleaseDir, $"{id}-Setup-[win-x64].exe");
|
||||
var setupPath1 = Path.Combine(tmpReleaseDir, $"{id}-[win-x64]-Setup.exe");
|
||||
Assert.True(File.Exists(setupPath1));
|
||||
|
||||
RunNoCoverage(setupPath1, new[] { "--nocolor", "--silent", "--installto", tmpInstallDir }, Environment.CurrentDirectory, logger);
|
||||
@@ -322,7 +357,7 @@ public class WindowsPackTests
|
||||
PackTestApp(id, "1.0.0", "version 1 test", releaseDir, logger);
|
||||
|
||||
// install app
|
||||
var setupPath1 = Path.Combine(releaseDir, $"{id}-Setup-[win-x64].exe");
|
||||
var setupPath1 = Path.Combine(releaseDir, $"{id}-[win-x64]-Setup.exe");
|
||||
RunNoCoverage(setupPath1, new string[] { "--nocolor", "--silent", "--installto", installDir },
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), logger);
|
||||
|
||||
@@ -396,7 +431,7 @@ public class WindowsPackTests
|
||||
PackTestApp(id, "1.0.0", "version 1 test", releaseDir, logger);
|
||||
|
||||
// install app
|
||||
var setupPath1 = Path.Combine(releaseDir, $"{id}-Setup-[win-x64].exe");
|
||||
var setupPath1 = Path.Combine(releaseDir, $"{id}-[win-x64]-Setup.exe");
|
||||
RunNoCoverage(setupPath1, new string[] { "--nocolor", "--installto", installDir },
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), logger);
|
||||
|
||||
@@ -445,7 +480,7 @@ public class WindowsPackTests
|
||||
PackTestApp(id, "1.0.0", "version 1 test", releaseDir, logger);
|
||||
|
||||
// install app
|
||||
var setupPath1 = Path.Combine(releaseDir, $"{id}-Setup-[win-x64].exe");
|
||||
var setupPath1 = Path.Combine(releaseDir, $"{id}-[win-x64]-Setup.exe");
|
||||
RunNoCoverage(setupPath1, new string[] { "--nocolor", "--silent", "--installto", installDir },
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.Desktop), logger);
|
||||
|
||||
@@ -610,6 +645,9 @@ public class WindowsPackTests
|
||||
{
|
||||
var outputfile = GetPath($"coverage.rundotnet.{RandomString(8)}.xml");
|
||||
|
||||
if (!File.Exists(exe))
|
||||
throw new Exception($"File {exe} does not exist.");
|
||||
|
||||
var psi = new ProcessStartInfo("dotnet-coverage");
|
||||
psi.WorkingDirectory = workingDir;
|
||||
psi.CreateNoWindow = true;
|
||||
@@ -637,6 +675,9 @@ public class WindowsPackTests
|
||||
|
||||
private string RunNoCoverage(string exe, string[] args, string workingDir, ILogger logger, int? exitCode = 0)
|
||||
{
|
||||
if (!File.Exists(exe))
|
||||
throw new Exception($"File {exe} does not exist.");
|
||||
|
||||
var psi = new ProcessStartInfo(exe);
|
||||
psi.WorkingDirectory = workingDir;
|
||||
psi.CreateNoWindow = true;
|
||||
|
||||
Reference in New Issue
Block a user