mirror of
https://github.com/game-ci/unity-builder.git
synced 2026-09-29 12:07:05 -07:00
Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ae0171202c |
@@ -19,6 +19,13 @@ inputs:
|
||||
description:
|
||||
'Release tag of the game-ci/cli binary to install and run (e.g. "v0.1.0"). Defaults to the latest release; pin
|
||||
this for reproducible builds.'
|
||||
githubToken:
|
||||
required: false
|
||||
default: '${{ github.token }}'
|
||||
description:
|
||||
'Token used to authenticate the GitHub API call that resolves cliVersion: latest to a concrete release tag.
|
||||
Defaults to the token GitHub Actions provides for every run automatically - no need to set this unless you
|
||||
have a reason to override it.'
|
||||
customImage:
|
||||
required: false
|
||||
default: ''
|
||||
|
||||
+26
-8
@@ -305,14 +305,28 @@ exports.binaryNameFor = binaryNameFor;
|
||||
* know how to resolve "latest" itself, by design, since which tag it's
|
||||
* fetched at IS the version it installs.
|
||||
*/
|
||||
async function resolveLatestTag(fetchFn = fetch) {
|
||||
async function resolveLatestTag(fetchFn = fetch, githubToken) {
|
||||
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;
|
||||
// exhausted by traffic this job never generated - hit live via
|
||||
// game-ci/unity-test-runner#328's consumer, whose six-version matrix failed
|
||||
// simultaneously with "GitHub API returned 403".
|
||||
//
|
||||
// process.env.GITHUB_TOKEN/GH_TOKEN alone does not fix this: GitHub Actions
|
||||
// does not inject GITHUB_TOKEN into a JS action's process environment
|
||||
// automatically - a calling workflow has to set it explicitly via env:,
|
||||
// which essentially no consumer had reason to do before this action started
|
||||
// making its own API calls. So the env-var-only version of this check was
|
||||
// unauthenticated for effectively every consumer, not just ones under
|
||||
// unusual load.
|
||||
//
|
||||
// The githubToken *input* (added alongside this) defaults to `${{
|
||||
// github.token }}`, which GitHub Actions populates on every run with no
|
||||
// consumer action needed - so it is the primary path, ahead of the env
|
||||
// vars, which stay as a fallback for the CLI/install.sh path that has no
|
||||
// Action input to read from.
|
||||
const token = githubToken || 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`, {
|
||||
@@ -397,8 +411,8 @@ async function saveToCache(version) {
|
||||
*
|
||||
* @param version A release tag (e.g. "v0.1.0"), or "latest".
|
||||
*/
|
||||
async function downloadCli(version) {
|
||||
const resolvedVersion = version === 'latest' ? await resolveLatestTag() : version;
|
||||
async function downloadCli(version, githubToken) {
|
||||
const resolvedVersion = version === 'latest' ? await resolveLatestTag(fetch, githubToken) : version;
|
||||
const cached = await restoreFromCache(resolvedVersion);
|
||||
if (cached)
|
||||
return cached;
|
||||
@@ -505,7 +519,11 @@ const resolve_project_path_1 = __nccwpck_require__(2833);
|
||||
async function run() {
|
||||
try {
|
||||
const cliVersion = core.getInput('cliVersion') || 'latest';
|
||||
const cliPath = await (0, download_cli_1.downloadCli)(cliVersion);
|
||||
// Defaults to ${{ github.token }}, always populated by Actions - see
|
||||
// download-cli.ts's resolveLatestTag for why this has to be threaded
|
||||
// through rather than left to a GITHUB_TOKEN env var.
|
||||
const githubToken = core.getInput('githubToken') || '';
|
||||
const cliPath = await (0, download_cli_1.downloadCli)(cliVersion, githubToken);
|
||||
const projectPath = (0, resolve_project_path_1.resolveProjectPath)({
|
||||
input: core.getInput('projectPath'),
|
||||
existsSync: fs.existsSync,
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
+1
-1
@@ -46,7 +46,7 @@
|
||||
"@vitest/coverage-istanbul": "^4.1.5",
|
||||
"cross-env": "^7.0.3",
|
||||
"husky": "9",
|
||||
"js-yaml": "^4.3.2",
|
||||
"js-yaml": "^4.1.0",
|
||||
"lint-staged": "^16.4.0",
|
||||
"oxfmt": "^0.48.0",
|
||||
"oxlint": "^1.63.0",
|
||||
|
||||
@@ -76,6 +76,31 @@ describe('resolveLatestTag', () => {
|
||||
}
|
||||
});
|
||||
|
||||
// The githubToken parameter is the action's own `githubToken` input,
|
||||
// which defaults to `${{ github.token }}` - populated by GitHub Actions on
|
||||
// every run with no consumer action needed. process.env.GITHUB_TOKEN, by
|
||||
// contrast, is NOT auto-injected into a JS action's environment - a
|
||||
// calling workflow has to set it explicitly, which essentially none did.
|
||||
// Confirmed live via game-ci/unity-test-runner#328: a consumer's six-
|
||||
// version matrix failed simultaneously with "GitHub API returned 403"
|
||||
// despite every job having a real, usable token the whole time.
|
||||
it('sends an Authorization header from the githubToken parameter even when no env var is set', async () => {
|
||||
const fetchFn = vi.fn(async () => ({
|
||||
ok: true,
|
||||
status: 200,
|
||||
json: async () => ({ tag_name: 'v0.1.15' }),
|
||||
})) as unknown as typeof fetch;
|
||||
|
||||
await resolveLatestTag(fetchFn, 'gha-token-from-input');
|
||||
|
||||
expect(fetchFn).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer gha-token-from-input' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
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';
|
||||
@@ -100,6 +125,61 @@ describe('resolveLatestTag', () => {
|
||||
}
|
||||
});
|
||||
|
||||
it('falls back to GH_TOKEN when GITHUB_TOKEN is unset', async () => {
|
||||
const originalGithub = process.env.GITHUB_TOKEN;
|
||||
const originalGh = process.env.GH_TOKEN;
|
||||
delete process.env.GITHUB_TOKEN;
|
||||
process.env.GH_TOKEN = 'gh-token-456';
|
||||
|
||||
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 gh-token-456' }),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
if (originalGithub !== undefined) process.env.GITHUB_TOKEN = originalGithub;
|
||||
if (originalGh === undefined) delete process.env.GH_TOKEN;
|
||||
else process.env.GH_TOKEN = originalGh;
|
||||
}
|
||||
});
|
||||
|
||||
it('prefers the explicit githubToken parameter over GITHUB_TOKEN/GH_TOKEN when both are present', async () => {
|
||||
const originalGithub = process.env.GITHUB_TOKEN;
|
||||
const originalGh = process.env.GH_TOKEN;
|
||||
process.env.GITHUB_TOKEN = 'env-github-token';
|
||||
process.env.GH_TOKEN = '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, 'explicit-input-token');
|
||||
expect(fetchFn).toHaveBeenCalledWith(
|
||||
expect.any(String),
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer explicit-input-token' }),
|
||||
}),
|
||||
);
|
||||
} finally {
|
||||
if (originalGithub === undefined) delete process.env.GITHUB_TOKEN;
|
||||
else process.env.GITHUB_TOKEN = originalGithub;
|
||||
if (originalGh === undefined) delete process.env.GH_TOKEN;
|
||||
else process.env.GH_TOKEN = originalGh;
|
||||
}
|
||||
});
|
||||
|
||||
it('throws with a clear message on a non-ok response', async () => {
|
||||
const fetchFn = vi.fn(async () => ({
|
||||
ok: false,
|
||||
@@ -171,6 +251,33 @@ describe('downloadCli', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// downloadCli is the only caller of resolveLatestTag in production code,
|
||||
// so this is what actually exercises the wiring index.ts depends on - the
|
||||
// resolveLatestTag-level test above only proves the function accepts the
|
||||
// parameter, not that anything passes it one.
|
||||
it('forwards its githubToken parameter to resolveLatestTag when resolving "latest"', async () => {
|
||||
globalThis.fetch = vi.fn(async (url: string) => {
|
||||
if (url.includes('/releases/latest')) {
|
||||
return { ok: true, status: 200, json: async () => ({ tag_name: 'v0.1.33' }) };
|
||||
}
|
||||
return { ok: true, status: 200, text: async () => 'echo mock install.sh' };
|
||||
}) as unknown as typeof fetch;
|
||||
|
||||
vi.mocked(exec.exec).mockImplementation(async (_cmd, _args, options) => {
|
||||
options?.listeners?.stdout?.(Buffer.from('/tmp/game-ci\n'));
|
||||
return 0;
|
||||
});
|
||||
|
||||
await downloadCli('latest', 'gha-token-from-input');
|
||||
|
||||
expect(globalThis.fetch).toHaveBeenCalledWith(
|
||||
'https://api.github.com/repos/game-ci/cli/releases/latest',
|
||||
expect.objectContaining({
|
||||
headers: expect.objectContaining({ Authorization: 'Bearer gha-token-from-input' }),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('resolves "latest" to a concrete tag before fetching install.sh', async () => {
|
||||
globalThis.fetch = vi.fn(async (url: string) => {
|
||||
if (url.includes('/releases/latest')) {
|
||||
|
||||
+29
-8
@@ -26,14 +26,31 @@ export function binaryNameFor(platform: NodeJS.Platform): string {
|
||||
* know how to resolve "latest" itself, by design, since which tag it's
|
||||
* fetched at IS the version it installs.
|
||||
*/
|
||||
export async function resolveLatestTag(fetchFn: typeof fetch = fetch): Promise<string> {
|
||||
export async function resolveLatestTag(
|
||||
fetchFn: typeof fetch = fetch,
|
||||
githubToken?: string,
|
||||
): 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;
|
||||
// exhausted by traffic this job never generated - hit live via
|
||||
// game-ci/unity-test-runner#328's consumer, whose six-version matrix failed
|
||||
// simultaneously with "GitHub API returned 403".
|
||||
//
|
||||
// process.env.GITHUB_TOKEN/GH_TOKEN alone does not fix this: GitHub Actions
|
||||
// does not inject GITHUB_TOKEN into a JS action's process environment
|
||||
// automatically - a calling workflow has to set it explicitly via env:,
|
||||
// which essentially no consumer had reason to do before this action started
|
||||
// making its own API calls. So the env-var-only version of this check was
|
||||
// unauthenticated for effectively every consumer, not just ones under
|
||||
// unusual load.
|
||||
//
|
||||
// The githubToken *input* (added alongside this) defaults to `${{
|
||||
// github.token }}`, which GitHub Actions populates on every run with no
|
||||
// consumer action needed - so it is the primary path, ahead of the env
|
||||
// vars, which stay as a fallback for the CLI/install.sh path that has no
|
||||
// Action input to read from.
|
||||
const token = githubToken || 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`, {
|
||||
@@ -125,8 +142,9 @@ async function saveToCache(version: string): Promise<void> {
|
||||
*
|
||||
* @param version A release tag (e.g. "v0.1.0"), or "latest".
|
||||
*/
|
||||
export async function downloadCli(version: string): Promise<string> {
|
||||
const resolvedVersion = version === 'latest' ? await resolveLatestTag() : version;
|
||||
export async function downloadCli(version: string, githubToken?: string): Promise<string> {
|
||||
const resolvedVersion =
|
||||
version === 'latest' ? await resolveLatestTag(fetch, githubToken) : version;
|
||||
|
||||
const cached = await restoreFromCache(resolvedVersion);
|
||||
if (cached) return cached;
|
||||
@@ -152,7 +170,10 @@ export async function downloadCli(version: string): Promise<string> {
|
||||
`GitHub returned ${scriptResponse.status} for ${installScriptUrl}.`,
|
||||
);
|
||||
}
|
||||
const scriptPath = path.join(os.tmpdir(), `game-ci-install-${resolvedVersion.replace(/[^\w.-]/g, '_')}.sh`);
|
||||
const scriptPath = path.join(
|
||||
os.tmpdir(),
|
||||
`game-ci-install-${resolvedVersion.replace(/[^\w.-]/g, '_')}.sh`,
|
||||
);
|
||||
await fs.writeFile(scriptPath, await scriptResponse.text(), { mode: 0o755 });
|
||||
|
||||
let stdout = '';
|
||||
|
||||
+5
-1
@@ -19,7 +19,11 @@ import { resolveProjectPath } from './resolve-project-path';
|
||||
export async function run() {
|
||||
try {
|
||||
const cliVersion = core.getInput('cliVersion') || 'latest';
|
||||
const cliPath = await downloadCli(cliVersion);
|
||||
// Defaults to ${{ github.token }}, always populated by Actions - see
|
||||
// download-cli.ts's resolveLatestTag for why this has to be threaded
|
||||
// through rather than left to a GITHUB_TOKEN env var.
|
||||
const githubToken = core.getInput('githubToken') || '';
|
||||
const cliPath = await downloadCli(cliVersion, githubToken);
|
||||
|
||||
const projectPath = resolveProjectPath({
|
||||
input: core.getInput('projectPath'),
|
||||
|
||||
@@ -2659,17 +2659,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"js-yaml@npm:^4.3.2":
|
||||
version: 4.3.2
|
||||
resolution: "js-yaml@npm:4.3.2"
|
||||
dependencies:
|
||||
argparse: "npm:^2.0.1"
|
||||
bin:
|
||||
js-yaml: bin/js-yaml.js
|
||||
checksum: 10/05c44b9c73e4901d92703b155e76518df64bf01ac62e4c036b47de4b391e19b72e32656e8954d51b436307f08cc9d0c0d4ec617d061cf2f65fffee9f3114bee7
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"jsesc@npm:^3.0.2":
|
||||
version: 3.1.0
|
||||
resolution: "jsesc@npm:3.1.0"
|
||||
@@ -3908,7 +3897,7 @@ __metadata:
|
||||
"@vitest/coverage-istanbul": "npm:^4.1.5"
|
||||
cross-env: "npm:^7.0.3"
|
||||
husky: "npm:9"
|
||||
js-yaml: "npm:^4.3.2"
|
||||
js-yaml: "npm:^4.1.0"
|
||||
lint-staged: "npm:^16.4.0"
|
||||
oxfmt: "npm:^0.48.0"
|
||||
oxlint: "npm:^1.63.0"
|
||||
|
||||
Reference in New Issue
Block a user