ceremony/.github/workflows/labels.yml
claude-bot-andresmgsl c0e796b6c9 fix: review_requested wakes the sweep — blocker:unrequested clears when the ask lands
The reconciler's rule was right and blind: the caller never listened on
review_requested/review_request_removed, so the one event that falsifies
(or restores) blocker:unrequested could not clear it, and a quiet repo
wore the red flag until the advisory cron (#137's timeline: 93 seconds,
cleared only by an unrelated PR's push).

- self-labels.yml + the CONSUMERS.md stub gain both types; the scope job
  skips them (no paths change; running labeler there widens #130's window)
- test/labels.test.sh: caller/stub parity row with mutation cases —
  dropped type either side, one-sided reorder, all red
- CONSUMERS.md no longer claims trigger adoption is a bare pin bump; the
  pending stub edit is named and rides the first tag carrying ceremony#137

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-24 12:50:09 +00:00

115 lines
5.3 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 — labeler reads changed paths via the
# API, and reconcile checks out the BASE branch only. Keep it that way.
#
# There is no pull_request_review_target, so a review landing cannot wake this
# workflow directly — and the */15 cron is advisory: GitHub deprioritises
# short intervals hard enough that a quiet repo goes hours between ticks. The
# handoff wakes the sweep itself: the author sets state:needs-human, and the
# caller's `labeled` event confirms or corrects that optimistic write within
# seconds. The cron stays as the last resort for a forgotten handoff.
#
# This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not
# create workflow runs from GITHUB_TOKEN-triggered events. Agent writes use a
# PAT and therefore do trigger — exactly the asymmetry wanted.
on:
workflow_call:
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.2.0"
jobs:
scope:
# Not on labeled/unlabeled: those events change no paths, so labeler 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 reconcile (#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:
- uses: actions/labeler@v5
with:
# labeler reads the consumer's .github/labeler.yml via the API
# additive only — a hand-applied scope must survive the machine
sync-labels: false
reconcile:
runs-on: ubuntu-latest
# ONE shared group: every reconcile sweeps every open PR, so cron and
# PR-event runs must serialize or two sweeps race the same PR's labels
# and both pass the request-the-human-once guard.
concurrency:
group: labels-reconcile
cancel-in-progress: false
steps:
# pull_request_target is required for fork PR write permission. It is
# safe here because no PR code is ever checked out or executed: labeler
# reads paths via the API, and reconcile checks out the BASE branch only.
# Keep it that way.
- uses: actions/checkout@v4
with:
repository: ${{ github.repository }}
ref: ${{ github.event.repository.default_branch }}
- uses: actions/checkout@v4
# The self-consumption bypass — release.yml's twin, and load-bearing
# for the same reason (#11): ceremony's own labels bootstrap must
# run BEFORE any release tag exists for this checkout to fetch — the
# release label the merge door reads is created by that dispatch, so
# without the bypass the first release deadlocks on its own pin. The
# base-branch checkout above already IS ceremony on the dogfood
# path.
if: github.repository != 'heavy-duty/ceremony'
with:
repository: heavy-duty/ceremony
ref: ${{ env.CEREMONY_SELF_REF }}
path: .ceremony-src
# Two steps, mutually exclusive `if:`s, because a `uses:` path must be
# a literal — the same fork release.yml's CEREMONY_DIR env line
# papers over for `run:` steps, which composite `uses:` has no
# equivalent of.
- name: reconcile state + stale
if: github.repository != 'heavy-duty/ceremony'
uses: ./.ceremony-src/actions/labels-reconcile
with:
bootstrap: ${{ github.event_name == 'workflow_dispatch' && 'yes' || 'no' }}
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
- name: reconcile state + stale (dogfood — the workspace IS ceremony)
if: github.repository == 'heavy-duty/ceremony'
uses: ./actions/labels-reconcile
with:
bootstrap: ${{ github.event_name == 'workflow_dispatch' && 'yes' || 'no' }}
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
- name: reconcile issue flow
if: github.repository != 'heavy-duty/ceremony'
uses: ./.ceremony-src/actions/issueflow-reconcile
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
- name: reconcile issue flow (dogfood — the workspace IS ceremony)
if: github.repository == 'heavy-duty/ceremony'
uses: ./actions/issueflow-reconcile
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}