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, # 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 # workflow directly — and the caller's cron is a backstop, not the wake (#199 # relaxed it from */15 to hourly): GitHub deprioritises short intervals anyway, # so a quiet repo goes a while between ticks. The handoff wakes the sweep # itself: the author sets state:needs-human, and the caller's `labeled` event # confirms or corrects that optimistic write within seconds. The cron stays as # the last resort for a forgotten handoff. # # This cannot loop: reconciler writes use GITHUB_TOKEN, and GitHub does not # create workflow runs from GITHUB_TOKEN-triggered events. Agent writes use a # PAT and therefore do trigger — exactly the asymmetry wanted. on: workflow_call: 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.3.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 reconcile (#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 }} reconcile: 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: # pull_request_target is required for fork PR write permission. It is # 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: GH_TOKEN: ${{ github.token }} REPO: ${{ github.repository }} - 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' && '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 }}