Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
58 changes: 51 additions & 7 deletions .github/workflows/readme-derive-reusable.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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
# <<foo>> 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:
Expand Down Expand Up @@ -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 '^<!-- (SPDX-License-Identifier|GENERATED)' "$1" \
| sed 's/[[:space:]]*$//' | cat -s; }
if [ ! -f "$DERIVED_PATH" ]; then
echo "::error::Declared derived README '$DERIVED_PATH' is missing. Regenerate and commit it."
SHOW_CMD=1
elif ! diff -q "$RUNNER_TEMP/derived.md" "$DERIVED_PATH" >/dev/null; then
elif ! diff -q <(norm "$RUNNER_TEMP/derived.md") <(norm "$DERIVED_PATH") >/dev/null; then
echo "::error::'$DERIVED_PATH' is STALE relative to '$CANONICAL'. Regenerate and commit it."
echo "----- diff (committed vs regenerated) -----"
diff "$DERIVED_PATH" "$RUNNER_TEMP/derived.md" || true
echo "----- diff (committed vs regenerated, normalised) -----"
diff <(norm "$DERIVED_PATH") <(norm "$RUNNER_TEMP/derived.md") || true
SHOW_CMD=1
else
echo "✓ $DERIVED_PATH is up to date with $CANONICAL"
Expand Down
50 changes: 50 additions & 0 deletions docs/decisions/ADR-004-readme-single-source-derive.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,56 @@ Rules:
* Any estate sweep touching README format MUST read this block first
and skip undeclared repos. This is the anti-runaway guard.

=== Bootstrapping a canonical `.adoc` from an existing `README.md`

The derive direction (`.adoc → .md`) is a clean one-shot. Producing the
*initial* canonical `.adoc` from a rich existing `README.md` is a
one-time authoring step, not part of the CI path, and has one sharp edge
verified on boj-server's real README (264 lines, 12 badges, 15-entry TOC):

**Anchor-ID convention (mandatory when the README has an intra-doc TOC).**
GitHub Markdown auto-anchors headings as lowercase-hyphenated
(`## Capabilities overview` → `#capabilities-overview`). Asciidoctor
instead defaults to `_prefixed_underscored` IDs
(`_capabilities_overview`), so a converted `link:#capabilities-overview[]`
TOC would dead-link on GitHub *and* re-derive to a broken Markdown anchor.
The canonical `.adoc` MUST therefore set, in its document header:

[source,asciidoc]
----
= Project Title
:idprefix:
:idseparator: -
----

With those two attributes, `== Features` → id `features`,
`== Capabilities overview` → id `capabilities-overview` — matching GitHub
Markdown exactly. `readme-derive-reusable.yml` enforces this: if the
canonical source contains `link:#…[]` anchors without both attributes, the
job fails-and-tells (the vocabulary gate cannot catch this — it checks
words, not link targets).

**One-time bootstrap recipe** (verified end-to-end on boj-server —
faithful round-trip: 11/11 TOC anchors resolve, 12/12 badges intact,
≥98% vocab, only AsciiDoc scaffolding tokens "lost"):

[source,bash]
----
# 1. md -> asciidoc body
pandoc -f gfm -t asciidoc --wrap=preserve README.md -o body.adoc
# 2. promote every heading one level (== -> =, === -> ==, …) so the top
# heading becomes the '= doctitle' and section nesting stays correct
sed -E 's/^==(=*) /=\1 /' body.adoc > body2.adoc
# 3. assemble canonical README.adoc: SPDX header + '= Title' +
# :idprefix:/:idseparator: - + body2 (minus its first title line)
# 4. sanity: render to HTML, confirm section ids match the TOC anchors
----

pandoc wraps inline code containing `_ * { +` in AsciiDoc passthroughs
(`++_*++`, `{plus}`) — correct and harmless (renders as literal text; the
derived `.md` comes back clean), just visible noise in the source. Accept
it, or tidy by hand; do not "fix" it in a way that changes rendered output.

=== Derivation pipeline (reusable workflow, `standards`)

One reusable workflow, `readme-derive.yml`, called by opted-in repos
Expand Down
Loading