Skip to content

Commit a8aa277

Browse files
committed
feat: authenticate GitHub API requests using GH_TOKEN/GITHUB_TOKEN in PowerShell module (#5949)
Add automatic detection of GH_TOKEN and GITHUB_TOKEN environment variables in the PowerShell module GitHubClient to authenticate GitHub API requests, increasing rate limits from 60 to 5,000/hour. Also add documentation to Repair-WinGetPackageManager and Assert-WinGetPackageManager help docs describing the environment variable behavior.
1 parent c8fe1ea commit a8aa277

10 files changed

Lines changed: 310 additions & 11 deletions

File tree

.github/actions/spelling/expect.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,7 @@ nsis
388388
NTFS
389389
objbase
390390
objidl
391+
octokitnet
391392
ofile
392393
oid
393394
omus
@@ -646,6 +647,7 @@ winreg
646647
winrtact
647648
winstring
648649
WMI
650+
wmmc
649651
wnd
650652
WNDCLASS
651653
WNDCLASSEX

doc/ReleaseNotes.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,10 @@ match criteria that factor into the result ordering. This will prevent them from
3232

3333
Added a new `--no-progress` command-line flag that disables all progress reporting (progress bars and spinners). This flag is universally available on all commands and takes precedence over the `visual.progressBar` setting. Useful for automation scenarios or when running WinGet in environments where progress output is undesirable.
3434

35+
### Authenticated GitHub API requests in PowerShell module
36+
37+
The PowerShell module now automatically uses `GH_TOKEN` or `GITHUB_TOKEN` environment variables to authenticate GitHub API requests. This increases the rate limit from 60 to 5,000 requests per hour, preventing failures in CI/CD pipelines (e.g., GitHub Actions). Use `-Verbose` to see which token is being used.
38+
3539
## Bug Fixes
3640

3741
<!-- Nothing yet! -->
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
---
2+
author: Melvin Wang @wmmc88
3+
created on: 2026-02-09
4+
last updated: 2026-02-26
5+
issue id: 5949
6+
---
7+
8+
# Authenticated GitHub API Requests for PowerShell Module
9+
10+
For [#5949](https://github.com/microsoft/winget-cli/issues/5949).
11+
12+
## Abstract
13+
14+
This spec describes adding support for authenticated GitHub API requests in the WinGet PowerShell module. The module's `GitHubClient` helper will automatically detect `GH_TOKEN` or `GITHUB_TOKEN` environment variables and use them to authenticate Octokit API calls, significantly increasing the GitHub API rate limit.
15+
16+
## Inspiration
17+
18+
Users running `Repair-WinGetPackageManager` in GitHub Actions pipelines hit unauthenticated rate limits (60 requests/hour). Authenticated requests allow 5,000 requests/hour. The GitHub CLI (`gh`) already uses `GH_TOKEN` and `GITHUB_TOKEN` for the same purpose, and GitHub Actions automatically provides `GITHUB_TOKEN`.
19+
20+
See: https://github.com/microsoft/windows-drivers-rs/actions/runs/20531244312/job/58982795057#step:3:43
21+
22+
## Solution Design
23+
24+
The `GitHubClient` class in `Microsoft.WinGet.Client.Engine` is updated to:
25+
26+
1. Read all known token environment variables (`GH_TOKEN`, `GITHUB_TOKEN`) on construction.
27+
2. Log the presence or absence of each token via `StreamType.Verbose`.
28+
3. Select the token to use based on precedence (`GH_TOKEN` > `GITHUB_TOKEN`), matching GitHub CLI behavior.
29+
4. Log which token source is being used, or that no token was found.
30+
5. Set `Octokit.GitHubClient.Credentials` if a token is available.
31+
32+
Token resolution is extracted into a static `ResolveGitHubToken` method for testability.
33+
34+
### Token Precedence
35+
36+
`GH_TOKEN` takes precedence over `GITHUB_TOKEN`, matching the [GitHub CLI convention](https://cli.github.com/manual/gh_help_environment). This is because `GH_TOKEN` is explicitly set by users, while `GITHUB_TOKEN` is automatically provided by GitHub Actions and may have more restricted permissions.
37+
38+
### Logging
39+
40+
All logging uses `StreamType.Verbose` via the existing `PowerShellCmdlet.Write` pattern, visible when users pass `-Verbose` to cmdlets. Example output:
41+
42+
```
43+
VERBOSE: GH_TOKEN environment variable: not found
44+
VERBOSE: GITHUB_TOKEN environment variable: found
45+
VERBOSE: Using authenticated GitHub API requests via GITHUB_TOKEN environment variable.
46+
```
47+
48+
### Files Changed
49+
50+
- `src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs` — Token resolution logic and logging.
51+
- `src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/AssemblyInfo.cs``InternalsVisibleTo` for unit tests.
52+
- `src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs` — Pass `PowerShellCmdlet` to `GitHubClient` constructor.
53+
- `src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs` — Pass `PowerShellCmdlet` to `GitHubClient` constructor.
54+
- `src/PowerShell/Microsoft.WinGet.UnitTests/GitHubClientTests.cs` — Unit tests for token resolution.
55+
56+
## UI/UX Design
57+
58+
No new command-line arguments or user-facing changes. The feature is automatic: if `GH_TOKEN` or `GITHUB_TOKEN` is set in the environment, authenticated requests are used. Users can see which token is being used by running any repair/assert cmdlet with `-Verbose`.
59+
60+
### Accessibility
61+
62+
No impact on accessibility.
63+
64+
### Security
65+
66+
- Tokens are never logged or written to output; only the environment variable name is logged.
67+
- Token values are read from environment variables which are a standard secure mechanism for passing secrets in CI/CD environments.
68+
- Whitespace-only token values are treated as unset to prevent accidental empty-credential authentication.
69+
70+
### Reliability
71+
72+
Improves reliability by reducing GitHub API rate limit failures in CI/CD pipelines. Unauthenticated requests are still used as a fallback when no tokens are set.
73+
74+
### Compatibility
75+
76+
Fully backward compatible. When no token environment variables are set, behavior is identical to the previous implementation.
77+
78+
### Performance, Power, and Efficiency
79+
80+
No measurable impact. A single environment variable read per `GitHubClient` construction.
81+
82+
## Potential Issues
83+
84+
- If a user has an expired or revoked token in `GH_TOKEN`/`GITHUB_TOKEN`, API calls will fail with a 401 rather than falling back to unauthenticated. This matches GitHub CLI behavior and is the expected outcome.
85+
86+
## Future considerations
87+
88+
- Support for additional token sources (e.g., `gh auth token` integration, Windows Credential Manager).
89+
- Applying authenticated requests to other parts of the WinGet client beyond the PowerShell module.
90+
91+
## Resources
92+
93+
- [GitHub API rate limits](https://docs.github.com/en/rest/using-the-rest-api/rate-limits-for-the-rest-api)
94+
- [GitHub CLI environment variables](https://cli.github.com/manual/gh_help_environment)
95+
- [Octokit.NET authenticated access](https://octokitnet.readthedocs.io/en/latest/getting-started/#authenticated-access)
96+
- [Issue #5949](https://github.com/microsoft/winget-cli/issues/5949)

src/PowerShell/Help/Microsoft.WinGet.Client/Assert-WinGetPackageManager.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,19 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
142142
143143
## NOTES
144144
145+
### Authenticated GitHub API requests
146+
147+
When using the `-Latest` or `-Version` parameters, this cmdlet makes GitHub API requests to query
148+
release information. Unauthenticated requests are limited to 60 per hour, which can cause failures
149+
in CI/CD pipelines. If the `GH_TOKEN` or `GITHUB_TOKEN` environment variable is set, the cmdlet
150+
automatically uses it to authenticate requests, increasing the rate limit to 5,000 per hour.
151+
152+
`GH_TOKEN` takes precedence over `GITHUB_TOKEN`, matching
153+
[GitHub CLI behavior](https://cli.github.com/manual/gh_help_environment). In GitHub Actions,
154+
`GITHUB_TOKEN` is automatically available.
155+
156+
Use `-Verbose` to see which token source is being used.
157+
145158
## RELATED LINKS
146159

147160
[Get-WinGetVersion](Get-WinGetVersion.md)

src/PowerShell/Help/Microsoft.WinGet.Client/Repair-WinGetPackageManager.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,19 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable
166166
167167
## NOTES
168168
169+
### Authenticated GitHub API requests
170+
171+
This cmdlet makes GitHub API requests to query release information. Unauthenticated requests are
172+
limited to 60 per hour, which can cause failures in CI/CD pipelines. If the `GH_TOKEN` or
173+
`GITHUB_TOKEN` environment variable is set, the cmdlet automatically uses it to authenticate
174+
requests, increasing the rate limit to 5,000 per hour.
175+
176+
`GH_TOKEN` takes precedence over `GITHUB_TOKEN`, matching
177+
[GitHub CLI behavior](https://cli.github.com/manual/gh_help_environment). In GitHub Actions,
178+
`GITHUB_TOKEN` is automatically available.
179+
180+
Use `-Verbose` to see which token source is being used.
181+
169182
## RELATED LINKS
170183

171184
[Get-WinGetVersion](Get-WinGetVersion.md)

src/PowerShell/Microsoft.WinGet.Client.Engine/Commands/WinGetPackageManagerCommand.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public void AssertUsingLatest(bool preRelease)
4343
var runningTask = this.RunOnMTA(
4444
async () =>
4545
{
46-
var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
46+
var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli, this);
4747
string expectedVersion = await gitHubClient.GetLatestReleaseTagNameAsync(preRelease);
4848
this.Assert(expectedVersion);
4949
return true;
@@ -73,7 +73,7 @@ public void RepairUsingLatest(bool preRelease, bool allUsers, bool force)
7373
var runningTask = this.RunOnMTA(
7474
async () =>
7575
{
76-
var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
76+
var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli, this);
7777
string expectedVersion = await gitHubClient.GetLatestReleaseTagNameAsync(preRelease);
7878
await this.RepairStateMachineAsync(expectedVersion, allUsers, force);
7979
return true;
@@ -98,7 +98,7 @@ public void Repair(string expectedVersion, bool allUsers, bool force, bool inclu
9898
if (!string.IsNullOrWhiteSpace(expectedVersion))
9999
{
100100
this.Write(StreamType.Verbose, $"Attempting to resolve version '{expectedVersion}'");
101-
var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
101+
var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli, this);
102102
try
103103
{
104104
var resolvedVersion = await gitHubClient.ResolveVersionAsync(expectedVersion, includePrerelease);
@@ -224,7 +224,7 @@ private async Task InstallAsync(string toInstallVersion, bool allUsers, bool for
224224
// this particular case we need to.
225225
if (string.IsNullOrEmpty(toInstallVersion))
226226
{
227-
var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
227+
var gitHubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli, this);
228228
toInstallVersion = await gitHubClient.GetLatestReleaseTagNameAsync(false);
229229
}
230230

src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/AppxModuleHelper.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -266,7 +266,7 @@ private static Tuple<string, string> GetXamlDependencyVersionInfo(string release
266266

267267
private async Task AddProvisionPackageAsync(string releaseTag)
268268
{
269-
var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
269+
var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli, this.pwshCmdlet);
270270
var release = await githubClient.GetReleaseAsync(releaseTag);
271271

272272
var bundleAsset = release.GetAsset(MsixBundleName);
@@ -325,7 +325,7 @@ private async Task AddAppInstallerBundleAsync(string releaseTag, bool downgrade,
325325

326326
try
327327
{
328-
var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
328+
var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli, this.pwshCmdlet);
329329
var release = await githubClient.GetReleaseAsync(releaseTag);
330330

331331
var bundleAsset = release.GetAsset(MsixBundleName);
@@ -560,7 +560,7 @@ private async Task InstallVCLibsDependenciesFromUriAsync()
560560
// Returns a boolean value indicating whether dependencies were successfully installed from the GitHub release assets.
561561
private async Task<bool> InstallDependenciesFromGitHubArchive(string releaseTag)
562562
{
563-
var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli);
563+
var githubClient = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.WinGetCli, this.pwshCmdlet);
564564
var release = await githubClient.GetReleaseAsync(releaseTag);
565565

566566
ReleaseAsset? dependenciesJsonAsset = release.TryGetAsset(DependenciesJsonName);
@@ -667,7 +667,7 @@ private async Task InstallUiXamlAsync(string releaseTag)
667667
var uiXamlObjs = this.GetAppxObject(xamlPackageName);
668668
if (uiXamlObjs is null)
669669
{
670-
var githubRelease = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.UiXaml);
670+
var githubRelease = new GitHubClient(RepositoryOwner.Microsoft, RepositoryName.UiXaml, this.pwshCmdlet);
671671

672672
var xamlRelease = await githubRelease.GetReleaseAsync(xamlReleaseTag);
673673

src/PowerShell/Microsoft.WinGet.Client.Engine/Helpers/GitHubClient.cs

Lines changed: 47 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,64 @@ internal class GitHubClient
2222
private readonly string owner;
2323
private readonly string repo;
2424
private readonly IGitHubClient gitHubClient;
25+
private readonly PowerShellCmdlet? pwshCmdlet;
2526

2627
/// <summary>
2728
/// Initializes a new instance of the <see cref="GitHubClient"/> class.
2829
/// </summary>
2930
/// <param name="owner">Owner.</param>
3031
/// <param name="repo">Repository.</param>
31-
public GitHubClient(string owner, string repo)
32+
/// <param name="pwshCmdlet">Optional PowerShell cmdlet for logging.</param>
33+
public GitHubClient(string owner, string repo, PowerShellCmdlet? pwshCmdlet = null)
3234
{
33-
this.gitHubClient = new Octokit.GitHubClient(new ProductHeaderValue(HttpClientHelper.UserAgent));
35+
this.pwshCmdlet = pwshCmdlet;
36+
var octokitClient = new Octokit.GitHubClient(new ProductHeaderValue(HttpClientHelper.UserAgent));
37+
38+
string? token = ResolveGitHubToken(pwshCmdlet);
39+
if (!string.IsNullOrWhiteSpace(token))
40+
{
41+
octokitClient.Credentials = new Credentials(token);
42+
}
43+
44+
this.gitHubClient = octokitClient;
3445
this.owner = owner;
3546
this.repo = repo;
3647
}
3748

49+
/// <summary>
50+
/// Reads all known GitHub token environment variables, logs their presence,
51+
/// and selects the one to use based on precedence.
52+
/// GH_TOKEN takes precedence over GITHUB_TOKEN, matching GitHub CLI behavior.
53+
/// See: https://cli.github.com/manual/gh_help_environment.
54+
/// </summary>
55+
/// <param name="pwshCmdlet">Optional PowerShell cmdlet for logging.</param>
56+
/// <returns>The selected token value, or null if none found.</returns>
57+
internal static string? ResolveGitHubToken(PowerShellCmdlet? pwshCmdlet = null)
58+
{
59+
string? ghToken = Environment.GetEnvironmentVariable("GH_TOKEN");
60+
string? githubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
61+
62+
bool hasGhToken = !string.IsNullOrWhiteSpace(ghToken);
63+
bool hasGithubToken = !string.IsNullOrWhiteSpace(githubToken);
64+
65+
pwshCmdlet?.Write(StreamType.Verbose, $"GH_TOKEN environment variable: {(hasGhToken ? "found" : "not found")}");
66+
pwshCmdlet?.Write(StreamType.Verbose, $"GITHUB_TOKEN environment variable: {(hasGithubToken ? "found" : "not found")}");
67+
68+
if (hasGhToken)
69+
{
70+
pwshCmdlet?.Write(StreamType.Verbose, "Using authenticated GitHub API requests via GH_TOKEN environment variable.");
71+
return ghToken;
72+
}
73+
else if (hasGithubToken)
74+
{
75+
pwshCmdlet?.Write(StreamType.Verbose, "Using authenticated GitHub API requests via GITHUB_TOKEN environment variable.");
76+
return githubToken;
77+
}
78+
79+
pwshCmdlet?.Write(StreamType.Verbose, "No GitHub token found. Using unauthenticated GitHub API requests.");
80+
return null;
81+
}
82+
3883
/// <summary>
3984
/// Gets a release.
4085
/// </summary>

src/PowerShell/Microsoft.WinGet.Client.Engine/Properties/AssemblyInfo.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,14 @@
44
// </copyright>
55
// -----------------------------------------------------------------------------
66

7+
using System.Runtime.CompilerServices;
78
#if NET
8-
99
using System.Runtime.Versioning;
10+
#endif
11+
12+
[assembly: InternalsVisibleTo("Microsoft.WinGet.UnitTests")]
13+
14+
#if NET
1015

1116
// Forcibly set the target and supported platforms due to the internal build setup.
1217
// Keep in sync with project versions.

0 commit comments

Comments
 (0)