crew#250 verified two facts the design prose now carries: a queue- displaced run is not independently rerunnable (gh run rerun / --failed / --job all refuse), so a victim PR had no manual escape hatch; and the displacing burst is deterministic — one review_requested event per panelist per request — so displacement is the steady state of a working fleet, scaling with panel size, not a traffic spike. CONSUMERS.md now walks the adoption as one atomic four-edit PR with crew as the worked example: the pin bump in every ceremony uses: reference, the new labels-sweep.yml caller, the cron RELOCATED (bold warning: a copied-not-moved schedule double-fires sweeps into the one shared group and reads as the bug getting worse after the fix), and actions: write replacing the labels caller's actions: read. Refs #209 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
131 lines
6.6 KiB
YAML
131 lines
6.6 KiB
YAML
name: labels
|
||
# Reusable half of the labels automation. Triggers and permissions live in
|
||
# the caller; docs/CONSUMERS.md carries the complete caller stub.
|
||
#
|
||
# The caller uses pull_request_target, not pull_request: every PR in this
|
||
# family arrives from a fork, where pull_request runs with a READ-ONLY token
|
||
# and cannot label anything. _target is safe in this workflow because no PR
|
||
# code is ever checked out or executed — scope reads changed paths and the
|
||
# path mapping via the API and checks out only the ceremony implementation.
|
||
# Keep it that way.
|
||
#
|
||
# The reconcile sweep lived here until #209. Riding the PR-triggered run
|
||
# meant every displacement in the sweep's shared concurrency queue recorded
|
||
# a CANCELLED `reconcile` check on some PR — read as red CI by every human
|
||
# and agent, though the surviving sweep does the displaced run's work. Two
|
||
# field facts made that untenable (crew#250): a displaced run cannot be
|
||
# rerun — `gh run rerun`, `--failed`, and `--job` all refuse — so a victim
|
||
# PR has no manual escape hatch; and the displacing burst is deterministic,
|
||
# one `review_requested` event per panelist per request, so every review
|
||
# round displaces runs and the rate scales with panel size. The
|
||
# sweep now lives in labels-sweep.yml behind its own caller, and the
|
||
# trigger job below is its wake: it fires on every event this caller
|
||
# subscribes — the exact surface that used to run reconcile directly — so
|
||
# the wake latency (#137) is unchanged, while a displaced sweep cancels on
|
||
# the Actions tab, attached to no PR. PR checks show scope + trigger only.
|
||
#
|
||
# This cannot loop: the trigger's dispatch and the reconciler's label
|
||
# writes both use GITHUB_TOKEN. GitHub does not create workflow runs from
|
||
# GITHUB_TOKEN-raised events — workflow_dispatch and repository_dispatch
|
||
# are the two documented exemptions, which is exactly why the trigger can
|
||
# wake the sweep with no PAT anywhere in the path — and the sweep itself
|
||
# dispatches nothing. Agent writes use a PAT and therefore do trigger —
|
||
# exactly the asymmetry wanted.
|
||
on:
|
||
workflow_call:
|
||
inputs:
|
||
sweep_workflow:
|
||
description: >-
|
||
Filename of the consumer's sweep caller — the workflow that
|
||
calls labels-sweep.yml (docs/CONSUMERS.md carries the stub).
|
||
The trigger job dispatches it by this name. Override it only
|
||
when the caller file is not named labels-sweep.yml (ceremony's
|
||
own dogfood names it self-labels-sweep.yml).
|
||
type: string
|
||
required: false
|
||
default: labels-sweep.yml
|
||
|
||
env:
|
||
# A called workflow arrives without its repository. Keep this literal pin
|
||
# aligned with the ceremony release consumed by callers (issue #9 D3).
|
||
CEREMONY_SELF_REF: "0.4.0"
|
||
|
||
jobs:
|
||
scope:
|
||
# Not on labeled/unlabeled: those events change no paths, so scope has
|
||
# nothing new to derive — and label churn is precisely what they are.
|
||
# review_requested/review_request_removed likewise change no paths — they
|
||
# exist to wake the sweep (#137) — and running labeler on them widens
|
||
# exactly the window #130 documents, where a label written during a
|
||
# scope run is clobbered.
|
||
if: >-
|
||
github.event_name == 'pull_request_target' &&
|
||
github.event.action != 'labeled' &&
|
||
github.event.action != 'unlabeled' &&
|
||
github.event.action != 'review_requested' &&
|
||
github.event.action != 'review_request_removed'
|
||
runs-on: ubuntu-latest
|
||
concurrency:
|
||
group: labels-scope-${{ github.event.pull_request.number }}
|
||
cancel-in-progress: true
|
||
steps:
|
||
# actions/labeler@v5 held this seat until #130. Even with
|
||
# sync-labels: false it wrote the WHOLE label set — PUT of
|
||
# (labels-fetched-at-job-start ∪ derived) — so a label applied while
|
||
# the job ran was silently removed: ceremony#128 lost its `release`,
|
||
# the merge door's declared-intent read, two seconds after the
|
||
# builder set it. v6/v7 write the same way, so the step was replaced
|
||
# rather than repinned. labels-scope reads the consumer's
|
||
# .github/labeler.yml and the changed paths via the API, and its
|
||
# only write is an additive POST of the derived scopes: a label
|
||
# applied mid-job survives by construction.
|
||
#
|
||
# Still no PR code: both checkouts below fetch the ceremony
|
||
# implementation only. The dogfood checkout rides github.sha — the
|
||
# base-branch commit the workflow file itself came from, so the
|
||
# script and workflow can never skew — and doubles as the #11
|
||
# bootstrap: ceremony's own labels must work before any release tag
|
||
# exists for the pinned checkout to fetch.
|
||
- uses: actions/checkout@v4
|
||
if: github.repository == 'heavy-duty/ceremony'
|
||
with:
|
||
repository: ${{ github.repository }}
|
||
ref: ${{ github.sha }}
|
||
- uses: actions/checkout@v4
|
||
if: github.repository != 'heavy-duty/ceremony'
|
||
with:
|
||
repository: heavy-duty/ceremony
|
||
ref: ${{ env.CEREMONY_SELF_REF }}
|
||
- uses: ./actions/labels-scope
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
REPO: ${{ github.repository }}
|
||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||
# the BASE branch commit — a PR must not label itself by editing
|
||
# the mapping it is judged by
|
||
CONFIG_REF: ${{ github.sha }}
|
||
|
||
trigger:
|
||
# The sweep's wake (#209). No `if:`: reconcile carried none, so the
|
||
# trigger keeps the whole event surface the caller subscribes —
|
||
# workflow_dispatch of the labels caller itself included. That cannot
|
||
# double-fire bootstrap: this dispatch always carries bootstrap=no, so
|
||
# a dispatched labels caller yields one plain sweep, and the taxonomy
|
||
# bootstrap fires solely on a manual dispatch of the sweep caller
|
||
# (whose input defaults to "yes"). Excluding workflow_dispatch here
|
||
# would instead make a dispatched labels caller do nothing at all —
|
||
# a silent no-op run is worse than a redundant sweep.
|
||
#
|
||
# LOUD on failure — never `|| true`: a red trigger is the
|
||
# misconfiguration alarm. A consumer that bumps the pin without adding
|
||
# the sweep caller (workflow-not-found), without its declared
|
||
# `bootstrap` input (unexpected input), or without `actions: write`
|
||
# on this caller (permission denied) fails HERE, visibly on the PR,
|
||
# instead of silently never sweeping again.
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: dispatch the sweep
|
||
env:
|
||
GH_TOKEN: ${{ github.token }}
|
||
SWEEP_WORKFLOW: ${{ inputs.sweep_workflow }}
|
||
run: gh workflow run "$SWEEP_WORKFLOW" -R "$GITHUB_REPOSITORY" -f bootstrap=no
|