Merge pull request #211 from dan-claude-bot/build/209-detach-reconcile-sweep
labels: detach the reconcile sweep from PR-triggered runs (#209)
This commit is contained in:
commit
c2987fd8d8
8 changed files with 448 additions and 140 deletions
125
.github/workflows/labels-sweep.yml
vendored
Normal file
125
.github/workflows/labels-sweep.yml
vendored
Normal file
|
|
@ -0,0 +1,125 @@
|
||||||
|
name: labels-sweep
|
||||||
|
# Reusable sweep half of the labels automation — the reconcile + issueflow
|
||||||
|
# jobs that rode labels.yml until #209. Triggers and permissions live in the
|
||||||
|
# caller; docs/CONSUMERS.md carries the complete caller stub
|
||||||
|
# (workflow_dispatch plus the hourly cron, which relocated here with the
|
||||||
|
# sweep). Board events still yield a sweep within seconds: labels.yml's
|
||||||
|
# trigger job dispatches this workflow's caller on every event it used to
|
||||||
|
# run reconcile on.
|
||||||
|
#
|
||||||
|
# Detached on purpose (#209): every sweep covers every open PR and all
|
||||||
|
# sweeps serialize through ONE shared concurrency group, so GitHub's
|
||||||
|
# one-running-plus-one-pending queue records every extra run as CANCELLED.
|
||||||
|
# That displacement is semantically lossless — the surviving sweep does the
|
||||||
|
# displaced run's work — but while the sweep rode pull_request_target runs
|
||||||
|
# the ❌ landed on that PR's checks and read as red CI, with no manual
|
||||||
|
# escape hatch: GitHub refuses to rerun a queue-displaced run (crew#250).
|
||||||
|
# And displacement is the steady state of a working fleet, not a spike —
|
||||||
|
# one panel request emits one review_requested event per reviewer, so
|
||||||
|
# every review round over-fills the one-running-plus-one-pending queue.
|
||||||
|
# Here a displaced run attaches to no PR: the cancellations live on the
|
||||||
|
# Actions tab only.
|
||||||
|
#
|
||||||
|
# Bootstrap semantics: a manual dispatch of the caller bootstraps the
|
||||||
|
# taxonomy (its `bootstrap` input defaults to "yes"), exactly what
|
||||||
|
# dispatching the labels caller did before the split. The trigger job's
|
||||||
|
# dispatches carry bootstrap=no — ~20 label upserts per sweep is too chatty
|
||||||
|
# for every board event, the same reason cron runs never bootstrapped.
|
||||||
|
#
|
||||||
|
# This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not
|
||||||
|
# create workflow runs from GITHUB_TOKEN-raised events (the trigger's
|
||||||
|
# workflow_dispatch is one of the two documented exemptions; this workflow
|
||||||
|
# dispatches nothing). Agent writes use a PAT and therefore do trigger —
|
||||||
|
# exactly the asymmetry wanted.
|
||||||
|
on:
|
||||||
|
workflow_call:
|
||||||
|
inputs:
|
||||||
|
pr_workflow_name:
|
||||||
|
description: >-
|
||||||
|
The `name:` of the consumer's PR-facing labels caller, exported
|
||||||
|
to the reconcile step as SELF_WORKFLOW so the sweep can leave
|
||||||
|
the label machinery's own check entries (scope, trigger) out of
|
||||||
|
its CI verdict: a red trigger means "fix the caller", which no
|
||||||
|
PR edit can do, so it must never count toward blocker:ci-red.
|
||||||
|
Read by the #208 reconciler; harmless to earlier ones.
|
||||||
|
type: string
|
||||||
|
required: false
|
||||||
|
default: labels
|
||||||
|
|
||||||
|
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:
|
||||||
|
reconcile:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
# ONE shared group: every reconcile sweeps every open PR, so cron and
|
||||||
|
# dispatched 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:
|
||||||
|
# No PR code is ever checked out or executed: the sweep checks out
|
||||||
|
# the consumer's default branch and the pinned ceremony
|
||||||
|
# implementation 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.
|
||||||
|
#
|
||||||
|
# bootstrap: every trigger-driven wake arrives as workflow_dispatch
|
||||||
|
# too (that is how `gh workflow run` wakes the caller), so the event
|
||||||
|
# name alone no longer separates the operator's manual full-board
|
||||||
|
# bootstrap from an event-woken sweep — the caller's `bootstrap`
|
||||||
|
# dispatch input does: the trigger passes "no", a bare manual
|
||||||
|
# dispatch defaults to "yes". A caller reached on any other event
|
||||||
|
# (the cron) has no input and stays "no".
|
||||||
|
- name: reconcile state + stale
|
||||||
|
if: github.repository != 'heavy-duty/ceremony'
|
||||||
|
uses: ./.ceremony-src/actions/labels-reconcile
|
||||||
|
with:
|
||||||
|
bootstrap: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.bootstrap != 'no' && 'yes' || 'no' }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
SELF_WORKFLOW: ${{ inputs.pr_workflow_name }}
|
||||||
|
- 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' && github.event.inputs.bootstrap != 'no' && 'yes' || 'no' }}
|
||||||
|
env:
|
||||||
|
GH_TOKEN: ${{ github.token }}
|
||||||
|
REPO: ${{ github.repository }}
|
||||||
|
SELF_WORKFLOW: ${{ inputs.pr_workflow_name }}
|
||||||
|
- 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 }}
|
||||||
128
.github/workflows/labels.yml
vendored
128
.github/workflows/labels.yml
vendored
|
|
@ -6,24 +6,44 @@ name: labels
|
||||||
# family arrives from a fork, where pull_request runs with a READ-ONLY token
|
# 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
|
# 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
|
# 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,
|
# path mapping via the API and checks out only the ceremony implementation.
|
||||||
# and reconcile checks out the BASE branch only. Keep it that way.
|
# Keep it that way.
|
||||||
#
|
#
|
||||||
# There is no pull_request_review_target, so a review landing cannot wake this
|
# The reconcile sweep lived here until #209. Riding the PR-triggered run
|
||||||
# workflow directly — which is why the caller's cron is load-bearing, not a
|
# meant every displacement in the sweep's shared concurrency queue recorded
|
||||||
# safety net (#199 relaxed it from */15 to hourly, but did NOT drop it). The
|
# a CANCELLED `reconcile` check on some PR — read as red CI by every human
|
||||||
# cron is the sweep's only discovery path for every transition no subscribed
|
# and agent, though the surviving sweep does the displaced run's work. Two
|
||||||
# event carries: a verdict landing, blocker:ci-red set/cleared, a
|
# field facts made that untenable (crew#250): a displaced run cannot be
|
||||||
# blocker:conflict when another PR merges under this one, and the time-based
|
# rerun — `gh run rerun`, `--failed`, and `--job` all refuse — so a victim
|
||||||
# stale / 48h claim-reclaim. Where an event IS subscribed the wake is direct —
|
# PR has no manual escape hatch; and the displacing burst is deterministic,
|
||||||
# the handoff sets state:needs-human and the caller's `labeled` event confirms
|
# one `review_requested` event per panelist per request, so every review
|
||||||
# or corrects that optimistic write within seconds.
|
# 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: reconciler writes use GITHUB_TOKEN, and GitHub does not
|
# This cannot loop: the trigger's dispatch and the reconciler's label
|
||||||
# create workflow runs from GITHUB_TOKEN-triggered events. Agent writes use a
|
# writes both use GITHUB_TOKEN. GitHub does not create workflow runs from
|
||||||
# PAT and therefore do trigger — exactly the asymmetry wanted.
|
# 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:
|
on:
|
||||||
workflow_call:
|
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:
|
env:
|
||||||
# A called workflow arrives without its repository. Keep this literal pin
|
# A called workflow arrives without its repository. Keep this literal pin
|
||||||
|
|
@ -35,7 +55,7 @@ jobs:
|
||||||
# Not on labeled/unlabeled: those events change no paths, so scope has
|
# Not on labeled/unlabeled: those events change no paths, so scope has
|
||||||
# nothing new to derive — and label churn is precisely what they are.
|
# nothing new to derive — and label churn is precisely what they are.
|
||||||
# review_requested/review_request_removed likewise change no paths — they
|
# review_requested/review_request_removed likewise change no paths — they
|
||||||
# exist to wake reconcile (#137) — and running labeler on them widens
|
# exist to wake the sweep (#137) — and running labeler on them widens
|
||||||
# exactly the window #130 documents, where a label written during a
|
# exactly the window #130 documents, where a label written during a
|
||||||
# scope run is clobbered.
|
# scope run is clobbered.
|
||||||
if: >-
|
if: >-
|
||||||
|
|
@ -85,65 +105,27 @@ jobs:
|
||||||
# the mapping it is judged by
|
# the mapping it is judged by
|
||||||
CONFIG_REF: ${{ github.sha }}
|
CONFIG_REF: ${{ github.sha }}
|
||||||
|
|
||||||
reconcile:
|
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
|
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:
|
steps:
|
||||||
# pull_request_target is required for fork PR write permission. It is
|
- name: dispatch the sweep
|
||||||
# safe here because no PR code is ever checked out or executed:
|
|
||||||
# labels-scope reads the mapping and changed 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:
|
env:
|
||||||
GH_TOKEN: ${{ github.token }}
|
GH_TOKEN: ${{ github.token }}
|
||||||
REPO: ${{ github.repository }}
|
SWEEP_WORKFLOW: ${{ inputs.sweep_workflow }}
|
||||||
- name: reconcile state + stale (dogfood — the workspace IS ceremony)
|
run: gh workflow run "$SWEEP_WORKFLOW" -R "$GITHUB_REPOSITORY" -f bootstrap=no
|
||||||
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 }}
|
|
||||||
|
|
|
||||||
46
.github/workflows/self-labels-sweep.yml
vendored
Normal file
46
.github/workflows/self-labels-sweep.yml
vendored
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
name: labels-sweep
|
||||||
|
# Ceremony's own sweep caller (#209) — self-labels.yml's detached half,
|
||||||
|
# wearing the same local-`uses:` deviation and the same warning: consumers
|
||||||
|
# must NEVER copy the local form (it rides main, unpinned — correct only
|
||||||
|
# for the repo that IS the source). Consumers write:
|
||||||
|
# uses: heavy-duty/ceremony/.github/workflows/labels-sweep.yml@<pinned-tag>
|
||||||
|
on:
|
||||||
|
# The consumer owns this cadence (#203). Hourly is the recommended default
|
||||||
|
# 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 on the labels caller), 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 labels caller's events carry the rest in seconds, one
|
||||||
|
# trigger-job dispatch away. 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. A bare dispatch (input default "yes") also
|
||||||
|
# bootstraps the taxonomy on a fresh repo — what dispatching the labels
|
||||||
|
# caller did before #209. The reusable's trigger job wakes this workflow
|
||||||
|
# with bootstrap=no on every board event — an event-woken sweep must not
|
||||||
|
# re-upsert ~20 labels each time — so declaring this input is part of the
|
||||||
|
# caller contract: a dispatch naming an undeclared input is refused, and
|
||||||
|
# the trigger job goes loudly red.
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
bootstrap:
|
||||||
|
description: Bootstrap the label taxonomy before sweeping
|
||||||
|
type: choice
|
||||||
|
options: ["yes", "no"]
|
||||||
|
default: "yes"
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
checks: read # mergeability/check-rollup read for PR state
|
||||||
|
statuses: read # commit-status rollup read for PR state
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
jobs:
|
||||||
|
sweep:
|
||||||
|
# pr_workflow_name keeps its default: ceremony's PR-facing caller is
|
||||||
|
# named `labels` (self-labels.yml).
|
||||||
|
uses: ./.github/workflows/labels-sweep.yml
|
||||||
26
.github/workflows/self-labels.yml
vendored
26
.github/workflows/self-labels.yml
vendored
|
|
@ -4,22 +4,13 @@ name: labels
|
||||||
# same warning: consumers must NEVER copy the local form (it rides main,
|
# same warning: consumers must NEVER copy the local form (it rides main,
|
||||||
# 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>
|
||||||
|
#
|
||||||
|
# Since #209 this caller carries the PR/issue event surface only. The
|
||||||
|
# reconcile sweep no longer rides these runs — the reusable's trigger job
|
||||||
|
# dispatches the sweep caller (self-labels-sweep.yml here), which owns the
|
||||||
|
# hourly cron and the manual/bootstrap workflow_dispatch. A board event
|
||||||
|
# below still yields a sweep within seconds, one dispatch hop later.
|
||||||
on:
|
on:
|
||||||
# The consumer owns this cadence (#203). Hourly is the recommended default
|
|
||||||
# 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
|
# Narrowed (#199) to the actions that carry a queue-state change the hourly
|
||||||
# cron cannot wait one cadence for — dropping only labeled/unlabeled/assigned/
|
# cron cannot wait one cadence for — dropping only labeled/unlabeled/assigned/
|
||||||
# unassigned, which feed validation and the 48h claim clock (caught within one
|
# unassigned, which feed validation and the 48h claim clock (caught within one
|
||||||
|
|
@ -48,8 +39,13 @@ permissions:
|
||||||
contents: read
|
contents: read
|
||||||
checks: read # mergeability/check-rollup read for PR state
|
checks: read # mergeability/check-rollup read for PR state
|
||||||
statuses: read # commit-status rollup read for PR state
|
statuses: read # commit-status rollup read for PR state
|
||||||
|
actions: write # the trigger job's `gh workflow run` dispatch of the sweep caller (#209)
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
jobs:
|
jobs:
|
||||||
labels:
|
labels:
|
||||||
uses: ./.github/workflows/labels.yml
|
uses: ./.github/workflows/labels.yml
|
||||||
|
with:
|
||||||
|
# Dogfood filename deviation only — consumers keep the default,
|
||||||
|
# labels-sweep.yml, and pass nothing.
|
||||||
|
sweep_workflow: self-labels-sweep.yml
|
||||||
|
|
|
||||||
10
changelog.d/209.md
Normal file
10
changelog.d/209.md
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- The reconcile sweep is detached from PR-triggered runs: a new reusable
|
||||||
|
`labels-sweep.yml` carries it, woken by `labels.yml`'s new `trigger` job, so
|
||||||
|
a queue-displaced sweep cancels on the Actions tab instead of landing a
|
||||||
|
cancelled `reconcile` check on a PR (#209).
|
||||||
|
- Labels consumers add a sweep caller (`labels-sweep.yml`, stub in
|
||||||
|
docs/CONSUMERS.md), relocate the hourly cron and manual bootstrap dispatch
|
||||||
|
to it, and grant the labels caller `actions: write`; a pin bump without the
|
||||||
|
sweep caller goes loudly red at the trigger job (#209).
|
||||||
|
|
@ -27,7 +27,7 @@ edits to this guide (#12).
|
||||||
- **The `release` label must exist** before the first ceremony PR — it is
|
- **The `release` label must exist** before the first ceremony PR — it is
|
||||||
the merge door's declared-intent read
|
the merge door's declared-intent read
|
||||||
([lib/facts.sh](../lib/facts.sh#L88-L101)). Bootstrap it via the labels
|
([lib/facts.sh](../lib/facts.sh#L88-L101)). Bootstrap it via the labels
|
||||||
workflow's `workflow_dispatch`
|
sweep caller's `workflow_dispatch`
|
||||||
([Labels automation](#labels-automation)), or create it by hand,
|
([Labels automation](#labels-automation)), or create it by hand,
|
||||||
matching the core table
|
matching the core table
|
||||||
([actions/labels-reconcile/labels-reconcile.sh](../actions/labels-reconcile/labels-reconcile.sh#L369)):
|
([actions/labels-reconcile/labels-reconcile.sh](../actions/labels-reconcile/labels-reconcile.sh#L369)):
|
||||||
|
|
@ -118,12 +118,13 @@ the machinery at all:
|
||||||
consumer. In particular, `0.1.0` carries `changelog-armed`,
|
consumer. In particular, `0.1.0` carries `changelog-armed`,
|
||||||
`changelog-monotonic` and `drill-recorded` plus `docs-sync`, but not
|
`changelog-monotonic` and `drill-recorded` plus `docs-sync`, but not
|
||||||
`changelog-assembled` or `runner-isolated`.
|
`changelog-assembled` or `runner-isolated`.
|
||||||
6. **Labels automation** (optional but recommended): the caller from
|
6. **Labels automation** (optional but recommended): the two callers from
|
||||||
[Labels automation](#labels-automation), plus `.github/labels.conf`
|
[Labels automation](#labels-automation) — the event-facing labels
|
||||||
|
caller and the sweep caller (#209) — 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 the sweep caller's `workflow_dispatch` once —
|
||||||
the taxonomy, `release` label included** — and use it again whenever an
|
**this bootstraps the taxonomy, `release` label included** — and use it
|
||||||
operator needs a full-board sweep immediately.
|
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.
|
||||||
|
|
@ -150,7 +151,8 @@ precisely so the machinery is safe to work on
|
||||||
- [ ] Swap the guard *script* steps in `ci.yml` for the `uses:` steps in
|
- [ ] Swap the guard *script* steps in `ci.yml` for the `uses:` steps in
|
||||||
the bootstrap list above (with `fetch-depth: 0` on the checkout).
|
the bootstrap list above (with `fetch-depth: 0` on the checkout).
|
||||||
- [ ] Replace `labels.yml` with the caller from
|
- [ ] Replace `labels.yml` with the caller from
|
||||||
[Labels automation](#labels-automation); extract
|
[Labels automation](#labels-automation) and add the sweep caller
|
||||||
|
`labels-sweep.yml` beside it (#209); extract
|
||||||
`.github/labels.conf` from the old reconciler's embedded config —
|
`.github/labels.conf` from the old reconciler's embedded config —
|
||||||
the `panel=` roster line and the repo's `scope:*` rows
|
the `panel=` roster line and the repo's `scope:*` rows
|
||||||
([the format](#labels-automation)). `.github/labeler.yml` stays as
|
([the format](#labels-automation)). `.github/labeler.yml` stays as
|
||||||
|
|
@ -269,13 +271,32 @@ build (#15) and incubator's GHCR image push (#16).
|
||||||
|
|
||||||
## Labels automation
|
## Labels automation
|
||||||
|
|
||||||
The reusable labels workflow owns two independent jobs: additive path-based
|
The labels automation is two reusable workflows since #209, adopted
|
||||||
`scope:*` labels and reconciliation of PR state, blockers, handoff, stale
|
together at the same pin:
|
||||||
status, and the `needs-ruling` invariants on both surfaces — the bare-flag
|
|
||||||
check and the 7-day comment-only nudge (#52; the sweep reads that flag and
|
- **`labels.yml`** — the event-facing half, called on PR and issue events.
|
||||||
never writes it). The consumer keeps its path mapping in
|
Two jobs: additive path-based `scope:*` labels, and a few-seconds
|
||||||
`.github/labeler.yml` and its review panel plus scope taxonomy in
|
`trigger` job that wakes the sweep by dispatching the consumer's sweep
|
||||||
`.github/labels.conf`.
|
caller (`gh workflow run`, plain `GITHUB_TOKEN` — `workflow_dispatch` is
|
||||||
|
one of the two documented exemptions from the token's no-retrigger rule,
|
||||||
|
so no PAT anywhere in the path and no loop: the sweep dispatches
|
||||||
|
nothing).
|
||||||
|
- **`labels-sweep.yml`** — the reconcile sweep: PR state, blockers,
|
||||||
|
handoff, stale status, the issue work queue, and the `needs-ruling`
|
||||||
|
invariants on both surfaces — the bare-flag check and the 7-day
|
||||||
|
comment-only nudge (#52; the sweep reads that flag and never writes it).
|
||||||
|
Detached from PR-triggered runs on purpose: all sweeps serialize through
|
||||||
|
one shared concurrency group, and GitHub records every queue-displaced
|
||||||
|
run as CANCELLED — harmless (the surviving sweep does its work) until it
|
||||||
|
rode a `pull_request_target` run and the ❌ landed on that PR's checks
|
||||||
|
as fake red CI that GitHub refuses to rerun (crew#250: `gh run rerun`
|
||||||
|
and its `--failed`/`--job` forms all decline a queue-displaced run).
|
||||||
|
Behind its own caller, a displaced sweep cancels on the
|
||||||
|
Actions tab, attached to no PR; PR checks show `scope` and the green
|
||||||
|
`trigger` only.
|
||||||
|
|
||||||
|
The consumer keeps its path mapping in `.github/labeler.yml` and its
|
||||||
|
review panel plus scope taxonomy in `.github/labels.conf`.
|
||||||
|
|
||||||
**Additive means additive** (unreleased — #130): the scope job's only label
|
**Additive means additive** (unreleased — #130): the scope job's only label
|
||||||
write is `POST /issues/{n}/labels`, which adds the derived scopes and removes
|
write is `POST /issues/{n}/labels`, which adds the derived scopes and removes
|
||||||
|
|
@ -292,25 +313,11 @@ half-honoured. The reconcile sweep also warns (never sets) when a non-draft
|
||||||
PR carries a bare `X.Y.Z` version differing from its base but no `release`
|
PR carries a bare `X.Y.Z` version differing from its base but no `release`
|
||||||
label — the merge door would refuse that merge, and the sweep says so first.
|
label — the merge door would refuse that merge, and the sweep says so first.
|
||||||
|
|
||||||
The complete caller is:
|
The complete event-facing caller is:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
name: labels
|
name: labels
|
||||||
on:
|
on:
|
||||||
# The consumer owns this cadence (#203). Hourly is the recommended default
|
|
||||||
# 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:
|
||||||
# Fork PRs; these carry the head/draft/review facts state:* derives from.
|
# Fork PRs; these carry the head/draft/review facts state:* derives from.
|
||||||
# labeled/unlabeled are the handoff wake (state:needs-human confirmed here);
|
# labeled/unlabeled are the handoff wake (state:needs-human confirmed here);
|
||||||
|
|
@ -334,18 +341,81 @@ permissions:
|
||||||
contents: read
|
contents: read
|
||||||
checks: read # mergeability/check-rollup read for PR state
|
checks: read # mergeability/check-rollup read for PR state
|
||||||
statuses: read # commit-status rollup read for PR state
|
statuses: read # commit-status rollup read for PR state
|
||||||
actions: read # workflow-run nodes inside the check rollup — private repos do not imply it (incubator#60)
|
actions: write # the trigger job's `gh workflow run` dispatch of the sweep caller (#209)
|
||||||
issues: write
|
issues: write
|
||||||
pull-requests: write
|
pull-requests: write
|
||||||
jobs:
|
jobs:
|
||||||
labels:
|
labels:
|
||||||
uses: heavy-duty/ceremony/.github/workflows/labels.yml@<pinned-tag>
|
uses: heavy-duty/ceremony/.github/workflows/labels.yml@<pinned-tag>
|
||||||
|
# If the sweep caller below is named anything but labels-sweep.yml,
|
||||||
|
# say so: `with: { sweep_workflow: <filename> }`. Ceremony's own
|
||||||
|
# dogfood does (self-labels-sweep.yml).
|
||||||
|
```
|
||||||
|
|
||||||
|
And the complete sweep caller, `labels-sweep.yml` beside it — the hourly
|
||||||
|
cron lives HERE since #209, not on the labels caller:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
name: labels-sweep
|
||||||
|
on:
|
||||||
|
# The consumer owns this cadence (#203). Hourly is the recommended default
|
||||||
|
# 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 on the labels caller), blocker:ci-red
|
||||||
|
# set/cleared, blocker:conflict when another PR merges under this one, and
|
||||||
|
# time-based stale / 48h claim-reclaim. The labels caller's events carry the
|
||||||
|
# rest in seconds, one trigger-job dispatch away. 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. A bare dispatch (input default "yes") also
|
||||||
|
# bootstraps the taxonomy on a fresh repo. The labels caller's trigger job
|
||||||
|
# wakes this workflow with bootstrap=no on every board event, so the
|
||||||
|
# declared input is part of the contract: a dispatch naming an undeclared
|
||||||
|
# input is refused, and the trigger job goes loudly red.
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
bootstrap:
|
||||||
|
description: Bootstrap the label taxonomy before sweeping
|
||||||
|
type: choice
|
||||||
|
options: ["yes", "no"]
|
||||||
|
default: "yes"
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
checks: read # mergeability/check-rollup read for PR state
|
||||||
|
statuses: read # commit-status rollup read for PR state
|
||||||
|
actions: read # workflow-run nodes inside the check rollup — private repos do not imply it (incubator#60)
|
||||||
|
issues: write
|
||||||
|
pull-requests: write
|
||||||
|
jobs:
|
||||||
|
sweep:
|
||||||
|
uses: heavy-duty/ceremony/.github/workflows/labels-sweep.yml@<pinned-tag>
|
||||||
|
# If this repo's PR-facing labels caller is named anything but `labels`,
|
||||||
|
# pass that name: `with: { pr_workflow_name: <name> }`. The sweep exports
|
||||||
|
# it as SELF_WORKFLOW so the label machinery's own check entries (scope,
|
||||||
|
# trigger) never count toward blocker:ci-red — a red trigger means "fix
|
||||||
|
# the caller", which no PR edit can do (#208 reads it).
|
||||||
```
|
```
|
||||||
|
|
||||||
Naming any permission sets every unnamed permission to `none`. Public
|
Naming any permission sets every unnamed permission to `none`. Public
|
||||||
repositories allow check data to be read regardless, but a private consumer
|
repositories allow check data to be read regardless, but a private consumer
|
||||||
needs all three explicit reads above; without them the failure appears as an empty
|
needs the explicit reads above; without them the failure appears as an empty
|
||||||
`state:*` axis on the board rather than a red workflow run.
|
`state:*` axis on the board rather than a red workflow run. The labels
|
||||||
|
caller's `actions: write` is different — it is required everywhere, public
|
||||||
|
repos included: the trigger job's `gh workflow run` is a write, and without
|
||||||
|
it every event run goes red at the trigger.
|
||||||
|
|
||||||
|
**The failure mode to know before bumping**: a consumer that bumps its pin
|
||||||
|
to a #209-carrying tag without adding the sweep caller keeps green-looking
|
||||||
|
silence nowhere — the trigger job goes **red on every PR and issue event**
|
||||||
|
(workflow-not-found; likewise on a sweep caller missing its `bootstrap`
|
||||||
|
input, or a labels caller missing `actions: write`), and event-woken sweeps
|
||||||
|
stop until the caller lands. That loudness is deliberate: never read
|
||||||
|
silence, or a green `scope` alone, as health. Make the adoption one atomic
|
||||||
|
PR — pin bump, sweep caller file, `actions: write` line together.
|
||||||
|
|
||||||
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
|
||||||
|
|
@ -366,8 +436,42 @@ 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
|
the stub and ceremony's own caller stay byte-for-byte identical, the parity
|
||||||
#144 established.
|
#144 established.
|
||||||
|
|
||||||
|
The two-caller split (ceremony#209) is **unreleased**. A consumer pinned to
|
||||||
|
`0.4.0` or earlier keeps the previous single-caller shape — the labels
|
||||||
|
caller carrying the cron, `workflow_dispatch`, and `actions: read` — and
|
||||||
|
adopts the split at the pin bump to the first tag carrying ceremony#209.
|
||||||
|
Never mix refs to adopt it early.
|
||||||
|
|
||||||
|
The migration is **one atomic PR** with exactly four edits — crew, the
|
||||||
|
consumer whose displaced-check evidence drove #209 (crew#227, crew#250),
|
||||||
|
is the worked example; written here against `0.4.1` as the illustrative
|
||||||
|
first tag carrying the split:
|
||||||
|
|
||||||
|
1. **Pin bump, every reference together** ([Version pinning](#version-pinning)):
|
||||||
|
`0.4.0` → `0.4.1` in the labels caller's `uses:` line **and in every
|
||||||
|
other ceremony `uses:` in the repo** — crew also pins in
|
||||||
|
`release.yml` and its `ci.yml` guard steps. A repo on the doctrine
|
||||||
|
mirror re-runs `docs-sync --fix` in the same PR.
|
||||||
|
2. **New file `.github/workflows/labels-sweep.yml`** — the sweep caller
|
||||||
|
stub above, verbatim, `bootstrap` input included (the trigger's
|
||||||
|
`-f bootstrap=no` dispatch is refused if the input is undeclared).
|
||||||
|
3. **The hourly cron RELOCATES — it is moved, never copied.** Delete the
|
||||||
|
`schedule:` block (and the bare `workflow_dispatch:`) from the labels
|
||||||
|
caller in the same edit that adds the sweep caller.
|
||||||
|
**Warning**: a consumer that copies the sweep caller and leaves the
|
||||||
|
old schedule on the labels caller gets DOUBLE sweeps — every cron tick
|
||||||
|
fires both callers into the one shared `labels-reconcile` group — so
|
||||||
|
displacement goes **up**, and the fix reads as the bug getting worse.
|
||||||
|
4. **`actions: write` on the labels caller** — consumers carry
|
||||||
|
`actions: read` today (crew does); the trigger job's `gh workflow run`
|
||||||
|
is a write. The sweep caller keeps `actions: read`.
|
||||||
|
|
||||||
|
Bump without the sweep caller and the trigger job goes red on every PR
|
||||||
|
and issue event — the loud failure mode above — so never split these
|
||||||
|
four edits across PRs.
|
||||||
|
|
||||||
`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 workflows execute no PR code. They check
|
||||||
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
|
||||||
|
|
@ -403,21 +507,22 @@ way — keep the file data only).
|
||||||
Core state, blocker, work-queue, and release labels come from ceremony. Scope
|
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 callers and configuration, dispatch the sweep caller once
|
||||||
bootstrap labels on a fresh repository. It is also the operator's general
|
to bootstrap labels on a fresh repository. A bare dispatch is also the
|
||||||
manual full-board sweep — the answer when the board looks wrong now rather
|
operator's general manual full-board sweep — the answer when the board
|
||||||
than after the next scheduled cadence:
|
looks wrong now rather than after the next scheduled cadence:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
gh workflow run labels.yml -R <owner>/<repo>
|
gh workflow run labels-sweep.yml -R <owner>/<repo>
|
||||||
```
|
```
|
||||||
|
|
||||||
Ceremony dogfoods the caller under the filename `self-labels.yml`, so the
|
Ceremony dogfoods the callers under the filenames `self-labels.yml` and
|
||||||
equivalent command in this repository substitutes that filename. Scheduled
|
`self-labels-sweep.yml`, so the equivalent command in this repository
|
||||||
and PR-triggered runs only reconcile; they do not repeatedly upsert the
|
substitutes that filename. Scheduled and trigger-driven runs only
|
||||||
taxonomy. When a ceremony pin bump adds a core label, bump the pin first and
|
reconcile; they do not repeatedly upsert the taxonomy (the trigger's
|
||||||
then re-dispatch `workflow_dispatch`; the scheduled sweep warns when the
|
dispatch carries `bootstrap=no`). When a ceremony pin bump adds a core
|
||||||
pinned taxonomy declares a core label the repository lacks.
|
label, bump the pin first and then re-dispatch; the scheduled sweep warns
|
||||||
|
when the pinned taxonomy declares a core label the repository lacks.
|
||||||
|
|
||||||
## Doctrine mirror
|
## Doctrine mirror
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -30,7 +30,7 @@ check "dogfood caller wakes on issue events" 0 " issues:" \
|
||||||
grep -F " issues:" "$ROOT/.github/workflows/self-labels.yml"
|
grep -F " issues:" "$ROOT/.github/workflows/self-labels.yml"
|
||||||
dogfood_pr_step="$(sed -n \
|
dogfood_pr_step="$(sed -n \
|
||||||
'/name: reconcile state + stale (dogfood/,/name: reconcile issue flow/p' \
|
'/name: reconcile state + stale (dogfood/,/name: reconcile issue flow/p' \
|
||||||
"$ROOT/.github/workflows/labels.yml")"
|
"$ROOT/.github/workflows/labels-sweep.yml")"
|
||||||
# shellcheck disable=SC2016 # GitHub expressions are asserted as literals
|
# shellcheck disable=SC2016 # GitHub expressions are asserted as literals
|
||||||
check "dogfood PR reconcile receives repository" 0 ' REPO: ${{ github.repository }}' \
|
check "dogfood PR reconcile receives repository" 0 ' REPO: ${{ github.repository }}' \
|
||||||
grep -F ' REPO: ${{ github.repository }}' <<<"$dogfood_pr_step"
|
grep -F ' REPO: ${{ github.repository }}' <<<"$dogfood_pr_step"
|
||||||
|
|
|
||||||
|
|
@ -12,8 +12,10 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||||
source "$ROOT/test/harness.sh"
|
source "$ROOT/test/harness.sh"
|
||||||
|
|
||||||
REUSABLE="$ROOT/.github/workflows/labels.yml"
|
REUSABLE="$ROOT/.github/workflows/labels.yml"
|
||||||
|
SWEEP="$ROOT/.github/workflows/labels-sweep.yml"
|
||||||
SELF="$ROOT/.github/workflows/self-labels.yml"
|
SELF="$ROOT/.github/workflows/self-labels.yml"
|
||||||
STUB="$ROOT/docs/CONSUMERS.md" # the published caller stub, a fenced yaml block
|
SELF_SWEEP="$ROOT/.github/workflows/self-labels-sweep.yml"
|
||||||
|
STUB="$ROOT/docs/CONSUMERS.md" # the published caller stubs, fenced yaml blocks
|
||||||
|
|
||||||
# The `cancel-in-progress:` value of a named top-level job, read from the first
|
# 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.
|
# such line inside that job's block. Job keys sit at two-space indent.
|
||||||
|
|
@ -39,26 +41,68 @@ trigger_types() { # $1 = file, $2 = trigger key
|
||||||
# ---- the guard the cost fix must never trade away (#199 test plan must-fail) --
|
# ---- 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
|
# 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
|
# the shared concurrency group exists to prevent. It WOULD cut run count — by
|
||||||
# trading correctness for minutes — so it stays false, forever.
|
# trading correctness for minutes — so it stays false, forever. The job lives
|
||||||
|
# in labels-sweep.yml since #209; the guard moved with it.
|
||||||
check "reconcile serializes, never cancels mid-board" 0 "false" \
|
check "reconcile serializes, never cancels mid-board" 0 "false" \
|
||||||
job_cancel_in_progress "$REUSABLE" reconcile
|
job_cancel_in_progress "$SWEEP" reconcile
|
||||||
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
|
# shellcheck disable=SC2016 # the awk program runs in the nested bash, not here
|
||||||
check "reconcile is never cancel-in-progress: true" 1 "" \
|
check "reconcile is never cancel-in-progress: true" 1 "" \
|
||||||
bash -c 'job_cancel_in_progress() {
|
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"
|
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"
|
}; [ "$(job_cancel_in_progress "$1")" = true ]' _ "$SWEEP"
|
||||||
# scope MAY cancel — it is per-PR and additive, so a superseded run is waste,
|
# 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.
|
# 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" \
|
check "scope stays cancel-in-progress: true (per-PR, additive)" 0 "true" \
|
||||||
job_cancel_in_progress "$REUSABLE" scope
|
job_cancel_in_progress "$REUSABLE" scope
|
||||||
|
|
||||||
|
# ---- the sweep is detached from PR-triggered runs (#209) ---------------------
|
||||||
|
# While reconcile rode the PR-event run, every displacement in its shared
|
||||||
|
# queue recorded a CANCELLED check on some PR — fake red CI. The reusable
|
||||||
|
# labels.yml must never grow the job back; its trigger job wakes the sweep
|
||||||
|
# caller by dispatch instead, and that dispatch is the misconfiguration
|
||||||
|
# alarm: a pin bumped without the sweep caller must go loudly red at the
|
||||||
|
# trigger, so the dispatch line is never allowed to silence itself.
|
||||||
|
check "labels.yml carries no reconcile job" 1 "" \
|
||||||
|
grep -E '^ reconcile:' "$REUSABLE"
|
||||||
|
check "labels-sweep.yml carries the reconcile job" 0 " reconcile:" \
|
||||||
|
grep -E '^ reconcile:' "$SWEEP"
|
||||||
|
check "the sweep keeps the ONE shared concurrency group" 0 "group: labels-reconcile" \
|
||||||
|
grep -F 'group: labels-reconcile' "$SWEEP"
|
||||||
|
check "labels.yml carries the trigger job" 0 " trigger:" \
|
||||||
|
grep -E '^ trigger:' "$REUSABLE"
|
||||||
|
# shellcheck disable=SC2016 # $SWEEP_WORKFLOW is the workflow's own env var, asserted literally
|
||||||
|
check "the trigger dispatches the sweep caller, never bootstrapping" 0 \
|
||||||
|
'gh workflow run "$SWEEP_WORKFLOW" -R "$GITHUB_REPOSITORY" -f bootstrap=no' \
|
||||||
|
grep -F 'gh workflow run' "$REUSABLE"
|
||||||
|
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
|
||||||
|
check "the trigger dispatch is never silenced with || true" 1 "" \
|
||||||
|
bash -c 'grep -F "gh workflow run" "$1" | grep -qF "|| true"' _ "$REUSABLE"
|
||||||
|
check "the sweep caller filename input defaults to labels-sweep.yml" 0 \
|
||||||
|
"default: labels-sweep.yml" grep -F 'default: labels-sweep.yml' "$REUSABLE"
|
||||||
|
# the dogfood callers wear the split: the event caller names its deviant
|
||||||
|
# sweep filename, and the sweep caller declares the bootstrap input the
|
||||||
|
# trigger's -f flag requires (an undeclared input reds every dispatch)
|
||||||
|
check "self caller passes its dogfood sweep filename" 0 \
|
||||||
|
"sweep_workflow: self-labels-sweep.yml" \
|
||||||
|
grep -F 'sweep_workflow: self-labels-sweep.yml' "$SELF"
|
||||||
|
check "self sweep caller declares the bootstrap dispatch input" 0 \
|
||||||
|
"bootstrap:" grep -E '^ bootstrap:' "$SELF_SWEEP"
|
||||||
|
check "stub sweep caller declares the bootstrap dispatch input" 0 \
|
||||||
|
"bootstrap:" grep -E '^ bootstrap:' "$STUB"
|
||||||
|
# the labels caller's event runs must not carry the sweep's cron or manual
|
||||||
|
# dispatch — those relocated to the sweep caller with #209
|
||||||
|
check "self caller carries no cron" 1 "" grep -F 'cron:' "$SELF"
|
||||||
|
check "self caller carries no workflow_dispatch" 1 "" \
|
||||||
|
grep -E '^ workflow_dispatch:' "$SELF"
|
||||||
|
|
||||||
# ---- the cron is a backstop, relaxed to hourly (#199 candidate 1) -----------
|
# ---- 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
|
# 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.
|
# 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"
|
# The cron rides the sweep caller since #209.
|
||||||
|
check "self sweep caller cron is hourly" 0 '0 * * * *' grep -F 'cron:' "$SELF_SWEEP"
|
||||||
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
|
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
|
||||||
check "self caller cron line no longer fires */15" 1 "" \
|
check "self sweep caller cron line no longer fires */15" 1 "" \
|
||||||
bash -c 'grep -F "cron:" "$1" | grep -qF "*/15"' _ "$SELF"
|
bash -c 'grep -F "cron:" "$1" | grep -qF "*/15"' _ "$SELF_SWEEP"
|
||||||
check "stub cron is hourly" 0 '0 * * * *' grep -F 'cron:' "$STUB"
|
check "stub cron is hourly" 0 '0 * * * *' grep -F 'cron:' "$STUB"
|
||||||
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
|
# shellcheck disable=SC2016 # $1 expands in the nested bash, not here
|
||||||
check "stub cron line no longer fires */15" 1 "" \
|
check "stub cron line no longer fires */15" 1 "" \
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue