feat(ci): add runner selector and runner input for self-hosted fallback

This commit is contained in:
Adam Moussa 2026-10-05 17:53:57 -04:00
parent b1aeebfca5
commit 2dddc2c910
No known key found for this signature in database
36 changed files with 437 additions and 45 deletions

View file

@ -2,6 +2,14 @@ name: Dependency Review
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
allow-ghsas: allow-ghsas:
description: >- description: >-
Comma-separated GHSA IDs to exclude from failing the review. Comma-separated GHSA IDs to exclude from failing the review.
@ -14,7 +22,7 @@ permissions:
contents: read contents: read
jobs: jobs:
dependency-review: dependency-review:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0

View file

@ -23,6 +23,15 @@ name: Labeler
on: on:
workflow_call: workflow_call:
inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
permissions: permissions:
contents: read contents: read
@ -35,7 +44,7 @@ concurrency:
jobs: jobs:
label: label:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
steps: steps:
- name: Write central label rules - name: Write central label rules
run: | run: |

View file

@ -0,0 +1,122 @@
name: Select runner
# Picks the `runs-on` label for a caller's Linux jobs: GitHub-hosted while
# GitHub Actions is operational, the org self-hosted runner when it is not.
#
# How it decides, in order:
# 1. Fork pull requests always get `primary`. Fork code must never execute on
# a machine the org owns.
# 2. If the caller repo (or org) defines the Actions variable
# CI_RUNNER_OVERRIDE, its value is used verbatim. Set it to `self-hosted`
# 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
# automatic selection.
# 3. Otherwise the public GitHub status page is consulted. `fallback` is
# returned only when the Actions component reports `partial_outage`,
# `major_outage`, or `under_maintenance`. Everything else, including
# `degraded_performance` and an unreadable status page, returns
# `primary`. Degraded means slow-but-working, and one self-hosted box
# serialising every run is slower than that; treating "unknown" as
# healthy means a network problem on the self-hosted box does not
# silently route every run onto it.
#
# 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
# that adopts this selector makes the self-hosted runner a hard dependency.
# One org runner exists today; concurrent runs serialise on it. Keep this job
# short.
#
# There is no native `runs-on` fallback in GitHub Actions. An array of labels
# is an AND match, not an ordered preference, and a job whose labels match no
# online runner sits queued for 24 hours before failing. This selector plus
# the `runner` input on every org reusable is the supported substitute.
#
# Caller example:
# jobs:
# select-runner:
# uses: Sea-Haven-Industries/.github/.github/workflows/callable-select-runner.yaml@<sha> # vX.Y.Z
# ci:
# needs: select-runner
# uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@<sha> # vX.Y.Z
# with:
# runner: ${{ needs.select-runner.outputs.runner }}
# own-job:
# needs: select-runner
# runs-on: ${{ needs.select-runner.outputs.runner || 'ubuntu-latest' }}
#
# The self-hosted runner needs curl and jq for this job, and whatever the
# downstream jobs need (python3, shellcheck, Node/Python tool-cache access,
# Chromium system libraries, and so on). Steps that use `sudo` on
# ubuntu-latest must branch on the selected runner.
on:
workflow_call:
inputs:
primary:
description: "Runner label returned while GitHub Actions is operational."
type: string
required: false
default: ubuntu-latest
fallback:
description: >-
Runner label returned when GitHub Actions is not operational. This
selector job runs on this label.
type: string
required: false
default: self-hosted
outputs:
runner:
description: "Runner label for downstream `runs-on` and `runner` inputs."
value: ${{ jobs.select.outputs.runner }}
permissions: {}
jobs:
select:
runs-on: ${{ inputs.fallback }}
timeout-minutes: 5
outputs:
runner: ${{ steps.pick.outputs.runner }}
steps:
- name: Pick runner
id: pick
env:
PRIMARY: ${{ inputs.primary }}
FALLBACK: ${{ inputs.fallback }}
OVERRIDE: ${{ vars.CI_RUNNER_OVERRIDE }}
IS_FORK: ${{ github.event.pull_request.head.repo.fork }}
shell: bash
run: |
set -euo pipefail
pick() {
echo "Selected runner: $1 ($2)"
echo "runner=$1" >> "$GITHUB_OUTPUT"
}
if [ "$IS_FORK" = "true" ]; then
pick "$PRIMARY" "fork pull request"
exit 0
fi
if [ -n "$OVERRIDE" ]; then
pick "$OVERRIDE" "CI_RUNNER_OVERRIDE"
exit 0
fi
actions_status=$(
curl -sf --max-time 10 https://www.githubstatus.com/api/v2/components.json \
| jq -r '.components[] | select(.name == "Actions") | .status' \
|| true
)
actions_status=${actions_status:-unknown}
echo "GitHub Actions status: $actions_status"
# Statuspage component values: operational, degraded_performance,
# partial_outage, major_outage, under_maintenance.
case "$actions_status" in
partial_outage|major_outage|under_maintenance)
pick "$FALLBACK" "status $actions_status" ;;
*)
pick "$PRIMARY" "status $actions_status" ;;
esac

