From 7951c94171dd1cdd35b6e32c60447d5dac50833c Mon Sep 17 00:00:00 2001 From: "Jonathan D.A. Jewell" <6759885+hyperpolymath@users.noreply.github.com> Date: Thu, 3 Sep 2026 17:10:05 +0100 Subject: [PATCH] Stop the README generator trusting two exit codes that carry no verdict readme-derive-reusable.yml runs asciidoctor under `set -euo pipefail` and commits what it produces into a consumer repo. Both of its AsciiDoc tools report a clean run the same way they report a broken one. asciidoctor's --failure-level defaults to FATAL, so a table cell containing a bare '|' emits "ERROR: dropping cells from incomplete row" and still exits 0. Every later cell in that row shifts, and the derived README.md would have been committed missing them. Measured on the docbook5 backend this workflow uses, not just html5: default exits 0, --failure-level=WARN exits 1, same message. asciidoctor-reducer has no --failure-level at all, only --log-level, which changes what is printed and not what is returned. Its exit code therefore cannot be made to carry its verdict, so its stderr is captured and checked instead. That guard turns out to be the load-bearing one: the reducer parses the document as well, so it is the first tool to see a malformed table. Verified before pushing, against the real consumers rather than in the abstract. The two repos that actually opt in via [publishing.readme] are boj-server and hyperpolymath; standards itself declares no such block and derives nothing. Both consumers' README.adoc reduce and convert with zero bytes on stderr and exit 0 under the new flag, so no consumer's README regeneration is blocked by this change. The step was then extracted from the YAML and run against a planted malformed table and a planted well-formed one: exit 1 with the diagnostic named, and exit 0, respectively. A guard nobody has watched fail is not evidence. The local-regeneration recipe printed on failure is updated to match, so a contributor following it cannot generate a README that CI would reject. --- .github/workflows/readme-derive-reusable.yml | 30 +++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/.github/workflows/readme-derive-reusable.yml b/.github/workflows/readme-derive-reusable.yml index 4e1541035..ca2df9602 100644 --- a/.github/workflows/readme-derive-reusable.yml +++ b/.github/workflows/readme-derive-reusable.yml @@ -170,10 +170,32 @@ jobs: CANONICAL: ${{ steps.decl.outputs.canonical }} run: | set -euo pipefail + # This step COMMITS what it generates, so a diagnostic that does not + # stop it is a wrong README baked into a consumer repo. Neither tool + # below reports cleanliness through its exit code by default, and + # each needs a different remedy: + # + # asciidoctor's --failure-level defaults to FATAL, so even + # "ERROR: dropping cells from incomplete row" exits 0 — one stray + # '|' in a table cell shifts every later cell and the derived + # Markdown silently loses them. The flag below makes the exit code + # mean what `set -e` already assumes it means. + # + # asciidoctor-reducer has no --failure-level at all (only + # --log-level, which changes what is printed, not what is + # returned), so its exit code cannot be made to carry its verdict. + # Its stderr is captured and treated as the verdict instead. + # # 1) flatten includes so the source is self-contained - asciidoctor-reducer "$CANONICAL" -o "$RUNNER_TEMP/reduced.adoc" + asciidoctor-reducer "$CANONICAL" -o "$RUNNER_TEMP/reduced.adoc" \ + 2>"$RUNNER_TEMP/reducer.err" + if [ -s "$RUNNER_TEMP/reducer.err" ]; then + echo "::error::asciidoctor-reducer reported a problem flattening $CANONICAL, so the derived README would be generated from a source it could not read cleanly. Its exit code is 0 either way, which is why this is checked on stderr." + cat "$RUNNER_TEMP/reducer.err" >&2 + exit 1 + fi # 2) reliable route: asciidoc -> docbook -> gfm (direct converters lose structure) - asciidoctor -b docbook5 -a attribute-missing=drop \ + asciidoctor --failure-level=WARN -b docbook5 -a attribute-missing=drop \ "$RUNNER_TEMP/reduced.adoc" -o "$RUNNER_TEMP/reduced.xml" pandoc -f docbook -t gfm --wrap=preserve \ "$RUNNER_TEMP/reduced.xml" -o "$RUNNER_TEMP/body.md" @@ -228,8 +250,8 @@ jobs: To regenerate locally (requires asciidoctor, asciidoctor-reducer, pandoc): - asciidoctor-reducer README.adoc -o /tmp/reduced.adoc - asciidoctor -b docbook5 -a attribute-missing=drop /tmp/reduced.adoc -o /tmp/reduced.xml + asciidoctor-reducer README.adoc -o /tmp/reduced.adoc # must print nothing + asciidoctor --failure-level=WARN -b docbook5 -a attribute-missing=drop /tmp/reduced.adoc -o /tmp/reduced.xml pandoc -f docbook -t gfm --wrap=preserve /tmp/reduced.xml -o /tmp/body.md # then prepend the SPDX + GENERATED banner (see the workflow's stamp step)