forked from heavy-duty/ceremony
@codex-reviewer-andresmgsl's blocker 1 is right and the filename exemption was
the wrong shape. It exempted the whole FILE — any later `gh` call anywhere in
labels.yml would have ridden in free — and it let the merge ship a step that
dies with `command not found` on every sweep on this forge, which #197's bar
does not permit.
The declaration mechanism already existed; a workflow simply could not reach
it. It can: `CEREMONY_FORGE_CLIENT: gh` in the step's env is the same
declaration actions/refs-not-closing carries, and the refusal that a script
gets from forge_preflight is inline here because a workflow has no shell to
call it from. The dispatch now warns by name, cites #205, and exits 0 rather
than reddening every sweep for a known gap.
So the guard needs no exemption list at all. It now requires the pair —
declared AND refusing — and reports a declaration that carries no refusal,
which is a permission slip for `command not found`.
That predicate was wrong on its first write, and its mutation test caught it:
`refuses_when_unavailable` matched the word `forge_preflight` inside
labels.yml's own comment explaining that it has NO forge_preflight to call. A
guard reading prose as evidence is the blind sweep again, in the guard written
to forbid it. Comments are stripped now, as gh_calls already stripped them.
Blocker 4: the nudge strips a trailing slash from the server URL. Reverting the
strip reds two cases.
Blockers 2 and 3 were already fixed in 97e63ac, before either review landed.
test/run.sh 28 files 0 failed under CI's env; shellcheck 0.10.0 (CI's pin),
actionlint, self-ref, marker, vendored and changelog-armed all clean, with
every file tracked this time.
Refs #198
146 lines
7.6 KiB
YAML
146 lines
7.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.6.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 }}
|
||
# This step speaks gh and says so, the same declaration
|
||
# actions/refs-not-closing carries (#198 spec 4). A workflow has no
|
||
# shell to call forge_preflight from, so the refusal is inline
|
||
# below; #205 owns the REST port that removes both.
|
||
CEREMONY_FORGE_CLIENT: gh
|
||
run: |
|
||
# Never `command not found`. On a runner without gh the wake is
|
||
# genuinely lost, and that is worth a warning rather than a failed
|
||
# job: this trigger is the misconfiguration alarm for a CONSUMER's
|
||
# missing sweep caller, and reddening every sweep on a forge whose
|
||
# runner has no gh would drown that signal in a known gap (#205).
|
||
if ! command -v gh >/dev/null 2>&1; then
|
||
echo "::warning::labels: the sweep was NOT woken from this trigger — it dispatches with \`gh\`, which this runner does not carry. #205 ports it to REST. Scheduled and issue-event sweeps are unaffected; only this caller's event-driven wake is lost."
|
||
exit 0
|
||
fi
|
||
gh workflow run "$SWEEP_WORKFLOW" -R "$GITHUB_REPOSITORY" -f bootstrap=no
|