Sitelet https://github.com/opal/opal/commit/4c34cbbe4fdcd73523bc30b1da401f164a9f5889
Skip to content

Commit 4c34cbb

Browse files
eliaclaude
andcommitted
Check the docs in CI so they stop rotting
The audit that prompted this branch found a link to a page that never existed, a command that silently produced a directory named after an inspected IO object, and constants documented two major versions out of date. Nothing in CI looked at documentation, so all of it was found by reading. Add three checks, split by whether their result is decidable from the commit: - docs:links resolves every relative link between guides. Pages link each other in their rendered .html form, so an off-the-shelf checker either reports all of them as missing or skips them; this maps .html back to .md and resolves relative to the linking file. This is the check that would have caught headless_chrome.html. It blocks. - markdownlint, advisory rather than blocking. A formatting gate turns a drive-by typo fix into a red check a first-time contributor cannot debug. The annotations still appear on the pull request. Every disabled rule in the config says why it is disabled; the ones left on catch things that render wrong, such as the reversed links in cdp_common.md. - lychee for external URLs, on a schedule, opening a single rolling issue. Whether a third-party site is up has nothing to do with the diff under review, so failing a pull request for it reports the problem to the one person who cannot fix it. .yardopts listed two markdown files, leaving the other 30 guides out of the generated documentation entirely, and pointed at compiler_directives.md at its old path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 7f7212c commit 4c34cbb

7 files changed

Lines changed: 434 additions & 1 deletion

File tree

