docs(labels): split fork and same-repo wake latency

This commit is contained in:
codex-bot-andresmgsl 2026-08-24 23:59:17 +00:00
parent ffbc1afc3d
commit e639e67f09
5 changed files with 61 additions and 45 deletions

View file

@ -3,9 +3,10 @@ name: labels-sweep
# jobs that rode labels.yml until #209. Triggers and permissions live in the # jobs that rode labels.yml until #209. Triggers and permissions live in the
# caller; docs/CONSUMERS.md carries the complete caller stub # caller; docs/CONSUMERS.md carries the complete caller stub
# (workflow_dispatch plus the hourly cron, which relocated here with the # (workflow_dispatch plus the hourly cron, which relocated here with the
# sweep). Board events still yield a sweep within seconds: labels.yml's # sweep). Issue events and same-repository PR events still yield a sweep within
# trigger job dispatches this workflow's caller on every event it used to # seconds: labels.yml's trigger job dispatches this workflow's caller on those
# run reconcile on. # events. Fork-headed PR events carry a read-only token on this Forgejo and
# wait for the caller's scheduled cadence instead (#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

View file

@ -7,12 +7,14 @@ name: labels-sweep
on: on:
# The consumer owns this cadence (#203). Hourly is the recommended default # The consumer owns this cadence (#203). Hourly is the recommended default
# when no other engine drives board state: the cron is then the sweep's ONLY # when no other engine drives board state: the cron is then the sweep's ONLY
# wake for four transition classes — a review verdict landing (there is no # wake for a review verdict landing (there is no
# 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. The labels caller's events carry the rest in seconds, one # claim-reclaim, plus every fork-headed PR transition on this Forgejo because
# trigger-job dispatch away. Hourly trades ≤1h of latency on those four # its pull_request_target token is read-only (#241). Issue events and
# same-repository PR events carry the rest in seconds, one trigger-job
# dispatch away. Hourly trades ≤1h of latency on the scheduled classes
# while cutting nominal scheduled sweeps from four an hour to one at # 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
@ -22,8 +24,9 @@ on:
# 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 — what dispatching the labels # bootstraps the taxonomy on a fresh repo — what dispatching the labels
# caller did before #209. The reusable's trigger job wakes this workflow # caller did before #209. The reusable's trigger job wakes this workflow
# with bootstrap=no on every board event — an event-woken sweep must not # with bootstrap=no on every issue and same-repository PR event — an
# re-upsert ~20 labels each time — so declaring this input is part of the # event-woken sweep must not re-upsert ~20 labels each time — so declaring
# this input is part of the
# caller contract: a dispatch naming an undeclared input is refused, and # caller contract: a dispatch naming an undeclared input is refused, and
# the trigger job goes loudly red. # the trigger job goes loudly red.
workflow_dispatch: workflow_dispatch:

View file

@ -8,8 +8,10 @@ name: labels
# Since #209 this caller carries the PR/issue event surface only. The # Since #209 this caller carries the PR/issue event surface only. The
# reconcile sweep no longer rides these runs — the reusable's trigger job # reconcile sweep no longer rides these runs — the reusable's trigger job
# dispatches the sweep caller (self-labels-sweep.yml here), which owns the # dispatches the sweep caller (self-labels-sweep.yml here), which owns the
# hourly cron and the manual/bootstrap workflow_dispatch. A board event # hourly cron and the manual/bootstrap workflow_dispatch. Issue events and
# below still yields a sweep within seconds, one dispatch hop later. # 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,
# so their successful labels run leaves writes to the hourly sweep (#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/
@ -26,14 +28,14 @@ on:
issues: issues:
types: [opened, closed, edited, reopened] types: [opened, closed, edited, reopened]
pull_request_target: pull_request_target:
# Every PR arrives from a fork, so these carry the head/draft/review facts # These carry the head/draft/review facts the sweep derives state:* from.
# the sweep derives state:* from. labeled/unlabeled are the handoff wake — # Same-repository heads wake that sweep in seconds; fork heads cannot write
# the author's optimistic state:needs-human write, confirmed or corrected # with this Forgejo's read-only token and wait for its scheduled cadence.
# here in seconds (#11); synchronize re-derives on every push; # labeled/unlabeled are the same-repository handoff wake — the author's
# review_requested/review_request_removed wake the sweep that clears (or # optimistic state:needs-human write, confirmed or corrected here in
# restores) blocker:unrequested — without them the one event that makes # seconds (#11); synchronize re-derives on every push; review_requested/
# the label false could not clear it, and a quiet repo wore the red flag # review_request_removed clear (or restore) blocker:unrequested on that
# until the advisory cron (#137). # same instant path (#137).
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed] types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed]
permissions: permissions:
contents: read contents: read

3
changelog.d/241.md Normal file
View file

@ -0,0 +1,3 @@
### Fixed
- 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

