From 4e7f4cdb773c24caf07eccd37d63cfaed501d7c0 Mon Sep 17 00:00:00 2001 From: Ryan Fu <69221034+ryfu-msft@users.noreply.github.com> Date: Thu, 5 Aug 2021 14:40:47 -0700 Subject: [PATCH] Notify users of latest release version (#121) * Show warning message if version is outdated * fix variable name * fix spacing * catch ratelimitexceeded error if no token is provided * make loadgithubclient public * revert githubclient to be protected * fix tests * handle command null error --- src/WingetCreateCLI/Commands/BaseCommand.cs | 98 +++++------ src/WingetCreateCLI/Commands/NewCommand.cs | 13 +- src/WingetCreateCLI/Commands/UpdateCommand.cs | 5 - src/WingetCreateCLI/Program.cs | 160 ++++++++++-------- .../Properties/Resources.Designer.cs | 29 +++- src/WingetCreateCLI/Properties/Resources.resx | 20 ++- src/WingetCreateCore/Common/GitHub.cs | 10 ++ .../WingetCreateTests/E2ETests/E2ETests.cs | 1 + .../UnitTests/SettingsCommandTests.cs | 2 +- 9 files changed, 200 insertions(+), 138 deletions(-) diff --git a/src/WingetCreateCLI/Commands/BaseCommand.cs b/src/WingetCreateCLI/Commands/BaseCommand.cs index 1b3e2049..69de1ba7 100644 --- a/src/WingetCreateCLI/Commands/BaseCommand.cs +++ b/src/WingetCreateCLI/Commands/BaseCommand.cs @@ -76,7 +76,7 @@ public abstract class BaseCommand /// /// Gets the GitHubClient instance to use for interacting with GitHub from the CLI. /// - protected GitHub GitHubClient { get; private set; } + internal GitHub GitHubClient { get; private set; } /// /// Abstract method executing the command. @@ -84,6 +84,54 @@ public abstract class BaseCommand /// Boolean representing success or fail of the command. public abstract Task Execute(); + /// + /// 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 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. + public async Task LoadGitHubClient(bool requireToken = false) + { + bool isCacheToken = false; + + if (string.IsNullOrEmpty(this.GitHubToken)) + { + Logger.Trace("No token parameter, reading cached token"); + this.GitHubToken = GitHubOAuth.ReadTokenCache(); + + if (string.IsNullOrEmpty(this.GitHubToken)) + { + if (requireToken) + { + Logger.Trace("No token found in cache, launching OAuth flow"); + if (!await this.GetTokenFromOAuth()) + { + return false; + } + } + } + else + { + isCacheToken = true; + } + } + + if (await this.CheckGitHubTokenAndSetClient()) + { + return true; + } + else + { + if (isCacheToken) + { + GitHubOAuth.DeleteTokenCache(); + } + + return false; + } + } + /// /// Creates a formatted directory of manifest files from the manifest object models and saves the directory to a local path. /// @@ -312,54 +360,6 @@ protected static void DisplayManifestPreview(Manifests manifests) Console.WriteLine(manifests.DefaultLocaleManifest.ToYaml()); } - /// - /// 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 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) - { - bool isCacheToken = false; - - if (string.IsNullOrEmpty(this.GitHubToken)) - { - Logger.Trace("No token parameter, reading cached token"); - this.GitHubToken = GitHubOAuth.ReadTokenCache(); - - if (string.IsNullOrEmpty(this.GitHubToken)) - { - if (requireToken) - { - Logger.Trace("No token found in cache, launching OAuth flow"); - if (!await this.GetTokenFromOAuth()) - { - return false; - } - } - } - else - { - isCacheToken = true; - } - } - - if (await this.CheckGitHubTokenAndSetClient()) - { - return true; - } - else - { - if (isCacheToken) - { - GitHubOAuth.DeleteTokenCache(); - } - - return false; - } - } - /// /// Launches the GitHub OAuth flow and obtains a GitHub token. /// diff --git a/src/WingetCreateCLI/Commands/NewCommand.cs b/src/WingetCreateCLI/Commands/NewCommand.cs index efd100f8..1767eb2e 100644 --- a/src/WingetCreateCLI/Commands/NewCommand.cs +++ b/src/WingetCreateCLI/Commands/NewCommand.cs @@ -141,17 +141,10 @@ public override async Task Execute() if (this.WingetRepoOwner == DefaultWingetRepoOwner && this.WingetRepo == DefaultWingetRepo) { - if (await this.LoadGitHubClient()) - { - if (!await this.PromptPackageIdentifierAndCheckDuplicates(manifests)) - { - Console.WriteLine(); - Logger.ErrorLocalized(nameof(Resources.PackageIdAlreadyExists_Error)); - return false; - } - } - else + if (!await this.PromptPackageIdentifierAndCheckDuplicates(manifests)) { + Console.WriteLine(); + Logger.ErrorLocalized(nameof(Resources.PackageIdAlreadyExists_Error)); return false; } } diff --git a/src/WingetCreateCLI/Commands/UpdateCommand.cs b/src/WingetCreateCLI/Commands/UpdateCommand.cs index 32dd610c..79f3f53d 100644 --- a/src/WingetCreateCLI/Commands/UpdateCommand.cs +++ b/src/WingetCreateCLI/Commands/UpdateCommand.cs @@ -109,11 +109,6 @@ public override async Task Execute() return false; } - if (!await this.LoadGitHubClient()) - { - return false; - } - Logger.DebugLocalized(nameof(Resources.RetrievingManifest_Message), this.Id); string exactId; diff --git a/src/WingetCreateCLI/Program.cs b/src/WingetCreateCLI/Program.cs index 76f2ae9e..da6347eb 100644 --- a/src/WingetCreateCLI/Program.cs +++ b/src/WingetCreateCLI/Program.cs @@ -1,88 +1,110 @@ -// Copyright (c) Microsoft. All rights reserved. -// Licensed under the MIT license. - -namespace Microsoft.WingetCreateCLI -{ - using System; +// Copyright (c) Microsoft. All rights reserved. +// Licensed under the MIT license. + +namespace Microsoft.WingetCreateCLI +{ + using System; using System.Linq; - using System.Threading.Tasks; - using CommandLine; + using System.Threading.Tasks; + using CommandLine; using CommandLine.Text; - using Microsoft.WingetCreateCLI.Commands; - using Microsoft.WingetCreateCLI.Logging; - using Microsoft.WingetCreateCLI.Properties; - using Microsoft.WingetCreateCLI.Telemetry; - using Microsoft.WingetCreateCLI.Telemetry.Events; + using Microsoft.WingetCreateCLI.Commands; + using Microsoft.WingetCreateCLI.Logging; + using Microsoft.WingetCreateCLI.Properties; + using Microsoft.WingetCreateCLI.Telemetry; + using Microsoft.WingetCreateCLI.Telemetry.Events; using Microsoft.WingetCreateCore.Common; - - /// - /// Main entry class for the CLI. - /// - internal class Program + + /// + /// Main entry class for the CLI. + /// + internal class Program { - private static async Task Main(string[] args) + private static async Task Main(string[] args) { Logger.Initialize(); UserSettings.FirstRunTelemetryConsent(); TelemetryEventListener.EventListener.IsTelemetryEnabled(); - - string arguments = string.Join(' ', Environment.GetCommandLineArgs()); - Logger.Trace($"Command line args: {arguments}"); - - Parser myParser = new Parser(config => config.HelpWriter = null); + + string arguments = string.Join(' ', Environment.GetCommandLineArgs()); + Logger.Trace($"Command line args: {arguments}"); + + Parser myParser = new Parser(config => config.HelpWriter = null); var types = new Type[] { typeof(NewCommand), typeof(UpdateCommand), typeof(SubmitCommand), typeof(SettingsCommand), typeof(TokenCommand), typeof(CacheCommand) }; var parserResult = myParser.ParseArguments(args, types); BaseCommand command = parserResult.MapResult(c => c as BaseCommand, err => null); - if (command == null) + if (command == null) { DisplayHelp(parserResult as NotParsed); - DisplayParsingErrors(parserResult as NotParsed); - return args.Any() ? 1 : 0; - } - - try - { - WingetCreateCore.Serialization.ProducedBy = string.Join(" ", Constants.ProgramName, Utils.GetEntryAssemblyVersion()); - return await command.Execute() ? 0 : 1; - } - catch (Exception ex) - { - TelemetryManager.Log.WriteEvent(new GlobalExceptionEvent - { - ExceptionType = ex.GetType().ToString(), - ErrorMessage = ex.Message, - StackTrace = ex.StackTrace, - }); - - Logger.Error(ex.ToString()); - return 1; - } + DisplayParsingErrors(parserResult as NotParsed); + return args.Any() ? 1 : 0; + } + + if (!await command.LoadGitHubClient()) + { + return 1; + } + + try + { + string latestVersion = await command.GitHubClient.GetLatestRelease(); + string trimmedVersion = latestVersion.TrimStart('v').Split('-').First(); + if (trimmedVersion != Utils.GetEntryAssemblyVersion()) + { + Logger.WarnLocalized(nameof(Resources.OutdatedVersionNotice_Message)); + Logger.WarnLocalized(nameof(Resources.GetLatestVersion_Message), latestVersion, "https://github.com/microsoft/winget-create/releases"); + Logger.WarnLocalized(nameof(Resources.UpgradeUsingWinget_Message)); + Console.WriteLine(); + } + } + catch (Exception ex) when (ex is Octokit.ApiException || ex is Octokit.RateLimitExceededException) + { + // Since this is only notifying the user if an update is available, don't block if the token is invalid or a rate limit error is encountered. + } + + try + { + WingetCreateCore.Serialization.ProducedBy = string.Join(" ", Constants.ProgramName, Utils.GetEntryAssemblyVersion()); + return await command.Execute() ? 0 : 1; + } + catch (Exception ex) + { + TelemetryManager.Log.WriteEvent(new GlobalExceptionEvent + { + ExceptionType = ex.GetType().ToString(), + ErrorMessage = ex.Message, + StackTrace = ex.StackTrace, + }); + + Logger.Error(ex.ToString()); + return 1; + } } - - private static void DisplayHelp(NotParsed result) - { - var helpText = HelpText.AutoBuild( - result, - h => - { - h.AddDashesToOption = true; - h.AdditionalNewLineAfterOption = false; - h.Heading = string.Format(Resources.Heading, Utils.GetEntryAssemblyVersion()) + Environment.NewLine; - h.Copyright = Constants.MicrosoftCopyright; - h.AddNewLineBetweenHelpSections = true; - h.AddPreOptionsLine(Resources.AppDescription_HelpText); - h.AddPostOptionsLines(new string[] { Resources.MoreHelp_HelpText, Resources.PrivacyStatement_HelpText }); - h.MaximumDisplayWidth = 100; - h.AutoHelp = false; - h.AutoVersion = false; - return h; - }, - e => e, - verbsIndex: true); - Console.WriteLine(helpText); + + private static void DisplayHelp(NotParsed result) + { + var helpText = HelpText.AutoBuild( + result, + h => + { + h.AddDashesToOption = true; + h.AdditionalNewLineAfterOption = false; + h.Heading = string.Format(Resources.Heading, Utils.GetEntryAssemblyVersion()) + Environment.NewLine; + h.Copyright = Constants.MicrosoftCopyright; + h.AddNewLineBetweenHelpSections = true; + h.AddPreOptionsLine(Resources.AppDescription_HelpText); + h.AddPostOptionsLines(new string[] { Resources.MoreHelp_HelpText, Resources.PrivacyStatement_HelpText }); + h.MaximumDisplayWidth = 100; + h.AutoHelp = false; + h.AutoVersion = false; + return h; + }, + e => e, + verbsIndex: true); + Console.WriteLine(helpText); Console.WriteLine(); } @@ -97,4 +119,4 @@ private static void DisplayParsingErrors(ParserResult result) } } } -} +} diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs index e161354d..d3344702 100644 --- a/src/WingetCreateCLI/Properties/Resources.Designer.cs +++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs @@ -492,6 +492,15 @@ public static string GenerateNewSettingsFile_Message { } } + /// + /// Looks up a localized string similar to For the latest version ({0}), go to {1}. + /// + public static string GetLatestVersion_Message { + get { + return ResourceManager.GetString("GetLatestVersion_Message", resourceCulture); + } + } + /// /// Looks up a localized string similar to A GitHub account or personal access token must be linked in order to continue with this command. . /// @@ -1041,6 +1050,15 @@ public static string Open_HelpText { } } + /// + /// Looks up a localized string similar to This is an older version of Winget-Create and may be missing some critical features.. + /// + public static string OutdatedVersionNotice_Message { + get { + return ResourceManager.GetString("OutdatedVersionNotice_Message", resourceCulture); + } + } + /// /// Looks up a localized string similar to Output directory where the newly created manifests will be saved locally. /// @@ -1240,7 +1258,7 @@ 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.. + /// 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 { @@ -1545,6 +1563,15 @@ public static string UpgradeBehavior_KeywordDescription { } } + /// + /// Looks up a localized string similar to If you have the winget client installed, you can update by running the command: 'winget upgrade Microsoft.WingetCreate'. + /// + public static string UpgradeUsingWinget_Message { + get { + return ResourceManager.GetString("UpgradeUsingWinget_Message", resourceCulture); + } + } + /// /// Looks up a localized string similar to The installer URL. /// diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx index 6ff1e408..48314439 100644 --- a/src/WingetCreateCLI/Properties/Resources.resx +++ b/src/WingetCreateCLI/Properties/Resources.resx @@ -656,9 +656,9 @@ 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 + 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... @@ -666,4 +666,18 @@ Token stored in cache successfully. + + For the latest version ({0}), go to {1} + {0} - Will be replaced with the latest release version +{1} - Will be replaced with the URL to the latest version of the tool. + + + This is an older version of Winget-Create and may be missing some critical features. + + + If you have the winget client installed, you can update by running the command: 'winget upgrade Microsoft.WingetCreate' + 'winget upgrade Microsoft.WingetCreate' refers to a specific command to be executed in order to upgrade the tool to the latest version. + +"winget" is the proper name of the tool + \ No newline at end of file diff --git a/src/WingetCreateCore/Common/GitHub.cs b/src/WingetCreateCore/Common/GitHub.cs index 7e547a7f..45324eb9 100644 --- a/src/WingetCreateCore/Common/GitHub.cs +++ b/src/WingetCreateCore/Common/GitHub.cs @@ -156,6 +156,16 @@ public Task SubmitPullRequestAsync(Manifests manifests, bool submit return this.SubmitPRAsync(id, version, contents, submitToFork); } + /// + /// Gets the latest release tag name of winget-create. + /// + /// Latest release tag name. + public async Task GetLatestRelease() + { + var latestRelease = await this.github.Repository.Release.GetLatest("microsoft", "winget-create"); + return latestRelease.TagName; + } + /// /// Closes an open pull request and deletes its branch if not on forked repo. /// diff --git a/src/WingetCreateTests/WingetCreateTests/E2ETests/E2ETests.cs b/src/WingetCreateTests/WingetCreateTests/E2ETests/E2ETests.cs index ac77553b..96c056d1 100644 --- a/src/WingetCreateTests/WingetCreateTests/E2ETests/E2ETests.cs +++ b/src/WingetCreateTests/WingetCreateTests/E2ETests/E2ETests.cs @@ -79,6 +79,7 @@ private async Task RunSubmitAndUpdateFlow(string packageId, string manifestPath, OpenPRInBrowser = false, }; + Assert.IsTrue(await updateCommand.LoadGitHubClient(), "Failed to create GitHub client"); Assert.IsTrue(await updateCommand.Execute(), "Command should execute successfully"); string pathToValidate = Path.Combine(Directory.GetCurrentDirectory(), Utils.GetAppManifestDirPath(packageId, PackageVersion)); diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/SettingsCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/SettingsCommandTests.cs index 25a11c72..2bd0b551 100644 --- a/src/WingetCreateTests/WingetCreateTests/UnitTests/SettingsCommandTests.cs +++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/SettingsCommandTests.cs @@ -121,7 +121,7 @@ public async Task VerifyInvalidGitHubRepoSettingShowsError() StringWriter sw = new StringWriter(); System.Console.SetOut(sw); UpdateCommand command = new UpdateCommand { Id = "testId" }; - await command.Execute(); + await command.LoadGitHubClient(); string result = sw.ToString(); Assert.That(result, Does.Contain(string.Format(Resources.RepositoryNotFound_Error, testRepoOwner, testRepoName)), "Repository not found error should be shown"); }