diff --git a/test/labels-triggers.test.sh b/test/labels-triggers.test.sh index 14847c5..f76ed2d 100644 --- a/test/labels-triggers.test.sh +++ b/test/labels-triggers.test.sh @@ -150,4 +150,38 @@ done check "pull_request_target keeps the labeled handoff wake" 0 "labeled" \ trigger_types "$SELF" pull_request_target +# ---- fork heads carry a read-only token on this Forgejo (#241) -------------- +# Same-repo heads keep the existing immediate scope + sweep-dispatch path. A +# fork-headed pull_request_target run must attempt no write: both write-capable +# jobs exclude it, while one successful job explains that the scheduled sweep +# owns its labels. These read the parsed workflow fields rather than grepping +# prose, so a comment cannot satisfy the contract. +job_if_contains_all() { # $1 = file, $2 = job, remaining args = predicates + local file="$1" job="$2" expression predicate + shift 2 + expression="$(yq -r ".jobs.$job.if // \"\"" "$file")" + for predicate in "$@"; do + grep -qF "$predicate" <<<"$expression" || return 1 + done +} +check "scope writes only for a same-repo PR head" 0 "" \ + job_if_contains_all "$REUSABLE" scope \ + "github.event.pull_request.head.repo.full_name == github.repository" +check "the sweep trigger preserves non-PR events and excludes fork heads" 0 "" \ + job_if_contains_all "$REUSABLE" trigger \ + "github.event_name != 'pull_request_target'" \ + "github.event.pull_request.head.repo.full_name == github.repository" +check "a fork-headed PR selects the successful explanation job" 0 "" \ + job_if_contains_all "$REUSABLE" fork_head \ + "github.event_name == 'pull_request_target'" \ + "github.event.pull_request.head.repo.full_name != github.repository" + +fork_head_step() { + yq -r '.jobs.fork_head.steps[] | select(.name == "explain deferred fork labels") | .run' \ + "$REUSABLE" | bash +} +check "the fork path names its read-only token and sweep-cadence wake" 0 \ + "read-only token; writes deferred to the scheduled sweep cadence" \ + fork_head_step + summary