diff --git a/README.md b/README.md
index 60f9f88f..80cef0f1 100644
--- a/README.md
+++ b/README.md
@@ -176,16 +176,13 @@ Running unit and E2E tests are a great way to ensure that functionality is prese
* Fill out the test parameters in the `WingetCreateTests/Test.runsettings` file
* `WingetPkgsTestRepoOwner`: The repository owner of the winget-pkgs-submission-test repo. (Repo owner must be forked from main "winget-pkgs-submission-test" repo)
* `WingetPkgsTestRepo`: The winget-pkgs test repository. (winget-pkgs-submission-test)
- * `GitHubApiKey`: GitHub personal access token for testing.
- * Instructions on [how to generate your own GitHubApiKey](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token).
- * Direct link to GitHub [Personal Access Tokens page](https://github.com/settings/tokens).
- * `GitHubAppPrivateKey`: Leave blank, this is only used by the build server.
* Set the solution wide runsettings file for the tests
* Go to `Test` menu > `Configure Run Settings` -> `Select Solution Wide runsettings File` -> Choose your configured runsettings file
-> [!CAUTION]
-> You should treat your access token like a password. To avoid exposing your PAT, be sure to reset changes to the `WingetCreateTests/Test.runsettings` file before committing your changes. You can also use the command `git update-index --skip-worktree src/WingetCreateTests/WingetCreateTests/Test.runsettings` command to untrack changes to the file and prevent it from being committed.
+* Set up your github token:
+ * __[Recommended]__ Run 'wingetcreate token -s` to go through the Github authentication flow
+ * Or create a personal access token with the `repo` permission and set it as an environment variable `WINGET_CREATE_GITHUB_TOKEN`. _(This option is more convenient for CI/CD pipelines.)_
## Contributing
diff --git a/doc/new-locale.md b/doc/new-locale.md
index e6d09e67..a7eceee4 100644
--- a/doc/new-locale.md
+++ b/doc/new-locale.md
@@ -28,7 +28,7 @@ The following arguments are available:
| **-r, --reference-locale** | Existing locale manifest to be used as reference for default values. If not provided, the default locale manifest will be used.
| **-o, --out** | The output directory where the newly created manifests will be saved locally.
| **-f,--format** | Output format of the manifest. Default is "yaml". |
-| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo |
+| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ |
| **-?, --help** | Gets additional help on this command |
Instructions on setting up GitHub Token for Winget-Create can be found [here](../README.md#github-personal-access-token-classic-permissions).
diff --git a/doc/new.md b/doc/new.md
index b3687803..e0471c2a 100644
--- a/doc/new.md
+++ b/doc/new.md
@@ -28,7 +28,7 @@ The following arguments are available:
|--------------|-------------|
| **-o,--out** | The output directory where the newly created manifests will be saved locally |
| **-f,--format** | Output format of the manifest. Default is "yaml". |
-| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo |
+| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ |
| **-?, --help** | Gets additional help on this command |
## Winget-Create New Command flow
diff --git a/doc/show.md b/doc/show.md
index 2265ed76..1586b683 100644
--- a/doc/show.md
+++ b/doc/show.md
@@ -28,7 +28,7 @@ The following arguments are available:
| **-l, --locale-manifests** | Switch to display all locale manifests.
| **--version-manifest** | Switch to display the version manifest.
| **-f,--format** | Output format of the manifest. Default is "yaml". |
-| **-t, --token** | GitHub personal access token used for authenticated access to the GitHub API. It is recommended to provide a token to get a higher [API rate limit](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting).
+| **-t, --token** | GitHub personal access token used for authenticated access to the GitHub API. It is recommended to provide a token to get a higher [API rate limit](https://docs.github.com/en/rest/overview/resources-in-the-rest-api#rate-limiting).
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ |
| **-?, --help** | Gets additional help on this command. |
Instructions on setting up GitHub Token for Winget-Create can be found [here](../README.md#github-personal-access-token-classic-permissions).
diff --git a/doc/submit.md b/doc/submit.md
index 6d70f56d..fea551cb 100644
--- a/doc/submit.md
+++ b/doc/submit.md
@@ -17,7 +17,7 @@ The following arguments are available:
|--------------|-------------|
| **-p, --prtitle** | The title of the pull request submitted to GitHub.
| **-r, --replace** | Boolean value for replacing an existing manifest from the Windows Package Manager repo. Optionally provide a version or else the latest version will be replaced. Default is false.
-| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.
+| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ |
| **-?, --help** | Gets additional help on this command. |
If you have provided your [GitHub token](https://docs.github.com/en/github/authenticating-to-github/creating-a-personal-access-token) on the command line with the **submit** command and the device is registered with GitHub, **Winget-Create** will submit your PR to [Windows Package Manager repo](https://docs.microsoft.com/windows/package-manager/).
diff --git a/doc/token.md b/doc/token.md
index 478ff798..e8a0e38d 100644
--- a/doc/token.md
+++ b/doc/token.md
@@ -7,6 +7,13 @@ Instructions on setting up GitHub Token for Winget-Create can be found [here](..
## Usage
+> [!WARNING]
+> Using the `--token` argument may result in the token being logged.
+>
+> For local development, it is recommended to go through the OAuth flow by omitting the `--token` argument.
+>
+> For CI/CD scenarios, it is recommended to use the 'WINGET_CREATE_GITHUB_TOKEN' environment variable to store the token.
+
`wingetcreate.exe token [\]`
### Store a new GitHub token in your local cache
@@ -25,5 +32,5 @@ The following arguments are available:
|---------------- |-------------|
| **-c, --clear** | Required. Clear the cached GitHub token
| **-s, --store** | Required. Set the cached GitHub token. Can specify token to cache with --token parameter, otherwise will initiate OAuth flow.
-| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.
+| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._
| **-?, --help** | Gets additional help on this command. |
diff --git a/doc/update-locale.md b/doc/update-locale.md
index 3fc33118..c4e0e966 100644
--- a/doc/update-locale.md
+++ b/doc/update-locale.md
@@ -27,7 +27,7 @@ The following arguments are available:
| **-l, --locale** | The package locale to update the manifest for. If not provided, the tool will prompt you a list of existing locales to choose from.
| **-o, --out** | The output directory where the newly created manifests will be saved locally.
| **-f,--format** | Output format of the manifest. Default is "yaml". |
-| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo |
+| **-t,--token** | GitHub personal access token used for direct submission to the Windows Package Manager repo.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ |
| **-?, --help** | Gets additional help on this command |
Instructions on setting up GitHub Token for Winget-Create can be found [here](../README.md#github-personal-access-token-classic-permissions).
diff --git a/doc/update.md b/doc/update.md
index 457a750e..513647d2 100644
--- a/doc/update.md
+++ b/doc/update.md
@@ -119,7 +119,7 @@ The following arguments are available:
| **-r, --replace** | Boolean value for replacing an existing manifest from the Windows Package Manager repo. Optionally provide a version or else the latest version will be replaced. Default is false. |
| **-i, --interactive** | Boolean value for making the update command interactive. If true, the tool will prompt the user for input. Default is false. |
| **-f,--format** | Output format of the manifest. Default is "yaml". |
-| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials. |
+| **-t, --token** | GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.
⚠️ _Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token._ |
| **-?, --help** | Gets additional help on this command. |
## Submit
diff --git a/pipelines/azure-pipelines.yml b/pipelines/azure-pipelines.yml
index 435f195c..e1073bc2 100644
--- a/pipelines/azure-pipelines.yml
+++ b/pipelines/azure-pipelines.yml
@@ -134,7 +134,9 @@ extends:
testSelector: "testAssemblies"
testAssemblyVer2: 'src\WingetCreateTests\WingetCreateTests\bin\$(buildPlatform)\$(buildConfiguration)\$(targetFramework)\WingetCreateTests.dll'
runSettingsFile: 'src\WingetCreateTests\WingetCreateTests\Test.runsettings'
- overrideTestrunParameters: '-WingetPkgsTestRepoOwner microsoft -WingetPkgsTestRepo winget-pkgs-submission-test -GitHubAppPrivateKey "$(GitHubApp_PrivateKey)"'
+ overrideTestrunParameters: '-WingetPkgsTestRepoOwner microsoft -WingetPkgsTestRepo winget-pkgs-submission-test'
+ env:
+ WINGET_CREATE_APP_KEY: $(GitHubApp_PrivateKey)
- task: 1ES.PublishPipelineArtifact@1
inputs:
diff --git a/src/WingetCreateCLI/Commands/BaseCommand.cs b/src/WingetCreateCLI/Commands/BaseCommand.cs
index c7e6ac17..c6c2edb7 100644
--- a/src/WingetCreateCLI/Commands/BaseCommand.cs
+++ b/src/WingetCreateCLI/Commands/BaseCommand.cs
@@ -126,22 +126,19 @@ public async Task LoadGitHubClient(bool requireToken = false)
if (string.IsNullOrEmpty(this.GitHubToken))
{
Logger.Trace("No token parameter, reading cached token");
- this.GitHubToken = GitHubOAuth.ReadTokenCache();
- if (string.IsNullOrEmpty(this.GitHubToken))
+ if (GitHubOAuth.TryReadTokenCache(out var token))
{
- if (requireToken)
- {
- Logger.Trace("No token found in cache, launching OAuth flow");
- if (!await this.GetTokenFromOAuth())
- {
- Logger.Trace("Failed to obtain token from OAuth flow.");
- return false;
- }
- }
+ this.GitHubToken = token;
+ isCacheToken = true;
}
- else
+ else if (requireToken)
{
- isCacheToken = true;
+ Logger.Trace("No token found in cache, launching OAuth flow");
+ if (!await this.GetTokenFromOAuth())
+ {
+ Logger.Trace("Failed to obtain token from OAuth flow.");
+ return false;
+ }
}
}
diff --git a/src/WingetCreateCLI/GitHubOAuth.cs b/src/WingetCreateCLI/GitHubOAuth.cs
index 045807bd..cada66ca 100644
--- a/src/WingetCreateCLI/GitHubOAuth.cs
+++ b/src/WingetCreateCLI/GitHubOAuth.cs
@@ -5,12 +5,9 @@ namespace Microsoft.WingetCreateCLI
{
using System;
using System.ComponentModel;
- using System.IO;
- using System.Security.Cryptography;
- using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
- using System.Threading.Tasks;
+ using System.Threading.Tasks;
using Microsoft.WingetCreateCLI.Logging;
using Microsoft.WingetCreateCLI.Properties;
using Microsoft.WingetCreateCLI.Telemetry;
@@ -25,49 +22,27 @@ public static class GitHubOAuth
{
private const string GitHubDeviceEndpoint = "https://github.com/login/device/code";
private const string GitHubTokenEndpoint = "https://github.com/login/oauth/access_token";
- private const string GrantType = "urn:ietf:params:oauth:grant-type:device_code";
- private static readonly string TokenFile = Path.Combine(Common.LocalAppStatePath, "tokenCache.bin");
-
- ///
- /// Create byte array for additional entropy when using Protect method.
- ///
- private static readonly byte[] EntropyBytes = Encoding.UTF8.GetBytes(TokenFile);
-
- ///
- /// Deletes the cached token.
- ///
- public static void DeleteTokenCache()
- {
- File.Delete(TokenFile);
- }
-
- ///
- /// Reads and decrypts the cached token, if one exists.
- ///
- /// Decrypted cached token.
- public static string ReadTokenCache()
- {
- if (File.Exists(TokenFile))
- {
- var protectedBytes = File.ReadAllBytes(TokenFile);
- var bytes = ProtectedData.Unprotect(protectedBytes, EntropyBytes, DataProtectionScope.CurrentUser);
- return Encoding.UTF8.GetString(bytes);
- }
- else
- {
- return null;
- }
- }
-
- ///
- /// Encrypts and writes the token to the file cache.
- ///
- /// Token to be cached.
- public static void WriteTokenCache(string token)
- {
- var bytes = ProtectedData.Protect(Encoding.UTF8.GetBytes(token), EntropyBytes, DataProtectionScope.CurrentUser);
- File.WriteAllBytes(TokenFile, bytes);
- }
+ private const string GrantType = "urn:ietf:params:oauth:grant-type:device_code";
+
+ ///
+ /// Deletes the token.
+ ///
+ /// True if the token was deleted, false otherwise.
+ public static bool DeleteTokenCache() => TokenHelper.Delete();
+
+ ///
+ /// Writes the token.
+ ///
+ /// Token to be cached.
+ /// True if the token was written, false otherwise.
+ public static bool WriteTokenCache(string token) => TokenHelper.Write(token);
+
+ ///
+ /// Reads the token.
+ ///
+ /// Output token.
+ /// True if the token was read, false otherwise.
+ public static bool TryReadTokenCache(out string token) => TokenHelper.TryRead(out token);
///
/// Sends a POST request to GitHub's authorization server to obtain a device code.
@@ -236,5 +211,5 @@ public class TokenErrorResponse
[JsonPropertyName("error_uri")]
public string ErrorUri { get; set; }
}
- }
-}
+ }
+}
diff --git a/src/WingetCreateCLI/NativeMethods.txt b/src/WingetCreateCLI/NativeMethods.txt
new file mode 100644
index 00000000..3fbe5436
--- /dev/null
+++ b/src/WingetCreateCLI/NativeMethods.txt
@@ -0,0 +1,4 @@
+CredDelete
+CredFree
+CredRead
+CredWrite
diff --git a/src/WingetCreateCLI/Program.cs b/src/WingetCreateCLI/Program.cs
index 85f4d453..9cb7e5d5 100644
--- a/src/WingetCreateCLI/Program.cs
+++ b/src/WingetCreateCLI/Program.cs
@@ -64,6 +64,12 @@ private static async Task Main(string[] args)
return args.Any() ? 1 : 0;
}
+ // If the user has provided a token via the command line, warn them that it may be logged
+ if (!string.IsNullOrEmpty(command.GitHubToken))
+ {
+ Logger.WarnLocalized(nameof(Resources.GitHubTokenWarning_Message));
+ }
+
bool commandHandlesToken = command is not CacheCommand and not InfoCommand and not SettingsCommand;
// Do not load github client for commands that do not deal with a GitHub token.
diff --git a/src/WingetCreateCLI/Properties/Resources.Designer.cs b/src/WingetCreateCLI/Properties/Resources.Designer.cs
index 5e7fb4b1..f4f99c6d 100644
--- a/src/WingetCreateCLI/Properties/Resources.Designer.cs
+++ b/src/WingetCreateCLI/Properties/Resources.Designer.cs
@@ -1150,7 +1150,9 @@ public static string GitHubLoginFail_Error {
}
///
- /// Looks up a localized string similar to GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials..
+ /// Looks up a localized string similar to GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.
+ ///
+ ///Warning: Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token..
///
public static string GitHubToken_HelpText {
get {
@@ -1158,6 +1160,15 @@ public static string GitHubToken_HelpText {
}
}
+ ///
+ /// Looks up a localized string similar to Warning: Using the --token argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token..
+ ///
+ public static string GitHubTokenWarning_Message {
+ get {
+ return ResourceManager.GetString("GitHubTokenWarning_Message", resourceCulture);
+ }
+ }
+
///
/// Looks up a localized string similar to Windows Package Manager Manifest Creator v{0}.
///
diff --git a/src/WingetCreateCLI/Properties/Resources.resx b/src/WingetCreateCLI/Properties/Resources.resx
index c1faa830..42b13aaa 100644
--- a/src/WingetCreateCLI/Properties/Resources.resx
+++ b/src/WingetCreateCLI/Properties/Resources.resx
@@ -237,7 +237,9 @@
GitHub login failed.
- GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.
+ GitHub personal access token used for direct submission to the Windows Package Manager repo. If no token is provided, tool will prompt for GitHub login credentials.
+
+Warning: Using this argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token.
Windows Package Manager Manifest Creator v{0}
@@ -653,6 +655,9 @@
Token was invalid. Please generate a new GitHub token and try again.
+
+ Warning: Using the --token argument may result in the token being logged. Consider an alternative approach https://aka.ms/winget-create-token.
+
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
diff --git a/src/WingetCreateCLI/TokenHelper.cs b/src/WingetCreateCLI/TokenHelper.cs
new file mode 100644
index 00000000..364feb25
--- /dev/null
+++ b/src/WingetCreateCLI/TokenHelper.cs
@@ -0,0 +1,199 @@
+// Copyright (c) Microsoft. All rights reserved.
+// Licensed under the MIT license.
+
+namespace Microsoft.WingetCreateCLI
+{
+ using System;
+ using System.IO;
+ using System.Runtime.InteropServices;
+ using System.Security.Cryptography;
+ using System.Text;
+ using Microsoft.WingetCreateCLI.Logging;
+ using Windows.Win32;
+ using Windows.Win32.Foundation;
+ using Windows.Win32.Security.Credentials;
+
+ ///
+ /// Provides functionality for caching and retrieving the GitHub OAuth token.
+ ///
+ public static class TokenHelper
+ {
+ // Windows credentials manager
+ private const string CredTargetName = "winget-create:GitHub [repo]";
+ private const string CredUserName = "Personal Access Token";
+
+ // Environment variable
+ private const string TokenEnvironmentVariable = "WINGET_CREATE_GITHUB_TOKEN";
+
+ // File (legacy cache)
+ private static readonly string TokenFile = Path.Combine(Common.LocalAppStatePath, "tokenCache.bin");
+ private static readonly byte[] EntropyBytes = Encoding.UTF8.GetBytes(TokenFile);
+
+ ///
+ /// Deletes the token.
+ ///
+ /// True if the token was deleted, false otherwise.
+ public static bool Delete()
+ {
+ return PInvoke.CredDelete(CredTargetName, CRED_TYPE.CRED_TYPE_GENERIC);
+ }
+
+ ///
+ /// Reads the token.
+ ///
+ /// Output token.
+ /// True if the token was read, false otherwise.
+ public static unsafe bool TryRead(out string token) =>
+ TryReadFromEnvironmentVariable(out token) ||
+ TryReadFromCredentialManager(out token) ||
+ TryReadFromFileAndMigrate(out token);
+
+ ///
+ /// Writes the token.
+ ///
+ /// Token to be cached.
+ /// True if the token was written, false otherwise.
+ public static unsafe bool Write(string token)
+ {
+ var tokenBytes = Encoding.Unicode.GetBytes(token);
+ fixed (byte* tokenBytesPtr = tokenBytes)
+ {
+ var credTargetNamePtr = Marshal.StringToHGlobalUni(CredTargetName);
+ var credUserNamePtr = Marshal.StringToHGlobalUni(CredUserName);
+
+ try
+ {
+ var credential = new CREDENTIALW
+ {
+ Type = CRED_TYPE.CRED_TYPE_GENERIC,
+ TargetName = new PWSTR(credTargetNamePtr),
+ UserName = new PWSTR(credUserNamePtr),
+ CredentialBlobSize = (uint)tokenBytes.Length,
+ CredentialBlob = tokenBytesPtr,
+ Persist = CRED_PERSIST.CRED_PERSIST_LOCAL_MACHINE,
+ };
+
+ return PInvoke.CredWrite(&credential, /* None */ 0);
+ }
+ finally
+ {
+ Marshal.FreeHGlobal(credTargetNamePtr);
+ Marshal.FreeHGlobal(credUserNamePtr);
+ }
+ }
+ }
+
+ ///
+ /// Tries to read the token from the Windows credentials manager.
+ ///
+ /// Output token.
+ /// True if the token was read, false otherwise.
+ private static unsafe bool TryReadFromCredentialManager(out string token)
+ {
+ if (PInvoke.CredRead(CredTargetName, CRED_TYPE.CRED_TYPE_GENERIC, out CREDENTIALW* credentialObject) && credentialObject != null)
+ {
+ try
+ {
+ var accessTokenInBytes = new byte[credentialObject->CredentialBlobSize];
+ Marshal.Copy((IntPtr)credentialObject->CredentialBlob, accessTokenInBytes, 0, accessTokenInBytes.Length);
+ token = Encoding.Unicode.GetString(accessTokenInBytes);
+ return true;
+ }
+ finally
+ {
+ PInvoke.CredFree(credentialObject);
+ }
+ }
+
+ token = null;
+ return false;
+ }
+
+ ///
+ /// Tries to read the token from the environment variable.
+ ///
+ /// Output token.
+ /// True if the token was read, false otherwise.
+ private static bool TryReadFromEnvironmentVariable(out string token)
+ {
+ var envToken = Environment.GetEnvironmentVariable(TokenEnvironmentVariable);
+ if (!string.IsNullOrEmpty(envToken))
+ {
+ token = envToken;
+ return true;
+ }
+
+ token = null;
+ return false;
+ }
+
+ ///
+ /// Tries to read the token from the file and migrate it to the Windows credentials manager.
+ ///
+ /// Output token.
+ /// True if the token was read, false otherwise.
+ private static bool TryReadFromFileAndMigrate(out string token)
+ {
+ try
+ {
+ if (TryReadFromFile(out token))
+ {
+ // Migrate to Windows credentials manager
+ Write(token);
+ return true;
+ }
+ }
+ finally
+ {
+ CleanUpLegacyTokenFile();
+ }
+
+ token = null;
+ return false;
+ }
+
+ ///
+ /// Tries to read the token from the file.
+ ///
+ /// Output token.
+ /// True if the token was read, false otherwise.
+ private static bool TryReadFromFile(out string token)
+ {
+ if (File.Exists(TokenFile))
+ {
+ try
+ {
+ var protectedBytes = File.ReadAllBytes(TokenFile);
+ var bytes = ProtectedData.Unprotect(protectedBytes, EntropyBytes, DataProtectionScope.CurrentUser);
+ token = Encoding.UTF8.GetString(bytes);
+ return true;
+ }
+ catch (Exception e)
+ {
+ Logger.Trace($"Failed to read token from file. Message: {e.Message}");
+ }
+ }
+
+ token = null;
+ return false;
+ }
+
+ ///
+ /// Cleans up the legacy token file.
+ ///
+ private static void CleanUpLegacyTokenFile()
+ {
+ if (File.Exists(TokenFile))
+ {
+ try
+ {
+ File.Delete(TokenFile);
+ }
+ catch (Exception e)
+ {
+ Logger.Trace($"Failed to delete token file. Message: {e.Message}");
+ }
+ }
+ }
+ }
+}
diff --git a/src/WingetCreateCLI/WingetCreateCLI.csproj b/src/WingetCreateCLI/WingetCreateCLI.csproj
index ada97f5d..ed20a01c 100644
--- a/src/WingetCreateCLI/WingetCreateCLI.csproj
+++ b/src/WingetCreateCLI/WingetCreateCLI.csproj
@@ -21,6 +21,10 @@
+
+ all
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/WingetCreateTests/WingetCreateTests/Test.runsettings b/src/WingetCreateTests/WingetCreateTests/Test.runsettings
index ae727b6d..ec18a8cf 100644
--- a/src/WingetCreateTests/WingetCreateTests/Test.runsettings
+++ b/src/WingetCreateTests/WingetCreateTests/Test.runsettings
@@ -5,16 +5,10 @@
Parameters used by tests at run time.
WingetPkgsTestRepoOwner: The repository owner of the winget-pkgs submission test repo. (Repo owner must be forked from main "winget-pkgs-submission-test" repo)
WingetPkgsTestRepo: The winget-pkgs test repository.
- GitHubApiKey: GitHub access token for testing.
- GitHubAppPrivateKey: GitHub app installation access token for the winget-create app.
-->
-
-
-
-
diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/GitHubTestsBase.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/GitHubTestsBase.cs
index 7f153ecf..af320f41 100644
--- a/src/WingetCreateTests/WingetCreateTests/UnitTests/GitHubTestsBase.cs
+++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/GitHubTestsBase.cs
@@ -4,11 +4,13 @@
namespace Microsoft.WingetCreateUnitTests
{
using System;
- using System.Threading.Tasks;
- using Microsoft.WingetCreateCLI.Commands;
+ using System.Threading.Tasks;
+ using Microsoft.WingetCreateCLI;
+ using Microsoft.WingetCreateCLI.Commands;
using Microsoft.WingetCreateCore.Common;
- using NUnit.Framework;
-
+ using NUnit.Framework;
+ using NUnit.Framework.Legacy;
+
///
/// Base class for unit tests which need to interact with GitHub.
///
@@ -41,6 +43,10 @@ public class GitHubTestsBase
[OneTimeSetUp]
public async Task SetupBase()
{
+ // Ensure keys are not set in runsettings file
+ ClassicAssert.True(string.IsNullOrEmpty(TestContext.Parameters.Get("GitHubApiKey")), "GitHubApiKey should not be set in runsettings file");
+ ClassicAssert.True(string.IsNullOrEmpty(TestContext.Parameters.Get("GitHubAppPrivateKey")), "GitHubAppPrivateKey should not be set in runsettings file");
+
this.WingetPkgsTestRepoOwner = TestContext.Parameters.Get("WingetPkgsTestRepoOwner") ?? throw new ArgumentNullException("WingetPkgsTestRepoOwner must be set in runsettings file");
this.WingetPkgsTestRepo = TestContext.Parameters.Get("WingetPkgsTestRepo") ?? throw new ArgumentNullException("WingetPkgsTestRepo must be set in runsettings file");
@@ -48,26 +54,46 @@ public async Task SetupBase()
{
throw new ArgumentException($"Invalid configuration specified, you can not run tests against default repo {BaseCommand.DefaultWingetRepoOwner}/{BaseCommand.DefaultWingetRepo}");
}
-
- string gitHubApiKey = TestContext.Parameters.Get("GitHubApiKey");
- string gitHubAppPrivateKey = TestContext.Parameters.Get("GitHubAppPrivateKey");
-
- if (!string.IsNullOrEmpty(gitHubApiKey))
- {
- TestContext.Progress.WriteLine("Using GitHubApiKey value for tests");
- this.GitHubApiKey = gitHubApiKey;
- this.SubmitPRToFork = true;
- }
- else if (!string.IsNullOrEmpty(gitHubAppPrivateKey))
- {
- TestContext.Progress.WriteLine("Using GitHubAppPrivateKey value for tests");
- this.GitHubApiKey = await GitHub.GetGitHubAppInstallationAccessToken(gitHubAppPrivateKey, Constants.GitHubAppId, this.WingetPkgsTestRepoOwner, this.WingetPkgsTestRepo);
- this.SubmitPRToFork = false;
+
+ string gitHubAppPrivateKey = Environment.GetEnvironmentVariable("WINGET_CREATE_APP_KEY");
+ if (string.IsNullOrEmpty(gitHubAppPrivateKey))
+ {
+ await this.ConfigureForLocalTestsAsync();
}
else
- {
- throw new ArgumentNullException("Either GitHubApiKey or GitHubAppPrivateKey must be set in runsettings file");
+ {
+ await this.ConfigureForPipelineTestsAsync(gitHubAppPrivateKey);
}
+ }
+
+ ///
+ /// Configures the tests to run in a pipeline.
+ ///
+ /// Github app private key.
+ private async Task ConfigureForPipelineTestsAsync(string gitHubAppPrivateKey)
+ {
+ TestContext.Progress.WriteLine("Running in pipeline, using GitHubAppPrivateKey value for tests");
+ this.GitHubApiKey = await GitHub.GetGitHubAppInstallationAccessToken(gitHubAppPrivateKey, Constants.GitHubAppId, this.WingetPkgsTestRepoOwner, this.WingetPkgsTestRepo);
+ this.SubmitPRToFork = false;
+ }
+
+ ///
+ /// Configures the tests to run locally.
+ ///
+ private async Task ConfigureForLocalTestsAsync()
+ {
+ TestContext.Progress.WriteLine("Running locally, using Github token for tests");
+ if (TokenHelper.TryRead(out string token))
+ {
+ this.GitHubApiKey = token;
+ this.SubmitPRToFork = true;
+ }
+ else
+ {
+ ClassicAssert.Fail("No GitHub token found.\n" +
+ ">> Please run 'wingetcreate token -s'\n" +
+ ">> Or set the 'WINGET_CREATE_GITHUB_TOKEN' environment variable to a valid GitHub token.");
+ }
}
- }
+ }
}
diff --git a/src/WingetCreateTests/WingetCreateTests/UnitTests/TokenCommandTests.cs b/src/WingetCreateTests/WingetCreateTests/UnitTests/TokenCommandTests.cs
index 5cc8a0a7..9193556b 100644
--- a/src/WingetCreateTests/WingetCreateTests/UnitTests/TokenCommandTests.cs
+++ b/src/WingetCreateTests/WingetCreateTests/UnitTests/TokenCommandTests.cs
@@ -2,8 +2,8 @@
// Licensed under the MIT license.
namespace Microsoft.WingetCreateUnitTests
-{
- using System.IO;
+{
+ using System;
using System.Threading.Tasks;
using Microsoft.WingetCreateCLI;
using Microsoft.WingetCreateCLI.Commands;
@@ -15,29 +15,89 @@ namespace Microsoft.WingetCreateUnitTests
/// Test cases for verifying that the "token" command is working as expected.
///
public class TokenCommandTests : GitHubTestsBase
- {
- ///
- /// Verifies that the Token command works as expected.
- ///
- /// A representing the asynchronous unit test.
- [Test]
- public async Task TokenCommand()
- {
- Logger.Initialize();
-
- // Preemptively clear existing token
- var command = new TokenCommand { Clear = true };
- ClassicAssert.IsTrue(await command.Execute(), "Command should have succeeded");
-
- string tokenCacheFile = Path.Combine(Common.LocalAppStatePath, "tokenCache.bin");
- FileAssert.DoesNotExist(tokenCacheFile, "Token cache file shouldn't exist before running Token --store command");
- command = new TokenCommand { Store = true, GitHubToken = this.GitHubApiKey };
- ClassicAssert.IsTrue(await command.Execute(), "Command should have succeeded");
- FileAssert.Exists(tokenCacheFile, "Token cache file should exist after storing token");
-
- command = new TokenCommand { Clear = true };
- ClassicAssert.IsTrue(await command.Execute(), "Command should have succeeded");
- FileAssert.DoesNotExist(tokenCacheFile, "Token cache file shouldn't exist after running Token --clear command");
- }
+ {
+ private const string TokenEnvironmentVariable = "WINGET_CREATE_GITHUB_TOKEN";
+
+ ///
+ /// OneTimeSetup method for the unit tests.
+ ///
+ [OneTimeSetUp]
+ public void OneTimeSetUp()
+ {
+ Logger.Initialize();
+ }
+
+ ///
+ /// SetUp method for the unit tests.
+ ///
+ [SetUp]
+ public void SetUp()
+ {
+ TokenHelper.Delete();
+ Environment.SetEnvironmentVariable(TokenEnvironmentVariable, null);
+ }
+
+ ///
+ /// TearDown method for the unit tests.
+ ///
+ [TearDown]
+ public void TearDown()
+ {
+ TokenHelper.Delete();
+ Environment.SetEnvironmentVariable(TokenEnvironmentVariable, null);
+ }
+
+ ///
+ /// Test case for verifying that the "token" can be read from the environment variable after clearing the token cache.
+ ///
+ [Test]
+ public async Task TokenClearAndReadFromEnvironmentVariable()
+ {
+ await this.ExecuteTokenClearCommand();
+ ClassicAssert.IsFalse(TokenHelper.TryRead(out var _), "Token cache shouldn't exist");
+
+ Environment.SetEnvironmentVariable(TokenEnvironmentVariable, "MockToken");
+ ClassicAssert.IsTrue(TokenHelper.TryRead(out var _), "Token cache should exist after setting environment variable");
+ }
+
+ ///
+ /// Test case for verifying that the "token --clear" command is working as expected.
+ ///
+ [Test]
+ public async Task TokenClearCommand()
+ {
+ await this.ExecuteTokenStoreCommand();
+ await this.ExecuteTokenClearCommand();
+ ClassicAssert.IsFalse(TokenHelper.TryRead(out var _), "Token cache shouldn't exist after running Token --clear command");
+ }
+
+ ///
+ /// Test case for verifying that the "token --store" command is working as expected.
+ ///
+ [Test]
+ public async Task TokenStoreCommand()
+ {
+ await this.ExecuteTokenClearCommand();
+ await this.ExecuteTokenStoreCommand();
+ ClassicAssert.IsTrue(TokenHelper.TryRead(out var _), "Token cache should exist after running Token --store command");
+ }
+
+ ///
+ /// Executes the token clear command.
+ ///
+ private async Task ExecuteTokenClearCommand()
+ {
+ var command = new TokenCommand { Clear = true };
+ ClassicAssert.IsTrue(await command.Execute(), "Command should have succeeded");
+ }
+
+ ///
+ /// Executes the token store command.
+ ///
+ private async Task ExecuteTokenStoreCommand()
+ {
+ var command = new TokenCommand { Store = true, GitHubToken = this.GitHubApiKey };
+ ClassicAssert.IsTrue(await command.Execute(), "Command should have succeeded");
+ }
}
}