mirror of
https://github.com/openclaw/openclaw.git
synced 2026-10-03 09:39:25 +00:00
* fix(test): checkout fixture budgets cut healthy runs on loaded hosts
On a loaded macOS host (load 55-83), ci-platform-checkout.test.ts and the
other checkout fixture consumers failed through three fixture-internal
wall-clock budgets, not through the workflow under test:
- the supervisor's mock-resolution preflight ran bash under its own 2 s
timeout ("mock command resolution failed: spawnSync bash ETIMEDOUT");
- the POSIX process census shadowed its caller's operation deadline with a
fresh 1 s budget for all singleton ps queries ("Fixture process census
failed (ETIMEDOUT)"); the owner-ancestry walk had the same 1 s shape;
- the supervisor cut every run at 45 s ("fixture deadline exceeded"), nested
under the helper's 50 s close race only because the helper could not see
the Vitest test timeout.
Measured breakdown: a multi-attempt scenario serially starts 25-36 Node
actors plus the Python owner; at load ~72 Linux git-failure spent 19.4 s,
of which the fixture's own 82 ps calls were 1.6 s. The work scales with
scheduler latency and is the contract under test, so the fix is ownership
of the time bound, not less work.
The owning test's AbortSignal now bounds a supervised run.
withCiCheckoutFixture takes it, rejects its close join on abort, and asks
the live supervisor to cancel over IPC so its own stop() retires the
detached shell group and actors, keeping the SIGKILL fallback for a wedged
supervisor. The supervisor drops its 45 s watchdog; its operation deadline
becomes the existing 60 s actor lifetime ceiling, which only bounds orphans.
Census, preflight, and ancestry queries borrow that operation deadline, as
the Windows census witness already did.
Every runCiGitStep and withCiCheckoutFixture caller passes its test signal;
.each registrations that need the context move to .for, which is the only
form Vitest 5 hands the context to.
Regressions: the shared slow-witness preload now gives POSIX supervisors a
first native query that spends 1.1 s on their clock (fails main's fixture
7/7 with "census failed (unverified)"), and the outer-runner retention
proof gains a cancel fault proving the aborted supervisor retires its shell.
* fix(test): keep checkout signal bindings lint-clean
649 lines
26 KiB
TypeScript
649 lines
26 KiB
TypeScript
import { execFileSync } from "node:child_process";
|
|
import {
|
|
existsSync,
|
|
mkdirSync,
|
|
readdirSync,
|
|
readFileSync,
|
|
symlinkSync,
|
|
writeFileSync,
|
|
} from "node:fs";
|
|
import path from "node:path";
|
|
import { expect } from "vitest";
|
|
import { parse } from "yaml";
|
|
import {
|
|
ciCheckoutFixture,
|
|
expectCiCheckoutCleanup,
|
|
readCiCheckoutStep,
|
|
renderGitTestClock,
|
|
withCiCheckoutFixture,
|
|
} from "./ci-checkout.test-support.js";
|
|
import {
|
|
prepareGeneratedPublisherFixture,
|
|
type GeneratedPublisherOptions,
|
|
} from "./generated-publisher.test-support.js";
|
|
import {
|
|
preparePerformanceFixture,
|
|
type PerformanceFixtureOptions,
|
|
} from "./openclaw-performance-workflow.test-support.js";
|
|
|
|
type Step = {
|
|
name?: string;
|
|
run?: string;
|
|
env?: Record<string, string | number>;
|
|
"working-directory"?: string;
|
|
};
|
|
type WorkflowTarget = { file: string; job: string; step: string };
|
|
type WorkflowInput = { jobs: Record<string, { steps: Step[] }> };
|
|
type ActionInput = { runs: { steps: (Step & { run: string })[] } };
|
|
export type FetchResult = number | "hang" | "cleanup-failure";
|
|
|
|
const workflowInputs = new Map<string, WorkflowInput>();
|
|
const actionInputs = new Map<string, ActionInput>();
|
|
const ownerSource = readFileSync(".github/actions/git-owner/owner.py", "utf8");
|
|
const basePolicySource = readFileSync(".github/actions/ensure-base-commit/policy.py", "utf8");
|
|
const publisherPolicySource = readFileSync(
|
|
".github/actions/publish-generated-pr/policy.py",
|
|
"utf8",
|
|
);
|
|
|
|
function readActionSteps(action: string) {
|
|
const input =
|
|
actionInputs.get(action) ??
|
|
(parse(readFileSync(`.github/actions/${action}/action.yml`, "utf8")) as ActionInput);
|
|
actionInputs.set(action, input);
|
|
return input.runs.steps;
|
|
}
|
|
|
|
const candidate = "a".repeat(40);
|
|
const harness = "b".repeat(40);
|
|
const base = "c".repeat(40);
|
|
const moved = "d".repeat(40);
|
|
const merge = "e".repeat(40);
|
|
const defaults: Record<string, string> = {
|
|
CHECKOUT_REPO: "fixture/checkout",
|
|
CHECKOUT_TOKEN: "",
|
|
CHECKOUT_REF: candidate,
|
|
CHECKOUT_SHA: candidate,
|
|
CHECKOUT_FALLBACK_REF: candidate,
|
|
CHECKOUT_EVENT_REF: "refs/heads/main",
|
|
WORKFLOW_SHA: harness,
|
|
CHECKOUT_GIT_COMMITS_JSON: "null",
|
|
GITHUB_EVENT_NAME: "push",
|
|
GITHUB_REPOSITORY: "fixture/checkout",
|
|
DEFAULT_BRANCH: "main",
|
|
EVENT_BASE_SHA: base,
|
|
GH_TOKEN: "",
|
|
PULL_REQUEST_NUMBER: "17",
|
|
TARGET_SHA: candidate,
|
|
RELEASE_GATE: "false",
|
|
FROZEN_TARGET: "false",
|
|
HISTORICAL_TARGET: "false",
|
|
FORMAT_CHECK: "false",
|
|
SKIP_NPM_LOCK: "false",
|
|
CHANGED_CORE_TEST_PATHS_JSON: "",
|
|
RUN_CONTROL_UI_I18N: "false",
|
|
RUN_UI_TESTS: "false",
|
|
HOSTED_RUNNER_STRIPES: "false",
|
|
RUNNER_PROFILE: "github",
|
|
PROTOCOL_SINCE_BASE_SHA: base,
|
|
RATCHET_PR_HEAD_SHA: candidate,
|
|
};
|
|
|
|
function stepEnvironment(step: Step, supplied: Record<string, string>) {
|
|
const resolved = { ...defaults, ...supplied };
|
|
for (const [key, value] of Object.entries(step.env ?? {})) {
|
|
if (String(value).startsWith("${{")) {
|
|
if (resolved[key] === undefined) {
|
|
throw new Error(`Unresolved fixture workflow environment: ${key}`);
|
|
}
|
|
} else {
|
|
resolved[key] = String(value);
|
|
}
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
function readWorkflowStep({ file, job, step: name }: WorkflowTarget): Step & { run: string } {
|
|
const parsed = workflowInputs.get(file) ?? (parse(readFileSync(file, "utf8")) as WorkflowInput);
|
|
workflowInputs.set(file, parsed);
|
|
const step = parsed.jobs[job]?.steps.find((entry) => entry.name === name);
|
|
if (!step?.run) {
|
|
throw new Error(`Missing executable workflow step ${file}/${job}/${name}`);
|
|
}
|
|
return { ...step, run: step.run };
|
|
}
|
|
|
|
export async function runCiGitStep(options: {
|
|
signal: AbortSignal;
|
|
workflow?: "workflow-sanity" | WorkflowTarget;
|
|
job?: string;
|
|
action?:
|
|
| "ensure-base-commit"
|
|
| "git-owner"
|
|
| "mantis-validate-trusted-ref"
|
|
| "publish-generated-pr";
|
|
performance?: PerformanceFixtureOptions;
|
|
publisher?: GeneratedPublisherOptions & { baseChangePath?: "a" | "b" | null };
|
|
gitFault?: { match: string; occurrence?: number; code: FetchResult | "cancel"; output?: string };
|
|
gitFaults?: {
|
|
match: string;
|
|
occurrence?: number;
|
|
code: FetchResult | "cancel";
|
|
output?: string;
|
|
}[];
|
|
policy?: string;
|
|
inlinePolicy?: boolean;
|
|
step?: string;
|
|
stepOutputs?: Record<string, Record<string, string>>;
|
|
env?: Record<string, string>;
|
|
fetchResults: FetchResult[];
|
|
cloneResults?: FetchResult[];
|
|
worktreeResults?: FetchResult[];
|
|
rebaseResults?: FetchResult[];
|
|
pushResults?: FetchResult[];
|
|
revParseResult?: FetchResult;
|
|
diffResult?: number;
|
|
commandResults?: Record<string, { code: FetchResult; output?: string }>;
|
|
workflowRuns?: {
|
|
id: number;
|
|
run_attempt: number;
|
|
created_at: string;
|
|
status: string;
|
|
conclusion: string | null;
|
|
head_sha: string;
|
|
}[];
|
|
workflowJobs?: {
|
|
runId: number;
|
|
runAttempt: number;
|
|
jobs: {
|
|
name: string;
|
|
status: string;
|
|
conclusion: string | null;
|
|
steps: { name: string; status: string; conclusion: string | null }[];
|
|
}[];
|
|
}[];
|
|
publishPath?: "directory" | "file" | "symlink";
|
|
checkoutResults?: number[];
|
|
mergeSnapshots?: { sha: string; head: string }[];
|
|
prepare?: boolean;
|
|
checkoutBeforeStep?: boolean;
|
|
cancelDuringCleanup?: boolean;
|
|
cleanupCancelMatch?: string;
|
|
startupDelay?: { tree: number };
|
|
revisions?: Record<string, string>;
|
|
mergeBase?: { ancestor: boolean; revision: string };
|
|
poisonPython?: boolean;
|
|
baseAvailableAfter?: number;
|
|
invalidRef?: boolean;
|
|
scenario?: string;
|
|
lsRemoteResults?: { output: string; code: number | "hang" | "cleanup-failure" }[];
|
|
realClock?: boolean;
|
|
virtualBackoff?: boolean;
|
|
realDrain?: boolean;
|
|
readyFetchClockAdvanceSeconds?: number;
|
|
objects?: Record<string, { probe?: number; code?: number; text: string }>;
|
|
cooperativeTrees?: boolean;
|
|
cancelDuringBackoff?: boolean;
|
|
setupFailure?: "owner" | "python" | "git";
|
|
}) {
|
|
const maturity =
|
|
typeof options.workflow === "object" &&
|
|
options.workflow.file === ".github/workflows/maturity-scorecard.yml";
|
|
const docsPublish =
|
|
typeof options.workflow === "object" &&
|
|
options.workflow.file === ".github/workflows/docs-sync-publish.yml";
|
|
const docsAgent =
|
|
typeof options.workflow === "object" &&
|
|
options.workflow.file === ".github/workflows/docs-agent.yml";
|
|
const releaseAdmission =
|
|
typeof options.workflow === "object" &&
|
|
[
|
|
".github/workflows/linux-app-release.yml",
|
|
".github/workflows/macos-release.yml",
|
|
".github/workflows/npm-placeholder-bootstrap.yml",
|
|
].includes(options.workflow.file);
|
|
const pluginRelease =
|
|
typeof options.workflow === "object" &&
|
|
[
|
|
".github/workflows/plugin-clawhub-release.yml",
|
|
".github/workflows/plugin-npm-release.yml",
|
|
].includes(options.workflow.file);
|
|
const publisher = options.action === "publish-generated-pr";
|
|
const externalOwner =
|
|
options.workflow || options.action === "mantis-validate-trusted-ref" || publisher;
|
|
const clock = {
|
|
...options,
|
|
realDrain:
|
|
options.cancelDuringCleanup || options.scenario?.startsWith("cancel-") || options.realDrain,
|
|
};
|
|
const step: (Step & { run: string }) | undefined = options.action
|
|
? readActionSteps(options.action).find((entry) =>
|
|
options.step ? entry.name === options.step : entry.run,
|
|
)
|
|
: options.workflow
|
|
? readWorkflowStep(
|
|
options.workflow === "workflow-sanity"
|
|
? {
|
|
file: ".github/workflows/workflow-sanity.yml",
|
|
job: "actionlint",
|
|
step: "Prepare trusted workflow audit configs",
|
|
}
|
|
: options.workflow,
|
|
)
|
|
: readCiCheckoutStep(
|
|
options.job ?? "security-fast",
|
|
options.step ?? (options.job ? "Checkout" : "Prepare Git owner"),
|
|
);
|
|
if (!step?.run) {
|
|
throw new Error("Missing executable action step");
|
|
}
|
|
let env: Record<string, string>;
|
|
let performanceFixture: ReturnType<typeof preparePerformanceFixture> | undefined;
|
|
let publisherFixture: ReturnType<typeof prepareGeneratedPublisherFixture> | undefined;
|
|
return withCiCheckoutFixture(
|
|
`linux:${options.scenario ?? "configured"}`,
|
|
options.signal,
|
|
(root) => {
|
|
const actions = path.join(root, "trusted-actions");
|
|
if (options.performance) {
|
|
performanceFixture = preparePerformanceFixture(root, options.performance);
|
|
}
|
|
env = stepEnvironment(step, {
|
|
PUBLISH_ACTION_PATH: path.resolve(".github/actions/publish-generated-pr"),
|
|
CONTENTS_TOKEN: "fixture-contents",
|
|
HEAD_BRANCH: "automation/locale",
|
|
BASE_BRANCH: "main",
|
|
COMMIT_MESSAGE: "fixture",
|
|
PR_TITLE: "fixture",
|
|
PR_BODY: "fixture",
|
|
GENERATED_PATHS: "generated",
|
|
INVALIDATION_PATHS: "source",
|
|
OVERLAP_POLICY: "defer",
|
|
AUTO_MERGE: "false",
|
|
BASE_SHA: base,
|
|
BASE_REF: "main",
|
|
FETCH_REF: "fixture-base",
|
|
BASE_ACTION_PATH: path.join(actions, "ensure-base-commit"),
|
|
OWNER_ACTION_PATH: path.join(actions, "git-owner"),
|
|
...performanceFixture?.env,
|
|
...options.env,
|
|
});
|
|
const workspace = path.join(root, "workspace");
|
|
if (publisher) {
|
|
publisherFixture = prepareGeneratedPublisherFixture(
|
|
root,
|
|
options.publisher?.baseChangePath ?? null,
|
|
options.publisher,
|
|
"workspace",
|
|
);
|
|
env = {
|
|
...env,
|
|
...publisherFixture.env,
|
|
...options.env,
|
|
PUBLISH_ACTION_PATH: path.resolve(".github/actions/publish-generated-pr"),
|
|
GITHUB_STEP_SUMMARY: path.join(root, "github-summary"),
|
|
FAKE_REAL_GIT: execFileSync("which", ["git"], { encoding: "utf8" }).trim(),
|
|
};
|
|
delete env.PATH;
|
|
}
|
|
if (docsAgent) {
|
|
env.GITHUB_TOKEN = "fixture-docs-agent-token";
|
|
env.GH_TOKEN = "";
|
|
}
|
|
if (docsPublish) {
|
|
env.GITHUB_SHA = candidate;
|
|
// Never let a caller's credential reach fixture command reports.
|
|
env.OPENCLAW_DOCS_SYNC_TOKEN = "fixture-docs-token";
|
|
env.OPENCLAW_DOCS_MDX_CACHE = path.join(root, "docs-mdx-cache.json");
|
|
mkdirSync(path.join(workspace, "clawhub-source/.git"), { recursive: true });
|
|
const publish = path.join(workspace, "publish");
|
|
if (options.publishPath === "file") {
|
|
writeFileSync(publish, "previous publish path\n");
|
|
} else if (options.publishPath === "symlink") {
|
|
symlinkSync(root, publish, "junction");
|
|
} else if (options.publishPath === "directory") {
|
|
mkdirSync(publish);
|
|
writeFileSync(path.join(publish, ".previous-checkout"), "stale\n");
|
|
}
|
|
}
|
|
if (externalOwner) {
|
|
// Workflow bodies follow actions/checkout; selected-source bootstrap must
|
|
// still create its own directory, while later selected steps inherit one.
|
|
for (const directory of [
|
|
workspace,
|
|
...(!publisher && step["working-directory"]
|
|
? [path.join(workspace, step["working-directory"])]
|
|
: []),
|
|
]) {
|
|
mkdirSync(path.join(directory, ".git"), { recursive: true });
|
|
writeFileSync(path.join(directory, ".git/preexisting.lock"), "not invocation-owned\n");
|
|
}
|
|
if (pluginRelease) {
|
|
writeFileSync(path.join(workspace, "package.json"), '{"version":"2026.8.33"}\n');
|
|
}
|
|
}
|
|
if (options.startupDelay?.tree) {
|
|
writeFileSync(
|
|
path.join(root, "tree-start-delay-1.json"),
|
|
String(options.startupDelay.tree),
|
|
);
|
|
}
|
|
for (const action of ["git-owner", "ensure-base-commit"]) {
|
|
mkdirSync(path.join(actions, action), { recursive: true });
|
|
const name = action === "git-owner" ? "owner.py" : "policy.py";
|
|
let source = renderGitTestClock(
|
|
action === "git-owner" ? ownerSource : basePolicySource,
|
|
clock,
|
|
);
|
|
if (
|
|
action === "git-owner" &&
|
|
options.cancelDuringCleanup &&
|
|
!source.includes("cleanup-cancelled.json")
|
|
) {
|
|
throw new Error("Missing copied Git owner cleanup cancellation boundary");
|
|
}
|
|
if (
|
|
action === "git-owner" &&
|
|
(publisher || maturity || pluginRelease || releaseAdmission || options.performance)
|
|
) {
|
|
source = source.replace(
|
|
"def main():",
|
|
`def fixture_file_boundary(event, args):
|
|
names = {os.environ["GITHUB_OUTPUT"]: "output", os.environ["GITHUB_STEP_SUMMARY"]: "summary",
|
|
os.environ["GITHUB_ENV"]: "environment",
|
|
os.path.join(os.environ["RUNNER_TEMP"], "generated-pr-push.log"): "push-log"}
|
|
if event == "open" and args[0] in names:
|
|
subprocess.run([${JSON.stringify(process.execPath)}, ${JSON.stringify(ciCheckoutFixture)},
|
|
"observe", os.environ["TMPDIR"], "linux:configured", names[args[0]]], check=True)
|
|
|
|
sys.addaudithook(fixture_file_boundary)
|
|
|
|
|
|
def main():`,
|
|
);
|
|
}
|
|
if (action === "git-owner" && options.performance) {
|
|
source = source.replace(
|
|
"def backoff(seconds):",
|
|
`def backoff(seconds):
|
|
subprocess.run([${JSON.stringify(process.execPath)}, ${JSON.stringify(ciCheckoutFixture)},
|
|
"observe", os.environ["TMPDIR"], "linux:configured", "backoff"], check=True)`,
|
|
);
|
|
}
|
|
if (action === "git-owner" && options.cancelDuringBackoff) {
|
|
if (!options.realClock || options.virtualBackoff) {
|
|
throw new Error("Backoff cancellation requires the real owner clock");
|
|
}
|
|
// Existing callers exec Python into the supervised shell's PID.
|
|
// A non-exec caller would require separate shell propagation proof.
|
|
if (!step.run.includes('exec python3 -I -S "$CI_GIT_OWNER" --policy')) {
|
|
throw new Error("Backoff cancellation requires an exec-owned Python policy");
|
|
}
|
|
const boundary = " while time.monotonic() < retry_at:\n check_cancelled()";
|
|
if (source.split(boundary).length !== 2) {
|
|
throw new Error("Missing unique Git owner backoff cancellation boundary");
|
|
}
|
|
// Claim before acknowledgment so a dropped first signal cannot be retried.
|
|
source = source.replace(
|
|
boundary,
|
|
`${boundary}
|
|
fixture_cancel = os.path.join(os.environ["TMPDIR"], "backoff-cancel-claimed.json")
|
|
if not os.path.exists(fixture_cancel):
|
|
with open(fixture_cancel, "x") as receipt:
|
|
json.dump(os.getpid(), receipt)
|
|
subprocess.run([${JSON.stringify(process.execPath)}, ${JSON.stringify(ciCheckoutFixture)},
|
|
"observe", os.environ["TMPDIR"], "linux:configured", "backoff-cancel"], check=True)
|
|
os.kill(os.getpid(), signal.SIGTERM)`,
|
|
);
|
|
}
|
|
writeFileSync(path.join(actions, action, name), source);
|
|
}
|
|
if (publisher) {
|
|
mkdirSync(path.join(actions, "publish-generated-pr"), { recursive: true });
|
|
writeFileSync(
|
|
path.join(actions, "publish-generated-pr/policy.py"),
|
|
renderGitTestClock(publisherPolicySource, clock),
|
|
);
|
|
env.PUBLISH_ACTION_PATH = path.join(actions, "publish-generated-pr");
|
|
}
|
|
const protectedFile = path.join(
|
|
env.CHECKOUT_KIND === "clawhub" ? workspace : root,
|
|
"protected",
|
|
);
|
|
writeFileSync(protectedFile, "not checkout-owned\n");
|
|
if (["android", "clawhub"].includes(env.CHECKOUT_KIND ?? "")) {
|
|
const checkout =
|
|
env.CHECKOUT_KIND === "clawhub" ? path.join(workspace, "clawhub-source") : workspace;
|
|
mkdirSync(checkout, { recursive: true });
|
|
writeFileSync(path.join(checkout, ".previous-checkout"), "stale\n");
|
|
}
|
|
if (options.poisonPython) {
|
|
env.PYTHONPATH = workspace;
|
|
const poison = `from pathlib import Path\nPath(${JSON.stringify(path.join(root, "python-injected"))}).write_text("injected")\nraise RuntimeError("candidate Python startup executed")\n`;
|
|
for (const name of ["sitecustomize.py", "subprocess.py"]) {
|
|
writeFileSync(path.join(workspace, name), poison);
|
|
}
|
|
}
|
|
const revisions = {
|
|
HEAD: candidate,
|
|
"refs/heads/main": moved,
|
|
"refs/pull/17/merge": merge,
|
|
"refs/remotes/origin/release-gate-merge^1": base,
|
|
"refs/remotes/origin/release-gate-merge^2": candidate,
|
|
...options.revisions,
|
|
};
|
|
writeFileSync(
|
|
path.join(root, "fixture-options.json"),
|
|
JSON.stringify({
|
|
env,
|
|
publisher: publisherFixture
|
|
? { git: env.FAKE_REAL_GIT, gh: path.join(publisherFixture.fakeBin, "gh") }
|
|
: undefined,
|
|
performance: performanceFixture?.proxy,
|
|
gitFault: options.gitFault,
|
|
gitFaults: options.gitFaults,
|
|
revisions,
|
|
mergeBase: options.mergeBase,
|
|
workingDirectory: publisher ? undefined : step["working-directory"],
|
|
fetchResults: options.fetchResults,
|
|
cloneResults: options.cloneResults,
|
|
worktreeResults: options.worktreeResults,
|
|
rebaseResults: options.rebaseResults,
|
|
pushResults: options.pushResults,
|
|
revParseResult: options.revParseResult,
|
|
diffResult: options.diffResult,
|
|
commandResults: options.commandResults,
|
|
workflowRuns: options.workflowRuns,
|
|
workflowJobs: options.workflowJobs,
|
|
docsAgent,
|
|
docsPublish,
|
|
maturity,
|
|
pluginRelease,
|
|
releaseAdmission,
|
|
checkoutResults: options.checkoutResults,
|
|
mergeSnapshots: options.mergeSnapshots,
|
|
consumers: Boolean(options.prepare || options.checkoutBeforeStep || externalOwner),
|
|
cancelDuringCleanup: options.cancelDuringCleanup,
|
|
cleanupCancelMatch: options.cleanupCancelMatch,
|
|
baseAvailableAfter: options.baseAvailableAfter,
|
|
invalidRef: options.invalidRef,
|
|
lsRemoteResults: options.lsRemoteResults,
|
|
objects: options.objects,
|
|
cooperativeTrees: options.cooperativeTrees,
|
|
setupFailure: options.setupFailure,
|
|
}),
|
|
);
|
|
let run = renderGitTestClock(step.run, clock);
|
|
for (const [stepId, outputs] of Object.entries(options.stepOutputs ?? {})) {
|
|
for (const [name, value] of Object.entries(outputs)) {
|
|
run = run.replaceAll(`\${{ steps.${stepId}.outputs.${name} }}`, value);
|
|
}
|
|
}
|
|
if (externalOwner) {
|
|
const prepareRun = readActionSteps("git-owner")[0]?.run;
|
|
if (!prepareRun) {
|
|
throw new Error("Missing Git owner preparation body");
|
|
}
|
|
writeFileSync(path.join(root, "prepare.sh"), prepareRun);
|
|
// Model the runner's environment handoff, not shell evaluation of paths with spaces.
|
|
run = `bash --noprofile --norc -eo pipefail "$TMPDIR/prepare.sh"
|
|
export CI_GIT_OWNER
|
|
CI_GIT_OWNER="$(sed -n 's/^CI_GIT_OWNER=//p' "$GITHUB_ENV")"
|
|
: > "$GITHUB_ENV"
|
|
: > "$GITHUB_OUTPUT"
|
|
${options.setupFailure === "owner" ? 'rm "$CI_GIT_OWNER"' : ""}
|
|
${run}`;
|
|
}
|
|
if (options.policy) {
|
|
const policy = path.join(root, "policy.py");
|
|
writeFileSync(policy, options.policy);
|
|
run =
|
|
'unset RUNNER_OS GITHUB_WORKSPACE RUNNER_TEMP\nexec python3 -I -S "$OWNER_ACTION_PATH/owner.py" --policy ' +
|
|
(options.inlinePolicy ? '- < "$TMPDIR/policy.py"' : '"$TMPDIR/policy.py"');
|
|
}
|
|
if (options.prepare) {
|
|
const prepare = readCiCheckoutStep("security-fast", "Prepare Git owner");
|
|
const prepareEnv = stepEnvironment(prepare, {});
|
|
writeFileSync(path.join(root, "prepare.sh"), renderGitTestClock(prepare.run, clock));
|
|
// Run the actual prepare body in its own shell: its exec must not replace the caller.
|
|
run = `CHECKOUT_KIND=${prepareEnv.CHECKOUT_KIND} bash --noprofile --norc -eo pipefail "$TMPDIR/prepare.sh"\n${run}`;
|
|
}
|
|
if (options.checkoutBeforeStep) {
|
|
const checkout = readCiCheckoutStep(options.job ?? "checks-fast-core");
|
|
writeFileSync(path.join(root, "bootstrap.sh"), renderGitTestClock(checkout.run, clock));
|
|
run = `bash --noprofile --norc -eo pipefail "$TMPDIR/bootstrap.sh"\n${run}`;
|
|
}
|
|
if (options.performance) {
|
|
const mapfileShim =
|
|
options.performance.mode === "prepare"
|
|
? `if ! type mapfile >/dev/null 2>&1; then
|
|
mapfile() {
|
|
local delimiter=$'\\n'
|
|
if [[ "\${1:-}" == "-d" ]]; then delimiter="$2"; shift 2; fi
|
|
local destination="$1" item quoted index=0
|
|
eval "$destination=()"
|
|
while IFS= read -r -d "$delimiter" item; do
|
|
printf -v quoted '%q' "$item"
|
|
eval "$destination[$index]=$quoted"
|
|
index=$((index + 1))
|
|
done
|
|
}
|
|
fi
|
|
`
|
|
: "";
|
|
// Observe immediately after each owner invocation; Python policies separately
|
|
// expose output/summary writes through the audit hook, and exit is always censused.
|
|
run = `${mapfileShim}performance_owner_pending=false
|
|
performance_owner_boundary() {
|
|
local command="$1"
|
|
if [[ "$performance_owner_pending" == "true" ]]; then
|
|
${JSON.stringify(process.execPath)} ${JSON.stringify(ciCheckoutFixture)} observe "$TMPDIR" linux:configured shell-command
|
|
performance_owner_pending=false
|
|
fi
|
|
if [[ "$command" == *CI_GIT_OWNER* ]]; then performance_owner_pending=true; fi
|
|
}
|
|
trap 'performance_owner_boundary "$BASH_COMMAND"' DEBUG
|
|
${run}`;
|
|
}
|
|
writeFileSync(path.join(root, "checkout.sh"), run);
|
|
},
|
|
(report, result, stderr, root) => {
|
|
const workspace = path.join(root, "workspace");
|
|
const protectedFile = path.join(
|
|
env.CHECKOUT_KIND === "clawhub" ? workspace : root,
|
|
"protected",
|
|
);
|
|
const actions = path.join(root, "trusted-actions");
|
|
console.log(
|
|
`${typeof options.workflow === "object" ? `${options.workflow.file}/${options.workflow.job}/${options.workflow.step}` : `${options.workflow ?? options.action ?? options.job}/${options.step ?? "Checkout"}`}: ${JSON.stringify(report)}`,
|
|
);
|
|
expect(result, `${stderr}\n${report.error ?? ""}`).toEqual({ code: 0, signal: null });
|
|
expect(report.error, stderr).toBeUndefined();
|
|
expectCiCheckoutCleanup(report);
|
|
if (docsAgent) {
|
|
expect(
|
|
readdirSync(path.join(root, "temp")).filter((name) => name.startsWith("docs-agent-")),
|
|
).toEqual([]);
|
|
}
|
|
if (externalOwner) {
|
|
for (const directory of new Set([
|
|
workspace,
|
|
...report.commands
|
|
.filter(({ tool, args }) => tool === "git" && args[0] === "fetch")
|
|
.map(({ cwd }) => cwd),
|
|
...report.commands
|
|
.filter(
|
|
({ tool, args }) => tool === "git" && (args[0] === "clone" || args[0] === "worktree"),
|
|
)
|
|
.map(({ cwd, args }) => path.resolve(cwd, args.at(args[0] === "clone" ? -1 : -2)!)),
|
|
])) {
|
|
expect(readFileSync(path.join(directory, ".git/preexisting.lock"), "utf8")).toBe(
|
|
"not invocation-owned\n",
|
|
);
|
|
}
|
|
}
|
|
expect(readFileSync(protectedFile, "utf8")).toBe("not checkout-owned\n");
|
|
expect(
|
|
existsSync(path.join(root, "python-injected")),
|
|
"candidate Python startup executed",
|
|
).toBe(false);
|
|
const readOutput = (name: string) =>
|
|
existsSync(path.join(root, name)) ? readFileSync(path.join(root, name), "utf8") : "";
|
|
if (options.action === "ensure-base-commit") {
|
|
expect(report.output).not.toContain("fixture quiet probe");
|
|
}
|
|
if (options.action === "git-owner") {
|
|
const ownerPath = readOutput("github-env")
|
|
.trim()
|
|
.replace(/^CI_GIT_OWNER=/u, "");
|
|
expect(path.relative(path.join(root, "temp"), ownerPath)).not.toMatch(/^\.\./u);
|
|
expect(readFileSync(ownerPath, "utf8")).toBe(
|
|
readFileSync(path.join(actions, "git-owner/owner.py"), "utf8"),
|
|
);
|
|
expect(readOutput("github-output")).toBe(`owner-path=${ownerPath}\n`);
|
|
}
|
|
const authHeaderPresent = publisher
|
|
? execFileSync(env.FAKE_REAL_GIT!, ["-C", workspace, "config", "--local", "--list"], {
|
|
encoding: "utf8",
|
|
}).includes("http.https://github.com/.extraheader=")
|
|
: false;
|
|
return {
|
|
...report,
|
|
backoffClockAdvancedSeconds: [
|
|
...report.output.matchAll(/fixture backoff advanced: ([\d.]+)/gu),
|
|
].reduce((total, match) => total + Number(match[1]), 0),
|
|
...(options.readyFetchClockAdvanceSeconds === undefined
|
|
? {}
|
|
: {
|
|
fetchClockAdvancedSeconds:
|
|
options.readyFetchClockAdvanceSeconds *
|
|
readdirSync(root).filter((name) => /^fetch-tick-\d+\.json$/u.test(name)).length,
|
|
}),
|
|
authHeaderPresent,
|
|
initialBranch: publisherFixture?.initialBranch,
|
|
publication: publisherFixture?.inspect(report.output, false),
|
|
performance: performanceFixture?.inspect(),
|
|
pluginSourcePackage: pluginRelease ? readOutput("temp/fixture-source-package.json") : "",
|
|
pushLog: readOutput("runner-temp/generated-pr-push.log"),
|
|
workspace,
|
|
githubOutput: readOutput("github-output"),
|
|
githubEnv: readOutput("github-env"),
|
|
githubSummary: readOutput("github-summary"),
|
|
githubPath: readOutput("github-path"),
|
|
trustedConfig: readOutput("temp/pre-commit-base.yaml"),
|
|
trustedZizmor: readOutput("temp/zizmor-base.yml"),
|
|
runnerTemp: performanceFixture?.env.RUNNER_TEMP ?? path.join(root, "temp"),
|
|
fetches: report.commands.filter(({ tool, args }) => tool === "git" && args[0] === "fetch"),
|
|
clones: report.commands.filter(({ tool, args }) => tool === "git" && args[0] === "clone"),
|
|
worktrees: report.commands.filter(
|
|
({ tool, args }) => tool === "git" && args[0] === "worktree",
|
|
),
|
|
rebases: report.commands.filter(({ tool, args }) => tool === "git" && args[0] === "rebase"),
|
|
pushes: report.commands.filter(({ tool, args }) => tool === "git" && args[0] === "push"),
|
|
checkouts: report.commands.filter(
|
|
({ tool, args }) => tool === "git" && args[0] === "checkout",
|
|
),
|
|
};
|
|
},
|
|
);
|
|
}
|