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; a called workflow cannot read the caller's dispatch inputs on every forge (Forgejo drops them at the workflow_call boundary — ceremony#215, probe runs 6/7 vs merged runs 459/523), so the value must arrive through this declared boundary, never via github.event.inputs. 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.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: read from the DECLARED workflow_call input and nothing # else. The old gate read `github.event.inputs.bootstrap` from inside # this called workflow — which Forgejo leaves empty at the # workflow_call boundary even though the top-level caller receives the # value in both contexts (ceremony#215; probe runs 6/7 measured the # boundary, merged runs 459/523 paid for it: every dispatch-woken # sweep bootstrapped). The caller passes the value through # `with.bootstrap`; anything not exactly "yes" is fed through for # labels-reconcile's own yes|no 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 }}