diff --git a/.github/workflows/readme-derive-reusable.yml b/.github/workflows/readme-derive-reusable.yml index 2710f161..bbcb1789 100644 --- a/.github/workflows/readme-derive-reusable.yml +++ b/.github/workflows/readme-derive-reusable.yml @@ -110,11 +110,22 @@ jobs: - name: Install toolchain if: steps.decl.outputs.derive == 'true' + env: + # Pinned so derivation is REPRODUCIBLE: the same versions used to + # generate the committed README.md must run in CI, or the freshness + # check would flag cosmetic toolchain-drift diffs as staleness. Bump + # deliberately (and regenerate every derived README) — never float. + PANDOC_VERSION: "3.10" + ASCIIDOCTOR_VERSION: "2.0.26" + ASCIIDOCTOR_REDUCER_VERSION: "1.1.1" run: | - sudo apt-get update -qq - sudo apt-get install -y -qq pandoc - # asciidoctor + include-reducer from rubygems (ubuntu ships ruby) - sudo gem install --no-document asciidoctor asciidoctor-reducer + set -euo pipefail + curl -fsSL -o /tmp/pandoc.deb \ + "https://github.com/jgm/pandoc/releases/download/${PANDOC_VERSION}/pandoc-${PANDOC_VERSION}-1-amd64.deb" + sudo dpkg -i /tmp/pandoc.deb + sudo gem install --no-document \ + asciidoctor -v "$ASCIIDOCTOR_VERSION" \ + asciidoctor-reducer -v "$ASCIIDOCTOR_REDUCER_VERSION" pandoc --version | head -1 asciidoctor --version | head -1 @@ -126,6 +137,31 @@ jobs: https://github.com/hyperpolymath/standards.git "$RUNNER_TEMP/standards" chmod +x "$RUNNER_TEMP/standards/.github/scripts/readme-vocab-gate.sh" + - name: Anchor-ID convention guard + if: steps.decl.outputs.derive == 'true' + env: + CANONICAL: ${{ steps.decl.outputs.canonical }} + run: | + set -euo pipefail + # A canonical .adoc with intra-document anchor links (link:#foo[] or + # <> written as #foo) only renders a working TOC on GitHub and + # re-derives to matching Markdown anchors when it opts out of + # Asciidoctor's default '_prefixed_underscored' section IDs. So if the + # source uses #anchors, it MUST set :idprefix: (empty) and + # :idseparator: - — otherwise every TOC link silently dead-links + # (a failure the vocabulary gate cannot see, since it checks words not + # link targets). See ADR-004 "Bootstrapping a canonical .adoc". + if grep -qE 'link:#[a-z0-9-]+\[' "$CANONICAL"; then + if ! grep -qE '^:idprefix:[[:space:]]*$' "$CANONICAL" \ + || ! grep -qE '^:idseparator:[[:space:]]+-[[:space:]]*$' "$CANONICAL"; then + echo "::error::$CANONICAL has intra-doc anchor links (link:#…) but does not set ':idprefix:' (empty) and ':idseparator: -' in its header. Without them Asciidoctor generates '_underscored' section IDs and every #anchor TOC link dead-links on GitHub and in the derived README.md. Add both attributes to the document header (see ADR-004)." + exit 1 + fi + echo "✓ anchor-ID convention satisfied (idprefix/idseparator set)" + else + echo "✓ no intra-doc anchors — convention not required" + fi + - name: Derive Markdown from AsciiDoc if: steps.decl.outputs.derive == 'true' env: @@ -165,13 +201,21 @@ jobs: CANONICAL: ${{ steps.decl.outputs.canonical }} run: | set -euo pipefail + # Normalise before comparing: drop the banner comment lines, trim + # trailing whitespace, and squeeze blank-line runs. The toolchain is + # version-pinned so output is reproducible; this normalisation is a + # belt-and-braces layer that keeps a harmless whitespace nudge from + # being reported as staleness, while any real prose/structure/anchor + # change still shows. + norm() { grep -vE '^