probe: ceremony CI gates at 9357f09 on a Forgejo runner
Some checks failed
CI / test (push) Failing after -1s
Some checks failed
CI / test (push) Failing after -1s
This commit is contained in:
parent
9357f09aea
commit
bf1666bd4f
6 changed files with 0 additions and 920 deletions
107
.github/workflows/ci.yml
vendored
107
.github/workflows/ci.yml
vendored
|
|
@ -50,110 +50,3 @@ jobs:
|
|||
# push+refs/heads/main event — the merge door's exact gate — opening a
|
||||
# live door from CI. A pull_request event can never satisfy either
|
||||
# door's `if:`.
|
||||
release-exercise:
|
||||
if: github.event_name == 'pull_request'
|
||||
uses: ./.github/workflows/release-exercise.yml
|
||||
|
||||
# The self-guards (issue #11): this repo eats exactly what it serves. The
|
||||
# guard actions run against the REAL tree — VERSION, CHANGELOG.md,
|
||||
# drills/, .github/workflows/ — through the same `uses:` steps every
|
||||
# consumer's CI carries.
|
||||
# These steps are also the composite-action wiring proof (issue #5's
|
||||
# acceptance criterion: action.yml resolving, $GITHUB_ACTION_PATH, the
|
||||
# relative lib sourcing) that action-exercise carried with scratch files
|
||||
# while this repo had no tree of its own to guard; the armed and
|
||||
# drill-recorded scratch steps moved here per the armed step's own
|
||||
# eviction note — the file backend hardcodes the VERSION name, so a
|
||||
# scratch write would SHADOW the real file, not sit beside it.
|
||||
self-guards:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# The monotonic guard compares HEAD against the merge base; a
|
||||
# shallow checkout cannot resolve it, and in CI that is a hard
|
||||
# failure, not a skip (the action's description).
|
||||
fetch-depth: 0
|
||||
- uses: ./actions/changelog-armed
|
||||
- uses: ./actions/changelog-monotonic
|
||||
- uses: ./actions/changelog-assembled
|
||||
- uses: ./actions/drill-recorded
|
||||
- uses: ./actions/runner-isolated
|
||||
|
||||
# Exercises changelog-monotonic the way a consumer does, against a
|
||||
# CONSTRUCTED history. The self-guards job above runs the same action on
|
||||
# the real tree, but there its containment half is only as interesting as
|
||||
# the PR's own diff; this job commits a known base and an insert-above
|
||||
# edit on top, so a real, non-vacuous containment run is standing
|
||||
# evidence on every PR. (Armed and drill-recorded moved to self-guards —
|
||||
# the real tree now exercises them; monotonic stays because it reads no
|
||||
# version source, so it is immune to the VERSION-shadowing problem that
|
||||
# evicted the other two.)
|
||||
action-exercise:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Construct a scratch history for the monotonic guard
|
||||
# The monotonic guard's input is a DIFF, so its exercise needs
|
||||
# history, not just a file: commit a scratch changelog, mark that
|
||||
# commit as the fixture base, then commit an insert-above edit on
|
||||
# top — a real containment run, not just an action.yml parse. The
|
||||
# base ref is the in-job branch, passed explicitly, because this
|
||||
# job's shallow PR checkout carries no origin/main for the input's
|
||||
# default to resolve (consumers get that via fetch-depth: 0, per
|
||||
# the action's description). Scratch-named file so the real
|
||||
# CHANGELOG.md is never shadowed; the commits live only in this
|
||||
# job's checkout and are never pushed.
|
||||
run: |
|
||||
git config user.name ceremony-ci
|
||||
git config user.email ceremony-ci@users.noreply.github.com
|
||||
printf '# Changelog\n\n## Unreleased\n\n## 0.1.0 — 2026-07-01\n\n- Shipped entry.\n' > CHANGELOG.monotonic.scratch.md
|
||||
git add CHANGELOG.monotonic.scratch.md
|
||||
git commit -m 'fixture: monotonic base'
|
||||
git branch monotonic-fixture-base
|
||||
printf '# Changelog\n\n## Unreleased\n\n- Entry inserted above.\n\n## 0.1.0 — 2026-07-01\n\n- Shipped entry.\n' > CHANGELOG.monotonic.scratch.md
|
||||
git commit -am 'fixture: insert above'
|
||||
- uses: ./actions/changelog-monotonic
|
||||
with:
|
||||
changelog: CHANGELOG.monotonic.scratch.md
|
||||
base-ref: monotonic-fixture-base
|
||||
|
||||
# Exercises actions/docs-sync the way a consumer does (issue #19's
|
||||
# acceptance criterion). Its own job, unlike the exercises above: the
|
||||
# composite reads the CONSUMER's tree at the workspace root, and a
|
||||
# `uses:` step cannot change directory — so the fixture consumer must BE
|
||||
# the workspace root, with ceremony itself checked out to a subdirectory
|
||||
# (that path also serves as the action reference and the --source
|
||||
# override; no ref carrying docs/VENDORED.txt exists to fetch until this
|
||||
# lands, and the exercised bytes should be THIS PR's anyway).
|
||||
docs-sync-exercise:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
path: ceremony-src
|
||||
- name: Construct a fixture consumer at the workspace root
|
||||
# The pin ref is scratch — --source overrides the fetch, but the
|
||||
# pin line itself is still parsed and required (one pin governs
|
||||
# machinery and doctrine; a consumer without one has nothing for
|
||||
# the mirror to be verified against).
|
||||
run: |
|
||||
mkdir -p .github/workflows
|
||||
printf '%s\n' \
|
||||
'name: release' \
|
||||
'on:' \
|
||||
' push:' \
|
||||
' branches: [main]' \
|
||||
'jobs:' \
|
||||
' release:' \
|
||||
' uses: heavy-duty/ceremony/.github/workflows/release.yml@0.0.0-fixture' \
|
||||
> .github/workflows/release.yml
|
||||
- name: Bootstrap the mirror (--fix)
|
||||
uses: ./ceremony-src/actions/docs-sync
|
||||
with:
|
||||
mode: fix
|
||||
source: ceremony-src
|
||||
- name: Verify the mirror (--check, the mode consumers run)
|
||||
uses: ./ceremony-src/actions/docs-sync
|
||||
with:
|
||||
source: ceremony-src
|
||||
|
|
|
|||
149
.github/workflows/labels.yml
vendored
149
.github/workflows/labels.yml
vendored
|
|
@ -1,149 +0,0 @@
|
|||
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 — which is why the caller's cron is load-bearing, not a
|
||||
# safety net (#199 relaxed it from */15 to hourly, but did NOT drop it). The
|
||||
# cron is the sweep's only discovery path for every transition no subscribed
|
||||
# event carries: a verdict landing, blocker:ci-red set/cleared, a
|
||||
# blocker:conflict when another PR merges under this one, and the time-based
|
||||
# stale / 48h claim-reclaim. Where an event IS subscribed the wake is direct —
|
||||
# the handoff sets state:needs-human and the caller's `labeled` event confirms
|
||||
# or corrects that optimistic write within seconds.
|
||||
#
|
||||
# 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.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 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 }}
|
||||
211
.github/workflows/release-exercise.yml
vendored
211
.github/workflows/release-exercise.yml
vendored
|
|
@ -1,211 +0,0 @@
|
|||
name: release exercise
|
||||
# The scratch caller (issue #9's acceptance criterion): dry wiring only —
|
||||
# nothing is ever tagged, published, or bumped. Three jobs, three claims:
|
||||
#
|
||||
# * `call` — a workflow_call `uses:` validates and parses the called file
|
||||
# when the run starts, so a green run proves release.yml parses and its
|
||||
# input contract wires. Both jobs inside it are gated on the push event
|
||||
# (rig's form), so a non-push caller — dispatch here, pull_request via
|
||||
# ci.yml — skips them by design.
|
||||
# * `step-replay` — the merge door's early step sequence executed for
|
||||
# real (round 1's blocking catch: `call` proves the parse but runs
|
||||
# zero steps): the two-checkout dance including the `path:
|
||||
# .ceremony-src` checkout, both branches of the self-consumption
|
||||
# bypass, the CEREMONY_DIR / RELEASE_ASSETS_DIR wiring, then facts →
|
||||
# decide → notes through the real $GITHUB_OUTPUT step plumbing — all
|
||||
# against a constructed fixture tree with a stubbed gh, so a wrong
|
||||
# `path:`, an inverted bypass, or a CEREMONY_DIR pointing nowhere fails
|
||||
# HERE, not in a consumer's release. The steps are release.yml's own,
|
||||
# copied 1:1 where the context allows; where it cannot, the deviation
|
||||
# is commented at the step.
|
||||
# * `fixture-chain` — the same script chain offline, via the contract
|
||||
# test CI runs on every PR (test/release-chain.test.sh).
|
||||
#
|
||||
# Runs on workflow_dispatch, and on every PR via ci.yml's workflow_call
|
||||
# (PR-only there, on purpose — see ci.yml's gate comment). The live doors
|
||||
# remain the stated honest gap, closed by #11 (ceremony's own 0.1.0
|
||||
# release calls this exact workflow by local path) and the #13 pilot's
|
||||
# rehearsal.
|
||||
on:
|
||||
workflow_dispatch:
|
||||
workflow_call:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
call:
|
||||
# Dry: version-source exercises the input contract; the doors stay shut
|
||||
# on a non-push event. The real caller stub — triggers, permissions,
|
||||
# the pinned ref — lives in release.yml's header and docs/CONSUMERS.md.
|
||||
uses: ./.github/workflows/release.yml
|
||||
with:
|
||||
version-source: file
|
||||
|
||||
step-replay:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
# release.yml keys its bypass on `github.repository ==
|
||||
# 'heavy-duty/ceremony'`; the matrix stands in for that condition so
|
||||
# BOTH branches run from this one repo — the dogfood repo can never
|
||||
# take the consumer branch for real, and vice versa.
|
||||
shape: [dogfood, consumer]
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# release.yml's first checkout, verbatim: the pushed head and its
|
||||
# first parent.
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 2
|
||||
- uses: actions/checkout@v4
|
||||
# release.yml's second checkout — the consumer path's pinned
|
||||
# ceremony source, same `path:` wiring. One forced deviation: the
|
||||
# ref is github.sha, not CEREMONY_SELF_REF — the pinned tag cannot
|
||||
# exist before the first release (the exact deadlock the bypass
|
||||
# solves), and the pin's VALUE is already guarded by
|
||||
# .github/scripts/self-ref-check.sh in CI. What this step proves is
|
||||
# the wiring: the checkout lands in .ceremony-src and every later
|
||||
# step resolves libs through it.
|
||||
if: matrix.shape == 'consumer'
|
||||
with:
|
||||
repository: ${{ github.repository }}
|
||||
ref: ${{ github.sha }}
|
||||
path: .ceremony-src
|
||||
- name: wire CEREMONY_DIR and the assets dir
|
||||
env:
|
||||
SHAPE: ${{ matrix.shape }}
|
||||
# release.yml's wiring step with the matrix standing in for the
|
||||
# GITHUB_REPOSITORY test (comment on the matrix above).
|
||||
run: |
|
||||
if [ "$SHAPE" = "dogfood" ]; then
|
||||
echo "CEREMONY_DIR=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "CEREMONY_DIR=$GITHUB_WORKSPACE/.ceremony-src" >> "$GITHUB_ENV"
|
||||
fi
|
||||
mkdir -p "$RUNNER_TEMP/release-assets"
|
||||
echo "RELEASE_ASSETS_DIR=$RUNNER_TEMP/release-assets" >> "$GITHUB_ENV"
|
||||
- name: construct the fixture consumer tree and the gh stub
|
||||
# The fixture release.yml's steps run against (below): a base at
|
||||
# 0.6.9-dev armed the fragment way (#112) — changelog.d/ with its
|
||||
# marker and one fragment — then the ceremony merge: VERSION bumped
|
||||
# bare and the section stamped by the REAL assembler, the command
|
||||
# the real ceremony PR runs by hand (#112 D12), so the exercise
|
||||
# consumes the tool end to end instead of hand-writing its output.
|
||||
# Same shape as test/release-chain.test.sh. The gh stub answers the
|
||||
# one API fact the ceremony path consults (the merged
|
||||
# release-labeled PR) so nothing here talks to GitHub.
|
||||
run: |
|
||||
mkdir -p "$RUNNER_TEMP/stub"
|
||||
cat > "$RUNNER_TEMP/stub/gh" <<'EOF'
|
||||
#!/usr/bin/env bash
|
||||
if [ "$1" = api ]; then echo true; exit 0; fi
|
||||
echo "gh stub: unexpected call: gh $*" >&2
|
||||
exit 97
|
||||
EOF
|
||||
chmod +x "$RUNNER_TEMP/stub/gh"
|
||||
echo "$RUNNER_TEMP/stub" >> "$GITHUB_PATH"
|
||||
git init -q "$RUNNER_TEMP/fixture"
|
||||
cd "$RUNNER_TEMP/fixture"
|
||||
git config user.email fixture@example.invalid
|
||||
git config user.name fixture
|
||||
printf '0.6.9-dev\n' > VERSION
|
||||
cat > CHANGELOG.md <<'EOF'
|
||||
# Changelog
|
||||
|
||||
## 0.6.8 — 2026-07-01
|
||||
|
||||
- An older entry.
|
||||
EOF
|
||||
mkdir changelog.d
|
||||
printf '# changelog.d/ — assembled at release (heavy-duty/ceremony#112); the marker keeps the directory tracked.\n' > changelog.d/README.md
|
||||
printf -- '- The entry this release ships.\n' > changelog.d/42.md
|
||||
git add VERSION CHANGELOG.md changelog.d
|
||||
git commit -qm "base"
|
||||
printf '0.7.0\n' > VERSION
|
||||
bash "$CEREMONY_DIR/bin/changelog-assemble" 0.7.0 2026-07-21
|
||||
git add -A
|
||||
git commit -qm "release: 0.7.0"
|
||||
echo "FIXTURE_SHA=$(git rev-parse HEAD)" >> "$GITHUB_ENV"
|
||||
- name: gather the facts — version, base version, released, labeled
|
||||
id: facts
|
||||
working-directory: ${{ runner.temp }}/fixture
|
||||
env:
|
||||
MERGE_SHA: ${{ env.FIXTURE_SHA }}
|
||||
# Empty exercises the branch-create fallback: facts.sh must fall
|
||||
# back to the merge commit's first parent (#1 constraint 10).
|
||||
EVENT_BEFORE: ""
|
||||
VERSION_SOURCE: file
|
||||
# release.yml's step verbatim — same invocation, same
|
||||
# $GITHUB_OUTPUT plumbing — cwd'd at the fixture instead of the
|
||||
# workspace (the one thing a replay cannot inherit).
|
||||
run: bash "$CEREMONY_DIR/lib/facts.sh" >> "$GITHUB_OUTPUT"
|
||||
- name: 'decide: ceremony, or release-flow work under the label?'
|
||||
id: decide
|
||||
env:
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
BASE_VER: ${{ steps.facts.outputs.base_ver }}
|
||||
RELEASED: ${{ steps.facts.outputs.released }}
|
||||
LABELED: ${{ steps.facts.outputs.labeled }}
|
||||
# release.yml's step verbatim.
|
||||
run: |
|
||||
out="$(bash "$CEREMONY_DIR/lib/decide.sh")"
|
||||
printf '%s\n' "$out"
|
||||
printf '%s\n' "$out" | grep '^ceremony=' >> "$GITHUB_OUTPUT"
|
||||
- name: release notes — the version's own changelog section
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
working-directory: ${{ runner.temp }}/fixture
|
||||
env:
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
# release.yml's step verbatim, cwd'd at the fixture.
|
||||
run: |
|
||||
# shellcheck source=/dev/null
|
||||
. "$CEREMONY_DIR/lib/changelog.sh"
|
||||
if ! diagnosis="$(changelog_section_problem CHANGELOG.md "$VER")"; then
|
||||
echo "CHANGELOG.md has no '## $VER' section at the merge commit — the ceremony PR must stamp it; refusing to publish an empty release" >&2
|
||||
printf '%s\n' "$diagnosis" >&2
|
||||
exit 1
|
||||
fi
|
||||
changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md"
|
||||
cat "$RUNNER_TEMP/notes.md"
|
||||
- name: an entry-less stamped fixture is refused by the notes predicate
|
||||
working-directory: ${{ runner.temp }}/fixture
|
||||
env:
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
run: |
|
||||
cp CHANGELOG.md "$RUNNER_TEMP/CHANGELOG.good.md"
|
||||
awk -v ver="$VER" '
|
||||
/^## / { in_section = ($2 == ver) }
|
||||
in_section && /^[[:space:]]*[-*][[:space:]]/ { next }
|
||||
{ print }
|
||||
' "$RUNNER_TEMP/CHANGELOG.good.md" > CHANGELOG.md
|
||||
# shellcheck source=/dev/null
|
||||
. "$CEREMONY_DIR/lib/changelog.sh"
|
||||
if diagnosis="$(changelog_section_problem CHANGELOG.md "$VER")"; then
|
||||
echo "entry-less stamped section unexpectedly passed" >&2
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$diagnosis" | grep -F "section '$VER' has no entries"
|
||||
cp "$RUNNER_TEMP/CHANGELOG.good.md" CHANGELOG.md
|
||||
- name: the chain must land where the fixture says it lands
|
||||
env:
|
||||
CEREMONY: ${{ steps.decide.outputs.ceremony }}
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
BASE_VER: ${{ steps.facts.outputs.base_ver }}
|
||||
# Not a release.yml step — the replay's own assertion that the real
|
||||
# steps produced the facts and verdict the fixture encodes, so a
|
||||
# green job means the wiring carried real values, not empties.
|
||||
run: |
|
||||
[ "$VER" = "0.7.0" ] || { echo "ver: got '$VER'" >&2; exit 1; }
|
||||
[ "$BASE_VER" = "0.6.9-dev" ] || { echo "base_ver: got '$BASE_VER'" >&2; exit 1; }
|
||||
[ "$CEREMONY" = "yes" ] || { echo "ceremony: got '$CEREMONY'" >&2; exit 1; }
|
||||
grep -q "The entry this release ships" "$RUNNER_TEMP/notes.md" \
|
||||
|| { echo "notes.md missing the fixture's entry" >&2; exit 1; }
|
||||
echo "step-replay ($CEREMONY_DIR): facts -> decide -> notes carried real values end to end"
|
||||
|
||||
fixture-chain:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: the merge door's script chain against a fixture ceremony
|
||||
run: bash test/release-chain.test.sh
|
||||
371
.github/workflows/release.yml
vendored
371
.github/workflows/release.yml
vendored
|
|
@ -1,371 +0,0 @@
|
|||
name: release
|
||||
# THE reusable release workflow — two doors into one act, implemented once
|
||||
# for the whole family (issue #9; lineage box#83/#96 · rig#32/#47 ·
|
||||
# cast#96/#111 — this essay is condensed from those three sources, and every
|
||||
# rule in it was bought with an incident).
|
||||
#
|
||||
# ## The two doors
|
||||
#
|
||||
# * The MERGE door: a release is a PR — `release: X.Y.Z`, carrying the
|
||||
# hand-set `release` label, bumping the version from X.Y.Z-dev to bare
|
||||
# X.Y.Z and stamping the changelog — and MERGING it is the ship decision.
|
||||
# The label is the intent, the version transition is the interlock: the
|
||||
# 5-state table (lib/decide.sh, issue #8) tells a ceremony apart from
|
||||
# release-flow work under the same label, turns every legitimate
|
||||
# non-ceremony into a green NOTICE no-op, and refuses every half-ceremony
|
||||
# loudly, creating nothing. The job then tags the merge commit via the API
|
||||
# and publishes in the SAME job, on purpose: a GITHUB_TOKEN-created tag
|
||||
# fires no workflows (GitHub's anti-recursion), so that tag can never
|
||||
# re-enter the tag door below and double-publish — this job is the
|
||||
# release's only chance to publish, and the nothing-exists assert covers a
|
||||
# manual tag racing the merge. Afterwards the job re-arms main itself:
|
||||
# bump to X.Y.(Z+1)-dev, pushed directly with the job's token (fires
|
||||
# nothing), falling back to a labeled PR if branch protection refuses —
|
||||
# loudly, never leaving main armed to impersonate the release.
|
||||
#
|
||||
# * The TAG door: a bare X.Y.Z tag push (no 'v' prefix — box's 0.6.0 set
|
||||
# the scheme) is the documented manual fallback and backfill. The tag must
|
||||
# name the tree's own version; a mismatch fails loudly and creates
|
||||
# nothing. No decide and no label check — the tag is the operator's
|
||||
# explicit act — and no bump: the fallback does not rewrite main (cast's
|
||||
# precedent).
|
||||
#
|
||||
# Both doors publish the release body from the version's own CHANGELOG.md
|
||||
# section (lib/changelog.sh — the one canonical extractor): the curated
|
||||
# prose, never the generated PR list. Assets come only from the consumer's
|
||||
# optional artifact hook (below); with no hook, GitHub's source tarball for
|
||||
# the tag IS the package (box, rig).
|
||||
#
|
||||
# ## The caller contract
|
||||
#
|
||||
# This is the consumer's ENTIRE release.yml (also in docs/CONSUMERS.md).
|
||||
# Triggers and permissions MUST live in the caller — a called workflow
|
||||
# cannot define them:
|
||||
#
|
||||
# name: release
|
||||
# on:
|
||||
# # ONE push key, both filters — YAML maps are last-key-wins; a second
|
||||
# # sibling `push:` silently replaces the first and kills a door (rig's
|
||||
# # review catch: the tag fallback had stopped triggering).
|
||||
# push:
|
||||
# tags: ["**"] # every tag — a wrong tag must FAIL the assert
|
||||
# # loudly below, never be skipped by a shape
|
||||
# # filter that didn't match
|
||||
# branches: [main]
|
||||
# permissions:
|
||||
# contents: write # tag ref create + release create + the bump push
|
||||
# pull-requests: write # the label read; the bump-fallback `gh pr create`
|
||||
# issues: write # --label on that fallback PR rides the issues API
|
||||
# jobs:
|
||||
# release:
|
||||
# uses: heavy-duty/ceremony/.github/workflows/release.yml@<pinned-tag>
|
||||
# with:
|
||||
# version-source: file # or: package-json
|
||||
#
|
||||
# The called workflow runs in the CALLER's context: the caller's event
|
||||
# payload (github.ref / github.sha / github.event.before), the caller's
|
||||
# GITHUB_TOKEN, the caller's permission grant. The doors split on the pushed
|
||||
# ref exactly as the sources did, and the anti-recursion property is
|
||||
# unchanged: tags and pushes created with GITHUB_TOKEN fire no workflows.
|
||||
# The merge door MUST keep riding `push` to main, never `pull_request`: a
|
||||
# pull_request run from a public FORK gets a READ-ONLY token that
|
||||
# `permissions:` cannot raise (box#97) — and every ceremony PR in this org
|
||||
# is cross-repo from a bot fork — so the asserts would pass and the tag
|
||||
# create would 403, red on main, every release.
|
||||
#
|
||||
# ## The self-ref pin (#1 D3)
|
||||
#
|
||||
# A called workflow file arrives alone; it does not bring its repository.
|
||||
# So each door checks out heavy-duty/ceremony at the literal pinned
|
||||
# CEREMONY_SELF_REF below (into .ceremony-src, inside the workspace) to get
|
||||
# lib/ at run time — except on the dogfood path: when the caller IS
|
||||
# heavy-duty/ceremony, the workspace already holds this repo at the merge
|
||||
# commit, libs included, and fetching tag X.Y.Z from the very run that
|
||||
# creates it would deadlock (#11). Every script call goes through
|
||||
# CEREMONY_DIR, so the bypass is one `if:` plus one env line.
|
||||
#
|
||||
# ## The artifact hook (#1 D4)
|
||||
#
|
||||
# If the consumer carries .github/actions/release-artifact/action.yml, both
|
||||
# doors invoke it — after the tag exists, before `gh release create` — with
|
||||
# `version` as input and RELEASE_ASSETS_DIR exported; every file the hook
|
||||
# drops there is uploaded as a release asset. Exit non-zero to abort the
|
||||
# release. No hook → no assets.
|
||||
#
|
||||
# ## What is honestly untested
|
||||
#
|
||||
# Every decision this workflow takes lives in a tested script: version state
|
||||
# (lib/version.sh), the 5-state verdict (lib/decide.sh), fact gathering
|
||||
# (lib/facts.sh), notes extraction (lib/changelog.sh), and the facts →
|
||||
# decide → notes chain is rehearsed end-to-end against fixtures
|
||||
# (test/release-chain.test.sh). The merge door's early step sequence — both
|
||||
# checkout shapes, both branches of the self-consumption bypass, the
|
||||
# CEREMONY_DIR wiring, and the facts → decide → notes steps with their real
|
||||
# $GITHUB_OUTPUT plumbing — is executed against a fixture by
|
||||
# release-exercise.yml's step-replay job, on every PR via ci.yml. What
|
||||
# remains, honestly untested until it runs live: the doors themselves —
|
||||
# door gating on a real push event, tag create, publish, and bump. That gap
|
||||
# is closed by #11 (ceremony's own 0.1.0 release runs this exact workflow
|
||||
# via a local-path call) and by the #13 pilot's rehearsal.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
version-source:
|
||||
description: >-
|
||||
Where the tree's version lives: "file" (a VERSION file — box, rig,
|
||||
incubator) or "package-json" (the version field, lockfile kept in
|
||||
sync on bump — cast)
|
||||
type: string
|
||||
required: false
|
||||
default: file
|
||||
|
||||
env:
|
||||
# A called workflow arrives without its repository. This literal pin is
|
||||
# stamped by ceremony's own release PR to the version being released —
|
||||
# one more line in the same ritual as stamping the changelog (#11) — and
|
||||
# .github/scripts/self-ref-check.sh fails ceremony's own CI when it is
|
||||
# stale: a stale pin dies here, not in a consumer's release. checkout's
|
||||
# `ref:` accepts ${{ env }}; `uses:` strings do not — which is why the
|
||||
# shared logic arrives as script files via checkout, not as inner `uses:`
|
||||
# references.
|
||||
CEREMONY_SELF_REF: "0.4.0"
|
||||
VERSION_SOURCE: ${{ inputs.version-source }}
|
||||
|
||||
jobs:
|
||||
release-on-merge:
|
||||
# The merge door. Gated on the push EVENT as well as the ref (rig's
|
||||
# form): a workflow_dispatch of a caller sitting on main — this repo's
|
||||
# own release-exercise.yml — must stay dry wiring, never a live door.
|
||||
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
# The pushed head is what ships; its first parent (fetch-depth: 2)
|
||||
# is main the instant before the PR landed, which the version
|
||||
# transition is measured against (lib/facts.sh adds the
|
||||
# belt-and-braces fetch of event.before — cast's precedent).
|
||||
ref: ${{ github.sha }}
|
||||
fetch-depth: 2
|
||||
- uses: actions/checkout@v4
|
||||
# The self-consumption bypass (load-bearing — without it, ceremony's
|
||||
# own release deadlocks): on the dogfood path the workspace IS this
|
||||
# repo at the merge commit, libs included, so nothing is fetched —
|
||||
# the 0.1.0 run would otherwise check out tag 0.1.0, which is
|
||||
# created only AFTER that very run succeeds (#11).
|
||||
if: github.repository != 'heavy-duty/ceremony'
|
||||
with:
|
||||
repository: heavy-duty/ceremony
|
||||
ref: ${{ env.CEREMONY_SELF_REF }}
|
||||
path: .ceremony-src
|
||||
- name: wire CEREMONY_DIR and the assets dir
|
||||
run: |
|
||||
if [ "$GITHUB_REPOSITORY" = "heavy-duty/ceremony" ]; then
|
||||
echo "CEREMONY_DIR=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "CEREMONY_DIR=$GITHUB_WORKSPACE/.ceremony-src" >> "$GITHUB_ENV"
|
||||
fi
|
||||
mkdir -p "$RUNNER_TEMP/release-assets"
|
||||
echo "RELEASE_ASSETS_DIR=$RUNNER_TEMP/release-assets" >> "$GITHUB_ENV"
|
||||
- name: gather the facts — version, base version, released, labeled
|
||||
id: facts
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
MERGE_SHA: ${{ github.sha }}
|
||||
EVENT_BEFORE: ${{ github.event.before }}
|
||||
# Facts on stdout in $GITHUB_OUTPUT form, diagnostics on stderr;
|
||||
# the API facts are gathered only in the states that consult them.
|
||||
run: bash "$CEREMONY_DIR/lib/facts.sh" >> "$GITHUB_OUTPUT"
|
||||
- name: 'decide: ceremony, or release-flow work under the label?'
|
||||
id: decide
|
||||
env:
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
BASE_VER: ${{ steps.facts.outputs.base_ver }}
|
||||
RELEASED: ${{ steps.facts.outputs.released }}
|
||||
LABELED: ${{ steps.facts.outputs.labeled }}
|
||||
# The 5-state table lives in lib/decide.sh (issue #8) — pure, so it
|
||||
# is contract-tested offline. `ceremony=no` ends this job green (the
|
||||
# NOTICE already printed); a refusal is red with nothing created.
|
||||
run: |
|
||||
out="$(bash "$CEREMONY_DIR/lib/decide.sh")"
|
||||
printf '%s\n' "$out"
|
||||
printf '%s\n' "$out" | grep '^ceremony=' >> "$GITHUB_OUTPUT"
|
||||
- name: release notes — the version's own changelog section
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
run: |
|
||||
# shellcheck source=/dev/null
|
||||
. "$CEREMONY_DIR/lib/changelog.sh"
|
||||
if ! diagnosis="$(changelog_section_problem CHANGELOG.md "$VER")"; then
|
||||
echo "CHANGELOG.md has no '## $VER' section at the merge commit — the ceremony PR must stamp it; refusing to publish an empty release" >&2
|
||||
printf '%s\n' "$diagnosis" >&2
|
||||
exit 1
|
||||
fi
|
||||
changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md"
|
||||
cat "$RUNNER_TEMP/notes.md"
|
||||
- name: nothing may exist yet — no tag, no release (re-runs refuse loudly)
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
# What makes a re-run of a completed ceremony refuse instead of
|
||||
# clobber, and what catches a manual tag racing the merge.
|
||||
run: |
|
||||
if git ls-remote --exit-code origin "refs/tags/$VER" >/dev/null 2>&1; then
|
||||
echo "tag '$VER' already exists — this release already happened, or a manual tag won the race; refusing to re-release, creating nothing." >&2
|
||||
exit 1
|
||||
fi
|
||||
if gh release view "$VER" -R "$GITHUB_REPOSITORY" --json name >/dev/null 2>&1; then
|
||||
echo "release '$VER' already exists — refusing to re-release, creating nothing." >&2
|
||||
exit 1
|
||||
fi
|
||||
- name: tag the merge commit — same job as the publish, on purpose
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
MERGE_SHA: ${{ github.sha }}
|
||||
# A GITHUB_TOKEN-created tag triggers nothing (anti-recursion), so
|
||||
# the tag door cannot double-fire off this tag — and this job is
|
||||
# the only chance to publish (the sources' central comment).
|
||||
run: |
|
||||
gh api "repos/$GITHUB_REPOSITORY/git/refs" \
|
||||
-f "ref=refs/tags/$VER" -f "sha=$MERGE_SHA"
|
||||
- name: artifact hook — the consumer's own release-artifact action
|
||||
# Runs after the tag exists, before the publish (#1 D4). The local
|
||||
# path resolves in the consumer checkout at the workspace root —
|
||||
# legal in a called workflow because the action is on disk. Hook
|
||||
# contract: drop finished files into $RELEASE_ASSETS_DIR; exit
|
||||
# non-zero to abort the release (docs/CONSUMERS.md).
|
||||
if: steps.decide.outputs.ceremony == 'yes' && hashFiles('.github/actions/release-artifact/action.yml') != ''
|
||||
uses: ./.github/actions/release-artifact
|
||||
with:
|
||||
version: ${{ steps.facts.outputs.ver }}
|
||||
- name: publish the release
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
run: |
|
||||
assets=()
|
||||
for f in "$RELEASE_ASSETS_DIR"/*; do
|
||||
if [ -e "$f" ]; then assets+=("$f"); fi
|
||||
done
|
||||
gh release create "$VER" --verify-tag --title "$VER" \
|
||||
--notes-file "$RUNNER_TEMP/notes.md" -R "$GITHUB_REPOSITORY" \
|
||||
"${assets[@]}"
|
||||
# The post-release bump, folded into the release act (the sources'
|
||||
# operator decision: a mechanical one-liner deserves no PR of its
|
||||
# own). X.Y.(Z+1)-dev is arithmetic, not judgment (version_next_dev
|
||||
# refuses anything but bare X.Y.Z). A GITHUB_TOKEN push fires no
|
||||
# workflows (anti-recursion), so the bump triggers neither this door
|
||||
# nor a red run; should branch protection refuse the direct push, the
|
||||
# step opens the bump PR itself and says so, loudly, instead of
|
||||
# leaving main armed to impersonate the release.
|
||||
- name: bump main to the next -dev — the release re-arms main itself
|
||||
if: steps.decide.outputs.ceremony == 'yes'
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VER: ${{ steps.facts.outputs.ver }}
|
||||
run: |
|
||||
# shellcheck source=/dev/null
|
||||
. "$CEREMONY_DIR/lib/version.sh"
|
||||
next="$(version_next_dev "$VER")"
|
||||
git config user.name "github-actions[bot]"
|
||||
git config user.email "github-actions[bot]@users.noreply.github.com"
|
||||
# main may have moved since the merge; release+1 lands on the
|
||||
# newer head — the intended arithmetic either way (cast's
|
||||
# comment).
|
||||
git fetch origin main
|
||||
git checkout -B main origin/main
|
||||
version_write "$VERSION_SOURCE" "$next"
|
||||
# NEVER `git add -A` here: .ceremony-src sits UNTRACKED in this
|
||||
# workspace on the consumer path, and -A would commit the whole
|
||||
# ceremony checkout into the consumer's main. Exactly the files
|
||||
# the bump wrote, nothing else.
|
||||
case "$VERSION_SOURCE" in
|
||||
file) git add VERSION ;;
|
||||
package-json) git add package.json package-lock.json ;;
|
||||
esac
|
||||
git commit -m "chore: bump main to $next — a dev install must not impersonate $VER"
|
||||
if ! git push origin main; then
|
||||
echo "direct push refused (branch protection?) — opening the bump PR instead" >&2
|
||||
git checkout -b "chore/bump-$next"
|
||||
git push origin "chore/bump-$next"
|
||||
gh pr create -R "$GITHUB_REPOSITORY" --head "chore/bump-$next" \
|
||||
--title "chore: bump main to $next" \
|
||||
--body "The post-release re-arm, opened by release.yml because the direct push was refused. One version bump, nothing else — never leave main armed to impersonate $VER." \
|
||||
--label release
|
||||
fi
|
||||
|
||||
release-on-tag:
|
||||
# The tag door — the manual fallback and backfill. The tag is the
|
||||
# operator's explicit act: no decide, no label check — and no bump
|
||||
# (cast's precedent: the fallback does not rewrite main). Event-gated
|
||||
# like the merge door: dispatch runs stay dry.
|
||||
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/')
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v4
|
||||
# The self-consumption bypass — see the merge door's twin step.
|
||||
if: github.repository != 'heavy-duty/ceremony'
|
||||
with:
|
||||
repository: heavy-duty/ceremony
|
||||
ref: ${{ env.CEREMONY_SELF_REF }}
|
||||
path: .ceremony-src
|
||||
- name: wire CEREMONY_DIR and the assets dir
|
||||
run: |
|
||||
if [ "$GITHUB_REPOSITORY" = "heavy-duty/ceremony" ]; then
|
||||
echo "CEREMONY_DIR=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"
|
||||
else
|
||||
echo "CEREMONY_DIR=$GITHUB_WORKSPACE/.ceremony-src" >> "$GITHUB_ENV"
|
||||
fi
|
||||
mkdir -p "$RUNNER_TEMP/release-assets"
|
||||
echo "RELEASE_ASSETS_DIR=$RUNNER_TEMP/release-assets" >> "$GITHUB_ENV"
|
||||
- name: the tag must name the tree's own version
|
||||
id: assert
|
||||
run: |
|
||||
# shellcheck source=/dev/null
|
||||
. "$CEREMONY_DIR/lib/version.sh"
|
||||
ver="$(version_read "$VERSION_SOURCE")"
|
||||
if [ "$GITHUB_REF_NAME" != "$ver" ]; then
|
||||
echo "tag '$GITHUB_REF_NAME' does not match the tree's version '$ver' — creating nothing." >&2
|
||||
echo "A release is a PR, then a tag: the release PR bumps the version and stamps the changelog; the tag goes on its MERGE commit. Delete this tag and re-tag the right commit." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "ver=$ver" >> "$GITHUB_OUTPUT"
|
||||
- name: release notes — the version's own changelog section
|
||||
env:
|
||||
VER: ${{ steps.assert.outputs.ver }}
|
||||
run: |
|
||||
# shellcheck source=/dev/null
|
||||
. "$CEREMONY_DIR/lib/changelog.sh"
|
||||
if ! diagnosis="$(changelog_section_problem CHANGELOG.md "$VER")"; then
|
||||
echo "CHANGELOG.md has no '## $VER' section — run changelog-assemble in the release PR before tagging; refusing to publish an empty release" >&2
|
||||
printf '%s\n' "$diagnosis" >&2
|
||||
exit 1
|
||||
fi
|
||||
changelog_section CHANGELOG.md "$VER" > "$RUNNER_TEMP/notes.md"
|
||||
cat "$RUNNER_TEMP/notes.md"
|
||||
- name: artifact hook — the consumer's own release-artifact action
|
||||
# After the tag exists (it fired this door), before the publish —
|
||||
# the same contract as the merge door's twin step.
|
||||
if: hashFiles('.github/actions/release-artifact/action.yml') != ''
|
||||
uses: ./.github/actions/release-artifact
|
||||
with:
|
||||
version: ${{ steps.assert.outputs.ver }}
|
||||
- name: publish the release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
VER: ${{ steps.assert.outputs.ver }}
|
||||
run: |
|
||||
assets=()
|
||||
for f in "$RELEASE_ASSETS_DIR"/*; do
|
||||
if [ -e "$f" ]; then assets+=("$f"); fi
|
||||
done
|
||||
gh release create "$VER" --verify-tag --title "$VER" \
|
||||
--notes-file "$RUNNER_TEMP/notes.md" -R "$GITHUB_REPOSITORY" \
|
||||
"${assets[@]}"
|
||||
55
.github/workflows/self-labels.yml
vendored
55
.github/workflows/self-labels.yml
vendored
|
|
@ -1,55 +0,0 @@
|
|||
name: labels
|
||||
# Ceremony's own caller for the labels automation — the dogfood of issue
|
||||
# #11, wearing the same local-`uses:` deviation as self-release.yml and the
|
||||
# same warning: consumers must NEVER copy the local form (it rides main,
|
||||
# unpinned — correct only for the repo that IS the source). Consumers write:
|
||||
# uses: heavy-duty/ceremony/.github/workflows/labels.yml@<pinned-tag>
|
||||
on:
|
||||
# The consumer owns this cadence (#203). Hourly is the recommended default
|
||||
# when no other engine drives board state: the cron is then the sweep's ONLY
|
||||
# wake for four transition classes — a review verdict landing (there is no
|
||||
# pull_request_review trigger here), blocker:ci-red set or cleared (no
|
||||
# check_suite/check_run/workflow_run), a blocker:conflict when ANOTHER PR
|
||||
# merges under this one, and the time-based stale / 48h claim-reclaim. The
|
||||
# events below carry the rest in seconds. Hourly trades ≤1h of latency on
|
||||
# those four while cutting nominal scheduled sweeps from four an hour to one
|
||||
# at GitHub's 1-minute billing floor. Do not delete the cron: it is their
|
||||
# discovery path. If another engine writes some of those transitions, only
|
||||
# the classes with no other writer bound the cadence; relax it only as that
|
||||
# list shrinks.
|
||||
schedule: [{cron: "0 * * * *"}]
|
||||
# A manual full-board sweep, including taxonomy bootstrap on a fresh repo.
|
||||
workflow_dispatch:
|
||||
# Narrowed (#199) to the actions that carry a queue-state change the hourly
|
||||
# cron cannot wait one cadence for — dropping only labeled/unlabeled/assigned/
|
||||
# unassigned, which feed validation and the 48h claim clock (caught within one
|
||||
# cadence) and were the dominant issues-churn source. Kept: `opened` (the
|
||||
# mint→needs-triage check, issueflow's opened-only path), `closed` (the
|
||||
# blocker-closes→ready self-heal, crew#96/#98), `edited` (a body rewrite of the
|
||||
# `Blocked by #N` declaration the sweep parses — issueflow-reconcile.sh:179),
|
||||
# `reopened` (a closed issue re-entering the queue wearing labels derived when
|
||||
# it closed). The must-fail in #199 is exactly "a queue-state transition waits
|
||||
# on the schedule when an event could have carried it", so edited/reopened stay
|
||||
# on events. The PR handoff wake is pull_request_target:labeled, NOT issues, so
|
||||
# this does not touch the handoff.
|
||||
issues:
|
||||
types: [opened, closed, edited, reopened]
|
||||
pull_request_target:
|
||||
# Every PR arrives from a fork, so these carry the head/draft/review facts
|
||||
# the sweep derives state:* from. labeled/unlabeled are the handoff wake —
|
||||
# the author's optimistic state:needs-human write, confirmed or corrected
|
||||
# here in seconds (#11); synchronize re-derives on every push;
|
||||
# review_requested/review_request_removed wake the sweep that clears (or
|
||||
# restores) blocker:unrequested — without them the one event that makes
|
||||
# the label false could not clear it, and a quiet repo wore the red flag
|
||||
# until the advisory cron (#137).
|
||||
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled, review_requested, review_request_removed]
|
||||
permissions:
|
||||
contents: read
|
||||
checks: read # mergeability/check-rollup read for PR state
|
||||
statuses: read # commit-status rollup read for PR state
|
||||
issues: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
labels:
|
||||
uses: ./.github/workflows/labels.yml
|
||||
27
.github/workflows/self-release.yml
vendored
27
.github/workflows/self-release.yml
vendored
|
|
@ -1,27 +0,0 @@
|
|||
name: release
|
||||
# Ceremony's own caller — the dogfood of issue #11. This is the consumer
|
||||
# stub from docs/CONSUMERS.md with ONE deviation, and consumers must NEVER
|
||||
# copy it: `uses:` below is a LOCAL path, so every run executes THIS tree's
|
||||
# release.yml. No pin is the point here — ceremony's own release cannot
|
||||
# check out a tag that the very run creates (#9's self-consumption bypass
|
||||
# is the same fact one layer down) — and would be a bug anywhere else: a
|
||||
# consumer without a pin rides main and eats every unreleased change.
|
||||
# Consumers write:
|
||||
# uses: heavy-duty/ceremony/.github/workflows/release.yml@<pinned-tag>
|
||||
on:
|
||||
# ONE push key, both filters — YAML maps are last-key-wins; a second
|
||||
# sibling `push:` silently replaces the first and kills a door (rig's
|
||||
# review catch).
|
||||
push:
|
||||
tags: ["**"] # every tag — a wrong tag must FAIL the assert loudly,
|
||||
# never be skipped by a shape filter that didn't match
|
||||
branches: [main]
|
||||
permissions:
|
||||
contents: write # tag ref create + release create + the bump push
|
||||
pull-requests: write # decide's label read; the bump-fallback `gh pr create`
|
||||
issues: write # --label on that fallback PR rides the issues API
|
||||
jobs:
|
||||
release:
|
||||
uses: ./.github/workflows/release.yml
|
||||
with:
|
||||
version-source: file
|
||||
Loading…
Reference in a new issue