‎.github/workflows/docs.yml‎

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,146 @@
1+
# Documentation anti-rot gates.
2+
#
3+
# Split deliberately into blocking and non-blocking halves:
4+
#
5+
# * `docs` (on PRs) -- checks only things that are decidable from the contents
6+
# of the commit itself: do internal doc links resolve, and is the markdown
7+
# well formed. Deterministic, offline, fast. Safe to block a merge on.
8+
#
9+
# * `external-links` (cron) -- checks whether third-party URLs are still alive.
10+
# That depends on the internet, not on the diff, so it must never block a
11+
# merge: a PR touching the compiler should not go red because someone else's
12+
# blog went down overnight. It runs on a schedule and opens an issue instead.
13+
#
14+
# This is not a live-with-the-flakiness compromise; a link that rots in week 30
15+
# has nothing to do with whatever PR happens to be open in week 30, so gating a
16+
# PR on it reports the failure to the one person who cannot fix it.
17+
#
18+
# MAINTAINERS: do NOT mark `docs (blocking)` as a required status check in
19+
# branch protection. The `paths:` filters below mean it never runs at all on a
20+
# PR that touches no documentation, and a required check that never reports
21+
# leaves such PRs waiting forever on a check that will never arrive.
22+
name: docs
23+
24+
on:
25+
push:
26+
branches:
27+
- master
28+
- '*-stable'
29+
paths:
30+
- 'docs/**'
31+
- '*.md'
32+
- '.markdownlint-cli2.jsonc'
33+
- '.lycheeignore'
34+
- 'lychee.toml'
35+
- '.yardopts'
36+
- 'tasks/docs.rake'
37+
- '.github/workflows/docs.yml'
38+
pull_request:
39+
paths:
40+
- 'docs/**'
41+
- '*.md'
42+
- '.markdownlint-cli2.jsonc'
43+
- '.lycheeignore'
44+
- 'lychee.toml'
45+
- '.yardopts'
46+
- 'tasks/docs.rake'
47+
- '.github/workflows/docs.yml'
48+
schedule:
49+
# Mondays 06:17 UTC. Off-the-hour to dodge the top-of-hour scheduling crush
50+
# that makes GitHub delay or drop cron runs.
51+
- cron: '17 6 * * 1'
52+
workflow_dispatch: {}
53+
54+
permissions:
55+
contents: read
56+
57+
jobs:
58+
# ---------------------------------------------------------------------------
59+
# BLOCKING. Deterministic, no network.
60+
# ---------------------------------------------------------------------------
61+
docs:
62+
name: docs (blocking)
63+
if: github.event_name != 'schedule'
64+
runs-on: ubuntu-latest
65+
steps:
66+
- uses: actions/checkout@v4
67+
68+
- uses: ruby/setup-ruby@v1
69+
with:
70+
ruby-version: '3.4'
71+
# No `bundle install`: docs:links is plain stdlib Ruby and needs no
72+
# gems, so it is invoked as `rake`, not `bundle exec rake`. Installing
73+
# the bundle here would cost minutes for nothing.
74+
bundler-cache: false
75+
76+
# Internal link integrity. This is the check that would have caught
77+
# docs/index.md pointing at headless_chrome.html, a page that never
78+
# existed. It maps the published .html form back to the .md source and
79+
# resolves it relative to the linking file, so it understands the
80+
# docs/ layout instead of guessing.
81+
#
82+
# Run via `rake -f` so it works whether or not the root Rakefile has
83+
# picked up tasks/docs.rake yet.
84+
- name: Internal doc links resolve
85+
run: rake -f tasks/docs.rake docs:links
86+
87+
- uses: actions/setup-node@v4
88+
with:
89+
node-version: '20'
90+
91+
# Markdown lint. Config is tuned so the current tree passes; see the
92+
# comments in .markdownlint-cli2.jsonc for why each rule is off.
93+
#
94+
# Deliberately advisory (`continue-on-error`) rather than blocking. A hard
95+
# formatting gate turns a drive-by typo fix into a red X that a first-time
96+
# contributor cannot debug, and the maintainers end up pushing the fixup
97+
# anyway. The annotations still show up in the PR, so the signal is there
98+
# without the toll booth. Promote it to blocking later if the tree stays
99+
# clean on its own.
100+
- name: markdownlint
101+
continue-on-error: true
102+
run: npx --yes markdownlint-cli2@0.18.1
103+
104+
# ---------------------------------------------------------------------------
105+
# NON-BLOCKING. Touches the network, so it only ever reports.
106+
# ---------------------------------------------------------------------------
107+
external-links:
108+
name: external links (scheduled, non-blocking)
109+
if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
110+
runs-on: ubuntu-latest
111+
permissions:
112+
contents: read
113+
issues: write
114+
steps:
115+
- uses: actions/checkout@v4
116+
117+
- name: lychee
118+
id: lychee
119+
uses: lycheeverse/lychee-action@v2
120+
with:
121+
# --root-dir lets lychee resolve site-absolute links such as `/docs`
122+
# against the checkout instead of erroring on them.
123+
args: >-
124+
--config lychee.toml
125+
--root-dir ${{ github.workspace }}
126+
--no-progress
127+
docs/**/*.md
128+
README.md
129+
HACKING.md
130+
CONTRIBUTING.md
131+
AGENTS.md
132+
CONDUCT.md
133+
output: lychee/out.md
134+
# Do not let a dead third-party link fail the workflow run; the issue
135+
# below is the notification channel.
136+
fail: false
137+
138+
# Surface results as a single rolling issue rather than a red X. Reusing
139+
# one issue avoids opening a duplicate every Monday.
140+
- name: Report broken links
141+
if: steps.lychee.outputs.exit_code != 0
142+
uses: peter-evans/create-issue-from-file@v5
143+
with:
144+
title: 'Docs: broken external links detected'
145+
content-filepath: lychee/out.md
146+
labels: documentation

