All checks were successful
CI / test (pull_request) Successful in 3m48s
CI / release-exercise (pull_request) Successful in 25s
CI / self-guards (pull_request) Successful in 20s
CI / action-exercise (pull_request) Successful in 20s
CI / docs-sync-exercise (pull_request) Successful in 20s
labels / labels (pull_request) Successful in 22s
Refs guard / refs-not-closing (pull_request) Successful in 20s
222 lines
12 KiB
YAML
222 lines
12 KiB
YAML
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, so same-repository PRs
|
||
# keep the base repository's write token without running PR code. On this
|
||
# Forgejo, unlike GitHub, fork-headed _target runs still receive a read-only
|
||
# token. Those runs therefore attempt no writes. The scheduled sweep later
|
||
# reconciles state, blockers, and handoff, but it does not apply path-derived
|
||
# scope labels; consumers that require those labels on fork heads apply them
|
||
# manually. The explicit fork_head job below records that disposition as a
|
||
# successful check. Both write paths execute only for same-repository heads.
|
||
# Scope reads changed paths and the path mapping through the API and checks out
|
||
# only the ceremony implementation. Keep it that way (#241).
|
||
#
|
||
# 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. Two
|
||
# field facts made that untenable (crew#250): a displaced run cannot be
|
||
# rerun — `gh run rerun`, `--failed`, and `--job` all refuse — so a victim
|
||
# PR has no manual escape hatch; and the displacing burst is deterministic,
|
||
# one `review_requested` event per panelist per request, so every review
|
||
# 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 instant wake: it fires on every issue event and
|
||
# same-repository PR event this caller subscribes to, preserving that part of
|
||
# the surface that used to run reconcile directly. Same-repository wake latency
|
||
# (#137) remains seconds-scale, while a displaced sweep cancels on the Actions
|
||
# tab, attached to no PR. Fork-headed runs cannot dispatch with their read-only
|
||
# token, so state, blocker, and handoff reconciliation waits for the scheduled
|
||
# sweep; path-derived scope labels are not applied to fork heads. PR checks show
|
||
# scope + trigger for same-repository heads, or fork_head for fork heads.
|
||
#
|
||
# 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.6.2"
|
||
|
||
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.pull_request.head.repo.full_name == github.repository &&
|
||
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 instant wake (#209) keeps the whole non-PR event surface and
|
||
# same-repository PRs. Fork-headed PRs are excluded because this Forgejo
|
||
# gives their pull_request_target run a read-only token (#241); fork_head
|
||
# records which reconciliation waits for the sweep and that path-derived
|
||
# scope labels are not applied there.
|
||
#
|
||
# 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.
|
||
if: >-
|
||
github.event_name != 'pull_request_target' ||
|
||
github.event.pull_request.head.repo.full_name == github.repository
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: dispatch the sweep
|
||
env:
|
||
GITHUB_TOKEN: ${{ github.token }}
|
||
SWEEP_WORKFLOW: ${{ inputs.sweep_workflow }}
|
||
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
|
||
run: |
|
||
# REST, not `gh` (#205). The workflow-dispatch endpoint has the SAME
|
||
# shape on both forges —
|
||
# POST {api}/repos/{owner}/{repo}/actions/workflows/{file}/dispatches
|
||
# {"ref": "<branch>", "inputs": {...}} -> 204, empty body
|
||
# — so this step no longer decides a forge at all. That is why the
|
||
# `CEREMONY_FORGE_CLIENT: gh` declaration and both inline refusals are
|
||
# gone rather than ported: there is nothing left to refuse. Measured
|
||
# on this instance (Forgejo 8.0.3+gitea-1.22.0) and published in its
|
||
# own swagger; run 459 was raised this way.
|
||
#
|
||
# STILL LOUD on failure, per this job's contract: a consumer missing
|
||
# the sweep caller, its `bootstrap` input, or `actions: write` must
|
||
# fail HERE and visibly, not sweep silently never again.
|
||
# NEVER "probably github" (lib/forge.sh). Defaulting an unset
|
||
# GITHUB_API_URL to api.github.com would send this forge's dispatch
|
||
# to GitHub and report success — the same unset-environment guess
|
||
# #201 just refused for docs-sync. The API root is injected by the
|
||
# forge running us; if it is absent we do not know where we are, and
|
||
# a guess is worse than a red trigger
|
||
# (@codex-reviewer-andresmgsl, #205 review).
|
||
api="${GITHUB_API_URL:-}"
|
||
if [ -z "$api" ]; then
|
||
echo "::error::labels: the sweep was NOT woken — GITHUB_API_URL is unset, so the forge's API root is unknown. Refusing to guess a forge."
|
||
exit 1
|
||
fi
|
||
|
||
# `gh workflow run` defaulted the ref to the repository's default
|
||
# branch; REST has no default and 400s without one. Prefer the event
|
||
# payload, fall back to an API read: on a `pull_request_target` run
|
||
# GITHUB_REF_NAME is `<n>/merge`, which is not a branch and would
|
||
# dispatch nothing.
|
||
branch="${DEFAULT_BRANCH:-}"
|
||
if [ -z "$branch" ]; then
|
||
branch="$(curl -fsS -H "Authorization: Bearer $GITHUB_TOKEN" \
|
||
"$api/repos/$GITHUB_REPOSITORY" | jq -r '.default_branch // empty')"
|
||
fi
|
||
if [ -z "$branch" ]; then
|
||
echo "::error::labels: the sweep was NOT woken — could not determine the default branch to dispatch $SWEEP_WORKFLOW on."
|
||
exit 1
|
||
fi
|
||
|
||
out="$(mktemp)"
|
||
err="$(mktemp)"
|
||
trap 'rm -f "$out" "$err"' EXIT
|
||
# A transport failure is named, not merely propagated. Letting `set
|
||
# -e` carry curl's own exit code out of the assignment DID fail the
|
||
# job — the invariant holds — but it failed with a bare status and no
|
||
# sentence, which is the opposite of this step owning its diagnostic.
|
||
if ! code="$(curl -sS -o "$out" -w '%{http_code}' -X POST \
|
||
-H "Authorization: Bearer $GITHUB_TOKEN" \
|
||
-H 'Content-Type: application/json' \
|
||
-d "$(jq -nc --arg ref "$branch" '{ref: $ref, inputs: {bootstrap: "no"}}')" \
|
||
"$api/repos/$GITHUB_REPOSITORY/actions/workflows/$SWEEP_WORKFLOW/dispatches" \
|
||
2>"$err")"; then
|
||
echo "::error::labels: the sweep was NOT woken — the request to $api never completed: $(tr -d '\n' <"$err")"
|
||
exit 1
|
||
fi
|
||
|
||
if [ "$code" != "204" ]; then
|
||
# Own the diagnostic rather than pass the status through. This
|
||
# Forgejo answers an unknown workflow name — and a bare ref that
|
||
# does not resolve — with `500` and an EMPTY body, so the raw
|
||
# status alone sends the reader looking for a server fault that is
|
||
# not there.
|
||
echo "::error::labels: the sweep was NOT woken — POST $api/repos/$GITHUB_REPOSITORY/actions/workflows/$SWEEP_WORKFLOW/dispatches (ref=$branch) returned HTTP $code: $(tr -d '\n' <"$out")"
|
||
echo "::error::labels: check that $SWEEP_WORKFLOW exists on $branch, declares a \`bootstrap\` workflow_dispatch input, and that this caller grants \`actions: write\`. An empty 500 body from Forgejo means the workflow name or the ref did not resolve."
|
||
exit 1
|
||
fi
|
||
echo "labels: sweep dispatched — $SWEEP_WORKFLOW on $branch (bootstrap=no)"
|
||
|
||
fork_head:
|
||
# This Forgejo keeps pull_request_target read-only for fork heads (#241),
|
||
# so name the deliberately unsupported scope write as well as the deferred
|
||
# state machine instead of letting a green no-op promise full labelling.
|
||
if: >-
|
||
github.event_name == 'pull_request_target' &&
|
||
github.event.pull_request.head.repo.full_name != github.repository
|
||
runs-on: ubuntu-latest
|
||
steps:
|
||
- name: explain deferred fork labels
|
||
run: >-
|
||
echo "labels: fork head has a read-only token; state, blocker, and handoff reconciliation deferred to the scheduled sweep; path-derived scope labels are not applied to fork heads"
|