View file

@ -3,6 +3,14 @@ name: CD — CDK Deploy
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
node-version: node-version:
description: "Node.js version to use" description: "Node.js version to use"
type: string type: string
@ -54,7 +62,7 @@ permissions:
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 30 timeout-minutes: 30
# Serialise per deploy target so two pushes cannot deploy over each other. # Serialise per deploy target so two pushes cannot deploy over each other.
# The target is the stack selector, NOT stack-name: a multi-account app can # The target is the stack selector, NOT stack-name: a multi-account app can

View file

@ -19,6 +19,14 @@ name: CD — .NET Elastic Beanstalk
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
dotnet-version: dotnet-version:
description: ".NET SDK version" description: ".NET SDK version"
type: string type: string
@ -70,7 +78,7 @@ permissions:
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 30 timeout-minutes: 30
# Serialise per environment so two pushes cannot deploy over each other. # Serialise per environment so two pushes cannot deploy over each other.
# cancel-in-progress is FALSE on purpose: unlike CI, aborting midway can # cancel-in-progress is FALSE on purpose: unlike CI, aborting midway can

View file

@ -29,6 +29,14 @@ name: CD — HCP Fargate
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
environment: environment:
description: "GitHub Environment to deploy to (dev, staging, prod)" description: "GitHub Environment to deploy to (dev, staging, prod)"
type: string type: string
@ -105,7 +113,7 @@ permissions:
jobs: jobs:
deploy: deploy:
name: Deploy Fargate to ${{ inputs.environment }} name: Deploy Fargate to ${{ inputs.environment }}
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 30 timeout-minutes: 30
environment: ${{ inputs.environment }} environment: ${{ inputs.environment }}
concurrency: concurrency:

View file

@ -34,6 +34,14 @@ name: CD — HCP Lambda (Python)
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
environment: environment:
description: "GitHub Environment to deploy to (dev, staging, prod)" description: "GitHub Environment to deploy to (dev, staging, prod)"
type: string type: string
@ -69,7 +77,7 @@ permissions:
jobs: jobs:
deploy: deploy:
name: Deploy Lambda to ${{ inputs.environment }} name: Deploy Lambda to ${{ inputs.environment }}
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 30 timeout-minutes: 30
environment: ${{ inputs.environment }} environment: ${{ inputs.environment }}
concurrency: concurrency:

View file

@ -28,6 +28,14 @@ name: CD — HCP Lambda
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
environment: environment:
description: "GitHub Environment to deploy to (dev, staging, prod)" description: "GitHub Environment to deploy to (dev, staging, prod)"
type: string type: string
@ -63,7 +71,7 @@ permissions:
jobs: jobs:
deploy: deploy:
name: Deploy Lambda to ${{ inputs.environment }} name: Deploy Lambda to ${{ inputs.environment }}
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 30 timeout-minutes: 30
environment: ${{ inputs.environment }} environment: ${{ inputs.environment }}
concurrency: concurrency:

View file

@ -30,6 +30,14 @@ name: CD — HCP SPA
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
environment: environment:
description: "GitHub Environment to deploy to (dev, staging, prod)" description: "GitHub Environment to deploy to (dev, staging, prod)"
type: string type: string
@ -71,7 +79,7 @@ permissions:
jobs: jobs:
deploy: deploy:
name: Deploy SPA to ${{ inputs.environment }} name: Deploy SPA to ${{ inputs.environment }}
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 45 timeout-minutes: 45
environment: ${{ inputs.environment }} environment: ${{ inputs.environment }}
concurrency: concurrency:

View file

@ -55,6 +55,14 @@ name: CD — HCP static site
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
environment: environment:
description: "GitHub Environment to deploy to (dev, staging, prod)" description: "GitHub Environment to deploy to (dev, staging, prod)"
type: string type: string
@ -111,7 +119,7 @@ permissions:
jobs: jobs:
deploy: deploy:
name: Deploy static site to ${{ inputs.environment }} name: Deploy static site to ${{ inputs.environment }}
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 45 timeout-minutes: 45
environment: ${{ inputs.environment }} environment: ${{ inputs.environment }}
concurrency: concurrency:

View file

@ -3,6 +3,14 @@ name: CD — SAM Deploy
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
python-version: python-version:
description: "Python version to use" description: "Python version to use"
type: string type: string
@ -37,7 +45,7 @@ permissions:
jobs: jobs:
deploy: deploy:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 15 timeout-minutes: 15
# Serialise per stack so two pushes cannot deploy over each other. # Serialise per stack so two pushes cannot deploy over each other.
# stack-name is required and region always defaults, so the group is never # stack-name is required and region always defaults, so the group is never

View file

@ -34,6 +34,14 @@ name: CI — Autofix
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
presets: presets:
description: "Comma-separated presets: prettier, eslint, ruff, terraform" description: "Comma-separated presets: prettier, eslint, ruff, terraform"
type: string type: string
@ -88,7 +96,7 @@ jobs:
autofix: autofix:
name: autofix name: autofix
if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }}
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 15 timeout-minutes: 15
concurrency: concurrency:
group: ci-autofix-${{ github.workflow }}-${{ github.ref }} group: ci-autofix-${{ github.workflow }}-${{ github.ref }}

View file

@ -3,6 +3,14 @@ name: CI — .NET
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
dotnet-version: dotnet-version:
description: ".NET SDK version" description: ".NET SDK version"
type: string type: string
@ -25,7 +33,7 @@ permissions:
jobs: jobs:
ci: ci:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 20 timeout-minutes: 20
concurrency: concurrency:
group: ci-dotnet-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }} group: ci-dotnet-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}

View file

@ -20,6 +20,14 @@ name: CI — Frontend
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
node-version: node-version:
description: "Node.js version to use" description: "Node.js version to use"
type: string type: string
@ -47,7 +55,7 @@ permissions:
jobs: jobs:
guard: guard:
name: guard name: guard
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 10 timeout-minutes: 10
concurrency: concurrency:
group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-guard group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-guard
@ -152,7 +160,7 @@ jobs:
static: static:
name: static name: static
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 15 timeout-minutes: 15
concurrency: concurrency:
group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-static group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-static
@ -177,7 +185,7 @@ jobs:
build: build:
name: build name: build
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 15 timeout-minutes: 15
concurrency: concurrency:
group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-build group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-build
@ -201,7 +209,7 @@ jobs:
unit: unit:
name: unit (${{ matrix.shard }}) name: unit (${{ matrix.shard }})
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 15 timeout-minutes: 15
concurrency: concurrency:
group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-unit-${{ matrix.shard }} group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-unit-${{ matrix.shard }}
@ -234,7 +242,7 @@ jobs:
browser-smoke: browser-smoke:
name: browser-smoke name: browser-smoke
if: ${{ inputs.run-e2e }} if: ${{ inputs.run-e2e }}
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 20 timeout-minutes: 20
concurrency: concurrency:
group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-browser-smoke group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-browser-smoke

