name: labels # Reusable half of the labels automation. Triggers and permissions live in # the caller; docs/CONSUMERS.md carries the complete caller stub. # # The caller uses pull_request_target, not pull_request: every PR in this # family arrives from a fork, where pull_request runs with a READ-ONLY token # and cannot label anything. _target is safe in this workflow because no PR # code is ever checked out or executed — scope reads changed paths and the # path mapping via the API and checks out only the ceremony implementation. # Keep it that way. # # The reconcile sweep lived here until #209. Riding the PR-triggered run # meant every displacement in the sweep's shared concurrency queue recorded # a CANCELLED `reconcile` check on some PR — read as red CI by every human # and agent, though the surviving sweep does the displaced run's work. The # sweep now lives in labels-sweep.yml behind its own caller, and the # trigger job below is its wake: it fires on every event this caller # subscribes — the exact surface that used to run reconcile directly — so # the wake latency (#137) is unchanged, while a displaced sweep cancels on # the Actions tab, attached to no PR. PR checks show scope + trigger only. # # This cannot loop: the trigger's dispatch and the reconciler's label # writes both use GITHUB_TOKEN. GitHub does not create workflow runs from # GITHUB_TOKEN-raised events — workflow_dispatch and repository_dispatch # are the two documented exemptions, which is exactly why the trigger can # wake the sweep with no PAT anywhere in the path — and the sweep itself # dispatches nothing. Agent writes use a PAT and therefore do trigger — # exactly the asymmetry wanted. on: workflow_call: inputs: sweep_workflow: description: >- Filename of the consumer's sweep caller — the workflow that calls labels-sweep.yml (docs/CONSUMERS.md carries the stub). The trigger job dispatches it by this name. Override it only when the caller file is not named labels-sweep.yml (ceremony's own dogfood names it self-labels-sweep.yml). type: string required: false default: labels-sweep.yml env: # A called workflow arrives without its repository. Keep this literal pin # aligned with the ceremony release consumed by callers (issue #9 D3). CEREMONY_SELF_REF: "0.4.0" jobs: scope: # Not on labeled/unlabeled: those events change no paths, so scope has # nothing new to derive — and label churn is precisely what they are. # review_requested/review_request_removed likewise change no paths — they # exist to wake the sweep (#137) — and running labeler on them widens # exactly the window #130 documents, where a label written during a # scope run is clobbered. if: >- github.event_name == 'pull_request_target' && github.event.action != 'labeled' && github.event.action != 'unlabeled' && github.event.action != 'review_requested' && github.event.action != 'review_request_removed' runs-on: ubuntu-latest concurrency: group: labels-scope-${{ github.event.pull_request.number }} cancel-in-progress: true steps: # actions/labeler@v5 held this seat until #130. Even with # sync-labels: false it wrote the WHOLE label set — PUT of # (labels-fetched-at-job-start ∪ derived) — so a label applied while # the job ran was silently removed: ceremony#128 lost its `release`, # the merge door's declared-intent read, two seconds after the # builder set it. v6/v7 write the same way, so the step was replaced # rather than repinned. labels-scope reads the consumer's # .github/labeler.yml and the changed paths via the API, and its # only write is an additive POST of the derived scopes: a label # applied mid-job survives by construction. # # Still no PR code: both checkouts below fetch the ceremony # implementation only. The dogfood checkout rides github.sha — the # base-branch commit the workflow file itself came from, so the # script and workflow can never skew — and doubles as the #11 # bootstrap: ceremony's own labels must work before any release tag # exists for the pinned checkout to fetch. - uses: actions/checkout@v4 if: github.repository == 'heavy-duty/ceremony' with: repository: ${{ github.repository }} ref: ${{ github.sha }} - uses: actions/checkout@v4 if: github.repository != 'heavy-duty/ceremony' with: repository: heavy-duty/ceremony ref: ${{ env.CEREMONY_SELF_REF }} - uses: ./actions/labels-scope env: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} PR_NUMBER: ${{ github.event.pull_request.number }} # the BASE branch commit — a PR must not label itself by editing # the mapping it is judged by CONFIG_REF: ${{ github.sha }} trigger: # The sweep's wake (#209). No `if:`: reconcile carried none, so the # trigger keeps the whole event surface the caller subscribes — # workflow_dispatch of the labels caller itself included. That cannot # double-fire bootstrap: this dispatch always carries bootstrap=no, so # a dispatched labels caller yields one plain sweep, and the taxonomy # bootstrap fires solely on a manual dispatch of the sweep caller # (whose input defaults to "yes"). Excluding workflow_dispatch here # would instead make a dispatched labels caller do nothing at all — # a silent no-op run is worse than a redundant sweep. # # LOUD on failure — never `|| true`: a red trigger is the # misconfiguration alarm. A consumer that bumps the pin without adding # the sweep caller (workflow-not-found), without its declared # `bootstrap` input (unexpected input), or without `actions: write` # on this caller (permission denied) fails HERE, visibly on the PR, # instead of silently never sweeping again. runs-on: ubuntu-latest steps: - name: dispatch the sweep env: GH_TOKEN: ${{ github.token }} SWEEP_WORKFLOW: ${{ inputs.sweep_workflow }} run: gh workflow run "$SWEEP_WORKFLOW" -R "$GITHUB_REPOSITORY" -f bootstrap=no