forked from heavy-duty/ceremony
Merge pull request #44 from claude-bot-andresmgsl/build/11-dogfood
feat: dogfood bootstrap — ceremony adopts its own ceremony
This commit is contained in:
commit
6305827a7d
8 changed files with 217 additions and 26 deletions
55
.github/labeler.yml
vendored
Normal file
55
.github/labeler.yml
vendored
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
# Path → scope:* mapping for the labels workflow's scope job
|
||||
# (actions/labeler@v5; additive only — the reusable workflow keeps
|
||||
# sync-labels off, so a hand-applied scope survives the machine). The scope
|
||||
# taxonomy itself lives in .github/labels.conf; LABELS.md carries the table
|
||||
# these globs implement. Scopes locate, they do not alert — a path that maps
|
||||
# to nothing is fine (labeler is advisory), so these rows chase the big
|
||||
# surfaces, not every file.
|
||||
scope:release-flow:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .github/workflows/release.yml
|
||||
- .github/workflows/self-release.yml
|
||||
- .github/workflows/release-exercise.yml
|
||||
- .github/scripts/self-ref-check.sh
|
||||
- lib/**
|
||||
- bin/**
|
||||
- VERSION
|
||||
- CHANGELOG.md
|
||||
- drills/**
|
||||
- test/decide.test.sh
|
||||
- test/facts.test.sh
|
||||
- test/release-chain.test.sh
|
||||
- test/version.test.sh
|
||||
- test/changelog.test.sh
|
||||
- test/self-ref.test.sh
|
||||
scope:guards:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- actions/changelog-armed/**
|
||||
- actions/changelog-monotonic/**
|
||||
- actions/drill-recorded/**
|
||||
- test/changelog-armed.test.sh
|
||||
- test/changelog-monotonic.test.sh
|
||||
- test/drill-recorded.test.sh
|
||||
scope:labels:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- .github/workflows/labels.yml
|
||||
- .github/workflows/self-labels.yml
|
||||
- .github/labeler.yml
|
||||
- .github/labels.conf
|
||||
- actions/labels-reconcile/**
|
||||
- LABELS.md
|
||||
- test/labels.test.sh
|
||||
- test/labels-reconcile.test.sh
|
||||
scope:docs:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file:
|
||||
- README
|
||||
- docs/**
|
||||
- AGENTS.md
|
||||
- BUILDER.md
|
||||
- REVIEWER.md
|
||||
- TRIAGE.md
|
||||
- CONTRIBUTING.md
|
||||
61
.github/workflows/ci.yml
vendored
61
.github/workflows/ci.yml
vendored
|
|
@ -51,33 +51,42 @@ jobs:
|
|||
if: github.event_name == 'pull_request'
|
||||
uses: ./.github/workflows/release-exercise.yml
|
||||
|
||||
# Exercises the composite actions the way a consumer does — action.yml
|
||||
# resolving, $GITHUB_ACTION_PATH, the relative lib sourcing — which the
|
||||
# test suite, driving the scripts directly, cannot prove (issue #5's
|
||||
# acceptance criterion).
|
||||
# The self-guards (issue #11): this repo eats exactly what it serves. The
|
||||
# three guard actions run against the REAL tree — VERSION, CHANGELOG.md,
|
||||
# drills/ — 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/drill-recorded
|
||||
|
||||
# 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 an armed scratch tree in the workspace
|
||||
# This repo carries no VERSION or CHANGELOG.md of its own (until
|
||||
# #11 dogfoods); the guard reads the workspace, so give it a
|
||||
# transient armed tree there. The changelog is scratch-named so a
|
||||
# future real CHANGELOG.md is never shadowed; VERSION cannot be —
|
||||
# the file backend hardcodes it — so if #11 ever adds a real one,
|
||||
# this write must go.
|
||||
run: |
|
||||
printf '0.0.1-dev\n' > VERSION
|
||||
printf '# Changelog\n\n## Unreleased\n\n- Scratch entry.\n' > CHANGELOG.scratch.md
|
||||
- uses: ./actions/changelog-armed
|
||||
with:
|
||||
changelog: CHANGELOG.scratch.md
|
||||
# The same scratch tree exercises drill-recorded: 0.0.1-dev is a
|
||||
# development tree, so the guard proves its wiring (action.yml,
|
||||
# $GITHUB_ACTION_PATH, the lib sourcing) through the nothing-to-assert
|
||||
# path — the state every consumer PR is in. The bare path is the test
|
||||
# suite's job.
|
||||
- uses: ./actions/drill-recorded
|
||||
- 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
|
||||
|
|
@ -86,9 +95,9 @@ jobs:
|
|||
# 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 for the same
|
||||
# shadowing reason as above; the commits live only in this job's
|
||||
# checkout and are never pushed.
|
||||
# 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
|
||||
|
|
|
|||
21
.github/workflows/labels.yml
vendored
21
.github/workflows/labels.yml
vendored
|
|
@ -63,14 +63,35 @@ jobs:
|
|||
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 }}
|
||||
|
|
|
|||
18
.github/workflows/self-labels.yml
vendored
Normal file
18
.github/workflows/self-labels.yml
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
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:
|
||||
schedule: [{cron: "*/15 * * * *"}] # advisory; the handoff label is the real wake
|
||||
workflow_dispatch: # bootstraps missing labels on a fresh repo
|
||||
pull_request_target:
|
||||
types: [opened, reopened, ready_for_review, converted_to_draft, synchronize, labeled, unlabeled]
|
||||
permissions:
|
||||
contents: read
|
||||
issues: write
|
||||
pull-requests: write
|
||||
jobs:
|
||||
labels:
|
||||
uses: ./.github/workflows/labels.yml
|
||||
27
.github/workflows/self-release.yml
vendored
Normal file
27
.github/workflows/self-release.yml
vendored
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
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
|
||||
17
CHANGELOG.md
Normal file
17
CHANGELOG.md
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# Changelog
|
||||
|
||||
The curated history of the ceremony itself. Each release's section is
|
||||
published verbatim as that release's body (lib/changelog.sh extracts it),
|
||||
so entries say what changed, cite the issue, and stop.
|
||||
|
||||
## Unreleased
|
||||
|
||||
- `lib/version.sh` — one version abstraction, `file` and `package-json` backends (#3).
|
||||
- `lib/changelog.sh` + `bin/changelog-section` — the one canonical changelog-section extractor (#4).
|
||||
- `actions/changelog-armed` — the version-keyed arming guard (#5).
|
||||
- `actions/changelog-monotonic` — shipped release headings are append-only: no deletion, no duplication (#6).
|
||||
- `actions/drill-recorded` — a release tree must carry its drill record (#7).
|
||||
- `lib/decide.sh` — the merge door's five-state decision, pure and exhaustively tested (#8).
|
||||
- `.github/workflows/release.yml` + `lib/facts.sh` — the reusable two-door release workflow (#9).
|
||||
- `.github/workflows/labels.yml` + `actions/labels-reconcile` — label taxonomy bootstrap and PR-state reconciliation (#10).
|
||||
- Ceremony adopts its own ceremony: `VERSION`, this changelog, the drill doctrine, the self-callers, and the self-guards in CI (#11).
|
||||
1
VERSION
Normal file
1
VERSION
Normal file
|
|
@ -0,0 +1 @@
|
|||
0.1.0-dev
|
||||
43
drills/README.md
Normal file
43
drills/README.md
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
# Drills
|
||||
|
||||
What a drill means in this repo: an **end-to-end rehearsal of both doors of
|
||||
the release workflow on a disposable repo**. The contract suite proves every
|
||||
decision offline — facts → decide → notes against fixtures, the merge door's
|
||||
step sequence replayed in release-exercise.yml — but the doors themselves
|
||||
only ever run live: gating on a real push event, the tag create, the
|
||||
publish, the `-dev` re-arm (release.yml's "what is honestly untested"). The
|
||||
drill is where they run live *before* a version rests on them.
|
||||
|
||||
## The rehearsal
|
||||
|
||||
1. Create a scratch **private** repo. It is disposable by design — it gets
|
||||
deleted at the end.
|
||||
2. Install the docs/CONSUMERS.md caller stubs, pinned to the release
|
||||
candidate ref. A branch ref works: refs are static identifiers — the
|
||||
family's own drill doctrine.
|
||||
3. Give it a fixture `VERSION` / `CHANGELOG.md` / `drills/` in the armed
|
||||
state (`X.Y.Z-dev`, `## Unreleased` on top).
|
||||
4. Exercise both doors, one probe at a time:
|
||||
|
||||
1. a merge-door ceremony publishes exactly one release and re-arms main
|
||||
to `-dev`;
|
||||
2. a mislabeled ordinary PR is a green NOTICE no-op;
|
||||
3. a bare-version PR without the `release` label refuses;
|
||||
4. a re-run of the completed ceremony refuses;
|
||||
5. a tag-door release from a manual tag;
|
||||
6. a mismatched tag refuses.
|
||||
|
||||
Every refusal must refuse **creating nothing** — a probe that leaves a
|
||||
tag or a release behind on a refusal path is a failed probe.
|
||||
|
||||
## The record
|
||||
|
||||
One file per version, `drills/X.Y.Z.md` — the shape the siblings use: what
|
||||
was run, where, the result of each probe, failures written down plainly. The
|
||||
record is the evidence; the scratch repo is the evidence's scaffolding and
|
||||
is deleted afterwards.
|
||||
|
||||
`actions/drill-recorded` refuses any bare-version tree whose record is
|
||||
missing or blank. A waived drill is still a record: the file says WAIVED and
|
||||
why — a maintainer's call, visible and reviewable in the release PR's diff,
|
||||
never a silent skip.
|
||||
Loading…
Reference in a new issue