chore(repo): resolve subagent narration + tool-registration failures #195
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # 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" |