Fix github_actions iss

This commit is contained in:
Colin McDonnell
2026-01-08 14:11:11 -08:00
parent d30532979a
commit 9291ee5952
3 changed files with 1264 additions and 24 deletions
+1213 -6
View File
File diff suppressed because it is too large Load Diff
+35 -10
View File
@@ -49,11 +49,11 @@ function filterEnv(): Record<string, string> {
*/ */
function spawnSandboxed( function spawnSandboxed(
command: string, command: string,
options: { env: Record<string, string>; cwd: string }, options: { env: Record<string, string>; cwd: string }
): ChildProcess { ): ChildProcess {
const stdio: ["ignore", "pipe", "pipe"] = ["ignore", "pipe", "pipe"]; const stdio: ["ignore", "pipe", "pipe"] = ["ignore", "pipe", "pipe"];
const spawnOpts = { env: options.env, cwd: options.cwd, stdio, detached: true }; const spawnOpts = { env: options.env, cwd: options.cwd, stdio, detached: true };
return process.env.GITHUB_ACTIONS === "true" return process.env.CI === "true" // requires linux runner or this will fail.
? spawn("unshare", ["--pid", "--fork", "--mount-proc", "bash", "-c", command], spawnOpts) ? spawn("unshare", ["--pid", "--fork", "--mount-proc", "bash", "-c", command], spawnOpts)
: spawn("bash", ["-c", command], spawnOpts); : spawn("bash", ["-c", command], spawnOpts);
} }
@@ -66,7 +66,11 @@ async function killProcessGroup(proc: ChildProcess): Promise<void> {
await new Promise((r) => setTimeout(r, 200)); await new Promise((r) => setTimeout(r, 200));
process.kill(-proc.pid, "SIGKILL"); process.kill(-proc.pid, "SIGKILL");
} catch { } catch {
try { proc.kill("SIGKILL"); } catch { /* already dead */ } try {
proc.kill("SIGKILL");
} catch {
/* already dead */
}
} }
} }
@@ -88,24 +92,45 @@ The command runs in a bash shell with a filtered environment that excludes sensi
const cwd = params.working_directory ?? process.cwd(); const cwd = params.working_directory ?? process.cwd();
const proc = spawnSandboxed(params.command, { env: filterEnv(), cwd }); const proc = spawnSandboxed(params.command, { env: filterEnv(), cwd });
let stdout = "", stderr = "", timedOut = false, exited = false; let stdout = "",
proc.stdout?.on("data", (chunk: Buffer) => { stdout += chunk.toString(); }); stderr = "",
proc.stderr?.on("data", (chunk: Buffer) => { stderr += chunk.toString(); }); timedOut = false,
exited = false;
proc.stdout?.on("data", (chunk: Buffer) => {
stdout += chunk.toString();
});
proc.stderr?.on("data", (chunk: Buffer) => {
stderr += chunk.toString();
});
const timeoutId = setTimeout(async () => { const timeoutId = setTimeout(async () => {
if (!exited) { timedOut = true; await killProcessGroup(proc); } if (!exited) {
timedOut = true;
await killProcessGroup(proc);
}
}, timeout); }, timeout);
const exitCode = await new Promise<number | null>((resolve) => { const exitCode = await new Promise<number | null>((resolve) => {
const done = (code: number | null) => { exited = true; clearTimeout(timeoutId); resolve(code); }; const done = (code: number | null) => {
exited = true;
clearTimeout(timeoutId);
resolve(code);
};
proc.on("exit", done); proc.on("exit", done);
proc.on("error", () => done(null)); proc.on("error", () => done(null));
}); });
let output = stderr ? (stdout ? `${stdout}\n${stderr}` : stderr) : stdout; let output = stderr ? (stdout ? `${stdout}\n${stderr}` : stderr) : stdout;
if (timedOut) output = output ? `${output}\n[timed out after ${timeout}ms]` : `[timed out after ${timeout}ms]`; if (timedOut)
output = output
? `${output}\n[timed out after ${timeout}ms]`
: `[timed out after ${timeout}ms]`;
return { output: output.trim(), exit_code: exitCode ?? (timedOut ? 124 : -1), timed_out: timedOut }; return {
output: output.trim(),
exit_code: exitCode ?? (timedOut ? 124 : -1),
timed_out: timedOut,
};
}), }),
}); });
} }
+16 -8
View File
@@ -1,8 +1,8 @@
import assert from "node:assert/strict";
import { createSign } from "node:crypto";
import * as core from "@actions/core"; import * as core from "@actions/core";
import { throttling } from "@octokit/plugin-throttling"; import { throttling } from "@octokit/plugin-throttling";
import { Octokit } from "@octokit/rest"; import { Octokit } from "@octokit/rest";
import assert from "node:assert/strict";
import { createSign } from "node:crypto";
import { log } from "./cli.ts"; import { log } from "./cli.ts";
import { retry } from "./retry.ts"; import { retry } from "./retry.ts";
@@ -47,8 +47,11 @@ interface RepositoriesResponse {
repositories: Repository[]; repositories: Repository[];
} }
function isGitHubActionsEnvironment(): boolean { function isOIDCAvailable(): boolean {
return Boolean(process.env.GITHUB_ACTIONS); // OIDC requires both env vars to be set (only in real GitHub Actions with id-token permission)
return Boolean(
process.env.ACTIONS_ID_TOKEN_REQUEST_URL && process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN
);
} }
async function acquireTokenViaOIDC(): Promise<string> { async function acquireTokenViaOIDC(): Promise<string> {
@@ -236,7 +239,7 @@ async function acquireTokenViaGitHubApp(): Promise<string> {
} }
async function acquireNewToken(): Promise<string> { async function acquireNewToken(): Promise<string> {
if (isGitHubActionsEnvironment()) { if (isOIDCAvailable()) {
return await retry(() => acquireTokenViaOIDC(), { label: "token exchange" }); return await retry(() => acquireTokenViaOIDC(), { label: "token exchange" });
} else { } else {
return await acquireTokenViaGitHubApp(); return await acquireTokenViaGitHubApp();
@@ -267,7 +270,10 @@ export async function setupGitHubInstallationToken() {
* Get the GitHub installation token from memory * Get the GitHub installation token from memory
*/ */
export function getGitHubInstallationToken(): string { export function getGitHubInstallationToken(): string {
assert(githubInstallationToken, "GitHub installation token not set. Call setupGitHubInstallationToken first."); assert(
githubInstallationToken,
"GitHub installation token not set. Call setupGitHubInstallationToken first."
);
return githubInstallationToken; return githubInstallationToken;
} }
@@ -313,7 +319,9 @@ export function parseRepoContext(): RepoContext {
return { owner, name }; return { owner, name };
} }
export type OctokitWithPlugins = InstanceType<ReturnType<typeof Octokit.plugin<typeof Octokit, [typeof throttling]>>> export type OctokitWithPlugins = InstanceType<
ReturnType<typeof Octokit.plugin<typeof Octokit, [typeof throttling]>>
>;
export function createOctokit(token: string): OctokitWithPlugins { export function createOctokit(token: string): OctokitWithPlugins {
// `OctokitWithPlugins` initialization based on https://github.com/actions/toolkit/blob/2506e78e82fbd2f9e94d63e75f5309118c8de1b1/packages/github/src/github.ts#L15-L22 // `OctokitWithPlugins` initialization based on https://github.com/actions/toolkit/blob/2506e78e82fbd2f9e94d63e75f5309118c8de1b1/packages/github/src/github.ts#L15-L22
@@ -328,6 +336,6 @@ export function createOctokit(token: string): OctokitWithPlugins {
onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => { onSecondaryRateLimit: (retryAfter, options, octokit, retryCount) => {
return retryCount <= 2; return retryCount <= 2;
}, },
} },
}); });
} }