Merge branch 'main' into build/115-changelog-armed-fragment-mode
This commit is contained in:
commit
fc070ea896
13 changed files with 718 additions and 21 deletions
1
.github/workflows/ci.yml
vendored
1
.github/workflows/ci.yml
vendored
|
|
@ -73,6 +73,7 @@ jobs:
|
|||
fetch-depth: 0
|
||||
- uses: ./actions/changelog-armed
|
||||
- uses: ./actions/changelog-monotonic
|
||||
- uses: ./actions/changelog-assembled
|
||||
- uses: ./actions/drill-recorded
|
||||
- uses: ./actions/runner-isolated
|
||||
|
||||
|
|
|
|||
15
BUILDER.md
15
BUILDER.md
|
|
@ -15,7 +15,7 @@ triage bug, and the move is to say so on the issue, not to guess.
|
|||
writing or revising a deliverable — finish or release that work before
|
||||
starting new work. The rule counts build work in flight, not claims: a
|
||||
claim does not consume the slot while it is **parked**, meaning the next
|
||||
move belongs to someone else. Exactly four shapes qualify:
|
||||
move belongs to someone else. Exactly five shapes qualify:
|
||||
1. the issue carries `needs-ruling`, its escalation names a decider, and
|
||||
its `Blocked:` line stops the remaining work;
|
||||
2. the deliverable is in a review round where every outstanding verdict
|
||||
|
|
@ -28,6 +28,10 @@ triage bug, and the move is to say so on the issue, not to guess.
|
|||
4. the deliverable is **handed off** — the round passed, no `blocker:*`
|
||||
stands, and you set `state:needs-human` per Handoff (below). The
|
||||
remaining move is the human's merge.
|
||||
5. the claim is **held by directive** — triage or the operator has told
|
||||
you to stop, the direction names what the hold waits on, and that thing
|
||||
is not yours to move. This is not "waiting for a good moment": somebody
|
||||
else has decided the work must not proceed, and only they end it.
|
||||
Not parked — these are what the rule defends against: waiting on
|
||||
yourself, waiting on CI, or waiting for a good moment. An issue you have
|
||||
simply stopped working on is not parked either — that is abandonment,
|
||||
|
|
@ -63,7 +67,14 @@ triage bug, and the move is to say so on the issue, not to guess.
|
|||
- **Pick up `attention` before anything else.** On your claim, first post a
|
||||
short pickup comment and remove `attention`; the removal is the ack. A
|
||||
demand on a parked claim is usually its unpark, so take the slot back under
|
||||
the existing rule below rather than leaving the demand parked.
|
||||
the existing rule below rather than leaving the demand parked. A demand
|
||||
that *is* the park is different: the pickup comment is the declaration,
|
||||
so one comment does both jobs, and the demand does not take the slot back.
|
||||
- **A directed hold keeps its bookkeeping visible.** The PR carries `blocked`
|
||||
with a comment naming what it waits on; the issue stays `claimed` and
|
||||
carries `attention` until the builder acknowledges it. Nobody unassigns
|
||||
the issue, and the 48-hour reclaim does not fire because the claim has an
|
||||
open PR. Unparking follows the existing rule below.
|
||||
- **Unparking is a claim like any other.** When the wait ends, the parked
|
||||
issue is work again and takes the slot. If you are already active
|
||||
elsewhere, finish or release that work first, and say which you did on
|
||||
|
|
|
|||
|
|
@ -7,10 +7,14 @@ so entries say what changed, cite the issue, and stop.
|
|||
## Unreleased
|
||||
|
||||
- `changelog-armed` — treat `changelog.d/` as the arming, validate every development fragment, and require bare releases to consume the directory into their exact publishable section (#115).
|
||||
- `actions/changelog-assembled` — a release PR's stamped section must be byte-for-byte what the fragments it consumed assemble to, replayed from the merge base; inapplicable trees pass with a NOTICE (#116).
|
||||
- `lib/changelog.sh` + `bin/changelog-assemble` — read the `changelog.d/` fragments, assemble one release section (canonical group order, one shape per repo), and consume exactly what was published (#114).
|
||||
- BUILDER.md — the directed hold is the parked claim's fifth shape, its attention demand is acknowledged in the declaration comment, and its board bookkeeping covers in-flight work; TRIAGE.md no longer excludes it (#113).
|
||||
- BUILDER.md — the handed-off PR is the parked claim's fourth shape, its handoff is its declaration, and shape 2 covers the round awaiting its first verdicts (#109).
|
||||
- `labels-reconcile` — a degraded mergeability/checks read now logs gh's actual stderr (collapsed, bounded) beside the byte-identical counted line, and the blind-sweep warning leads with the observed reason instead of asserting the permissions cause (#101).
|
||||
- `LABELS.md` — drop the vendored scope-table enumeration; the per-repo set lives in `.github/labels.conf` and the repo's own CONTRIBUTING (#104).
|
||||
- Changelog publication — count entries instead of bytes, refuse dangling grouped headings, and seed grouped re-arms with Added/Changed/Fixed (#98).
|
||||
- `labels-reconcile` — warn once per sweep when a repository lacks labels declared by the pinned core taxonomy (#105).
|
||||
- `labels-reconcile` — grant callers private-repo check reads and warn when an entire PR sweep is blind (#95).
|
||||
- `labels-reconcile` — the bootstrap now retires the six GitHub defaults `LABELS.md` publishes as deleted, tolerating both an already-absent label and a refused delete (#93).
|
||||
- `issueflow-reconcile` — a triage-authored issue arrival stands down with exit 0 instead of killing the run before the sweep (#91).
|
||||
|
|
|
|||
|
|
@ -86,7 +86,11 @@ roster is config, not doctrine — swapping a vendor is an edit to this table
|
|||
(and to `panel=` in `.github/labels.conf` once #10 lands), nothing more.
|
||||
|
||||
Each governed repo names its own roster in its CONTRIBUTING; this one is
|
||||
ceremony's.
|
||||
ceremony's. Its `scope:*` set is the same kind of repo-specific fact:
|
||||
ceremony's scopes are defined in [`.github/labels.conf`](.github/labels.conf)
|
||||
— one `name|color|description` row each, with PR path mapping in
|
||||
[`.github/labeler.yml`](.github/labeler.yml). The conf is the set; no prose
|
||||
table repeats it (#104).
|
||||
|
||||
## Code conventions
|
||||
|
||||
|
|
|
|||
25
LABELS.md
25
LABELS.md
|
|
@ -170,15 +170,13 @@ re-arms mentions, but only the writer can declare that a move is owed (#83).
|
|||
## Scope — which surface? (PRs and issues, any number)
|
||||
|
||||
All scopes share one calm color, `#C5DEF5` — scopes locate, states alert. The
|
||||
set is per-repo (`.github/labels.conf`); PRs get theirs from changed paths via
|
||||
actions/labeler, issues get theirs from triage. This repo's set:
|
||||
|
||||
| Label | Covers |
|
||||
|---|---|
|
||||
| `scope:release-flow` | the reusable release workflow, decide, the doors |
|
||||
| `scope:guards` | changelog-armed / changelog-monotonic / drill-recorded |
|
||||
| `scope:labels` | the labels workflow, reconciler, this taxonomy |
|
||||
| `scope:docs` | README doctrine, CONSUMERS.md, the role files |
|
||||
set is per-repo: PRs get theirs from changed paths via actions/labeler, issues
|
||||
get theirs from triage. This file never enumerates a set — it is mirrored
|
||||
byte-identically into every governed repo, and any list it carried would be
|
||||
true in one repo and false in the rest (#104). The set for the repo you are
|
||||
standing in lives in the two places that are true wherever you read them: its
|
||||
`.github/labels.conf` (the definitions, one `name|color|description` row per
|
||||
scope) and its own `CONTRIBUTING.md`, beside the other repo-specific facts.
|
||||
|
||||
## Issue types
|
||||
|
||||
|
|
@ -190,7 +188,8 @@ on a PR would say the same thing twice and drift.
|
|||
|
||||
The labels workflow (issue #10) recomputes PR state statelessly on PR events
|
||||
plus a 15-minute advisory cron, and bootstraps this taxonomy idempotently on
|
||||
manual dispatch. The same workflow reconciles issue-flow labels on issue
|
||||
events and during the scheduled sweep. Default GitHub labels (`duplicate`, `invalid`,
|
||||
`question`, `wontfix`, `help wanted`, `good first issue`) are deleted at
|
||||
bootstrap — a `question` is a discussion, not an issue.
|
||||
manual dispatch. The sweep warns when the core taxonomy declares a label the
|
||||
repository lacks. The same workflow reconciles issue-flow labels on issue
|
||||
events and during the scheduled sweep. Default GitHub labels (`duplicate`,
|
||||
`invalid`, `question`, `wontfix`, `help wanted`, `good first issue`) are
|
||||
deleted at bootstrap — a `question` is a discussion, not an issue.
|
||||
|
|
|
|||
|
|
@ -44,9 +44,9 @@ is the failure this whole flow exists to prevent.
|
|||
blocks something already on the board — an existing issue, or minted work
|
||||
a discussion's ruling gates — set `needs-ruling` on it too, so the board
|
||||
shows where the human's turn is; the issue keeps its queue label.
|
||||
When you direct a builder to hold a claim that has no build work left,
|
||||
say the claim is **parked** and name what it waits on — the directive
|
||||
and the builder's doctrine
|
||||
When you direct a builder to hold a claim, say the claim is **parked**,
|
||||
name what it waits on, and set `attention` so the assignee's ack is visible
|
||||
on the board — the directive and the builder's doctrine
|
||||
([BUILDER.md](BUILDER.md#claiming)) must use one word.
|
||||
Past 24 hours from the current episode's `labeled` event, if the ruling
|
||||
still stands and doubt remains, it is triage's duty to pick the option the
|
||||
|
|
|
|||
53
actions/changelog-assembled/action.yml
Normal file
53
actions/changelog-assembled/action.yml
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
name: Changelog assembled
|
||||
description: >-
|
||||
Assert a release PR's stamped section is exactly what the changelog.d/
|
||||
fragments it consumed assemble to — the fragments as of the MERGE BASE
|
||||
are replayed through the assembler's --check and compared byte-for-byte
|
||||
against the section on HEAD (#116; the fragment flow is #112). Needs the
|
||||
HISTORY: the caller's checkout must use fetch-depth: 0, or the base ref
|
||||
will not resolve and strict mode (the default — CI must never skip)
|
||||
fails red with a message naming that fix. Inapplicable trees — a -dev
|
||||
tree, a legacy repo with no fragments directory at the merge base — pass
|
||||
with a NOTICE, never a silent skip.
|
||||
inputs:
|
||||
base-ref:
|
||||
description: >-
|
||||
The ref the fragment set is read relative to. The default resolves
|
||||
the event's base branch — the PR's target on pull_request, the
|
||||
pushed branch itself on push (where the merge base IS HEAD and the
|
||||
check is vacuous by construction, named honestly in the log).
|
||||
required: false
|
||||
default: origin/${{ github.base_ref || github.ref_name }}
|
||||
changelog:
|
||||
description: Path to the changelog, relative to the workspace
|
||||
required: false
|
||||
default: CHANGELOG.md
|
||||
fragments-dir:
|
||||
description: Path to the fragments directory, relative to the workspace
|
||||
required: false
|
||||
default: changelog.d
|
||||
version-source:
|
||||
description: Where the tree's version lives ("file" or "package-json")
|
||||
required: false
|
||||
default: file
|
||||
strict:
|
||||
description: >-
|
||||
"1" (the default) makes an unresolvable base ref a hard failure
|
||||
instead of a loud skip — a guard that can quietly stop guarding is
|
||||
the failure shape this family of checks exists to refuse. The local
|
||||
default in the script itself stays "0", so a plain working-copy run
|
||||
degrades sensibly.
|
||||
required: false
|
||||
default: "1"
|
||||
runs:
|
||||
using: composite
|
||||
steps:
|
||||
- name: changelog assembled
|
||||
shell: bash
|
||||
env:
|
||||
CHANGELOG_ASSEMBLED_BASE: ${{ inputs.base-ref }}
|
||||
CHANGELOG: ${{ inputs.changelog }}
|
||||
CHANGELOG_ASSEMBLED_DIR: ${{ inputs.fragments-dir }}
|
||||
VERSION_SOURCE: ${{ inputs.version-source }}
|
||||
CHANGELOG_ASSEMBLED_STRICT: ${{ inputs.strict }}
|
||||
run: bash "$GITHUB_ACTION_PATH/changelog-assembled.sh"
|
||||
251
actions/changelog-assembled/changelog-assembled.sh
Normal file
251
actions/changelog-assembled/changelog-assembled.sh
Normal file
|
|
@ -0,0 +1,251 @@
|
|||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
# changelog-assembled.sh [<base-ref>] [<changelog>] [<fragments-dir>] [<version-source>]
|
||||
# — assert that a release PR's stamped section is EXACTLY what the fragments
|
||||
# it consumed assemble to: read the fragments as of the MERGE BASE (they are
|
||||
# gone from HEAD's tree — that is the point of the ceremony), replay the
|
||||
# assembler's --check over that set, and compare byte-for-byte against
|
||||
# changelog_section on HEAD (#116; the fragment flow is #112).
|
||||
#
|
||||
# The failure it exists to catch leaves no trace — the shape every guard in
|
||||
# this family was bought by. bin/changelog-assemble is run BY HAND in the
|
||||
# release PR, deliberately: the assembled section must land in the PR diff
|
||||
# where the panel reads it (#114). Drop one fragment from the deletion and
|
||||
# its entry is simply absent from the release: the file is well-formed,
|
||||
# changelog-armed is green (the section exists and has prose),
|
||||
# changelog-monotonic is green (no heading was deleted), and the publisher
|
||||
# happily publishes the shortened section. Hand-edit one word of the
|
||||
# assembled prose and the published history quietly stops being what the
|
||||
# authors wrote. The only way anyone finds out is by reading the release
|
||||
# body against a directory that no longer exists.
|
||||
#
|
||||
# Why this cannot live in changelog-armed: "the section matches the
|
||||
# fragments it consumed" is not a property of a TREE — no single tree holds
|
||||
# both the fragments and the section they became. It is a property of a
|
||||
# DIFF: what existed at the merge base versus what HEAD stamped. That is
|
||||
# exactly the argument changelog-monotonic made for being its own git-aware
|
||||
# action rather than a clause inside changelog-armed, and this is the third
|
||||
# guard on the same reasoning. changelog-armed.sh stays drivable against
|
||||
# constructed two-file trees that are not git repos at all.
|
||||
#
|
||||
# The date is never compared as prose: --check prints the section BODY with
|
||||
# no '## ' heading, and changelog_section extracts the body below HEAD's
|
||||
# heading — so the date HEAD stamped into its heading never enters the
|
||||
# comparison, and a date difference can never masquerade as a prose one.
|
||||
|
||||
base_ref="${1:-${CHANGELOG_ASSEMBLED_BASE:-origin/main}}"
|
||||
changelog="${2:-${CHANGELOG:-CHANGELOG.md}}"
|
||||
dir="${3:-${CHANGELOG_ASSEMBLED_DIR:-changelog.d}}"
|
||||
version_source="${4:-${VERSION_SOURCE:-file}}"
|
||||
|
||||
# Fail-closed switch, changelog-monotonic's stance exactly: CI sets it (the
|
||||
# action defaults strict to "1"), so a degradation that is sensible on a
|
||||
# laptop becomes a red run there. A guard that can quietly stop guarding is
|
||||
# the failure shape this whole family of checks exists to refuse.
|
||||
strict="${CHANGELOG_ASSEMBLED_STRICT:-0}"
|
||||
|
||||
# The shared libs and the assembler travel with this action: a consumer's
|
||||
# `uses: heavy-duty/ceremony/actions/changelog-assembled@<tag>` downloads
|
||||
# this whole repository at that ref, so ../../lib and ../../bin are always
|
||||
# present and always at the same ref — no checkout step, no version skew.
|
||||
here="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
|
||||
# shellcheck source=lib/version.sh
|
||||
. "$here/../../lib/version.sh"
|
||||
# shellcheck source=lib/changelog.sh
|
||||
. "$here/../../lib/changelog.sh"
|
||||
assemble="$here/../../bin/changelog-assemble"
|
||||
|
||||
skip() {
|
||||
if [ "$strict" = "1" ]; then
|
||||
echo "changelog-assembled: $* — and CHANGELOG_ASSEMBLED_STRICT=1, so this is a FAILURE, not a skip." >&2
|
||||
echo " CI sets STRICT because a guard that quietly stops guarding is worse than no guard." >&2
|
||||
echo " Fix the checkout, not this script: the base ref must be fetched (fetch-depth: 0)." >&2
|
||||
exit 1
|
||||
fi
|
||||
echo "changelog-assembled: SKIPPED — $*"
|
||||
echo " (Everything this guard asserts compares HEAD against the merge base —"
|
||||
echo " without the history there is nothing it can honestly say. In CI this"
|
||||
echo " same condition is a hard failure.)"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# A pass with a NOTICE, never a skip in silence: an inapplicable tree is a
|
||||
# legitimate green, and the log says why instead of implying a check ran.
|
||||
notice() {
|
||||
echo "changelog-assembled: NOTICE — $*"
|
||||
exit 0
|
||||
}
|
||||
|
||||
[ -f "$changelog" ] || { echo "changelog-assembled: no such file: $changelog" >&2; exit 1; }
|
||||
|
||||
# --- everything here needs the HISTORY ---------------------------------------
|
||||
# Even applicability does: "fragment mode" is a fact about the merge base,
|
||||
# not about HEAD's tree, whose fragments are consumed by construction. So
|
||||
# unlike changelog-monotonic there is no history-free half to run first —
|
||||
# an unusable checkout degrades (or, under STRICT, refuses) before the
|
||||
# guard claims anything.
|
||||
|
||||
git rev-parse --is-inside-work-tree >/dev/null 2>&1 \
|
||||
|| skip "not inside a git work tree, so there is no merge base to read the fragments from"
|
||||
|
||||
git rev-parse --verify --quiet "$base_ref^{commit}" >/dev/null \
|
||||
|| skip "base ref '$base_ref' does not resolve here (a shallow clone, or a fork checkout without the upstream remote)"
|
||||
|
||||
merge_base="$(git merge-base "$base_ref" HEAD 2>/dev/null || true)"
|
||||
[ -n "$merge_base" ] \
|
||||
|| skip "no merge base between '$base_ref' and HEAD (unrelated histories, or a clone too shallow to reach one)"
|
||||
|
||||
short_base="$(git rev-parse --short "$merge_base")"
|
||||
|
||||
# Push-to-main shape: the merge base IS HEAD, so the fragment set this guard
|
||||
# would replay is HEAD's own — nothing was consumed between the two points,
|
||||
# and comparing a tree against itself would assert nothing. Named honestly,
|
||||
# the same discipline as changelog-monotonic's vacuous line.
|
||||
if [ "$merge_base" = "$(git rev-parse HEAD)" ]; then
|
||||
echo "changelog-assembled: vacuous (the merge base IS HEAD, so no fragments were consumed between them — there is no diff for the section to answer to)."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# --- applicability: the ceremony PR, and nothing else ------------------------
|
||||
|
||||
if ! git cat-file -e "$merge_base:$dir" 2>/dev/null; then
|
||||
notice "no '$dir/' at the merge base ($short_base) — legacy mode; the changelog is edited directly and there is no fragment set for a section to answer to"
|
||||
fi
|
||||
|
||||
# version_read refuses loudly on a missing or empty source; the wrapper line
|
||||
# names the guard so a workflow log shows which check refused.
|
||||
ver="$(version_read "$version_source")" || {
|
||||
echo "changelog-assembled: cannot read the version (version-source: $version_source)" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
if version_is_dev "$ver"; then
|
||||
notice "version '$ver' is a development tree — no release section is being stamped; whatever this PR does to '$dir/' is cargo for a future ceremony, not a consumption to verify"
|
||||
fi
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
trap 'rm -rf "$tmp"' EXIT
|
||||
|
||||
git show "$merge_base:$changelog" >"$tmp/base-changelog.md" 2>/dev/null || : >"$tmp/base-changelog.md"
|
||||
|
||||
# A branch that merely SITS on a release is not the ceremony that stamped
|
||||
# it: right after a release merges, main's version is bare until the -dev
|
||||
# bump lands, and a PR branched in that window would otherwise be asked to
|
||||
# answer for a consumption that happened at its merge base, not on it. If
|
||||
# the section already existed at the merge base, this branch did not stamp
|
||||
# it. Whole-version match, as everywhere in this family.
|
||||
if awk -v ver="$ver" '/^## / && $2 == ver { found = 1; exit } END { exit !found }' "$tmp/base-changelog.md"; then
|
||||
notice "the section for '$ver' already exists at the merge base ($short_base) — this branch is not the ceremony that stamped it, so there is no consumed set to answer to"
|
||||
fi
|
||||
|
||||
# --- the replay: the merge base's fragment set, byte for byte ----------------
|
||||
# Every blob in the directory is extracted — strays included, and subtrees
|
||||
# recreated — so the replay refuses exactly what a real assembler run over
|
||||
# that tree would have refused, instead of quietly narrowing the set.
|
||||
|
||||
mkdir -p "$tmp/$dir"
|
||||
base_frags=""
|
||||
while IFS= read -r -d '' entry; do
|
||||
meta="${entry%%$'\t'*}"
|
||||
path="${entry#*$'\t'}"
|
||||
otype="$(printf '%s\n' "$meta" | awk '{ print $2 }')"
|
||||
name="${path##*/}"
|
||||
case "$otype" in
|
||||
blob)
|
||||
git show "$merge_base:$path" >"$tmp/$dir/$name"
|
||||
case "$name" in
|
||||
README.md) ;;
|
||||
*.md) base_frags="${base_frags}${path}"$'\n' ;;
|
||||
esac
|
||||
;;
|
||||
tree)
|
||||
mkdir -p "$tmp/$dir/$name"
|
||||
;;
|
||||
esac
|
||||
done < <(git ls-tree -z "$merge_base" -- "$dir/")
|
||||
|
||||
frag_count="$(printf '%s' "$base_frags" | grep -c . || true)"
|
||||
|
||||
failures=0
|
||||
|
||||
# Refusal: a fragment the ceremony consumed is still present on HEAD. The
|
||||
# ceremony deletes exactly what it assembles (#112) — a fragment that
|
||||
# survives its own release sits in the directory and is assembled AGAIN
|
||||
# into the NEXT section, republishing its prose as if it were new.
|
||||
survivors=""
|
||||
while IFS= read -r p; do
|
||||
[ -n "$p" ] || continue
|
||||
if [ -e "$p" ]; then
|
||||
survivors="${survivors} ${p}"$'\n'
|
||||
fi
|
||||
done <<<"$base_frags"
|
||||
if [ -n "$survivors" ]; then
|
||||
{
|
||||
echo "changelog-assembled: fragment(s) present at the merge base ($short_base) are STILL PRESENT on HEAD:"
|
||||
echo
|
||||
printf '%s' "$survivors"
|
||||
echo
|
||||
echo " The ceremony deletes exactly what it assembles. A fragment that survives"
|
||||
echo " its own release is assembled AGAIN into the next section, republishing"
|
||||
echo " its prose as if it were new. Delete it in this PR — its entry is (or"
|
||||
echo " should be) already in the stamped section."
|
||||
} >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
expected=""
|
||||
if expected="$( (cd "$tmp" && "$assemble" "$ver" --check --changelog "$tmp/base-changelog.md" --dir "$dir") 2>&1)"; then
|
||||
found="$(changelog_section "$changelog" "$ver")"
|
||||
if [ -z "$found" ]; then
|
||||
{
|
||||
echo "changelog-assembled: the version is '$ver' (a release tree) and '$dir/' at the"
|
||||
echo " merge base ($short_base) holds $frag_count fragment(s), but $changelog has no"
|
||||
echo " non-empty section for '$ver' on HEAD."
|
||||
echo
|
||||
echo " The fragments were consumed and their prose went nowhere: the release"
|
||||
echo " this tree is about to publish would have a body the authors never got"
|
||||
echo " to write. The ceremony's edit is one tool run, in this PR:"
|
||||
echo
|
||||
echo " bin/changelog-assemble '$ver'"
|
||||
} >&2
|
||||
failures=$((failures + 1))
|
||||
else
|
||||
if ! diff_out="$(diff -u \
|
||||
--label "expected — assembled from the $frag_count fragment(s) at the merge base ($short_base)" \
|
||||
--label "found — section '$ver' in $changelog on HEAD" \
|
||||
<(printf '%s\n' "$expected") <(printf '%s\n' "$found"))"; then
|
||||
{
|
||||
echo "changelog-assembled: the section '$ver' in $changelog is NOT what the fragments it consumed assemble to:"
|
||||
echo
|
||||
printf '%s\n' "$diff_out" | sed 's/^/ /'
|
||||
echo
|
||||
echo " The release body is published verbatim from this section, so any"
|
||||
echo " difference here ships: a missing line is an author's entry silently"
|
||||
echo " dropped from history, an extra or edited line is prose nobody wrote,"
|
||||
echo " and a re-ordering is not the canonical order the assembler produces."
|
||||
echo " The fix is to redo the ceremony's edit with the tool — re-run"
|
||||
echo " bin/changelog-assemble '$ver' <date> from the merge base's fragments —"
|
||||
echo " never to hand-edit the section into agreement."
|
||||
} >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
fi
|
||||
else
|
||||
{
|
||||
echo "changelog-assembled: the merge base's fragment set does not assemble — the replay refuses:"
|
||||
echo
|
||||
printf '%s\n' "$expected" | sed 's/^/ /'
|
||||
echo
|
||||
echo " The set replayed is exactly '$dir/' as of the merge base ($short_base)."
|
||||
echo " A section that bin/changelog-assemble would refuse to produce cannot"
|
||||
echo " have been produced by it — whatever stamped this section did it by"
|
||||
echo " hand, and the release body cannot be trusted to be what the fragment"
|
||||
echo " authors wrote."
|
||||
} >&2
|
||||
failures=$((failures + 1))
|
||||
fi
|
||||
|
||||
[ "$failures" -eq 0 ] || exit 1
|
||||
|
||||
echo "changelog-assembled: section '$ver' in $changelog is byte-for-byte the assembly of the $frag_count fragment(s) consumed at the merge base ($short_base)"
|
||||
|
|
@ -96,6 +96,21 @@ read_failure_reason() { # $1 = captured stderr → one bounded line; pure (#101)
|
|||
fi
|
||||
}
|
||||
|
||||
missing_core_labels_warning() { # $1 = declared rows, $2 = repo label names
|
||||
local rows="$1" repo_labels="$2" row name missing=""
|
||||
[ -n "$repo_labels" ] || return 0
|
||||
while IFS= read -r row; do
|
||||
[ -n "$row" ] || continue
|
||||
name="${row%%|*}"
|
||||
if ! grep -qxF "$name" <<<"$repo_labels"; then
|
||||
if [ -n "$missing" ]; then missing="$missing, $name"; else missing="$name"; fi
|
||||
fi
|
||||
done <<<"$rows"
|
||||
if [ -n "$missing" ]; then
|
||||
echo "::warning::labels: missing core label(s): $missing; bump the ceremony pin, then re-dispatch workflow_dispatch to bootstrap the taxonomy"
|
||||
fi
|
||||
}
|
||||
|
||||
load_config() { # $1 = consumer labels.conf; panel is mandatory, scopes optional
|
||||
local conf="$1" line panel_seen=false
|
||||
[ -f "$conf" ] || {
|
||||
|
|
@ -627,6 +642,7 @@ main() {
|
|||
# add against it, because one unknown name fails the whole edit call.
|
||||
REPO_LABELS="$(gh label list -R "$REPO" --limit 200 --json name --jq '.[].name' 2>/dev/null || echo "")"
|
||||
[ -z "$REPO_LABELS" ] && log "WARNING: could not read the label set — applying labels unfiltered"
|
||||
missing_core_labels_warning "$(core_label_rows)" "$REPO_LABELS"
|
||||
|
||||
local n output status total=0 unreadable=0 sampled_reason=""
|
||||
while IFS= read -r n; do
|
||||
|
|
|
|||
|
|
@ -319,7 +319,10 @@ rows remain consumer-owned because paths and surfaces differ by repository.
|
|||
|
||||
After adding the caller and configuration, run `workflow_dispatch` once to
|
||||
bootstrap labels on a fresh repository. Scheduled and PR-triggered runs only
|
||||
reconcile; they do not repeatedly upsert the taxonomy.
|
||||
reconcile; they do not repeatedly upsert the taxonomy. When a ceremony pin
|
||||
bump adds a core label, bump the pin first and then re-dispatch
|
||||
`workflow_dispatch`; the scheduled sweep warns when the pinned taxonomy
|
||||
declares a core label the repository lacks.
|
||||
|
||||
## Doctrine mirror
|
||||
|
||||
|
|
|
|||
325
test/changelog-assembled.test.sh
Normal file
325
test/changelog-assembled.test.sh
Normal file
|
|
@ -0,0 +1,325 @@
|
|||
#!/usr/bin/env bash
|
||||
# Contract tests for actions/changelog-assembled (issue #116). Like the
|
||||
# monotonic guard's suite, every applicable case is a constructed git repo —
|
||||
# "the section matches the fragments it consumed" is a property of a DIFF:
|
||||
# the fragments are gone from HEAD's tree by construction, so the fixture is
|
||||
# a history: a base commit holding the fragments and a -dev version, a HEAD
|
||||
# commit holding the ceremony's edit, and a 'base' branch standing in for
|
||||
# origin/main. set -u, not -e: failing commands are behavior for the
|
||||
# harness to inspect.
|
||||
set -u
|
||||
|
||||
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
# shellcheck source=test/harness.sh
|
||||
. "$ROOT/test/harness.sh"
|
||||
|
||||
SCRIPT="$ROOT/actions/changelog-assembled/changelog-assembled.sh"
|
||||
ARMED="$ROOT/actions/changelog-armed/changelog-armed.sh"
|
||||
MONOTONIC="$ROOT/actions/changelog-monotonic/changelog-monotonic.sh"
|
||||
ASSEMBLE="$ROOT/bin/changelog-assemble"
|
||||
|
||||
TMP="$(mktemp -d)"
|
||||
trap 'rm -rf "$TMP"' EXIT
|
||||
|
||||
init_repo() {
|
||||
local dir="$TMP/$1"
|
||||
mkdir -p "$dir"
|
||||
git -C "$dir" init -q -b main
|
||||
git -C "$dir" config user.email ci@example.invalid
|
||||
git -C "$dir" config user.name ci
|
||||
}
|
||||
|
||||
# commit_base <name> — commit the tree as the merge base and pin 'base' there.
|
||||
commit_base() {
|
||||
git -C "$TMP/$1" add -A
|
||||
git -C "$TMP/$1" commit -qm base
|
||||
git -C "$TMP/$1" branch base
|
||||
}
|
||||
|
||||
commit_head() {
|
||||
git -C "$TMP/$1" add -A
|
||||
git -C "$TMP/$1" commit -qm head
|
||||
}
|
||||
|
||||
# seed_flat <name> — the shared pre-ceremony tree: an armed changelog, two
|
||||
# flat fragments (numeric order will put 12 before 9), a -dev version.
|
||||
seed_flat() {
|
||||
local name="$1" dir="$TMP/$1"
|
||||
init_repo "$name"
|
||||
mkdir -p "$dir/changelog.d"
|
||||
printf 'Machine-assembled; see heavy-duty/ceremony#112.\n' >"$dir/changelog.d/README.md"
|
||||
cat >"$dir/CHANGELOG.md" <<'EOF'
|
||||
# Changelog
|
||||
|
||||
Preamble prose belongs to no section.
|
||||
|
||||
## 0.1.0 — 2026-07-01
|
||||
|
||||
- The shipped entry.
|
||||
EOF
|
||||
printf '0.1.1-dev\n' >"$dir/VERSION"
|
||||
printf -- '- Twelve landed.\n' >"$dir/changelog.d/12.md"
|
||||
printf -- '- Nine landed, and its prose wraps onto a\n continuation line.\n' >"$dir/changelog.d/9.md"
|
||||
commit_base "$name"
|
||||
}
|
||||
|
||||
# ceremony <name> <ver> <date> — the faithful release edit on the working
|
||||
# tree, exactly as a human runs it: the real assembler in write mode, then
|
||||
# the bare version stamp. Deliberately does NOT commit, so a case can break
|
||||
# the tree before committing HEAD.
|
||||
ceremony() {
|
||||
local name="$1" ver="$2" stamp="$3"
|
||||
(cd "$TMP/$name" && "$ASSEMBLE" "$ver" "$stamp" >/dev/null 2>&1) || return 1
|
||||
printf '%s\n' "$ver" >"$TMP/$name/VERSION"
|
||||
}
|
||||
|
||||
run() { local name="$1"; shift; (cd "$TMP/$name" && bash "$SCRIPT" "$@"); }
|
||||
run_strict() { local name="$1"; shift; (cd "$TMP/$name" && CHANGELOG_ASSEMBLED_STRICT=1 bash "$SCRIPT" "$@"); }
|
||||
|
||||
# --- the documented flow passes ----------------------------------------------
|
||||
|
||||
seed_flat faithful-flat
|
||||
ceremony faithful-flat 0.2.0 2026-07-24
|
||||
commit_head faithful-flat
|
||||
check "faithful flat ceremony: the section is byte-for-byte the assembly" 0 \
|
||||
"byte-for-byte" run faithful-flat base
|
||||
|
||||
seed_flat faithful-grouped
|
||||
printf -- '### Fixed\n\n- Fixed twenty-one.\n' >"$TMP/faithful-grouped/changelog.d/21.md"
|
||||
printf -- '### Added\n\n- Added twenty.\n\n### Docs\n\n- Docs twenty.\n' >"$TMP/faithful-grouped/changelog.d/20.md"
|
||||
rm "$TMP/faithful-grouped/changelog.d/12.md" "$TMP/faithful-grouped/changelog.d/9.md"
|
||||
git -C "$TMP/faithful-grouped" add -A
|
||||
git -C "$TMP/faithful-grouped" commit -qm regroup
|
||||
git -C "$TMP/faithful-grouped" branch -f base
|
||||
ceremony faithful-grouped 0.2.0 2026-07-24
|
||||
commit_head faithful-grouped
|
||||
check "faithful grouped ceremony passes" 0 "byte-for-byte" run faithful-grouped base
|
||||
|
||||
# The date is HEAD's to choose: a stamp nowhere near today must not read as
|
||||
# a prose difference — the comparison is body against body, headings (and
|
||||
# so dates) never enter it.
|
||||
seed_flat old-date
|
||||
ceremony old-date 0.2.0 2020-01-01
|
||||
commit_head old-date
|
||||
check "the stamp's date never enters the comparison" 0 "byte-for-byte" \
|
||||
run old-date base
|
||||
|
||||
# --- inapplicable trees: green NOTICE, never a silent skip -------------------
|
||||
|
||||
seed_flat ordinary-add
|
||||
printf -- '- Thirteen incoming.\n' >"$TMP/ordinary-add/changelog.d/13.md"
|
||||
commit_head ordinary-add
|
||||
check "-dev PR adding a fragment: green NOTICE" 0 "NOTICE" run ordinary-add base
|
||||
|
||||
seed_flat ordinary-none
|
||||
printf 'code\n' >"$TMP/ordinary-none/code.txt"
|
||||
commit_head ordinary-none
|
||||
check "-dev PR touching no fragment: green NOTICE" 0 "NOTICE" run ordinary-none base
|
||||
|
||||
seed_flat ordinary-del
|
||||
rm "$TMP/ordinary-del/changelog.d/12.md"
|
||||
commit_head ordinary-del
|
||||
check "-dev PR even deleting a fragment: green NOTICE" 0 "NOTICE" run ordinary-del base
|
||||
|
||||
# Legacy mode: no changelog.d/ at the merge base — always a NOTICE, even on
|
||||
# a release tree, because the mid-adoption ceremony edits the changelog by
|
||||
# hand and there is no fragment set for its section to answer to.
|
||||
init_repo legacy
|
||||
printf '# Changelog\n\n## Unreleased\n\n- An entry.\n' >"$TMP/legacy/CHANGELOG.md"
|
||||
printf '0.1.1-dev\n' >"$TMP/legacy/VERSION"
|
||||
commit_base legacy
|
||||
printf '# Changelog\n\n## 0.2.0 — 2026-07-24\n\n- An entry.\n' >"$TMP/legacy/CHANGELOG.md"
|
||||
printf '0.2.0\n' >"$TMP/legacy/VERSION"
|
||||
commit_head legacy
|
||||
check "legacy repo (no changelog.d at base): green NOTICE, even on a release tree" 0 \
|
||||
"NOTICE" run legacy base
|
||||
|
||||
# The un-rearmed window: a PR branched right after a release merges sits on
|
||||
# a bare version whose section was stamped at its MERGE BASE — it is not
|
||||
# the ceremony and must not be asked to answer for one.
|
||||
seed_flat post-release
|
||||
ceremony post-release 0.2.0 2026-07-24
|
||||
git -C "$TMP/post-release" add -A
|
||||
git -C "$TMP/post-release" commit -qm release
|
||||
git -C "$TMP/post-release" branch -f base
|
||||
printf 'code\n' >"$TMP/post-release/code.txt"
|
||||
commit_head post-release
|
||||
check "a PR atop the un-rearmed release: green NOTICE (this branch did not stamp)" 0 \
|
||||
"NOTICE" run post-release base
|
||||
|
||||
# --- the refusals ------------------------------------------------------------
|
||||
|
||||
# The issue's headline failure: one fragment kept out of the ceremony — it
|
||||
# survives at HEAD and its entry is absent from the section. Both refusals
|
||||
# fire: the diff names the missing entry, the survivor list names the file.
|
||||
seed_flat dropped
|
||||
mv "$TMP/dropped/changelog.d/9.md" "$TMP/dropped/9.md.hold"
|
||||
ceremony dropped 0.2.0 2026-07-24
|
||||
mv "$TMP/dropped/9.md.hold" "$TMP/dropped/changelog.d/9.md"
|
||||
commit_head dropped
|
||||
check "a fragment kept out of the ceremony fails" 1 "" run dropped base
|
||||
check "the dropped-entry diff names the missing entry" 1 "Nine landed" \
|
||||
run dropped base
|
||||
check "the surviving fragment is listed by path" 1 "changelog.d/9.md" \
|
||||
run dropped base
|
||||
|
||||
# The same drop, but the fragment was deleted anyway: its prose vanished
|
||||
# without ever being published. Only the diff can say so.
|
||||
seed_flat vanished
|
||||
mv "$TMP/vanished/changelog.d/9.md" "$TMP/vanished/9.md.hold"
|
||||
ceremony vanished 0.2.0 2026-07-24
|
||||
rm "$TMP/vanished/9.md.hold"
|
||||
commit_head vanished
|
||||
check "a deleted fragment whose entry never landed fails" 1 "Nine landed" \
|
||||
run vanished base
|
||||
|
||||
seed_flat edited
|
||||
ceremony edited 0.2.0 2026-07-24
|
||||
sed -i 's/Twelve landed/Twelve allegedly landed/' "$TMP/edited/CHANGELOG.md"
|
||||
commit_head edited
|
||||
check "a hand-edited entry fails with a unified diff" 1 "+++" run edited base
|
||||
check "the edit is visible in the diff" 1 "allegedly" run edited base
|
||||
check "the diff failure teaches redo-with-the-tool, never hand-edit" 1 \
|
||||
"never to hand-edit" run edited base
|
||||
|
||||
# Re-ordering away from the canonical order: the section is hand-built with
|
||||
# the right entries in the wrong order (the assembler puts 12 before 9).
|
||||
seed_flat reordered
|
||||
rm "$TMP/reordered/changelog.d/12.md" "$TMP/reordered/changelog.d/9.md"
|
||||
cat >"$TMP/reordered/CHANGELOG.md" <<'EOF'
|
||||
# Changelog
|
||||
|
||||
Preamble prose belongs to no section.
|
||||
|
||||
## 0.2.0 — 2026-07-24
|
||||
|
||||
- Nine landed, and its prose wraps onto a
|
||||
continuation line.
|
||||
- Twelve landed.
|
||||
|
||||
## 0.1.0 — 2026-07-01
|
||||
|
||||
- The shipped entry.
|
||||
EOF
|
||||
printf '0.2.0\n' >"$TMP/reordered/VERSION"
|
||||
commit_head reordered
|
||||
check "re-ordered entries fail" 1 "NOT what the fragments" run reordered base
|
||||
|
||||
# A faithful assembly that forgot one deletion: the section is right, the
|
||||
# directory is not — only the survivor refusal fires.
|
||||
seed_flat survivor
|
||||
ceremony survivor 0.2.0 2026-07-24
|
||||
printf -- '- Nine landed, and its prose wraps onto a\n continuation line.\n' >"$TMP/survivor/changelog.d/9.md"
|
||||
commit_head survivor
|
||||
check "a surviving fragment with its entry present fails" 1 "STILL PRESENT" \
|
||||
run survivor base
|
||||
check "the survivor refusal names the file" 1 "changelog.d/9.md" \
|
||||
run survivor base
|
||||
|
||||
# Fragments consumed, section never stamped: the prose went nowhere.
|
||||
seed_flat halfdone
|
||||
rm "$TMP/halfdone/changelog.d/12.md" "$TMP/halfdone/changelog.d/9.md"
|
||||
printf '0.2.0\n' >"$TMP/halfdone/VERSION"
|
||||
commit_head halfdone
|
||||
check "fragments consumed but no section stamped fails" 1 \
|
||||
"non-empty section for '0.2.0'" run halfdone base
|
||||
|
||||
# A release stamped out of a fragment-free directory: the replay refuses the
|
||||
# way the real assembler would have — an empty release.
|
||||
seed_flat empty-release
|
||||
rm "$TMP/empty-release/changelog.d/12.md" "$TMP/empty-release/changelog.d/9.md"
|
||||
git -C "$TMP/empty-release" add -A
|
||||
git -C "$TMP/empty-release" commit -qm consume-early
|
||||
git -C "$TMP/empty-release" branch -f base
|
||||
cat >"$TMP/empty-release/CHANGELOG.md" <<'EOF'
|
||||
# Changelog
|
||||
|
||||
Preamble prose belongs to no section.
|
||||
|
||||
## 0.2.0 — 2026-07-24
|
||||
|
||||
- Prose from nowhere.
|
||||
|
||||
## 0.1.0 — 2026-07-01
|
||||
|
||||
- The shipped entry.
|
||||
EOF
|
||||
printf '0.2.0\n' >"$TMP/empty-release/VERSION"
|
||||
commit_head empty-release
|
||||
check "a section stamped from zero fragments fails on the replay's refusal" 1 \
|
||||
"zero fragments" run empty-release base
|
||||
|
||||
check "missing changelog fails" 1 "no such file" run faithful-flat base NOPE.md
|
||||
|
||||
# --- the trio interaction row (the issue's whole argument) -------------------
|
||||
# On the faithful tree all three guards are green; on the dropped-entry tree
|
||||
# armed is green (the section exists and has prose), monotonic is green (no
|
||||
# heading was deleted) — this guard is the ONLY one that goes red.
|
||||
|
||||
run_armed() { (cd "$TMP/$1" && bash "$ARMED"); }
|
||||
run_monotonic() { local name="$1"; shift; (cd "$TMP/$name" && bash "$MONOTONIC" "$@"); }
|
||||
|
||||
check "trio, faithful tree: changelog-armed green" 0 "agrees" run_armed faithful-flat
|
||||
check "trio, faithful tree: changelog-monotonic green" 0 "still present" \
|
||||
run_monotonic faithful-flat base
|
||||
check "trio, faithful tree: changelog-assembled green" 0 "byte-for-byte" \
|
||||
run faithful-flat base
|
||||
check "trio, dropped-entry tree: changelog-armed stays green" 0 "agrees" \
|
||||
run_armed dropped
|
||||
check "trio, dropped-entry tree: changelog-monotonic stays green" 0 "still present" \
|
||||
run_monotonic dropped base
|
||||
check "trio, dropped-entry tree: changelog-assembled is the only red" 1 "" \
|
||||
run dropped base
|
||||
|
||||
# --- degradation: the loud skip and the STRICT refusal -----------------------
|
||||
|
||||
seed_flat no-base
|
||||
ceremony no-base 0.2.0 2026-07-24
|
||||
commit_head no-base
|
||||
check "base ref missing, STRICT=0: loud skip, exit 0" 0 "SKIPPED" \
|
||||
run no-base does-not-exist
|
||||
check "base ref missing, STRICT=1: hard failure" 1 "FAILURE" \
|
||||
run_strict no-base does-not-exist
|
||||
check "the STRICT failure names the checkout fix, not the script" 1 "fetch-depth: 0" \
|
||||
run_strict no-base does-not-exist
|
||||
|
||||
mkdir -p "$TMP/plain"
|
||||
printf '# Changelog\n\n## Unreleased\n' >"$TMP/plain/CHANGELOG.md"
|
||||
check "not a git repo, STRICT=0: loud skip" 0 "not inside a git work tree" \
|
||||
run plain
|
||||
check "not a git repo, STRICT=1: hard failure" 1 "FAILURE" run_strict plain
|
||||
|
||||
# --- the honest edges --------------------------------------------------------
|
||||
|
||||
# Push-to-main shape: the merge base IS HEAD, nothing was consumed between
|
||||
# them, and the success line must say so instead of claiming a comparison.
|
||||
seed_flat vacuous
|
||||
ceremony vacuous 0.2.0 2026-07-24
|
||||
commit_head vacuous
|
||||
check "merge base IS HEAD: vacuous, named honestly" 0 "vacuous" run vacuous HEAD
|
||||
|
||||
# --- the action's wiring: inputs arrive as env vars --------------------------
|
||||
|
||||
# Non-default names for everything action.yml passes, STRICT included, prove
|
||||
# the env vars are honored the way the composite sets them.
|
||||
init_repo env-tree
|
||||
mkdir -p "$TMP/env-tree/frags"
|
||||
printf '# Changelog\n\n## 0.1.0 — 2026-07-01\n\n- Shipped.\n' >"$TMP/env-tree/NOTES.md"
|
||||
printf '0.1.1-dev\n' >"$TMP/env-tree/VERSION"
|
||||
printf -- '- Flagged entry.\n' >"$TMP/env-tree/frags/2.md"
|
||||
git -C "$TMP/env-tree" add -A
|
||||
git -C "$TMP/env-tree" commit -qm base
|
||||
git -C "$TMP/env-tree" branch fixture-base
|
||||
(cd "$TMP/env-tree" && "$ASSEMBLE" 0.2.0 2026-07-24 --changelog NOTES.md --dir frags >/dev/null 2>&1)
|
||||
printf '0.2.0\n' >"$TMP/env-tree/VERSION"
|
||||
git -C "$TMP/env-tree" add -A
|
||||
git -C "$TMP/env-tree" commit -qm head
|
||||
env_tree() {
|
||||
(cd "$TMP/env-tree" && \
|
||||
CHANGELOG_ASSEMBLED_BASE=fixture-base CHANGELOG=NOTES.md \
|
||||
CHANGELOG_ASSEMBLED_DIR=frags VERSION_SOURCE=file \
|
||||
CHANGELOG_ASSEMBLED_STRICT=1 bash "$SCRIPT")
|
||||
}
|
||||
check "env vars drive the script the way action.yml does" 0 "byte-for-byte" env_tree
|
||||
|
||||
summary
|
||||
|
|
@ -74,6 +74,24 @@ expect "...within the 304-byte bound" yes \
|
|||
exact_reason="$(read_failure_reason "$(printf 'e%.0s' {1..300})")"
|
||||
expect "a 300-char reason passes through whole" 300 "${#exact_reason}"
|
||||
|
||||
# -- a missing core taxonomy row is visible without mutating labels ----------
|
||||
core_rows="$(core_label_rows)"
|
||||
core_names="$(cut -d'|' -f1 <<<"$core_rows")"
|
||||
expect "a complete core taxonomy does not warn" "" \
|
||||
"$(missing_core_labels_warning "$core_rows" "$core_names")"
|
||||
expect "one missing core label is named exactly" \
|
||||
"::warning::labels: missing core label(s): attention; bump the ceremony pin, then re-dispatch workflow_dispatch to bootstrap the taxonomy" \
|
||||
"$(missing_core_labels_warning "$core_rows" "$(grep -vxF attention <<<"$core_names")")"
|
||||
expect "three missing core labels are named in table order" \
|
||||
"::warning::labels: missing core label(s): offsite, needs-ruling, attention; bump the ceremony pin, then re-dispatch workflow_dispatch to bootstrap the taxonomy" \
|
||||
"$(missing_core_labels_warning "$core_rows" "$(grep -vxF -e offsite -e needs-ruling -e attention <<<"$core_names")")"
|
||||
expect "an unreadable empty label set does not report the taxonomy missing" "" \
|
||||
"$(missing_core_labels_warning "$core_rows" "")"
|
||||
expect "unrelated scope labels do not affect a complete core taxonomy" "" \
|
||||
"$(missing_core_labels_warning "$core_rows" "$core_names
|
||||
scope:consumer-one
|
||||
scope:consumer-two")"
|
||||
|
||||
# -- drafts are building, whoever is requested --------------------------------
|
||||
DRAFT=true HEAD_SHA=head1 REQUESTED="" REVIEWS_JSON='[]'
|
||||
expect "draft PR is building" state:building "$(decide_state)"
|
||||
|
|
@ -636,7 +654,7 @@ blind_main_probe() {
|
|||
LABELS_CONF=.github/labels.conf
|
||||
gh() {
|
||||
if [ "$1" = label ] && [ "$2" = list ]; then
|
||||
printf 'state:building\nstate:addressing\n'
|
||||
core_label_rows | cut -d'|' -f1
|
||||
elif [ "$1" = pr ] && [ "$2" = list ]; then
|
||||
printf '101\n102\n'
|
||||
elif [ "$1" = pr ] && [ "$2" = view ]; then
|
||||
|
|
|
|||
|
|
@ -53,4 +53,16 @@ load_config "$TMP/good.conf"
|
|||
set_required_bots two
|
||||
check "PR author is recused from the required panel" 0 "one three" printf '%s\n' "${REQUIRED_BOTS[*]}"
|
||||
|
||||
# LABELS.md is mirrored byte-identically into every governed repo, so any
|
||||
# scope enumeration it carries is true at home and false everywhere else —
|
||||
# 14 of 16 vendored rows were false across the family when this fired (#104).
|
||||
# The set lives in labels.conf and the repo's CONTRIBUTING; the mirror never
|
||||
# names it. A concrete label is `scope:` followed by a name character — the
|
||||
# doctrine spellings (bare `scope:`, wildcard `scope:*`) put a backtick or `*`
|
||||
# there instead, so any name, current or future, in any shape (table row,
|
||||
# name|color|description row, prose) re-reds this while doctrine stays green.
|
||||
# grep -c prints the count and exits 1 when that count is 0.
|
||||
check "LABELS.md enumerates no repo's scope labels" 1 "0" \
|
||||
grep -c 'scope:[a-z0-9]' "$ROOT/LABELS.md"
|
||||
|
||||
summary
|
||||
|
|
|
|||
Loading…
Reference in a new issue