feat: CI refuses a release PR with no drill record #138
2 changed files with 49 additions and 9 deletions
34
.github/scripts/drill-recorded.sh
vendored
34
.github/scripts/drill-recorded.sh
vendored
|
|
@ -95,15 +95,31 @@ esac
|
||||||
# humans reading the log.
|
# humans reading the log.
|
||||||
#
|
#
|
||||||
# grab is re-armed by every '## ' line, so the section ends at the next one —
|
# grab is re-armed by every '## ' line, so the section ends at the next one —
|
||||||
# a record cannot borrow the body of the record below it. sed drops the blank
|
# a record cannot borrow the body of the record below it.
|
||||||
# padding under the heading and the command substitution eats the trailing
|
#
|
||||||
# blanks, which is what makes "present but EMPTY" distinguishable from
|
# `grab && NF` is the non-blank rule, and it is load-bearing rather than
|
||||||
# "present with content": a heading with nothing but whitespace under it
|
# tidiness. NF is 0 on a line that is empty OR contains only whitespace, so
|
||||||
# extracts to the empty string.
|
# `record` is non-empty exactly when a line with real content exists. The
|
||||||
record="$(awk -v ver="$ver" '
|
# first cut of this piped through `sed '/./,$!d'` and the comment here claimed
|
||||||
/^## / { grab = ($2 == "Release" && $3 == "drill" && $4 == "—" && $5 == ver); next }
|
# "a heading with nothing but whitespace under it extracts to the empty
|
||||||
grab { print }
|
# string" — which is precisely what that pipeline did NOT guarantee, because
|
||||||
' "$runs" | sed '/./,$!d')"
|
# `.` matches a space. A heading followed by one tab satisfied the gate. The
|
||||||
|
# comment documented the intended contract and the code did not meet it, which
|
||||||
|
# on a gate is the whole ballgame: an evidence-free release for the price of an
|
||||||
|
# invisible character. Found by all three reviewers on #138, independently.
|
||||||
|
#
|
||||||
|
# The '(NF == 5 || $6 == dash)' tail constraint keeps this in step with box's
|
||||||
|
# twin: without it '## Release drill — 0.2.0 stray words' counts as a record
|
||||||
|
# here and does not there. Two sibling guards disagreeing about what the same
|
||||||
|
# heading means is the same trap as disagreeing with release-notes.sh.
|
||||||
|
record="$(awk -v ver="$ver" -v dash="—" '
|
||||||
|
/^## / {
|
||||||
|
grab = ($2 == "Release" && $3 == "drill" && $4 == dash && $5 == ver \
|
||||||
|
&& (NF == 5 || $6 == dash))
|
||||||
|
next
|
||||||
|
}
|
||||||
|
grab && NF { print }
|
||||||
|
' "$runs")"
|
||||||
|
|
||||||
if [ -z "$record" ]; then
|
if [ -z "$record" ]; then
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -798,6 +798,30 @@ describe("drill-recorded.sh — a release version has a drill record", () => {
|
||||||
expect(r.output).toContain("no drill record for version '0.2.0'");
|
expect(r.output).toContain("no drill record for version '0.2.0'");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// ...and whitespace is not evidence either. The first cut extracted with
|
||||||
|
// `sed '/./,$!d'`, where `.` matches a space, so a heading followed by one
|
||||||
|
// tab passed the gate — while the comment above the extractor claimed the
|
||||||
|
// opposite. An evidence-free release for the price of an invisible
|
||||||
|
// character, on the one check whose whole job is to demand evidence.
|
||||||
|
// Found independently by all three reviewers on #138.
|
||||||
|
it("a record body of only spaces and tabs fails (#138)", async () => {
|
||||||
|
const runs = `# Drill runs\n\n${heading("0.2.0")}\n \n\t\n\n## Notes\n\nUnrelated.\n`;
|
||||||
|
const r = await check(treeWith("blank", "0.2.0", runs));
|
||||||
|
expect(r.code).toBe(1);
|
||||||
|
expect(r.output).toContain("no drill record for version '0.2.0'");
|
||||||
|
});
|
||||||
|
|
||||||
|
// The sibling guards must agree about what a heading IS, not just about the
|
||||||
|
// version in it. box#149 requires the version to be the last field or be
|
||||||
|
// followed by the em dash; without the same constraint here, this heading is
|
||||||
|
// a record in cast and not in box.
|
||||||
|
it("a stray tail after the version is not a heading — in step with box#149", async () => {
|
||||||
|
const runs = `# Drill runs\n\n## Release drill — 0.2.0 stray words\n${legs}`;
|
||||||
|
const r = await check(treeWith("tail", "0.2.0", runs));
|
||||||
|
expect(r.code).toBe(1);
|
||||||
|
expect(r.output).toContain("no drill record for version '0.2.0'");
|
||||||
|
});
|
||||||
|
|
||||||
// The version is matched WHOLE, both directions — release-notes.sh's trap,
|
// The version is matched WHOLE, both directions — release-notes.sh's trap,
|
||||||
// solved the same way (awk field equality, no regex, no dot-escaping). A
|
// solved the same way (awk field equality, no regex, no dot-escaping). A
|
||||||
// release candidate's drill is not the release's drill: different tree,
|
// release candidate's drill is not the release's drill: different tree,
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue