From 2905080096d34cd31defe47ff3821bad759d226c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 29 Aug 2026 12:59:00 +0100 Subject: [PATCH 1/2] fix(governance): reject invalid reusable timeouts --- scripts/tests/policy-gates-test.sh | 14 ++++++++++++++ tools/policy/check-workflows-parse.sh | 20 ++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/scripts/tests/policy-gates-test.sh b/scripts/tests/policy-gates-test.sh index 86170e9d..94f44b0e 100755 --- a/scripts/tests/policy-gates-test.sh +++ b/scripts/tests/policy-gates-test.sh @@ -98,6 +98,20 @@ printf '%s\n' 'name: test' 'jobs: [' > "$invalid/.github/workflows/test.yml" git -C "$invalid" add .github/workflows/test.yml (cd "$invalid" && expect_fail "$workflow_gate") +reusable_valid="$fixture/reusable-valid" +init_fixture "$reusable_valid" +mkdir -p "$reusable_valid/.github/workflows" +printf '%s\n' 'name: reusable' 'on: push' 'jobs:' ' gate:' ' uses: owner/repo/.github/workflows/gate.yml@0123456789012345678901234567890123456789' > "$reusable_valid/.github/workflows/test.yml" +git -C "$reusable_valid" add .github/workflows/test.yml +(cd "$reusable_valid" && expect_pass "$workflow_gate") + +reusable_timeout="$fixture/reusable-timeout" +init_fixture "$reusable_timeout" +mkdir -p "$reusable_timeout/.github/workflows" +printf '%s\n' 'name: reusable' 'on: push' 'jobs:' ' gate:' ' timeout-minutes: 10' ' uses: owner/repo/.github/workflows/gate.yml@0123456789012345678901234567890123456789' > "$reusable_timeout/.github/workflows/test.yml" +git -C "$reusable_timeout" add .github/workflows/test.yml +(cd "$reusable_timeout" && expect_fail "$workflow_gate") + control="$fixture/control" init_fixture "$control" mkdir -p "$control/.github/workflows" diff --git a/tools/policy/check-workflows-parse.sh b/tools/policy/check-workflows-parse.sh index fcf18829..5d99eae5 100755 --- a/tools/policy/check-workflows-parse.sh +++ b/tools/policy/check-workflows-parse.sh @@ -38,6 +38,23 @@ parse_ok() { esac } +# GitHub rejects a reusable-workflow call job before creating any jobs when it +# contains step-job-only keys such as timeout-minutes. The file remains valid +# YAML, so the parser gate alone cannot see this zero-check failure mode. +has_reusable_timeout() { + case "$parser" in + yq) + yq -e '[.jobs[] | select(has("uses") and has("timeout-minutes"))] | length > 0' "$1" >/dev/null 2>&1 + ;; + python) + python3 -c 'import sys,yaml; d=yaml.safe_load(open(sys.argv[1], encoding="utf-8")) or {}; sys.exit(not any(isinstance(j,dict) and "uses" in j and "timeout-minutes" in j for j in (d.get("jobs") or {}).values()))' "$1" + ;; + ruby) + ruby -ryaml -e 'd=YAML.safe_load(File.read(ARGV[0]), aliases: true) || {}; jobs=d["jobs"] || {}; exit(jobs.values.any? { |j| j.is_a?(Hash) && j.key?("uses") && j.key?("timeout-minutes") } ? 0 : 1)' "$1" + ;; + esac +} + has_forbidden_control() { od -An -v -tu1 "$1" | awk ' { for (i=1; i<=NF; i++) if (($i < 9) || ($i > 10 && $i < 13) || ($i > 13 && $i < 32)) found=1 } @@ -54,6 +71,9 @@ for file in "${workflows[@]}"; do if has_forbidden_control "$file"; then echo ' contains a YAML-forbidden control character' fi + elif has_reusable_timeout "$file"; then + status=1 + printf '%s\n' "::error file=$file::a reusable-workflow call job cannot declare timeout-minutes; GitHub rejects it before creating any jobs" fi done From 5a8cf266ee20d7ce19d435832875610c3fe963c9 Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Sat, 29 Aug 2026 13:06:12 +0100 Subject: [PATCH 2/2] test: preserve runner timeout support --- scripts/tests/policy-gates-test.sh | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/scripts/tests/policy-gates-test.sh b/scripts/tests/policy-gates-test.sh index 94f44b0e..71b38704 100755 --- a/scripts/tests/policy-gates-test.sh +++ b/scripts/tests/policy-gates-test.sh @@ -112,6 +112,13 @@ printf '%s\n' 'name: reusable' 'on: push' 'jobs:' ' gate:' ' timeout-minutes git -C "$reusable_timeout" add .github/workflows/test.yml (cd "$reusable_timeout" && expect_fail "$workflow_gate") +runner_timeout="$fixture/runner-timeout" +init_fixture "$runner_timeout" +mkdir -p "$runner_timeout/.github/workflows" +printf '%s\n' 'name: runner' 'on: push' 'jobs:' ' test:' ' runs-on: ubuntu-latest' ' timeout-minutes: 10' ' steps:' ' - run: true' > "$runner_timeout/.github/workflows/test.yml" +git -C "$runner_timeout" add .github/workflows/test.yml +(cd "$runner_timeout" && expect_pass "$workflow_gate") + control="$fixture/control" init_fixture "$control" mkdir -p "$control/.github/workflows"