Compare commits

..

No commits in common. "76af45445845420db5a61acaeb0e9945d5d98d49" and "6bf47a68d446d47921a8449fa470e5811d893ced" have entirely different histories.

7 changed files with 58 additions and 77 deletions

View file

@ -5,9 +5,8 @@ name: labels-sweep
# (workflow_dispatch plus the hourly cron, which relocated here with the # (workflow_dispatch plus the hourly cron, which relocated here with the
# sweep). Issue events and same-repository PR events still yield a sweep within # sweep). Issue events and same-repository PR events still yield a sweep within
# seconds: labels.yml's trigger job dispatches this workflow's caller on those # seconds: labels.yml's trigger job dispatches this workflow's caller on those
# events. Fork-headed PR events carry a read-only token on this Forgejo, so # events. Fork-headed PR events carry a read-only token on this Forgejo and
# state, blocker, and handoff reconciliation waits for the caller's scheduled # wait for the caller's scheduled cadence instead (#241).
# cadence; the sweep does not apply path-derived scope labels (#241).
# #
# Detached on purpose (#209): every sweep covers every open PR and all # Detached on purpose (#209): every sweep covers every open PR and all
# sweeps serialize through ONE shared concurrency group, so GitHub's # sweeps serialize through ONE shared concurrency group, so GitHub's
@ -26,8 +25,7 @@ name: labels-sweep
# taxonomy (its `bootstrap` input defaults to "yes"), exactly what # taxonomy (its `bootstrap` input defaults to "yes"), exactly what
# dispatching the labels caller did before the split. The trigger job's # dispatching the labels caller did before the split. The trigger job's
# dispatches carry bootstrap=no — ~20 label upserts per sweep is too chatty # dispatches carry bootstrap=no — ~20 label upserts per sweep is too chatty
# for every issue and same-repository PR wake, the same reason cron runs never # for every board event, the same reason cron runs never bootstrapped.
# bootstrapped.
# #
# This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not # This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not
# create workflow runs from GITHUB_TOKEN-raised events (the trigger's # create workflow runs from GITHUB_TOKEN-raised events (the trigger's

View file