‎.lycheeignore‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Hosts and URL shapes that lychee must not report on.
2+
#
3+
# Each entry is a regex matched against the whole URL. Only add a host here if
4+
# it is *hostile to bots* rather than actually broken -- an entry here means we
5+
# stop hearing about that link forever, so a genuinely dead link hidden behind
6+
# one of these patterns will never be caught.
7+
8+
# --- Bot-hostile / anti-scraping (verified 403 or challenge page to non-browsers) ---
9+
# Slack's invite endpoint answers 403 to anything without a browser fingerprint.
10+
^https?://slack\.opalrb\.com
11+
# Cloudflare-fronted Q&A sites: 403/503 to datacenter IPs.
12+
^https?://(www\.)?stackoverflow\.com
13+
^https?://(www\.)?stackexchange\.com
14+
^https?://(meta\.)?stackoverflow\.com
15+
16+
# --- Rate limiters (429 under CI's shared egress IPs) ---
17+
^https?://(www\.)?opencollective\.com
18+
^https?://(www\.)?patreon\.com
19+
^https?://(www\.)?bountysource\.com
20+
^https?://(www\.)?reddit\.com
21+
^https?://(www\.)?twitter\.com
22+
^https?://(www\.)?x\.com
23+
^https?://(www\.)?linkedin\.com
24+
25+
# --- Shields/badge endpoints: dynamic SVG, frequently 429, never "documentation" ---
26+
^https?://img\.shields\.io
27+
^https?://badge\.fury\.io
28+
^https?://badgen\.net
29+
^https?://.*\.svg(\?.*)?$
30+
^https?://coveralls\.io
31+
^https?://codeclimate\.com
32+
^https?://api\.codeclimate\.com
33+
^https?://travis-ci\.(org|com)
34+
^https?://github\.com/.*/(actions|workflows)/.*badge
35+
^https?://github\.com/.*\.svg
36+
37+
# --- Local/example hosts that appear in code samples and are never resolvable ---
38+
^https?://localhost
39+
^https?://127\.0\.0\.1
40+
^https?://0\.0\.0\.0
41+
^https?://\[::1\]
42+
^https?://example\.(com|org|net)
43+
^https?://(www\.)?your-?(site|domain|app|server).*
44+
^https?://.*\.local(/|$)
45+
^https?://.*:(3000|4567|8000|8080|9222|9229|9292)(/|$)
46+
47+
# --- Template/placeholder URLs from docs prose ---
48+
.*\{.*\}.*
49+
.*<.*>.*
50+
.*%7B.*
51+
^https?://.*\.\.\..*
52+
53+
# --- Site-absolute paths ---
54+
# `/docs` points at the guides index on the published opalrb.com site. With
55+
# --root-dir set to the checkout, lychee resolves it against the repo, where
56+
# `docs/` does exist -- so it is checked rather than ignored.
57+
58+
# --- GitHub blob/tree links to this repo's own files ---
59+
# These are checked by `rake docs:links` against the working tree instead, which
60+
# is both faster and correct on branches where the file is new.
61+
^https?://github\.com/opal/opal/(blob|tree)/

‎.markdownlint-cli2.jsonc‎

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// markdownlint-cli2 configuration for Opal's prose.
2+
//
3+
// Tuning philosophy: this gate blocks pull requests, so it may only flag things
4+
// that are unambiguously mistakes. Rules that merely disagree with the house
5+
// style are turned off rather than left on with a backlog of pre-existing
6+
// failures — a check that starts red gets ignored, then disabled, then the docs
7+
// rot anyway. Every disable below is justified against the actual tree.
8+
{
9+
"globs": [
10+
"docs/**/*.md",
11+
"*.md"
12+
],
13+
14+
"ignores": [
15+
"node_modules",
16+
"vendor",
17+
// Append-only release log: 900+ findings, machine-assembled from commit
18+
// subjects, and rewriting history to satisfy a linter has negative value.
19+
"CHANGELOG.md",
20+
// Staging area for the next CHANGELOG entry, same shape and same reasons.
21+
"UNRELEASED.md",
22+
// `__`-prefixed files are personal scratch notes, not published docs.
23+
"__*.md"
24+
],
25+
26+
"config": {
27+
"default": true,
28+
29+
// MD013 line-length: the house style hard-wraps prose near 100 but tables,
30+
// long URLs, and code samples routinely exceed any limit. 678 findings.
31+
// Enforcing this would mean reflowing every guide for zero reader benefit.
32+
"MD013": false,
33+
34+
// MD012 no-multiple-blanks: double blank lines are used deliberately to
35+
// separate sections in the longer references. 376 findings, all cosmetic.
36+
"MD012": false,
37+
38+
// MD007 ul-indent / MD004 ul-style / MD005 list-indent / MD030
39+
// list-marker-space: the docs mix `-`/`*` markers and 2/4-space indents
40+
// across files written over a decade. All render identically.
41+
"MD007": false,
42+
"MD004": false,
43+
"MD005": false,
44+
"MD030": false,
45+
46+
// MD024 no-duplicate-heading: reference pages legitimately repeat headings
47+
// like "Example", "Options" and "See also" under different parents, and the
48+
// CLI/config references repeat one option name per section.
49+
"MD024": false,
50+
51+
// MD033 no-inline-html: README badges and the docs' `<details>` blocks need
52+
// raw HTML; markdown has no equivalent.
53+
"MD033": false,
54+
55+
// MD034 no-bare-urls: bare URLs in prose and code output are intentional --
56+
// wrapping compiler output in angle brackets would misrepresent it.
57+
"MD034": false,
58+
59+
// MD014 commands-show-output: shell samples deliberately use `$` prompts
60+
// even without output so readers can tell commands from output.
61+
"MD014": false,
62+
63+
// MD046 code-block-style / MD048 / MD049 / MD050: fenced vs indented blocks
64+
// and `*`/`_` emphasis both appear; neither is wrong.
65+
"MD046": false,
66+
"MD049": false,
67+
"MD050": false,
68+
69+
// MD041 first-line-heading: some partials/fragments open with context
70+
// rather than an h1.
71+
"MD041": false,
72+
73+
// MD036 no-emphasis-as-heading: bolded lead-ins inside list items are a
74+
// deliberate device, not a substitute heading.
75+
"MD036": false,
76+
77+
// MD026 no-trailing-punctuation: a few headings are questions ("Why?").
78+
"MD026": false,
79+
80+
// MD060 table-column-style: padding inside table pipes (`|---|` vs
81+
// `| --- |`) is invisible once rendered. The reference pages are heavily
82+
// tabular, so this is 181 findings of pure whitespace preference.
83+
"MD060": false,
84+
85+
// --- Rules kept ON: these catch real defects ---
86+
// MD011 no-reversed-links -> `(text)[url]` is always a typo
87+
// MD038 no-space-in-code -> `` ` code ` `` renders wrong
88+
// MD009 no-trailing-spaces -> invisible, causes stray <br>
89+
// MD010 no-hard-tabs -> breaks code block alignment
90+
// MD040 fenced-code-language -> unlabelled fences lose highlighting
91+
// MD001/MD003/MD022/MD025 -> heading structure, affects generated TOCs
92+
// MD031/MD032 -> missing blanks break rendering in some parsers
93+
// MD045 no-alt-text -> accessibility
94+
"MD040": {
95+
// Allow bare fences only where the content genuinely has no language
96+
// (plain terminal output). Everything else must be labelled.
97+
"allowed_languages": [
98+
"ruby", "js", "javascript", "json", "sh", "bash", "shell", "console",
99+
"erb", "haml", "html", "css", "yaml", "yml", "diff", "text", "plain",
100+
"opal", "jsx", "ts", "xml", "sql", "csv", "make", "dockerfile"
101+
],
102+
"language_only": false
103+
}
104+
}
105+
}

