Compare commits
16 commits
0e3d8de2a6
...
84bb1a424d
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
84bb1a424d | ||
|
|
0a54e31972 | ||
|
|
98c8dc2eb4 | ||
|
|
7c755bcd40 | ||
|
|
fa04d67033 | ||
|
|
4198597834 | ||
|
|
3eff28e624 | ||
|
|
1de6b742f8 | ||
|
|
6fe549cdb7 | ||
|
|
c4b49cd8d3 | ||
|
|
8cf3c335df | ||
|
|
0a812c4b19 | ||
|
|
155828a069 | ||
|
|
f8ad0b34c7 | ||
|
|
12887cc9de | ||
|
|
dfafeedeec |
24 changed files with 388 additions and 196 deletions
15
.github/workflows/labels.yml
vendored
15
.github/workflows/labels.yml
vendored
|
|
@ -10,11 +10,14 @@ name: labels
|
||||||
# and reconcile checks out the BASE branch only. Keep it that way.
|
# 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
|
# There is no pull_request_review_target, so a review landing cannot wake this
|
||||||
# workflow directly — and the */15 cron is advisory: GitHub deprioritises
|
# workflow directly — which is why the caller's cron is load-bearing, not a
|
||||||
# short intervals hard enough that a quiet repo goes hours between ticks. The
|
# safety net (#199 relaxed it from */15 to hourly, but did NOT drop it). The
|
||||||
# handoff wakes the sweep itself: the author sets state:needs-human, and the
|
# cron is the sweep's only discovery path for every transition no subscribed
|
||||||
# caller's `labeled` event confirms or corrects that optimistic write within
|
# event carries: a verdict landing, blocker:ci-red set/cleared, a
|
||||||
# seconds. The cron stays as the last resort for a forgotten handoff.
|
# blocker:conflict when another PR merges under this one, and the time-based
|
||||||
|
# stale / 48h claim-reclaim. Where an event IS subscribed the wake is direct —
|
||||||
|
# the handoff sets state:needs-human and the caller's `labeled` event confirms
|
||||||
|
# or corrects that optimistic write within seconds.
|
||||||
#
|
#
|
||||||
# This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not
|
# This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not
|
||||||
# create workflow runs from GITHUB_TOKEN-triggered events. Agent writes use a
|
# create workflow runs from GITHUB_TOKEN-triggered events. Agent writes use a
|
||||||
|
|
@ -25,7 +28,7 @@ on:
|
||||||
env:
|
env:
|
||||||
# A called workflow arrives without its repository. Keep this literal pin
|
# A called workflow arrives without its repository. Keep this literal pin
|
||||||
# aligned with the ceremony release consumed by callers (issue #9 D3).
|
# aligned with the ceremony release consumed by callers (issue #9 D3).
|
||||||
CEREMONY_SELF_REF: "0.3.0"
|
CEREMONY_SELF_REF: "0.4.0"
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
scope:
|
scope:
|
||||||
|
|
|
||||||
2
.github/workflows/release.yml
vendored
2
.github/workflows/release.yml
vendored
|
|
@ -129,7 +129,7 @@ env:
|
||||||
# `ref:` accepts ${{ env }}; `uses:` strings do not — which is why the
|
# `ref:` accepts ${{ env }}; `uses:` strings do not — which is why the
|
||||||
# shared logic arrives as script files via checkout, not as inner `uses:`
|
# shared logic arrives as script files via checkout, not as inner `uses:`
|
||||||
# references.
|
# references.
|
||||||
CEREMONY_SELF_REF: "0.3.0"
|
CEREMONY_SELF_REF: "0.4.0"
|
||||||
VERSION_SOURCE: ${{ inputs.version-source }}
|
VERSION_SOURCE: ${{ inputs.version-source }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
|
|
|
||||||
35
.github/workflows/self-labels.yml
vendored
35
.github/workflows/self-labels.yml
vendored
|
|
@ -5,11 +5,40 @@ name: labels
|
||||||
# unpinned — correct only for the repo that IS the source). Consumers write:
|
# unpinned — correct only for the repo that IS the source). Consumers write:
|
||||||
# uses: heavy-duty/ceremony/.github/workflows/labels.yml@<pinned-tag>
|
# uses: heavy-duty/ceremony/.github/workflows/labels.yml@<pinned-tag>
|
||||||
on:
|
on:
|
||||||
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake
|
# The consumer owns this cadence (#203). Hourly is the recommended default
|
||||||
workflow_dispatch: # bootstraps missing labels on a fresh repo
|
# 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
|
||||||
|
# pull_request_review trigger here), blocker:ci-red set or cleared (no
|
||||||
|
# check_suite/check_run/workflow_run), a blocker:conflict when ANOTHER PR
|
||||||
|
# merges under this one, and the time-based stale / 48h claim-reclaim. The
|
||||||
|
# events below carry the rest in seconds. Hourly trades ≤1h of latency on
|
||||||
|
# those four 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
|
||||||
|
# discovery path. If another engine writes some of those transitions, only
|
||||||
|
# the classes with no other writer bound the cadence; relax it only as that
|
||||||
|
# list shrinks.
|
||||||
|
schedule: [{cron: "0 * * * *"}]
|
||||||
|
# A manual full-board sweep, including taxonomy bootstrap on a fresh repo.
|
||||||
|
workflow_dispatch:
|
||||||
|
# Narrowed (#199) to the actions that carry a queue-state change the hourly
|
||||||
|
# cron cannot wait one cadence for — dropping only labeled/unlabeled/assigned/
|
||||||
|
# unassigned, which feed validation and the 48h claim clock (caught within one
|
||||||
|
# cadence) and were the dominant issues-churn source. Kept: `opened` (the
|
||||||
|
# mint→needs-triage check, issueflow's opened-only path), `closed` (the
|
||||||
|
# blocker-closes→ready self-heal, crew#96/#98), `edited` (a body rewrite of the
|
||||||
|
# `Blocked by #N` declaration the sweep parses — issueflow-reconcile.sh:179),
|
||||||
|
# `reopened` (a closed issue re-entering the queue wearing labels derived when
|
||||||
|
# it closed). The must-fail in #199 is exactly "a queue-state transition waits
|
||||||
|
# on the schedule when an event could have carried it", so edited/reopened stay
|
||||||
|
# on events. The PR handoff wake is pull_request_target:labeled, NOT issues, so
|
||||||
|
# this does not touch the handoff.
|
||||||
issues:
|
issues:
|
||||||
types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, 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
|
||||||
|
# the sweep derives state:* from. labeled/unlabeled are the handoff wake —
|
||||||
|
# the author's optimistic state:needs-human write, confirmed or corrected
|
||||||
|
# here in seconds (#11); synchronize re-derives on every push;
|
||||||
# review_requested/review_request_removed wake the sweep that clears (or
|
# review_requested/review_request_removed wake the sweep that clears (or
|
||||||
# restores) blocker:unrequested — without them the one event that makes
|
# restores) blocker:unrequested — without them the one event that makes
|
||||||
# the label false could not clear it, and a quiet repo wore the red flag
|
# the label false could not clear it, and a quiet repo wore the red flag
|
||||||
|
|
|
||||||
|
|
@ -208,9 +208,9 @@ triage bug, and the move is to say so on the issue, not to guess.
|
||||||
|
|
||||||
## The review round
|
## The review round
|
||||||
|
|
||||||
(If you are reading this as `.ceremony/BUILDER.md` in a governed repo: the
|
(If you are reading this as `.ceremony/BUILDER.md` in a governed repo:
|
||||||
panel roster and any repo-specific flow notes live in that repo's own
|
repo-specific facts such as the panel roster live in that repo's own
|
||||||
CONTRIBUTING; everything below is the shared flow.)
|
CONTRIBUTING; the shared flow lives here and is not restated there.)
|
||||||
|
|
||||||
1. Mark ready-for-review; request **the whole panel**. The panel is the roster
|
1. Mark ready-for-review; request **the whole panel**. The panel is the roster
|
||||||
of the repo the **PR** is in, minus you — never the roster of the repo the
|
of the repo the **PR** is in, minus you — never the roster of the repo the
|
||||||
|
|
|
||||||
96
CHANGELOG.md
96
CHANGELOG.md
|
|
@ -9,6 +9,102 @@ Entries arrive as fragments — one `changelog.d/<issue>.md` per PR, never
|
||||||
an edit to this file — and the release PR assembles them into the next
|
an edit to this file — and the release PR assembles them into the next
|
||||||
section here (`bin/changelog-assemble`, #112).
|
section here (`bin/changelog-assemble`, #112).
|
||||||
|
|
||||||
|
## 0.4.0 — 2026-07-29
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- `changelog.d/shape` — an optional one-line sentinel, `flat` or `grouped`,
|
||||||
|
that pins the fragment set's shape and outranks the newest-published-section
|
||||||
|
inference; absent, the inference binds unchanged (#182).
|
||||||
|
- Add `post-merge` issue state for merged `Refs` work awaiting triage-owned verification.
|
||||||
|
- `changelog_fragment_problem` bounds every entry at 300 normalized
|
||||||
|
characters, red on the PR that writes the fragment; the armed guard and
|
||||||
|
the assembler inherit the one definition (#167).
|
||||||
|
- BUILDER.md and CHANGELOG.md state the bound and the split rule: a long
|
||||||
|
change ships several short entries, never one long one (#167).
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Labels automation docs now make sweep cadence a consumer-owned tradeoff,
|
||||||
|
retain hourly as the engine-less default, and document manual dispatch as
|
||||||
|
the operator's immediate full-board sweep (#203).
|
||||||
|
- `labels` — the reconcile cron relaxes from `*/15` to hourly (#199), cutting a
|
||||||
|
private consumer's schedule-triggered full-board sweeps ~4× at GitHub's
|
||||||
|
1-minute billing floor.
|
||||||
|
- `labels` — the hourly cron is the sweep's only wake for transitions no
|
||||||
|
subscribed event carries — a verdict landing, blocker:ci-red, a
|
||||||
|
blocker:conflict when another PR merges, the time-based stale/reclaim — so it
|
||||||
|
bounds their latency to ≤1h, delaying no event-carried transition (#199).
|
||||||
|
- `labels` — the caller's `issues:` trigger narrows to
|
||||||
|
`[opened, closed, edited, reopened]` (#199), the actions that carry a
|
||||||
|
queue-state change the cron cannot wait a cadence for. The churn/validation
|
||||||
|
actions — labeled/unlabeled/assigned/unassigned — come off; the PR handoff
|
||||||
|
wake is unaffected.
|
||||||
|
- `labels` — each caller trigger now carries a comment saying why it is
|
||||||
|
subscribed, and reconcile keeps `cancel-in-progress: false` (#199) —
|
||||||
|
cancelling a sweep mid-board is the race that guard exists to prevent.
|
||||||
|
- `CONTRIBUTING.md` now points to `BUILDER.md` for the shared PR flow instead
|
||||||
|
of restating doctrine that can drift, while retaining ceremony's roster and
|
||||||
|
other repo-specific facts (#198).
|
||||||
|
- Builder doctrine makes each whole-round reply the durable Round log record
|
||||||
|
mirrored by the engine, leaving handoff as a mechanical facts-only step
|
||||||
|
instead of a newly composed summary (#196).
|
||||||
|
- `FLEET.md` removes its duplicate bench roster, records crew as a general
|
||||||
|
operator-configured tool, and advances its whole-file audit stamp to
|
||||||
|
`crew@eaeb302` with every surviving crew link re-pinned (#193).
|
||||||
|
- `FLEET.md` keeps the registry's authorization rule and its crew#16/crew#66
|
||||||
|
provenance, while replacing duplicated mechanism and path claims with a
|
||||||
|
pinned pointer to crew's registry header (#192).
|
||||||
|
- `BUILDER.md` gates both review-request points on a green check at the
|
||||||
|
head, carries crew#45's argued exception for failures outside the PR,
|
||||||
|
and states the ruled classification: cancelled and stale are not a
|
||||||
|
green head; skipped and neutral are (#189).
|
||||||
|
- `BUILDER.md` documents CI-red recovery in pickup precedence: a red head
|
||||||
|
of your own PR is picked up before claiming another issue, is never a
|
||||||
|
parked claim, and follows crew#17's recovery path (#189).
|
||||||
|
- `FLEET.md` writes the ci-red wake into the duty order between resume
|
||||||
|
and build, now as deployed engine rather than on paper: the
|
||||||
|
reconciliation stamp advances to the crew SHA carrying crew#64 (#189).
|
||||||
|
- `FLEET.md` describes the build wake's check gate as the engine
|
||||||
|
implements it: a green head, or one with no checks configured, opens a
|
||||||
|
round; a red head and an unfinished one are held and reported
|
||||||
|
separately (#189).
|
||||||
|
- `FLEET.md` corrects the attention wake to the crew#66 ruling: the query
|
||||||
|
is cross-repo, the action is registry-bounded, and an out-of-scope
|
||||||
|
demand is reported and escalated to the operator rather than worked. It
|
||||||
|
no longer claims attention is exempt from the registry (#189).
|
||||||
|
- `FLEET.md` distinguishes an attention session that dies before acking,
|
||||||
|
which relaunches, from one that completes without acking, which is a
|
||||||
|
decline a ledger keeps from re-firing (#189).
|
||||||
|
- `BUILDER.md` re-requests by head, not by verdict: a push while
|
||||||
|
answering a round stales every approval, so every panelist is
|
||||||
|
re-requested; only an unchanged head re-requests the non-approvers
|
||||||
|
alone (#190).
|
||||||
|
- FLEET.md's duty-loop mechanism is a pointer to crew's shared engine; the
|
||||||
|
wake lists follow the engine's duty order, the roster keeps the as-built
|
||||||
|
bench beside `fleet.roster`'s target, and the reconciliation stamp names
|
||||||
|
crew@`01fb49c` (#187).
|
||||||
|
- Ceremony's changelog is grouped from this release forward: the pending
|
||||||
|
fragments carry `### ` headings under a `grouped` sentinel (#182).
|
||||||
|
- BUILDER.md: a park declaration stands until its facts change — a
|
||||||
|
nothing-changed resumption posts nothing; only a no-open-PR park owes a
|
||||||
|
refresh, inside the 48-hour reclaim window (#178).
|
||||||
|
- Private-repository label callers document `actions: read` alongside checks and statuses for workflow-run check-rollup nodes (#173).
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
|
||||||
|
- FLEET.md no longer says a review request outside the registry is
|
||||||
|
authorization: `repos.txt` is the scope for the review queue, out-of-scope
|
||||||
|
requests are logged and never acted on, and the attention wake is stated
|
||||||
|
as the one registry-independent exception, by design (#187).
|
||||||
|
- `blocked_reference_records` unions every `Blocked by` clause in the body
|
||||||
|
instead of binding to the first marker occurrence — a repeated declaration
|
||||||
|
no longer promotes on its first sentence alone, and earlier prose that
|
||||||
|
merely mentions being blocked no longer hijacks the parse (#184).
|
||||||
|
- `decide_state()` refuses `state:needs-human` while the hand-set `blocked`
|
||||||
|
label stands — the PR falls to `state:addressing`, exactly parallel to the
|
||||||
|
`needs-ruling` exclusion; never emitted by `blockers()` (#180).
|
||||||
|
|
||||||
## 0.3.0 — 2026-07-24
|
## 0.3.0 — 2026-07-24
|
||||||
|
|
||||||
- Make `changelog-armed` reject fragment shape drift on the PR that introduces it.
|
- Make `changelog-armed` reject fragment shape drift on the PR that introduces it.
|
||||||
|
|
|
||||||
|
|
@ -35,45 +35,9 @@ Who may set which label is [LABELS.md](LABELS.md)'s contract.
|
||||||
|
|
||||||
## The PR flow
|
## The PR flow
|
||||||
|
|
||||||
The same flow the sibling repos run, and the part of this pipeline that is
|
PRs move through review rounds that builders answer whole, and only a human
|
||||||
already proven:
|
merges. [BUILDER.md](BUILDER.md) is the shared flow contract; this file names
|
||||||
|
only ceremony-specific facts such as the roster and code conventions.
|
||||||
1. **One issue, one PR**, opened as a **draft** while building, with
|
|
||||||
`Closes #N` in the body — its exceptions (cross-repo work, a post-merge
|
|
||||||
criterion) live in [BUILDER.md](BUILDER.md). Drafts are invisible to the
|
|
||||||
reviewer panel on purpose. Every behavior change writes one fragment,
|
|
||||||
`changelog.d/<issue>.md` — the exact prose to publish, nothing else
|
|
||||||
(cross-repo work names it `<repo>-<issue>.md`; a grouped repo puts its
|
|
||||||
`### Added` / `### Changed` / `### Fixed` headings inside the fragment).
|
|
||||||
Never edit `CHANGELOG.md` for an entry — the release PR assembles the
|
|
||||||
section from the fragments (#112).
|
|
||||||
|
|
||||||
The sole exception is the release PR: it writes no fragment. It consumes
|
|
||||||
the directory and stamps the section, so a fragment it created would be
|
|
||||||
absent from
|
|
||||||
[`changelog-assembled`](https://github.com/heavy-duty/ceremony/blob/a602fd0/actions/changelog-assembled/changelog-assembled.sh)'s
|
|
||||||
merge-base replay if consumed, or refused by
|
|
||||||
[`changelog-armed`](https://github.com/heavy-duty/ceremony/blob/a602fd0/actions/changelog-armed/changelog-armed.sh)
|
|
||||||
if left to survive into the next release. A change that must ship inside
|
|
||||||
the release PR therefore ships without an entry. If it can wait and wants
|
|
||||||
an entry, land it as an ordinary PR before the release PR, then rebase and
|
|
||||||
re-assemble the release.
|
|
||||||
2. **When it's ready**: mark ready-for-review and request the whole panel.
|
|
||||||
3. **Rounds are answered whole.** Wait until every reviewer has a verdict in,
|
|
||||||
then answer the entire round in a **single reply**, push the fixes, and
|
|
||||||
re-request the reviewers that didn't approve. Prefer verification over
|
|
||||||
argument: a test settles what a comment thread can't.
|
|
||||||
4. **Reviews end in a verdict** — approve or request-changes, never a bare
|
|
||||||
comment. The verdict carries blockingness only; the body carries the
|
|
||||||
feedback. ([REVIEWER.md](REVIEWER.md) for why a comment-only review stalls
|
|
||||||
the machine.)
|
|
||||||
5. **Handoff**: when the round passes — every panel verdict is an approval of
|
|
||||||
the current head and no `blocker:*` label stands — the engine requests the
|
|
||||||
human's review, sets `state:needs-human`, and posts the handoff facts
|
|
||||||
(approvals at the current head, head SHA, and Round-log pointer) on the
|
|
||||||
author's behalf. The label write is optimistic; the reconciler validates
|
|
||||||
it within seconds.
|
|
||||||
6. **A human merges.** Nothing else merges.
|
|
||||||
|
|
||||||
### Roster
|
### Roster
|
||||||
|
|
||||||
|
|
|
||||||
14
LABELS.md
14
LABELS.md
|
|
@ -3,7 +3,8 @@
|
||||||
The taxonomy shared across the heavy-duty repos. Only the `scope:` set
|
The taxonomy shared across the heavy-duty repos. Only the `scope:` set
|
||||||
differs per repo (each repo's `.github/labels.conf` names its actual
|
differs per repo (each repo's `.github/labels.conf` names its actual
|
||||||
surfaces); everything else below is core and identical everywhere, created by
|
surfaces); everything else below is core and identical everywhere, created by
|
||||||
the labels workflow's bootstrap dispatch (issue #10).
|
the labels workflow's dispatch (which is also the operator's manual
|
||||||
|
full-board reconcile sweep; issue #10).
|
||||||
|
|
||||||
Two state machines share the taxonomy: the **PR machine** (proven in
|
Two state machines share the taxonomy: the **PR machine** (proven in
|
||||||
box/rig/cast, reconciled by machinery) and the **issue flow** (the
|
box/rig/cast, reconciled by machinery) and the **issue flow** (the
|
||||||
|
|
@ -211,10 +212,13 @@ on a PR would say the same thing twice and drift.
|
||||||
|
|
||||||
## Maintenance
|
## Maintenance
|
||||||
|
|
||||||
The labels workflow (issue #10) recomputes PR state statelessly on PR events
|
The labels workflow (issue #10) recomputes PR state statelessly on subscribed
|
||||||
plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on
|
events plus a consumer-owned scheduled discovery sweep. Hourly is the
|
||||||
manual dispatch. The sweep warns when the core taxonomy declares a label the
|
recommended default when no other engine drives board state; relax it only as
|
||||||
repository lacks. The same workflow reconciles issue-flow labels on issue
|
the transition classes with no other writer shrink. Manual dispatch both
|
||||||
|
bootstraps this taxonomy idempotently and runs the operator's on-demand
|
||||||
|
full-board reconcile. The sweep warns when the core taxonomy declares a label
|
||||||
|
the repository lacks. The same workflow reconciles issue-flow labels on issue
|
||||||
events and during the scheduled sweep. Default GitHub labels (`duplicate`,
|
events and during the scheduled sweep. Default GitHub labels (`duplicate`,
|
||||||
`invalid`, `question`, `wontfix`, `help wanted`, `good first issue`) are
|
`invalid`, `question`, `wontfix`, `help wanted`, `good first issue`) are
|
||||||
deleted at bootstrap — a `question` is a discussion, not an issue.
|
deleted at bootstrap — a `question` is a discussion, not an issue.
|
||||||
|
|
|
||||||
2
VERSION
2
VERSION
|
|
@ -1 +1 @@
|
||||||
0.3.1-dev
|
0.4.1-dev
|
||||||
|
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
### Added
|
|
||||||
|
|
||||||
- `changelog_fragment_problem` bounds every entry at 300 normalized
|
|
||||||
characters, red on the PR that writes the fragment; the armed guard and
|
|
||||||
the assembler inherit the one definition (#167).
|
|
||||||
- BUILDER.md and CHANGELOG.md state the bound and the split rule: a long
|
|
||||||
change ships several short entries, never one long one (#167).
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Private-repository label callers document `actions: read` alongside checks and statuses for workflow-run check-rollup nodes (#173).
|
|
||||||
|
|
@ -1,3 +0,0 @@
|
||||||
### Added
|
|
||||||
|
|
||||||
- Add `post-merge` issue state for merged `Refs` work awaiting triage-owned verification.
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
### Changed
|
|
||||||
|
|
||||||
- BUILDER.md: a park declaration stands until its facts change — a
|
|
||||||
nothing-changed resumption posts nothing; only a no-open-PR park owes a
|
|
||||||
refresh, inside the 48-hour reclaim window (#178).
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `decide_state()` refuses `state:needs-human` while the hand-set `blocked`
|
|
||||||
label stands — the PR falls to `state:addressing`, exactly parallel to the
|
|
||||||
`needs-ruling` exclusion; never emitted by `blockers()` (#180).
|
|
||||||
|
|
@ -1,10 +0,0 @@
|
||||||
### Added
|
|
||||||
|
|
||||||
- `changelog.d/shape` — an optional one-line sentinel, `flat` or `grouped`,
|
|
||||||
that pins the fragment set's shape and outranks the newest-published-section
|
|
||||||
inference; absent, the inference binds unchanged (#182).
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Ceremony's changelog is grouped from this release forward: the pending
|
|
||||||
fragments carry `### ` headings under a `grouped` sentinel (#182).
|
|
||||||
|
|
@ -1,6 +0,0 @@
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- `blocked_reference_records` unions every `Blocked by` clause in the body
|
|
||||||
instead of binding to the first marker occurrence — a repeated declaration
|
|
||||||
no longer promotes on its first sentence alone, and earlier prose that
|
|
||||||
merely mentions being blocked no longer hijacks the parse (#184).
|
|
||||||
|
|
@ -1,13 +0,0 @@
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- FLEET.md no longer says a review request outside the registry is
|
|
||||||
authorization: `repos.txt` is the scope for the review queue, out-of-scope
|
|
||||||
requests are logged and never acted on, and the attention wake is stated
|
|
||||||
as the one registry-independent exception, by design (#187).
|
|
||||||
|
|
||||||
### Changed
|
|
||||||
|
|
||||||
- FLEET.md's duty-loop mechanism is a pointer to crew's shared engine; the
|
|
||||||
wake lists follow the engine's duty order, the roster keeps the as-built
|
|
||||||
bench beside `fleet.roster`'s target, and the reconciliation stamp names
|
|
||||||
crew@`01fb49c` (#187).
|
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
### Changed
|
|
||||||
|
|
||||||
- `BUILDER.md` gates both review-request points on a green check at the
|
|
||||||
head, carries crew#45's argued exception for failures outside the PR,
|
|
||||||
and states the ruled classification: cancelled and stale are not a
|
|
||||||
green head; skipped and neutral are (#189).
|
|
||||||
- `BUILDER.md` documents CI-red recovery in pickup precedence: a red head
|
|
||||||
of your own PR is picked up before claiming another issue, is never a
|
|
||||||
parked claim, and follows crew#17's recovery path (#189).
|
|
||||||
- `FLEET.md` writes the ci-red wake into the duty order between resume
|
|
||||||
and build, now as deployed engine rather than on paper: the
|
|
||||||
reconciliation stamp advances to the crew SHA carrying crew#64 (#189).
|
|
||||||
- `FLEET.md` describes the build wake's check gate as the engine
|
|
||||||
implements it: a green head, or one with no checks configured, opens a
|
|
||||||
round; a red head and an unfinished one are held and reported
|
|
||||||
separately (#189).
|
|
||||||
- `FLEET.md` corrects the attention wake to the crew#66 ruling: the query
|
|
||||||
is cross-repo, the action is registry-bounded, and an out-of-scope
|
|
||||||
demand is reported and escalated to the operator rather than worked. It
|
|
||||||
no longer claims attention is exempt from the registry (#189).
|
|
||||||
- `FLEET.md` distinguishes an attention session that dies before acking,
|
|
||||||
which relaunches, from one that completes without acking, which is a
|
|
||||||
decline a ledger keeps from re-firing (#189).
|
|
||||||
- `BUILDER.md` re-requests by head, not by verdict: a push while
|
|
||||||
answering a round stales every approval, so every panelist is
|
|
||||||
re-requested; only an unchanged head re-requests the non-approvers
|
|
||||||
alone (#190).
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
### Changed
|
|
||||||
|
|
||||||
- `FLEET.md` keeps the registry's authorization rule and its crew#16/crew#66
|
|
||||||
provenance, while replacing duplicated mechanism and path claims with a
|
|
||||||
pinned pointer to crew's registry header (#192).
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
### Changed
|
|
||||||
|
|
||||||
- `FLEET.md` removes its duplicate bench roster, records crew as a general
|
|
||||||
operator-configured tool, and advances its whole-file audit stamp to
|
|
||||||
`crew@eaeb302` with every surviving crew link re-pinned (#193).
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
### Changed
|
|
||||||
|
|
||||||
- Builder doctrine makes each whole-round reply the durable Round log record
|
|
||||||
mirrored by the engine, leaving handoff as a mechanical facts-only step
|
|
||||||
instead of a newly composed summary (#196).
|
|
||||||
|
|
@ -122,7 +122,8 @@ the machinery at all:
|
||||||
[Labels automation](#labels-automation), plus `.github/labels.conf`
|
[Labels automation](#labels-automation), plus `.github/labels.conf`
|
||||||
(panel + the repo's `scope:*` rows) and `.github/labeler.yml` (the
|
(panel + the repo's `scope:*` rows) and `.github/labeler.yml` (the
|
||||||
path→scope globs). Run `workflow_dispatch` once — **this bootstraps
|
path→scope globs). Run `workflow_dispatch` once — **this bootstraps
|
||||||
the taxonomy, `release` label included**.
|
the taxonomy, `release` label included** — and use it again whenever an
|
||||||
|
operator needs a full-board sweep immediately.
|
||||||
7. **The artifact hook** (optional): `.github/actions/release-artifact/`
|
7. **The artifact hook** (optional): `.github/actions/release-artifact/`
|
||||||
per [The artifact hook](#the-artifact-hook). No hook → the source
|
per [The artifact hook](#the-artifact-hook). No hook → the source
|
||||||
tarball is the package.
|
tarball is the package.
|
||||||
|
|
@ -296,20 +297,39 @@ The complete caller is:
|
||||||
```yaml
|
```yaml
|
||||||
name: labels
|
name: labels
|
||||||
on:
|
on:
|
||||||
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake
|
# The consumer owns this cadence (#203). Hourly is the recommended default
|
||||||
workflow_dispatch: # bootstraps missing labels on a fresh repo
|
# 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
|
||||||
|
# pull_request_review trigger), blocker:ci-red set/cleared, blocker:conflict
|
||||||
|
# when another PR merges under this one, and time-based stale / 48h
|
||||||
|
# claim-reclaim. Events below carry the rest in seconds. Hourly trades ≤1h of
|
||||||
|
# latency on those four while cutting nominal scheduled sweeps from four an
|
||||||
|
# hour to one at GitHub's 1-minute floor. Do not delete the cron: it is their
|
||||||
|
# discovery path. If another engine writes some of those transitions, only
|
||||||
|
# the classes with no other writer bound the cadence; relax it only as that
|
||||||
|
# list shrinks.
|
||||||
|
schedule: [{cron: "0 * * * *"}]
|
||||||
|
# A manual full-board sweep, including taxonomy bootstrap on a fresh repo.
|
||||||
|
workflow_dispatch:
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
# review_requested/review_request_removed are unreleased — not in 0.2.0;
|
# Fork PRs; these carry the head/draft/review facts state:* derives from.
|
||||||
# add them with the pin bump to the first tag carrying ceremony#137. They
|
# labeled/unlabeled are the handoff wake (state:needs-human confirmed here);
|
||||||
# wake the sweep that clears blocker:unrequested when the panel is asked.
|
# synchronize re-derives on every push. review_requested/review_request_removed
|
||||||
|
# (shipped in 0.3.0, ceremony#137) wake the sweep that clears
|
||||||
|
# blocker:unrequested 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.
|
||||||
issues:
|
issues:
|
||||||
# edited/reopened are unreleased — not in 0.2.0; add them with the pin
|
# Narrowed (#199) to the actions carrying a queue-state change the hourly
|
||||||
# bump to the first tag carrying ceremony#144, the same bump as the
|
# cron cannot wait one cadence for: opened → the mint→needs-triage check,
|
||||||
# pull_request_target additions above.
|
# closed → the blocker-closes→ready self-heal, edited → a body rewrite of the
|
||||||
types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]
|
# `Blocked by #N` declaration the sweep parses, reopened → a closed issue
|
||||||
|
# re-entering the queue. Dropped: labeled/unlabeled/assigned/unassigned —
|
||||||
|
# validation + the 48h claim clock, caught within one cadence, and
|
||||||
|
# labeled/unlabeled were the issues-churn source. The handoff wake is
|
||||||
|
# pull_request_target:labeled, not issues, so this leaves it intact.
|
||||||
|
types: [opened, closed, edited, reopened]
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
checks: read # mergeability/check-rollup read for PR state
|
checks: read # mergeability/check-rollup read for PR state
|
||||||
|
|
@ -330,23 +350,32 @@ needs all three explicit reads above; without them the failure appears as an emp
|
||||||
The `issues:` trigger is available at `0.2.0` and later — `0.2.0` is the
|
The `issues:` trigger is available at `0.2.0` and later — `0.2.0` is the
|
||||||
first tag carrying ceremony#32. A consumer pinned to `0.1.0` omits it. Adopt
|
first tag carrying ceremony#32. A consumer pinned to `0.1.0` omits it. Adopt
|
||||||
it only by bumping every ceremony reference to `0.2.0` or later; never mix
|
it only by bumping every ceremony reference to `0.2.0` or later; never mix
|
||||||
refs to adopt it early. At `0.2.0` the type list stops at `closed`: `edited`
|
refs to adopt it early. The type list has grown then narrowed across tags:
|
||||||
and `reopened` are newer, part of the pending stub edit below.
|
`0.2.0` (ceremony#32) shipped `[opened, labeled, unlabeled, assigned,
|
||||||
|
unassigned, closed]`; `0.3.0` (ceremony#144) added `edited` and `reopened`;
|
||||||
|
ceremony#199 narrows it to `[opened, closed, edited, reopened]` and relaxes the
|
||||||
|
cron to hourly, so a consumer picks up the smaller trigger surface at the pin
|
||||||
|
bump to the first tag carrying ceremony#199. The narrowing drops
|
||||||
|
`labeled`/`unlabeled`/`assigned`/`unassigned` — validation and the 48h claim
|
||||||
|
clock, which the hourly cron catches within one cadence, and `labeled`/
|
||||||
|
`unlabeled` were the issues-churn source — while **keeping** #144's `edited`/
|
||||||
|
`reopened`: those carry a queue-state change an event uniquely carries (a body
|
||||||
|
rewrite of `Blocked by #N`, and a closed issue re-entering the queue), so the
|
||||||
|
must-fail in ceremony#199 keeps them on events. `opened` drives the
|
||||||
|
mint→`needs-triage` check and `closed` the blocker-closes→`ready` self-heal;
|
||||||
|
the stub and ceremony's own caller stay byte-for-byte identical, the parity
|
||||||
|
#144 established.
|
||||||
|
|
||||||
`pull_request_target` is intentional: fork PRs need the base repository's
|
`pull_request_target` is intentional: fork PRs need the base repository's
|
||||||
token to write labels. The reusable workflow executes no PR code. It checks
|
token to write labels. The reusable workflow executes no PR code. It checks
|
||||||
out only the consumer's base branch and the pinned ceremony implementation.
|
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. The pending edits are `review_requested` and
|
a bare pin bump. `review_requested` and `review_request_removed` on
|
||||||
`review_request_removed` on `pull_request_target:` (#137) — the wake that
|
`pull_request_target:` shipped in `0.3.0` (ceremony#137) — the wake that
|
||||||
clears `blocker:unrequested` the moment the panel is asked, without which a
|
clears `blocker:unrequested` the moment the panel is asked, without which a
|
||||||
quiet repo wears that flag until the advisory cron — and `edited` and
|
quiet repo wears that flag until the backstop cron; a consumer picks them up
|
||||||
`reopened` on `issues:` (#144): the wakes for a body edit rewriting the
|
by pinning `0.3.0` or later, never through mixed refs.
|
||||||
`Blocked by #N` declaration the reconcile sweep parses, and for a closed
|
|
||||||
issue re-entering the queue wearing labels derived when it closed. Make both
|
|
||||||
edits together, with the pin bump to the first tag carrying ceremony#137 and
|
|
||||||
ceremony#144 — never before it and 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, and then zero or more scope rows:
|
`triage-actors` setting, and then zero or more scope rows:
|
||||||
|
|
@ -375,11 +404,20 @@ Core state, blocker, work-queue, and release labels come from ceremony. Scope
|
||||||
rows remain consumer-owned because paths and surfaces differ by repository.
|
rows remain consumer-owned because paths and surfaces differ by repository.
|
||||||
|
|
||||||
After adding the caller and configuration, run `workflow_dispatch` once to
|
After adding the caller and configuration, run `workflow_dispatch` once to
|
||||||
bootstrap labels on a fresh repository. Scheduled and PR-triggered runs only
|
bootstrap labels on a fresh repository. It is also the operator's general
|
||||||
reconcile; they do not repeatedly upsert the taxonomy. When a ceremony pin
|
manual full-board sweep — the answer when the board looks wrong now rather
|
||||||
bump adds a core label, bump the pin first and then re-dispatch
|
than after the next scheduled cadence:
|
||||||
`workflow_dispatch`; the scheduled sweep warns when the pinned taxonomy
|
|
||||||
declares a core label the repository lacks.
|
```sh
|
||||||
|
gh workflow run labels.yml -R <owner>/<repo>
|
||||||
|
```
|
||||||
|
|
||||||
|
Ceremony dogfoods the caller under the filename `self-labels.yml`, so the
|
||||||
|
equivalent command in this repository substitutes that filename. Scheduled
|
||||||
|
and PR-triggered runs only reconcile; they do not repeatedly upsert the
|
||||||
|
taxonomy. When a ceremony pin bump adds a core label, bump the pin first and
|
||||||
|
then re-dispatch `workflow_dispatch`; the scheduled sweep warns when the
|
||||||
|
pinned taxonomy declares a core label the repository lacks.
|
||||||
|
|
||||||
## Doctrine mirror
|
## Doctrine mirror
|
||||||
|
|
||||||
|
|
|
||||||
50
drills/0.4.0.md
Normal file
50
drills/0.4.0.md
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
# 0.4.0 — drill record
|
||||||
|
|
||||||
|
Run 2026-07-29 by `codex-bot-andresmgsl` against release PR #207 head
|
||||||
|
`7c755bcd402ba7f9a38ecd406a025c149c77aa57`.
|
||||||
|
|
||||||
|
Where: disposable private repo
|
||||||
|
`codex-bot-andresmgsl/ceremony-drill-0.4.0`, carrying the
|
||||||
|
`docs/CONSUMERS.md` release caller and a fragment-mode fixture armed at
|
||||||
|
`0.4.0-dev`. The fixture had `changelog.d/README.md`, one release fragment,
|
||||||
|
and a non-blank drill record. The repository is archived, pending the
|
||||||
|
operator's delete.
|
||||||
|
|
||||||
|
## Candidate-ref deviation
|
||||||
|
|
||||||
|
The pure consumer path cannot resolve `CEREMONY_SELF_REF: "0.4.0"` before
|
||||||
|
the candidate creates that tag. No `0.4.0` branch was created in
|
||||||
|
`heavy-duty/ceremony`. The scratch caller instead used
|
||||||
|
`codex-bot-andresmgsl/ceremony@drill/0.4.0`, whose parent is PR #207 head
|
||||||
|
`7c755bcd402ba7f9a38ecd406a025c149c77aa57` and whose only additional
|
||||||
|
commit rewrites both `CEREMONY_SELF_REF` carriers to that same canonical
|
||||||
|
candidate SHA. All runtime machinery was therefore fetched from the 0.4.0
|
||||||
|
candidate tree.
|
||||||
|
|
||||||
|
## Probes
|
||||||
|
|
||||||
|
| # | probe | run | result |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 1 | merge-door ceremony | 30445585532 (attempt 1) | ✅ one `0.4.0` release; tag equals merge commit; main re-armed to `0.4.1-dev` with only `changelog.d/README.md` |
|
||||||
|
| 2 | mislabeled ordinary PR | 30445473977 | ✅ green NOTICE no-op; no tag or release |
|
||||||
|
| 3 | bare-version PR without `release` | 30445513860 | ✅ refused at decide; no tag or release |
|
||||||
|
| 4 | re-run completed ceremony | 30445585532 (attempt 2) | ✅ refused because the tag/release already existed; release count stayed one |
|
||||||
|
| 5 | manual matching tag | 30445658952 | ✅ `0.5.0` published with its changelog section; main untouched |
|
||||||
|
| 6 | mismatched tag | 30445684268 | ✅ refused before publication; no `9.9.9` release, and the probe tag was removed afterward |
|
||||||
|
|
||||||
|
The merge-door `0.4.0` tag and PR #3 merge commit were both
|
||||||
|
`2b2d592ffcc5c376e55bd5fcf2dd5ffbdd692d64`. Its release body was exactly:
|
||||||
|
|
||||||
|
```text
|
||||||
|
- Fragment mode is exercised by the 0.4.0 drill.
|
||||||
|
```
|
||||||
|
|
||||||
|
## Setup
|
||||||
|
|
||||||
|
The armed fixture was committed before the caller so the first workflow run
|
||||||
|
had a real parent version to inspect. Installing the caller then produced
|
||||||
|
green baseline run 30445432039.
|
||||||
|
|
||||||
|
After the unlabeled bare-version refusal, its merge commit was reverted to
|
||||||
|
restore the armed fixture. That setup correction produced green no-op run
|
||||||
|
30445544068 before the merge-door probe began.
|
||||||
98
test/labels-triggers.test.sh
Normal file
98
test/labels-triggers.test.sh
Normal file
|
|
@ -0,0 +1,98 @@
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -u
|
||||||
|
|
||||||
|
# The labels TRIGGER SURFACE is a cost lever (#199): a full-board sweep is
|
||||||
|
# billed a 1-minute minimum every time a trigger fires, so how OFTEN it fires
|
||||||
|
# is what exhausted the fleet's shared Actions allotment. These assertions
|
||||||
|
# pin the reductions #199 made and the guard it must not trade away — none of
|
||||||
|
# them touch the reconciler's LOGIC, which its own fixtures cover.
|
||||||
|
|
||||||
|
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
|
# shellcheck source=test/harness.sh
|
||||||
|
source "$ROOT/test/harness.sh"
|
||||||
|
|
||||||
|
REUSABLE="$ROOT/.github/workflows/labels.yml"
|
||||||
|
SELF="$ROOT/.github/workflows/self-labels.yml"
|
||||||
|
STUB="$ROOT/docs/CONSUMERS.md" # the published caller stub, a fenced yaml block
|
||||||
|
|
||||||
|
# The `cancel-in-progress:` value of a named top-level job, read from the first
|
||||||
|
# such line inside that job's block. Job keys sit at two-space indent.
|
||||||
|
job_cancel_in_progress() { # $1 = file, $2 = job name
|
||||||
|
awk -v job="^ $2:\$" '
|
||||||
|
$0 ~ job { f = 1; next }
|
||||||
|
f && /^ [a-z]/ { exit } # next job — stop before leaking into it
|
||||||
|
f && /cancel-in-progress:/ { sub(/.*cancel-in-progress:[[:space:]]*/, ""); print; exit }
|
||||||
|
' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# The `types:` list of a trigger key (issues:, pull_request_target:), read from
|
||||||
|
# the first `types:` line after the bare key. The key is bare (nothing after
|
||||||
|
# the colon) so it never collides with `issues: write` in the permissions block.
|
||||||
|
trigger_types() { # $1 = file, $2 = trigger key
|
||||||
|
awk -v key="^ $2:\$" '
|
||||||
|
$0 ~ key { f = 1; next }
|
||||||
|
f && /^ types:/ { sub(/^ types:[[:space:]]*/, ""); print; exit }
|
||||||
|
f && /^ [a-z]/ { exit }
|
||||||
|
' "$1"
|
||||||
|
}
|
||||||
|
|
||||||
|
# ---- the guard the cost fix must never trade away (#199 test plan must-fail) --
|
||||||
|
# cancel-in-progress: true on reconcile kills a sweep mid-board, the exact race
|
||||||
|
# the shared concurrency group exists to prevent. It WOULD cut run count — by
|
||||||
|
# trading correctness for minutes — so it stays false, forever.
|
||||||
|
check "reconcile serializes, never cancels mid-board" 0 "false" \
|
||||||
|
job_cancel_in_progress "$REUSABLE" reconcile
|
||||||
|
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
|
||||||
|
check "reconcile is never cancel-in-progress: true" 1 "" \
|
||||||
|
bash -c 'job_cancel_in_progress() {
|
||||||
|
awk -v job="^ reconcile:\$" "\$0 ~ job{f=1;next} f&&/^ [a-z]/{exit} f&&/cancel-in-progress:/{sub(/.*cancel-in-progress:[[:space:]]*/,\"\");print;exit}" "$1"
|
||||||
|
}; [ "$(job_cancel_in_progress "$1")" = true ]' _ "$REUSABLE"
|
||||||
|
# scope MAY cancel — it is per-PR and additive, so a superseded run is waste,
|
||||||
|
# not a lost sweep. This asserts the must-fail above is scoped to reconcile.
|
||||||
|
check "scope stays cancel-in-progress: true (per-PR, additive)" 0 "true" \
|
||||||
|
job_cancel_in_progress "$REUSABLE" scope
|
||||||
|
|
||||||
|
# ---- the cron is a backstop, relaxed to hourly (#199 candidate 1) -----------
|
||||||
|
# Scope the */15 assertion to the cron LINE — the prose comments cite */15 by
|
||||||
|
# name to explain the change, and must not re-red their own documentation.
|
||||||
|
check "self caller cron is hourly" 0 '0 * * * *' grep -F 'cron:' "$SELF"
|
||||||
|
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
|
||||||
|
check "self caller cron line no longer fires */15" 1 "" \
|
||||||
|
bash -c 'grep -F "cron:" "$1" | grep -qF "*/15"' _ "$SELF"
|
||||||
|
check "stub cron is hourly" 0 '0 * * * *' grep -F 'cron:' "$STUB"
|
||||||
|
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
|
||||||
|
check "stub cron line no longer fires */15" 1 "" \
|
||||||
|
bash -c 'grep -F "cron:" "$1" | grep -qF "*/15"' _ "$STUB"
|
||||||
|
|
||||||
|
# ---- issues: is narrowed to the queue-state-changing actions (#199) ----------
|
||||||
|
# Kept because each carries a queue-state change an event uniquely carries, so
|
||||||
|
# dropping it would trip #199's must-fail (a transition waiting on the schedule
|
||||||
|
# when an event could have carried it): opened → mint→needs-triage; closed →
|
||||||
|
# blocker-closes→ready self-heal; edited → a body rewrite of the `Blocked by #N`
|
||||||
|
# declaration the sweep parses; reopened → a closed issue re-entering the queue.
|
||||||
|
# (labels.test.sh owns the exact-list and caller<->stub parity assertions.)
|
||||||
|
for keep in opened closed edited reopened; do
|
||||||
|
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
|
||||||
|
check "self caller issues surface keeps '$keep'" 0 "" \
|
||||||
|
bash -c 'trigger_types() {
|
||||||
|
awk -v key="^ issues:\$" "\$0 ~ key{f=1;next} f&&/^ types:/{sub(/^ types:[[:space:]]*/,\"\");print;exit} f&&/^ [a-z]/{exit}" "$1"
|
||||||
|
}; trigger_types "$1" | grep -qw "$2"' _ "$SELF" "$keep"
|
||||||
|
done
|
||||||
|
# The churn actions must not reappear on the issues surface without a fresh why.
|
||||||
|
# labeled/unlabeled were the dominant issues-churn source; assigned/unassigned
|
||||||
|
# only feed validation and the 48h claim clock, caught within one cadence.
|
||||||
|
for churn in labeled unlabeled assigned unassigned; do
|
||||||
|
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
|
||||||
|
check "self caller issues surface drops '$churn'" 1 "" \
|
||||||
|
bash -c 'trigger_types() {
|
||||||
|
awk -v key="^ issues:\$" "\$0 ~ key{f=1;next} f&&/^ types:/{sub(/^ types:[[:space:]]*/,\"\");print;exit} f&&/^ [a-z]/{exit}" "$1"
|
||||||
|
}; trigger_types "$1" | grep -qw "$2"' _ "$SELF" "$churn"
|
||||||
|
done
|
||||||
|
|
||||||
|
# ---- the PR handoff wake is NOT collateral of the issues narrowing ----------
|
||||||
|
# The handoff (state:needs-human, confirmed by the caller's labeled event) rides
|
||||||
|
# pull_request_target, not issues. A future edit that strips it there re-reds.
|
||||||
|
check "pull_request_target keeps the labeled handoff wake" 0 "labeled" \
|
||||||
|
trigger_types "$SELF" pull_request_target
|
||||||
|
|
||||||
|
summary
|
||||||
|
|
@ -69,15 +69,16 @@ check "LABELS.md enumerates no repo's scope labels" 1 "0" \
|
||||||
# same lists. review_requested/review_request_removed are the wake that
|
# same lists. review_requested/review_request_removed are the wake that
|
||||||
# clears blocker:unrequested — the label sat false for as long as a quiet
|
# clears blocker:unrequested — the label sat false for as long as a quiet
|
||||||
# repo stayed quiet because the one event that falsifies it was never
|
# repo stayed quiet because the one event that falsifies it was never
|
||||||
# listed (#137). edited/reopened are the wakes for the two events that
|
# listed (#137). The issues list narrowed to [opened, closed, edited, reopened]
|
||||||
# falsify issue labels silently — an edited body rewrites the `Blocked by
|
# (#199): each carries a queue-state change the hourly cron cannot wait one
|
||||||
# #N` declaration the reconcile sweep parses, and a reopened issue
|
# cadence for — opened drives mint→needs-triage, closed the blocker-closes→ready
|
||||||
# re-enters the queue wearing labels derived at close; PR #32 widened the
|
# self-heal, edited a body rewrite of the `Blocked by #N` line the sweep parses,
|
||||||
# caller by both and the stub never followed (#144). The stub is prose, so
|
# reopened a closed issue re-entering the queue — while the churn/validation
|
||||||
# nothing but these rows keeps the lists from drifting: a type in one file
|
# actions (labeled/unlabeled/assigned/unassigned) came off. The stub is prose, so nothing but these rows
|
||||||
# only is a wake that fires at home and nowhere in the fleet, or the
|
# keeps the lists from drifting: a type in one file only is a wake that fires
|
||||||
# reverse. The NF guard keeps `issues: write` under permissions: from
|
# at home and nowhere in the fleet, or the reverse — the drift #144 caught.
|
||||||
# matching the issues: trigger key.
|
# The NF guard keeps `issues: write` under permissions: from matching the
|
||||||
|
# issues: trigger key.
|
||||||
event_types() { # $1 = file, $2 = trigger key → that trigger's types line, unindented
|
event_types() { # $1 = file, $2 = trigger key → that trigger's types line, unindented
|
||||||
awk -v key="$2:" '$1 == key && NF == 1 {f=1; next} f && /types: /{sub(/^ */,""); print; exit}' "$1"
|
awk -v key="$2:" '$1 == key && NF == 1 {f=1; next} f && /types: /{sub(/^ */,""); print; exit}' "$1"
|
||||||
}
|
}
|
||||||
|
|
@ -88,17 +89,20 @@ types_in_sync() { # $1 = trigger key, $2 = caller, $3 = stub → 0 when both lis
|
||||||
}
|
}
|
||||||
CALLER="$ROOT/.github/workflows/self-labels.yml"
|
CALLER="$ROOT/.github/workflows/self-labels.yml"
|
||||||
STUB="$ROOT/docs/CONSUMERS.md"
|
STUB="$ROOT/docs/CONSUMERS.md"
|
||||||
|
# event_types anchors on the bare trigger key (NF == 1), so it reads the real
|
||||||
|
# types line even though the #199 comments name pull_request_target: and
|
||||||
|
# issues: in prose above the keys — an inline /pull_request_target:/ scan would
|
||||||
|
# latch onto the first mention and read the wrong list.
|
||||||
|
pr_has_both_review_wakes() {
|
||||||
|
event_types "$CALLER" pull_request_target | grep -F review_requested | grep -qF review_request_removed
|
||||||
|
}
|
||||||
check "caller and stub pull_request_target lists are identical" 0 "" \
|
check "caller and stub pull_request_target lists are identical" 0 "" \
|
||||||
types_in_sync pull_request_target "$CALLER" "$STUB"
|
types_in_sync pull_request_target "$CALLER" "$STUB"
|
||||||
# shellcheck disable=SC2016 # expansion belongs to the nested bash
|
check "the caller lists both review-request wakes" 0 "" pr_has_both_review_wakes
|
||||||
check "the caller lists both review-request wakes" 0 "" bash -c \
|
|
||||||
'awk "/pull_request_target:/{f=1; next} f && /types: /{print; exit}" "$1" |
|
|
||||||
grep -F review_requested | grep -qF review_request_removed' _ "$CALLER"
|
|
||||||
check "caller and stub issues lists are identical" 0 "" \
|
check "caller and stub issues lists are identical" 0 "" \
|
||||||
types_in_sync issues "$CALLER" "$STUB"
|
types_in_sync issues "$CALLER" "$STUB"
|
||||||
check "the caller still lists all eight issue types" 0 \
|
check "the caller lists exactly the queue-state-changing issue types" 0 \
|
||||||
"types: [opened, edited, assigned, unassigned, labeled, unlabeled, closed, reopened]" \
|
"types: [opened, closed, edited, reopened]" event_types "$CALLER" issues
|
||||||
event_types "$CALLER" issues
|
|
||||||
# the failing cases: drop a type from either file, or reorder one list only,
|
# the failing cases: drop a type from either file, or reorder one list only,
|
||||||
# and the identity rows above go red — exercised here on mutated copies
|
# and the identity rows above go red — exercised here on mutated copies
|
||||||
mut_caller="$TMP/mut-caller.yml" mut_stub="$TMP/mut-stub.md"
|
mut_caller="$TMP/mut-caller.yml" mut_stub="$TMP/mut-stub.md"
|
||||||
|
|
@ -112,13 +116,13 @@ sed 's/review_requested, review_request_removed/review_request_removed, review_r
|
||||||
"$STUB" >"$mut_stub"
|
"$STUB" >"$mut_stub"
|
||||||
check "a reorder in one list only goes red" 1 "" \
|
check "a reorder in one list only goes red" 1 "" \
|
||||||
types_in_sync pull_request_target "$CALLER" "$mut_stub"
|
types_in_sync pull_request_target "$CALLER" "$mut_stub"
|
||||||
sed 's/, edited//' "$CALLER" >"$mut_caller"
|
sed 's/, closed//' "$CALLER" >"$mut_caller"
|
||||||
check "an issue type dropped from the caller goes red" 1 "" \
|
check "an issue type dropped from the caller goes red" 1 "" \
|
||||||
types_in_sync issues "$mut_caller" "$STUB"
|
types_in_sync issues "$mut_caller" "$STUB"
|
||||||
sed 's/, edited//' "$STUB" >"$mut_stub"
|
sed 's/, closed//' "$STUB" >"$mut_stub"
|
||||||
check "an issue type dropped from the stub goes red" 1 "" \
|
check "an issue type dropped from the stub goes red" 1 "" \
|
||||||
types_in_sync issues "$CALLER" "$mut_stub"
|
types_in_sync issues "$CALLER" "$mut_stub"
|
||||||
sed 's/closed, reopened/reopened, closed/' "$STUB" >"$mut_stub"
|
sed 's/opened, closed/closed, opened/' "$STUB" >"$mut_stub"
|
||||||
check "an issue-list reorder in one file only goes red" 1 "" \
|
check "an issue-list reorder in one file only goes red" 1 "" \
|
||||||
types_in_sync issues "$CALLER" "$mut_stub"
|
types_in_sync issues "$CALLER" "$mut_stub"
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue