diff --git a/.github/workflows/pr-fast-ci.yml b/.github/workflows/pr-fast-ci.yml index 912950e..0ef1659 100644 --- a/.github/workflows/pr-fast-ci.yml +++ b/.github/workflows/pr-fast-ci.yml @@ -78,9 +78,9 @@ jobs: with: ref: ${{ github.event.pull_request.head.sha }} - - uses: actions/setup-node@v6 + - uses: ./actions/setup-shell-safe-node with: - node-version: "24" + node-version: 24.14.1 - uses: pnpm/action-setup@v6 with: diff --git a/docker/runner-entrypoint.ps1 b/docker/runner-entrypoint.ps1 index 9a815ed..2e35f6d 100644 --- a/docker/runner-entrypoint.ps1 +++ b/docker/runner-entrypoint.ps1 @@ -74,6 +74,36 @@ function Request-RunnerToken { return $response.token } + +function Invoke-ActionsRunner { + $credentialNames = @( + "GITHUB_PAT", + "GITHUB_TOKEN", + "GH_TOKEN", + "GITHUB_APP_ID", + "GITHUB_APP_INSTALLATION_ID", + "GITHUB_APP_PRIVATE_KEY" + ) + $savedCredentials = @{} + + foreach ($name in $credentialNames) { + $savedCredentials[$name] = [Environment]::GetEnvironmentVariable($name, "Process") + [Environment]::SetEnvironmentVariable($name, $null, "Process") + } + + try { + & .\run.cmd 2>&1 | + Tee-Object -FilePath (Join-Path $env:RUNNER_LOG_DIR "runner.log") -Append | + Out-Null + $runnerExitCode = $LASTEXITCODE + return $runnerExitCode + } finally { + foreach ($name in $credentialNames) { + [Environment]::SetEnvironmentVariable($name, $savedCredentials[$name], "Process") + } + } +} + function Clear-RunnerState { Remove-Item -Force -ErrorAction SilentlyContinue ` (Join-Path $env:RUNNER_HOME ".runner"), ` @@ -178,8 +208,8 @@ try { & .\config.cmd @configArgs $script:RunnerConfigured = $true Write-RunnerLog "starting runner $env:RUNNER_NAME" - & .\run.cmd 2>&1 | Tee-Object -FilePath (Join-Path $env:RUNNER_LOG_DIR "runner.log") -Append - exit $LASTEXITCODE + $runnerExitCode = Invoke-ActionsRunner + exit $runnerExitCode } finally { Pop-Location } diff --git a/docker/runner-entrypoint.sh b/docker/runner-entrypoint.sh index c83050e..aec807e 100755 --- a/docker/runner-entrypoint.sh +++ b/docker/runner-entrypoint.sh @@ -16,11 +16,21 @@ run_runner_bash() { shift || true if [[ "${runner_exec_mode}" == "root" ]]; then - env RUNNER_ALLOW_RUNASROOT=1 RUNNER_EXECUTION_MODE="${runner_exec_mode}" "$@" bash -lc "${command}" + env "$@" RUNNER_ALLOW_RUNASROOT=1 RUNNER_EXECUTION_MODE="${runner_exec_mode}" bash -lc "${command}" return fi - env RUNNER_EXECUTION_MODE="${runner_exec_mode}" "$@" gosu runner bash -lc "${command}" + env "$@" RUNNER_EXECUTION_MODE="${runner_exec_mode}" gosu runner bash -lc "${command}" +} + +run_actions_runner() { + run_runner_bash "cd '${RUNNER_HOME}' && exec ./run.sh" \ + -u GITHUB_PAT \ + -u GITHUB_TOKEN \ + -u GH_TOKEN \ + -u GITHUB_APP_ID \ + -u GITHUB_APP_INSTALLATION_ID \ + -u GITHUB_APP_PRIVATE_KEY } cleanup_local_state() { @@ -254,5 +264,4 @@ runner_configured="true" audit_event runner_registered log "starting runner ${RUNNER_NAME}" -run_runner_bash "cd '${RUNNER_HOME}' && exec ./run.sh" \ - 2>&1 | tee -a "${RUNNER_LOG_DIR}/runner.log" +run_actions_runner 2>&1 | tee -a "${RUNNER_LOG_DIR}/runner.log" diff --git a/scripts/ci/run-fast-checks.sh b/scripts/ci/run-fast-checks.sh index 6d9340d..675a96e 100755 --- a/scripts/ci/run-fast-checks.sh +++ b/scripts/ci/run-fast-checks.sh @@ -11,7 +11,10 @@ set -euo pipefail repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" cd "$repo_root" -corepack enable +corepack_bin_dir="${COREPACK_BIN_DIR:-${RUNNER_TEMP:-${TMPDIR:-/tmp}}/corepack-bin}" +mkdir -p "${corepack_bin_dir}" +corepack enable --install-directory "${corepack_bin_dir}" +export PATH="${corepack_bin_dir}:${PATH}" corepack prepare pnpm@10.32.1 --activate pnpm install --frozen-lockfile diff --git a/scripts/guest/macos-runner-bootstrap.sh b/scripts/guest/macos-runner-bootstrap.sh index b827570..fcc387c 100755 --- a/scripts/guest/macos-runner-bootstrap.sh +++ b/scripts/guest/macos-runner-bootstrap.sh @@ -108,6 +108,7 @@ RUNNER_CONFIGURED="true" log "starting runner ${RUNNER_NAME}" ( + unset GITHUB_PAT GITHUB_TOKEN GH_TOKEN GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY cd "${RUNNER_ROOT}" ./run.sh ) diff --git a/scripts/smoke/actions-runner/run.sh b/scripts/smoke/actions-runner/run.sh index e19fc1a..8a02658 100755 --- a/scripts/smoke/actions-runner/run.sh +++ b/scripts/smoke/actions-runner/run.sh @@ -4,6 +4,12 @@ set -euo pipefail printf '%s run.sh stub executed\n' "$(date -Iseconds)" >> "${RUNNER_STATE_DIR}/run.log" printf 'run path: %s\n' "$(pwd)" >> "${RUNNER_STATE_DIR}/run-context.log" printf 'run mode: %s\n' "${RUNNER_EXECUTION_MODE:-unknown}" >> "${RUNNER_STATE_DIR}/run-context.log" +for credential_name in GITHUB_PAT GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY; do + if [[ -n "${!credential_name:-}" ]]; then + printf 'leaked credential: %s\n' "${credential_name}" >> "${RUNNER_STATE_DIR}/run-context.log" + exit 1 + fi +done mkdir -p "${RUNNER_WORK_DIR}/workspace" touch "${RUNNER_WORK_DIR}/workspace/job.txt" echo "job output" diff --git a/test/smoke-harness.test.ts b/test/smoke-harness.test.ts index 9c47bb8..3662db2 100644 --- a/test/smoke-harness.test.ts +++ b/test/smoke-harness.test.ts @@ -5,6 +5,14 @@ import path from "node:path"; import { afterEach, describe, expect, test } from "vitest"; const tempRoots: string[] = []; +const runnerCredentialNames = [ + "GITHUB_PAT", + "GITHUB_TOKEN", + "GH_TOKEN", + "GITHUB_APP_ID", + "GITHUB_APP_INSTALLATION_ID", + "GITHUB_APP_PRIVATE_KEY", +]; afterEach(() => { for (const tempRoot of tempRoots.splice(0)) { @@ -18,6 +26,14 @@ function makeTempRoot() { return tempRoot; } +function withoutRunnerCredentials(env: NodeJS.ProcessEnv): NodeJS.ProcessEnv { + const scrubbedEnv = { ...env }; + for (const name of runnerCredentialNames) { + delete scrubbedEnv[name]; + } + return scrubbedEnv; +} + // Resolve as soon as the mock API prints its readiness sentinel on // stdout. This is event-driven (no fixed-interval polling), and rejects // immediately if the child fails to spawn or exits early, so the only @@ -124,7 +140,7 @@ describe("runner registration smoke harness", () => { const resolvedRunnerHome = fs.realpathSync(runnerHome); const env = { - ...process.env, + ...withoutRunnerCredentials(process.env), RUNNER_EXECUTION_MODE: "runner", RUNNER_STATE_DIR: tempRoot, RUNNER_WORK_DIR: workDir, @@ -180,6 +196,34 @@ describe("runner registration smoke harness", () => { expect(fs.existsSync(path.join(workDir, "workspace", "job.txt"))).toBe(true); }); + test("scrubs GitHub credentials before launching runner jobs", () => { + const linuxEntrypoint = fs.readFileSync( + path.resolve("docker/runner-entrypoint.sh"), + "utf8" + ); + const windowsEntrypoint = fs.readFileSync( + path.resolve("docker/runner-entrypoint.ps1"), + "utf8" + ); + const macosBootstrap = fs.readFileSync( + path.resolve("scripts/guest/macos-runner-bootstrap.sh"), + "utf8" + ); + + for (const name of runnerCredentialNames) { + expect(linuxEntrypoint).toContain(`-u ${name}`); + expect(windowsEntrypoint).toContain(`\"${name}\"`); + } + expect(linuxEntrypoint).toContain("run_actions_runner 2>&1"); + expect(windowsEntrypoint).toContain("Out-Null"); + expect(windowsEntrypoint).toContain("$runnerExitCode = Invoke-ActionsRunner"); + expect(windowsEntrypoint).toContain("exit $runnerExitCode"); + expect(windowsEntrypoint).not.toContain("exit (Invoke-ActionsRunner)"); + expect(macosBootstrap).toContain( + "unset GITHUB_PAT GITHUB_TOKEN GH_TOKEN GITHUB_APP_ID GITHUB_APP_INSTALLATION_ID GITHUB_APP_PRIVATE_KEY" + ); + }); + test("wires the smoke script to run both execution modes and verify cleanup", () => { const script = fs.readFileSync( path.resolve("scripts/smoke-test.sh"), diff --git a/test/workflow.test.ts b/test/workflow.test.ts index a2cd75e..2f32bd1 100644 --- a/test/workflow.test.ts +++ b/test/workflow.test.ts @@ -247,7 +247,7 @@ describe("CI workflow", () => { } expect( (workflow.jobs["fast-checks"].steps as Array>).some( - (step) => step.uses === "actions/setup-node@v6" + (step) => step.uses === "./actions/setup-shell-safe-node" ) ).toBe(true);