mirror of
https://github.com/Sea-Haven-Industries/.github.git
synced 2026-10-07 12:48:56 +00:00
feat(ci): fall back when hosted jobs sit queued past max-queue-minutes
This commit is contained in:
parent
2dddc2c910
commit
e6eb9de2e5
15 changed files with 110 additions and 13 deletions
95
.github/workflows/callable-select-runner.yaml
vendored
95
.github/workflows/callable-select-runner.yaml
vendored
|
|
@ -11,14 +11,28 @@ name: Select runner
|
||||||
# to exercise the fallback path on demand, or to `ubuntu-latest` to pin
|
# to exercise the fallback path on demand, or to `ubuntu-latest` to pin
|
||||||
# hosted runners if the status page misreports. Delete it to return to
|
# hosted runners if the status page misreports. Delete it to return to
|
||||||
# automatic selection.
|
# automatic selection.
|
||||||
# 3. Otherwise the public GitHub status page is consulted. `fallback` is
|
# 3. The public GitHub status page is consulted. `fallback` is returned
|
||||||
# returned only when the Actions component reports `partial_outage`,
|
# when the Actions component reports `partial_outage`, `major_outage`,
|
||||||
# `major_outage`, or `under_maintenance`. Everything else, including
|
# or `under_maintenance`. `degraded_performance` and an unreadable
|
||||||
# `degraded_performance` and an unreadable status page, returns
|
# status page do not trigger fallback on their own. Degraded means
|
||||||
# `primary`. Degraded means slow-but-working, and one self-hosted box
|
# slow-but-working, and one self-hosted box serialising every run is
|
||||||
# serialising every run is slower than that; treating "unknown" as
|
# slower than that; treating "unknown" as healthy means a network
|
||||||
# healthy means a network problem on the self-hosted box does not
|
# problem on the self-hosted box does not silently route every run
|
||||||
# silently route every run onto it.
|
# onto it.
|
||||||
|
# 4. The calling repo's own job queue is consulted. If any job targeting
|
||||||
|
# `primary` has sat in `queued` for more than `max-queue-minutes`
|
||||||
|
# (default 5), hosted runners are not picking up work regardless of
|
||||||
|
# what the status page says, and `fallback` is returned. This catches
|
||||||
|
# the common failure mode of Actions "operational" on paper but
|
||||||
|
# queueing in practice. It only sees the calling repo, so a quiet repo
|
||||||
|
# with nothing in flight gets no signal here and relies on step 3. The
|
||||||
|
# caller must grant `actions: read` to this job; without it the check
|
||||||
|
# logs a warning and is skipped.
|
||||||
|
#
|
||||||
|
# What this cannot do: move a job that is already queued on a hosted runner.
|
||||||
|
# `timeout-minutes` does not start until a runner picks the job up, and a
|
||||||
|
# job's `runs-on` is fixed once created. Fallback is decided up front, once
|
||||||
|
# per run, before any downstream job is queued.
|
||||||
#
|
#
|
||||||
# This job itself runs on `fallback`. That is the only runner that can be
|
# This job itself runs on `fallback`. That is the only runner that can be
|
||||||
# expected to pick up work when hosted runners are down, so every workflow
|
# expected to pick up work when hosted runners are down, so every workflow
|
||||||
|
|
@ -35,6 +49,8 @@ name: Select runner
|
||||||
# jobs:
|
# jobs:
|
||||||
# select-runner:
|
# select-runner:
|
||||||
# uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@<sha> # vX.Y.Z
|
# uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@<sha> # vX.Y.Z
|
||||||
|
# permissions:
|
||||||
|
# actions: read
|
||||||
# ci:
|
# ci:
|
||||||
# needs: select-runner
|
# needs: select-runner
|
||||||
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@<sha> # vX.Y.Z
|
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@<sha> # vX.Y.Z
|
||||||
|
|
@ -64,12 +80,21 @@ on:
|
||||||
type: string
|
type: string
|
||||||
required: false
|
required: false
|
||||||
default: self-hosted
|
default: self-hosted
|
||||||
|
max-queue-minutes:
|
||||||
|
description: >-
|
||||||
|
Return `fallback` when any job in the calling repo targeting
|
||||||
|
`primary` has been queued longer than this many minutes. 0 disables
|
||||||
|
the queue check. Requires the caller to grant `actions: read`.
|
||||||
|
type: number
|
||||||
|
required: false
|
||||||
|
default: 5
|
||||||
outputs:
|
outputs:
|
||||||
runner:
|
runner:
|
||||||
description: "Runner label for downstream `runs-on` and `runner` inputs."
|
description: "Runner label for downstream `runs-on` and `runner` inputs."
|
||||||
value: ${{ jobs.select.outputs.runner }}
|
value: ${{ jobs.select.outputs.runner }}
|
||||||
|
|
||||||
permissions: {}
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
select:
|
select:
|
||||||
|
|
@ -83,8 +108,10 @@ jobs:
|
||||||
env:
|
env:
|
||||||
PRIMARY: ${{ inputs.primary }}
|
PRIMARY: ${{ inputs.primary }}
|
||||||
FALLBACK: ${{ inputs.fallback }}
|
FALLBACK: ${{ inputs.fallback }}
|
||||||
|
MAX_QUEUE_MINUTES: ${{ inputs.max-queue-minutes }}
|
||||||
OVERRIDE: ${{ vars.CI_RUNNER_OVERRIDE }}
|
OVERRIDE: ${{ vars.CI_RUNNER_OVERRIDE }}
|
||||||
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
|
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
@ -116,7 +143,51 @@ jobs:
|
||||||
# partial_outage, major_outage, under_maintenance.
|
# partial_outage, major_outage, under_maintenance.
|
||||||
case "$actions_status" in
|
case "$actions_status" in
|
||||||
partial_outage|major_outage|under_maintenance)
|
partial_outage|major_outage|under_maintenance)
|
||||||
pick "$FALLBACK" "status $actions_status" ;;
|
pick "$FALLBACK" "status $actions_status"
|
||||||
*)
|
exit 0 ;;
|
||||||
pick "$PRIMARY" "status $actions_status" ;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
|
# Second signal: is this repo already waiting on hosted runners?
|
||||||
|
# Jobs blocked on `needs` are not listed by the jobs API until they
|
||||||
|
# are actually queued, so status == "queued" plus age is a clean
|
||||||
|
# "no runner has picked this up" measure. A token without
|
||||||
|
# actions:read makes every call fail; that degrades to stuck=0 with
|
||||||
|
# a warning rather than failing the job.
|
||||||
|
stuck=0
|
||||||
|
if [ "$MAX_QUEUE_MINUTES" -gt 0 ]; then
|
||||||
|
api() {
|
||||||
|
curl -sf --max-time 10 \
|
||||||
|
-H "Accept: application/vnd.github+json" \
|
||||||
|
-H "Authorization: Bearer $GH_TOKEN" \
|
||||||
|
"$GITHUB_API_URL/repos/$GITHUB_REPOSITORY/$1"
|
||||||
|
}
|
||||||
|
api_ok=true
|
||||||
|
runs=""
|
||||||
|
for status in queued in_progress; do
|
||||||
|
ids=$(api "actions/runs?status=$status&per_page=20" | jq -r '.workflow_runs[].id') || { api_ok=false; break; }
|
||||||
|
runs="$runs $ids"
|
||||||
|
done
|
||||||
|
if [ "$api_ok" = true ]; then
|
||||||
|
for run in $(echo "$runs" | tr ' ' '\n' | sort -u | sed '/^$/d'); do
|
||||||
|
n=$(api "actions/runs/$run/jobs?per_page=100" | jq \
|
||||||
|
--arg primary "$PRIMARY" --argjson max "$MAX_QUEUE_MINUTES" \
|
||||||
|
'[.jobs[] | select(.status == "queued"
|
||||||
|
and (.labels | index($primary))
|
||||||
|
and (.created_at | fromdateiso8601) < (now - $max * 60))]
|
||||||
|
| length') || { api_ok=false; break; }
|
||||||
|
stuck=$((stuck + n))
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
if [ "$api_ok" = true ]; then
|
||||||
|
echo "Hosted jobs queued longer than ${MAX_QUEUE_MINUTES}m in this repo: $stuck"
|
||||||
|
else
|
||||||
|
echo "::warning::Could not read this repo's job queue (does the caller grant actions: read to this job?). Skipping the queue check."
|
||||||
|
stuck=0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$stuck" -gt 0 ]; then
|
||||||
|
pick "$FALLBACK" "$stuck hosted job(s) queued over ${MAX_QUEUE_MINUTES}m"
|
||||||
|
else
|
||||||
|
pick "$PRIMARY" "status $actions_status"
|
||||||
|
fi
|
||||||
|
|
|
||||||
2
.github/workflows/ci.yaml
vendored
2
.github/workflows/ci.yaml
vendored
|
|
@ -55,6 +55,8 @@ permissions:
|
||||||
jobs:
|
jobs:
|
||||||
select-runner:
|
select-runner:
|
||||||
uses: ./.github/workflows/callable-select-runner.yaml
|
uses: ./.github/workflows/callable-select-runner.yaml
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
isolation-tests:
|
isolation-tests:
|
||||||
name: isolation-tests
|
name: isolation-tests
|
||||||
|
|
|
||||||
2
.github/workflows/labeler.yaml
vendored
2
.github/workflows/labeler.yaml
vendored
|
|
@ -23,6 +23,8 @@ permissions:
|
||||||
jobs:
|
jobs:
|
||||||
select-runner:
|
select-runner:
|
||||||
uses: ./.github/workflows/callable-select-runner.yaml
|
uses: ./.github/workflows/callable-select-runner.yaml
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
label:
|
label:
|
||||||
needs: select-runner
|
needs: select-runner
|
||||||
|
|
|
||||||
|
|
@ -87,7 +87,7 @@ The formatter GitHub App is not on the main-branch bypass list.
|
||||||
|
|
||||||
**`.github/workflows/callable-dependency-review.yaml`** — Dependency review on PRs, failing on high severity. Requires Dependency Graph.
|
**`.github/workflows/callable-dependency-review.yaml`** — Dependency review on PRs, failing on high severity. Requires Dependency Graph.
|
||||||
|
|
||||||
**`.github/workflows/callable-select-runner.yaml`** — Runner selector. Outputs `runner`: `self-hosted` when the GitHub status page reports the Actions component as `partial_outage`, `major_outage`, or `under_maintenance`; otherwise `ubuntu-latest`, including on `degraded_performance` and when the status page cannot be read. Fork PRs always get `ubuntu-latest`. A caller-repo Actions variable `CI_RUNNER_OVERRIDE` forces a value; set it to `self-hosted` to exercise the fallback. The selector job itself runs on the fallback runner, so adopting it makes the org self-hosted runner a hard dependency of that workflow. Every Linux reusable above takes a `runner` input (default `ubuntu-latest`) to receive the output; `cd-mobile-ios.yaml` is macOS-only and does not. There is no native `runs-on` fallback in GitHub Actions; a label array is an AND match and an unmatched job queues for 24 hours.
|
**`.github/workflows/callable-select-runner.yaml`** — Runner selector. Outputs `runner`: `self-hosted` when the GitHub status page reports the Actions component as `partial_outage`, `major_outage`, or `under_maintenance`, or when any job in the calling repo targeting `ubuntu-latest` has been queued longer than `max-queue-minutes` (default 5); otherwise `ubuntu-latest`, including on `degraded_performance` and when the status page cannot be read. The queue check needs the caller to grant `permissions: actions: read` on the selector job; without it the check is skipped with a warning. Fork PRs always get `ubuntu-latest`. A caller-repo Actions variable `CI_RUNNER_OVERRIDE` forces a value; set it to `self-hosted` to exercise the fallback. Fallback is decided once per run before downstream jobs are queued; a job already waiting on a hosted runner cannot be moved, and `timeout-minutes` does not count queue time. The selector job itself runs on the fallback runner, so adopting it makes the org self-hosted runner a hard dependency of that workflow. Every Linux reusable above takes a `runner` input (default `ubuntu-latest`) to receive the output; `cd-mobile-ios.yaml` is macOS-only and does not. There is no native `runs-on` fallback in GitHub Actions; a label array is an AND match and an unmatched job queues for 24 hours.
|
||||||
|
|
||||||
**`.github/workflows/release.yaml`** — Reusable release workflow: creates an annotated git tag at a commit and publishes a GitHub Release pointing at it. The version is an input (not read from a manifest).
|
**`.github/workflows/release.yaml`** — Reusable release workflow: creates an annotated git tag at a commit and publishes a GitHub Release pointing at it. The version is an input (not read from a manifest).
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ jobs:
|
||||||
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
||||||
# runner otherwise. See callable-select-runner.yaml for the policy.
|
# runner otherwise. See callable-select-runner.yaml for the policy.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
|
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ jobs:
|
||||||
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
||||||
# runner otherwise. See callable-select-runner.yaml for the policy.
|
# runner otherwise. See callable-select-runner.yaml for the policy.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
autofix:
|
autofix:
|
||||||
needs: select-runner
|
needs: select-runner
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ jobs:
|
||||||
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
||||||
# runner otherwise. See callable-select-runner.yaml for the policy.
|
# runner otherwise. See callable-select-runner.yaml for the policy.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
# Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the
|
# Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ jobs:
|
||||||
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
||||||
# runner otherwise. See callable-select-runner.yaml for the policy.
|
# runner otherwise. See callable-select-runner.yaml for the policy.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
needs: select-runner
|
needs: select-runner
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ jobs:
|
||||||
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
||||||
# runner otherwise. See callable-select-runner.yaml for the policy.
|
# runner otherwise. See callable-select-runner.yaml for the policy.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
# Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the
|
# Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ jobs:
|
||||||
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
||||||
# runner otherwise. See callable-select-runner.yaml for the policy.
|
# runner otherwise. See callable-select-runner.yaml for the policy.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
needs: select-runner
|
needs: select-runner
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ jobs:
|
||||||
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
||||||
# runner otherwise. See callable-select-runner.yaml for the policy.
|
# runner otherwise. See callable-select-runner.yaml for the policy.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
|
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ jobs:
|
||||||
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
||||||
# runner otherwise. See callable-select-runner.yaml for the policy.
|
# runner otherwise. See callable-select-runner.yaml for the policy.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
terraform:
|
terraform:
|
||||||
needs: select-runner
|
needs: select-runner
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,8 @@ jobs:
|
||||||
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
||||||
# runner otherwise. See callable-select-runner.yaml for the policy.
|
# runner otherwise. See callable-select-runner.yaml for the policy.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
ci:
|
ci:
|
||||||
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
|
# Job id MUST stay `ci`: the reusable's job is also `ci`, so the check
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ jobs:
|
||||||
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
||||||
# runner otherwise. See callable-select-runner.yaml for the policy.
|
# runner otherwise. See callable-select-runner.yaml for the policy.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
dependency-review:
|
dependency-review:
|
||||||
needs: select-runner
|
needs: select-runner
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,8 @@ jobs:
|
||||||
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
# Picks GitHub-hosted while Actions is operational, the org self-hosted
|
||||||
# runner otherwise. See callable-select-runner.yaml for the policy.
|
# runner otherwise. See callable-select-runner.yaml for the policy.
|
||||||
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@REPLACE-ME # vX.Y.Z
|
||||||
|
permissions:
|
||||||
|
actions: read
|
||||||
|
|
||||||
label:
|
label:
|
||||||
needs: select-runner
|
needs: select-runner
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue