fix(ci): bound manual git fetches (#87839)

* fix(ci): bound manual git fetches

* fix(ci): cover platform fetch guards

* fix(ci): fail timed out target fetches

* fix(ci): repair typecheck regressions

* fix(ci): refresh CI expectations

* fix(ci): preserve main cron coverage
This commit is contained in:
Dallin Romney
2026-05-28 22:56:54 -07:00
committed by GitHub
parent 2e042fbca8
commit ed36f423da
9 changed files with 242 additions and 27 deletions

View File

@@ -38,9 +38,15 @@ runs:
exit 0
fi
fetch_base_ref() {
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch "$@"
}
for deepen_by in 25 100 300; do
echo "Base commit missing; deepening $FETCH_REF by $deepen_by."
if ! git fetch --no-tags --deepen="$deepen_by" origin -- "$FETCH_REF"; then
if ! fetch_base_ref --no-tags --deepen="$deepen_by" origin -- "$FETCH_REF"; then
echo "::warning title=ensure-base-commit fetch failed::Failed to deepen $FETCH_REF by $deepen_by while looking for $BASE_SHA"
fi
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then
@@ -50,7 +56,7 @@ runs:
done
echo "Base commit still missing; fetching full history for $FETCH_REF."
if ! git fetch --no-tags origin -- "$FETCH_REF"; then
if ! fetch_base_ref --no-tags origin -- "$FETCH_REF"; then
echo "::warning title=ensure-base-commit fetch failed::Failed to fetch full history for $FETCH_REF while looking for $BASE_SHA"
fi
if git rev-parse --verify "$BASE_SHA^{commit}" >/dev/null 2>&1; then

View File

@@ -188,7 +188,10 @@ jobs:
run: |
set -euo pipefail
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
sudo ln -sf "$node_bin/node" /usr/local/bin/node

View File

@@ -89,7 +89,10 @@ jobs:
run: |
set -euo pipefail
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
node_bin="$(dirname "$(node -p 'process.execPath')")"
sudo ln -sf "$node_bin/node" /usr/local/bin/node

View File

@@ -86,12 +86,26 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
if ! git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_REF}:refs/remotes/origin/checkout"; then
fetch_checkout_ref() {
local ref="$1"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${ref}:refs/remotes/origin/checkout"
}
if fetch_checkout_ref "$CHECKOUT_REF"; then
:
else
fetch_status="$?"
if [ "$fetch_status" = "124" ] || [ "$fetch_status" = "137" ]; then
echo "::error::checkout fetch for '$CHECKOUT_REF' timed out"
exit "$fetch_status"
fi
if [ "$GITHUB_EVENT_NAME" != "workflow_dispatch" ] || [ "$CHECKOUT_REF" = "$CHECKOUT_FALLBACK_REF" ]; then
exit 1
exit "$fetch_status"
fi
echo "::warning::workflow_dispatch target_ref '$CHECKOUT_REF' is unavailable; falling back to head SHA '$CHECKOUT_FALLBACK_REF'"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_FALLBACK_REF}:refs/remotes/origin/checkout"
fetch_checkout_ref "$CHECKOUT_FALLBACK_REF"
fi
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
@@ -321,12 +335,26 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
if ! git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_REF}:refs/remotes/origin/checkout"; then
fetch_checkout_ref() {
local ref="$1"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${ref}:refs/remotes/origin/checkout"
}
if fetch_checkout_ref "$CHECKOUT_REF"; then
:
else
fetch_status="$?"
if [ "$fetch_status" = "124" ] || [ "$fetch_status" = "137" ]; then
echo "::error::checkout fetch for '$CHECKOUT_REF' timed out"
exit "$fetch_status"
fi
if [ "$GITHUB_EVENT_NAME" != "workflow_dispatch" ] || [ "$CHECKOUT_REF" = "$CHECKOUT_FALLBACK_REF" ]; then
exit 1
exit "$fetch_status"
fi
echo "::warning::workflow_dispatch target_ref '$CHECKOUT_REF' is unavailable; falling back to head SHA '$CHECKOUT_FALLBACK_REF'"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_FALLBACK_REF}:refs/remotes/origin/checkout"
fetch_checkout_ref "$CHECKOUT_FALLBACK_REF"
fi
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
@@ -1555,7 +1583,10 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Python
@@ -1603,7 +1634,27 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
fetch_checkout_ref() {
git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
local fetch_pid="$!"
local elapsed=0
while kill -0 "$fetch_pid" 2>/dev/null; do
if [ "$elapsed" -ge 30 ]; then
kill -TERM "$fetch_pid" 2>/dev/null || true
sleep 10
kill -KILL "$fetch_pid" 2>/dev/null || true
wait "$fetch_pid" || true
return 124
fi
sleep 1
elapsed=$((elapsed + 1))
done
wait "$fetch_pid"
}
fetch_checkout_ref
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Try to exclude workspace from Windows Defender (best-effort)
@@ -1703,7 +1754,27 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
fetch_checkout_ref() {
git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
local fetch_pid="$!"
local elapsed=0
while kill -0 "$fetch_pid" 2>/dev/null; do
if [ "$elapsed" -ge 30 ]; then
kill -TERM "$fetch_pid" 2>/dev/null || true
sleep 10
kill -KILL "$fetch_pid" 2>/dev/null || true
wait "$fetch_pid" || true
return 124
fi
sleep 1
elapsed=$((elapsed + 1))
done
wait "$fetch_pid"
}
fetch_checkout_ref
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Node environment
@@ -1749,7 +1820,27 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
fetch_checkout_ref() {
git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout" &
local fetch_pid="$!"
local elapsed=0
while kill -0 "$fetch_pid" 2>/dev/null; do
if [ "$elapsed" -ge 30 ]; then
kill -TERM "$fetch_pid" 2>/dev/null || true
sleep 10
kill -KILL "$fetch_pid" 2>/dev/null || true
wait "$fetch_pid" || true
return 124
fi
sleep 1
elapsed=$((elapsed + 1))
done
wait "$fetch_pid"
}
fetch_checkout_ref
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Install XcodeGen / SwiftLint / SwiftFormat

