Obtain job-level token by default for less privileged runs (#198)

This commit is contained in:
Mateusz Burzyński
2026-01-29 21:30:08 +00:00
committed by pullfrog[bot]
parent bb7e7584d4
commit 2daab6fc78
6 changed files with 73 additions and 37 deletions
+4
View File
@@ -30,6 +30,10 @@ inputs:
bash: 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." 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 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: runs:
using: "node24" using: "node24"
+32 -18
View File
@@ -19732,7 +19732,7 @@ var require_core = __commonJS({
process.env["PATH"] = `${inputPath}${path4.delimiter}${process.env["PATH"]}`; process.env["PATH"] = `${inputPath}${path4.delimiter}${process.env["PATH"]}`;
} }
exports.addPath = addPath; exports.addPath = addPath;
function getInput2(name, options) { function getInput3(name, options) {
const val = process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || ""; const val = process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) { if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`); throw new Error(`Input required and not supplied: ${name}`);
@@ -19742,9 +19742,9 @@ var require_core = __commonJS({
} }
return val.trim(); return val.trim();
} }
exports.getInput = getInput2; exports.getInput = getInput3;
function getMultilineInput(name, options) { 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) { if (options && options.trimWhitespace === false) {
return inputs; return inputs;
} }
@@ -19754,7 +19754,7 @@ var require_core = __commonJS({
function getBooleanInput(name, options) { function getBooleanInput(name, options) {
const trueValue = ["true", "True", "TRUE"]; const trueValue = ["true", "True", "TRUE"];
const falseValue = ["false", "False", "FALSE"]; const falseValue = ["false", "False", "FALSE"];
const val = getInput2(name, options); const val = getInput3(name, options);
if (trueValue.includes(val)) if (trueValue.includes(val))
return true; return true;
if (falseValue.includes(val)) if (falseValue.includes(val))
@@ -141999,29 +141999,40 @@ function AwaitDependencyInstallationTool(ctx) {
var core3 = __toESM(require_core(), 1); var core3 = __toESM(require_core(), 1);
import assert2 from "node:assert/strict"; import assert2 from "node:assert/strict";
var githubInstallationToken; 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() { async function resolveInstallationToken() {
assert2(!githubInstallationToken, "GitHub installation token is already set."); assert2(!githubInstallationToken, "GitHub installation token is already set.");
const originalToken = process.env.GITHUB_TOKEN; const githubJobToken = core3.getInput("token");
if (originalToken) {
process.env.ORIGINAL_GITHUB_TOKEN = originalToken;
}
const externalToken = process.env.GH_TOKEN; const externalToken = process.env.GH_TOKEN;
const token = externalToken || await acquireNewToken(); const token = externalToken || await acquireNewToken();
process.env.GITHUB_TOKEN = token; const revertGithubToken = setEnvironmentVariable("GITHUB_TOKEN", token);
githubInstallationToken = token; githubInstallationToken = token;
if (isGitHubActions) { if (isGitHubActions) {
core3.setSecret(token); core3.setSecret(token);
} }
return { return {
token, 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]() { async [Symbol.asyncDispose]() {
githubInstallationToken = void 0; githubInstallationToken = void 0;
if (originalToken) { revertGithubToken();
process.env.GITHUB_TOKEN = originalToken;
} else {
delete process.env.GITHUB_TOKEN;
}
if (externalToken) { if (externalToken) {
return; return;
} }
@@ -162542,7 +162553,11 @@ async function setupGit(params) {
} catch { } catch {
log.debug("\xBB no existing authentication headers to remove"); 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) { if (params.event.is_pr !== true || !params.event.issue_number) {
const originUrl2 = `https://x-access-token:${originToken}@github.com/${params.owner}/${params.name}.git`; const originUrl2 = `https://x-access-token:${originToken}@github.com/${params.owner}/${params.name}.git`;
$("git", ["remote", "set-url", "origin", originUrl2], { cwd: repoDir }); $("git", ["remote", "set-url", "origin", originUrl2], { cwd: repoDir });
@@ -162651,7 +162666,6 @@ async function main() {
normalizeEnv(); normalizeEnv();
const timer = new Timer(); const timer = new Timer();
const tokenRef = __using(_stack2, await resolveInstallationToken(), true); const tokenRef = __using(_stack2, await resolveInstallationToken(), true);
process.env.GITHUB_TOKEN = tokenRef.token;
const octokit = createOctokit(tokenRef.token); const octokit = createOctokit(tokenRef.token);
const runInfo = await resolveRun({ octokit }); const runInfo = await resolveRun({ octokit });
const toolState = initToolState({ runInfo }); const toolState = initToolState({ runInfo });
@@ -162686,7 +162700,7 @@ async function main() {
}); });
await setupGit({ await setupGit({
token: tokenRef.token, token: tokenRef.token,
originalToken: tokenRef.originalToken, githubJobToken: tokenRef.githubJobToken,
bashPermission: payload.bash, bashPermission: payload.bash,
owner: runContext.repo.owner, owner: runContext.repo.owner,
name: runContext.repo.name, name: runContext.repo.name,
+4 -4
View File
@@ -19680,7 +19680,7 @@ var require_core = __commonJS({
process.env["PATH"] = `${inputPath}${path.delimiter}${process.env["PATH"]}`; process.env["PATH"] = `${inputPath}${path.delimiter}${process.env["PATH"]}`;
} }
exports.addPath = addPath; exports.addPath = addPath;
function getInput2(name, options) { function getInput3(name, options) {
const val = process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || ""; const val = process.env[`INPUT_${name.replace(/ /g, "_").toUpperCase()}`] || "";
if (options && options.required && !val) { if (options && options.required && !val) {
throw new Error(`Input required and not supplied: ${name}`); throw new Error(`Input required and not supplied: ${name}`);
@@ -19690,9 +19690,9 @@ var require_core = __commonJS({
} }
return val.trim(); return val.trim();
} }
exports.getInput = getInput2; exports.getInput = getInput3;
function getMultilineInput(name, options) { 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) { if (options && options.trimWhitespace === false) {
return inputs; return inputs;
} }
@@ -19702,7 +19702,7 @@ var require_core = __commonJS({
function getBooleanInput(name, options) { function getBooleanInput(name, options) {
const trueValue = ["true", "True", "TRUE"]; const trueValue = ["true", "True", "TRUE"];
const falseValue = ["false", "False", "FALSE"]; const falseValue = ["false", "False", "FALSE"];
const val = getInput2(name, options); const val = getInput3(name, options);
if (trueValue.includes(val)) if (trueValue.includes(val))
return true; return true;
if (falseValue.includes(val)) if (falseValue.includes(val))
+1 -2
View File
@@ -34,7 +34,6 @@ export async function main(): Promise<MainResult> {
const timer = new Timer(); const timer = new Timer();
await using tokenRef = await resolveInstallationToken(); await using tokenRef = await resolveInstallationToken();
process.env.GITHUB_TOKEN = tokenRef.token;
const octokit = createOctokit(tokenRef.token); const octokit = createOctokit(tokenRef.token);
const runInfo = await resolveRun({ octokit }); const runInfo = await resolveRun({ octokit });
@@ -81,7 +80,7 @@ export async function main(): Promise<MainResult> {
await setupGit({ await setupGit({
token: tokenRef.token, token: tokenRef.token,
originalToken: tokenRef.originalToken, githubJobToken: tokenRef.githubJobToken,
bashPermission: payload.bash, bashPermission: payload.bash,
owner: runContext.repo.owner, owner: runContext.repo.owner,
name: runContext.repo.name, name: runContext.repo.name,
+6 -2
View File
@@ -44,7 +44,7 @@ export function setupTestRepo(options: SetupOptions): void {
interface SetupGitParams { interface SetupGitParams {
token: string; token: string;
originalToken: string | undefined; githubJobToken: string | undefined;
bashPermission: BashPermission; bashPermission: BashPermission;
owner: string; owner: string;
name: string; name: string;
@@ -134,7 +134,11 @@ export async function setupGit(params: SetupGitParams): Promise<void> {
// - restricted/disabled: workflow token (limited by permissions block) // - restricted/disabled: workflow token (limited by permissions block)
// this protects the base repo while allowing fork PR edits via fork remote // this protects the base repo while allowing fork PR edits via fork remote
const originToken = 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 // non-PR events: set up origin with token, stay on default branch
if (params.event.is_pr !== true || !params.event.issue_number) { if (params.event.is_pr !== true || !params.event.issue_number) {
+26 -11
View File
@@ -11,18 +11,35 @@ export { revokeGitHubInstallationToken as revokeInstallationToken };
// store token in memory instead of process.env // store token in memory instead of process.env
let githubInstallationToken: string | undefined; 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 * Setup GitHub installation token for the action
*/ */
export async function resolveInstallationToken() { export async function resolveInstallationToken() {
assert(!githubInstallationToken, "GitHub installation token is already set."); assert(!githubInstallationToken, "GitHub installation token is already set.");
const originalToken = process.env.GITHUB_TOKEN; const githubJobToken = core.getInput("token");
if (originalToken) {
process.env.ORIGINAL_GITHUB_TOKEN = originalToken;
}
const externalToken = process.env.GH_TOKEN; const externalToken = process.env.GH_TOKEN;
const token = externalToken || (await acquireNewToken()); const token = externalToken || (await acquireNewToken());
process.env.GITHUB_TOKEN = token;
const revertGithubToken = setEnvironmentVariable("GITHUB_TOKEN", token);
githubInstallationToken = token; githubInstallationToken = token;
if (isGitHubActions) { if (isGitHubActions) {
@@ -33,14 +50,12 @@ export async function resolveInstallationToken() {
return { return {
token, 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]() { async [Symbol.asyncDispose]() {
githubInstallationToken = undefined; githubInstallationToken = undefined;
if (originalToken) { revertGithubToken();
process.env.GITHUB_TOKEN = originalToken;
} else {
delete process.env.GITHUB_TOKEN;
}
// GH_TOKEN isn't acquired here, so it's not revoked here either // GH_TOKEN isn't acquired here, so it's not revoked here either
if (externalToken) { if (externalToken) {
return; return;