Skip to content

chore(repo): resolve subagent narration + tool-registration failures #195

chore(repo): resolve subagent narration + tool-registration failures

chore(repo): resolve subagent narration + tool-registration failures #195

Workflow file for this run

# Doc-corpus integrity CI gate. Pre-commit checks staged-impact-only; this
# workflow runs the full sweep on every PR and develop/main push.
#
# Three jobs, two risk classes:
# 1. lychee-inbound — file:// + anchor integrity (offline). PR-introduced
# regression risk we own. REQUIRED via docs-corpus-gate. Mirrors
# lefthook.yml's `docs-anchor-check` pre-commit hook for ADR-023
# §Axis 2 D-1 parity (pre-commit and CI verify the same invariant).
# 2. custom-checks — mermaid + cite-target + path-ripple + tooling tests.
# Same risk class as lychee-inbound — PR-introduced regression we own.
# REQUIRED via docs-corpus-gate.
# 3. lychee-outbound — outbound HTTP probe (online). Third-party drift /
# link rot we don't own (Cloudflare WAF rejecting bot UAs, Jenkins infra
# 502, GitHub-runner egress timeouts on slow remotes). ADVISORY — runs
# on every PR for visibility, but excluded from docs-corpus-gate so a
# flapping third party doesn't block merge on work unrelated to the
# failing URL. The split reflects the real error model: outbound
# failures mean a third-party server farted; inbound failures mean
# the PR introduced a broken anchor.
#
# Branch protection on develop and main MUST require the `docs-corpus-gate`
# job (only). Per ADR-023 §Axis 1, individual matrix legs are not stable
# required-check targets — the aggregator pattern is. The gate name is
# stable across the lychee-inbound/lychee-outbound split (its dependency
# set changed; its identity didn't), so no branch-protection update is
# needed when this change ships.
#
# Lychee install is duplicated across the inbound + outbound jobs (inline
# curl + sha256 verify against the v0.24.2 musl-static asset). The
# `LYCHEE_VERSION` constant in both jobs MUST stay in sync with the
# `VERSION` constant in `tools/install-lychee.mjs` (the pre-commit local
# installer) — three sites, one binary. Could be DRYed behind a
# `.github/actions/install-lychee` composite action; deferred to keep
# this PR scoped to the inbound/outbound split itself.
name: docs-corpus
on:
pull_request:
branches: [develop, main]
push:
branches: [develop, main]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
lychee-inbound:
name: lychee — inbound anchors (required)
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
# Inline install mirrors `tools/install-lychee.mjs` semantics: pin to
# v0.24.2, musl-static, SHA256-verified. `lycheeverse/lychee-action@v2`
# is broken for v0.24.x: its install script runs `tar -xvzf` without
# `--strip-components=1` and the binary lands at `lychee-<triple>/lychee`
# instead of the flat `lychee` it then tries to install.
# Keep `LYCHEE_VERSION` here in sync with the `VERSION` constant in
# `tools/install-lychee.mjs` AND with the lychee-outbound job below.
- name: Install lychee v0.24.2
run: |
set -euo pipefail
LYCHEE_VERSION="v0.24.2"
BASE="https://github.com/lycheeverse/lychee/releases/download/lychee-${LYCHEE_VERSION}"
ASSET="lychee-x86_64-unknown-linux-musl.tar.gz"
curl -fsSL "${BASE}/${ASSET}" -o /tmp/lychee.tar.gz
curl -fsSL "${BASE}/${ASSET}.sha256" -o /tmp/lychee.sha256
EXPECTED=$(awk '{print $1}' /tmp/lychee.sha256)
ACTUAL=$(sha256sum /tmp/lychee.tar.gz | awk '{print $1}')
if [ "${EXPECTED}" != "${ACTUAL}" ]; then
echo "SHA256 mismatch: expected ${EXPECTED}, got ${ACTUAL}" >&2
exit 1
fi
mkdir -p "${HOME}/.local/bin"
tar -xzf /tmp/lychee.tar.gz -C "${HOME}/.local/bin" --strip-components=1
chmod +x "${HOME}/.local/bin/lychee"
echo "${HOME}/.local/bin" >> "${GITHUB_PATH}"
- name: Run lychee (offline — inbound file refs + anchors)
# Mirrors `lefthook.yml`'s `docs-anchor-check` job: file:// resolution
# + anchor checks (`include_fragments = "full"` from `.lychee.toml`).
# ADR-023 §Axis 2 D-1 parity — pre-commit and CI verify the same
# inbound-integrity invariant against the same config. This is the
# PR-introduced regression we own: a broken `[text](./file.md#anchor)`
# cite from a code or doc edit fails here and blocks merge via the
# docs-corpus-gate aggregator.
run: |
lychee --offline --no-progress --config .lychee.toml './**/*.md'
lychee-outbound:
name: lychee — outbound HTTP (advisory)
runs-on: ubuntu-latest
# ADVISORY job — `docs-corpus-gate` does NOT depend on this job's
# result, so a 502/403/timeout from a third party (Jenkins outage,
# Cloudflare WAF, slow remote) does NOT block PR merge. The job runs
# on every PR + develop/main push so reviewers see drift signal in
# the checks UI.
#
# The probe step ALWAYS exits 0; outbound failures surface as
# `::warning::` annotations (yellow icon in the GitHub Actions UI,
# not red) so the workflow status reflects the actual signal class:
# "advisory drift detected, not regression-blocking". Without the
# explicit exit-0 + warning, the job's red ❌ propagates to the
# workflow header even though the gate is green — which reads as
# "CI failed" to a casual reviewer and undermines the inbound /
# outbound semantic split. Drift findings live in the run log
# above the warning annotation; CI noise is the cost of that
# visibility, but the noise should be yellow (advisory), not red
# (failure).
#
# The split is a behavior change from the prior single-`lychee` job
# (which conflated inbound-integrity with outbound-drift in one
# required check). Rationale: ADR-023 §Axis 2 D-1 frames inbound
# integrity as the load-bearing invariant; outbound was bolted on
# for drift detection. Drift detectors warn; gates fail-closed —
# they want different SLAs.
steps:
- name: Checkout
uses: actions/checkout@v5
# Identical to lychee-inbound's install step — same binary, same
# SHA, same pinned VERSION constraint. See the workflow header
# comment for the three-site sync contract.
- name: Install lychee v0.24.2
run: |
set -euo pipefail
LYCHEE_VERSION="v0.24.2"
BASE="https://github.com/lycheeverse/lychee/releases/download/lychee-${LYCHEE_VERSION}"
ASSET="lychee-x86_64-unknown-linux-musl.tar.gz"
curl -fsSL "${BASE}/${ASSET}" -o /tmp/lychee.tar.gz
curl -fsSL "${BASE}/${ASSET}.sha256" -o /tmp/lychee.sha256
EXPECTED=$(awk '{print $1}' /tmp/lychee.sha256)
ACTUAL=$(sha256sum /tmp/lychee.tar.gz | awk '{print $1}')
if [ "${EXPECTED}" != "${ACTUAL}" ]; then
echo "SHA256 mismatch: expected ${EXPECTED}, got ${ACTUAL}" >&2
exit 1
fi
mkdir -p "${HOME}/.local/bin"
tar -xzf /tmp/lychee.tar.gz -C "${HOME}/.local/bin" --strip-components=1
chmod +x "${HOME}/.local/bin/lychee"
echo "${HOME}/.local/bin" >> "${GITHUB_PATH}"
- name: Run lychee (online — outbound HTTP probes)
# Whole-repo HTTP probing for outbound link rot. `--include-fragments=
# none` disables anchor checking on HTTP-fetched pages (anchors on
# JS-rendered third-party docs aren't in the static HTML lychee parses
# — Node.js / NIST / Cloudflare / Docusaurus / Sphinx / Swagger UI all
# do this). Inbound `file.md#anchor` checks are still covered by the
# offline pass in the lychee-inbound job above. Accept-list for
# 200..=299 and 429 lives in `.lychee.toml`'s `accept = [...]` so
# the same policy applies whichever runner invokes lychee.
#
# Step-level retry absorbs transient GitHub-runner network flakes
# ("Connection failed" on healthy hosts that respond 200 from any
# other vantage). lychee's per-URL `max_retries` is already 2, but a
# whole-run flake (DNS hiccup, packet drop in the runner's egress
# path) retries every URL together. One additional outer attempt
# after a 15s pause is the empirical sweet spot — it catches the
# observed flakes (kubernetes.io, electron-vite.org, etcd.io) and
# still surfaces real link rot when both passes agree.
run: |
attempt() {
lychee --no-progress --config .lychee.toml --include-fragments=none './**/*.md'
}
if attempt; then
exit 0
fi
echo "lychee online pass failed; retrying once after 15s to absorb runner-network flakes" >&2
sleep 15
if attempt; then
exit 0
fi
# Both attempts failed. Surface as a warning (yellow icon) rather
# than a step failure (red icon) — this is the advisory drift
# channel, not a regression gate. Drift findings live in the run
# log above the annotation; reviewers see yellow on the workflow
# header (advisory) instead of red (failure), preserving the
# inbound / outbound semantic split per the job-level header.
echo "::warning title=lychee outbound rot::Outbound HTTP probe found broken external links (advisory — not blocking merge). See run log above for the failing URLs."
exit 0
custom-checks:
name: custom hooks — mermaid + cite + path-ripple + tests
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
- name: Setup pnpm
uses: pnpm/action-setup@8b2eead6074fefa43a68bf4c9e0f03ea4126b5ba # v6.0.3
with:
version: 10.33.2
run_install: false
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: "22.12"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: docs-corpus checks (full repo)
# Single runner composing path-canonical-ripple + mermaid-set-coherence +
# cite-target-existence. The runner reads argv for per-file checks
# (mermaid + cite, scoped to `.md`) and runs path-canonical-ripple
# unconditionally (whole-repo grep via the registry's `scope` globs).
run: |
mapfile -t files < <(git ls-files '*.md' | grep -v '^docs/archive/' || true)
node --experimental-strip-types tools/docs-corpus/bin/pre-commit-runner.ts "${files[@]}"
- name: Unit tests for hook implementations
run: pnpm exec vitest run --root tools/docs-corpus
docs-corpus-gate:
name: docs-corpus-gate
runs-on: ubuntu-latest
# Required-check aggregator. Depends ONLY on the load-bearing
# invariant jobs (lychee-inbound + custom-checks) — both verify
# PR-introduced regression risk we own. lychee-outbound is
# intentionally absent from `needs:` because its failures are
# third-party drift signal, not a PR-introduced regression. See
# the workflow header comment for the full advisory-vs-required
# rationale.
needs: [lychee-inbound, custom-checks]
if: always()
steps:
- name: Aggregate required-checks status
run: |
fail=0
if [[ "${{ needs.lychee-inbound.result }}" != "success" ]]; then
echo "docs-corpus-gate: required job 'lychee-inbound' did not succeed (result=${{ needs.lychee-inbound.result }})"
fail=1
fi
if [[ "${{ needs.custom-checks.result }}" != "success" ]]; then
echo "docs-corpus-gate: required job 'custom-checks' did not succeed (result=${{ needs.custom-checks.result }})"
fail=1
fi
if [[ $fail -ne 0 ]]; then
exit 1
fi
echo "docs-corpus-gate: all required jobs succeeded"