View file

@ -43,6 +43,14 @@ name: CI — Mobile iOS
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
node-version: node-version:
description: "Node.js version to use" description: "Node.js version to use"
type: string type: string
@ -119,7 +127,7 @@ permissions:
jobs: jobs:
js: js:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: ${{ inputs.js-timeout-minutes }} timeout-minutes: ${{ inputs.js-timeout-minutes }}
# cancel-in-progress is TRUE: superseding a push should abandon the older # cancel-in-progress is TRUE: superseding a push should abandon the older
# CI run, which produces no external side effects. # CI run, which produces no external side effects.
@ -293,7 +301,7 @@ jobs:
# Aggregator — keyed `ci` so a caller job keyed `ci` reports `ci / ci`. # Aggregator — keyed `ci` so a caller job keyed `ci` reports `ci / ci`.
needs: [js, ios-build] needs: [js, ios-build]
if: always() if: always()
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
concurrency: concurrency:
group: ci-mobile-ios-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-ci group: ci-mobile-ios-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-ci
cancel-in-progress: true cancel-in-progress: true

View file

@ -13,6 +13,14 @@ name: CI — Python (app)
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
python-version: python-version:
description: "Python version to use" description: "Python version to use"
type: string type: string
@ -31,7 +39,7 @@ permissions:
jobs: jobs:
lint: lint:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 10 timeout-minutes: 10
# The trailing segment of every group in this file is the job id written # The trailing segment of every group in this file is the job id written
# out literally, NOT `${{ github.job }}`. In a called workflow that # out literally, NOT `${{ github.job }}`. In a called workflow that
@ -86,7 +94,7 @@ jobs:
# Aggregator — keyed `ci` so a caller job keyed `ci` reports `ci / ci`. # Aggregator — keyed `ci` so a caller job keyed `ci` reports `ci / ci`.
needs: [lint] needs: [lint]
if: always() if: always()
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
concurrency: concurrency:
group: ci-python-app-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}-ci group: ci-python-app-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}-ci
cancel-in-progress: true cancel-in-progress: true

View file

@ -3,6 +3,14 @@ name: CI — Python / SAM
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
python-version: python-version:
description: "Python version to use" description: "Python version to use"
type: string type: string
@ -49,7 +57,7 @@ permissions:
jobs: jobs:
ci: ci:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 10 timeout-minutes: 10
concurrency: concurrency:
group: ci-python-sam-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }} group: ci-python-sam-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}

View file

@ -23,6 +23,14 @@ name: CI — Static Site
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
check-dir: check-dir:
description: "Directory the checks run against (repo root in source mode, build output dir in build mode)" description: "Directory the checks run against (repo root in source mode, build output dir in build mode)"
type: string type: string
@ -61,7 +69,7 @@ permissions:
jobs: jobs:
ci: ci:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 15 timeout-minutes: 15
concurrency: concurrency:
group: ci-static-${{ github.workflow }}-${{ github.ref }} group: ci-static-${{ github.workflow }}-${{ github.ref }}

View file

@ -28,6 +28,14 @@ name: CI — Terraform
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
terraform-version: terraform-version:
description: "Terraform version to install" description: "Terraform version to install"
type: string type: string
@ -47,7 +55,7 @@ permissions:
jobs: jobs:
terraform: terraform:
name: terraform name: terraform
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 15 timeout-minutes: 15
concurrency: concurrency:
group: ci-terraform-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }} group: ci-terraform-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}

View file

@ -3,6 +3,14 @@ name: CI — TypeScript / CDK
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
node-version: node-version:
description: "Node.js version to use" description: "Node.js version to use"
type: string type: string
@ -61,7 +69,7 @@ permissions:
jobs: jobs:
ci: ci:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 30 timeout-minutes: 30
concurrency: concurrency:
group: ci-typescript-cdk-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }} group: ci-typescript-cdk-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}

