diff --git a/src/WingetCreateCLI/Commands/BaseCommand.cs b/src/WingetCreateCLI/Commands/BaseCommand.cs
index 4d88eb50..1b3e2049 100644
--- a/src/WingetCreateCLI/Commands/BaseCommand.cs
+++ b/src/WingetCreateCLI/Commands/BaseCommand.cs
@@ -313,105 +313,142 @@ protected static void DisplayManifestPreview(Manifests manifests)
}
///
- /// Validates the GitHubToken provided on the command-line, or if not present, the cached token if one exists.
- /// Attempts a simple operation against the target repo, and if that fails, then:
- /// If token provided on command-line, errors out
- /// If not, and cached token was present, then deletes token cache, and starts OAuth flow
- /// Otherwise, sets the instance variable to hold the validated token.
- /// If no token is present on command-line or in cache, starts the OAuth flow to retrieve one.
+ /// Creates a new GitHub client using the provided or cached token if present.
+ /// If the requireToken bool is set to TRUE, OAuth flow can be launched to acquire a new token for the client.
+ /// The OAuth flow will only be launched if no token is provided in the command line and no token is present in the token cache.
///
- /// Boolean to override default behavior and force caching of token.
- /// True if the token is now present and valid, false otherwise.
- protected async Task SetAndCheckGitHubToken(bool cacheToken = false)
+ /// Boolean value indicating whether a token is required for the client and whether to initiate an OAuth flow.
+ /// A boolean value indicating whether a new GitHub client was created and accessed successfully.
+ protected async Task LoadGitHubClient(bool requireToken = false)
{
- string cachedToken = null;
- bool hasPatToken = !string.IsNullOrEmpty(this.GitHubToken);
- string token = this.GitHubToken;
+ bool isCacheToken = false;
- if (!hasPatToken)
+ if (string.IsNullOrEmpty(this.GitHubToken))
{
Logger.Trace("No token parameter, reading cached token");
- token = cachedToken = GitHubOAuth.ReadTokenCache();
+ this.GitHubToken = GitHubOAuth.ReadTokenCache();
- if (string.IsNullOrEmpty(token))
+ if (string.IsNullOrEmpty(this.GitHubToken))
{
- Logger.Trace("No cached token found.");
- Logger.DebugLocalized(nameof(Resources.GitHubAccountMustBeLinked_Message));
- Logger.DebugLocalized(nameof(Resources.ExecutionPaused_Message));
- Console.WriteLine();
- token = await GitHubOAuthLoginFlow();
- if (string.IsNullOrEmpty(token))
+ if (requireToken)
{
- // User must've cancelled OAuth flow, we can't proceed successfully
- Logger.WarnLocalized(nameof(Resources.NoTokenResponse_Message));
- return false;
+ Logger.Trace("No token found in cache, launching OAuth flow");
+ if (!await this.GetTokenFromOAuth())
+ {
+ return false;
+ }
}
-
- Logger.DebugLocalized(nameof(Resources.ResumingCommandExecution_Message));
}
else
{
- Logger.DebugLocalized(nameof(Resources.UsingTokenFromCache_Message));
+ isCacheToken = true;
}
}
- this.GitHubClient = new GitHub(token, this.WingetRepoOwner, this.WingetRepo);
+ if (await this.CheckGitHubTokenAndSetClient())
+ {
+ return true;
+ }
+ else
+ {
+ if (isCacheToken)
+ {
+ GitHubOAuth.DeleteTokenCache();
+ }
+
+ return false;
+ }
+ }
+ ///
+ /// Launches the GitHub OAuth flow and obtains a GitHub token.
+ ///
+ /// A boolean value indicating whether the OAuth login flow was successful.
+ protected async Task GetTokenFromOAuth()
+ {
+ Logger.DebugLocalized(nameof(Resources.GitHubAccountMustBeLinked_Message));
+ Logger.DebugLocalized(nameof(Resources.ExecutionPaused_Message));
+ Console.WriteLine();
+ this.GitHubToken = await GitHubOAuthLoginFlow();
+
+ if (string.IsNullOrEmpty(this.GitHubToken))
+ {
+ // User must've cancelled OAuth flow, we can't proceed successfully
+ Logger.WarnLocalized(nameof(Resources.NoTokenResponse_Message));
+ return false;
+ }
+
+ this.StoreTokenInCache();
+ Logger.DebugLocalized(nameof(Resources.ResumingCommandExecution_Message));
+ return true;
+ }
+
+ ///
+ /// If the provided token is valid, stores the token in cache.
+ ///
+ /// Returns a boolean value indicating whether storing the token in cache was successful.
+ protected bool StoreTokenInCache()
+ {
try
{
- Logger.Trace("Checking repo access using OAuth token");
- await this.GitHubClient.CheckAccess();
- Logger.Trace("Access check was successful, proceeding");
- this.GitHubToken = token;
+ Logger.Trace("Writing token to cache");
+ GitHubOAuth.WriteTokenCache(this.GitHubToken);
+ Logger.InfoLocalized(nameof(Resources.StoringToken_Message));
+ }
+ catch (Exception ex)
+ {
+ // Failing to cache the token shouldn't be fatal.
+ Logger.WarnLocalized(nameof(Resources.WritingCacheTokenFailed_Message), ex.Message);
+ return false;
+ }
- // Only cache the token if it came from Oauth, instead of PAT parameter or cache
- if (cacheToken || (!hasPatToken && token != cachedToken))
- {
- try
- {
- Logger.Trace("Writing token to cache");
- GitHubOAuth.WriteTokenCache(token);
- }
- catch (Exception ex)
- {
- // Failing to cache the token shouldn't be fatal.
- Logger.WarnLocalized(nameof(Resources.WritingCacheTokenFailed_Message), ex.Message);
- }
- }
+ return true;
+ }
- return true;
+ ///
+ /// Verifies if the GitHub token has valid access.
+ ///
+ /// A boolean value indicating whether the GitHub token had valid access.
+ protected async Task CheckGitHubTokenAndSetClient()
+ {
+ var client = new GitHub(this.GitHubToken, this.WingetRepoOwner, this.WingetRepo);
+
+ try
+ {
+ Logger.Trace("Checking repo access using provided token");
+ await client.CheckAccess();
+ Logger.Trace("Access check was successful, proceeding");
}
catch (Exception e)
{
- if (token == cachedToken)
+ if (e is AuthorizationException)
{
- // There's an issue with the cached token, so let's delete it and try again
- Logger.WarnLocalized(nameof(Resources.InvalidCachedToken));
- GitHubOAuth.DeleteTokenCache();
- return await this.SetAndCheckGitHubToken();
+ Logger.ErrorLocalized(nameof(Resources.InvalidGitHubToken_Message));
}
- else if (e is AuthorizationException)
+ else if (e is RateLimitExceededException)
{
- Logger.ErrorLocalized(nameof(Resources.Error_Prefix), e.Message);
- Logger.ErrorLocalized(nameof(Resources.InvalidTokenError_Message));
- return false;
+ Logger.ErrorLocalized(nameof(Resources.RateLimitExceeded_Message));
}
- else
+ else if (e is NotFoundException)
{
- throw;
+ Logger.ErrorLocalized(nameof(Resources.RepositoryNotFound_Error), this.WingetRepoOwner, this.WingetRepo);
}
+
+ return false;
}
+
+ this.GitHubClient = client;
+ return true;
}
///
/// Submits a pull request with multifile manifests using the user's GitHub access token.
///
/// Wrapper object for manifest object models to be submitted.
- /// Access token to allow for this tool to submit a pull request on behalf of the user.
/// A representing the success of the asynchronous operation.
- protected async Task GitHubSubmitManifests(Manifests manifests, string token)
+ protected async Task GitHubSubmitManifests(Manifests manifests)
{
- if (string.IsNullOrEmpty(token))
+ if (string.IsNullOrEmpty(this.GitHubToken))
{
Logger.WarnLocalized(nameof(Resources.NoTokenProvided_Message));
return false;
diff --git a/src/WingetCreateCLI/Commands/NewCommand.cs b/src/WingetCreateCLI/Commands/NewCommand.cs
index d502e81a..efd100f8 100644
--- a/src/WingetCreateCLI/Commands/NewCommand.cs
+++ b/src/WingetCreateCLI/Commands/NewCommand.cs
@@ -1,184 +1,193 @@
-// Copyright (c) Microsoft. All rights reserved.
-// Licensed under the MIT license.
-
-namespace Microsoft.WingetCreateCLI.Commands
-{
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Microsoft.WingetCreateCLI.Commands
+{
using System;
- using System.Collections.Generic;
- using System.ComponentModel.DataAnnotations;
- using System.IO;
- using System.Linq;
- using System.Reflection;
- using System.Threading.Tasks;
- using CommandLine;
- using CommandLine.Text;
- using Microsoft.WingetCreateCLI.Logging;
- using Microsoft.WingetCreateCLI.Properties;
- using Microsoft.WingetCreateCLI.Telemetry;
- using Microsoft.WingetCreateCLI.Telemetry.Events;
+ using System.Collections.Generic;
+ using System.ComponentModel.DataAnnotations;
+ using System.IO;
+ using System.Linq;
+ using System.Reflection;
+ using System.Threading.Tasks;
+ using CommandLine;
+ using CommandLine.Text;
+ using Microsoft.WingetCreateCLI.Logging;
+ using Microsoft.WingetCreateCLI.Properties;
+ using Microsoft.WingetCreateCLI.Telemetry;
+ using Microsoft.WingetCreateCLI.Telemetry.Events;
using Microsoft.WingetCreateCore;
using Microsoft.WingetCreateCore.Common;
- using Microsoft.WingetCreateCore.Models;
- using Microsoft.WingetCreateCore.Models.DefaultLocale;
- using Microsoft.WingetCreateCore.Models.Installer;
+ using Microsoft.WingetCreateCore.Models;
+ using Microsoft.WingetCreateCore.Models.DefaultLocale;
+ using Microsoft.WingetCreateCore.Models.Installer;
using Microsoft.WingetCreateCore.Models.Version;
using Newtonsoft.Json;
- using Sharprompt;
-
- ///
- /// Command to launch a wizard that prompt users for information to generate a new manifest.
- ///
- [Verb("new", HelpText = "NewCommand_HelpText", ResourceType = typeof(Resources))]
- public class NewCommand : BaseCommand
- {
- ///
- /// The url path to the manifest documentation site.
- ///
- private const string ManifestDocumentationUrl = "https://github.com/microsoft/winget-cli/blob/master/doc/ManifestSpecv1.0.md";
-
- ///
- /// Installer types for which we can trust that the detected architecture is correct, so don't need to prompt the user to confirm.
- ///
- private static readonly InstallerType[] ReliableArchitectureInstallerTypes = new[] { InstallerType.Msix, InstallerType.Appx };
-
- ///
- /// Gets the usage examples for the New command.
- ///
- [Usage(ApplicationAlias = ProgramApplicationAlias)]
- public static IEnumerable Examples
- {
- get
- {
- yield return new Example(Resources.Example_NewCommand_StartFromScratch, new NewCommand { });
+ using Sharprompt;
+
+ ///
+ /// Command to launch a wizard that prompt users for information to generate a new manifest.
+ ///
+ [Verb("new", HelpText = "NewCommand_HelpText", ResourceType = typeof(Resources))]
+ public class NewCommand : BaseCommand
+ {
+ ///
+ /// The url path to the manifest documentation site.
+ ///
+ private const string ManifestDocumentationUrl = "https://github.com/microsoft/winget-cli/blob/master/doc/ManifestSpecv1.0.md";
+
+ ///
+ /// Installer types for which we can trust that the detected architecture is correct, so don't need to prompt the user to confirm.
+ ///
+ private static readonly InstallerType[] ReliableArchitectureInstallerTypes = new[] { InstallerType.Msix, InstallerType.Appx };
+
+ ///
+ /// Gets the usage examples for the New command.
+ ///
+ [Usage(ApplicationAlias = ProgramApplicationAlias)]
+ public static IEnumerable Examples
+ {
+ get
+ {
+ yield return new Example(Resources.Example_NewCommand_StartFromScratch, new NewCommand { });
yield return new Example(Resources.Example_NewCommand_DownloadInstaller, new NewCommand { InstallerUrls = new string[] { "", ", .." } });
- yield return new Example(Resources.Example_NewCommand_SaveLocallyOrSubmit, new NewCommand
- {
+ yield return new Example(Resources.Example_NewCommand_SaveLocallyOrSubmit, new NewCommand
+ {
InstallerUrls = new string[] { "", ", .." },
- OutputDir = "",
- GitHubToken = "",
- });
- }
- }
-
- ///
+ OutputDir = "",
+ GitHubToken = "",
+ });
+ }
+ }
+
+ ///
/// Gets or sets the installer URL(s) used for downloading and parsing the installer file(s).
- ///
+ ///
[Value(0, MetaName = "urls", Required = false, HelpText = "InstallerUrl_HelpText", ResourceType = typeof(Resources))]
public IEnumerable InstallerUrls { get; set; }
-
- ///
- /// Gets or sets the outputPath where the generated manifest file should be saved to.
- ///
- [Option('o', "out", Required = false, HelpText = "OutputDirectory_HelpText", ResourceType = typeof(Resources))]
- public string OutputDir { get; set; }
+
+ ///
+ /// Gets or sets the outputPath where the generated manifest file should be saved to.
+ ///
+ [Option('o', "out", Required = false, HelpText = "OutputDirectory_HelpText", ResourceType = typeof(Resources))]
+ public string OutputDir { get; set; }
///
/// Gets or sets the GitHub token used to submit a pull request on behalf of the user.
///
[Option('t', "token", Required = false, HelpText = "GitHubToken_HelpText", ResourceType = typeof(Resources))]
public override string GitHubToken { get => base.GitHubToken; set => base.GitHubToken = value; }
-
- ///
- /// Executes the new command flow.
- ///
- /// Boolean representing success or fail of the command.
- public override async Task Execute()
- {
- CommandExecutedEvent commandEvent = new CommandExecutedEvent
- {
- Command = nameof(NewCommand),
+
+ ///
+ /// Executes the new command flow.
+ ///
+ /// Boolean representing success or fail of the command.
+ public override async Task Execute()
+ {
+ CommandExecutedEvent commandEvent = new CommandExecutedEvent
+ {
+ Command = nameof(NewCommand),
InstallerUrl = string.Join(',', this.InstallerUrls),
- HasGitHubToken = !string.IsNullOrEmpty(this.GitHubToken),
- };
-
- try
- {
- Prompt.Symbols.Done = new Symbol(string.Empty, string.Empty);
- Prompt.Symbols.Prompt = new Symbol(string.Empty, string.Empty);
-
- Manifests manifests = new Manifests();
+ HasGitHubToken = !string.IsNullOrEmpty(this.GitHubToken),
+ };
+
+ try
+ {
+ Prompt.Symbols.Done = new Symbol(string.Empty, string.Empty);
+ Prompt.Symbols.Prompt = new Symbol(string.Empty, string.Empty);
+
+ Manifests manifests = new Manifests();
if (!this.InstallerUrls.Any())
{
this.InstallerUrls = PromptProperty(
- new Installer(),
+ new Installer(),
this.InstallerUrls,
- nameof(Installer.InstallerUrl));
+ nameof(Installer.InstallerUrl));
Console.Clear();
- }
-
+ }
+
var packageFiles = await DownloadInstallers(this.InstallerUrls);
if (packageFiles == null)
- {
- return false;
- }
-
+ {
+ return false;
+ }
+
if (!PackageParser.ParsePackages(
packageFiles,
this.InstallerUrls,
manifests,
out List detectedArchs))
- {
- Logger.ErrorLocalized(nameof(Resources.PackageParsing_Error));
- return false;
+ {
+ Logger.ErrorLocalized(nameof(Resources.PackageParsing_Error));
+ return false;
}
DisplayMismatchedArchitectures(detectedArchs);
-
- Console.WriteLine(Resources.NewCommand_Header);
- Console.WriteLine();
- Logger.InfoLocalized(nameof(Resources.ManifestDocumentation_HelpText), ManifestDocumentationUrl);
- Console.WriteLine();
- Console.WriteLine(Resources.NewCommand_Description);
- Console.WriteLine();
-
+
+ Console.WriteLine(Resources.NewCommand_Header);
+ Console.WriteLine();
+ Logger.InfoLocalized(nameof(Resources.ManifestDocumentation_HelpText), ManifestDocumentationUrl);
+ Console.WriteLine();
+ Console.WriteLine(Resources.NewCommand_Description);
+ Console.WriteLine();
+
Logger.DebugLocalized(nameof(Resources.EnterFollowingFields_Message));
bool isManifestValid;
- do
+ do
{
if (this.WingetRepoOwner == DefaultWingetRepoOwner &&
- this.WingetRepo == DefaultWingetRepo &&
- !await this.PromptPackageIdentifierAndCheckDuplicates(manifests))
+ this.WingetRepo == DefaultWingetRepo)
{
- Console.WriteLine();
- Logger.ErrorLocalized(nameof(Resources.PackageIdAlreadyExists_Error));
- return false;
+ if (await this.LoadGitHubClient())
+ {
+ if (!await this.PromptPackageIdentifierAndCheckDuplicates(manifests))
+ {
+ Console.WriteLine();
+ Logger.ErrorLocalized(nameof(Resources.PackageIdAlreadyExists_Error));
+ return false;
+ }
+ }
+ else
+ {
+ return false;
+ }
}
-
+
PromptPropertiesAndDisplayManifests(manifests);
- isManifestValid = ValidateManifestsInTempDir(manifests);
- }
- while (Prompt.Confirm(Resources.ConfirmManifestCreation_Message));
-
- if (string.IsNullOrEmpty(this.OutputDir))
- {
- this.OutputDir = Directory.GetCurrentDirectory();
- }
-
+ isManifestValid = ValidateManifestsInTempDir(manifests);
+ }
+ while (Prompt.Confirm(Resources.ConfirmManifestCreation_Message));
+
+ if (string.IsNullOrEmpty(this.OutputDir))
+ {
+ this.OutputDir = Directory.GetCurrentDirectory();
+ }
+
SaveManifestDirToLocalPath(manifests, this.OutputDir);
if (isManifestValid && Prompt.Confirm(Resources.ConfirmGitHubSubmitManifest_Message))
- {
- if (await this.SetAndCheckGitHubToken())
+ {
+ if (await this.LoadGitHubClient(true))
{
- return commandEvent.IsSuccessful = await this.GitHubSubmitManifests(manifests, this.GitHubToken);
+ return commandEvent.IsSuccessful = await this.GitHubSubmitManifests(manifests);
}
return false;
}
else
{
- Console.WriteLine();
- Logger.WarnLocalized(nameof(Resources.SkippingPullRequest_Message));
+ Console.WriteLine();
+ Logger.WarnLocalized(nameof(Resources.SkippingPullRequest_Message));
return commandEvent.IsSuccessful = isManifestValid;
- }
- }
- finally
- {
- TelemetryManager.Log.WriteEvent(commandEvent);
- }
+ }
+ }
+ finally
+ {
+ TelemetryManager.Log.WriteEvent(commandEvent);
+ }
}
private static void PromptPropertiesAndDisplayManifests(Manifests manifests)
@@ -196,51 +205,51 @@ private static void PromptPropertiesAndDisplayManifests(Manifests manifests)
Console.WriteLine();
DisplayManifestPreview(manifests);
}
-
- private static void PromptRequiredProperties(T manifest, VersionManifest versionManifest = null)
- {
- var properties = manifest.GetType().GetProperties().ToList();
- var requiredProperties = properties.Where(p => p.GetCustomAttribute() != null).ToList();
-
- foreach (var property in requiredProperties)
- {
- if (property.PropertyType.IsGenericType)
- {
- // Generic logic for handling nested object models
- Type itemType = property.GetValue(manifest).GetType().GetGenericArguments().Single();
-
- if (itemType.Name == nameof(Installer))
- {
- PromptInstallerProperties(manifest, property);
- }
- }
- else if (property.PropertyType.IsValueType || property.PropertyType == typeof(string))
- {
+
+ private static void PromptRequiredProperties(T manifest, VersionManifest versionManifest = null)
+ {
+ var properties = manifest.GetType().GetProperties().ToList();
+ var requiredProperties = properties.Where(p => p.GetCustomAttribute() != null).ToList();
+
+ foreach (var property in requiredProperties)
+ {
+ if (property.PropertyType.IsGenericType)
+ {
+ // Generic logic for handling nested object models
+ Type itemType = property.GetValue(manifest).GetType().GetGenericArguments().Single();
+
+ if (itemType.Name == nameof(Installer))
+ {
+ PromptInstallerProperties(manifest, property);
+ }
+ }
+ else if (property.PropertyType.IsValueType || property.PropertyType == typeof(string))
+ {
if (property.Name == nameof(VersionManifest.PackageIdentifier) ||
property.Name == nameof(VersionManifest.ManifestType) ||
- property.Name == nameof(VersionManifest.ManifestVersion))
- {
- continue;
- }
-
- if (property.Name == nameof(VersionManifest.PackageVersion) && versionManifest != null)
- {
- property.SetValue(manifest, versionManifest.PackageVersion);
- continue;
- }
-
- if (property.Name == nameof(DefaultLocaleManifest.PackageLocale) && versionManifest != null)
- {
- property.SetValue(manifest, versionManifest.DefaultLocale);
- continue;
- }
-
- var currentValue = property.GetValue(manifest);
- var result = PromptProperty(manifest, currentValue, property.Name);
- property.SetValue(manifest, result);
- Logger.Trace($"Property [{property.Name}] set to the value [{result}]");
- }
- }
+ property.Name == nameof(VersionManifest.ManifestVersion))
+ {
+ continue;
+ }
+
+ if (property.Name == nameof(VersionManifest.PackageVersion) && versionManifest != null)
+ {
+ property.SetValue(manifest, versionManifest.PackageVersion);
+ continue;
+ }
+
+ if (property.Name == nameof(DefaultLocaleManifest.PackageLocale) && versionManifest != null)
+ {
+ property.SetValue(manifest, versionManifest.DefaultLocale);
+ continue;
+ }
+
+ var currentValue = property.GetValue(manifest);
+ var result = PromptProperty(manifest, currentValue, property.Name);
+ property.SetValue(manifest, result);
+ Logger.Trace($"Property [{property.Name}] set to the value [{result}]");
+ }
+ }
}
private static void PromptOptionalProperties(T manifest)
@@ -259,13 +268,13 @@ private static void PromptOptionalProperties(T manifest)
}
}
- private static void PromptInstallerProperties(T manifest, PropertyInfo property)
- {
- List installers = new List((ICollection)property.GetValue(manifest));
+ private static void PromptInstallerProperties(T manifest, PropertyInfo property)
+ {
+ List installers = new List((ICollection)property.GetValue(manifest));
foreach (var installer in installers)
{
var installerProperties = installer.GetType().GetProperties().ToList();
-
+
var requiredInstallerProperties = installerProperties
.Where(p => Attribute.IsDefined(p, typeof(RequiredAttribute)) || p.Name == nameof(InstallerType)).ToList();
@@ -295,59 +304,59 @@ private static void PromptInstallerProperties(T manifest, PropertyInfo proper
Logger.Debug($"Additional metadata needed for installer from {installer.InstallerUrl}");
prompted = true;
}
-
+
var result = PromptProperty(installer, currentValue, requiredProperty.Name);
requiredProperty.SetValue(installer, result);
- }
- }
- }
- }
-
- private static void PromptInstallerSwitchesForExe(T manifest)
- {
- InstallerSwitches installerSwitches = new InstallerSwitches();
-
- var silentSwitchResult = PromptProperty(installerSwitches, installerSwitches.Silent, nameof(InstallerSwitches.Silent));
- var silentWithProgressSwitchResult = PromptProperty(installerSwitches, installerSwitches.SilentWithProgress, nameof(InstallerSwitches.SilentWithProgress));
-
- bool updateSwitches = false;
-
- if (!string.IsNullOrEmpty(silentSwitchResult))
- {
- installerSwitches.Silent = silentSwitchResult;
- updateSwitches = true;
- }
-
- if (!string.IsNullOrEmpty(silentWithProgressSwitchResult))
- {
- installerSwitches.SilentWithProgress = silentWithProgressSwitchResult;
- updateSwitches = true;
- }
-
- if (updateSwitches)
- {
- manifest.GetType().GetProperty(nameof(InstallerSwitches)).SetValue(manifest, installerSwitches);
- }
- }
-
- private static T PromptProperty(object model, T property, string memberName, string message = null)
- {
- message ??= $"[{memberName}] " +
+ }
+ }
+ }
+ }
+
+ private static void PromptInstallerSwitchesForExe(T manifest)
+ {
+ InstallerSwitches installerSwitches = new InstallerSwitches();
+
+ var silentSwitchResult = PromptProperty(installerSwitches, installerSwitches.Silent, nameof(InstallerSwitches.Silent));
+ var silentWithProgressSwitchResult = PromptProperty(installerSwitches, installerSwitches.SilentWithProgress, nameof(InstallerSwitches.SilentWithProgress));
+
+ bool updateSwitches = false;
+
+ if (!string.IsNullOrEmpty(silentSwitchResult))
+ {
+ installerSwitches.Silent = silentSwitchResult;
+ updateSwitches = true;
+ }
+
+ if (!string.IsNullOrEmpty(silentWithProgressSwitchResult))
+ {
+ installerSwitches.SilentWithProgress = silentWithProgressSwitchResult;
+ updateSwitches = true;
+ }
+
+ if (updateSwitches)
+ {
+ manifest.GetType().GetProperty(nameof(InstallerSwitches)).SetValue(manifest, installerSwitches);
+ }
+ }
+
+ private static T PromptProperty(object model, T property, string memberName, string message = null)
+ {
+ message ??= $"[{memberName}] " +
Resources.ResourceManager.GetString($"{memberName}_KeywordDescription") ?? memberName;
// Because some properties don't have a current value, we can't rely on T or the property to obtain the type.
// Use reflection to obtain the type by looking up the property type by membername based on the model.
Type typeFromModel = model.GetType().GetProperty(memberName).PropertyType;
- if (typeFromModel.IsEnum)
- {
- // For enums, we want to call Prompt.Select, specifically the overload that takes 4 parameters
- var generic = typeof(Prompt)
- .GetMethods()
- .Where(mi => mi.Name == nameof(Prompt.Select) && mi.GetParameters().Length == 5)
- .Single()
- .MakeGenericMethod(property.GetType());
-
- return (T)generic.Invoke(null, new object[] { message, property.GetType().GetEnumValues(), null, property, null });
+ if (typeFromModel.IsEnum)
+ {
+ // For enums, we want to call Prompt.Select, specifically the overload that takes 4 parameters
+ var generic = typeof(Prompt)
+ .GetMethods()
+ .Where(mi => mi.Name == nameof(Prompt.Select) && mi.GetParameters().Length == 5)
+ .Single()
+ .MakeGenericMethod(property.GetType());
+
+ return (T)generic.Invoke(null, new object[] { message, property.GetType().GetEnumValues(), null, property, null });
}
else if (typeof(IEnumerable).IsAssignableFrom(typeof(T)) || typeof(IEnumerable).IsAssignableFrom(typeFromModel))
{
@@ -362,11 +371,11 @@ private static T PromptProperty(object model, T property, string memberName,
string promptResult = Prompt.Input(message, combinedString, new[] { FieldValidation.ValidateProperty(model, memberName, property) });
return (T)(object)promptResult?.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).ToList();
}
- else
- {
- var promptResult = Prompt.Input(message, property, new[] { FieldValidation.ValidateProperty(model, memberName, property) });
- return promptResult is string str ? (T)(object)str.Trim() : promptResult;
- }
+ else
+ {
+ var promptResult = Prompt.Input(message, property, new[] { FieldValidation.ValidateProperty(model, memberName, property) });
+ return promptResult is string str ? (T)(object)str.Trim() : promptResult;
+ }
}
///
@@ -377,20 +386,19 @@ private static T PromptProperty(object model, T property, string memberName,
/// Boolean value indicating whether the package identifier is valid.
private async Task PromptPackageIdentifierAndCheckDuplicates(Manifests manifests)
{
- GitHub client = new GitHub(this.GitHubToken, this.WingetRepoOwner, this.WingetRepo);
-
- if (!string.IsNullOrEmpty(this.GitHubToken))
- {
- if (!await this.SetAndCheckGitHubToken())
- {
- return false;
- }
- }
-
VersionManifest versionManifest = manifests.VersionManifest;
versionManifest.PackageIdentifier = PromptProperty(versionManifest, versionManifest.PackageIdentifier, nameof(versionManifest.PackageIdentifier));
- string exactMatch = await client.FindPackageId(versionManifest.PackageIdentifier);
+ string exactMatch;
+ try
+ {
+ exactMatch = await this.GitHubClient.FindPackageId(versionManifest.PackageIdentifier);
+ }
+ catch (Octokit.RateLimitExceededException)
+ {
+ Logger.ErrorLocalized(nameof(Resources.RateLimitExceeded_Message));
+ return false;
+ }
if (!string.IsNullOrEmpty(exactMatch))
{
@@ -402,6 +410,6 @@ private async Task PromptPackageIdentifierAndCheckDuplicates(Manifests man
manifests.DefaultLocaleManifest.PackageIdentifier = versionManifest.PackageIdentifier;
return true;
}
- }
- }
-}
+ }
+ }
+}
diff --git a/src/WingetCreateCLI/Commands/SubmitCommand.cs b/src/WingetCreateCLI/Commands/SubmitCommand.cs
index a8923bdb..85f8cb82 100644
--- a/src/WingetCreateCLI/Commands/SubmitCommand.cs
+++ b/src/WingetCreateCLI/Commands/SubmitCommand.cs
@@ -76,7 +76,7 @@ public override async Task Execute()
return false;
}
- if (!await this.SetAndCheckGitHubToken())
+ if (!await this.LoadGitHubClient(true))
{
return false;
}
@@ -95,13 +95,13 @@ private async Task SubmitManifest()
{
Manifests manifests = new Manifests();
manifests.SingletonManifest = Serialization.DeserializeFromPath(this.Path);
- return await this.GitHubSubmitManifests(manifests, this.GitHubToken);
+ return await this.GitHubSubmitManifests(manifests);
}
else if (Directory.Exists(this.Path) && ValidateManifest(this.Path))
{
List manifestContents = Directory.GetFiles(this.Path).Select(f => File.ReadAllText(f)).ToList();
Manifests manifests = Serialization.DeserializeManifestContents(manifestContents);
- return await this.GitHubSubmitManifests(manifests, this.GitHubToken);
+ return await this.GitHubSubmitManifests(manifests);
}
else
{
diff --git a/src/WingetCreateCLI/Commands/TokenCommand.cs b/src/WingetCreateCLI/Commands/TokenCommand.cs
index 98a0c910..a61ab218 100644
--- a/src/WingetCreateCLI/Commands/TokenCommand.cs
+++ b/src/WingetCreateCLI/Commands/TokenCommand.cs
@@ -56,8 +56,10 @@ public override async Task Execute()
}
else if (this.Store)
{
- Logger.InfoLocalized(nameof(Resources.StoreToken_Message));
- return commandEvent.IsSuccessful = await this.SetAndCheckGitHubToken(true);
+ Logger.InfoLocalized(nameof(Resources.SettingToken_Message));
+ return commandEvent.IsSuccessful = string.IsNullOrEmpty(this.GitHubToken) ?
+ await this.GetTokenFromOAuth() :
+ this.StoreTokenInCache();
}
return false;
diff --git a/src/WingetCreateCLI/Commands/UpdateCommand.cs b/src/WingetCreateCLI/Commands/UpdateCommand.cs
index 373e5873..32dd610c 100644
--- a/src/WingetCreateCLI/Commands/UpdateCommand.cs
+++ b/src/WingetCreateCLI/Commands/UpdateCommand.cs
@@ -16,7 +16,6 @@ namespace Microsoft.WingetCreateCLI.Commands
using Microsoft.WingetCreateCLI.Telemetry;
using Microsoft.WingetCreateCLI.Telemetry.Events;
using Microsoft.WingetCreateCore;
- using Microsoft.WingetCreateCore.Common;
using Microsoft.WingetCreateCore.Models;
using Microsoft.WingetCreateCore.Models.DefaultLocale;
using Microsoft.WingetCreateCore.Models.Installer;
@@ -110,14 +109,9 @@ public override async Task Execute()
return false;
}
- GitHub client = new GitHub(this.GitHubToken, this.WingetRepoOwner, this.WingetRepo);
-
- if (!string.IsNullOrEmpty(this.GitHubToken))
+ if (!await this.LoadGitHubClient())
{
- if (!await this.SetAndCheckGitHubToken())
- {
- return false;
- }
+ return false;
}
Logger.DebugLocalized(nameof(Resources.RetrievingManifest_Message), this.Id);
@@ -125,11 +119,11 @@ public override async Task Execute()
string exactId;
try
{
- exactId = await client.FindPackageId(this.Id);
+ exactId = await this.GitHubClient.FindPackageId(this.Id);
}
- catch (Octokit.NotFoundException)
+ catch (Octokit.RateLimitExceededException)
{
- Logger.ErrorLocalized(nameof(Resources.RepositoryNotFound_Error), this.WingetRepoOwner, this.WingetRepo);
+ Logger.ErrorLocalized(nameof(Resources.RateLimitExceeded_Message));
return false;
}
@@ -142,7 +136,7 @@ public override async Task Execute()
try
{
- latestManifestContent = await client.GetLatestManifestContentAsync(this.Id);
+ latestManifestContent = await this.GitHubClient.GetLatestManifestContentAsync(this.Id);
}
catch (Octokit.NotFoundException e)
{
@@ -291,8 +285,8 @@ public async Task ExecuteManifestUpdate(List latestManifestContent
return false;
}
- return await this.SetAndCheckGitHubToken()
- ? (commandEvent.IsSuccessful = await this.GitHubSubmitManifests(updatedManifests, this.GitHubToken))
+ return await this.LoadGitHubClient(true)
+ ? (commandEvent.IsSuccessful = await this.GitHubSubmitManifests(updatedManifests))
: false;
}
diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs
index 843f5ccd..e161354d 100644
--- a/src/WingetCreateCLI/Properties/Resources.Designer.cs
+++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs
@@ -276,6 +276,15 @@ public static string DeletingInstaller_Message {
}
}
+ ///
+ /// Looks up a localized string similar to Cached token was invalid, deleting token from cache....
+ ///
+ public static string DeletingInvalidCachedToken_Message {
+ get {
+ return ResourceManager.GetString("DeletingInvalidCachedToken_Message", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Collection of package dependencies.
///
@@ -718,11 +727,11 @@ public static string Interactive_KeywordDescription {
}
///
- /// Looks up a localized string similar to Cached token was invalid, deleting and launching OAuth flow.
+ /// Looks up a localized string similar to Token was invalid. Please generate a new GitHub token and try again..
///
- public static string InvalidCachedToken {
+ public static string InvalidGitHubToken_Message {
get {
- return ResourceManager.GetString("InvalidCachedToken", resourceCulture);
+ return ResourceManager.GetString("InvalidGitHubToken_Message", resourceCulture);
}
}
@@ -1230,6 +1239,15 @@ public static string PullRequestURI_Message {
}
}
+ ///
+ /// Looks up a localized string similar to GitHub api rate limit exceeded. To extend your rate limit, provide your GitHub token with the "-t" flag or store one using the "token --store" command..
+ ///
+ public static string RateLimitExceeded_Message {
+ get {
+ return ResourceManager.GetString("RateLimitExceeded_Message", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Input does not match the valid format pattern for this field..
///
@@ -1293,6 +1311,15 @@ public static string SettingsCommand_HelpText {
}
}
+ ///
+ /// Looks up a localized string similar to Setting GitHub token....
+ ///
+ public static string SettingToken_Message {
+ get {
+ return ResourceManager.GetString("SettingToken_Message", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to The SHA256 installer hash.
///
@@ -1357,11 +1384,11 @@ public static string Store_HelpText {
}
///
- /// Looks up a localized string similar to Setting cached GitHub token.
+ /// Looks up a localized string similar to Token stored in cache successfully..
///
- public static string StoreToken_Message {
+ public static string StoringToken_Message {
get {
- return ResourceManager.GetString("StoreToken_Message", resourceCulture);
+ return ResourceManager.GetString("StoringToken_Message", resourceCulture);
}
}
@@ -1527,15 +1554,6 @@ public static string Url_KeywordDescription {
}
}
- ///
- /// Looks up a localized string similar to Using GitHub token from cache....
- ///
- public static string UsingTokenFromCache_Message {
- get {
- return ResourceManager.GetString("UsingTokenFromCache_Message", resourceCulture);
- }
- }
-
///
/// Looks up a localized string similar to Please check and verify the usage of this command by passing in the --help flag..
///
@@ -1582,7 +1600,7 @@ public static string WindowsLibraries_KeywordDescription {
}
///
- /// Looks up a localized string similar to Writing the OAuth token to cache failed: {0}.
+ /// Looks up a localized string similar to Writing the token to cache failed: {0}.
///
public static string WritingCacheTokenFailed_Message {
get {
diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx
index b208e2eb..6ff1e408 100644
--- a/src/WingetCreateCLI/Properties/Resources.resx
+++ b/src/WingetCreateCLI/Properties/Resources.resx
@@ -268,9 +268,6 @@
The interactive installer switches
-
- Cached token was invalid, deleting and launching OAuth flow
-
Overrides default language
@@ -380,9 +377,6 @@
The silent installer switches
-
- Setting cached GitHub token
-
Set the cached GitHub token. Can specify token to cache with --token parameter, otherwise will initiate OAuth flow.
@@ -429,7 +423,7 @@
The package version |e.g. 1.2.3.4|
- Writing the OAuth token to cache failed: {0}
+ Writing the token to cache failed: {0}
Would you like to make changes to this manifest?
@@ -459,9 +453,6 @@
Resuming command execution...
-
- Using GitHub token from cache...
-
The package name |e.g. Visual Studio|
@@ -658,4 +649,21 @@
Installer cache cleaned.
+
+ Cached token was invalid, deleting token from cache...
+
+
+ Token was invalid. Please generate a new GitHub token and try again.
+
+
+ GitHub api rate limit exceeded. To extend your rate limit, provide your GitHub token with the "-t" flag or store one using the "token --store" command.
+ "-t" refers to a command line switch argument
+"token --store" refers to a command line argument
+
+
+ Setting GitHub token...
+
+
+ Token stored in cache successfully.
+
\ No newline at end of file
diff --git a/src/WingetCreateCore/Common/GitHub.cs b/src/WingetCreateCore/Common/GitHub.cs
index e6cbf318..7e547a7f 100644
--- a/src/WingetCreateCore/Common/GitHub.cs
+++ b/src/WingetCreateCore/Common/GitHub.cs
@@ -1,205 +1,205 @@
-// Copyright (c) Microsoft. All rights reserved.
-// Licensed under the MIT license.
-
-namespace Microsoft.WingetCreateCore.Common
-{
- using System;
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Microsoft.WingetCreateCore.Common
+{
+ using System;
using System.Collections.Generic;
using System.IO;
- using System.Linq;
- using System.Security.Cryptography;
- using System.Threading.Tasks;
- using Jose;
- using Microsoft.WingetCreateCore.Models;
- using Octokit;
-
- ///
- /// Provides functionality for interacting a user's GitHub account.
- ///
- public class GitHub
- {
- private const string HeadMasterRef = "heads/master";
- private const string PRDescriptionRepoPath = ".github/PULL_REQUEST_TEMPLATE.md";
- private const string UserAgentName = "WingetCreate";
- private readonly GitHubClient github;
- private readonly string wingetRepoOwner;
- private readonly string wingetRepo;
-
- ///
- /// Initializes a new instance of the class.
- ///
- /// GitHub access token.
- /// Winget repository owner.
- /// Winget repository.
- public GitHub(string githubApiToken, string wingetRepoOwner, string wingetRepo)
- {
- this.wingetRepoOwner = wingetRepoOwner;
- this.wingetRepo = wingetRepo;
- this.github = new GitHubClient(new ProductHeaderValue(UserAgentName));
- if (githubApiToken != null)
- {
- this.github.Credentials = new Credentials(githubApiToken, AuthenticationType.Bearer);
- }
- }
-
- ///
- /// Gets an access token to use for GitHub operations performed from a GitHub app context.
- ///
- /// The private key for the GitHub app in PEM format.
- /// The id for the GitHub app.
- /// Winget repository owner.
- /// Winget repository.
- /// GitHub app installation access token to use for GitHub operations.
- public static async Task GetGitHubAppInstallationAccessToken(string gitHubAppPrivateKeyPem, int gitHubAppId, string wingetRepoOwner, string wingetRepo)
- {
- string jwtToken = GetJwtToken(gitHubAppPrivateKeyPem, gitHubAppId);
-
- var github = new GitHubClient(new ProductHeaderValue(UserAgentName));
- github.Credentials = new Credentials(jwtToken, AuthenticationType.Bearer);
-
- var installation = await github.GitHubApps.GetRepositoryInstallationForCurrent(wingetRepoOwner, wingetRepo);
- var response = await github.GitHubApps.CreateInstallationToken(installation.Id);
- return response.Token;
- }
-
- ///
- /// Gets all app manifests in the repo.
- ///
- /// A list of , each representing a single app manifest version.
- public async Task> GetAppVersions()
- {
- var reference = await this.github.Git.Reference.Get(this.wingetRepoOwner, this.wingetRepo, HeadMasterRef);
- var tree = await this.github.Git.Tree.GetRecursive(this.wingetRepoOwner, this.wingetRepo, reference.Object.Sha);
- return tree.Tree
- .Where(i => i.Path.StartsWith(Constants.WingetManifestRoot + "/") && i.Type.Value == TreeType.Blob)
- .Select(i => new { i.Path, PathTokens = i.Path[Constants.WingetManifestRoot.Length..].Split('/') })
- .Where(i => i.PathTokens.Length >= 3)
- .Select(i =>
- {
- // Substring path will be in the form of
- // Microsoft/PowerToys/0.15.2.yaml, or
- // Microsoft/VisualStudio/Community/16.0.30011.22.yaml
- string publisher = i.PathTokens[0];
- string version = i.PathTokens[^1].Replace(".yaml", string.Empty, StringComparison.OrdinalIgnoreCase);
- string app = string.Join('.', i.PathTokens[1..^1]);
- return new PublisherAppVersion(publisher, app, version, $"{publisher}.{app}", i.Path);
- })
- .ToList();
+ using System.Linq;
+ using System.Security.Cryptography;
+ using System.Threading.Tasks;
+ using Jose;
+ using Microsoft.WingetCreateCore.Models;
+ using Octokit;
+
+ ///
+ /// Provides functionality for interacting a user's GitHub account.
+ ///
+ public class GitHub
+ {
+ private const string HeadMasterRef = "heads/master";
+ private const string PRDescriptionRepoPath = ".github/PULL_REQUEST_TEMPLATE.md";
+ private const string UserAgentName = "WingetCreate";
+ private readonly GitHubClient github;
+ private readonly string wingetRepoOwner;
+ private readonly string wingetRepo;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// GitHub access token.
+ /// Winget repository owner.
+ /// Winget repository.
+ public GitHub(string githubApiToken, string wingetRepoOwner, string wingetRepo)
+ {
+ this.wingetRepoOwner = wingetRepoOwner;
+ this.wingetRepo = wingetRepo;
+ this.github = new GitHubClient(new ProductHeaderValue(UserAgentName));
+ if (githubApiToken != null)
+ {
+ this.github.Credentials = new Credentials(githubApiToken, AuthenticationType.Bearer);
+ }
+ }
+
+ ///
+ /// Gets an access token to use for GitHub operations performed from a GitHub app context.
+ ///
+ /// The private key for the GitHub app in PEM format.
+ /// The id for the GitHub app.
+ /// Winget repository owner.
+ /// Winget repository.
+ /// GitHub app installation access token to use for GitHub operations.
+ public static async Task GetGitHubAppInstallationAccessToken(string gitHubAppPrivateKeyPem, int gitHubAppId, string wingetRepoOwner, string wingetRepo)
+ {
+ string jwtToken = GetJwtToken(gitHubAppPrivateKeyPem, gitHubAppId);
+
+ var github = new GitHubClient(new ProductHeaderValue(UserAgentName));
+ github.Credentials = new Credentials(jwtToken, AuthenticationType.Bearer);
+
+ var installation = await github.GitHubApps.GetRepositoryInstallationForCurrent(wingetRepoOwner, wingetRepo);
+ var response = await github.GitHubApps.CreateInstallationToken(installation.Id);
+ return response.Token;
+ }
+
+ ///
+ /// Gets all app manifests in the repo.
+ ///
+ /// A list of , each representing a single app manifest version.
+ public async Task> GetAppVersions()
+ {
+ var reference = await this.github.Git.Reference.Get(this.wingetRepoOwner, this.wingetRepo, HeadMasterRef);
+ var tree = await this.github.Git.Tree.GetRecursive(this.wingetRepoOwner, this.wingetRepo, reference.Object.Sha);
+ return tree.Tree
+ .Where(i => i.Path.StartsWith(Constants.WingetManifestRoot + "/") && i.Type.Value == TreeType.Blob)
+ .Select(i => new { i.Path, PathTokens = i.Path[Constants.WingetManifestRoot.Length..].Split('/') })
+ .Where(i => i.PathTokens.Length >= 3)
+ .Select(i =>
+ {
+ // Substring path will be in the form of
+ // Microsoft/PowerToys/0.15.2.yaml, or
+ // Microsoft/VisualStudio/Community/16.0.30011.22.yaml
+ string publisher = i.PathTokens[0];
+ string version = i.PathTokens[^1].Replace(".yaml", string.Empty, StringComparison.OrdinalIgnoreCase);
+ string app = string.Join('.', i.PathTokens[1..^1]);
+ return new PublisherAppVersion(publisher, app, version, $"{publisher}.{app}", i.Path);
+ })
+ .ToList();
+ }
+
+ ///
+ /// Obtains the latest manifest using the specified packageId.
+ ///
+ /// PackageId of the manifest to be retrieved.
+ /// Manifest as a string.
+ public async Task> GetLatestManifestContentAsync(string packageId)
+ {
+ string appPath = Utils.GetAppManifestDirPath(packageId, string.Empty, '/');
+ var contents = await this.github.Repository.Content.GetAllContents(this.wingetRepoOwner, this.wingetRepo, appPath);
+
+ string version = contents
+ .Where(c => c.Type == ContentType.Dir)
+ .OrderByDescending(c => c.Name, new VersionComparer())
+ .Select(c => c.Path)
+ .FirstOrDefault();
+
+ var packageContents = (await this.github.Repository.Content.GetAllContents(this.wingetRepoOwner, this.wingetRepo, version))
+ .Where(c => c.Type != ContentType.Dir && Path.GetExtension(c.Name).EqualsIC(".yaml"));
+
+ // If all contents of version directory are directories themselves, user must've provided an invalid packageId.
+ if (!packageContents.Any())
+ {
+ throw new NotFoundException(nameof(packageId), System.Net.HttpStatusCode.NotFound);
+ }
+
+ List manifestContent = new List();
+
+ foreach (RepositoryContent content in packageContents)
+ {
+ string fileContent = await this.GetFileContentsAsync(content.Path);
+ manifestContent.Add(fileContent);
+ }
+
+ return manifestContent;
}
-
- ///
- /// Obtains the latest manifest using the specified packageId.
- ///
- /// PackageId of the manifest to be retrieved.
- /// Manifest as a string.
- public async Task> GetLatestManifestContentAsync(string packageId)
- {
- string appPath = Utils.GetAppManifestDirPath(packageId, string.Empty, '/');
- var contents = await this.github.Repository.Content.GetAllContents(this.wingetRepoOwner, this.wingetRepo, appPath);
-
- string version = contents
- .Where(c => c.Type == ContentType.Dir)
- .OrderByDescending(c => c.Name, new VersionComparer())
- .Select(c => c.Path)
- .FirstOrDefault();
-
- var packageContents = (await this.github.Repository.Content.GetAllContents(this.wingetRepoOwner, this.wingetRepo, version))
- .Where(c => c.Type != ContentType.Dir && string.Equals(Path.GetExtension(c.Name), ".yaml", StringComparison.OrdinalIgnoreCase));
-
- // If all contents of version directory are directories themselves, user must've provided an invalid packageId.
- if (!packageContents.Any())
- {
- throw new NotFoundException(nameof(packageId), System.Net.HttpStatusCode.NotFound);
- }
-
- List manifestContent = new List();
-
- foreach (RepositoryContent content in packageContents)
+
+ ///
+ /// Submits a pull request on behalf of the user.
+ ///
+ /// Wrapper object for manifest object models to be submitted in the PR.
+ /// Bool indicating whether or not to submit the PR via a fork.
+ /// Pull request object.
+ public Task SubmitPullRequestAsync(Manifests manifests, bool submitToFork)
+ {
+ Dictionary contents = new Dictionary();
+ string id;
+ string version;
+
+ if (manifests.SingletonManifest != null)
{
- string fileContent = await this.GetFileContentsAsync(content.Path);
- manifestContent.Add(fileContent);
- }
-
- return manifestContent;
- }
-
- ///
- /// Submits a pull request on behalf of the user.
- ///
- /// Wrapper object for manifest object models to be submitted in the PR.
- /// Bool indicating whether or not to submit the PR via a fork.
- /// Pull request object.
- public Task SubmitPullRequestAsync(Manifests manifests, bool submitToFork)
- {
- Dictionary contents = new Dictionary();
- string id;
- string version;
-
- if (manifests.SingletonManifest != null)
- {
- id = manifests.SingletonManifest.PackageIdentifier;
- version = manifests.SingletonManifest.PackageVersion;
- contents.Add(manifests.SingletonManifest.PackageIdentifier, manifests.SingletonManifest.ToYaml());
- }
- else
- {
- id = manifests.VersionManifest.PackageIdentifier;
- version = manifests.VersionManifest.PackageVersion;
-
- contents = manifests.LocaleManifests.ToDictionary(locale => $"{id}.locale.{locale.PackageLocale}", locale => locale.ToYaml());
-
- contents.Add(id, manifests.VersionManifest.ToYaml());
- contents.Add($"{id}.installer", manifests.InstallerManifest.ToYaml());
- contents.Add($"{id}.locale.{manifests.DefaultLocaleManifest.PackageLocale}", manifests.DefaultLocaleManifest.ToYaml());
- }
-
- return this.SubmitPRAsync(id, version, contents, submitToFork);
- }
-
- ///
- /// Closes an open pull request and deletes its branch if not on forked repo.
- ///
- /// The pull request number.
- /// A representing the asynchronous operation.
- public async Task ClosePullRequest(int pullRequestId)
- {
- // Close PR and delete its branch.
- await this.github.PullRequest.Update(this.wingetRepoOwner, this.wingetRepo, pullRequestId, new PullRequestUpdate() { State = ItemState.Closed });
- await this.DeletePullRequestBranch(pullRequestId);
- }
-
- ///
- /// Merges an open pull request.
- ///
- /// The pull request number.
- /// A representing the asynchronous operation.
- public async Task MergePullRequest(int pullRequestId)
- {
- await this.github.PullRequest.Merge(this.wingetRepoOwner, this.wingetRepo, pullRequestId, new MergePullRequest());
- await this.DeletePullRequestBranch(pullRequestId);
- }
-
- ///
- /// Retrieves file contents from a specified GitHub path.
- ///
- /// GitHub path where the files should be retrieved from.
- /// Contents from the specified GitHub path.
- public async Task GetFileContentsAsync(string path)
- {
- var contents = (await this.github.Repository.Content.GetAllContents(this.wingetRepoOwner, this.wingetRepo, path))
- .Select(f => f.Content)
- .First();
-
- return contents;
- }
-
- ///
- /// Checks that the GitHub client can perform operations against the repo using the auth token.
- ///
- /// A representing the asynchronous operation.
- public async Task CheckAccess()
- {
- await this.github.Repository.Get(this.wingetRepoOwner, this.wingetRepo);
+ id = manifests.SingletonManifest.PackageIdentifier;
+ version = manifests.SingletonManifest.PackageVersion;
+ contents.Add(manifests.SingletonManifest.PackageIdentifier, manifests.SingletonManifest.ToYaml());
+ }
+ else
+ {
+ id = manifests.VersionManifest.PackageIdentifier;
+ version = manifests.VersionManifest.PackageVersion;
+
+ contents = manifests.LocaleManifests.ToDictionary(locale => $"{id}.locale.{locale.PackageLocale}", locale => locale.ToYaml());
+
+ contents.Add(id, manifests.VersionManifest.ToYaml());
+ contents.Add($"{id}.installer", manifests.InstallerManifest.ToYaml());
+ contents.Add($"{id}.locale.{manifests.DefaultLocaleManifest.PackageLocale}", manifests.DefaultLocaleManifest.ToYaml());
+ }
+
+ return this.SubmitPRAsync(id, version, contents, submitToFork);
+ }
+
+ ///
+ /// Closes an open pull request and deletes its branch if not on forked repo.
+ ///
+ /// The pull request number.
+ /// A representing the asynchronous operation.
+ public async Task ClosePullRequest(int pullRequestId)
+ {
+ // Close PR and delete its branch.
+ await this.github.PullRequest.Update(this.wingetRepoOwner, this.wingetRepo, pullRequestId, new PullRequestUpdate() { State = ItemState.Closed });
+ await this.DeletePullRequestBranch(pullRequestId);
+ }
+
+ ///
+ /// Merges an open pull request.
+ ///
+ /// The pull request number.
+ /// A representing the asynchronous operation.
+ public async Task MergePullRequest(int pullRequestId)
+ {
+ await this.github.PullRequest.Merge(this.wingetRepoOwner, this.wingetRepo, pullRequestId, new MergePullRequest());
+ await this.DeletePullRequestBranch(pullRequestId);
+ }
+
+ ///
+ /// Retrieves file contents from a specified GitHub path.
+ ///
+ /// GitHub path where the files should be retrieved from.
+ /// Contents from the specified GitHub path.
+ public async Task GetFileContentsAsync(string path)
+ {
+ var contents = (await this.github.Repository.Content.GetAllContents(this.wingetRepoOwner, this.wingetRepo, path))
+ .Select(f => f.Content)
+ .First();
+
+ return contents;
+ }
+
+ ///
+ /// Checks that the GitHub client can perform operations against the repo using the auth token.
+ ///
+ /// A representing the asynchronous operation.
+ public async Task CheckAccess()
+ {
+ await this.github.Repository.Get(this.wingetRepoOwner, this.wingetRepo);
}
///
@@ -211,32 +211,32 @@ public async Task FindPackageId(string packageId)
{
string path = Constants.WingetManifestRoot + '/' + $"{char.ToLowerInvariant(packageId[0])}";
return await this.FindPackageIdRecursive(packageId.Split('.'), path, string.Empty, 0);
- }
-
- ///
- /// 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.
- ///
- /// The private key for the GitHub app in PEM format.
- /// The id for the GitHub app.
- /// Signed JWT token, expiring in 10 minutes.
- private static string GetJwtToken(string gitHubAppPrivateKeyPem, int gitHubAppId)
- {
- var rsa = RSA.Create();
- rsa.ImportFromPem(gitHubAppPrivateKeyPem);
-
- var payload = new
- {
- // issued at time, 60 seconds in the past to allow for clock drift
- iat = DateTimeOffset.UtcNow.AddMinutes(-1).ToUnixTimeSeconds(),
-
- // JWT expiration time (10 minute maximum)
- exp = DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds(),
-
- // GitHub App's identifier
- iss = gitHubAppId,
- };
-
- return JWT.Encode(payload, rsa, JwsAlgorithm.RS256);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// The private key for the GitHub app in PEM format.
+ /// The id for the GitHub app.
+ /// Signed JWT token, expiring in 10 minutes.
+ private static string GetJwtToken(string gitHubAppPrivateKeyPem, int gitHubAppId)
+ {
+ var rsa = RSA.Create();
+ rsa.ImportFromPem(gitHubAppPrivateKeyPem);
+
+ var payload = new
+ {
+ // issued at time, 60 seconds in the past to allow for clock drift
+ iat = DateTimeOffset.UtcNow.AddMinutes(-1).ToUnixTimeSeconds(),
+
+ // JWT expiration time (10 minute maximum)
+ exp = DateTimeOffset.UtcNow.AddMinutes(10).ToUnixTimeSeconds(),
+
+ // GitHub App's identifier
+ iss = gitHubAppId,
+ };
+
+ return JWT.Encode(payload, rsa, JwsAlgorithm.RS256);
}
private async Task FindPackageIdRecursive(string[] packageId, string path, string exactPackageId, int index)
@@ -261,99 +261,99 @@ private async Task FindPackageIdRecursive(string[] packageId, string pat
}
return null;
- }
-
- private async Task SubmitPRAsync(string packageId, string version, Dictionary contents, bool submitToFork)
- {
- bool createdRepo = false;
-
- Repository repo;
- if (submitToFork)
- {
- try
- {
- var user = await this.github.User.Current();
- repo = await this.github.Repository.Get(user.Login, this.wingetRepo);
- }
- catch (NotFoundException)
- {
- repo = await this.github.Repository.Forks.Create(this.wingetRepoOwner, this.wingetRepo, new NewRepositoryFork());
- createdRepo = true;
- }
- }
- else
- {
- repo = await this.github.Repository.Get(this.wingetRepoOwner, this.wingetRepo);
- }
-
- string newBranchName = $"autogenerated/{packageId}/{Guid.NewGuid()}";
- string newBranchNameHeads = $"heads/{newBranchName}";
-
- string message = $"{packageId} version {version}";
-
- var upstreamMaster = await this.github.Git.Reference.Get(this.wingetRepoOwner, this.wingetRepo, HeadMasterRef);
- var upstreamMasterSha = upstreamMaster.Object.Sha;
-
- Reference newBranch = null;
- try
- {
- // Create new branch synced to upstream master
- await this.github.Git.Reference.Create(repo.Id, new NewReference($"refs/{newBranchNameHeads}", upstreamMasterSha));
-
- // Update from upstream branch master
- newBranch = await this.github.Git.Reference.Update(repo.Id, newBranchNameHeads, new ReferenceUpdate(upstreamMasterSha));
- var updatedSha = newBranch.Object.Sha;
-
- var nt = new NewTree { BaseTree = updatedSha };
- string appPath = Utils.GetAppManifestDirPath(packageId, version, '/');
-
- foreach (KeyValuePair item in contents)
- {
- string file = $"{appPath}/{item.Key}.yaml";
- nt.Tree.Add(new NewTreeItem { Path = file, Mode = "100644", Type = TreeType.Blob, Content = item.Value });
- }
-
- var newTree = await this.github.Git.Tree.Create(repo.Id, nt);
-
- var newCommit = new NewCommit(message, newTree.Sha, updatedSha);
- var commit = await this.github.Git.Commit.Create(repo.Id, newCommit);
-
- await this.github.Git.Reference.Update(repo.Id, newBranchNameHeads, new ReferenceUpdate(commit.Sha));
-
- // Get latest description template from repo
- string description = await this.GetFileContentsAsync(PRDescriptionRepoPath);
-
- string targetBranch = submitToFork ? repo.Parent.DefaultBranch : repo.DefaultBranch;
- var newPullRequest = new NewPullRequest(message, $"{repo.Owner.Login}:{newBranchName}", targetBranch) { Body = description };
- var pullRequest = await this.github.PullRequest.Create(this.wingetRepoOwner, this.wingetRepo, newPullRequest);
-
- return pullRequest;
- }
- catch (Exception)
- {
- // On error, cleanup created branch/repo before re-throwing
- if (createdRepo)
- {
- await this.github.Repository.Delete(repo.Id);
- }
- else if (newBranch != null)
- {
- await this.github.Git.Reference.Delete(repo.Id, newBranch.Ref);
- }
-
- throw;
- }
- }
-
- private async Task DeletePullRequestBranch(int pullRequestId)
- {
- // Delete branch if it's not on a forked repo.
- var pullRequest = await this.github.PullRequest.Get(this.wingetRepoOwner, this.wingetRepo, pullRequestId);
- if (pullRequest.Base.Repository.Id == pullRequest.Head.Repository.Id)
- {
- string newBranchNameHeads = $"heads/{pullRequest.Head.Ref}";
- await this.github.Git.Reference.Delete(this.wingetRepoOwner, this.wingetRepo, newBranchNameHeads);
- }
- }
- }
-}
+ }
+
+ private async Task SubmitPRAsync(string packageId, string version, Dictionary contents, bool submitToFork)
+ {
+ bool createdRepo = false;
+
+ Repository repo;
+ if (submitToFork)
+ {
+ try
+ {
+ var user = await this.github.User.Current();
+ repo = await this.github.Repository.Get(user.Login, this.wingetRepo);
+ }
+ catch (NotFoundException)
+ {
+ repo = await this.github.Repository.Forks.Create(this.wingetRepoOwner, this.wingetRepo, new NewRepositoryFork());
+ createdRepo = true;
+ }
+ }
+ else
+ {
+ repo = await this.github.Repository.Get(this.wingetRepoOwner, this.wingetRepo);
+ }
+
+ string newBranchName = $"autogenerated/{packageId}/{Guid.NewGuid()}";
+ string newBranchNameHeads = $"heads/{newBranchName}";
+
+ string message = $"{packageId} version {version}";
+
+ var upstreamMaster = await this.github.Git.Reference.Get(this.wingetRepoOwner, this.wingetRepo, HeadMasterRef);
+ var upstreamMasterSha = upstreamMaster.Object.Sha;
+
+ Reference newBranch = null;
+ try
+ {
+ // Create new branch synced to upstream master
+ await this.github.Git.Reference.Create(repo.Id, new NewReference($"refs/{newBranchNameHeads}", upstreamMasterSha));
+
+ // Update from upstream branch master
+ newBranch = await this.github.Git.Reference.Update(repo.Id, newBranchNameHeads, new ReferenceUpdate(upstreamMasterSha));
+ var updatedSha = newBranch.Object.Sha;
+
+ var nt = new NewTree { BaseTree = updatedSha };
+ string appPath = Utils.GetAppManifestDirPath(packageId, version, '/');
+
+ foreach (KeyValuePair item in contents)
+ {
+ string file = $"{appPath}/{item.Key}.yaml";
+ nt.Tree.Add(new NewTreeItem { Path = file, Mode = "100644", Type = TreeType.Blob, Content = item.Value });
+ }
+
+ var newTree = await this.github.Git.Tree.Create(repo.Id, nt);
+
+ var newCommit = new NewCommit(message, newTree.Sha, updatedSha);
+ var commit = await this.github.Git.Commit.Create(repo.Id, newCommit);
+
+ await this.github.Git.Reference.Update(repo.Id, newBranchNameHeads, new ReferenceUpdate(commit.Sha));
+
+ // Get latest description template from repo
+ string description = await this.GetFileContentsAsync(PRDescriptionRepoPath);
+
+ string targetBranch = submitToFork ? repo.Parent.DefaultBranch : repo.DefaultBranch;
+ var newPullRequest = new NewPullRequest(message, $"{repo.Owner.Login}:{newBranchName}", targetBranch) { Body = description };
+ var pullRequest = await this.github.PullRequest.Create(this.wingetRepoOwner, this.wingetRepo, newPullRequest);
+
+ return pullRequest;
+ }
+ catch (Exception)
+ {
+ // On error, cleanup created branch/repo before re-throwing
+ if (createdRepo)
+ {
+ await this.github.Repository.Delete(repo.Id);
+ }
+ else if (newBranch != null)
+ {
+ await this.github.Git.Reference.Delete(repo.Id, newBranch.Ref);
+ }
+
+ throw;
+ }
+ }
+
+ private async Task DeletePullRequestBranch(int pullRequestId)
+ {
+ // Delete branch if it's not on a forked repo.
+ var pullRequest = await this.github.PullRequest.Get(this.wingetRepoOwner, this.wingetRepo, pullRequestId);
+ if (pullRequest.Base.Repository.Id == pullRequest.Head.Repository.Id)
+ {
+ string newBranchNameHeads = $"heads/{pullRequest.Head.Ref}";
+ await this.github.Git.Reference.Delete(this.wingetRepoOwner, this.wingetRepo, newBranchNameHeads);
+ }
+ }
+ }
+}