From e9ac4a33afe361066b1a1256ad99bab5a6d69c3b Mon Sep 17 00:00:00 2001 From: askalf <263217947+askalf@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:17:55 -0400 Subject: [PATCH 1/3] ci: a fork repository runs on GitHub's runners In a fork of this repository a push, schedule or dispatch carries no pull_request, so the own-code expression chose cordon-exec, which a fork does not have, and the job would wait for a runner forever. The expression now also sends any run outside askalf/cordon to GitHub's runners, and the jobs that never run PR code use cordon-exec only here. The runner comments state the constraint without the runners' deployment details. scripts/fleet-status.test.mjs checks every event in a fork repository. --- .github/workflows/build.yml | 4 +- .github/workflows/ci.yml | 10 ++--- .github/workflows/codeql.yml | 10 ++--- .github/workflows/fleet-status-backfill.yml | 3 +- .github/workflows/fleet-status-self-test.yml | 10 ++--- .github/workflows/fleet-status.yml | 3 +- .github/workflows/labels.yml | 10 ++--- scripts/fleet-status.test.mjs | 45 ++++++++++++-------- 8 files changed, 53 insertions(+), 42 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 7cfe374..cc404b0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -25,7 +25,7 @@ jobs: # tsconfig.json, src/, or this file. The required check docker-build runs on our runner and passes only when that # job passed or was not owed, so a PR that changes none of them never waits on GitHub's queue. docker-build-inputs: - runs-on: ${{ github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} + runs-on: ${{ (github.repository != 'askalf/cordon' || github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]')) && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} timeout-minutes: 5 outputs: changed: ${{ steps.diff.outputs.changed }} @@ -118,7 +118,7 @@ jobs: docker-build: needs: [docker-build-inputs, docker-build-image] if: always() - runs-on: ${{ github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} + runs-on: ${{ (github.repository != 'askalf/cordon' || github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]')) && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} timeout-minutes: 5 steps: - name: the image job passed, or no change owed one diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f451f46..6e8a2cb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,11 +14,11 @@ concurrency: jobs: test: - # Our own code (pushes, same-repo PRs, schedules, dispatches) runs on cordon-exec, the - # non-root one-job runners on our host, so GitHub's hosted - # queue going down does not hold our PRs. A fork's PR and a Dependabot PR run code nobody - # here wrote, so they stay on GitHub's runners. - runs-on: ${{ github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} + # Our own code (pushes, same-repo PRs, schedules, dispatches) runs on cordon-exec, our non-root + # one-job runners, so GitHub's hosted queue going down does not hold our PRs. A fork's PR + # and a Dependabot PR run code nobody here wrote, and a fork repository has no cordon-exec + # runners, so those stay on GitHub's runners. + runs-on: ${{ (github.repository != 'askalf/cordon' || github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]')) && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 7c429b5..6962f14 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -18,11 +18,11 @@ concurrency: jobs: analyze: name: analyze (${{ matrix.language }}) - # Our own code (pushes, same-repo PRs, schedules, dispatches) runs on cordon-exec, the - # non-root one-job runners on our host, so GitHub's hosted - # queue going down does not hold our PRs. A fork's PR and a Dependabot PR run code nobody - # here wrote, so they stay on GitHub's runners. - runs-on: ${{ github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} + # Our own code (pushes, same-repo PRs, schedules, dispatches) runs on cordon-exec, our non-root + # one-job runners, so GitHub's hosted queue going down does not hold our PRs. A fork's PR + # and a Dependabot PR run code nobody here wrote, and a fork repository has no cordon-exec + # runners, so those stay on GitHub's runners. + runs-on: ${{ (github.repository != 'askalf/cordon' || github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]')) && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} timeout-minutes: 20 permissions: actions: read diff --git a/.github/workflows/fleet-status-backfill.yml b/.github/workflows/fleet-status-backfill.yml index 8b36828..f0310f7 100644 --- a/.github/workflows/fleet-status-backfill.yml +++ b/.github/workflows/fleet-status-backfill.yml @@ -11,7 +11,8 @@ jobs: backfill: # Our own script on its own trigger, never PR code: on our runner, so GitHub's hosted # queue going down does not stop it. - runs-on: [self-hosted, cordon-exec] + # A fork repository has no cordon-exec runners, so there it runs on GitHub's. + runs-on: ${{ github.repository == 'askalf/cordon' && fromJSON('["self-hosted","cordon-exec"]') || 'ubuntu-latest' }} timeout-minutes: 10 permissions: contents: read diff --git a/.github/workflows/fleet-status-self-test.yml b/.github/workflows/fleet-status-self-test.yml index 0d0ec44..1fc2cb9 100644 --- a/.github/workflows/fleet-status-self-test.yml +++ b/.github/workflows/fleet-status-self-test.yml @@ -16,11 +16,11 @@ permissions: {} jobs: self-test: - # Our own code (pushes, same-repo PRs, schedules, dispatches) runs on cordon-exec, the - # non-root one-job runners on our host, so GitHub's hosted - # queue going down does not hold our PRs. A fork's PR and a Dependabot PR run code nobody - # here wrote, so they stay on GitHub's runners. - runs-on: ${{ github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} + # Our own code (pushes, same-repo PRs, schedules, dispatches) runs on cordon-exec, our non-root + # one-job runners, so GitHub's hosted queue going down does not hold our PRs. A fork's PR + # and a Dependabot PR run code nobody here wrote, and a fork repository has no cordon-exec + # runners, so those stay on GitHub's runners. + runs-on: ${{ (github.repository != 'askalf/cordon' || github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]')) && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} timeout-minutes: 5 permissions: contents: read diff --git a/.github/workflows/fleet-status.yml b/.github/workflows/fleet-status.yml index 7205a7b..17aef32 100644 --- a/.github/workflows/fleet-status.yml +++ b/.github/workflows/fleet-status.yml @@ -44,7 +44,8 @@ jobs: # On our runner for every event, a fork's included: the job runs the default branch's script # and never PR code (see above), so GitHub's hosted queue going down does not hold the # fleet/verify and fleet/review statuses a merge waits on. - runs-on: [self-hosted, cordon-exec] + # A fork repository has no cordon-exec runners, so there it runs on GitHub's. + runs-on: ${{ github.repository == 'askalf/cordon' && fromJSON('["self-hosted","cordon-exec"]') || 'ubuntu-latest' }} timeout-minutes: 5 permissions: contents: read diff --git a/.github/workflows/labels.yml b/.github/workflows/labels.yml index 6ad6902..3d9aa6a 100644 --- a/.github/workflows/labels.yml +++ b/.github/workflows/labels.yml @@ -32,11 +32,11 @@ permissions: jobs: manifest: name: manifest matches the repo - # Our own code (pushes, same-repo PRs, schedules, dispatches) runs on cordon-exec, the - # non-root one-job runners on our host, so GitHub's hosted - # queue going down does not hold our PRs. A fork's PR and a Dependabot PR run code nobody - # here wrote, so they stay on GitHub's runners. - runs-on: ${{ github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]') && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} + # Our own code (pushes, same-repo PRs, schedules, dispatches) runs on cordon-exec, our non-root + # one-job runners, so GitHub's hosted queue going down does not hold our PRs. A fork's PR + # and a Dependabot PR run code nobody here wrote, and a fork repository has no cordon-exec + # runners, so those stay on GitHub's runners. + runs-on: ${{ (github.repository != 'askalf/cordon' || github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]')) && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} timeout-minutes: 3 permissions: contents: read diff --git a/scripts/fleet-status.test.mjs b/scripts/fleet-status.test.mjs index 30e8e29..9564736 100644 --- a/scripts/fleet-status.test.mjs +++ b/scripts/fleet-status.test.mjs @@ -514,40 +514,49 @@ console.log('\n fleet-status.yml: which events run the job for a fork'); && checkouts.every((m) => /ref: \$\{\{ github\.event\.repository\.default_branch \}\}/.test(m[1]))); check('no step reads the PR head ref or sha', !/(pull_request\.head\.(ref|sha)|workflow_run\.head_sha)/.test(own)); - // Our own code runs on cordon-exec, our host's runners; code nobody here wrote never does. A - // runs-on expression sends a fork's PR and a Dependabot PR to GitHub's runners and everything - // else to ours. A literal cordon-exec on a pull_request workflow needs a job if: that keeps forks off - // it, or a job that never runs PR code (fleet-status, checked above). + // Our own code runs on cordon-exec, our host's runners; code nobody here wrote never does, and a fork + // repository, which has no cordon-exec runners, never waits for one. The own-code expression sends a + // fork's PR, a Dependabot PR and any run in a fork repository to GitHub's runners and everything + // else to ours. The repository-only expression, for jobs that never run PR code, sends everything + // here to ours. A literal cordon-exec needs a job if: that keeps it to this repository's own code. const OWN_RE = /^\s+runs-on: \$\{\{ (.+) \}\}$/; - const exprs = []; + const HOME_REPO = 'askalf/cordon'; + const ownExprs = []; + const repoOnly = []; const literal = []; for (const f of readdirSync(dir).filter((x) => /\.ya?ml$/.test(x))) { const y = readFileSync(join(dir, f), 'utf8'); for (const line of y.split('\n')) { const m = OWN_RE.exec(line); - if (m && m[1].includes('cordon-exec')) exprs.push({ f, e: m[1] }); - else if (/^\s+runs-on: \[self-hosted, cordon-exec\]/.test(line)) literal.push(f); + if (m && m[1].includes('cordon-exec')) (m[1].includes('github.event.pull_request') ? ownExprs : repoOnly).push({ f, e: m[1] }); + else if (/^\s+runs-on: \[self-hosted, cordon\-exec\]/.test(line)) literal.push(f); } } - check('the own-code runs-on expression is in use', exprs.length >= 6); - const hosted = (e, github) => evalIf(`(${e}) == 'ubuntu-latest'`, { github: { repository: REPO, ...github } }); + check('the own-code runs-on expression is in use', ownExprs.length >= 6); + const hosted = (e, github, repository = HOME_REPO) => evalIf(`(${e}) == 'ubuntu-latest'`, { github: { repository, ...github } }); const prFrom = (event_name, headRepo, login = 'askalf') => ({ event_name, event: { pull_request: { head: { repo: { full_name: headRepo } }, user: { login } } } }); - for (const { f, e } of exprs) { + const EVENTS = [{ event_name: 'push', event: {} }, { event_name: 'schedule', event: {} }, { event_name: 'workflow_dispatch', event: {} }]; + const FORK_REPO = 'someone/cordon'; + for (const { f, e } of ownExprs) { check(`${f}: a fork's pull_request runs on GitHub's runners`, hosted(e, prFrom('pull_request', FORKED))); check(`${f}: a fork's pull_request_review runs on GitHub's runners`, hosted(e, prFrom('pull_request_review', FORKED))); - check(`${f}: a Dependabot PR runs on GitHub's runners`, hosted(e, prFrom('pull_request', REPO, 'dependabot[bot]'))); - check(`${f}: a same-repo PR runs on ours`, !hosted(e, prFrom('pull_request', REPO))); - check(`${f}: a push, schedule or dispatch runs on ours`, - !hosted(e, { event_name: 'push', event: {} }) && !hosted(e, { event_name: 'schedule', event: {} }) - && !hosted(e, { event_name: 'workflow_dispatch', event: {} })); + check(`${f}: a Dependabot PR runs on GitHub's runners`, hosted(e, prFrom('pull_request', HOME_REPO, 'dependabot[bot]'))); + check(`${f}: a same-repo PR runs on ours`, !hosted(e, prFrom('pull_request', HOME_REPO))); + check(`${f}: a push, schedule or dispatch runs on ours`, EVENTS.every((ev) => !hosted(e, ev))); + check(`${f}: in a fork repository every event runs on GitHub's runners`, + EVENTS.every((ev) => hosted(e, ev, FORK_REPO)) && hosted(e, prFrom('pull_request', FORK_REPO), FORK_REPO)); check(`${f}: the expression names exactly our label`, e.includes(`fromJSON('["self-hosted","cordon-exec"]')`)); } + for (const { f, e } of repoOnly) { + check(`${f}: every event here runs on ours`, EVENTS.every((ev) => !hosted(e, ev)) && !hosted(e, prFrom('pull_request', FORKED))); + check(`${f}: in a fork repository it runs on GitHub's runners`, EVENTS.every((ev) => hosted(e, ev, FORK_REPO))); + check(`${f}: only a job that never runs PR code uses it`, ['fleet-status.yml', 'fleet-status-backfill.yml'].includes(f)); + } for (const f of literal) { const y = readFileSync(join(dir, f), 'utf8'); - const prTriggered = /\bpull_request(_target|_review)?\b/.test(onBlockOf(y)); - check(`${f}: a literal cordon-exec is on a workflow no fork can run, or keeps forks off it`, - !prTriggered || f === 'fleet-status.yml' || /head\.repo\.full_name == github\.repository/.test(y)); + check(`${f}: a literal cordon-exec job runs only in this repository, or only on its own PRs`, + y.includes(`github.repository == '${HOME_REPO}'`) || /head\.repo\.full_name == github\.repository/.test(y)); } let relay = ''; From 83f15e8a6ea5498964eca4e31e8e44274a1bec2f Mon Sep 17 00:00:00 2001 From: askalf <263217947+askalf@users.noreply.github.com> Date: Tue, 6 Oct 2026 15:58:05 -0400 Subject: [PATCH 2/3] ci: CodeQL runs on GitHub's runners CodeQL sizes itself to the machine, and the exec runners of every repo share one host, so concurrent analyses from several repos exhausted its memory (six at once on 2026-10-06 drove host load past 150). The analyze job goes back to ubuntu-latest, without the ram and threads inputs it needed on the shared host. --- .github/workflows/codeql.yml | 14 +++----------- scripts/fleet-status.test.mjs | 2 +- 2 files changed, 4 insertions(+), 12 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 6962f14..e47d4be 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -18,11 +18,9 @@ concurrency: jobs: analyze: name: analyze (${{ matrix.language }}) - # Our own code (pushes, same-repo PRs, schedules, dispatches) runs on cordon-exec, our non-root - # one-job runners, so GitHub's hosted queue going down does not hold our PRs. A fork's PR - # and a Dependabot PR run code nobody here wrote, and a fork repository has no cordon-exec - # runners, so those stay on GitHub's runners. - runs-on: ${{ (github.repository != 'askalf/cordon' || github.event.pull_request && (github.event.pull_request.head.repo.full_name != github.repository || github.event.pull_request.user.login == 'dependabot[bot]')) && 'ubuntu-latest' || fromJSON('["self-hosted","cordon-exec"]') }} + # CodeQL runs on GitHub's runners: it sizes itself to the machine, and the exec runners of + # every repo share one host, so concurrent analyses would exhaust its memory. + runs-on: ubuntu-latest timeout-minutes: 20 permissions: actions: read @@ -36,16 +34,10 @@ jobs: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - # CodeQL sizes itself to the machine's RAM and cores; on our host every exec runner shares - # one memory ceiling, so on our runner it gets 2G and two threads, on GitHub's its defaults. - uses: github/codeql-action/init@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: languages: ${{ matrix.language }} queries: security-and-quality - ram: ${{ runner.environment == 'self-hosted' && '2048' || '' }} - threads: ${{ runner.environment == 'self-hosted' && '2' || '' }} - uses: github/codeql-action/analyze@2892aa5e19bbd11bc0cff5427e3b750a04d9e3c2 # v4.38.2 with: category: "/language:${{ matrix.language }}" - ram: ${{ runner.environment == 'self-hosted' && '2048' || '' }} - threads: ${{ runner.environment == 'self-hosted' && '2' || '' }} diff --git a/scripts/fleet-status.test.mjs b/scripts/fleet-status.test.mjs index 9564736..7e43977 100644 --- a/scripts/fleet-status.test.mjs +++ b/scripts/fleet-status.test.mjs @@ -532,7 +532,7 @@ console.log('\n fleet-status.yml: which events run the job for a fork'); else if (/^\s+runs-on: \[self-hosted, cordon\-exec\]/.test(line)) literal.push(f); } } - check('the own-code runs-on expression is in use', ownExprs.length >= 6); + check('the own-code runs-on expression is in use', ownExprs.length >= 5); const hosted = (e, github, repository = HOME_REPO) => evalIf(`(${e}) == 'ubuntu-latest'`, { github: { repository, ...github } }); const prFrom = (event_name, headRepo, login = 'askalf') => ({ event_name, event: { pull_request: { head: { repo: { full_name: headRepo } }, user: { login } } } }); From c6e4542803d85106f33a507c32bee8efa6b82c8b Mon Sep 17 00:00:00 2001 From: askalf <263217947+askalf@users.noreply.github.com> Date: Tue, 6 Oct 2026 17:27:36 -0400 Subject: [PATCH 3/3] ci: Dockerfile.dockerignore is an image input Docker reads a Dockerfile-specific ignore file, Dockerfile.dockerignore, in preference to .dockerignore. A PR that added one could drop the image's entry point while the input check said nothing changed, so the image smoke was skipped and the required docker-build passed. --- .github/workflows/build.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index cc404b0..8fa5eb2 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -21,7 +21,7 @@ concurrency: jobs: # docker-build needs a Docker daemon, and cordon-exec has none (Docker on our host is root). So the # image job runs on GitHub's runners, on every push to the default branch and on a PR that - # changes what the image is built from: the Dockerfile, .dockerignore, the package files, + # changes what the image is built from: the Dockerfile, .dockerignore or Dockerfile.dockerignore, the package files, # tsconfig.json, src/, or this file. The required check docker-build runs on our runner and passes only when that # job passed or was not owed, so a PR that changes none of them never waits on GitHub's queue. docker-build-inputs: @@ -57,7 +57,7 @@ jobs: while IFS= read -r -d '' f; do printf '%s\n' "$f" case "$f" in - Dockerfile|.dockerignore|package.json|package-lock.json|tsconfig.json|src/*|.github/workflows/build.yml) + Dockerfile|.dockerignore|Dockerfile.dockerignore|package.json|package-lock.json|tsconfig.json|src/*|.github/workflows/build.yml) changed=true ;; esac done < "$list"