fix(labels-scope): jq 1.6 cannot parse $label — the runner image ships 1.6
Some checks failed
CI / test (pull_request) Has been cancelled
CI / release-exercise (pull_request) Has been cancelled
CI / self-guards (pull_request) Has been cancelled
CI / action-exercise (pull_request) Has been cancelled
CI / docs-sync-exercise (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
Some checks failed
CI / test (pull_request) Has been cancelled
CI / release-exercise (pull_request) Has been cancelled
CI / self-guards (pull_request) Has been cancelled
CI / action-exercise (pull_request) Has been cancelled
CI / docs-sync-exercise (pull_request) Has been cancelled
labels / labels (pull_request) Has been cancelled
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
This commit is contained in:
parent
9357f09aea
commit
9db8317543
3 changed files with 58 additions and 13 deletions
|
|
@ -71,6 +71,14 @@ glob_to_regex() { # $1 = glob (the subset above) → anchored ERE, one line
|
||||||
}
|
}
|
||||||
|
|
||||||
parse_labeler_config() { # labeler.yml on stdin → "label<TAB>glob" lines
|
parse_labeler_config() { # labeler.yml on stdin → "label<TAB>glob" 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,
|
# 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.
|
# where an unsupported key is a loud error naming the label it sits under.
|
||||||
yq -o=json '.' - | jq -r '
|
yq -o=json '.' - | jq -r '
|
||||||
|
|
@ -78,38 +86,38 @@ parse_labeler_config() { # labeler.yml on stdin → "label<TAB>glob" lines
|
||||||
error("labeler config: top level must be a map of label -> rules")
|
error("labeler config: top level must be a map of label -> rules")
|
||||||
else . end
|
else . end
|
||||||
| to_entries[]
|
| to_entries[]
|
||||||
| .key as $label
|
| .key as $lbl
|
||||||
| (if (.value | type) != "array" then
|
| (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)[]
|
else .value end)[]
|
||||||
| (if type != "object" then
|
| (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)
|
else . end)
|
||||||
| ((keys - ["changed-files"]) as $extra
|
| ((keys - ["changed-files"]) as $extra
|
||||||
| if ($extra | length) > 0 then
|
| 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)
|
else . end)
|
||||||
| .["changed-files"]
|
| .["changed-files"]
|
||||||
| (if type == "object" then [.]
|
| (if type == "object" then [.]
|
||||||
elif type == "array" 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
|
| (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)
|
else . end)
|
||||||
| ((keys - ["any-glob-to-any-file"]) as $extra
|
| ((keys - ["any-glob-to-any-file"]) as $extra
|
||||||
| if ($extra | length) > 0 then
|
| 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)
|
else . end)
|
||||||
| .["any-glob-to-any-file"]
|
| .["any-glob-to-any-file"]
|
||||||
| (if type == "string" then [.]
|
| (if type == "string" then [.]
|
||||||
elif type == "array" 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
|
| (if type != "string" then
|
||||||
error("labeler config: \($label): globs must be strings")
|
error("labeler config: \($lbl): globs must be strings")
|
||||||
elif contains("\\") then
|
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)
|
else . end)
|
||||||
| [$label, .] | @tsv
|
| [$lbl, .] | @tsv
|
||||||
'
|
'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,11 @@
|
||||||
|
|
||||||
### Fixed
|
### 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
|
- `labels-reconcile` and `labels-scope` no longer exit 0 on a Forgejo
|
||||||
consumer having read zero facts — measured on `heavy-duty/rig`, where the
|
consumer having read zero facts — measured on `heavy-duty/rig`, where the
|
||||||
sweep printed `reconciled.` over an empty PR list and scope reported "no
|
sweep printed `reconciled.` over an empty PR list and scope reported "no
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,16 @@
|
||||||
set -u
|
set -u
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
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
|
# shellcheck source=test/harness.sh
|
||||||
. "$ROOT/test/harness.sh"
|
. "$ROOT/test/harness.sh"
|
||||||
# shellcheck source=lib/forge.sh
|
# shellcheck source=lib/forge.sh
|
||||||
|
|
@ -25,8 +35,24 @@ detect_in() {
|
||||||
env -i PATH="$PATH" "$@" bash -c '. '"$ROOT"'/lib/forge.sh; forge_detect'
|
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() {
|
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 --------------------------------
|
# --- 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 "forgejo backend wants rest" 0 "" eq rest forge_client forgejo
|
||||||
check "forge_client refuses an unknown backend" 1 "unknown forge" forge_client gitlab
|
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
|
||||||
Loading…
Reference in a new issue