Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support auto-fetching metadata from GitHub API and add manual args #543

Merged
merged 8 commits into from
Aug 6, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions src/WingetCreateCLI/Commands/NewCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,20 @@ public override async Task<bool> Execute()
}

ShiftRootFieldsToInstallerLevel(manifests.InstallerManifest);
try
{
Logger.InfoLocalized(nameof(Resources.PopulatingGitHubMetadata_Message));
if (this.GitHubClient != null)
{
await this.GitHubClient.PopulateGitHubMetadata(manifests, this.Format.ToString());
}
}
catch (Octokit.ApiException)
{
// Print a warning, but continue with the command flow.
Logger.ErrorLocalized(nameof(Resources.CouldNotPopulateGitHubMetadata_Warning));
}

PromptManifestProperties(manifests);
MergeNestedInstallerFilesIfApplicable(manifests.InstallerManifest);
ShiftInstallerFieldsToRootLevel(manifests.InstallerManifest);
Expand Down
77 changes: 77 additions & 0 deletions src/WingetCreateCLI/Commands/UpdateCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,18 @@ public static IEnumerable<Example> Examples
[Option('d', "display-version", Required = false, HelpText = "DisplayVersion_HelpText", ResourceType = typeof(Resources))]
public string DisplayVersion { get; set; }

/// <summary>
/// Gets or sets the release notes URL for the manifest.
/// </summary>
[Option("release-notes-url", Required = false, HelpText = "ReleaseNotesUrl_HelpText", ResourceType = typeof(Resources))]
public string ReleaseNotesUrl { get; set; }

/// <summary>
/// Gets or sets the release date for the manifest.
/// </summary>
[Option("release-date", Required = false, HelpText = "ReleaseDate_HelpText", ResourceType = typeof(Resources))]
public DateTimeOffset? ReleaseDate { get; set; }
mdanish-kh marked this conversation as resolved.
Show resolved Hide resolved

/// <summary>
/// Gets or sets the outputPath where the generated manifest file should be saved to.
/// </summary>
Expand Down Expand Up @@ -153,6 +165,19 @@ public override async Task<bool> Execute()

bool submitFlagMissing = !this.SubmitToGitHub && (!string.IsNullOrEmpty(this.PRTitle) || this.Replace);

if (!string.IsNullOrEmpty(this.ReleaseNotesUrl))
{
Uri uriResult;
bool isValid = Uri.TryCreate(this.ReleaseNotesUrl, UriKind.Absolute, out uriResult) &&
(uriResult.Scheme == Uri.UriSchemeHttp || uriResult.Scheme == Uri.UriSchemeHttps);

if (!isValid)
{
Logger.ErrorLocalized(nameof(Resources.SentenceBadFormatConversionErrorOption), nameof(this.ReleaseNotesUrl));
return false;
}
}

if (submitFlagMissing)
{
Logger.WarnLocalized(nameof(Resources.SubmitFlagMissing_Warning));
Expand Down Expand Up @@ -440,6 +465,22 @@ public async Task<Manifests> UpdateManifestsAutonomously(Manifests manifests)
PackageParser.UpdateInstallerNodesAsync(installerMetadataList, installerManifest);
DisplayArchitectureWarnings(installerMetadataList);
ResetVersionSpecificFields(manifests);
try
{
Logger.InfoLocalized(nameof(Resources.PopulatingGitHubMetadata_Message));

if (this.GitHubClient != null)
{
await this.GitHubClient.PopulateGitHubMetadata(manifests, this.Format.ToString());
}
}
catch (Octokit.ApiException)
{
// Print a warning, but continue with the update.
Logger.ErrorLocalized(nameof(Resources.CouldNotPopulateGitHubMetadata_Warning));
}

this.AddVersionSpecificMetadata(manifests);
ShiftInstallerFieldsToRootLevel(manifests.InstallerManifest);
}
catch (InvalidOperationException)
Expand Down Expand Up @@ -711,6 +752,27 @@ private static bool AreInstallerUrlsVanityUrls(Manifests baseManifest, Manifests
return true;
}

private void AddVersionSpecificMetadata(Manifests updatedManifests)
{
if (this.ReleaseDate != null)
{
switch (this.Format)
{
case ManifestFormat.Yaml:
updatedManifests.InstallerManifest.ReleaseDateTime = this.ReleaseDate.Value.ToString("yyyy-MM-dd");
break;
case ManifestFormat.Json:
updatedManifests.InstallerManifest.ReleaseDate = this.ReleaseDate;
break;
}
}

if (!string.IsNullOrEmpty(this.ReleaseNotesUrl))
{
updatedManifests.DefaultLocaleManifest.ReleaseNotesUrl = this.ReleaseNotesUrl;
}
}

