From 2dddc2c910e106a06acaa6efee714be9f3083c94 Mon Sep 17 00:00:00 2001 From: Adam Moussa Date: Mon, 5 Oct 2026 17:53:57 -0400 Subject: [PATCH] feat(ci): add runner selector and runner input for self-hosted fallback --- .../workflows/callable-dependency-review.yaml | 10 +- .github/workflows/callable-labeler.yaml | 11 +- .github/workflows/callable-select-runner.yaml | 122 ++++++++++++++++++ .github/workflows/cd-cdk.yaml | 10 +- .github/workflows/cd-dotnet-eb.yaml | 10 +- .github/workflows/cd-hcp-fargate.yaml | 10 +- .github/workflows/cd-hcp-lambda-python.yaml | 10 +- .github/workflows/cd-hcp-lambda.yaml | 10 +- .github/workflows/cd-hcp-spa.yaml | 10 +- .github/workflows/cd-hcp-static.yaml | 10 +- .github/workflows/cd-sam.yaml | 10 +- .github/workflows/ci-autofix.yaml | 10 +- .github/workflows/ci-dotnet.yaml | 10 +- .github/workflows/ci-frontend.yaml | 18 ++- .github/workflows/ci-mobile-ios.yaml | 12 +- .github/workflows/ci-python-app.yaml | 12 +- .github/workflows/ci-python-sam.yaml | 10 +- .github/workflows/ci-static.yaml | 10 +- .github/workflows/ci-terraform.yaml | 10 +- .github/workflows/ci-typescript-cdk.yaml | 10 +- .github/workflows/ci-typescript-frontend.yaml | 10 +- .github/workflows/ci.yaml | 18 ++- .github/workflows/labeler.yaml | 6 + .github/workflows/release.yaml | 10 +- README.md | 4 +- workflow-templates/ci-dotnet.yml | 10 +- workflow-templates/ci-hcp.yml | 17 ++- workflow-templates/ci-mobile-ios.yml | 9 +- workflow-templates/ci-node.yml | 9 +- workflow-templates/ci-python-app.yml | 10 +- workflow-templates/ci-python.yml | 9 +- workflow-templates/ci-static.yml | 9 +- workflow-templates/ci-terraform.yml | 7 + workflow-templates/ci-typescript-frontend.yml | 9 +- workflow-templates/dependency-review.yml | 10 +- workflow-templates/labeler.yml | 10 +- 36 files changed, 437 insertions(+), 45 deletions(-) create mode 100644 .github/workflows/callable-select-runner.yaml diff --git a/.github/workflows/callable-dependency-review.yaml b/.github/workflows/callable-dependency-review.yaml index 0d1545a..e406728 100644 --- a/.github/workflows/callable-dependency-review.yaml +++ b/.github/workflows/callable-dependency-review.yaml @@ -2,6 +2,14 @@ name: Dependency Review on: 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 allow-ghsas: description: >- Comma-separated GHSA IDs to exclude from failing the review. @@ -14,7 +22,7 @@ permissions: contents: read jobs: dependency-review: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 diff --git a/.github/workflows/callable-labeler.yaml b/.github/workflows/callable-labeler.yaml index e0d0f21..aee8e15 100644 --- a/.github/workflows/callable-labeler.yaml +++ b/.github/workflows/callable-labeler.yaml @@ -23,6 +23,15 @@ name: Labeler on: 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: contents: read @@ -35,7 +44,7 @@ concurrency: jobs: label: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} steps: - name: Write central label rules run: | diff --git a/.github/workflows/callable-select-runner.yaml b/.github/workflows/callable-select-runner.yaml new file mode 100644 index 0000000..c7a4282 --- /dev/null +++ b/.github/workflows/callable-select-runner.yaml @@ -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@ # vX.Y.Z +# ci: +# needs: select-runner +# uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@ # 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 diff --git a/.github/workflows/cd-cdk.yaml b/.github/workflows/cd-cdk.yaml index 9583ebd..068687f 100644 --- a/.github/workflows/cd-cdk.yaml +++ b/.github/workflows/cd-cdk.yaml @@ -3,6 +3,14 @@ name: CD — CDK Deploy on: 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 node-version: description: "Node.js version to use" type: string @@ -54,7 +62,7 @@ permissions: jobs: deploy: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 30 # 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 diff --git a/.github/workflows/cd-dotnet-eb.yaml b/.github/workflows/cd-dotnet-eb.yaml index 01251f4..38fd1b7 100644 --- a/.github/workflows/cd-dotnet-eb.yaml +++ b/.github/workflows/cd-dotnet-eb.yaml @@ -19,6 +19,14 @@ name: CD — .NET Elastic Beanstalk on: 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 dotnet-version: description: ".NET SDK version" type: string @@ -70,7 +78,7 @@ permissions: jobs: deploy: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 30 # Serialise per environment so two pushes cannot deploy over each other. # cancel-in-progress is FALSE on purpose: unlike CI, aborting midway can diff --git a/.github/workflows/cd-hcp-fargate.yaml b/.github/workflows/cd-hcp-fargate.yaml index 8e90bba..aade4da 100644 --- a/.github/workflows/cd-hcp-fargate.yaml +++ b/.github/workflows/cd-hcp-fargate.yaml @@ -29,6 +29,14 @@ name: CD — HCP Fargate on: 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 environment: description: "GitHub Environment to deploy to (dev, staging, prod)" type: string @@ -105,7 +113,7 @@ permissions: jobs: deploy: name: Deploy Fargate to ${{ inputs.environment }} - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 30 environment: ${{ inputs.environment }} concurrency: diff --git a/.github/workflows/cd-hcp-lambda-python.yaml b/.github/workflows/cd-hcp-lambda-python.yaml index 07ec987..5eb7335 100644 --- a/.github/workflows/cd-hcp-lambda-python.yaml +++ b/.github/workflows/cd-hcp-lambda-python.yaml @@ -34,6 +34,14 @@ name: CD — HCP Lambda (Python) on: 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 environment: description: "GitHub Environment to deploy to (dev, staging, prod)" type: string @@ -69,7 +77,7 @@ permissions: jobs: deploy: name: Deploy Lambda to ${{ inputs.environment }} - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 30 environment: ${{ inputs.environment }} concurrency: diff --git a/.github/workflows/cd-hcp-lambda.yaml b/.github/workflows/cd-hcp-lambda.yaml index c731ef3..17b3050 100644 --- a/.github/workflows/cd-hcp-lambda.yaml +++ b/.github/workflows/cd-hcp-lambda.yaml @@ -28,6 +28,14 @@ name: CD — HCP Lambda on: 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 environment: description: "GitHub Environment to deploy to (dev, staging, prod)" type: string @@ -63,7 +71,7 @@ permissions: jobs: deploy: name: Deploy Lambda to ${{ inputs.environment }} - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 30 environment: ${{ inputs.environment }} concurrency: diff --git a/.github/workflows/cd-hcp-spa.yaml b/.github/workflows/cd-hcp-spa.yaml index b8f6ea3..b2c2da0 100644 --- a/.github/workflows/cd-hcp-spa.yaml +++ b/.github/workflows/cd-hcp-spa.yaml @@ -30,6 +30,14 @@ name: CD — HCP SPA on: 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 environment: description: "GitHub Environment to deploy to (dev, staging, prod)" type: string @@ -71,7 +79,7 @@ permissions: jobs: deploy: name: Deploy SPA to ${{ inputs.environment }} - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 45 environment: ${{ inputs.environment }} concurrency: diff --git a/.github/workflows/cd-hcp-static.yaml b/.github/workflows/cd-hcp-static.yaml index bfaaa91..3e7c5a8 100644 --- a/.github/workflows/cd-hcp-static.yaml +++ b/.github/workflows/cd-hcp-static.yaml @@ -55,6 +55,14 @@ name: CD — HCP static site on: 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 environment: description: "GitHub Environment to deploy to (dev, staging, prod)" type: string @@ -111,7 +119,7 @@ permissions: jobs: deploy: name: Deploy static site to ${{ inputs.environment }} - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 45 environment: ${{ inputs.environment }} concurrency: diff --git a/.github/workflows/cd-sam.yaml b/.github/workflows/cd-sam.yaml index af58269..2dbc364 100644 --- a/.github/workflows/cd-sam.yaml +++ b/.github/workflows/cd-sam.yaml @@ -3,6 +3,14 @@ name: CD — SAM Deploy on: 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 python-version: description: "Python version to use" type: string @@ -37,7 +45,7 @@ permissions: jobs: deploy: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 15 # Serialise per stack so two pushes cannot deploy over each other. # stack-name is required and region always defaults, so the group is never diff --git a/.github/workflows/ci-autofix.yaml b/.github/workflows/ci-autofix.yaml index 074eada..5b8f86c 100644 --- a/.github/workflows/ci-autofix.yaml +++ b/.github/workflows/ci-autofix.yaml @@ -34,6 +34,14 @@ name: CI — Autofix on: 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 presets: description: "Comma-separated presets: prettier, eslint, ruff, terraform" type: string @@ -88,7 +96,7 @@ jobs: autofix: name: autofix if: ${{ github.event_name == 'pull_request' && !github.event.pull_request.head.repo.fork }} - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 15 concurrency: group: ci-autofix-${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/ci-dotnet.yaml b/.github/workflows/ci-dotnet.yaml index cd9fb0d..3758926 100644 --- a/.github/workflows/ci-dotnet.yaml +++ b/.github/workflows/ci-dotnet.yaml @@ -3,6 +3,14 @@ name: CI — .NET on: 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 dotnet-version: description: ".NET SDK version" type: string @@ -25,7 +33,7 @@ permissions: jobs: ci: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 20 concurrency: group: ci-dotnet-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }} diff --git a/.github/workflows/ci-frontend.yaml b/.github/workflows/ci-frontend.yaml index 60c1035..1295105 100644 --- a/.github/workflows/ci-frontend.yaml +++ b/.github/workflows/ci-frontend.yaml @@ -20,6 +20,14 @@ name: CI — Frontend on: 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 node-version: description: "Node.js version to use" type: string @@ -47,7 +55,7 @@ permissions: jobs: guard: name: guard - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 10 concurrency: group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-guard @@ -152,7 +160,7 @@ jobs: static: name: static - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 15 concurrency: group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-static @@ -177,7 +185,7 @@ jobs: build: name: build - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 15 concurrency: group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-build @@ -201,7 +209,7 @@ jobs: unit: name: unit (${{ matrix.shard }}) - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 15 concurrency: group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-unit-${{ matrix.shard }} @@ -234,7 +242,7 @@ jobs: browser-smoke: name: browser-smoke if: ${{ inputs.run-e2e }} - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 20 concurrency: group: ci-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-browser-smoke diff --git a/.github/workflows/ci-mobile-ios.yaml b/.github/workflows/ci-mobile-ios.yaml index 5aa4c50..5c942ca 100644 --- a/.github/workflows/ci-mobile-ios.yaml +++ b/.github/workflows/ci-mobile-ios.yaml @@ -43,6 +43,14 @@ name: CI — Mobile iOS on: 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 node-version: description: "Node.js version to use" type: string @@ -119,7 +127,7 @@ permissions: jobs: js: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: ${{ inputs.js-timeout-minutes }} # cancel-in-progress is TRUE: superseding a push should abandon the older # 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`. needs: [js, ios-build] if: always() - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} concurrency: group: ci-mobile-ios-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }}-ci cancel-in-progress: true diff --git a/.github/workflows/ci-python-app.yaml b/.github/workflows/ci-python-app.yaml index bb41646..f1ee49d 100644 --- a/.github/workflows/ci-python-app.yaml +++ b/.github/workflows/ci-python-app.yaml @@ -13,6 +13,14 @@ name: CI — Python (app) on: 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 python-version: description: "Python version to use" type: string @@ -31,7 +39,7 @@ permissions: jobs: lint: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 10 # The trailing segment of every group in this file is the job id written # 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`. needs: [lint] if: always() - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} concurrency: group: ci-python-app-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }}-ci cancel-in-progress: true diff --git a/.github/workflows/ci-python-sam.yaml b/.github/workflows/ci-python-sam.yaml index 0db0c69..06b05b1 100644 --- a/.github/workflows/ci-python-sam.yaml +++ b/.github/workflows/ci-python-sam.yaml @@ -3,6 +3,14 @@ name: CI — Python / SAM on: 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 python-version: description: "Python version to use" type: string @@ -49,7 +57,7 @@ permissions: jobs: ci: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 10 concurrency: group: ci-python-sam-${{ github.workflow }}-${{ github.ref }}-${{ inputs.source-dirs }} diff --git a/.github/workflows/ci-static.yaml b/.github/workflows/ci-static.yaml index b8f5543..d18eddc 100644 --- a/.github/workflows/ci-static.yaml +++ b/.github/workflows/ci-static.yaml @@ -23,6 +23,14 @@ name: CI — Static Site on: 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 check-dir: description: "Directory the checks run against (repo root in source mode, build output dir in build mode)" type: string @@ -61,7 +69,7 @@ permissions: jobs: ci: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 15 concurrency: group: ci-static-${{ github.workflow }}-${{ github.ref }} diff --git a/.github/workflows/ci-terraform.yaml b/.github/workflows/ci-terraform.yaml index 29dc9f9..f942705 100644 --- a/.github/workflows/ci-terraform.yaml +++ b/.github/workflows/ci-terraform.yaml @@ -28,6 +28,14 @@ name: CI — Terraform on: 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 terraform-version: description: "Terraform version to install" type: string @@ -47,7 +55,7 @@ permissions: jobs: terraform: name: terraform - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 15 concurrency: group: ci-terraform-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }} diff --git a/.github/workflows/ci-typescript-cdk.yaml b/.github/workflows/ci-typescript-cdk.yaml index ad074ea..2a053b9 100644 --- a/.github/workflows/ci-typescript-cdk.yaml +++ b/.github/workflows/ci-typescript-cdk.yaml @@ -3,6 +3,14 @@ name: CI — TypeScript / CDK on: 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 node-version: description: "Node.js version to use" type: string @@ -61,7 +69,7 @@ permissions: jobs: ci: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 30 concurrency: group: ci-typescript-cdk-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }} diff --git a/.github/workflows/ci-typescript-frontend.yaml b/.github/workflows/ci-typescript-frontend.yaml index 733b137..2ca9aeb 100644 --- a/.github/workflows/ci-typescript-frontend.yaml +++ b/.github/workflows/ci-typescript-frontend.yaml @@ -22,6 +22,14 @@ name: CI — TypeScript Frontend on: 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 node-version: description: "Node.js version to use" type: string @@ -80,7 +88,7 @@ permissions: jobs: ci: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: 30 concurrency: group: ci-typescript-frontend-${{ github.workflow }}-${{ github.ref }}-${{ inputs.working-directory }} diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 60c1150..27299f7 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -37,6 +37,11 @@ name: ci # selectors into one argument and break those deploys. Every other finding was # fixed in the shell rather than suppressed. Suppressions stay per-line and # 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: pull_request: @@ -48,9 +53,13 @@ permissions: contents: read jobs: + select-runner: + uses: ./.github/workflows/callable-select-runner.yaml + isolation-tests: name: isolation-tests - runs-on: ubuntu-latest + needs: select-runner + runs-on: ${{ needs.select-runner.outputs.runner || 'ubuntu-latest' }} timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -61,7 +70,8 @@ jobs: actionlint: name: actionlint - runs-on: ubuntu-latest + needs: select-runner + runs-on: ${{ needs.select-runner.outputs.runner || 'ubuntu-latest' }} timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -85,9 +95,9 @@ jobs: ci-complete: name: ci-complete - needs: [isolation-tests, actionlint] + needs: [select-runner, isolation-tests, actionlint] if: always() && !cancelled() - runs-on: ubuntu-latest + runs-on: ${{ needs.select-runner.outputs.runner || 'ubuntu-latest' }} timeout-minutes: 5 steps: - name: Require portions diff --git a/.github/workflows/labeler.yaml b/.github/workflows/labeler.yaml index 6f3a8c0..d7cf960 100644 --- a/.github/workflows/labeler.yaml +++ b/.github/workflows/labeler.yaml @@ -21,5 +21,11 @@ permissions: issues: write jobs: + select-runner: + uses: ./.github/workflows/callable-select-runner.yaml + label: + needs: select-runner uses: ./.github/workflows/callable-labeler.yaml + with: + runner: ${{ needs.select-runner.outputs.runner }} diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7923e5c..f9ab17d 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -46,6 +46,14 @@ name: Release — Tag and GitHub Release on: 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 version: description: 'Version to release, e.g. "1.4.0". A leading "v" is accepted and stripped.' type: string @@ -103,7 +111,7 @@ permissions: jobs: release: - runs-on: ubuntu-latest + runs-on: ${{ inputs.runner }} timeout-minutes: ${{ inputs.timeout-minutes }} # 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 diff --git a/README.md b/README.md index 55d2d35..c47acc5 100644 --- a/README.md +++ b/README.md @@ -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-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-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/`) -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 diff --git a/workflow-templates/ci-dotnet.yml b/workflow-templates/ci-dotnet.yml index e2e79f3..b8d20ad 100644 --- a/workflow-templates/ci-dotnet.yml +++ b/workflow-templates/ci-dotnet.yml @@ -5,6 +5,11 @@ on: merge_group: 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: # Job id MUST stay `ci`: the reusable's job is also `ci`, so the check # context resolves to the required `ci / ci`. @@ -12,4 +17,7 @@ jobs: # Every input is optional. Common overrides: `solution` (defaults to *.sln # in the working directory), `working-directory`, and `dotnet-version` # (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 }} diff --git a/workflow-templates/ci-hcp.yml b/workflow-templates/ci-hcp.yml index ac655d2..24bc137 100644 --- a/workflow-templates/ci-hcp.yml +++ b/workflow-templates/ci-hcp.yml @@ -10,36 +10,45 @@ permissions: contents: read 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: + needs: select-runner 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 permissions: contents: write secrets: inherit with: + runner: ${{ needs.select-runner.outputs.runner }} presets: prettier,terraform frontend: - needs: autofix + needs: [select-runner, autofix] 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 with: + runner: ${{ needs.select-runner.outputs.runner }} node-version: "24" unit-shards: 4 run-e2e: true terraform: - needs: autofix + needs: [select-runner, autofix] 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 with: + runner: ${{ needs.select-runner.outputs.runner }} terraform-version: "1.16.0" 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') - runs-on: ubuntu-latest + runs-on: ${{ needs.select-runner.outputs.runner || 'ubuntu-latest' }} timeout-minutes: 5 steps: - name: Require portions diff --git a/workflow-templates/ci-mobile-ios.yml b/workflow-templates/ci-mobile-ios.yml index 29ec974..17e4037 100644 --- a/workflow-templates/ci-mobile-ios.yml +++ b/workflow-templates/ci-mobile-ios.yml @@ -5,11 +5,18 @@ on: merge_group: 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: # Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the # 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: + runner: ${{ needs.select-runner.outputs.runner }} # Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also # the reusable workflow's default — passed explicitly to pin against drift. node-version: "24" diff --git a/workflow-templates/ci-node.yml b/workflow-templates/ci-node.yml index 5e80b2c..290ec21 100644 --- a/workflow-templates/ci-node.yml +++ b/workflow-templates/ci-node.yml @@ -5,9 +5,16 @@ on: merge_group: 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: - 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: + runner: ${{ needs.select-runner.outputs.runner }} # Matches local dev (Node 24 / npm 11, lockfileVersion 3). This is also the # reusable workflow's default — passed explicitly to pin against drift. node-version: "24" diff --git a/workflow-templates/ci-python-app.yml b/workflow-templates/ci-python-app.yml index 3a6a2fb..6c91c42 100644 --- a/workflow-templates/ci-python-app.yml +++ b/workflow-templates/ci-python-app.yml @@ -5,9 +5,17 @@ on: merge_group: 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: # Job id MUST stay `ci`: the reusable's aggregator job is also `ci`, so the # check context resolves to the required `ci / ci`. # # 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 }} diff --git a/workflow-templates/ci-python.yml b/workflow-templates/ci-python.yml index 68bafe3..31a5e54 100644 --- a/workflow-templates/ci-python.yml +++ b/workflow-templates/ci-python.yml @@ -5,9 +5,16 @@ on: merge_group: 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: - 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: + runner: ${{ needs.select-runner.outputs.runner }} run-tests: true # 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 diff --git a/workflow-templates/ci-static.yml b/workflow-templates/ci-static.yml index 5809cee..43bebdf 100644 --- a/workflow-templates/ci-static.yml +++ b/workflow-templates/ci-static.yml @@ -5,11 +5,18 @@ on: merge_group: 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: # Job id MUST stay `ci`: the reusable's job is also `ci`, so the check # 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: + runner: ${{ needs.select-runner.outputs.runner }} # Pinned explicitly (handbook): local dev is Node 24 / npm 11, which # generates lockfileVersion 3. Being explicit avoids lockfile drift. node-version: "24" diff --git a/workflow-templates/ci-terraform.yml b/workflow-templates/ci-terraform.yml index 29beb5d..49d3afc 100644 --- a/workflow-templates/ci-terraform.yml +++ b/workflow-templates/ci-terraform.yml @@ -10,7 +10,14 @@ permissions: contents: read 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: + needs: select-runner uses: Sea-Haven-Industries/.github/.github/workflows/ci-terraform.yaml@REPLACE-ME # vX.Y.Z with: + runner: ${{ needs.select-runner.outputs.runner }} terraform-version: "1.16.0" diff --git a/workflow-templates/ci-typescript-frontend.yml b/workflow-templates/ci-typescript-frontend.yml index 78cd754..ccb5ef7 100644 --- a/workflow-templates/ci-typescript-frontend.yml +++ b/workflow-templates/ci-typescript-frontend.yml @@ -5,11 +5,18 @@ on: merge_group: 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: # Job id MUST stay `ci`: the reusable's job is also `ci`, so the check # 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: + runner: ${{ needs.select-runner.outputs.runner }} # Pinned explicitly (handbook): local dev is Node 24 / npm 11, which # generates lockfileVersion 3. Being explicit avoids lockfile drift. node-version: "24" diff --git a/workflow-templates/dependency-review.yml b/workflow-templates/dependency-review.yml index f3dd282..8194918 100644 --- a/workflow-templates/dependency-review.yml +++ b/workflow-templates/dependency-review.yml @@ -7,5 +7,13 @@ permissions: contents: read 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: - 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 }} diff --git a/workflow-templates/labeler.yml b/workflow-templates/labeler.yml index 531ece9..a5e5784 100644 --- a/workflow-templates/labeler.yml +++ b/workflow-templates/labeler.yml @@ -12,5 +12,13 @@ permissions: issues: write 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: - 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 }}