mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-09-30 04:27:07 -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.
|
* every run in between.
|
||||||
*/
|
*/
|
||||||
async function resolveLatestTag(fetchFn = fetch) {
|
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`, {
|
const response = await fetchFn(`https://api.github.com/repos/${CLI_REPO}/releases/latest`, {
|
||||||
headers: { Accept: 'application/vnd.github+json' },
|
headers,
|
||||||
});
|
});
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`Failed to resolve the latest game-ci CLI release: GitHub API returned ${response.status}.`);
|
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 () => {
|
it('throws with a clear message on a non-ok response', async () => {
|
||||||
const fetchFn = vi.fn(async () => ({
|
const fetchFn = vi.fn(async () => ({
|
||||||
ok: false,
|
ok: false,
|
||||||
|
|||||||
+10
-1
@@ -40,8 +40,17 @@ export function binaryNameFor(platform: NodeJS.Platform): string {
|
|||||||
* every run in between.
|
* every run in between.
|
||||||
*/
|
*/
|
||||||
export async function resolveLatestTag(fetchFn: typeof fetch = fetch): Promise<string> {
|
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`, {
|
const response = await fetchFn(`https://api.github.com/repos/${CLI_REPO}/releases/latest`, {
|
||||||
headers: { Accept: 'application/vnd.github+json' },
|
headers,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
|
|||||||
Reference in New Issue
Block a user