Repository navigation
ci: post a nightly afdocs report on the docs to #docs - #39484
Merged
Merged
Conversation
bosconi
added a commit
that referenced
this pull request
Oct 2, 2026
Agent-friendly docs change 3, revised after review. The preview step ran `npx --yes afdocs@0.22.2` in the job that holds $GITHUB_TOKEN for the status update and the AWS credentials for `hugo deploy`, on every docs PR. npm resolves afdocs' dependencies fresh on each run, so a compromised dependency could read those credentials. Scrubbing the environment would not cover credentials an agent can reach without it, such as an instance profile. Drop the afdocs run. The step keeps check-docs-markdown.py, which uses the Python standard library. The nightly afdocs report on production, with afdocs pinned by a lockfile and run without secrets, is in #39484. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bosconi
added a commit
that referenced
this pull request
Oct 16, 2026
Linear: [EDU-101](https://linear.app/materializeinc/issue/EDU-101/make-the-materialize-docs-agent-friendly) ### Motivation Coding agents read materialize.com/docs through the Markdown tree at `/docs/markdown-docs/` and through `/docs/llms.txt`, and both have defects that cost agents context or send them to dead links. [afdocs](https://afdocs.dev) 0.22.2, the checker for the [Agent-Friendly Documentation Spec](https://agentdocsspec.com/spec/web/), scores materialize.com/docs 74. Comparable docs sites score higher: RisingWave 96, Cloudflare 99. This PR makes the seven agent-friendly docs changes that can be built and checked locally. Each commit names the change it belongs to: | # | Change | Commits | |---|---|---| | 1 | Section pages list their children instead of embedding them | e30cbf6 | | 2 | Links in the Markdown are absolute | fe2c7b6, e4eb365, 793a9ea | | 3 | Docs previews report Markdown sizes, broken and relative links, and unrendered shortcodes; docs lint gates on links | 36ac7d5, 78b9181, 1b23cc0, bab4efe, 793a9ea | | 4 | Pages point agents at `llms.txt` and their Markdown | 20862e0, 1b9563e | | 5 | Markdown content cleanup: heading attributes, `<no value>`, two wrong descriptions | c34306f | | 6 | The docs sitemap has absolute URLs and is served as `application/xml` | 7e0bf94, 0db7b05 | | 7 | `/docs/llms.txt` splits into per-section indexes, which also list the per-version release notes | 958cc4a, 7b75276 | The nightly afdocs report to the #docs Slack channel is now its own PR, [#39484](#39484), as suggested in review. The preview step no longer runs afdocs: it ran `npx` in the job that holds `$GITHUB_TOKEN` and the S3 credentials, and #39484 runs afdocs from a lockfile in a job without secrets. Three more changes are in this repo but need a design decision or the docs owners, and are not in this PR: - **8, Render shortcodes nested in YAML data in the Markdown.** 29 Markdown pages, including the MySQL and PostgreSQL ingestion guides and the `CREATE TABLE` pages, carry literal `{{< ... >}}` text where the HTML shows content. The `.skill.md` shortcodes print a data field as written, while the HTML ones render shortcodes nested in it. - **9, Split the three Markdown pages over 100,000 characters:** `releases/`, `observability/appendix-metrics/` and `sql/system-catalog/mz_internal/`. - **10, Give pages distinct descriptions.** Eight descriptions are shared by two or three pages. Three more need the web team or the CDN owners, and are not in this PR: - **11, Markdown at the page URL.** Publish each page's Markdown at `/docs/<path>/index.md`, in `ci/deploy_website/website.sh`, with its alias redirects, and link that form from `llms.txt`. It can only be tested by deploying. - **12, Content before navigation.** Move the sidebar after `<main>` in `baseof.html`, and render less of the navigation tree on each page. It needs visual and accessibility review. - **13, CloudFront answering `Accept: text/markdown`.** This will let agents such as Claude Code, Cursor and OpenCode get Markdown from any page URL. `afdocs` ceiling is 97 without this :-) The distribution is `materialize-com` in [`i2/website.py`](https://github.com/MaterializeInc/i2/blob/main/i2/website.py). ### Description Each commit message explains its change. Decisions the diff does not show: - **The website and the skill bundle are told apart by base URL.** The Markdown is built twice from the same templates: by `website.sh`, with `--baseURL /docs`, and by `bin/gen-claude-skill`, with none. Change 1's index links and change 2's link rewrite produce absolute URLs in the website build. In the skill bundle they produce bundle-root paths, which is the form the bundle's existing links use. Change 4's `llms.txt` pointer is website-only, because the bundle has no `llms.txt`. The `markdown-url` partial already keyed off the base URL in the same way. - **New partials.** - `site-origin` is extracted from `markdown-url`, so changes 2, 4 and 6 share one origin. - `markdown-body` post-processes `.RenderShortcodes` for changes 2, 4 and 5, and `markdown-link-target` maps each link it finds for change 2. - `markdown-link-url`, `llms-txt-url`, `has-markdown` and `absolute-permalink` each do one job, described in their header comments. - **The HTML pointer in change 4 uses base-relative URLs.** `htmltest` then checks them against the built files, instead of treating them as external links to the lint build's host. - **Change 7 and [MaterializeInc/agent-skills#84](MaterializeInc/agent-skills#84 That PR has the `mz-docs` skill read `/docs/llms.txt` to find pages. It also follows per-section indexes, and was tested against this PR's preview, so the two can land in either order. - **Not fixed.** The sitemap's two future `lastmod` dates come from the scheduled v26.45 release notes. One absolute link in the v0.27 release notes, to `docs/lts/release-notes/`, already returns 404. ### Verification Built with Hugo 0.152.2, the version `ci/builder` installs, and compared with a build of `origin/main`. | Measurement | origin/main | This PR | |---|---|---| | Markdown pages over 100,000 characters | 14 | 3 | | Markdown pages over 50,000 characters | 40 | 11 | | `sql/index.md` | 750,064 bytes | 26,135 bytes | | Relative links in the Markdown, root-relative and path-relative | 9,651 and 2,148 | 0 and 0 | | Rewritten links whose target is not a built file | N/A | 1 of 8,244, the 404 link above | | Markdown pages with unrendered shortcode syntax | 35 | 29 | | Absolute sitemap `<loc>` values | 0 of 565 | 565 of 565 | | `/docs/llms.txt` | 85,604 characters | 1,655 characters, plus 15 section indexes of up to 30,991 | | Sitemap pages listed in `llms.txt` | 488 of 565 | 564 of 565, all but the docs home | | Skill bundle | 9,851,663 bytes | 5,142,901 bytes | - **afdocs, scored locally.** Both builds were served with Markdown as `text/markdown`, and scored with `--canonical-origin https://materialize.com/docs` on the same 49 pages, every tenth page in `llms.txt`. - The `origin/main` build scores 74, the same as production. - With changes 1 to 6 it scores 81, and with all seven, 83. - With changes 11 and 13 also simulated in the local server, including `llms.txt` links of the form `/docs/<path>/index.md`, it scores 97, and 98 when afdocs chooses its own 50 pages. `markdown-link-portability` and `llms-txt-coverage` pass there. - **What afdocs still fails.** `markdown-url-support`, `content-negotiation` and `llms-txt-directive-md` fail until change 11 or 13 lands. `llms-txt-coverage` fails until change 11 switches `llms.txt` to the page-URL form, because afdocs cannot map `markdown-docs/<path>/index.md` links back to pages. `content-start-position` and `page-size-html` need change 12, and `page-size-markdown` change 9. - **HTML.** The HTML output is unchanged apart from change 4's pointers and change 5's two descriptions. - **The bundle's links are unchanged.** Every one still points at the same file under the bundle root, and the bundle differs only by changes 1 and 5. - **Lints.** - The `hugo` and `htmltest` steps of `ci/test/lint-docs.sh` pass locally. - `check-copyright`, `check-whitespace`, ruff 0.16.5 and pyright 1.1.407 `--warnings` pass on the changed files. - **Change 3 ran in CI.** In build 137194, the preview's Markdown check found 0 broken and 0 relative links, as it did locally. - **Link regression gate.** `lint-docs.sh` now runs the Markdown checker in link-only failure mode. Four new fixture tests cover broken Markdown links, relative links, doubled `/docs/` prefixes, and warning-only size/shortcode findings. On local production and preview builds, the three link counts were 0; the existing 3 oversized pages and 29 pages with raw shortcodes remained warnings. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Add the "Agent-friendly docs report" workflow. Every day at 06:00 UTC it scores materialize.com/docs with afdocs (https://afdocs.dev), which checks a docs site against the Agent-Friendly Documentation Spec, and posts to the #docs Slack channel: - a one-line summary, the same at any score but for its emoji: ✅ at 100 and ❌ below it; - as a reply in its thread, the checks that fail or warn and the full report. afdocs and its npm dependencies never run where a secret is available: - ci/test/afdocs/package.json and package-lock.json pin afdocs 0.22.2 and its 64 dependencies by integrity hash, installed with `npm ci --ignore-scripts`. - The `score` job holds no secrets. ci/test/afdocs-report.py runs afdocs with only PATH and HOME in its environment, and writes the afdocs JSON, the message and the thread reply to an artifact. - The `post` job runs on a fresh runner, downloads that artifact, and posts it with SLACK_TOKEN. It runs only the script, which uses the Python standard library. A manual run defaults to a dry run, which prints the message and the reply in the "Score the docs" step's log and skips the `post` job. Pull requests that change the workflow, the script or the pins always dry-run. The script samples every tenth page in llms.txt, following per-section llms.txt files when the root lists those, and exits with an error when llms.txt lists no pages. It escapes &, < and > in afdocs' messages for Slack. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
bosconi
force-pushed
the
jc/afdocs-report
branch
from
October 17, 2026 00:12
6d3a82b to
7a44caf
Compare
bosconi
marked this pull request as ready for review
October 17, 2026 00:13
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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Linear: EDU-101
Motivation
afdocs checks a docs site against the Agent-Friendly Documentation Spec. materialize.com/docs scores 74 today, and #39470 makes the template changes that raise it. This workflow reports the score to #docs every night, so the docs team sees it move and sees regressions.
It was part of #39470, and is split out here so that its secret handling gets its own review, as suggested there.
Description
The "Agent-friendly docs report" workflow runs every day at 06:00 UTC / 02:00 EDT. It scores materialize.com/docs with afdocs on every tenth page listed in
llms.txt, and posts to #docs.postjob. Pull requests that change the workflow,ci/test/afdocs-report.pyorci/test/afdocs/always dry-run.afdocs and its npm dependencies never run where a secret is available:
ci/test/afdocs/package.jsonandpackage-lock.jsonpin afdocs 0.22.2 and its 64 dependencies by integrity hash. The workflow installs them withnpm ci --ignore-scripts, so no install script runs and npm resolves nothing fresh.scorejob holds no secrets, and checks out withpersist-credentials: false.afdocs-report.py scoreruns afdocs with onlyPATHandHOMEin its environment. It writes the afdocs JSON, the message and the thread reply, which the job uploads as an artifact.postjob downloads the artifact and posts it withSLACK_TOKEN. It runs onlyafdocs-report.py post, which uses the Python standard library, so no npm code runs on that runner.Slack token: the workflow uses the repo's existing
SLACK_TOKENsecret, the one theslack_notify_*workflows use, so no new secret is needed if that bot is a member of #docs. The report goes in a thread reply rather than an attached file, because uploading a file needs thefiles:writescope, andSLACK_TOKEN's scopes are unknown.afdocs-report.pyreadsllms.txtwhether it lists the pages itself, as on production today, or links onellms.txtper section, as after #39470. It exits with an error whenllms.txtlists no pages, rather than calling afdocs with no URLs. It escapes&,<and>in afdocs' messages for Slack, and sendsunfurl_linksas a boolean.Verification
afdocs-report.py score https://materialize.com/docsscored 74 over 49 pages, with 9 of 28 checks failing or warning, and wrote the JSON, the message and the reply.SLACK_TOKEN,GITHUB_TOKENandAWS_SECRET_ACCESS_KEYset, a stand-in afdocs binary saw onlyHOME,PATHand the variables the shell sets itself.postwas run against a stubbed Slack API. It sent the message, then the reply with the message'sthread_ts, withunfurl_linksasfalse.--warnings, zizmor--persona=pedanticwith the repo's config,check-copyrightandcheck-whitespacepass.🤖 Generated with Claude Code