diff --git a/actions/setup/clean.sh b/actions/setup/clean.sh index 3f3cd511f85..19bd60691c0 100755 --- a/actions/setup/clean.sh +++ b/actions/setup/clean.sh @@ -41,10 +41,11 @@ if [ -d "${tmpDir}" ]; then fi fi -# Remove AWF chroot home directories under /tmp (e.g. /tmp/awf-*-chroot-home). -# These are created by AWF when running with --enable-host-access on GitHub-hosted runners. -# Files inside may be owned by root (written by Docker containers or privileged AWF processes), -# causing EACCES failures if cleanup is attempted without sudo. +# Remove AWF chroot directories under /tmp (e.g. /tmp/awf-*-chroot-home and +# /tmp/awf-chroot-*). These are created by AWF when running with +# --enable-host-access on GitHub-hosted runners. Files inside may be owned by +# root (written by Docker containers or privileged AWF processes), causing +# EACCES failures if cleanup is attempted without sudo. if awf_chroot_home_dirs="$(sudo find /tmp -maxdepth 1 -name 'awf-*-chroot-home' -type d -print 2>/dev/null)"; then if [ -n "${awf_chroot_home_dirs}" ]; then if sudo find /tmp -maxdepth 1 -name 'awf-*-chroot-home' -type d -exec rm -rf -- {} + 2>/dev/null; then @@ -58,3 +59,17 @@ if awf_chroot_home_dirs="$(sudo find /tmp -maxdepth 1 -name 'awf-*-chroot-home' else echo "Warning: unable to inspect /tmp/awf-*-chroot-home directories with sudo" >&2 fi + +if awf_chroot_dirs="$(sudo find /tmp -maxdepth 1 -name 'awf-chroot-*' -type d -print 2>/dev/null)"; then + if [ -n "${awf_chroot_dirs}" ]; then + if sudo find /tmp -maxdepth 1 -name 'awf-chroot-*' -type d -exec rm -rf -- {} + 2>/dev/null; then + echo "Cleaned up /tmp/awf-chroot-* directories (sudo)" + else + echo "Warning: failed to clean /tmp/awf-chroot-* directories" >&2 + fi + else + echo "No /tmp/awf-chroot-* directories found" + fi +else + echo "Warning: unable to inspect /tmp/awf-chroot-* directories with sudo" >&2 +fi diff --git a/actions/setup/js/chroot_home_cleanup.test.js b/actions/setup/js/chroot_home_cleanup.test.js index 77e15492ad0..170fbed4e36 100644 --- a/actions/setup/js/chroot_home_cleanup.test.js +++ b/actions/setup/js/chroot_home_cleanup.test.js @@ -9,6 +9,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url)); const POST_SCRIPT_PATH = path.join(__dirname, "..", "post.js"); const CLEAN_SCRIPT_PATH = path.join(__dirname, "..", "clean.sh"); const INSTALL_COPILOT_CLI_SCRIPT_PATH = path.join(__dirname, "..", "sh", "install_copilot_cli.sh"); +const INSTALL_AWF_BINARY_SCRIPT_PATH = path.join(__dirname, "..", "sh", "install_awf_binary.sh"); const tempDirs = []; @@ -100,6 +101,19 @@ describe("post.js chroot-home cleanup", () => { expect(result.stdout).toContain("Cleaned up 2 /tmp/awf-*-chroot-home directories"); expect(fs.readFileSync(logPath, "utf8")).toContain("-exec rm -rf -- {} +"); }); + + it("logs count of cleaned chroot directories", () => { + const { fakeBin, logPath } = createFakeSudoEnvironment(); + const result = runPostScript({ + PATH: `${fakeBin}:${process.env.PATH}`, + FAKE_SUDO_LOG: logPath, + FAKE_FIND_PRINT_OUTPUT: "/tmp/awf-chroot-a\n/tmp/awf-chroot-b\n", + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("Cleaned up 2 /tmp/awf-chroot-* directories"); + expect(fs.readFileSync(logPath, "utf8")).toContain("-name awf-chroot-* -type d -exec rm -rf -- {} +"); + }); }); describe("clean.sh chroot-home cleanup", () => { @@ -136,6 +150,23 @@ describe("clean.sh chroot-home cleanup", () => { expect(result.stdout).toContain("Cleaned up /tmp/awf-*-chroot-home directories (sudo)"); expect(fs.readFileSync(logPath, "utf8")).toContain("-exec rm -rf -- {} +"); }); + + it("logs successful cleanup when chroot directories are found", () => { + const { fakeBin, logPath, root } = createFakeSudoEnvironment(); + const destination = path.join(root, "destination"); + fs.mkdirSync(destination, { recursive: true }); + + const result = runCleanScript({ + PATH: `${fakeBin}:${process.env.PATH}`, + FAKE_SUDO_LOG: logPath, + FAKE_FIND_PRINT_OUTPUT: "/tmp/awf-chroot-a\n", + INPUT_DESTINATION: destination, + }); + + expect(result.status).toBe(0); + expect(result.stdout).toContain("Cleaned up /tmp/awf-chroot-* directories (sudo)"); + expect(fs.readFileSync(logPath, "utf8")).toContain("-name awf-chroot-* -type d -exec rm -rf -- {} +"); + }); }); describe("install_copilot_cli.sh chroot-home cleanup", () => { @@ -143,13 +174,43 @@ describe("install_copilot_cli.sh chroot-home cleanup", () => { const script = fs.readFileSync(INSTALL_COPILOT_CLI_SCRIPT_PATH, "utf8"); const ownershipFixIndex = script.indexOf('sudo chown -R "$(id -u):$(id -g)" "$COPILOT_DIR"'); - const cleanupBannerIndex = script.indexOf('echo "Cleaning up stale AWF chroot home directories..."'); + const cleanupBannerIndex = script.indexOf('echo "Cleaning up stale AWF chroot directories..."'); const cleanupCommandIndex = script.indexOf( "sudo find /tmp -maxdepth 1 -name 'awf-*-chroot-home' -type d -exec rm -rf -- {} + 2>/dev/null || true" ); + const cleanupChrootCommandIndex = script.indexOf( + "sudo find /tmp -maxdepth 1 -name 'awf-chroot-*' -type d -exec rm -rf -- {} + 2>/dev/null || true" + ); expect(ownershipFixIndex).toBeGreaterThanOrEqual(0); expect(cleanupBannerIndex).toBeGreaterThan(ownershipFixIndex); expect(cleanupCommandIndex).toBeGreaterThan(cleanupBannerIndex); + expect(cleanupChrootCommandIndex).toBeGreaterThan(cleanupCommandIndex); + }); +}); + +describe("install_awf_binary.sh chroot-home cleanup", () => { + it("cleans stale chroot directories before starting AWF installation", () => { + const script = fs.readFileSync(INSTALL_AWF_BINARY_SCRIPT_PATH, "utf8"); + + const rootlessPreflightIndex = script.indexOf('if ! { mkdir -p "${AWF_INSTALL_DIR}" && [ -w "${AWF_INSTALL_DIR}" ]; }; then'); + const cleanupBannerIndex = script.indexOf('echo "Cleaning up stale AWF chroot directories..."'); + const sudoGuardIndex = script.indexOf("if command -v sudo >/dev/null 2>&1; then"); + const cleanupHomeCommandIndex = script.indexOf( + `sudo find /tmp -maxdepth 1 -name 'awf-*-chroot-home' -type d \\( -user root -o -user "$(id -un)" \\) -exec rm -rf -- {} + || true` + ); + const cleanupChrootCommandIndex = script.indexOf( + `sudo find /tmp -maxdepth 1 -name 'awf-chroot-*' -type d \\( -user root -o -user "$(id -un)" \\) -exec rm -rf -- {} + || true` + ); + const sudoWarningIndex = script.indexOf('echo "Warning: sudo is unavailable; skipping stale AWF chroot cleanup" >&2'); + const downloadUrlIndex = script.indexOf("# Download URLs"); + + expect(rootlessPreflightIndex).toBeGreaterThanOrEqual(0); + expect(cleanupBannerIndex).toBeGreaterThan(rootlessPreflightIndex); + expect(sudoGuardIndex).toBeGreaterThan(cleanupBannerIndex); + expect(cleanupHomeCommandIndex).toBeGreaterThan(sudoGuardIndex); + expect(cleanupChrootCommandIndex).toBeGreaterThan(cleanupHomeCommandIndex); + expect(sudoWarningIndex).toBeGreaterThan(cleanupChrootCommandIndex); + expect(downloadUrlIndex).toBeGreaterThan(cleanupChrootCommandIndex); }); }); diff --git a/actions/setup/js/git_helpers.cjs b/actions/setup/js/git_helpers.cjs index 66f711d2977..ecd14f41291 100644 --- a/actions/setup/js/git_helpers.cjs +++ b/actions/setup/js/git_helpers.cjs @@ -37,6 +37,54 @@ function getGitAuthEnv(token) { }; } +/** + * Ensure the given directory is trusted for git in the current process context. + * Injects safe.directory via GIT_CONFIG_COUNT/KEY/VALUE env vars so that all + * subsequent git commands in this process inherit the trust without writing to + * ~/.gitconfig (no persistent global git config side effects). + * + * This is specifically needed for the "bridge" path (the Process Safe Outputs step + * running outside the Docker container) where HOME or the uid may differ from the + * in-container user that originally configured safe.directory in ~/.gitconfig. + * + * GIT_CONFIG_COUNT/KEY/VALUE is supported since git 2.31. + * + * @param {string} gitCwd - The directory to trust (e.g. GITHUB_WORKSPACE or a repo checkout path) + * @param {Object|Function} [logger] - Optional logger object + * with a debug method (for example the MCP server) or a direct debug function. + * @returns {void} + */ +function ensureSafeDirectoryTrust(gitCwd, logger) { + if (!gitCwd) return; + + // Check if gitCwd is already present in the injected env-var config to avoid + // duplicate entries when the handler is called more than once in the same process. + // Malformed pre-existing GIT_CONFIG_COUNT values (NaN, negative, fractional, + // or unsafe integers) fall back to 0 to avoid corrupting the env-var config chain. + const rawCount = process.env.GIT_CONFIG_COUNT || "0"; + const parsedCount = Number(rawCount); + const existingCount = Number.isSafeInteger(parsedCount) && parsedCount >= 0 ? parsedCount : 0; + for (let i = 0; i < existingCount; i++) { + if (process.env[`GIT_CONFIG_KEY_${i}`] === "safe.directory" && process.env[`GIT_CONFIG_VALUE_${i}`] === gitCwd) { + return; + } + } + + const idx = existingCount; + process.env.GIT_CONFIG_COUNT = String(existingCount + 1); + process.env[`GIT_CONFIG_KEY_${idx}`] = "safe.directory"; + process.env[`GIT_CONFIG_VALUE_${idx}`] = gitCwd; + let debug; + if (typeof logger === "function") { + debug = logger; + } else if (typeof logger?.debug === "function") { + debug = logger.debug.bind(logger); + } else if (typeof globalThis.core?.debug === "function") { + debug = globalThis.core.debug.bind(globalThis.core); + } + debug?.(`Configured git safe.directory for bridge context: ${gitCwd}`); +} + /** * Safely execute git command using spawnSync with args array to prevent shell injection. * @@ -603,6 +651,7 @@ async function linearizeRangeAsCommit(baseRef, commitMessage, execApi, opts = {} } module.exports = { + ensureSafeDirectoryTrust, execGitSync, backfillCommitObjects, ensureFullHistoryForBundle, diff --git a/actions/setup/js/git_helpers.test.cjs b/actions/setup/js/git_helpers.test.cjs index 6f4035901a1..aa6574b8bb1 100644 --- a/actions/setup/js/git_helpers.test.cjs +++ b/actions/setup/js/git_helpers.test.cjs @@ -282,6 +282,156 @@ describe("git_helpers.cjs", () => { }); }); + describe("ensureSafeDirectoryTrust", () => { + let originalEnv; + + beforeEach(() => { + originalEnv = { ...process.env }; + // Clean up GIT_CONFIG_* vars injected by a previous test + for (const key of Object.keys(process.env)) { + if (key.startsWith("GIT_CONFIG_")) { + delete process.env[key]; + } + } + }); + + afterEach(() => { + // Restore to original, removing any vars added during the test + for (const key of Object.keys(process.env)) { + if (!(key in originalEnv)) { + delete process.env[key]; + } + } + Object.assign(process.env, originalEnv); + }); + + it("should export ensureSafeDirectoryTrust function", async () => { + const { ensureSafeDirectoryTrust } = await import("./git_helpers.cjs"); + expect(typeof ensureSafeDirectoryTrust).toBe("function"); + }); + + it("should set GIT_CONFIG_* env vars for the given directory", async () => { + const { ensureSafeDirectoryTrust } = await import("./git_helpers.cjs"); + + ensureSafeDirectoryTrust("/workspace/repo"); + + expect(process.env.GIT_CONFIG_COUNT).toBe("1"); + expect(process.env.GIT_CONFIG_KEY_0).toBe("safe.directory"); + expect(process.env.GIT_CONFIG_VALUE_0).toBe("/workspace/repo"); + }); + + it("should not add a duplicate entry when called twice with the same directory", async () => { + const { ensureSafeDirectoryTrust } = await import("./git_helpers.cjs"); + + ensureSafeDirectoryTrust("/workspace/repo"); + ensureSafeDirectoryTrust("/workspace/repo"); + + expect(process.env.GIT_CONFIG_COUNT).toBe("1"); + }); + + it("should append a new entry when called with a different directory", async () => { + const { ensureSafeDirectoryTrust } = await import("./git_helpers.cjs"); + + ensureSafeDirectoryTrust("/workspace/repo-a"); + ensureSafeDirectoryTrust("/workspace/repo-b"); + + expect(process.env.GIT_CONFIG_COUNT).toBe("2"); + expect(process.env.GIT_CONFIG_KEY_0).toBe("safe.directory"); + expect(process.env.GIT_CONFIG_VALUE_0).toBe("/workspace/repo-a"); + expect(process.env.GIT_CONFIG_KEY_1).toBe("safe.directory"); + expect(process.env.GIT_CONFIG_VALUE_1).toBe("/workspace/repo-b"); + }); + + it("should be a no-op when called with an empty string", async () => { + const { ensureSafeDirectoryTrust } = await import("./git_helpers.cjs"); + + ensureSafeDirectoryTrust(""); + + expect(process.env.GIT_CONFIG_COUNT).toBeUndefined(); + }); + + it("should be a no-op when called with undefined/falsy", async () => { + const { ensureSafeDirectoryTrust } = await import("./git_helpers.cjs"); + + ensureSafeDirectoryTrust(undefined); + + expect(process.env.GIT_CONFIG_COUNT).toBeUndefined(); + }); + + it("should preserve existing GIT_CONFIG_* entries set by getGitAuthEnv", async () => { + const { ensureSafeDirectoryTrust, getGitAuthEnv } = await import("./git_helpers.cjs"); + + // Simulate what getGitAuthEnv returns being already applied via env + const authEnv = getGitAuthEnv("test-token"); + Object.assign(process.env, authEnv); + const existingConfigCount = parseInt(authEnv.GIT_CONFIG_COUNT, 10); + + ensureSafeDirectoryTrust("/workspace/repo"); + + // The count should be incremented by 1. + expect(parseInt(process.env.GIT_CONFIG_COUNT, 10)).toBe(existingConfigCount + 1); + // Existing auth entries preserved + for (let i = 0; i < existingConfigCount; i++) { + expect(process.env[`GIT_CONFIG_KEY_${i}`]).toBe(authEnv[`GIT_CONFIG_KEY_${i}`]); + expect(process.env[`GIT_CONFIG_VALUE_${i}`]).toBe(authEnv[`GIT_CONFIG_VALUE_${i}`]); + } + // New safe.directory entry appended + expect(process.env[`GIT_CONFIG_KEY_${existingConfigCount}`]).toBe("safe.directory"); + expect(process.env[`GIT_CONFIG_VALUE_${existingConfigCount}`]).toBe("/workspace/repo"); + }); + + it("should handle malformed GIT_CONFIG_COUNT values gracefully", async () => { + const { ensureSafeDirectoryTrust } = await import("./git_helpers.cjs"); + + for (const malformedCount of ["not-a-number", "-1", "1.5", String(Number.MAX_SAFE_INTEGER + 1)]) { + for (const key of Object.keys(process.env)) { + if (key.startsWith("GIT_CONFIG_")) { + delete process.env[key]; + } + } + + process.env.GIT_CONFIG_COUNT = malformedCount; + + ensureSafeDirectoryTrust("/workspace/repo"); + + expect(process.env.GIT_CONFIG_COUNT).toBe("1"); + expect(process.env.GIT_CONFIG_KEY_0).toBe("safe.directory"); + expect(process.env.GIT_CONFIG_VALUE_0).toBe("/workspace/repo"); + } + }); + + it("should not require a shimmed core global", async () => { + const { ensureSafeDirectoryTrust } = await import("./git_helpers.cjs"); + const originalCore = global.core; + + global.core = undefined; + + try { + expect(() => ensureSafeDirectoryTrust("/workspace/repo")).not.toThrow(); + expect(process.env.GIT_CONFIG_COUNT).toBe("1"); + expect(process.env.GIT_CONFIG_KEY_0).toBe("safe.directory"); + expect(process.env.GIT_CONFIG_VALUE_0).toBe("/workspace/repo"); + } finally { + global.core = originalCore; + } + }); + + it("should use a provided logger when core is not shimmed", async () => { + const { ensureSafeDirectoryTrust } = await import("./git_helpers.cjs"); + const originalCore = global.core; + const logger = { debug: vi.fn() }; + + global.core = undefined; + + try { + ensureSafeDirectoryTrust("/workspace/repo", logger); + expect(logger.debug).toHaveBeenCalledWith("Configured git safe.directory for bridge context: /workspace/repo"); + } finally { + global.core = originalCore; + } + }); + }); + describe("ensureFullHistoryForBundle", () => { it("should unshallow the repository when the repository is shallow", async () => { const { ensureFullHistoryForBundle } = await import("./git_helpers.cjs"); diff --git a/actions/setup/js/push_to_pull_request_branch.cjs b/actions/setup/js/push_to_pull_request_branch.cjs index 3e38f3055fd..ea580ad1925 100644 --- a/actions/setup/js/push_to_pull_request_branch.cjs +++ b/actions/setup/js/push_to_pull_request_branch.cjs @@ -18,7 +18,7 @@ const { checkFileProtection, checkFileProtectionPostApply } = require("./manifes const { buildWorkflowRunUrl } = require("./workflow_metadata_helpers.cjs"); const { renderTemplateFromFile, buildProtectedFileList, getPromptPath } = require("./messages_core.cjs"); const { overridePersistedExtraheader, restorePersistedExtraheader } = require("./git_auth_helpers.cjs"); -const { ensureFullHistoryForBundle, extractBundlePrerequisiteCommits, isShallowOrSparseCheckout, linearizeRangeAsCommit } = require("./git_helpers.cjs"); +const { ensureFullHistoryForBundle, extractBundlePrerequisiteCommits, isShallowOrSparseCheckout, linearizeRangeAsCommit, ensureSafeDirectoryTrust } = require("./git_helpers.cjs"); const { normalizeCommitSHA } = require("./commit_sha_helpers.cjs"); const { findRepoCheckout } = require("./find_repo_checkout.cjs"); const { getThreatDetectedMarker } = require("./threat_detection_warning.cjs"); @@ -326,6 +326,13 @@ async function main(config = {}) { const { defaultTargetRepo, allowedRepos } = resolveTargetRepoConfig(config); const githubClient = await createAuthenticatedGitHubClient(config); + // Ensure the workspace is trusted in the bridge process (Process Safe Outputs step). + // The bridge runs outside the Docker container as a potentially different user/HOME, + // so the in-container `git config --global safe.directory` may not be visible here. + // Using GIT_CONFIG_* env vars avoids relying on ~/.gitconfig and covers cases where + // the conditional "Configure Git credentials" step was skipped or HOME differs. + ensureSafeDirectoryTrust(process.env.GITHUB_WORKSPACE || process.cwd()); + // Git network operations authenticate using the credentials actions/checkout // persisted into .git/config for the safe_outputs job (persist-credentials: true, // using the resolved push token). We intentionally do NOT inject an additional @@ -677,6 +684,12 @@ async function main(config = {}) { // Base options for all git exec calls - includes cwd when running in a subdirectory checkout const baseGitOpts = repoCwd ? { cwd: repoCwd } : {}; + + // For cross-repo checkouts, also trust the specific subdirectory. The factory-level call + // covers GITHUB_WORKSPACE; this per-message call covers subdirectory checkout paths. + if (repoCwd) { + ensureSafeDirectoryTrust(repoCwd); + } let pullRequest; try { const response = await githubClient.rest.pulls.get({ @@ -1116,7 +1129,7 @@ async function main(config = {}) { } catch { // Ignore } - return { success: false, error: "Failed to apply bundle" }; + return { success: false, error: `Failed to apply bundle: ${getErrorMessage(bundleError)}` }; } } else { // Patch transport (non-default): git am --3way diff --git a/actions/setup/js/safe_outputs_handlers.cjs b/actions/setup/js/safe_outputs_handlers.cjs index b05eb98f4cb..eac9c107b6b 100644 --- a/actions/setup/js/safe_outputs_handlers.cjs +++ b/actions/setup/js/safe_outputs_handlers.cjs @@ -13,7 +13,7 @@ const { getBaseBranch } = require("./get_base_branch.cjs"); const { lookupCheckout } = require("./checkout_manifest.cjs"); const { generateGitPatch } = require("./generate_git_patch.cjs"); const { generateGitBundle } = require("./generate_git_bundle.cjs"); -const { hasMergeCommitsInRange, execGitSync } = require("./git_helpers.cjs"); +const { hasMergeCommitsInRange, execGitSync, ensureSafeDirectoryTrust } = require("./git_helpers.cjs"); const { enforceCommentLimits } = require("./comment_limit_helpers.cjs"); const { getErrorMessage } = require("./error_helpers.cjs"); const { ERR_CONFIG, ERR_SYSTEM, ERR_VALIDATION } = require("./error_codes.cjs"); @@ -200,39 +200,6 @@ function resolvePatchWorkspacePath(workspacePath) { return { success: true, absolutePath: resolved }; } -/** - * Ensure the current git checkout path is trusted in this process context. - * Injects safe.directory via GIT_CONFIG_COUNT/KEY/VALUE env vars so that all - * subsequent git commands in this process inherit the trust without writing to - * ~/.gitconfig (no persistent global git config side effects). - * - * GIT_CONFIG_COUNT/KEY/VALUE is supported since git 2.31. - * - * @param {string} gitCwd - * @param {{ debug: (message: string) => void }} server - * @returns {void} - */ -function ensureSafeDirectoryTrust(gitCwd, server) { - if (!gitCwd) return; - - // Check if gitCwd is already present in the injected env-var config to avoid - // duplicate entries when the handler is called more than once in the same process. - // The `|| 0` guard converts NaN (from a malformed pre-existing GIT_CONFIG_COUNT) to 0, - // preventing GIT_CONFIG_KEY_NaN/VALUE_NaN entries that would corrupt the env-var config chain. - const existingCount = parseInt(process.env.GIT_CONFIG_COUNT || "0", 10) || 0; - for (let i = 0; i < existingCount; i++) { - if (process.env[`GIT_CONFIG_KEY_${i}`] === "safe.directory" && process.env[`GIT_CONFIG_VALUE_${i}`] === gitCwd) { - return; - } - } - - const idx = existingCount; - process.env.GIT_CONFIG_COUNT = String(existingCount + 1); - process.env[`GIT_CONFIG_KEY_${idx}`] = "safe.directory"; - process.env[`GIT_CONFIG_VALUE_${idx}`] = gitCwd; - server.debug(`Configured git safe.directory for bridge context: ${gitCwd}`); -} - /** * Create handlers for safe output tools * @param {Object} server - The MCP server instance for logging diff --git a/actions/setup/post.js b/actions/setup/post.js index c20bf339eda..89bac6b41d9 100644 --- a/actions/setup/post.js +++ b/actions/setup/post.js @@ -120,51 +120,37 @@ function listTmpGhAwFiles(tmpDir, maxDepth, maxFiles) { } } - // Clean up AWF chroot home directories under /tmp (e.g. /tmp/awf-*-chroot-home). - // These are created by AWF when running with --enable-host-access on GitHub-hosted runners. - // Files inside may be owned by root (written by Docker containers or privileged AWF processes), + // Clean up AWF chroot directories under /tmp (e.g. /tmp/awf-*-chroot-home + // and /tmp/awf-chroot-*). These are created by AWF when running with + // --enable-host-access on GitHub-hosted runners. Files inside may be owned + // by root (written by Docker containers or privileged AWF processes), // causing EACCES failures if cleanup is attempted without sudo. - const awfChrootHomeFindResult = spawnSync( - "sudo", - ["find", "/tmp", "-maxdepth", "1", "-name", "awf-*-chroot-home", "-type", "d", "-print"], - { encoding: "utf8" } - ); - if (awfChrootHomeFindResult.status !== 0) { - console.log("Failed to inspect /tmp/awf-*-chroot-home directories"); - } else { - const awfChrootHomeDirs = awfChrootHomeFindResult.stdout + for (const pattern of ["awf-*-chroot-home", "awf-chroot-*"]) { + const awfChrootFindResult = spawnSync("sudo", ["find", "/tmp", "-maxdepth", "1", "-name", pattern, "-type", "d", "-print"], { encoding: "utf8" }); + if (awfChrootFindResult.status !== 0) { + console.log(`Failed to inspect /tmp/${pattern} directories`); + continue; + } + + const awfChrootDirs = awfChrootFindResult.stdout .split("\n") .map(line => line.trim()) .filter(Boolean); - if (awfChrootHomeDirs.length === 0) { - console.log("No /tmp/awf-*-chroot-home directories found"); + if (awfChrootDirs.length === 0) { + console.log(`No /tmp/${pattern} directories found`); + continue; + } + + const awfChrootCleanupResult = spawnSync( + "sudo", + ["find", "/tmp", "-maxdepth", "1", "-name", pattern, "-type", "d", "-exec", "rm", "-rf", "--", "{}", "+"], + { stdio: "inherit" } + ); + if (awfChrootCleanupResult.status === 0) { + const awfChrootNoun = awfChrootDirs.length === 1 ? "directory" : "directories"; + console.log(`Cleaned up ${awfChrootDirs.length} /tmp/${pattern} ${awfChrootNoun}`); } else { - const awfChrootHomeCleanupResult = spawnSync( - "sudo", - [ - "find", - "/tmp", - "-maxdepth", - "1", - "-name", - "awf-*-chroot-home", - "-type", - "d", - "-exec", - "rm", - "-rf", - "--", - "{}", - "+" - ], - { stdio: "inherit" } - ); - if (awfChrootHomeCleanupResult.status === 0) { - const awfChrootHomeNoun = awfChrootHomeDirs.length === 1 ? "directory" : "directories"; - console.log(`Cleaned up ${awfChrootHomeDirs.length} /tmp/awf-*-chroot-home ${awfChrootHomeNoun}`); - } else { - console.log("Failed to clean /tmp/awf-*-chroot-home directories"); - } + console.log(`Failed to clean /tmp/${pattern} directories`); } } })(); diff --git a/actions/setup/sh/install_awf_binary.sh b/actions/setup/sh/install_awf_binary.sh index ff642fc0bbc..c62afcab532 100755 --- a/actions/setup/sh/install_awf_binary.sh +++ b/actions/setup/sh/install_awf_binary.sh @@ -85,6 +85,20 @@ if [ "$ROOTLESS" = "true" ]; then fi fi +# Clean up any stale AWF chroot directories left by previous runs before we +# execute awf. Rootless AWF can fail its writeConfigs startup path with EACCES +# when stale /tmp/awf-*-chroot-home or /tmp/awf-chroot-* directories remain +# from earlier jobs on the same runner. Limit cleanup to directories owned by +# root or the current runner user so we do not delete unrelated /tmp entries +# that happen to match the name pattern. +echo "Cleaning up stale AWF chroot directories..." +if command -v sudo >/dev/null 2>&1; then + sudo find /tmp -maxdepth 1 -name 'awf-*-chroot-home' -type d \( -user root -o -user "$(id -un)" \) -exec rm -rf -- {} + || true + sudo find /tmp -maxdepth 1 -name 'awf-chroot-*' -type d \( -user root -o -user "$(id -un)" \) -exec rm -rf -- {} + || true +else + echo "Warning: sudo is unavailable; skipping stale AWF chroot cleanup" >&2 +fi + # Download URLs BASE_URL="https://github.com/${AWF_REPO}/releases/download/${AWF_VERSION}" CHECKSUMS_URL="${BASE_URL}/checksums.txt" diff --git a/actions/setup/sh/install_copilot_cli.sh b/actions/setup/sh/install_copilot_cli.sh index d3ba2452ee7..191d0e79f87 100755 --- a/actions/setup/sh/install_copilot_cli.sh +++ b/actions/setup/sh/install_copilot_cli.sh @@ -43,14 +43,16 @@ echo "Ensuring correct ownership of $COPILOT_DIR..." mkdir -p "$COPILOT_DIR" sudo chown -R "$(id -u):$(id -g)" "$COPILOT_DIR" -# Clean up any stale AWF chroot home directories left by previous runs. +# Clean up any stale AWF chroot directories left by previous runs. # When AWF ran with `sudo -E awf --enable-host-access`, it created -# /tmp/awf-*-chroot-home directories with root-owned files. These cause -# EACCES failures in the Copilot CLI cleanup path (rimrafSync) on the same or -# subsequent runs, which reports as "engine terminated unexpectedly". +# /tmp/awf-*-chroot-home and /tmp/awf-chroot-* directories with root-owned +# files. These cause EACCES failures in the Copilot CLI cleanup path +# (rimrafSync) or AWF writeConfigs on the same or subsequent runs, which +# reports as "engine terminated unexpectedly" or fatal EACCES errors. # Remove them here before the agent starts so the runner is in a clean state. -echo "Cleaning up stale AWF chroot home directories..." +echo "Cleaning up stale AWF chroot directories..." sudo find /tmp -maxdepth 1 -name 'awf-*-chroot-home' -type d -exec rm -rf -- {} + 2>/dev/null || true +sudo find /tmp -maxdepth 1 -name 'awf-chroot-*' -type d -exec rm -rf -- {} + 2>/dev/null || true # Detect OS and architecture OS="$(uname -s)"