View file

@ -22,6 +22,14 @@ name: CI — TypeScript Frontend
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
node-version: node-version:
description: "Node.js version to use" description: "Node.js version to use"
type: string type: string
@ -80,7 +88,7 @@ permissions:
jobs: jobs:
ci: ci:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: 30 timeout-minutes: 30
concurrency: concurrency:
group: ci-typescript-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }} group: ci-typescript-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}

View file

@ -37,6 +37,11 @@ name: ci
# selectors into one argument and break those deploys. Every other finding was # selectors into one argument and break those deploys. Every other finding was
# fixed in the shell rather than suppressed. Suppressions stay per-line and # fixed in the shell rather than suppressed. Suppressions stay per-line and
# commented — never file-wide, and never by weakening this invocation. # commented — never file-wide, and never by weakening this invocation.
#
# Runner selection goes through callable-select-runner.yaml: GitHub-hosted
# while Actions is operational, the org self-hosted runner otherwise. The
# self-hosted box therefore needs python3, shellcheck, curl and jq, and must be
# x86_64 (the actionlint tarball below is linux_amd64).
on: on:
pull_request: pull_request:
@ -48,9 +53,13 @@ permissions:
contents: read contents: read
jobs: jobs:
select-runner:
uses: ./.github/workflows/callable-select-runner.yaml
isolation-tests: isolation-tests:
name: isolation-tests name: isolation-tests
runs-on: ubuntu-latest needs: select-runner
runs-on: ${{ needs.select-runner.outputs.runner || 'ubuntu-latest' }}
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@ -61,7 +70,8 @@ jobs:
actionlint: actionlint:
name: actionlint name: actionlint
runs-on: ubuntu-latest needs: select-runner
runs-on: ${{ needs.select-runner.outputs.runner || 'ubuntu-latest' }}
timeout-minutes: 10 timeout-minutes: 10
steps: steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
@ -85,9 +95,9 @@ jobs:
ci-complete: ci-complete:
name: ci-complete name: ci-complete
needs: [isolation-tests, actionlint] needs: [select-runner, isolation-tests, actionlint]
if: always() && !cancelled() if: always() && !cancelled()
runs-on: ubuntu-latest runs-on: ${{ needs.select-runner.outputs.runner || 'ubuntu-latest' }}
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- name: Require portions - name: Require portions

View file

@ -21,5 +21,11 @@ permissions:
issues: write issues: write
jobs: jobs:
select-runner:
uses: ./.github/workflows/callable-select-runner.yaml
label: label:
needs: select-runner
uses: ./.github/workflows/callable-labeler.yaml uses: ./.github/workflows/callable-labeler.yaml
with:
runner: ${{ needs.select-runner.outputs.runner }}

View file

@ -46,6 +46,14 @@ name: Release — Tag and GitHub Release
on: on:
workflow_call: workflow_call:
inputs: inputs:
runner:
description: >-
Runner label for this workflow's Linux jobs. Pass the `runner` output
of callable-select-runner.yaml to fall back to the org self-hosted
runner when GitHub Actions is degraded.
type: string
required: false
default: ubuntu-latest
version: version:
description: 'Version to release, e.g. "1.4.0". A leading "v" is accepted and stripped.' description: 'Version to release, e.g. "1.4.0". A leading "v" is accepted and stripped.'
type: string type: string
@ -103,7 +111,7 @@ permissions:
jobs: jobs:
release: release:
runs-on: ubuntu-latest runs-on: ${{ inputs.runner }}
timeout-minutes: ${{ inputs.timeout-minutes }} timeout-minutes: ${{ inputs.timeout-minutes }}
# Serialise per tag so two runs cannot race to create the same release. # Serialise per tag so two runs cannot race to create the same release.
# version is required and tag-prefix always defaults, so the group is never # version is required and tag-prefix always defaults, so the group is never

View file

