From 7e9255409da640db501512a0cc6e631e8806df4a Mon Sep 17 00:00:00 2001 From: Asen Lekov Date: Tue, 18 Aug 2026 23:29:58 +0300 Subject: [PATCH] fix(pick-runner): stop blaming the fleet when the fleet could not be read MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit On a repo without GH_APP_CLIENT_ID the validator reported: 'self-hosted,Linux,X64' asks for label(s): self-hosted Linux X64, which no runner carries. Three runners carry exactly those labels. The list simply could not be read — GITHUB_TOKEN gets 403 on /orgs/{org}/actions/runners. runners=$(gh api ... | jq -s '.' || echo '[]') A pipeline's exit status is the LAST command's, so jq succeeding masked gh failing and the || fallback never fired. gh also prints the error body on failure, which jq -s slurped into a ONE-element array — so the "is it empty" guard passed and the step went on to check labels against a fleet of one error object, and blamed the real one. gh's exit status is now captured separately, and the step refuses to say anything about labels unless the fleet was actually read. The two causes are reported distinctly: "could not read the list" versus "the org reports no runners". This matters more than a wording nit. The whole purpose of this step is to stop a silent hosted fallback from looking like a busy fleet, and it was doing the inverse — sending a reader to hunt a labelling problem that does not exist. Verified against all three paths with a stubbed gh. Why: a diagnostic that names the wrong cause costs more than no diagnostic. Refs: DODI-00012 Co-Authored-By: Claude Opus 5 --- .github/workflows/pick-runner.yml | 31 +++++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/.github/workflows/pick-runner.yml b/.github/workflows/pick-runner.yml index 2271c2a..a0107b2 100644 --- a/.github/workflows/pick-runner.yml +++ b/.github/workflows/pick-runner.yml @@ -158,10 +158,33 @@ jobs: # never fail. set +e set -uo pipefail - runners=$(gh api "/orgs/$ORG/actions/runners" --paginate \ - --jq '.runners[] | {name,labels:[.labels[].name]}' 2>/dev/null | jq -s '.' || echo '[]') - if [ "$(jq 'length' <<<"$runners")" = "0" ]; then - echo "::warning title=Runner selector was NOT validated::Could not read the org runner list, so '$SEL' was not checked against the live fleet. A selector matching nothing looks exactly like a busy fleet from here. Most often this means the GitHub App token is missing or lacks org scope." + # Key off gh's OWN exit status, captured separately. + # + # The previous form was `runners=$(gh api ... | jq -s '.' || echo '[]')`. + # A pipeline's status is the LAST command's, so jq succeeding masked gh + # failing, and `|| echo '[]'` never fired. Worse, gh prints the error + # body on failure, which jq -s then slurped into a ONE-element array — + # so the length check passed and the step went on to report + # "'self-hosted,Linux,X64' ... which no runner carries" against a fleet + # that carries exactly those labels and simply could not be read. + # + # Never claim the fleet lacks a label unless the fleet was actually read. + raw=$(gh api "/orgs/$ORG/actions/runners" --paginate \ + --jq '.runners[] | {name,labels:[.labels[].name]}' 2>/dev/null) + gh_rc=$? + + runners='[]' + if [ "$gh_rc" -eq 0 ] && [ -n "$raw" ]; then + runners=$(jq -s '.' <<<"$raw" 2>/dev/null) || runners='[]' + fi + count=$(jq 'length' <<<"$runners" 2>/dev/null || echo 0) + + if [ "$gh_rc" -ne 0 ]; then + echo "::warning title=Runner selector was NOT validated::Could not read the org runner list (gh exited $gh_rc), so '$SEL' was NOT checked against the live fleet. This says nothing about whether the labels exist. Usually the GitHub App token is missing or lacks org scope — see GH_APP_CLIENT_ID." + exit 0 + fi + if [ "$count" -eq 0 ]; then + echo "::warning title=Runner selector was NOT validated::The org reports no self-hosted runners at all, so '$SEL' could not be checked. If that is unexpected, the fleet is offline or the token cannot see it." exit 0 fi