|
| 1 | +#!/usr/bin/env bash |
| 2 | +# SPDX-License-Identifier: MPL-2.0 |
| 3 | +# fix-workflow-timeout-minutes.sh — Add `timeout-minutes:` to workflow |
| 4 | +# jobs that lack one. ERR-WF-013. |
| 5 | +# |
| 6 | +# Default GH Actions job timeout is 6 hours; a stuck `codeload` fetch |
| 7 | +# can burn that full budget. This script inserts `timeout-minutes: 10` |
| 8 | +# directly after each `runs-on:` line in a workflow YAML that doesn't |
| 9 | +# already have one within the job block. Override per-job via |
| 10 | +# `JOB_TIMEOUT_OVERRIDES` env var (`<job-name>=<minutes>,...`). |
| 11 | +# |
| 12 | +# Usage: |
| 13 | +# fix-workflow-timeout-minutes.sh <workflow.yml> |
| 14 | +# fix-workflow-timeout-minutes.sh <repo-path> (scans all .github/workflows/*.yml) |
| 15 | +# |
| 16 | +# Pairs with Hypatia.Rules.WorkflowAudit.check_missing_timeout_minutes/1 |
| 17 | +# and recipe-add-workflow-timeout-minutes.json. |
| 18 | + |
| 19 | +set -euo pipefail |
| 20 | + |
| 21 | +DEFAULT_TIMEOUT="${DEFAULT_TIMEOUT:-10}" |
| 22 | +TARGET="${1:?Usage: fix-workflow-timeout-minutes.sh <workflow.yml | repo-path>}" |
| 23 | + |
| 24 | +apply_to_file() { |
| 25 | + local f="$1" |
| 26 | + [ -f "$f" ] || { echo "SKIP (not a file): $f" >&2; return 0; } |
| 27 | + |
| 28 | + # Get list of (line_number, job_name) for jobs that lack timeout-minutes |
| 29 | + # within their block. We scan the file and find each top-level `^ <key>:$` |
| 30 | + # under `^jobs:$`, then check the next 30 lines for `timeout-minutes:`. |
| 31 | + awk -v default_to="$DEFAULT_TIMEOUT" ' |
| 32 | + /^jobs:[[:space:]]*$/ { in_jobs=1; next } |
| 33 | + /^[A-Za-z]/ && !/^jobs:/ { in_jobs=0 } |
| 34 | + in_jobs && /^ [A-Za-z0-9_-]+:[[:space:]]*$/ { |
| 35 | + if (curjob && !hadto) print curjob_line "\t" curjob |
| 36 | + curjob = $0; sub(/^ /, "", curjob); sub(/:.*$/, "", curjob) |
| 37 | + curjob_line = NR; hadto = 0 |
| 38 | + } |
| 39 | + in_jobs && /^ timeout-minutes:/ { hadto = 1 } |
| 40 | + END { if (curjob && !hadto) print curjob_line "\t" curjob } |
| 41 | + ' "$f" > /tmp/missing_timeouts.$$ |
| 42 | + |
| 43 | + if [ ! -s /tmp/missing_timeouts.$$ ]; then |
| 44 | + rm -f /tmp/missing_timeouts.$$ |
| 45 | + echo "OK (no missing timeouts): $f" |
| 46 | + return 0 |
| 47 | + fi |
| 48 | + |
| 49 | + # Now find the `runs-on:` line in each missing job and insert timeout after it. |
| 50 | + # We do this in reverse order to keep line numbers stable across inserts. |
| 51 | + tmp=$(mktemp) |
| 52 | + cp "$f" "$tmp" |
| 53 | + |
| 54 | + tac /tmp/missing_timeouts.$$ | while IFS=$'\t' read -r job_start_line job_name; do |
| 55 | + # Find the runs-on line within the next 20 lines from the job header |
| 56 | + runs_on_line=$(awk -v start="$job_start_line" -v end="$((job_start_line+20))" ' |
| 57 | + NR > start && NR < end && /^ runs-on:/ { print NR; exit } |
| 58 | + ' "$tmp") |
| 59 | + if [ -z "$runs_on_line" ]; then |
| 60 | + echo "WARN: job '$job_name' has no `runs-on:` within 20 lines — skipping" >&2 |
| 61 | + continue |
| 62 | + fi |
| 63 | + # Insert AFTER the runs-on line |
| 64 | + sed -i "${runs_on_line}a\ timeout-minutes: ${DEFAULT_TIMEOUT}" "$tmp" |
| 65 | + echo "FIXED $f job=$job_name inserted timeout-minutes: $DEFAULT_TIMEOUT" |
| 66 | + done |
| 67 | + |
| 68 | + if ! cmp -s "$f" "$tmp"; then |
| 69 | + mv "$tmp" "$f" |
| 70 | + else |
| 71 | + rm -f "$tmp" |
| 72 | + fi |
| 73 | + rm -f /tmp/missing_timeouts.$$ |
| 74 | +} |
| 75 | + |
| 76 | +if [ -d "$TARGET" ]; then |
| 77 | + for f in "$TARGET"/.github/workflows/*.yml "$TARGET"/.github/workflows/*.yaml; do |
| 78 | + [ -f "$f" ] || continue |
| 79 | + apply_to_file "$f" |
| 80 | + done |
| 81 | +else |
| 82 | + apply_to_file "$TARGET" |
| 83 | +fi |
0 commit comments