forked from heavy-duty/ceremony
A called workflow cannot read the caller's dispatch inputs on this forge: github.event.inputs is empty inside workflow_call even though the top-level caller receives the value in both contexts (probe runs 6/7). The sweep's gate read exactly that, so every dispatch-woken sweep bootstrapped — runs 459 and 523, ~20 label upserts per board event — while the trigger honestly logged bootstrap=no. The bridge, per the #6361 contract: labels-sweep.yml declares workflow_call.inputs.bootstrap (string, default "no"); the dogfood caller and the published CONSUMERS.md stub pass it via with.bootstrap with empty mapped to "no" at the caller — kimi's edge: on schedule the top-level context is empty, and an empty that slipped through would have turned every cron into a bootstrap. The gate feeds the declared input to labels-reconcile unchanged, so an invalid value meets the action's own yes|no refusal. test/labels-bootstrap.test.sh pins every hop: the declared boundary, both gates as the identity, no expression reading github.event.inputs (scoped to ${{ }} bodies — the file's prose names the context to explain it), the two pass-throughs byte-exact, and the four value paths driven through the shipped expressions into the action's real validator. Mutations: dropping the declaration reds 4, dropping the pass-through reds 3, restoring the old gate reds 2. Refs #215
141 lines
6.8 KiB
YAML
141 lines
6.8 KiB
YAML
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 }}
|