From 99f25f7db668396559c8e2ee28d9a20736169dc6 Mon Sep 17 00:00:00 2001 From: Nicacio Oliveira Date: Thu, 6 Aug 2026 17:28:01 -0300 Subject: [PATCH 1/2] ci: allow the four cheap check jobs to run on self-hosted runners MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit format, lint, typecheck and test now resolve `runs-on` from the CI_RUNNER_LINUX repository variable, defaulting to ubuntu-latest when it is unset. This commit is a no-op until someone sets the variable. These four run 0.5-2 min but have waited up to ~90 min at the p99 — GitHub's hosted concurrency ceiling saturating at ~915 runs/day, not slow execution (queue p50 is 6s). They also need nothing beyond checkout + setup-bun, so they run on the stock ARC runner image with no sudo, no docker and no privileged sidecar. changes, build and web-component-tests stay hosted. Driving it from a variable rather than a hardcoded label matters because these are required checks: a job pointed at an unavailable scale set sits queued until GitHub's 24h timeout instead of failing fast, and nothing merges. Rollback is `gh variable delete CI_RUNNER_LINUX`, effective on the next run, with no PR. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/test.yml | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0f4142b01b..62139e9bcb 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -23,6 +23,29 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ github.event_name == 'pull_request' }} +# Runner selection for the four cheap-but-queued jobs below (format, lint, +# typecheck, test). Resolved from a repository variable so the pilot can be +# switched on and rolled back WITHOUT a PR: +# +# on: gh variable set CI_RUNNER_LINUX --body deco-linux-amd64 --repo decocms/studio +# off: gh variable delete CI_RUNNER_LINUX --repo decocms/studio +# +# Unset resolves to ubuntu-latest, so this file is a no-op until someone sets +# the variable. That matters because these are required checks: if the +# self-hosted scale set is unavailable, jobs targeting its label sit queued +# until GitHub's 24h timeout rather than failing fast, and nothing merges. +# Deleting the variable restores hosted runners on the next run. +# +# Why only these four: they run 0.5-2 min but have waited up to ~90 min at the +# p99 (hosted concurrency ceiling saturating at ~915 runs/day). They also need +# nothing beyond checkout + setup-bun, so they run on the stock ARC runner +# image — no sudo, no docker, no privileged sidecar. `changes`, `build` and +# `web-component-tests` deliberately stay hosted. +# +# Inlined per job rather than hoisted into `env:` because `runs-on` cannot read +# the `env` context — only `github`, `needs`, `strategy`, `matrix`, `inputs` +# and `vars`. + jobs: # Gate: skip the (required) check jobs when a PR only touches workflow YAML, # docs, or markdown. Required checks can't be skipped via workflow-level @@ -88,7 +111,7 @@ jobs: format: needs: changes if: needs.changes.outputs.relevant == 'true' - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -102,7 +125,7 @@ jobs: lint: needs: changes if: needs.changes.outputs.relevant == 'true' - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -121,7 +144,7 @@ jobs: typecheck: needs: changes if: needs.changes.outputs.relevant == 'true' - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} steps: - name: Checkout repository uses: actions/checkout@v4 @@ -140,7 +163,7 @@ jobs: test: needs: changes if: needs.changes.outputs.relevant == 'true' - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} # The suite normally finishes in ~1 min. A single hung file under the # parallel runner stalls the whole job silently (per-file output is # buffered, so the culprit prints nothing) — without this bound that's From ca0ff68d773b0a816541d462033b41aba182907c Mon Sep 17 00:00:00 2001 From: Nicacio Oliveira Date: Thu, 6 Aug 2026 18:28:05 -0300 Subject: [PATCH 2/2] ci: extend self-hosted runner opt-in to build and release jobs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test.yml: `build` joins format/lint/typecheck/test on CI_RUNNER_LINUX. It needs nothing beyond checkout + setup-bun + node, so it runs on the stock ARC image. `changes` stays hosted so a scale-set outage still produces a run that reports; `web-component-tests` stays hosted because `playwright install --with-deps` shells out to sudo apt-get, which the stock image has no root for. release-studio.yaml: three separate variables, all defaulting to the hosted runners used today, so unset is a no-op. CI_RUNNER_RELEASE build-dist, publish-npm, release, bump-deco-apps-cd, notify-docs-agent CI_RUNNER_RELEASE_DIND prepare + docker jobs, linux/amd64 CI_RUNNER_RELEASE_DIND_ARM64 docker jobs, linux/arm64 Kept separate from CI_RUNNER_LINUX because the risk is asymmetric: a stuck PR check annoys one author, a stuck release blocks shipping. The arm64 variable is not cosmetic — this matrix builds arm64 natively on ubuntu-24.04-arm, and pointing that leg at an amd64 runner would silently regress it to QEMU emulation. `prepare` is grouped with the docker jobs because docker/login-action and `docker manifest inspect` need the docker CLI, which the stock runner image only has wired up in dind mode. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/release-studio.yaml | 50 ++++++++++++++++++++------- .github/workflows/test.yml | 18 ++++++---- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/.github/workflows/release-studio.yaml b/.github/workflows/release-studio.yaml index db2b527188..328c02ecc7 100644 --- a/.github/workflows/release-studio.yaml +++ b/.github/workflows/release-studio.yaml @@ -28,10 +28,36 @@ env: IMAGE_NAME: ${{ github.repository }}/studio NGINX_IMAGE_NAME: ${{ github.repository }}/studio-nginx +# Runner selection, driven by repository variables so the release path can be +# moved onto self-hosted runners — and moved back — without a PR. All three +# default to the hosted runners this workflow has always used, so an unset +# variable is a no-op. +# +# CI_RUNNER_RELEASE non-docker jobs (build-dist, publish-npm, +# release, bump-deco-apps-cd, notify-docs-agent) +# CI_RUNNER_RELEASE_DIND docker jobs, linux/amd64 +# CI_RUNNER_RELEASE_DIND_ARM64 docker jobs, linux/arm64 +# +# Deliberately SEPARATE from CI_RUNNER_LINUX in test.yml. The risk is +# asymmetric: a stuck PR check annoys one author, a stuck release blocks +# shipping. Prove the checks on self-hosted for a week before touching these. +# +# The arm64 variable exists because this matrix builds arm64 NATIVELY on +# ubuntu-24.04-arm rather than under QEMU. Pointing that leg at an amd64 +# runner would silently regress it to emulation and roughly triple the build, +# so it must resolve to a genuinely arm64 scale set or stay hosted. +# +# `prepare` is grouped with the docker jobs, not the light ones: it runs +# docker/login-action and `docker manifest inspect`, which need the docker CLI +# that the stock ARC runner image only has wired up in dind mode. +# +# Rollback for any of them: `gh variable delete --repo decocms/studio`, +# effective on the next run. + jobs: prepare: name: Prepare release metadata - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_RELEASE_DIND || 'ubuntu-latest' }} # Releases are cut by the [release]: version-bump commit that release-tagging # pushes to main (the bot push uses the App token, which re-triggers # workflows). We run on EVERY push here — a non-bump push just reads an @@ -114,7 +140,7 @@ jobs: needs.prepare.outputs.version-changed == 'true' || needs.prepare.outputs.image-exists == 'false' || needs.prepare.outputs.nginx-image-exists == 'false' - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_RELEASE || 'ubuntu-latest' }} permissions: contents: read steps: @@ -174,7 +200,7 @@ jobs: name: Publish to NPM needs: [prepare, build-dist] if: needs.prepare.outputs.version-changed == 'true' - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_RELEASE || 'ubuntu-latest' }} permissions: contents: read id-token: write @@ -221,10 +247,10 @@ jobs: include: - arch: amd64 platform: linux/amd64 - runner: ubuntu-latest + runner: ${{ vars.CI_RUNNER_RELEASE_DIND || 'ubuntu-latest' }} - arch: arm64 platform: linux/arm64 - runner: ubuntu-24.04-arm + runner: ${{ vars.CI_RUNNER_RELEASE_DIND_ARM64 || 'ubuntu-24.04-arm' }} steps: - uses: actions/checkout@v4 with: @@ -296,10 +322,10 @@ jobs: include: - arch: amd64 platform: linux/amd64 - runner: ubuntu-latest + runner: ${{ vars.CI_RUNNER_RELEASE_DIND || 'ubuntu-latest' }} - arch: arm64 platform: linux/arm64 - runner: ubuntu-24.04-arm + runner: ${{ vars.CI_RUNNER_RELEASE_DIND_ARM64 || 'ubuntu-24.04-arm' }} steps: - uses: actions/checkout@v4 with: @@ -360,7 +386,7 @@ jobs: always() && needs.prepare.outputs.image-exists == 'false' && needs.build-docker.result == 'success' - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_RELEASE_DIND || 'ubuntu-latest' }} permissions: contents: read packages: write @@ -419,7 +445,7 @@ jobs: always() && needs.prepare.outputs.nginx-image-exists == 'false' && needs.build-nginx-docker.result == 'success' - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_RELEASE_DIND || 'ubuntu-latest' }} permissions: contents: read packages: write @@ -485,7 +511,7 @@ jobs: (needs.publish-npm.result == 'success' || needs.publish-npm.result == 'skipped') && (needs.merge-docker.result == 'success' || needs.merge-docker.result == 'skipped') && (needs.merge-nginx-docker.result == 'success' || needs.merge-nginx-docker.result == 'skipped') - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_RELEASE || 'ubuntu-latest' }} permissions: contents: write steps: @@ -578,7 +604,7 @@ jobs: needs.merge-nginx-docker.result == 'success' ) ) - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_RELEASE || 'ubuntu-latest' }} # Reuses DEPLOY_REPO_TOKEN — the decobot machine-user PAT already used to # dispatch the Studio image bump (non-personal; decobot is admin on # decocms/deco-apps-cd). Same bot, same mechanism as the legacy repo, no @@ -633,7 +659,7 @@ jobs: name: Notify docs agent needs: [prepare, release] if: always() && needs.release.result == 'success' - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_RELEASE || 'ubuntu-latest' }} env: WEBHOOK_URL: ${{ vars.DOCS_AGENT_WEBHOOK_URL }} WEBHOOK_TOKEN: ${{ secrets.DOCS_AGENT_WEBHOOK_TOKEN }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 62139e9bcb..1ed615088c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -36,11 +36,17 @@ concurrency: # until GitHub's 24h timeout rather than failing fast, and nothing merges. # Deleting the variable restores hosted runners on the next run. # -# Why only these four: they run 0.5-2 min but have waited up to ~90 min at the -# p99 (hosted concurrency ceiling saturating at ~915 runs/day). They also need -# nothing beyond checkout + setup-bun, so they run on the stock ARC runner -# image — no sudo, no docker, no privileged sidecar. `changes`, `build` and -# `web-component-tests` deliberately stay hosted. +# Why these five: they have waited up to ~90 min at the p99 (hosted +# concurrency ceiling saturating at ~915 runs/day) for 0.4-2 min of work. They +# also need nothing beyond checkout + setup-bun, so they run on the stock ARC +# runner image — no sudo, no docker, no privileged sidecar. +# +# Two jobs stay hosted: +# changes 1 min, and it gates the rest — keeping it hosted means +# a scale-set outage still produces a run that reports. +# web-component-tests `playwright install --with-deps` / `install-deps` +# shell out to sudo apt-get, which the stock runner +# image has no root for. Needs a custom image first. # # Inlined per job rather than hoisted into `env:` because `runs-on` cannot read # the `env` context — only `github`, `needs`, `strategy`, `matrix`, `inputs` @@ -185,7 +191,7 @@ jobs: build: needs: changes if: needs.changes.outputs.relevant == 'true' - runs-on: ubuntu-latest + runs-on: ${{ vars.CI_RUNNER_LINUX || 'ubuntu-latest' }} steps: - name: Checkout repository uses: actions/checkout@v4