mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-09-29 12:07:05 -07:00
fix: authenticate resolveLatestTag's GitHub API call to avoid rate limiting
Confirmed hitting this for real on #844: "Failed to resolve the latest
game-ci CLI release: GitHub API returned 403" on both the MacOS and
Ubuntu re-triggered runs. Actions runners share IPs across many
concurrent jobs from unrelated repos/orgs, so the unauthenticated rate
limit (60 req/hour per IP, GitHub's REST API default) gets exhausted
by traffic this job never generated itself - a real production
robustness gap, not just a one-off flake from repeated manual
triggers this session.
Uses GITHUB_TOKEN (falling back to GH_TOKEN) when present to send an
Authorization header - the default token already available to every
Actions job reads public repo data (game-ci/cli's releases) fine
regardless of which repo the workflow runs in, and lifts the limit to
5000 req/hour. No token still works exactly as before (no header).
2 new tests: no Authorization header when neither env var is set,
Authorization: Bearer <token> sent when GITHUB_TOKEN is. 13/13 pass in
download-cli.test.ts, 40/40 across the full suite.
Rebuilds dist/index.js - action.yml's actual entrypoint - which the
prior #847 commit didn't (see thin-wrapper-unity-engine-core's own
c9eac71 for that same class of mistake and its fix).
This commit is contained in:
+10
-1
@@ -316,8 +316,17 @@ exports.binaryNameFor = binaryNameFor;
|
||||
* every run in between.
|
||||
*/
|
||||
async function resolveLatestTag(fetchFn = fetch) {
|
||||
const headers = { Accept: 'application/vnd.github+json' };
|
||||
// Actions runners share IPs across many concurrent jobs from unrelated
|
||||
// repos/orgs, so the unauthenticated rate limit (60 req/hour per IP) gets
|
||||
// exhausted by traffic this job never generated. The default GITHUB_TOKEN
|
||||
// reads public repo data (game-ci/cli's releases) fine regardless of which
|
||||
// repo the workflow runs in, and lifts the limit to 5000 req/hour.
|
||||
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
||||
if (token)
|
||||
headers.Authorization = `Bearer ${token}`;
|
||||
const response = await fetchFn(`https://api.github.com/repos/${CLI_REPO}/releases/latest`, {
|
||||
headers: { Accept: 'application/vnd.github+json' },
|
||||
headers,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to resolve the latest game-ci CLI release: GitHub API returned ${response.status}.`);
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
@@ -63,6 +63,55 @@ describe('resolveLatestTag', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('sends no Authorization header when GITHUB_TOKEN/GH_TOKEN are unset', async () => {
|
||||
const originalGithub = process.env.GITHUB_TOKEN;
|
||||
const originalGh = process.env.GH_TOKEN;
|
||||
delete process.env.GITHUB_TOKEN;
|
||||
delete process.env.GH_TOKEN;
|
||||
|
||||
const fetchFn = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ tag_name: 'v0.1.15' }),
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
try {
|
||||
await resolveLatestTag(fetchFn);
|
||||
const [, init] = vi.mocked(fetchFn).mock.calls[0] as [
|
||||
string,
|
||||
RequestInit & { headers: Record<string, string> },
|
||||
];
|
||||
expect(init.headers.Authorization).toBeUndefined();
|
||||
} finally {
|
||||
if (originalGithub !== undefined) process.env.GITHUB_TOKEN = originalGithub;
|
||||
if (originalGh !== undefined) process.env.GH_TOKEN = originalGh;
|
||||
}
|
||||
});
|
||||
|
||||
it('sends an Authorization header from GITHUB_TOKEN when set, to avoid the unauthenticated rate limit', async () => {
|
||||
const original = process.env.GITHUB_TOKEN;
|
||||
process.env.GITHUB_TOKEN = 'test-token-123';
|
||||
|
||||
const fetchFn = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ tag_name: 'v0.1.15' }),
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
try {
|
||||
await resolveLatestTag(fetchFn);
|
||||
expect(fetchFn).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer test-token-123' }),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
if (original === undefined) delete process.env.GITHUB_TOKEN;
|
||||
else process.env.GITHUB_TOKEN = original;
|
||||
}
|
||||
});
|
||||
|
||||
it('throws with a clear message on a non-ok response', async () => {
|
||||
const fetchFn = vi.fn(async () => ({
|
||||
ok: false,
|
||||
|
||||
+10
-1
@@ -40,8 +40,17 @@ export function binaryNameFor(platform: NodeJS.Platform): string {
|
||||
* every run in between.
|
||||
*/
|
||||
export async function resolveLatestTag(fetchFn: typeof fetch = fetch): Promise<string> {
|
||||
const headers: Record<string, string> = { Accept: 'application/vnd.github+json' };
|
||||
// Actions runners share IPs across many concurrent jobs from unrelated
|
||||
// repos/orgs, so the unauthenticated rate limit (60 req/hour per IP) gets
|
||||
// exhausted by traffic this job never generated. The default GITHUB_TOKEN
|
||||
// reads public repo data (game-ci/cli's releases) fine regardless of which
|
||||
// repo the workflow runs in, and lifts the limit to 5000 req/hour.
|
||||
const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN;
|
||||
if (token) headers.Authorization = `Bearer ${token}`;
|
||||
|
||||
const response = await fetchFn(`https://api.github.com/repos/${CLI_REPO}/releases/latest`, {
|
||||
headers: { Accept: 'application/vnd.github+json' },
|
||||
headers,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
Reference in New Issue
Block a user