Skip to content

ci: post a nightly afdocs report on the docs to #docs - #39484

Merged
bosconi merged 1 commit into
mainfrom
jc/afdocs-report
Oct 19, 2026
Merged

bosconi merged 1 commit into
mainfrom
jc/afdocs-report

Conversation

@bosconi

@bosconi bosconi commented Oct 2, 2026

Copy link
Copy Markdown
Member

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.

  • Top-level message: always one line, the same shape at any score, differing only in its emoji. For example: ":x: The docs score 74 / 100 for agent-friendliness on afdocs, over 49 pages: 9 of 28 checks fail or warn. Workflow run." At 100 the emoji is :white_check_mark:.
  • Thread reply: the checks that fail or warn, one bullet each, then the full report with every check and the category scores. A run at 100 gets the reply too, so every run has the same shape and the full report.
  • Dry run: a manual run is a dry run by default. It prints the message and the reply in the log of the "Score the docs" step, and skips the post job. Pull requests that change the workflow, ci/test/afdocs-report.py or ci/test/afdocs/ always dry-run.

afdocs and its npm dependencies never run where a secret is available:

  • Pinned dependency tree. ci/test/afdocs/package.json and package-lock.json pin afdocs 0.22.2 and its 64 dependencies by integrity hash. The workflow installs them with npm ci --ignore-scripts, so no install script runs and npm resolves nothing fresh.
  • Scoring without secrets. The score job holds no secrets, and checks out with persist-credentials: false. afdocs-report.py score runs afdocs with only PATH and HOME in its environment. It writes the afdocs JSON, the message and the thread reply, which the job uploads as an artifact.
  • Posting on a fresh runner. The post job downloads the artifact and posts it with SLACK_TOKEN. It runs only afdocs-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_TOKEN secret, the one the slack_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 the files:write scope, and SLACK_TOKEN's scopes are unknown.

afdocs-report.py reads llms.txt whether it lists the pages itself, as on production today, or links one llms.txt per section, as after #39470. It exits with an error when llms.txt lists no pages, rather than calling afdocs with no URLs. It escapes &, < and > in afdocs' messages for Slack, and sends unfurl_links as a boolean.

Verification

  • Dry run against production: afdocs-report.py score https://materialize.com/docs scored 74 over 49 pages, with 9 of 28 checks failing or warning, and wrote the JSON, the message and the reply.
  • Message at 100: checked by substituting the score in the saved report; only the emoji and the counts change.
  • Scrubbed environment: with SLACK_TOKEN, GITHUB_TOKEN and AWS_SECRET_ACCESS_KEY set, a stand-in afdocs binary saw only HOME, PATH and the variables the shell sets itself.
  • Posting: post was run against a stubbed Slack API. It sent the message, then the reply with the message's thread_ts, with unfurl_links as false.
  • Lints: ruff 0.16.5, pyright 1.1.407 --warnings, zizmor --persona=pedantic with the repo's config, check-copyright and check-whitespace pass.
  • This PR's own run: the workflow runs on this PR as a dry run.

🤖 Generated with Claude Code

@linear-code

linear-code Bot commented Oct 2, 2026

Copy link
Copy Markdown

EDU-101

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>
@bosconi
bosconi requested a review from antiguru October 16, 2026 23:59
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
bosconi marked this pull request as ready for review October 17, 2026 00:13
@bosconi
bosconi requested a review from a team as a code owner October 17, 2026 00:13

@antiguru antiguru left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

@bosconi
bosconi merged commit 975fff6 into main Oct 19, 2026
88 checks passed
@bosconi
bosconi deleted the jc/afdocs-report branch October 19, 2026 05:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants