Run the code blocks in your README. Fail CI when your quickstart breaks.
Every README promises npm install && npm start works. Then a dependency
bumps, a flag is renamed, a file moves – and the first thing every new user
sees is broken. Nobody notices, because nobody executes their docs.
readme-ci executes them. It extracts the fenced code blocks from your
markdown, runs them top-to-bottom in an isolated sandbox – state carries over
between blocks exactly like a reader following along – and exits non-zero the
moment a step breaks. Put it in CI and your quickstart can never silently rot
again.
$ readme-ci examples/demo.md
examples/demo.md
✓ examples/demo.md:10 [bash] mkdir hello-app && cd hello-app (0.4s)
✓ examples/demo.md:18 [bash] test -f package.json (0.3s)
✓ examples/demo.md:26 [js] const pkg = { name: 'hello-app' }; (0.5s)
✓ examples/demo.md:30 [python] print("2 + 2 =", 2 + 2) (0.4s)
✓ examples/demo.md:36 [console] $ echo it works (0.3s)
○ examples/demo.md:44 [bash] curl -fsSL https://example.com/install.sh | sh – skipped (directive)
✓ 5 passednpx readme-ciBy default blocks run in a throwaway Docker container (node:22-bookworm),
so your machine is never touched. One container per file – cd, exported
variables and installed packages persist from block to block.
Or as a GitHub Action:
jobs:
readme:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: ondraulehla/readme-ci@main
with:
files: README.md docs/getting-started.md- Fenced blocks are extracted with their line numbers.
- Each block is planned by language:
bash/sh/shell/zsh→ run as a shell script (set -e)console/terminal→ only the$-prefixed lines run; output lines are prosejs/javascript→ run withnodepython/py→ run withpython3- anything else (
json,yaml, …) → ignored automatically
- Blocks execute in order inside one session. Working directory and environment survive between blocks, so multi-step quickstarts just work.
- The first failing block fails the file (broken step ⇒ the rest is meaningless), prints the captured output, and – inside GitHub Actions – annotates the exact line of your markdown.
Add a comment right above a fence:
<!-- readme-ci skip -->
<!-- readme-ci timeout=600 -->
<!-- readme-ci cwd=examples NODE_ENV=production -->| directive | effect |
|---|---|
skip |
never run this block |
timeout=N |
per-block timeout in seconds (default 300) |
cwd=path |
run in this directory (relative to the session) |
KEY=value |
export an environment variable for this block |
| runner | isolation | needs |
|---|---|---|
docker (default) |
container per file | Docker |
e2b |
cloud sandbox per file | npm i @e2b/code-interpreter + E2B_API_KEY |
local |
none – temp dir on host | nothing (only for markdown you trust) |
# pick the image your quickstart expects
readme-ci --runner docker --image python:3.12-bookworm docs/tutorial.md
# no Docker around? run in an E2B cloud sandbox
E2B_API_KEY=... readme-ci --runner e2b README.md
# test the README against the repo it documents
readme-ci --runner docker --mount . README.mdWhen a step fails, readme-ci --fix sends the failing block and its captured
output to an AI model, applies the proposed edit to the markdown in place,
and re-runs – up to --fix-attempts times. You review the result like any
other change: git diff.
Two ways to authenticate, in order of precedence:
# 1. an Anthropic API key (direct Messages API call – no SDK, still zero deps)
ANTHROPIC_API_KEY=... readme-ci --fix README.md
# 2. no key? if you use Claude Code, your existing login is enough –
# readme-ci talks to the local `claude` CLI instead
readme-ci --fix README.mdOnly the failing block is ever edited, earlier blocks' side effects stay in
place for the re-run, and <no_fix/> from the model ends the loop cleanly.
In CI, generate a long-lived token with claude setup-token and expose it as
the CLAUDE_CODE_OAUTH_TOKEN secret – the claude CLI picks it up.
With fix-pr: 'true' the action opens a pull request containing the repaired
markdown whenever --fix changed anything, so the fix lands through normal
review instead of dying with the runner workspace:
jobs:
readme:
runs-on: ubuntu-latest
permissions:
contents: write
pull-requests: write
steps:
- uses: actions/checkout@v4
- uses: ondraulehla/readme-ci@main
with:
files: README.md
fix-pr: 'true'
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}Unit tests check your code. readme-ci checks the contract with your
users: the exact commands you tell them to type. Those are the commands
that break most often – install steps, CLI flags, config files – and the
breakage is invisible until someone churns.
- more languages (go, rust, ruby) and
Dockerfile/compose-aware sessions - assertions on block output (
<!-- readme-ci expect="2 + 2 = 4" -->)
Issues and PRs welcome. The whole tool is small, typed and dependency-free –
src/extract.ts parses, src/plan.ts plans,
src/runners/ executes. CI runs readme-ci on this repo's
own docs in all three runners.
MIT © Ondřej Úlehla