private string ObtainMatchingRelativeFilePath(string oldRelativeFilePath, string directory, string archiveName)
{
string fileName = Path.GetFileName(oldRelativeFilePath);
Expand Down Expand Up @@ -870,6 +932,21 @@ private async Task<Manifests> UpdateManifestsInteractively(Manifests manifests)
await this.UpdateInstallersInteractively(manifests.InstallerManifest.Installers);
ShiftInstallerFieldsToRootLevel(manifests.InstallerManifest);
ResetVersionSpecificFields(manifests);
try
{
Logger.InfoLocalized(nameof(Resources.PopulatingGitHubMetadata_Message));
if (this.GitHubClient != null)
{
await this.GitHubClient.PopulateGitHubMetadata(manifests, this.Format.ToString());
}
}
catch (Octokit.ApiException)
{
// Print a warning, but continue with the update.
Logger.ErrorLocalized(nameof(Resources.CouldNotPopulateGitHubMetadata_Warning));
}

this.AddVersionSpecificMetadata(manifests);
DisplayManifestPreview(manifests);
ValidateManifestsInTempDir(manifests);
}
Expand Down
36 changes: 36 additions & 0 deletions src/WingetCreateCLI/Properties/Resources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions src/WingetCreateCLI/Properties/Resources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -1371,4 +1371,16 @@
<data name="InstallerWithMultipleDisplayVersions_Warning" xml:space="preserve">
<value>Single installer with multiple display versions detected. Winget-Create will only update the first DisplayVersion for a given installer.</value>
</data>
<data name="CouldNotPopulateGitHubMetadata_Warning" xml:space="preserve">
<value>Could not populate manifest metadata through GitHub's API.</value>
</data>
<data name="PopulatingGitHubMetadata_Message" xml:space="preserve">
<value>GitHub URL detected. The CLI will automatically fill some manifests fields.</value>
</data>
<data name="ReleaseDate_HelpText" xml:space="preserve">
<value>Date to be used when updating the release date field. Expected format is "YYYY-MM-DD".</value>
</data>
<data name="ReleaseNotesUrl_HelpText" xml:space="preserve">
<value>URL to be used when updating the release notes url field.</value>
</data>
</root>
147 changes: 147 additions & 0 deletions src/WingetCreateCore/Common/GitHub.cs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ namespace Microsoft.WingetCreateCore.Common
using Jose;
using Microsoft.WingetCreateCore.Common.Exceptions;
using Microsoft.WingetCreateCore.Models;
using Microsoft.WingetCreateCore.Models.DefaultLocale;
using Microsoft.WingetCreateCore.Models.Installer;
using Octokit;
using Polly;

Expand Down Expand Up @@ -229,6 +231,21 @@ public async Task<string> FindPackageId(string packageId)
return await this.FindPackageIdRecursive(packageId.Split('.'), path, string.Empty, 0);
}

/// <summary>
/// Uses the GitHub API to retrieve and populate metadata for manifests in the provided <see cref="Manifests"/> object.
/// </summary>
/// <param name="manifests">Wrapper object for manifest object models to be populated with GitHub metadata.</param>
/// <param name="serializerFormat">The output format of the manifest serializer.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
public async Task PopulateGitHubMetadata(Manifests manifests, string serializerFormat)
{
// Only populate metadata if we have a valid GitHub token.
if (this.github.Credentials.AuthenticationType != AuthenticationType.Anonymous)
{
await GitHubManifestMetadata.PopulateManifestMetadata(manifests, serializerFormat, this.github);
}
}

/// <summary>
/// Generate a signed-JWT token for specified GitHub app, per instructions here: https://docs.github.com/en/developers/apps/authenticating-with-github-apps#authenticating-as-an-installation.
/// </summary>
Expand Down Expand Up @@ -479,5 +496,135 @@ private async Task DeletePullRequestBranch(int pullRequestId)
await this.github.Git.Reference.Delete(this.wingetRepoOwner, this.wingetRepo, newBranchNameHeads);
}
}

