From 9db83175431f5ca556bb109ee2d150ef76e3b759 Mon Sep 17 00:00:00 2001 From: cluade-reviewer-andresmgsl Date: Sun, 2 Aug 2026 20:34:02 +0000 Subject: [PATCH] =?UTF-8?q?fix(labels-scope):=20jq=201.6=20cannot=20parse?= =?UTF-8?q?=20$label=20=E2=80=94=20the=20runner=20image=20ships=201.6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Found by running ceremony's own CI at 9357f09 on a real Forgejo runner rather than reasoning about it. `label` is a reserved word in jq's grammar (`label $out | … | break $out`), so jq **1.6** rejects `$label` outright: jq: error: syntax error, unexpected label, expecting IDENT jq 1.7 parses it, which is why this survived: GitHub's hosted ubuntu-latest ships 1.7, and ghcr.io/catthehacker/ubuntu:act-22.04 — the image this instance maps ubuntu-latest to — ships 1.6. So parse_labeler_config died on a compile error before it read a byte of config, and EVERY scope derivation on this forge failed. Renamed to $lbl in the jq program only; the bash locals keep their names. Also makes test/forge.test.sh hermetic. Its "github + gh passes" case depended on gh being on the HOST's PATH, so it passed on a developer box and failed in the runner image, which has no gh. The preflight cases now run against stub binaries, and the missing-binary refusal gets its own arm on a PATH carrying the shell and text tools but no clients — the condition under test, rather than whatever the machine happens to have. Verified in both environments: local (jq 1.7, gh present) and the runner image (jq 1.6, no gh) — shellcheck 0, 22 files 0 failed in each. Refs #188 --- actions/labels-scope/labels-scope.sh | 30 ++++++++++++++--------- changelog.d/188.md | 5 ++++ test/forge.test.sh | 36 ++++++++++++++++++++++++++-- 3 files changed, 58 insertions(+), 13 deletions(-) diff --git a/actions/labels-scope/labels-scope.sh b/actions/labels-scope/labels-scope.sh index d61ce4c..af39d62 100644 --- a/actions/labels-scope/labels-scope.sh +++ b/actions/labels-scope/labels-scope.sh @@ -71,6 +71,14 @@ glob_to_regex() { # $1 = glob (the subset above) → anchored ERE, one line } parse_labeler_config() { # labeler.yml on stdin → "labelglob" lines + # The jq variable is $lbl, not $label: **`label` is a reserved keyword in + # jq's grammar** (`label $out | ... | break $out`), and jq 1.6 refuses + # `$label` outright — "syntax error, unexpected label, expecting IDENT". + # jq 1.7 parses it, which is why this survived: GitHub's hosted + # ubuntu-latest ships 1.7, and the Forgejo runner image + # (ghcr.io/catthehacker/ubuntu:act-22.04) ships **1.6**. Measured on both, + # 2026-08-02 (#188). Every scope-label derivation on this forge failed on a + # jq compile error before the config was even read. # yq only normalizes YAML to JSON; the shape contract is enforced in jq, # where an unsupported key is a loud error naming the label it sits under. yq -o=json '.' - | jq -r ' @@ -78,38 +86,38 @@ parse_labeler_config() { # labeler.yml on stdin → "labelglob" lines error("labeler config: top level must be a map of label -> rules") else . end | to_entries[] - | .key as $label + | .key as $lbl | (if (.value | type) != "array" then - error("labeler config: \($label): rules must be a list") + error("labeler config: \($lbl): rules must be a list") else .value end)[] | (if type != "object" then - error("labeler config: \($label): each rule must be a map") + error("labeler config: \($lbl): each rule must be a map") else . end) | ((keys - ["changed-files"]) as $extra | if ($extra | length) > 0 then - error("labeler config: \($label): unsupported key(s) \($extra | join(", ")) — the scope job accepts changed-files/any-glob-to-any-file only (#130)") + error("labeler config: \($lbl): unsupported key(s) \($extra | join(", ")) — the scope job accepts changed-files/any-glob-to-any-file only (#130)") else . end) | .["changed-files"] | (if type == "object" then [.] elif type == "array" then . - else error("labeler config: \($label): changed-files must be a list") end)[] + else error("labeler config: \($lbl): changed-files must be a list") end)[] | (if type != "object" then - error("labeler config: \($label): each changed-files entry must be a map") + error("labeler config: \($lbl): each changed-files entry must be a map") else . end) | ((keys - ["any-glob-to-any-file"]) as $extra | if ($extra | length) > 0 then - error("labeler config: \($label): unsupported matcher(s) \($extra | join(", ")) — the scope job accepts any-glob-to-any-file only (#130)") + error("labeler config: \($lbl): unsupported matcher(s) \($extra | join(", ")) — the scope job accepts any-glob-to-any-file only (#130)") else . end) | .["any-glob-to-any-file"] | (if type == "string" then [.] elif type == "array" then . - else error("labeler config: \($label): any-glob-to-any-file must be a glob or a list of globs") end)[] + else error("labeler config: \($lbl): any-glob-to-any-file must be a glob or a list of globs") end)[] | (if type != "string" then - error("labeler config: \($label): globs must be strings") + error("labeler config: \($lbl): globs must be strings") elif contains("\\") then - error("labeler config: \($label): backslash in glob \(.) — escapes are not supported (#130)") + error("labeler config: \($lbl): backslash in glob \(.) — escapes are not supported (#130)") else . end) - | [$label, .] | @tsv + | [$lbl, .] | @tsv ' } diff --git a/changelog.d/188.md b/changelog.d/188.md index ff19014..7c1dcec 100644 --- a/changelog.d/188.md +++ b/changelog.d/188.md @@ -34,6 +34,11 @@ ### Fixed +- `labels-scope` no longer fails to compile its jq program on jq 1.6, which + the Forgejo runner image ships: `label` is a reserved word in jq's grammar, + so `$label` is a syntax error there and every scope derivation died before + reading the config (#188). + - `labels-reconcile` and `labels-scope` no longer exit 0 on a Forgejo consumer having read zero facts — measured on `heavy-duty/rig`, where the sweep printed `reconciled.` over an empty PR list and scope reported "no diff --git a/test/forge.test.sh b/test/forge.test.sh index 34b09e5..85b2b9e 100644 --- a/test/forge.test.sh +++ b/test/forge.test.sh @@ -4,6 +4,16 @@ set -u ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +TMP="$(mktemp -d)" +trap 'rm -rf "$TMP"' EXIT +# A PATH with the shell and the text tools lib/forge.sh itself uses, but with +# NO forge clients on it — that is the condition under test. It cannot be a +# genuinely empty directory: `env -i PATH=…` would then fail to find `bash`, +# and the heredoc refusals use `cat`. +mkdir -p "$TMP/empty" +for _t in bash cat sed awk tr printf; do + _p="$(command -v "$_t" 2>/dev/null)" && ln -sf "$_p" "$TMP/empty/$_t" +done # shellcheck source=test/harness.sh . "$ROOT/test/harness.sh" # shellcheck source=lib/forge.sh @@ -25,8 +35,24 @@ detect_in() { env -i PATH="$PATH" "$@" bash -c '. '"$ROOT"'/lib/forge.sh; forge_detect' } +# A PATH carrying stub binaries for every client the preflight can require. +# Without this the "passes" cases depend on whatever the HOST happens to have, +# which is not hermetic and is wrong in the only place it matters: the Forgejo +# runner image (ghcr.io/catthehacker/ubuntu:act-22.04) has **no gh**, so +# "github + gh passes" failed there while passing on a developer box. Measured +# 2026-08-02 (#188) — the same class of hosted-image assumption this issue +# exists to find. +STUBBIN="$TMP/bin" +mkdir -p "$STUBBIN" +for _b in gh curl jq; do printf '#!/bin/sh\nexit 0\n' >"$STUBBIN/$_b"; chmod +x "$STUBBIN/$_b"; done + preflight_in() { - env -i PATH="$PATH" "$@" bash -c '. '"$ROOT"'/lib/forge.sh; forge_preflight' + env -i PATH="$STUBBIN:$PATH" "$@" bash -c '. '"$ROOT"'/lib/forge.sh; forge_preflight' +} + +# ...and one with NO clients at all, for the missing-binary refusal. +preflight_bare() { + env -i PATH="$TMP/empty" "$@" bash -c '. '"$ROOT"'/lib/forge.sh; forge_preflight' } # --- forge_detect: the explicit override -------------------------------- @@ -140,4 +166,10 @@ check "github backend wants gh" 0 "" eq gh forge_client github check "forgejo backend wants rest" 0 "" eq rest forge_client forgejo check "forge_client refuses an unknown backend" 1 "unknown forge" forge_client gitlab -summary +# The missing-binary arm, hermetically: an empty PATH has no client at all. +check "a forge whose client is not installed refuses" 1 "is not installed" \ + preflight_bare CEREMONY_FORGE=github +check "...and names the missing binary" 1 "gh" preflight_bare CEREMONY_FORGE=github +check "...the forgejo arm names its own tools" 1 "curl" preflight_bare CEREMONY_FORGE=forgejo + +summary \ No newline at end of file