View File

@@ -137,7 +137,10 @@ jobs:
set -euo pipefail
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
fi
- name: Prepare Crabbox shell
@@ -318,7 +321,24 @@ jobs:
$ErrorActionPreference = "Stop"
if (git rev-parse --is-inside-work-tree 2>$null) {
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
$fetch = Start-Process git -NoNewWindow -PassThru -ArgumentList @(
"-c",
"protocol.version=2",
"fetch",
"--no-tags",
"--prune",
"--no-recurse-submodules",
"--depth=50",
"origin",
"+refs/heads/main:refs/remotes/origin/main"
)
if (-not $fetch.WaitForExit(30000)) {
Stop-Process -Id $fetch.Id -Force -ErrorAction SilentlyContinue
throw "git fetch timed out after 30 seconds"
}
if ($fetch.ExitCode -ne 0) {
throw "git fetch failed with exit code $($fetch.ExitCode)"
}
}
- name: Setup pnpm and dependencies
@@ -513,7 +533,10 @@ jobs:
set -euo pipefail
if git rev-parse --is-inside-work-tree >/dev/null 2>&1; then
git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"
timeout --signal=TERM --kill-after=10s 30s git \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=50 origin \
"+refs/heads/main:refs/remotes/origin/main"
fi
node_bin="$(dirname "$(node -p 'process.execPath')")"

View File

@@ -34,7 +34,10 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Fail on tabs in workflow files
@@ -75,7 +78,10 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Install actionlint
@@ -116,7 +122,10 @@ jobs:
git init "$GITHUB_WORKSPACE"
git -C "$GITHUB_WORKSPACE" config gc.auto 0
git -C "$GITHUB_WORKSPACE" remote add origin "https://github.com/${CHECKOUT_REPO}.git"
git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1 origin "+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE" \
-c protocol.version=2 \
fetch --no-tags --prune --no-recurse-submodules --depth=1 origin \
"+${CHECKOUT_SHA}:refs/remotes/origin/checkout"
git -C "$GITHUB_WORKSPACE" checkout --detach refs/remotes/origin/checkout
- name: Setup Node environment

View File

