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: bootstrap: description: >- Bootstrap the label taxonomy before sweeping. The CALLER passes this through from its own workflow_dispatch input. The measured invariant (ceremony#215): the value must be DECLARED here and EXPLICITLY passed — on this instance the called workflow did not see the caller's event inputs as an implicit substitute (runs 459/523 bootstrapped on a bootstrap=no dispatch) while the top-level caller received the value in both contexts (probe runs 6/7). Absent means "no": an event- or cron-woken sweep must never re-upsert ~20 labels. type: string required: false default: "no" 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.6.1" 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: read from the DECLARED workflow_call input and nothing # else. The old gate read `github.event.inputs.bootstrap` from inside # this called workflow, and on this instance that context arrived # empty (runs 459/523: every dispatch-woken sweep bootstrapped on a # bootstrap=no body) while the top-level caller received the value in # both contexts (probe runs 6/7) — ceremony#215. The reliable channel # is declare-and-pass, so that is the only one used. The caller passes # the value through `with.bootstrap`; anything not exactly yes|no is # fed through for labels-reconcile's own validation to judge, so a # typo refuses loudly instead of silently bootstrapping. - name: reconcile state + stale if: github.repository != 'heavy-duty/ceremony' uses: ./.ceremony-src/actions/labels-reconcile with: bootstrap: ${{ inputs.bootstrap }} 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: ${{ inputs.bootstrap }} 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 }}