@ -5,13 +5,11 @@ name: labels
# The caller uses pull_request_target, not pull_request, so same-repository PRs # The caller uses pull_request_target, not pull_request, so same-repository PRs
# keep the base repository's write token without running PR code. On this # keep the base repository's write token without running PR code. On this
# Forgejo, unlike GitHub, fork-headed _target runs still receive a read-only # Forgejo, unlike GitHub, fork-headed _target runs still receive a read-only
# token. Those runs therefore attempt no writes. The scheduled sweep later # token. Those runs therefore attempt no writes and leave labeling to the
# reconciles state, blockers, and handoff, but it does not apply path-derived # scheduled sweep; the explicit fork_head job below records that disposition
# scope labels; consumers that require those labels on fork heads apply them # as a successful check. Both write paths execute only for same-repository
# manually. The explicit fork_head job below records that disposition as a # heads. Scope reads changed paths and the path mapping through the API and
# successful check. Both write paths execute only for same-repository heads. # checks out only the ceremony implementation. Keep it that way (#241).
# Scope reads changed paths and the path mapping through the API and checks out
# only the ceremony implementation. Keep it that way (#241).
# #
# The reconcile sweep lived here until #209. Riding the PR-triggered run # The reconcile sweep lived here until #209. Riding the PR-triggered run
# meant every displacement in the sweep's shared concurrency queue recorded # meant every displacement in the sweep's shared concurrency queue recorded
@ -28,8 +26,7 @@ name: labels
# the surface that used to run reconcile directly. Same-repository wake latency # the surface that used to run reconcile directly. Same-repository wake latency
# (#137) remains seconds-scale, while a displaced sweep cancels on the Actions # (#137) remains seconds-scale, while a displaced sweep cancels on the Actions
# tab, attached to no PR. Fork-headed runs cannot dispatch with their read-only # tab, attached to no PR. Fork-headed runs cannot dispatch with their read-only
# token, so state, blocker, and handoff reconciliation waits for the scheduled # token, so their labels arrive on the scheduled sweep cadence. PR checks show
# sweep; path-derived scope labels are not applied to fork heads. PR checks show
# scope + trigger for same-repository heads, or fork_head for fork heads. # scope + trigger for same-repository heads, or fork_head for fork heads.
# #
# This cannot loop: the trigger's dispatch and the reconciler's label # This cannot loop: the trigger's dispatch and the reconciler's label
@ -118,8 +115,7 @@ jobs:
# The sweep's instant wake (#209) keeps the whole non-PR event surface and # The sweep's instant wake (#209) keeps the whole non-PR event surface and
# same-repository PRs. Fork-headed PRs are excluded because this Forgejo # same-repository PRs. Fork-headed PRs are excluded because this Forgejo
# gives their pull_request_target run a read-only token (#241); fork_head # gives their pull_request_target run a read-only token (#241); fork_head
# records which reconciliation waits for the sweep and that path-derived # records the successful deferral to the scheduled sweep instead.
# scope labels are not applied there.
# #
# LOUD on failure — never `|| true`: a red trigger is the # LOUD on failure — never `|| true`: a red trigger is the
# misconfiguration alarm. A consumer that bumps the pin without adding # misconfiguration alarm. A consumer that bumps the pin without adding
@ -209,9 +205,6 @@ jobs:
echo "labels: sweep dispatched — $SWEEP_WORKFLOW on $branch (bootstrap=no)" echo "labels: sweep dispatched — $SWEEP_WORKFLOW on $branch (bootstrap=no)"
fork_head: fork_head:
# This Forgejo keeps pull_request_target read-only for fork heads (#241),
# so name the deliberately unsupported scope write as well as the deferred
# state machine instead of letting a green no-op promise full labelling.
if: >- if: >-
github.event_name == 'pull_request_target' && github.event_name == 'pull_request_target' &&
github.event.pull_request.head.repo.full_name != github.repository github.event.pull_request.head.repo.full_name != github.repository
@ -219,4 +212,4 @@ jobs:
steps: steps:
- name: explain deferred fork labels - name: explain deferred fork labels
run: >- run: >-
echo "labels: fork head has a read-only token; state, blocker, and handoff reconciliation deferred to the scheduled sweep; path-derived scope labels are not applied to fork heads" echo "labels: fork head has a read-only token; writes deferred to the scheduled sweep cadence"

View file

@ -11,12 +11,11 @@ on:
# pull_request_review trigger on the labels caller), blocker:ci-red set or # pull_request_review trigger on the labels caller), blocker:ci-red set or
# cleared (no check_suite/check_run/workflow_run), a blocker:conflict when # cleared (no check_suite/check_run/workflow_run), a blocker:conflict when
# ANOTHER PR merges under this one, and the time-based stale / 48h # ANOTHER PR merges under this one, and the time-based stale / 48h
# claim-reclaim, plus every state, blocker, and handoff transition for a # claim-reclaim, plus every fork-headed PR transition on this Forgejo because
# fork-headed PR on this Forgejo because its pull_request_target token is # its pull_request_target token is read-only (#241). Issue events and
# read-only (#241). The sweep never applies path-derived scope labels. Issue # same-repository PR events carry the rest in seconds, one trigger-job
# events and same-repository PR events carry the rest in seconds, one # dispatch away. Hourly trades ≤1h of latency on the scheduled classes
# trigger-job dispatch away. Hourly trades ≤1h of latency on the scheduled # while cutting nominal scheduled sweeps from four an hour to one at
# classes while cutting nominal scheduled sweeps from four an hour to one at
# GitHub's 1-minute billing floor. Do not delete the cron: it is their # GitHub's 1-minute billing floor. Do not delete the cron: it is their
# discovery path. If another engine writes some of those transitions, only # discovery path. If another engine writes some of those transitions, only
# the classes with no other writer bound the cadence; relax it only as that # the classes with no other writer bound the cadence; relax it only as that

View file

@ -11,9 +11,7 @@ name: labels
# hourly cron and the manual/bootstrap workflow_dispatch. Issue events and # hourly cron and the manual/bootstrap workflow_dispatch. Issue events and
# same-repository PR events below still yield a sweep within seconds, one # same-repository PR events below still yield a sweep within seconds, one
# dispatch hop later. Fork-headed PRs carry a read-only token on this Forgejo, # dispatch hop later. Fork-headed PRs carry a read-only token on this Forgejo,
# so their successful labels run leaves state, blocker, and handoff # so their successful labels run leaves writes to the hourly sweep (#241).
# reconciliation to the hourly sweep; path-derived scope labels are not
# applied to those heads (#241).
on: on:
# Narrowed (#199) to the actions that carry a queue-state change the hourly # Narrowed (#199) to the actions that carry a queue-state change the hourly
# cron cannot wait one cadence for — dropping only labeled/unlabeled/assigned/ # cron cannot wait one cadence for — dropping only labeled/unlabeled/assigned/
@ -32,9 +30,7 @@ on:
pull_request_target: pull_request_target:
# These carry the head/draft/review facts the sweep derives state:* from. # These carry the head/draft/review facts the sweep derives state:* from.
# Same-repository heads wake that sweep in seconds; fork heads cannot write # Same-repository heads wake that sweep in seconds; fork heads cannot write
# with this Forgejo's read-only token, so state, blocker, and handoff # with this Forgejo's read-only token and wait for its scheduled cadence.
# reconciliation waits for the scheduled cadence. The sweep does not apply
# path-derived scope labels to those heads (#241).
# labeled/unlabeled are the same-repository handoff wake — the author's # labeled/unlabeled are the same-repository handoff wake — the author's
# optimistic state:needs-human write, confirmed or corrected here in # optimistic state:needs-human write, confirmed or corrected here in
# seconds (#11); synchronize re-derives on every push; review_requested/ # seconds (#11); synchronize re-derives on every push; review_requested/

View file

@ -1,3 +1,3 @@
### Fixed ### Fixed
- Fork-headed label runs stay green without attempting forbidden writes, while same-repository heads keep instant scope and reconciliation wakes (#241). - Fork-headed label runs now stay green without attempting writes their read-only token cannot make, while same-repository heads keep instant labeling (#241).

View file

@ -345,10 +345,8 @@ together at the same pin:
one of the two documented exemptions from the token's no-retrigger rule, one of the two documented exemptions from the token's no-retrigger rule,
so no PAT anywhere in the path and no loop: the sweep dispatches so no PAT anywhere in the path and no loop: the sweep dispatches
nothing). On this Forgejo a fork-headed `pull_request_target` token is nothing). On this Forgejo a fork-headed `pull_request_target` token is
read-only, so those two jobs do not run. A successful `fork_head` job names read-only, so those two jobs do not run; a successful `fork_head` job names
the disposition: the scheduled sweep later reconciles state, blockers, and the deferral, and the scheduled sweep owns their labels.
handoff, while path-derived `scope:*` labels are not applied to fork heads.
Apply those scope labels manually when an outside contribution needs them.
- **`labels-sweep.yml`** — the reconcile sweep: PR state, blockers, - **`labels-sweep.yml`** — the reconcile sweep: PR state, blockers,
handoff, stale status, the issue work queue, and the `needs-ruling` handoff, stale status, the issue work queue, and the `needs-ruling`
invariants on both surfaces — the bare-flag check and the 7-day invariants on both surfaces — the bare-flag check and the 7-day
@ -362,8 +360,7 @@ together at the same pin:
Behind its own caller, a displaced sweep cancels on the Behind its own caller, a displaced sweep cancels on the
Actions tab, attached to no PR. Same-repository PR checks show `scope` and Actions tab, attached to no PR. Same-repository PR checks show `scope` and
the green `trigger`; fork-headed PRs show the green `fork_head` disposition the green `trigger`; fork-headed PRs show the green `fork_head` disposition
and wait for scheduled state, blocker, and handoff reconciliation. The sweep and wait for the scheduled sweep cadence.
does not supply their path-derived scope labels.
The consumer keeps its path mapping in `.github/labeler.yml` and its The consumer keeps its path mapping in `.github/labeler.yml` and its
review panel plus scope taxonomy in `.github/labels.conf`. review panel plus scope taxonomy in `.github/labels.conf`.
@ -392,9 +389,7 @@ on:
pull_request_target: pull_request_target:
# These carry the head/draft/review facts state:* derives from. Same-repo # These carry the head/draft/review facts state:* derives from. Same-repo
# heads take the instant write + sweep-dispatch path; this Forgejo gives # heads take the instant write + sweep-dispatch path; this Forgejo gives
# fork heads a read-only token, so state, blocker, and handoff reconciliation # fork heads a read-only token, so they wait for the scheduled sweep.
# waits for the scheduled sweep; path-derived scope labels require a manual
# write when wanted.
# labeled/unlabeled are the same-repo handoff wake; synchronize re-derives # labeled/unlabeled are the same-repo handoff wake; synchronize re-derives
# on every push. review_requested/review_request_removed shipped in 0.3.0 # on every push. review_requested/review_request_removed shipped in 0.3.0
# (ceremony#137) and wake the same-repo sweep when the panel is asked. # (ceremony#137) and wake the same-repo sweep when the panel is asked.
@ -437,10 +432,9 @@ on:
# wake for a review verdict landing (no # wake for a review verdict landing (no
# pull_request_review trigger on the labels caller), blocker:ci-red # pull_request_review trigger on the labels caller), blocker:ci-red
# set/cleared, blocker:conflict when another PR merges under this one, and # set/cleared, blocker:conflict when another PR merges under this one, and
# time-based stale / 48h claim-reclaim, plus every state, blocker, and handoff # time-based stale / 48h claim-reclaim, plus every fork-headed PR transition
# transition for a fork-headed PR on this Forgejo. The sweep never applies # on this Forgejo. Issue events and same-repo PR
# path-derived scope labels. Issue events and same-repo PR events carry the # events carry the rest in seconds, one trigger-job dispatch away. Hourly
# rest in seconds, one trigger-job dispatch away. Hourly
# trades ≤1h of latency on the scheduled classes while cutting nominal # trades ≤1h of latency on the scheduled classes while cutting nominal
# sweeps from four an hour to one at GitHub's 1-minute floor. Do not delete # sweeps from four an hour to one at GitHub's 1-minute floor. Do not delete
# the cron: it is their discovery path. If another engine writes some of # the cron: it is their discovery path. If another engine writes some of
@ -449,9 +443,9 @@ on:
schedule: [{cron: "0 * * * *"}] schedule: [{cron: "0 * * * *"}]
# A manual full-board sweep. A bare dispatch (input default "yes") also # A manual full-board sweep. A bare dispatch (input default "yes") also
# bootstraps the taxonomy on a fresh repo. The labels caller's trigger job # bootstraps the taxonomy on a fresh repo. The labels caller's trigger job
# wakes this workflow with bootstrap=no on every issue and same-repo PR # wakes this workflow with bootstrap=no on every issue and same-repo PR event, so the
# event, so the declared input is part of the contract: a dispatch naming an # declared input is part of the contract: a dispatch naming an undeclared
# undeclared input is refused, and the trigger job goes loudly red. # input is refused, and the trigger job goes loudly red.
workflow_dispatch: workflow_dispatch:
inputs: inputs:
bootstrap: bootstrap:
@ -491,8 +485,7 @@ caller's `actions: write` is different — it is required everywhere, public
repos included: the trigger job's dispatch is a write. Without it, issue and repos included: the trigger job's dispatch is a write. Without it, issue and
same-repository PR event runs go red at the trigger. Fork-headed PR runs do same-repository PR event runs go red at the trigger. Fork-headed PR runs do
not enter that write path on this Forgejo; they remain green and depend on a not enter that write path on this Forgejo; they remain green and depend on a
healthy scheduled sweep for state, blocker, and handoff reconciliation. That healthy scheduled sweep.
sweep does not apply their path-derived scope labels.
**The failure mode to know before bumping**: a consumer that bumps its pin **The failure mode to know before bumping**: a consumer that bumps its pin
to a #209-carrying tag without adding the sweep caller gets a loud red trigger to a #209-carrying tag without adding the sweep caller gets a loud red trigger
@ -555,17 +548,14 @@ carrying the split:
write. The sweep caller keeps `actions: read`. write. The sweep caller keeps `actions: read`.
Bump without the sweep caller and the trigger job goes red on every issue and Bump without the sweep caller and the trigger job goes red on every issue and
same-repository PR event. Fork-headed PRs stay green, receive state, blocker, same-repository PR event; fork-headed PRs stay green but receive no labels
and handoff reconciliation only from the scheduled sweep, and never receive without the scheduled sweep. Never split these four edits across PRs.
path-derived scope labels automatically; apply those manually when wanted.
Never split these four edits across PRs.
`pull_request_target` is intentional: same-repository PRs keep the base `pull_request_target` is intentional: same-repository PRs keep the base
repository's write token without executing PR code. This Forgejo still gives repository's write token without executing PR code. This Forgejo still gives
fork-headed `_target` runs a read-only token, so they attempt no writes. The fork-headed `_target` runs a read-only token, so they attempt no writes and
scheduled sweep later reconciles state, blockers, and handoff; it does not leave labels to the scheduled sweep. The reusable workflows check out only the
apply path-derived scope labels to those heads. The reusable workflows check consumer's base branch and the pinned ceremony implementation.
out only the consumer's base branch and the pinned ceremony implementation.
The #52 ruling invariants ride exactly these triggers — but the caller above The #52 ruling invariants ride exactly these triggers — but the caller above
is no longer the #18 shape, so adopting current triggers is a stub edit, not is no longer the #18 shape, so adopting current triggers is a stub edit, not
a bare pin bump. `review_requested` and `review_request_removed` on a bare pin bump. `review_requested` and `review_request_removed` on