@ -87,6 +87,8 @@ 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/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).
**`.github/workflows/release-on-merge.yaml`** — Repo automation (not callable): cuts a tag and GitHub Release for **this** repo whenever a merge to `main` changes a reusable workflow, so Dependabot has a release to advance consumer SHA pins to (see the pinning policy below). **`.github/workflows/release-on-merge.yaml`** — Repo automation (not callable): cuts a tag and GitHub Release for **this** repo whenever a merge to `main` changes a reusable workflow, so Dependabot has a release to advance consumer SHA pins to (see the pinning policy below).
@ -97,7 +99,7 @@ The formatter GitHub App is not on the main-branch bypass list.
### Workflow templates (`workflow-templates/`) ### Workflow templates (`workflow-templates/`)
Starter workflows offered on the org's **Actions → New workflow** page: `cdk-deploy`, `ci-dotnet`, `ci-hcp`, `ci-mobile-ios`, `ci-node`, `ci-python`, `ci-python-app`, `ci-static`, `ci-terraform`, `ci-typescript-frontend`, `dependency-review`, `dotnet-eb-deploy`, `hcp-fargate-deploy`, `hcp-spa-deploy`, `labeler`, `mobile-ios-deploy`, `release`, `sam-deploy`, `triage`. Each is a thin caller of the corresponding reusable workflow above (`triage` is standalone; `ci-hcp` is the converted-repo caller with autofix, frontend, terraform, and `ci-complete`). Every template has a paired `properties.json` (name, description, icon, `filePatterns` for auto-suggestion). Replace any `REPLACE-ME` placeholders before enabling. Templates are not scanned by Dependabot, so refresh their pinned SHAs opportunistically when editing one. Starter workflows offered on the org's **Actions → New workflow** page: `cdk-deploy`, `ci-dotnet`, `ci-hcp`, `ci-mobile-ios`, `ci-node`, `ci-python`, `ci-python-app`, `ci-static`, `ci-terraform`, `ci-typescript-frontend`, `dependency-review`, `dotnet-eb-deploy`, `hcp-fargate-deploy`, `hcp-spa-deploy`, `labeler`, `mobile-ios-deploy`, `release`, `sam-deploy`, `triage`. Each is a thin caller of the corresponding reusable workflow above (`triage` is standalone; `ci-hcp` is the converted-repo caller with autofix, frontend, terraform, and `ci-complete`). The CI, `dependency-review`, and `labeler` templates start with a `select-runner` job and pass its output as `runner`; the deploy and `release` templates do not, so deploys keep running on ephemeral GitHub-hosted runners with no self-hosted path. Every template has a paired `properties.json` (name, description, icon, `filePatterns` for auto-suggestion). Replace any `REPLACE-ME` placeholders before enabling. Templates are not scanned by Dependabot, so refresh their pinned SHAs opportunistically when editing one.
### Ref pinning policy ### Ref pinning policy

View file

@ -5,6 +5,11 @@ on:
merge_group: merge_group:
jobs: jobs:
select-runner:
# Picks GitHub-hosted while Actions is operational, the org self-hosted
# 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
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
# context resolves to the required `ci / ci`. # context resolves to the required `ci / ci`.
@ -12,4 +17,7 @@ jobs:
# Every input is optional. Common overrides: `solution` (defaults to *.sln # Every input is optional. Common overrides: `solution` (defaults to *.sln
# in the working directory), `working-directory`, and `dotnet-version` # in the working directory), `working-directory`, and `dotnet-version`
# (defaults to 8.0.x). This reusable has no `node-version` input. # (defaults to 8.0.x). This reusable has no `node-version` input.
uses: Sea-Haven-Industries/.github/.github/workflows/ci-dotnet.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21 needs: select-runner
uses: Sea-Haven-Industries/.github/.github/workflows/ci-dotnet.yaml@REPLACE-ME # vX.Y.Z
with:
runner: ${{ needs.select-runner.outputs.runner }}

View file

