forked from heavy-duty/box
A review landing was never a trigger for the labels workflow, so the exact moment `state:needs-human` became true — the third bot approving — fired nothing, and the label waited on the `*/15` cron. That cron does not run at its declared rate: measured across box, rig and cast over a two-hour window on 2026-07-20, one scheduled run each against the eight `*/15` implies. The obvious fix does not work. There is no `pull_request_review_target`, and on fork PRs — all of them here — `pull_request_review` runs with a read-only token and cannot label anything. So the handoff wakes the sweep itself: - `pull_request_target` also fires on `labeled`/`unlabeled` - the author sets `state:needs-human` at handoff, as the third act after the round summary and the review request The author's own label write fires the sweep that validates it — an optimistic write, not a transfer of ownership. The reconciler confirms or corrects it seconds later, and the cron falls back to a last resort. It cannot loop: the reconciler writes with GITHUB_TOKEN, which does not create workflow runs; agent writes use a PAT, which does. `labels-reconcile.sh` is unchanged — it already recomputes every open PR from scratch on every run, which is what makes the optimistic write safe. The `scope` job is skipped on label events, where no path can have changed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
78 lines
3.1 KiB
YAML
78 lines
3.1 KiB
YAML
name: labels
|
|
# The automation LABELS.md promises. Two halves:
|
|
# scope — path-derived scope:* labels on PRs (actions/labeler)
|
|
# reconcile — the state:* machine + the stale sweep (.github/scripts/labels-reconcile.sh)
|
|
#
|
|
# pull_request_target, not pull_request: every PR here arrives from a fork,
|
|
# where pull_request (and pull_request_review) run 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. So the
|
|
# handoff wakes the sweep itself: the author sets state:needs-human when handing
|
|
# the PR to the maintainer (CONTRIBUTING step 6), and `labeled` fires this
|
|
# workflow, which confirms or corrects that optimistic write within seconds. The
|
|
# cron stays as the last resort, for the round an agent forgets to hand off.
|
|
#
|
|
# This cannot loop: the reconciler's own label 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:
|
|
schedule:
|
|
- cron: "*/15 * * * *"
|
|
workflow_dispatch: # also bootstraps missing labels — run once on a fresh repo
|
|
pull_request_target:
|
|
types:
|
|
[
|
|
opened,
|
|
reopened,
|
|
ready_for_review,
|
|
converted_to_draft,
|
|
synchronize,
|
|
labeled,
|
|
unlabeled,
|
|
]
|
|
|
|
permissions:
|
|
contents: read
|
|
issues: write
|
|
pull-requests: write
|
|
|
|
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.
|
|
if: >-
|
|
github.event_name == 'pull_request_target' &&
|
|
github.event.action != 'labeled' &&
|
|
github.event.action != 'unlabeled'
|
|
runs-on: ubuntu-latest
|
|
concurrency:
|
|
group: labels-scope-${{ github.event.pull_request.number }}
|
|
cancel-in-progress: true
|
|
steps:
|
|
- uses: actions/labeler@v5
|
|
with:
|
|
# 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. GitHub keeps at most
|
|
# one queued run per group (older queued runs are superseded), which
|
|
# coalesces bursts instead of piling them up.
|
|
concurrency:
|
|
group: labels-reconcile
|
|
cancel-in-progress: false
|
|
steps:
|
|
- uses: actions/checkout@v4 # base branch only — never the PR's code
|
|
- name: reconcile state + stale
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
run: bash .github/scripts/labels-reconcile.sh
|