private static class GitHubManifestMetadata
{
public static async Task PopulateManifestMetadata(Manifests manifests, string serializerFormat, GitHubClient client)
{
// Get owner and repo from the installer manifest
GitHubUrlMetadata? metadata = GetMetadataFromGitHubUrl(manifests.InstallerManifest);

if (metadata == null)
{
// Could not populate GitHub metadata.
return;
}

string owner = metadata.Value.Owner;
string repo = metadata.Value.Repo;
string tag = metadata.Value.ReleaseTag;

var githubRepo = await client.Repository.Get(owner, repo);
var githubRelease = await client.Repository.Release.Get(owner, repo, tag);

// License
if (string.IsNullOrEmpty(manifests.DefaultLocaleManifest.License))
{
// License will only ever be empty in new command flow
manifests.DefaultLocaleManifest.License = githubRepo.License?.SpdxId ?? githubRepo.License?.Name;
}

// ShortDescription
if (string.IsNullOrEmpty(manifests.DefaultLocaleManifest.ShortDescription))
{
// ShortDescription will only ever be empty in new command flow
manifests.DefaultLocaleManifest.ShortDescription = githubRepo.Description;
}

// PackageUrl
if (string.IsNullOrEmpty(manifests.DefaultLocaleManifest.PackageUrl))
{
manifests.DefaultLocaleManifest.PackageUrl = githubRepo.HtmlUrl;
}

// PublisherUrl
if (string.IsNullOrEmpty(manifests.DefaultLocaleManifest.PublisherUrl))
{
manifests.DefaultLocaleManifest.PublisherUrl = githubRepo.Owner.HtmlUrl;
}

// PublisherSupportUrl
if (string.IsNullOrEmpty(manifests.DefaultLocaleManifest.PublisherSupportUrl) && githubRepo.HasIssues)
{
manifests.DefaultLocaleManifest.PublisherSupportUrl = $"{githubRepo.HtmlUrl}/issues";
}

// Tags
// 16 is the maximum number of tags allowed in the manifest
manifests.DefaultLocaleManifest.Tags ??= githubRepo.Topics?.Take(count: 16).ToList();

// ReleaseNotesUrl
if (string.IsNullOrEmpty(manifests.DefaultLocaleManifest.ReleaseNotesUrl))
{
manifests.DefaultLocaleManifest.ReleaseNotesUrl = githubRelease.HtmlUrl;
}

// ReleaseDate
SetReleaseDate(manifests, serializerFormat, githubRelease);

// Documentations
if (manifests.DefaultLocaleManifest.Documentations == null && githubRepo.HasWiki)
{
manifests.DefaultLocaleManifest.Documentations = new List<Documentation>
{
new()
{
DocumentLabel = "Wiki",
DocumentUrl = $"{githubRepo.HtmlUrl}/wiki",
},
};
}
}

private static void SetReleaseDate(Manifests manifests, string serializerFormat, Release githubRelease)
{
DateTimeOffset? releaseDate = githubRelease.PublishedAt;
if (releaseDate == null)
{
return;
}

switch (serializerFormat.ToLower())
{
case "yaml":
manifests.InstallerManifest.ReleaseDateTime = releaseDate.Value.ToString("yyyy-MM-dd");
break;
case "json":
manifests.InstallerManifest.ReleaseDate = releaseDate;
break;
}
}

private static GitHubUrlMetadata? GetMetadataFromGitHubUrl(InstallerManifest installerManifest)
{
// Get all GitHub URLs from the installer manifest
List<string> gitHubUrls = installerManifest.Installers
.Where(x => x.InstallerUrl.StartsWith("https://github.com/", StringComparison.OrdinalIgnoreCase))
.Select(x => x.InstallerUrl)
.ToList();

if (gitHubUrls.Count != installerManifest.Installers.Count)
{
// No GitHub URLs found OR not all manifest InstallerUrls are GitHub URLs.
return null;
}

string domainTrimmed = gitHubUrls.First().Replace("https://github.com/", string.Empty);
string[] parts = domainTrimmed.Split("/");
string owner = parts[0];
string repo = parts[1];
string tag = domainTrimmed.Replace($"{owner}/{repo}/releases/download/", string.Empty).Split("/")[0];

// Check if all GitHub URLs have the same owner, repo and tag
if (gitHubUrls.Any(x => !x.StartsWith($"https://github.com/{owner}/{repo}/releases/download/{tag}", StringComparison.OrdinalIgnoreCase)))
{
return null;
}

return new GitHubUrlMetadata(owner, repo, tag);
}

private record struct GitHubUrlMetadata(string Owner, string Repo, string ReleaseTag);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"PackageIdentifier": "Multifile.Json.GitHubAutoFillTest",
"PackageVersion": "1.2.3.4",
"Installers": [
{
"Architecture": "x64",
"InstallerUrl": "https://fakedomain.com/WingetCreateTestExeInstaller.exe",
"InstallerType": "exe",
"InstallerSha256": "A7803233EEDB6A4B59B3024CCF9292A6FFFB94507DC998AA67C5B745D197A5DC"
}
],
"ManifestType": "installer",
"ManifestVersion": "1.0.0"
}
Loading