@ -10,36 +10,45 @@ permissions:
contents: read contents: read
jobs: jobs:
select-runner:
# Picks GitHub-hosted while Actions is operational, the org self-hosted
# 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
autofix: autofix:
needs: select-runner
if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork if: github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork
uses: Sea-Haven-Industries/.github/.github/workflows/ci-autofix.yaml@REPLACE-ME # vX.Y.Z uses: Sea-Haven-Industries/.github/.github/workflows/ci-autofix.yaml@REPLACE-ME # vX.Y.Z
permissions: permissions:
contents: write contents: write
secrets: inherit secrets: inherit
with: with:
runner: ${{ needs.select-runner.outputs.runner }}
presets: prettier,terraform presets: prettier,terraform
frontend: frontend:
needs: autofix needs: [select-runner, autofix]
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true') if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
uses: Sea-Haven-Industries/.github/.github/workflows/ci-frontend.yaml@REPLACE-ME # vX.Y.Z uses: Sea-Haven-Industries/.github/.github/workflows/ci-frontend.yaml@REPLACE-ME # vX.Y.Z
with: with:
runner: ${{ needs.select-runner.outputs.runner }}
node-version: "24" node-version: "24"
unit-shards: 4 unit-shards: 4
run-e2e: true run-e2e: true
terraform: terraform:
needs: autofix needs: [select-runner, autofix]
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true') if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@REPLACE-ME # vX.Y.Z uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@REPLACE-ME # vX.Y.Z
with: with:
runner: ${{ needs.select-runner.outputs.runner }}
terraform-version: "1.16.0" terraform-version: "1.16.0"
ci-complete: ci-complete:
name: ci-complete name: ci-complete
needs: [autofix, frontend, terraform] needs: [select-runner, autofix, frontend, terraform]
if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true') if: always() && !cancelled() && (needs.autofix.result == 'skipped' || needs.autofix.outputs.committed != 'true')
runs-on: ubuntu-latest runs-on: ${{ needs.select-runner.outputs.runner || 'ubuntu-latest' }}
timeout-minutes: 5 timeout-minutes: 5
steps: steps:
- name: Require portions - name: Require portions

View file

@ -5,11 +5,18 @@ on:
merge_group: merge_group:
jobs: jobs:
select-runner:
# Picks GitHub-hosted while Actions is operational, the org self-hosted
# 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
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
# check context resolves to the required `ci / ci`. # check context resolves to the required `ci / ci`.
uses: Sea-Haven-Industries/.github/.github/workflows/ci-mobile-ios.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21 needs: select-runner
uses: Sea-Haven-Industries/.github/.github/workflows/ci-mobile-ios.yaml@REPLACE-ME # vX.Y.Z
with: with:
runner: ${{ needs.select-runner.outputs.runner }}
# Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also # Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also
# the reusable workflow's default — passed explicitly to pin against drift. # the reusable workflow's default — passed explicitly to pin against drift.
node-version: "24" node-version: "24"

View file

@ -5,9 +5,16 @@ on:
merge_group: merge_group:
jobs: jobs:
select-runner:
# Picks GitHub-hosted while Actions is operational, the org self-hosted
# 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
ci: ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21 needs: select-runner
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-cdk.yaml@REPLACE-ME # vX.Y.Z
with: with:
runner: ${{ needs.select-runner.outputs.runner }}
# Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also the # Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also the
# reusable workflow's default — passed explicitly to pin against drift. # reusable workflow's default — passed explicitly to pin against drift.
node-version: "24" node-version: "24"

View file

@ -5,9 +5,17 @@ on:
merge_group: merge_group:
jobs: jobs:
select-runner:
# Picks GitHub-hosted while Actions is operational, the org self-hosted
# 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
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
# check context resolves to the required `ci / ci`. # check context resolves to the required `ci / ci`.
# #
# Every input is optional. Common override: `source-dirs` (ruff targets). # Every input is optional. Common override: `source-dirs` (ruff targets).
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-app.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21 needs: select-runner
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-app.yaml@REPLACE-ME # vX.Y.Z
with:
runner: ${{ needs.select-runner.outputs.runner }}

View file

