diff --git a/scripts/tests/policy-gates-test.sh b/scripts/tests/policy-gates-test.sh index 86170e9d..71b38704 100755 --- a/scripts/tests/policy-gates-test.sh +++ b/scripts/tests/policy-gates-test.sh @@ -98,6 +98,27 @@ 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") + +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" 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