@ -337,14 +337,16 @@ The labels automation is two reusable workflows since #209, adopted
together at the same pin: together at the same pin:
- **`labels.yml`** — the event-facing half, called on PR and issue events. - **`labels.yml`** — the event-facing half, called on PR and issue events.
Two jobs: additive path-based `scope:*` labels, and a few-seconds Same-repository PRs keep two write-capable jobs: additive path-based
`trigger` job that wakes the sweep by dispatching the consumer's sweep `scope:*` labels, and a few-seconds `trigger` job that wakes the sweep by
caller (a REST `POST` to the forge's own dispatching the consumer's sweep caller (a REST `POST` to the forge's own
`${GITHUB_API_URL}/repos/{owner}/{repo}/actions/workflows/{file}/dispatches`, `${GITHUB_API_URL}/repos/{owner}/{repo}/actions/workflows/{file}/dispatches`,
plain `GITHUB_TOKEN``workflow_dispatch` is plain `GITHUB_TOKEN``workflow_dispatch` is
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). 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
the deferral, and the scheduled sweep owns their labels.
- **`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
@ -356,8 +358,9 @@ together at the same pin:
as fake red CI that GitHub refuses to rerun (crew#250: `gh run rerun` as fake red CI that GitHub refuses to rerun (crew#250: `gh run rerun`
and its `--failed`/`--job` forms all decline a queue-displaced run). and its `--failed`/`--job` forms all decline a queue-displaced run).
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; PR checks show `scope` and the green Actions tab, attached to no PR. Same-repository PR checks show `scope` and
`trigger` only. the green `trigger`; fork-headed PRs show the green `fork_head` disposition
and wait for the scheduled sweep cadence.
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`.
@ -384,11 +387,12 @@ The complete event-facing caller is:
name: labels name: labels
on: on:
pull_request_target: pull_request_target:
# Fork PRs; these carry the head/draft/review facts state:* derives from. # These carry the head/draft/review facts state:* derives from. Same-repo
# labeled/unlabeled are the handoff wake (state:needs-human confirmed here); # heads take the instant write + sweep-dispatch path; this Forgejo gives
# synchronize re-derives on every push. review_requested/review_request_removed # fork heads a read-only token, so they wait for the scheduled sweep.
# (shipped in 0.3.0, ceremony#137) wake the sweep that clears # labeled/unlabeled are the same-repo handoff wake; synchronize re-derives
# blocker:unrequested when the panel is asked. # 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.
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed] types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed]
# Available at 0.2.0 and later (the first tag carrying ceremony#32); a # Available at 0.2.0 and later (the first tag carrying ceremony#32); a
# consumer pinned to 0.1.0 omits this block. # consumer pinned to 0.1.0 omits this block.
@ -425,20 +429,21 @@ name: labels-sweep
on: on:
# The consumer owns this cadence (#203). Hourly is the recommended default # The consumer owns this cadence (#203). Hourly is the recommended default
# when no other engine drives board state: the cron is then the sweep's only # when no other engine drives board state: the cron is then the sweep's only
# wake for four transition classes — 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. The labels caller's events carry the # time-based stale / 48h claim-reclaim, plus every fork-headed PR transition
# rest in seconds, one trigger-job dispatch away. Hourly trades ≤1h of # on this Forgejo. Issue events and same-repo PR
# latency on those four while cutting nominal scheduled sweeps from four an # events carry the rest in seconds, one trigger-job dispatch away. Hourly
# hour to one at GitHub's 1-minute floor. Do not delete the cron: it is their # trades ≤1h of latency on the scheduled classes while cutting nominal
# discovery path. If another engine writes some of those transitions, only # sweeps from four an hour to one at GitHub's 1-minute floor. Do not delete
# the classes with no other writer bound the cadence; relax it only as that # the cron: it is their discovery path. If another engine writes some of
# list shrinks. # those transitions, only the classes with no other writer bound the cadence;
# relax it only as that list shrinks.
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 board event, so the # wakes this workflow with bootstrap=no on every issue and same-repo PR event, so the
# declared input is part of the contract: a dispatch naming an undeclared # declared input is part of the contract: a dispatch naming an 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:
@ -542,16 +547,18 @@ Bump without the sweep caller and the trigger job goes red on every PR
and issue event — the loud failure mode above — so never split these and issue event — the loud failure mode above — so never split these
four edits across PRs. four edits across PRs.
`pull_request_target` is intentional: fork PRs need the base repository's `pull_request_target` is intentional: same-repository PRs keep the base
token to write labels. The reusable workflows execute no PR code. They check repository's write token without executing PR code. This Forgejo still gives
out only the consumer's base branch and the pinned ceremony implementation. fork-headed `_target` runs a read-only token, so they attempt no writes and
leave labels to the scheduled sweep. The reusable workflows check 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
`pull_request_target:` shipped in `0.3.0` (ceremony#137) — the wake that `pull_request_target:` shipped in `0.3.0` (ceremony#137). It clears
clears `blocker:unrequested` the moment the panel is asked, without which a `blocker:unrequested` the moment the panel is asked on a same-repository head;
quiet repo wears that flag until the backstop cron; a consumer picks them up fork heads wait for the sweep cadence on this Forgejo. A consumer picks the
by pinning `0.3.0` or later, never through mixed refs. events up by pinning `0.3.0` or later, never through mixed refs.
`.github/labels.conf` has one mandatory panel setting, one mandatory `.github/labels.conf` has one mandatory panel setting, one mandatory
`triage-actors` setting, zero or more optional per-author panel rows, and `triage-actors` setting, zero or more optional per-author panel rows, and