‎.yardopts‎

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,12 @@ lib/opal/**/*.rb
33
--markup=markdown
44
--markup-provider=redcarpet
55
--hide-void-return
6+
--main docs/index.md
67
-
8+
docs/index.md
9+
docs/tutorial/*.md
10+
docs/how-to/*.md
11+
docs/reference/*.md
12+
docs/explanation/*.md
13+
docs/contributing/*.md
714
CHANGELOG.md
8-
docs/compiler_directives.md

‎AGENTS.md‎

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,11 @@ codebase healthy.
66

77

88
## Setup
9+
910
- Run `bin/setup` once after cloning to install gems, yarn packages and git submodules.
1011

1112
## Directory overview
13+
1214
- `lib/` holds the compiler and CLI implementations.
1315
- `opal/` provides the runtime and Ruby core library. Its layout is:
1416
- `corelib/` contains Ruby's built-ins implemented in Ruby.
@@ -22,6 +24,7 @@ codebase healthy.
2224
- `tasks/` defines Rake tasks used by `bin/rake`.
2325

2426
## Running tests
27+
2528
- Running `bin/rake` executes every suite on both Chrome and Node.js but is slow and requires a browser.
2629
- For day-to-day work rely on the Node.js tasks and ensure they pass before committing:
2730
- `bin/rake rspec` runs the RSpec suite covering Opal compiler and runtime internals.
@@ -33,9 +36,11 @@ codebase healthy.
3336
- `bin/rake minitest_nodejs` runs both Minitest suites on Node.js.
3437

3538
## Linting
39+
3640
- Run `bin/rake lint` to check code style. This builds the corelib and stdlib then executes RuboCop and ESLint.
3741

3842
## Notes
43+
3944
- The list of MSpec files is in `spec/ruby_specs` and filters live in `spec/filters`.
4045
- Tests depend on initialized submodules (`spec/mspec`, `spec/ruby`, `test/cruby`).
4146
- If an agent discovers information that required significant setup time, condense

0 commit comments

Comments
 (0)