View file

@ -153,30 +153,35 @@ check "pull_request_target keeps the labeled handoff wake" 0 "labeled" \
# ---- fork heads carry a read-only token on this Forgejo (#241) -------------- # ---- fork heads carry a read-only token on this Forgejo (#241) --------------
# Same-repo heads keep the existing immediate scope + sweep-dispatch path. A # 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 # fork-headed pull_request_target run must attempt no write: both write-capable
# jobs exclude it, while one successful job explains exactly what the scheduled # jobs exclude it, while one successful job explains that the scheduled sweep
# sweep does and does not supply. Compare the whole normalised expressions: a # owns its labels. These read the parsed workflow fields rather than grepping
# substring guard also accepts a future negation that inverts the gate. # prose, so a comment cannot satisfy the contract.
job_if_expression() { # $1 = file, $2 = job job_if_contains_all() { # $1 = file, $2 = job, remaining args = predicates
yq -r ".jobs.$2.if // \"\"" "$1" | local file="$1" job="$2" expression predicate
tr '\n' ' ' | shift 2
awk '{$1=$1; print}' 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 \ check "scope writes only for a same-repo PR head" 0 "" \
"github.event_name == 'pull_request_target' && github.event.pull_request.head.repo.full_name == github.repository && github.event.action != 'labeled' && github.event.action != 'unlabeled' && github.event.action != 'review_requested' && github.event.action != 'review_request_removed'" \ job_if_contains_all "$REUSABLE" scope \
job_if_expression "$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 \ check "the sweep trigger preserves non-PR events and excludes fork heads" 0 "" \
"github.event_name != 'pull_request_target' || github.event.pull_request.head.repo.full_name == github.repository" \ job_if_contains_all "$REUSABLE" trigger \
job_if_expression "$REUSABLE" trigger "github.event_name != 'pull_request_target'" \
check "a fork-headed PR selects the successful explanation job" 0 \ "github.event.pull_request.head.repo.full_name == github.repository"
"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_expression "$REUSABLE" fork_head 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() { fork_head_step() {
yq -r '.jobs.fork_head.steps[] | select(.name == "explain deferred fork labels") | .run' \ yq -r '.jobs.fork_head.steps[] | select(.name == "explain deferred fork labels") | .run' \
"$REUSABLE" | bash "$REUSABLE" | bash
} }
check "the fork path distinguishes swept state from unsupported scope writes" 0 \ check "the fork path names its read-only token and sweep-cadence wake" 0 \
"read-only token; state, blocker, and handoff reconciliation deferred to the scheduled sweep; path-derived scope labels are not applied to fork heads" \ "read-only token; writes deferred to the scheduled sweep cadence" \
fork_head_step fork_head_step
summary summary