Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions setup/clean.sh
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,21 @@ if [ -d "${tmpDir}" ]; then
echo "Warning: failed to clean up ${tmpDir}" >&2
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.
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
echo "Cleaned up /tmp/awf-*-chroot-home directories (sudo)"
else
echo "Warning: failed to clean /tmp/awf-*-chroot-home directories" >&2
fi
else
echo "No /tmp/awf-*-chroot-home directories found"
fi
else
echo "Warning: unable to inspect /tmp/awf-*-chroot-home directories with sudo" >&2
fi
26 changes: 22 additions & 4 deletions setup/js/add_workflow_run_comment.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,18 @@ function reportCommentError(rawContext, message) {
core.setFailed(message);
}

/**
* @param {Record<string, any>|null} awContext
* @param {string} key
* @returns {string}
*/
function readAwContextString(awContext, key) {
if (!awContext || typeof awContext[key] !== "string") {
return "";
}
return awContext[key].trim();
}

/**
* @param {ReusableStatusComment} reusableComment
* @param {{
Expand All @@ -184,8 +196,11 @@ function reportCommentError(rawContext, message) {
* @returns {Promise<string>}
*/
async function updateReusableStatusComment(reusableComment, invocationContext, rawContext) {
const runUrl = buildWorkflowRunUrl(rawContext, invocationContext.workflowRepo);
const commentBody = buildCommentBody(invocationContext.eventName, runUrl);
const awContext = extractAwContextFromPayload(rawContext?.payload);
const dispatchedRunUrl = readAwContextString(awContext, "dispatched_run_url");
const dispatchedWorkflowName = readAwContextString(awContext, "dispatched_workflow_name");
const runUrl = dispatchedRunUrl || buildWorkflowRunUrl(rawContext, invocationContext.workflowRepo);
const commentBody = buildCommentBody(invocationContext.eventName, runUrl, dispatchedWorkflowName || undefined);

// Discussion comments use GraphQL node IDs and a dedicated update mutation.
if (reusableComment.id.startsWith("DC_")) {
Expand Down Expand Up @@ -337,10 +352,13 @@ async function main() {
* Sanitizes the content and appends all required markers.
* @param {string} eventName - The event type
* @param {string} runUrl - The URL of the workflow run
* @param {string} [workflowNameOverride] - Optional dispatched workflow name override
* @returns {string} The assembled comment body
*/
function buildCommentBody(eventName, runUrl) {
const workflowName = process.env.GH_AW_WORKFLOW_NAME || process.env.GITHUB_WORKFLOW || "Workflow";
function buildCommentBody(eventName, runUrl, workflowNameOverride) {
// Whitespace-only overrides are treated as absent and fall back to env defaults.
const normalizedWorkflowNameOverride = workflowNameOverride?.trim();
const workflowName = normalizedWorkflowNameOverride || process.env.GH_AW_WORKFLOW_NAME || process.env.GITHUB_WORKFLOW || "Workflow";
const eventTypeDescription = EVENT_TYPE_DESCRIPTIONS[eventName] ?? "event";

// Sanitize before adding markers (defense in depth for custom message templates)
Expand Down
43 changes: 20 additions & 23 deletions setup/js/ai_credits_context.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -235,10 +235,7 @@ function parseMaxAICreditsExceededFromAuditEntry(entry) {
if (key === "reason") reason = value;
if (key === "forced_termination") forcedTermination = value;
}
if (typeof event === "string" && event === BUDGET_EXCEEDED_EVENT && typeof reason === "string" && reason === "hard_limit" && isTrueLike(forcedTermination)) {
return true;
}
return false;
return typeof event === "string" && event === BUDGET_EXCEEDED_EVENT && typeof reason === "string" && reason === "hard_limit" && isTrueLike(forcedTermination);
}

/**
Expand Down Expand Up @@ -315,6 +312,23 @@ function parseAuditLogCombined(auditJsonlPathOverride) {
});
}

/**
* Logs the provenance source and value for a single AI credits field.
* Outputs exactly one line regardless of which source resolved the value.
*
* @param {string} label
* @param {string} auditValue
* @param {string} stdioValue
* @param {string} envValue
* @param {string} envVarName
*/
function logAICreditSource(label, auditValue, stdioValue, envValue, envVarName) {
if (auditValue) console.log(`[ai-credits] ${label} source=audit_log value=${auditValue}`);
else if (stdioValue) console.log(`[ai-credits] ${label} source=agent_stdio value=${stdioValue}`);
else if (envValue) console.log(`[ai-credits] ${label} source=env(${envVarName}) value=${envValue}`);
else console.log(`[ai-credits] ${label} source=none ${envVarName}=${process.env[envVarName] || "(unset)"}`);
}

/**
* @param {{ logProvenance?: boolean }} [options]
* @returns {{ aiCredits: string, maxAICredits: string, aiCreditsRateLimitError: boolean, maxAICreditsExceeded: boolean }}
Expand All @@ -329,25 +343,8 @@ function resolveAICreditsFailureState({ logProvenance = true } = {}) {

// Log provenance so failing issues can be diagnosed when credit data is missing.
if (logProvenance) {
if (auditAICredits) {
console.log(`[ai-credits] aiCredits source=audit_log value=${auditAICredits}`);
} else if (stdioSignals.aiCredits) {
console.log(`[ai-credits] aiCredits source=agent_stdio value=${stdioSignals.aiCredits}`);
} else if (envAICredits) {
console.log(`[ai-credits] aiCredits source=env(GH_AW_AIC) value=${envAICredits}`);
} else {
console.log(`[ai-credits] aiCredits source=none GH_AW_AIC=${process.env.GH_AW_AIC || "(unset)"}`);
}

if (auditMaxAICredits) {
console.log(`[ai-credits] maxAICredits source=audit_log value=${auditMaxAICredits}`);
} else if (stdioSignals.maxAICredits) {
console.log(`[ai-credits] maxAICredits source=agent_stdio value=${stdioSignals.maxAICredits}`);
} else if (envMaxAICredits) {
console.log(`[ai-credits] maxAICredits source=env(GH_AW_MAX_AI_CREDITS) value=${envMaxAICredits}`);
} else {
console.log(`[ai-credits] maxAICredits source=none GH_AW_MAX_AI_CREDITS=${process.env.GH_AW_MAX_AI_CREDITS || "(unset)"}`);
}
logAICreditSource("aiCredits", auditAICredits, stdioSignals.aiCredits, envAICredits, "GH_AW_AIC");
logAICreditSource("maxAICredits", auditMaxAICredits, stdioSignals.maxAICredits, envMaxAICredits, "GH_AW_MAX_AI_CREDITS");

const rawRateLimitSignalSource = auditRateLimitError
? "audit_log"
Expand Down
17 changes: 13 additions & 4 deletions setup/js/apply_samples.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -223,10 +223,19 @@ async function derivePrHeadRef(entry) {
return directRef.trim();
}

// Determine the target repo for any API lookups. Prefer the entry's repo if
// the sample sets one (cross-repo workflows), otherwise fall back to
// GITHUB_REPOSITORY.
const repoSlug = (typeof entry.arguments.repo === "string" && entry.arguments.repo.trim()) || process.env.GITHUB_REPOSITORY || "";
// Determine the target repo for any API lookups.
// Resolution order:
// a. entry.arguments.repo — explicit per-sample override (cross-repo workflows).
// b. target-repo from the safe-outputs config file (GH_AW_SAFE_OUTPUTS_CONFIG_PATH)
// for the tool — covers siderepo workflow_dispatch where the sample arguments
// carry `pull_request_number` but not a `repo` override (issue #41292).
// c. GITHUB_REPOSITORY — host repo fallback.
let repoSlug = "";
if (typeof entry.arguments.repo === "string" && entry.arguments.repo.trim()) {
repoSlug = entry.arguments.repo.trim();
} else {
repoSlug = readConfiguredTargetRepo(entry.tool) || process.env.GITHUB_REPOSITORY || "";
}
const [owner, repo] = repoSlug.split("/");
if (!owner || !repo) return null;

Expand Down
138 changes: 138 additions & 0 deletions setup/js/chroot_home_cleanup.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { afterEach, describe, expect, it } from "vitest";
import fs from "fs";
import os from "os";
import path from "path";
import { spawnSync } from "child_process";
import { fileURLToPath } from "url";

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 tempDirs = [];

function createTempDir(prefix) {
const dir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
tempDirs.push(dir);
return dir;
}

function createFakeSudoEnvironment() {
const root = createTempDir("chroot-home-cleanup-");
const fakeBin = path.join(root, "fake-bin");
fs.mkdirSync(fakeBin, { recursive: true });

const logPath = path.join(root, "sudo.log");
const fakeSudoPath = path.join(fakeBin, "sudo");
fs.writeFileSync(
fakeSudoPath,
`#!/usr/bin/env bash
set -euo pipefail
echo "$*" >> "$FAKE_SUDO_LOG"
if [ "$1" = "find" ]; then
if printf '%s\\n' "$*" | grep -q -- '-print'; then
printf '%b' "\${FAKE_FIND_PRINT_OUTPUT:-}"
exit "\${FAKE_FIND_PRINT_STATUS:-0}"
fi
if printf '%s\\n' "$*" | grep -q -- '-exec'; then
exit "\${FAKE_FIND_EXEC_STATUS:-0}"
fi
fi
exit 0
`,
{ mode: 0o755 }
);

return {
fakeBin,
logPath,
root,
};
}

function runPostScript(env) {
return spawnSync(process.execPath, [POST_SCRIPT_PATH], {
encoding: "utf8",
env: { ...process.env, ...env },
});
}

function runCleanScript(env) {
return spawnSync("bash", [CLEAN_SCRIPT_PATH], {
encoding: "utf8",
env: { ...process.env, ...env },
});
}

afterEach(() => {
while (tempDirs.length > 0) {
const dir = tempDirs.pop();
if (dir && fs.existsSync(dir)) {
fs.rmSync(dir, { recursive: true, force: true });
}
}
});

describe("post.js chroot-home cleanup", () => {
it("logs that no directories were found when find output is empty", () => {
const { fakeBin, logPath } = createFakeSudoEnvironment();
const result = runPostScript({
PATH: `${fakeBin}:${process.env.PATH}`,
FAKE_SUDO_LOG: logPath,
FAKE_FIND_PRINT_OUTPUT: "",
});

expect(result.status).toBe(0);
expect(result.stdout).toContain("No /tmp/awf-*-chroot-home directories found");
expect(fs.readFileSync(logPath, "utf8")).not.toContain("-exec rm -rf -- {} +");
});

it("logs count of cleaned chroot-home directories", () => {
const { fakeBin, logPath } = createFakeSudoEnvironment();
const result = runPostScript({
PATH: `${fakeBin}:${process.env.PATH}`,
FAKE_SUDO_LOG: logPath,
FAKE_FIND_PRINT_OUTPUT: "/tmp/awf-1-chroot-home\n/tmp/awf-2-chroot-home\n",
});

expect(result.status).toBe(0);
expect(result.stdout).toContain("Cleaned up 2 /tmp/awf-*-chroot-home directories");
expect(fs.readFileSync(logPath, "utf8")).toContain("-exec rm -rf -- {} +");
});
});

describe("clean.sh chroot-home cleanup", () => {
it("logs when no chroot-home 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: "",
INPUT_DESTINATION: destination,
});

expect(result.status).toBe(0);
expect(result.stdout).toContain("No /tmp/awf-*-chroot-home directories found");
expect(fs.readFileSync(logPath, "utf8")).not.toContain("-exec rm -rf -- {} +");
});

it("logs successful cleanup when chroot-home 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-1-chroot-home\n",
INPUT_DESTINATION: destination,
});

expect(result.status).toBe(0);
expect(result.stdout).toContain("Cleaned up /tmp/awf-*-chroot-home directories (sudo)");
expect(fs.readFileSync(logPath, "utf8")).toContain("-exec rm -rf -- {} +");
});
});
Loading
Loading