From 2daab6fc78cf2cb9784f3c2fa8f6984b0d9fd4ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Burzy=C5=84ski?= Date: Thu, 29 Jan 2026 21:30:08 +0000 Subject: [PATCH] Obtain job-level token by default for less privileged runs (#198) --- action.yml | 4 +++ entry | 50 +++++++++++++++++++++++------------- get-installation-token/entry | 8 +++--- main.ts | 3 +-- utils/setup.ts | 8 ++++-- utils/token.ts | 37 ++++++++++++++++++-------- 6 files changed, 73 insertions(+), 37 deletions(-) diff --git a/action.yml b/action.yml index e4fe3b8..f056c44 100644 --- a/action.yml +++ b/action.yml @@ -30,6 +30,10 @@ inputs: bash: description: "Bash permission: disabled, restricted (filters secrets from env vars), or enabled. Public repos default to restricted for security; private repos default to enabled." required: false + token: + description: "GitHub-provided token with job-scoped permissions. Do not set this unless you know what you are doing." + required: false + default: ${{ github.token }} runs: using: "node24" diff --git a/entry b/entry index 4b29e1d..cacdce9 100755 --- a/entry +++ b/entry @@ -19732,7 +19732,7 @@ var require_core = __commonJS({ process.env["PATH"] = `${inputPath}${path4.delimiter}${process.env["PATH"]}`; } exports.addPath = addPath; - function getInput2(name, options) { + function getInput3(name, options) { const val = process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || ""; if (options && options.required && !val) { throw new Error(`Input required and not supplied: ${name}`); @@ -19742,9 +19742,9 @@ var require_core = __commonJS({ } return val.trim(); } - exports.getInput = getInput2; + exports.getInput = getInput3; function getMultilineInput(name, options) { - const inputs = getInput2(name, options).split("\n").filter((x) => x !== ""); + const inputs = getInput3(name, options).split("\n").filter((x) => x !== ""); if (options && options.trimWhitespace === false) { return inputs; } @@ -19754,7 +19754,7 @@ var require_core = __commonJS({ function getBooleanInput(name, options) { const trueValue = ["true", "True", "TRUE"]; const falseValue = ["false", "False", "FALSE"]; - const val = getInput2(name, options); + const val = getInput3(name, options); if (trueValue.includes(val)) return true; if (falseValue.includes(val)) @@ -141999,29 +141999,40 @@ function AwaitDependencyInstallationTool(ctx) { var core3 = __toESM(require_core(), 1); import assert2 from "node:assert/strict"; var githubInstallationToken; +function setEnvironmentVariable(name, value2) { + const hadValue = Object.hasOwn(process.env, name); + const originalValue = process.env[name]; + if (typeof value2 === "string") { + process.env[name] = value2; + } else { + delete process.env[name]; + } + return () => { + if (hadValue) { + process.env[name] = originalValue; + } else { + delete process.env[name]; + } + }; +} async function resolveInstallationToken() { assert2(!githubInstallationToken, "GitHub installation token is already set."); - const originalToken = process.env.GITHUB_TOKEN; - if (originalToken) { - process.env.ORIGINAL_GITHUB_TOKEN = originalToken; - } + const githubJobToken = core3.getInput("token"); const externalToken = process.env.GH_TOKEN; const token = externalToken || await acquireNewToken(); - process.env.GITHUB_TOKEN = token; + const revertGithubToken = setEnvironmentVariable("GITHUB_TOKEN", token); githubInstallationToken = token; if (isGitHubActions) { core3.setSecret(token); } return { token, - originalToken, + // in GitHub Actions environment this fallback token should always come from the action's input + // but in other environments there is no secondary token like this so we just use the installation token itself + githubJobToken, async [Symbol.asyncDispose]() { githubInstallationToken = void 0; - if (originalToken) { - process.env.GITHUB_TOKEN = originalToken; - } else { - delete process.env.GITHUB_TOKEN; - } + revertGithubToken(); if (externalToken) { return; } @@ -162542,7 +162553,11 @@ async function setupGit(params) { } catch { log.debug("\xBB no existing authentication headers to remove"); } - const originToken = params.bashPermission === "enabled" ? params.token : params.originalToken || params.token; + const originToken = params.bashPermission === "enabled" ? params.token : ( + // in GitHub Actions environment this less-capable job token should always be available in the action's input + // but in other environments there is no secondary token like this so we just use the installation token itself + params.githubJobToken || params.token + ); if (params.event.is_pr !== true || !params.event.issue_number) { const originUrl2 = `https://x-access-token:${originToken}@github.com/${params.owner}/${params.name}.git`; $("git", ["remote", "set-url", "origin", originUrl2], { cwd: repoDir }); @@ -162651,7 +162666,6 @@ async function main() { normalizeEnv(); const timer = new Timer(); const tokenRef = __using(_stack2, await resolveInstallationToken(), true); - process.env.GITHUB_TOKEN = tokenRef.token; const octokit = createOctokit(tokenRef.token); const runInfo = await resolveRun({ octokit }); const toolState = initToolState({ runInfo }); @@ -162686,7 +162700,7 @@ async function main() { }); await setupGit({ token: tokenRef.token, - originalToken: tokenRef.originalToken, + githubJobToken: tokenRef.githubJobToken, bashPermission: payload.bash, owner: runContext.repo.owner, name: runContext.repo.name, diff --git a/get-installation-token/entry b/get-installation-token/entry index a63a724..9bdf3f4 100755 --- a/get-installation-token/entry +++ b/get-installation-token/entry @@ -19680,7 +19680,7 @@ var require_core = __commonJS({ process.env["PATH"] = `${inputPath}${path.delimiter}${process.env["PATH"]}`; } exports.addPath = addPath; - function getInput2(name, options) { + function getInput3(name, options) { const val = process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || ""; if (options && options.required && !val) { throw new Error(`Input required and not supplied: ${name}`); @@ -19690,9 +19690,9 @@ var require_core = __commonJS({ } return val.trim(); } - exports.getInput = getInput2; + exports.getInput = getInput3; function getMultilineInput(name, options) { - const inputs = getInput2(name, options).split("\n").filter((x) => x !== ""); + const inputs = getInput3(name, options).split("\n").filter((x) => x !== ""); if (options && options.trimWhitespace === false) { return inputs; } @@ -19702,7 +19702,7 @@ var require_core = __commonJS({ function getBooleanInput(name, options) { const trueValue = ["true", "True", "TRUE"]; const falseValue = ["false", "False", "FALSE"]; - const val = getInput2(name, options); + const val = getInput3(name, options); if (trueValue.includes(val)) return true; if (falseValue.includes(val)) diff --git a/main.ts b/main.ts index 62d7da2..8820bf0 100644 --- a/main.ts +++ b/main.ts @@ -34,7 +34,6 @@ export async function main(): Promise { const timer = new Timer(); await using tokenRef = await resolveInstallationToken(); - process.env.GITHUB_TOKEN = tokenRef.token; const octokit = createOctokit(tokenRef.token); const runInfo = await resolveRun({ octokit }); @@ -81,7 +80,7 @@ export async function main(): Promise { await setupGit({ token: tokenRef.token, - originalToken: tokenRef.originalToken, + githubJobToken: tokenRef.githubJobToken, bashPermission: payload.bash, owner: runContext.repo.owner, name: runContext.repo.name, diff --git a/utils/setup.ts b/utils/setup.ts index 559eccd..54304e1 100644 --- a/utils/setup.ts +++ b/utils/setup.ts @@ -44,7 +44,7 @@ export function setupTestRepo(options: SetupOptions): void { interface SetupGitParams { token: string; - originalToken: string | undefined; + githubJobToken: string | undefined; bashPermission: BashPermission; owner: string; name: string; @@ -134,7 +134,11 @@ export async function setupGit(params: SetupGitParams): Promise { // - restricted/disabled: workflow token (limited by permissions block) // this protects the base repo while allowing fork PR edits via fork remote const originToken = - params.bashPermission === "enabled" ? params.token : params.originalToken || params.token; + params.bashPermission === "enabled" + ? params.token + : // in GitHub Actions environment this less-capable job token should always be available in the action's input + // but in other environments there is no secondary token like this so we just use the installation token itself + params.githubJobToken || params.token; // non-PR events: set up origin with token, stay on default branch if (params.event.is_pr !== true || !params.event.issue_number) { diff --git a/utils/token.ts b/utils/token.ts index cec43ca..ba8ad13 100644 --- a/utils/token.ts +++ b/utils/token.ts @@ -11,18 +11,35 @@ export { revokeGitHubInstallationToken as revokeInstallationToken }; // store token in memory instead of process.env let githubInstallationToken: string | undefined; +function setEnvironmentVariable(name: string, value: string | undefined) { + const hadValue = Object.hasOwn(process.env, name); + const originalValue = process.env[name]; + + if (typeof value === "string") { + process.env[name] = value; + } else { + delete process.env[name]; + } + + return () => { + if (hadValue) { + process.env[name] = originalValue; + } else { + delete process.env[name]; + } + }; +} + /** * Setup GitHub installation token for the action */ export async function resolveInstallationToken() { assert(!githubInstallationToken, "GitHub installation token is already set."); - const originalToken = process.env.GITHUB_TOKEN; - if (originalToken) { - process.env.ORIGINAL_GITHUB_TOKEN = originalToken; - } + const githubJobToken = core.getInput("token"); const externalToken = process.env.GH_TOKEN; const token = externalToken || (await acquireNewToken()); - process.env.GITHUB_TOKEN = token; + + const revertGithubToken = setEnvironmentVariable("GITHUB_TOKEN", token); githubInstallationToken = token; if (isGitHubActions) { @@ -33,14 +50,12 @@ export async function resolveInstallationToken() { return { token, - originalToken, + // in GitHub Actions environment this fallback token should always come from the action's input + // but in other environments there is no secondary token like this so we just use the installation token itself + githubJobToken, async [Symbol.asyncDispose]() { githubInstallationToken = undefined; - if (originalToken) { - process.env.GITHUB_TOKEN = originalToken; - } else { - delete process.env.GITHUB_TOKEN; - } + revertGithubToken(); // GH_TOKEN isn't acquired here, so it's not revoked here either if (externalToken) { return;