@@ -10,21 +10,92 @@ describe("ci workflow guards", () => {
it("kills timed manual checkout fetches after the grace period", () => {
const workflowPaths = [
".github/workflows/ci.yml",
".github/workflows/workflow-sanity.yml",
".github/workflows/ci-check-testbox.yml",
".github/workflows/ci-build-artifacts-testbox.yml",
".github/workflows/crabbox-hydrate.yml",
];
for (const workflowPath of workflowPaths) {
const workflow = readFileSync(workflowPath, "utf8");
const fetchTimeouts = workflow.match(/timeout --signal=TERM[^\n]* 30s git -C "\$workdir"/g);
const fetchTimeouts = workflow.match(
/timeout --signal=TERM[^\n]* 30s git(?: -C "(?:\$workdir|\$GITHUB_WORKSPACE|clawhub-source)")?/g,
);
expect(fetchTimeouts?.length, workflowPath).toBeGreaterThan(0);
expect(fetchTimeouts, workflowPath).toEqual(
fetchTimeouts?.map(() => 'timeout --signal=TERM --kill-after=10s 30s git -C "$workdir"'),
expect(
fetchTimeouts?.every((line) =>
line.startsWith("timeout --signal=TERM --kill-after=10s 30s git"),
),
workflowPath,
).toBe(true);
}
});
it("bounds shared base commit fetches", () => {
const action = readFileSync(".github/actions/ensure-base-commit/action.yml", "utf8");
expect(action).toContain("fetch_base_ref()");
expect(action).toContain("timeout --signal=TERM --kill-after=10s 30s git");
expect(action).toContain("-c protocol.version=2");
expect(action).not.toContain("if ! git fetch --no-tags");
});
it("bounds early unauthenticated checkout fetches", () => {
const workflow = readCiWorkflow();
for (const jobName of ["preflight", "security-fast", "skills-python"]) {
const checkoutStep = workflow.jobs[jobName].steps.find((step) => step.name === "Checkout");
expect(checkoutStep.run, jobName).toContain(
'timeout --signal=TERM --kill-after=10s 30s git -C "$GITHUB_WORKSPACE"',
);
expect(checkoutStep.run, jobName).toContain("-c protocol.version=2");
expect(checkoutStep.run, jobName).toContain(
"fetch --no-tags --prune --no-recurse-submodules --depth=1 origin",
);
if (jobName !== "skills-python") {
expect(checkoutStep.run, jobName).toContain('if [ "$fetch_status" = "124" ]');
expect(checkoutStep.run, jobName).toContain("timed out");
}
expect(checkoutStep.run, jobName).not.toContain(
'git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1',
);
}
});
it("bounds platform checkout fetches without GNU timeout", () => {
const workflow = readCiWorkflow();
for (const jobName of ["checks-windows", "macos-node", "macos-swift"]) {
const checkoutStep = workflow.jobs[jobName].steps.find((step) => step.name === "Checkout");
expect(checkoutStep.run, jobName).toContain("fetch_checkout_ref()");
expect(checkoutStep.run, jobName).toContain("-c protocol.version=2");
expect(checkoutStep.run, jobName).toContain(
"fetch --no-tags --prune --no-recurse-submodules --depth=1 origin",
);
expect(checkoutStep.run, jobName).toContain('kill -TERM "$fetch_pid"');
expect(checkoutStep.run, jobName).toContain('kill -KILL "$fetch_pid"');
expect(checkoutStep.run, jobName).not.toContain(
'git -C "$GITHUB_WORKSPACE" fetch --no-tags --depth=1',
);
}
});
it("bounds the Windows Crabbox hydrate main fetch", () => {
const workflow = readFileSync(".github/workflows/crabbox-hydrate.yml", "utf8");
expect(workflow).toContain("$fetch = Start-Process git");
expect(workflow).toContain('"protocol.version=2"');
expect(workflow).toContain('"--no-recurse-submodules"');
expect(workflow).toContain("$fetch.WaitForExit(30000)");
expect(workflow).toContain('throw "git fetch timed out after 30 seconds"');
expect(workflow).not.toContain(
'git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"',
);
});
it("runs dependency policy guards in PR CI preflight", () => {
const workflow = readFileSync(".github/workflows/ci.yml", "utf8");
const preflightGuards = workflow.slice(

View File

@@ -141,7 +141,13 @@ describe("package acceptance workflow", () => {
expect(hydratePnpm.run).toContain('corepack enable --install-directory "$PNPM_HOME"');
expect(hydratePnpm.run).toContain("COREPACK_HOME");
expect(workflowStep(hydrate, "Fetch main ref").run).toContain(
'git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"',
"timeout --signal=TERM --kill-after=10s 30s git",
);
expect(workflowStep(hydrate, "Fetch main ref").run).toContain(
"fetch --no-tags --prune --no-recurse-submodules --depth=50 origin",
);
expect(workflowStep(hydrate, "Fetch main ref").run).toContain(
'"+refs/heads/main:refs/remotes/origin/main"',
);
expect(workflowStep(hydrate, "Prepare Crabbox shell").if).toBeUndefined();
expect(workflowStep(hydrate, "Ensure Docker is running").if).toBeUndefined();
@@ -178,9 +184,11 @@ describe("package acceptance workflow", () => {
);
const hydrateWindowsFetch = workflowStep(hydrateWindowsDaemon, "Fetch main ref");
expect(hydrateWindowsFetch.shell).toBe("powershell");
expect(hydrateWindowsFetch.run).toContain(
'git fetch --no-tags --depth=50 origin "+refs/heads/main:refs/remotes/origin/main"',
);
expect(hydrateWindowsFetch.run).toContain("Start-Process git");
expect(hydrateWindowsFetch.run).toContain("WaitForExit(30000)");
expect(hydrateWindowsFetch.run).toContain('"fetch"');
expect(hydrateWindowsFetch.run).toContain('"--depth=50"');
expect(hydrateWindowsFetch.run).toContain('"+refs/heads/main:refs/remotes/origin/main"');
expect(workflowStep(hydrateWindowsDaemon, "Mark Crabbox ready").shell).toBe("powershell");
expect(workflowStep(hydrateWindowsDaemon, "Mark Crabbox ready").run).toContain('"NODE_BIN"');
expect(workflowStep(hydrateWindowsDaemon, "Mark Crabbox ready").run).toContain('"PNPM_HOME"');

View File

@@ -9,6 +9,7 @@ export default createUnitVitestConfigWithOptions(process.env, {
// The gateway-protocol package rides with gateway-client because the client
// package owns the browser/runtime protocol compatibility lane.
"packages/gateway-protocol/src/**/*.test.ts",
"packages/gateway-client/src/**/*.test.ts",
],
passWithNoTests: true,
});