Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
8 changes: 8 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ jobs:
lint:
name: Lint
runs-on: ubuntu-latest
# Bound the job so a hung step (a network fetch that stalls without refusing
# the connection, a stuck pip/npx) fails fast instead of grinding to GitHub's
# 360-minute default — the long-tail behind the fleet failure-rate/p95
# warning (issue #986). See test/workflows/ci/install-resilience.bats.
timeout-minutes: 10
permissions:
contents: read
steps:
Expand Down Expand Up @@ -81,6 +86,7 @@ jobs:
shellcheck:
name: ShellCheck
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
Expand All @@ -101,6 +107,7 @@ jobs:
agent-security:
name: Agent Security Scan
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
Expand Down Expand Up @@ -128,6 +135,7 @@ jobs:
secret-scan:
name: Secret scan (gitleaks)
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
Expand Down
63 changes: 63 additions & 0 deletions test/workflows/ci/install-resilience.bats
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,69 @@ step_block_with() {
[[ "$block" =~ npm_config_fetch_timeout:[[:space:]]*\'?[1-9] ]]
}

@test "install: every job in ci.yml declares a bounded timeout-minutes" {
# An unbounded job turns a single hung step (a network fetch that stalls
# without refusing the connection, a stuck npx/pip) into a run that grinds on
# to GitHub's 360-minute default before failing — the long-tail behind the
# fleet failure-rate/p95 warning (issue #986). Every job must therefore cap
# itself with a bounded timeout-minutes so a hang fails fast; this test asserts
# no job can silently drop that cap. Sibling fix: pr-auto-review-sweep.yml
# (#947) added timeout-minutes for the same reason.
[ -f "$TT_WORKFLOW" ]

# Walk the `jobs:` block. Job ids are the only keys indented exactly 2 spaces;
# each job's own keys (including timeout-minutes) sit at 4 spaces. For every
# job id we assert its block carries a `timeout-minutes: N` with 1 <= N <= 59
# (59 is GitHub's documented cap for setup-steps jobs and a sane fleet bound).
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved
local in_jobs=0
local job_count=0
local current_job="" current_ok=0
Comment thread
don-petry marked this conversation as resolved.
Resolved
finish_job() {
if [[ -n "$current_job" ]]; then
[ "$current_ok" -eq 1 ] || {
echo "job '$current_job' is missing a bounded timeout-minutes (1-59)" >&2
return 1
}
fi
return 0
}

while IFS= read -r line || [[ -n "$line" ]]; do
line="${line%$'\r'}"
# Enter the jobs: block (top-level key, no indentation).
if [[ "$line" =~ ^jobs:[[:space:]]*$ ]]; then
in_jobs=1
continue
fi
[ "$in_jobs" -eq 1 ] || continue
# A new top-level key (no leading space) ends the jobs: block.
if [[ "$line" =~ ^[^[:space:]] ]]; then
finish_job || return 1
break
fi
# A job id: exactly two spaces of indent then `name:`.
if [[ "$line" =~ ^\ \ ([A-Za-z0-9_-]+):[[:space:]]*$ ]]; then
finish_job || return 1
current_job="${BASH_REMATCH[1]}"
current_ok=0
job_count=$((job_count + 1))
continue
fi
# timeout-minutes for the current job (4-space indent), value 1-59.
if [[ "$line" =~ ^\ \ \ \ timeout-minutes:[[:space:]]*([0-9]+)[[:space:]]*$ ]]; then
local v="${BASH_REMATCH[1]}"
if [ "$v" -ge 1 ] && [ "$v" -le 59 ]; then
current_ok=1
fi
fi
done < "$TT_WORKFLOW"
# Handle the final job when the file ends inside the jobs: block.
finish_job || return 1

# Guard against a parser that silently matches nothing.
[ "$job_count" -ge 1 ]
}

@test "install: the gitleaks release download retries on transient failure" {
# `gh release download` pulls gitleaks from GitHub Releases (which redirects to
# objects.githubusercontent.com) and has no native retry — the exact failure
Expand Down
Loading