diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4b963e015..1d3f825ff 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -19,6 +19,17 @@ on: schedule: - cron: "42 6 * * 0" +# A newer push to a pull request cancels that PR's superseded run. Each job +# then joins a per-author queue for that job (its own `concurrency` block), so +# one person opening many PRs runs one copy of each job at a time and cannot +# fill the runner pool ahead of everyone else. Queued jobs start in the order they began waiting; GitHub +# keeps up to 100 pending per queue and cancels arrivals past that. Every job +# has a timeout so a hung job cannot hold its queue for hours. Bot-authored +# PRs and non-PR events key on the run ID and never wait on a queue. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: analyze: name: Analyze (${{ matrix.language }}) @@ -28,6 +39,10 @@ jobs: # - https://gh.io/using-larger-runners (GitHub.com only) # Consider using larger runners or machines with greater resources for possible analysis time improvements. runs-on: ${{ (matrix.language == 'swift' && 'macos-latest') || 'ubuntu-latest' }} + timeout-minutes: 45 + concurrency: + group: ${{ github.workflow }}-analyze-${{ matrix.language }}-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max permissions: # required for all workflows security-events: write diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml index f458572eb..037909389 100644 --- a/.github/workflows/format.yml +++ b/.github/workflows/format.yml @@ -8,10 +8,25 @@ on: permissions: contents: read +# A newer push to a pull request cancels that PR's superseded run. Each job +# then joins a per-author queue for that job (its own `concurrency` block), so +# one person opening many PRs runs one copy of each job at a time and cannot +# fill the runner pool ahead of everyone else. Queued jobs start in the order they began waiting; GitHub +# keeps up to 100 pending per queue and cancels arrivals past that. Every job +# has a timeout so a hung job cannot hold its queue for hours. Bot-authored +# PRs and non-PR events key on the run ID and never wait on a queue. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: check-claude-md-symlink: name: CLAUDE.md symlink guard runs-on: ubuntu-latest + timeout-minutes: 5 + concurrency: + group: ${{ github.workflow }}-check-claude-md-symlink-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -42,9 +57,55 @@ jobs: format-rust: name: cargo fmt runs-on: ubuntu-latest + timeout-minutes: 25 + concurrency: + group: ${{ github.workflow }}-format-rust-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 + # A hung job, a PR-triggered workflow without per-PR cancellation, or a + # job outside its per-author queue brings back the runner pile-up that + # the concurrency blocks prevent. Every PR-triggered workflow must carry + # test.yml's workflow-level block, and every job must set timeout-minutes + # and a per-author queue (keyed on the matrix too, for matrix jobs). + # Jobs that call a reusable workflow are skipped: they cannot set these. + - name: Require per-PR cancellation, per-author job queues and timeouts + shell: bash + run: | + reference=.github/workflows/test.yml + if [ "$(yq '.concurrency."cancel-in-progress" == true and (.concurrency.group // "" | test("pull_request\.number"))' "$reference")" != true ]; then + echo "::error file=$reference::test.yml must cancel superseded runs per pull request." + exit 1 + fi + expected=$(yq -o=json -I=0 '.concurrency' "$reference") + status=0 + for f in .github/workflows/*.yml .github/workflows/*.yaml; do + [ -f "$f" ] || continue + on_pr=$(yq '.on | ((tag == "!!map" and has("pull_request")) or (tag == "!!str" and . == "pull_request") or (tag == "!!seq" and ([.[] | select(. == "pull_request")] | length > 0)))' "$f") + [ "$on_pr" = true ] || continue + if [ "$(yq -o=json -I=0 '.concurrency' "$f")" != "$expected" ]; then + echo "::error file=$f::PR-triggered workflows must use the workflow-level concurrency block from test.yml." + status=1 + fi + while read -r job; do + [ -n "$job" ] || continue + echo "::error file=$f::Job '$job' must set timeout-minutes." + status=1 + done < <(yq '.jobs | to_entries[] | select(.value.uses == null and .value["timeout-minutes"] == null) | .key' "$f") + while read -r job; do + [ -n "$job" ] || continue + echo "::error file=$f::Job '$job' must join a per-author queue: concurrency.group keyed on pull_request.user.login and queue: max." + status=1 + done < <(yq '.jobs | to_entries[] | select(.value.uses == null and (.value.concurrency.queue != "max" or (.value.concurrency.group // "" | test("pull_request\.user\.login") | not))) | .key' "$f") + while read -r job; do + [ -n "$job" ] || continue + echo "::error file=$f::Matrix job '$job' must include the matrix in its concurrency group so its legs do not queue behind each other." + status=1 + done < <(yq '.jobs | to_entries[] | select(.value.strategy.matrix != null and (.value.concurrency.group // "" | test("matrix\.") | not)) | .key' "$f") + done + exit "$status" + # Rust jobs must set up the toolchain through ./.github/actions/setup-rust, # which removes the image's stable and only saves caches from main. A # direct setup-rust-toolchain or rust-cache step skips both silently. @@ -103,6 +164,10 @@ jobs: format-typescript: runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: ${{ github.workflow }}-format-typescript-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max defaults: run: working-directory: crates/trusted-server-js/lib @@ -134,6 +199,10 @@ jobs: format-docs: runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: ${{ github.workflow }}-format-docs-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max defaults: run: working-directory: docs @@ -175,6 +244,10 @@ jobs: docs-proposal-scripts: name: docs-proposal scripts runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: ${{ github.workflow }}-docs-proposal-scripts-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/integration-tests.yml b/.github/workflows/integration-tests.yml index e3f2b3334..a276d23ec 100644 --- a/.github/workflows/integration-tests.yml +++ b/.github/workflows/integration-tests.yml @@ -10,6 +10,17 @@ on: types: [opened, synchronize, reopened] workflow_dispatch: +# A newer push to a pull request cancels that PR's superseded run. Each job +# then joins a per-author queue for that job (its own `concurrency` block), so +# one person opening many PRs runs one copy of each job at a time and cannot +# fill the runner pool ahead of everyone else. Queued jobs start in the order they began waiting; GitHub +# keeps up to 100 pending per queue and cancels arrivals past that. Every job +# has a timeout so a hung job cannot hold its queue for hours. Bot-authored +# PRs and non-PR events key on the run ID and never wait on a queue. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + env: ORIGIN_PORT: 8888 ARTIFACTS_DIR: /tmp/integration-test-artifacts @@ -23,6 +34,9 @@ jobs: name: prepare integration artifacts runs-on: ubuntu-latest timeout-minutes: 30 + concurrency: + group: ${{ github.workflow }}-prepare-artifacts-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -60,6 +74,9 @@ jobs: needs: prepare-artifacts runs-on: ubuntu-latest timeout-minutes: 20 + concurrency: + group: ${{ github.workflow }}-integration-tests-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -119,6 +136,9 @@ jobs: needs: prepare-artifacts runs-on: ubuntu-latest timeout-minutes: 15 + concurrency: + group: ${{ github.workflow }}-integration-tests-fastly-ec-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -163,6 +183,9 @@ jobs: # once the pinned Playwright version supports the newer image. runs-on: ubuntu-24.04 timeout-minutes: 15 + concurrency: + group: ${{ github.workflow }}-browser-tests-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/pbs-example.yml b/.github/workflows/pbs-example.yml index dced0c7b6..d61072867 100644 --- a/.github/workflows/pbs-example.yml +++ b/.github/workflows/pbs-example.yml @@ -17,11 +17,25 @@ on: permissions: contents: read +# A newer push to a pull request cancels that PR's superseded run. Each job +# then joins a per-author queue for that job (its own `concurrency` block), so +# one person opening many PRs runs one copy of each job at a time and cannot +# fill the runner pool ahead of everyone else. Queued jobs start in the order they began waiting; GitHub +# keeps up to 100 pending per queue and cancels arrivals past that. Every job +# has a timeout so a hung job cannot hold its queue for hours. Bot-authored +# PRs and non-PR events key on the run ID and never wait on a queue. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: local-checks: name: PBS Terraform and Compose checks runs-on: ubuntu-latest timeout-minutes: 20 + concurrency: + group: ${{ github.workflow }}-local-checks-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max env: TF_IN_AUTOMATION: 'true' AWS_EC2_METADATA_DISABLED: 'true' diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 6cd995328..a5c669f83 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -8,11 +8,25 @@ on: branches: [main] pull_request: +# A newer push to a pull request cancels that PR's superseded run. Each job +# then joins a per-author queue for that job (its own `concurrency` block), so +# one person opening many PRs runs one copy of each job at a time and cannot +# fill the runner pool ahead of everyone else. Queued jobs start in the order they began waiting; GitHub +# keeps up to 100 pending per queue and cancels arrivals past that. Every job +# has a timeout so a hung job cannot hold its queue for hours. Bot-authored +# PRs and non-PR events key on the run ID and never wait on a queue. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: true + jobs: test-rust: name: cargo test runs-on: ubuntu-latest timeout-minutes: 45 + concurrency: + group: ${{ github.workflow }}-test-rust-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -69,6 +83,9 @@ jobs: name: HTML EOF runtime (Fastly) runs-on: ubuntu-latest timeout-minutes: 25 + concurrency: + group: ${{ github.workflow }}-test-fastly-eof-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -117,6 +134,10 @@ jobs: test-axum: name: cargo test (axum native) runs-on: ubuntu-latest + timeout-minutes: 30 + concurrency: + group: ${{ github.workflow }}-test-axum-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -187,6 +208,10 @@ jobs: test-cloudflare: name: cargo check (cloudflare native + wasm32-unknown-unknown) runs-on: ubuntu-latest + timeout-minutes: 15 + concurrency: + group: ${{ github.workflow }}-test-cloudflare-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -212,6 +237,10 @@ jobs: test-spin: name: cargo check/build/test (spin native + wasm32-wasip1) runs-on: ubuntu-latest + timeout-minutes: 20 + concurrency: + group: ${{ github.workflow }}-test-spin-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -249,6 +278,10 @@ jobs: test-parity: name: cargo test (cross-adapter parity) runs-on: ubuntu-latest + timeout-minutes: 15 + concurrency: + group: ${{ github.workflow }}-test-parity-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -274,6 +307,10 @@ jobs: matrix: os: [ubuntu-latest, macos-latest] runs-on: ${{ matrix.os }} + timeout-minutes: 45 + concurrency: + group: ${{ github.workflow }}-test-cli-${{ matrix.os }}-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max steps: - uses: actions/checkout@v4 @@ -338,6 +375,10 @@ jobs: test-typescript: name: vitest runs-on: ubuntu-latest + timeout-minutes: 10 + concurrency: + group: ${{ github.workflow }}-test-typescript-${{ github.event_name == 'pull_request' && github.event.pull_request.user.type != 'Bot' && github.event.pull_request.user.login || github.run_id }} + queue: max defaults: run: working-directory: crates/trusted-server-js/lib