@ -5,9 +5,16 @@ on:
merge_group: merge_group:
jobs: jobs:
select-runner:
# Picks GitHub-hosted while Actions is operational, the org self-hosted
# 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
ci: ci:
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21 needs: select-runner
uses: Sea-Haven-Industries/.github/.github/workflows/ci-python-sam.yaml@REPLACE-ME # vX.Y.Z
with: with:
runner: ${{ needs.select-runner.outputs.runner }}
run-tests: true run-tests: true
# ci-python-sam.yaml declares a `node-version` input (default "24") that # ci-python-sam.yaml declares a `node-version` input (default "24") that
# feeds the CDK CLI when run-cdk-synth is enabled. Passed explicitly so # feeds the CDK CLI when run-cdk-synth is enabled. Passed explicitly so

View file

@ -5,11 +5,18 @@ on:
merge_group: merge_group:
jobs: jobs:
select-runner:
# Picks GitHub-hosted while Actions is operational, the org self-hosted
# 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
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
# context resolves to the required `ci / ci`. # context resolves to the required `ci / ci`.
uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21 needs: select-runner
uses: Sea-Haven-Industries/.github/.github/workflows/ci-static.yaml@REPLACE-ME # vX.Y.Z
with: with:
runner: ${{ needs.select-runner.outputs.runner }}
# Pinned explicitly (handbook): local dev is Node 24 / npm 11, which # Pinned explicitly (handbook): local dev is Node 24 / npm 11, which
# generates lockfileVersion 3. Being explicit avoids lockfile drift. # generates lockfileVersion 3. Being explicit avoids lockfile drift.
node-version: "24" node-version: "24"

View file

@ -10,7 +10,14 @@ permissions:
contents: read contents: read
jobs: jobs:
select-runner:
# Picks GitHub-hosted while Actions is operational, the org self-hosted
# 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
terraform: terraform:
needs: select-runner
uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@REPLACE-ME # vX.Y.Z uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@REPLACE-ME # vX.Y.Z
with: with:
runner: ${{ needs.select-runner.outputs.runner }}
terraform-version: "1.16.0" terraform-version: "1.16.0"

View file

@ -5,11 +5,18 @@ on:
merge_group: merge_group:
jobs: jobs:
select-runner:
# Picks GitHub-hosted while Actions is operational, the org self-hosted
# 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
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
# context resolves to the required `ci / ci`. # context resolves to the required `ci / ci`.
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21 needs: select-runner
uses: Sea-Haven-Industries/.github/.github/workflows/ci-typescript-frontend.yaml@REPLACE-ME # vX.Y.Z
with: with:
runner: ${{ needs.select-runner.outputs.runner }}
# Pinned explicitly (handbook): local dev is Node 24 / npm 11, which # Pinned explicitly (handbook): local dev is Node 24 / npm 11, which
# generates lockfileVersion 3. Being explicit avoids lockfile drift. # generates lockfileVersion 3. Being explicit avoids lockfile drift.
node-version: "24" node-version: "24"

View file

@ -7,5 +7,13 @@ permissions:
contents: read contents: read
jobs: jobs:
select-runner:
# Picks GitHub-hosted while Actions is operational, the org self-hosted
# 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
dependency-review: dependency-review:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21 needs: select-runner
uses: Sea-Haven-Industries/.github/.github/workflows/callable-dependency-review.yaml@REPLACE-ME # vX.Y.Z
with:
runner: ${{ needs.select-runner.outputs.runner }}

View file

@ -12,5 +12,13 @@ permissions:
issues: write issues: write
jobs: jobs:
select-runner:
# Picks GitHub-hosted while Actions is operational, the org self-hosted
# 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
label: label:
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@47185fa602dffddb8297db5f3525d7c9bc05d7cd # v1.0.21 needs: select-runner
uses: Sea-Haven-Industries/.github/.github/workflows/callable-labeler.yaml@REPLACE-ME # vX.Y.Z
with:
runner: ${{ needs.select-runner.outputs.runner }}