Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ npm-debug.log*
*.log
.env
.env.*
dist/
.site-build/
39 changes: 32 additions & 7 deletions docs/deploy.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,29 @@
patchling.app is a Cloudflare Pages project (`patchling`) connected to
`255BITS/patchling` through the Cloudflare Workers and Pages GitHub app.
There is no `wrangler.toml` and no deploy workflow in this repo: Cloudflare
builds from Git on its side.
builds from Git on its side, running `scripts/build-site.sh`.

## What the build produces

`bash scripts/build-site.sh` writes `dist/`:

| Path | Source |
| --- | --- |
| `/` | `landing/` copied as-is (the demo page) |
| `/py/` | `255BITS/patchling-py` `main` docs (`mkdocs.yml`, `site_url` set to `https://patchling.app/py/`) |
| `/js/` | this repo's `README.md`, `CHANGELOG.md` and `docs/*.md`, rendered with `scripts/site/mkdocs.js.yml` |
| `/docs/*` | 301 to `/py/*` (`scripts/site/_redirects`) |

It makes a shallow public clone of patchling-py (no token) and installs
`mkdocs-material==9.6.22` into a throwaway venv under `.site-build/`. That's
the same version pinned by patchling-py's Deploy Documentation workflow.
Nothing is added to `package.json`. The npm package still ships only `src/`.
`PATCHLING_PY_REF` picks another patchling-py branch or tag.

Each Pages build fetches patchling-py `main` fresh. patchling-py's "Trigger
patchling.app deploy" workflow calls a Pages deploy hook when its docs or
package change on `main`. That rebuilds this site with the new `/py/` docs,
without a push here.

## Expected Pages settings

Expand All @@ -14,21 +36,24 @@ builds from Git on its side.
| Automatic production deployments | enabled |
| Preview deployments | `all` |
| PR preview comments | enabled |
| Build command | *(none)* |
| Build output directory | `landing` |
| Build command | `bash scripts/build-site.sh` |
| Build output directory | `dist` |
| Root directory | *(repo root)* |
| Custom domains (exact set) | `patchling.app`, `patchling.pages.dev` |

Environment variable (production and preview): `PYTHON_VERSION` = `3.12`.
The checker below does not read environment variables.

`scripts/check-pages-settings.mjs` holds these values as `EXPECTED`, and
`test/check_pages_settings.test.js` fails if this table and `EXPECTED`
disagree. "Preview deployments `all`" means every non-production branch gets
a preview, and "PR preview comments" (`pr_comments_enabled`) posts its URL
on each PR. The domain set is exact: a
missing or extra domain is drift.

Because the build output is `landing/` with no build step, everything the
page loads must live under `landing/`. `landing/lib/*.js` are byte-identical
copies of `src/*.js`.
Everything the landing page loads must live under `landing/`.
`landing/lib/*.js` are byte-identical copies of `src/*.js`. `landing/` must
not contain `py/`, `js/` or `docs/`, because the build owns those paths.

## What CI checks (from the repo alone)

Expand Down Expand Up @@ -70,7 +95,7 @@ invalid JSON), even when the project data would match. It never writes.
`npm test` runs it offline against `test/fixtures/pages/` with a negative case
for each check.

Last verified 2026-10-13 via the API: production branch `main`,
Last verified 2026-10-13 via the API, before the `dist/` build: production branch `main`,
automatic production deployments enabled, preview deployments `all`,
build output `landing`, no build command, domains `patchling.pages.dev` and
`patchling.app`, latest production deployment = `main` @ `58a3858` (#5).
5 changes: 4 additions & 1 deletion landing/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -352,6 +352,8 @@
<a class="nl" href="#built">Built with it</a>
<a class="nl" href="#how">How it works</a>
<a class="nl" href="#models">Any model</a>
<a class="nl" href="/js/">JS docs</a>
<a class="nl" href="/py/">Py docs</a>
<a class="nl" href="https://github.com/255BITS/patchling" target="_blank" rel="noopener">GitHub</a>
</nav>

Expand Down Expand Up @@ -697,6 +699,7 @@ <h3>JavaScript</h3>
<div class="links">
<a href="https://github.com/255BITS/patchling" target="_blank" rel="noopener">GitHub</a>
<a href="https://www.npmjs.com/package/patchling" target="_blank" rel="noopener">npm</a>
<a href="/js/">Docs</a>
<a href="https://255bits.github.io/patchling-examples/" target="_blank" rel="noopener">Live examples</a>
</div>
</div>
Expand All @@ -710,7 +713,7 @@ <h3>Python</h3>
<div class="links">
<a href="https://github.com/255BITS/patchling-py" target="_blank" rel="noopener">GitHub</a>
<a href="https://pypi.org/project/patchling/" target="_blank" rel="noopener">PyPI</a>
<a href="https://255bits.github.io/patchling-py" target="_blank" rel="noopener">Docs</a>
<a href="/py/">Docs</a>
</div>
</div>
</div>
Expand Down
67 changes: 67 additions & 0 deletions scripts/build-site.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
#!/usr/bin/env bash
# Build the patchling.app Cloudflare Pages output into dist/:
# dist/ landing/ as-is (the demo page)
# dist/py/ patchling-py docs (mkdocs), fetched from 255BITS/patchling-py
# dist/js/ this repo's README, CHANGELOG and docs/ rendered with mkdocs
# dist/_redirects /docs/* -> /py/*
# dist/build-info.json commit SHAs of both repos used for this build
# Build tooling (mkdocs-material in a throwaway venv) lives only here and in
# .site-build/; nothing is added to package.json or the npm package.
# Public clone, no token. Env:
# PATCHLING_PY_REF branch/tag of patchling-py to build (default main)
# MKDOCS_MATERIAL pip spec (default mkdocs-material==9.6.22, same pin as
# patchling-py's Deploy Documentation workflow)
# PYTHON interpreter (default python3)
set -euo pipefail
root=$(cd "$(dirname "$0")/.." && pwd)
cd "$root"
out="$root/dist"
work="$root/.site-build"
ref="${PATCHLING_PY_REF:-main}"
py="${PYTHON:-python3}"

rm -rf "$out" "$work"
mkdir -p "$work"
cp -R landing "$out"

echo "== fetch 255BITS/patchling-py@$ref"
git clone --quiet --depth 1 --branch "$ref" https://github.com/255BITS/patchling-py.git "$work/patchling-py"
py_sha=$(git -C "$work/patchling-py" rev-parse HEAD)
self_sha=$(git -C "$root" rev-parse HEAD 2>/dev/null || echo "${CF_PAGES_COMMIT_SHA:-unknown}")
echo "patchling commit: $self_sha"
echo "patchling-py commit: $py_sha (ref $ref)"

echo "== mkdocs (${MKDOCS_MATERIAL:-mkdocs-material==9.6.22})"
"$py" -m venv "$work/venv"
"$work/venv/bin/python" -m pip install --quiet --disable-pip-version-check "${MKDOCS_MATERIAL:-mkdocs-material==9.6.22}"
mkdocs="$work/venv/bin/mkdocs"

echo "== /py/"
# Same source and nav as gptdiff.255labs.xyz; only site_url changes.
cat > "$work/patchling-py/mkdocs.patchling-app.yml" <<YML
INHERIT: mkdocs.yml
site_url: https://patchling.app/py/
YML
"$mkdocs" build --strict --quiet -f "$work/patchling-py/mkdocs.patchling-app.yml" -d "$out/py"

echo "== /js/"
js="$work/js"
mkdir -p "$js/docs"
# README links docs/x.md and the root-level infographic; flatten for the site.
sed -e 's#](docs/#](#g' README.md > "$js/docs/index.md"
cp infographic.svg "$js/docs/"
cp CHANGELOG.md docs/*.md "$js/docs/"
cp scripts/site/mkdocs.js.yml "$js/mkdocs.yml"
"$mkdocs" build --strict --quiet -f "$js/mkdocs.yml" -d "$out/js"

cp scripts/site/_redirects "$out/_redirects"
# Record exactly what went into this build (served at /build-info.json).
cat > "$out/build-info.json" <<JSON
{
"patchling": {"repo": "255BITS/patchling", "commit": "$self_sha"},
"patchling_py": {"repo": "255BITS/patchling-py", "ref": "$ref", "commit": "$py_sha"},
"built_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
}
JSON
echo "== build-info.json: patchling $self_sha, patchling-py $py_sha"
echo "== done: $(find "$out" -type f | wc -l) files in dist/"
4 changes: 2 additions & 2 deletions scripts/check-pages-settings.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ export const EXPECTED = Object.freeze({
productionDeploymentsEnabled: true,
previewDeploymentSetting: 'all',
prCommentsEnabled: true,
buildCommand: '',
destinationDir: 'landing',
buildCommand: 'bash scripts/build-site.sh',
destinationDir: 'dist',
rootDir: '',
domains: Object.freeze(['patchling.app', 'patchling.pages.dev']),
});
Expand Down
4 changes: 4 additions & 0 deletions scripts/site/_redirects
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Cloudflare Pages redirects (copied to dist/ by scripts/build-site.sh).
/docs /py/ 301
/docs/ /py/ 301
/docs/* /py/:splat 301
27 changes: 27 additions & 0 deletions scripts/site/mkdocs.js.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# Rendered by scripts/build-site.sh into dist/js/ (patchling.app/js/).
site_name: patchling (JavaScript)
site_url: https://patchling.app/js/
repo_url: https://github.com/255BITS/patchling
edit_uri: ""
docs_dir: docs
strict: true
theme:
name: material
palette:
scheme: slate
primary: deep purple
accent: blue
nav:
- README: index.md
- Changelog: CHANGELOG.md
- Deploying patchling.app: deploy.md
extra:
social:
- name: Python docs
icon: fontawesome/brands/python
link: https://patchling.app/py/
- name: patchling.app
icon: fontawesome/solid/globe
link: https://patchling.app/
plugins:
- search
4 changes: 2 additions & 2 deletions test/check_pages_settings.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@ test('the saved live response passes every check', () => {
const negatives = {
'preview deployments none': [(p) => { p.source.config.preview_deployment_setting = 'none'; }, ['previewDeploymentSetting']],
'root_dir src': [(p) => { p.build_config.root_dir = 'src'; }, ['rootDir']],
'destination_dir changed': [(p) => { p.build_config.destination_dir = 'dist'; }, ['destinationDir']],
'build command added': [(p) => { p.build_config.build_command = 'npm run build'; }, ['buildCommand']],
'destination_dir still landing': [(p) => { p.build_config.destination_dir = 'landing'; }, ['destinationDir']],
'build command missing': [(p) => { p.build_config.build_command = ''; }, ['buildCommand']],
'missing patchling.pages.dev': [(p) => { p.domains = ['patchling.app']; }, ['domains']],
'missing patchling.app': [(p) => { p.domains = ['patchling.pages.dev']; }, ['domains']],
'extra domain': [(p) => { p.domains.push('www.patchling.app'); }, ['domains']],
Expand Down
42 changes: 39 additions & 3 deletions test/deploy_config.test.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/**
* Cloudflare Pages serves landing/ as-is (no build step, see docs/deploy.md).
* Cloudflare Pages runs scripts/build-site.sh and serves dist/: landing/ at /,
* patchling-py docs at /py/, this repo's docs at /js/ (see docs/deploy.md).
* These are the deploy assumptions CI can verify from the repo alone.
*/
import { test } from 'node:test';
Expand Down Expand Up @@ -28,10 +29,10 @@ test('landing/index.html is the Pages entry point', () => {
assert.ok(existsSync(join(landing, 'index.html')));
});

test('docs/deploy.md pins production branch main and output dir landing', () => {
test('docs/deploy.md pins production branch main and output dir dist', () => {
const doc = readFileSync(join(root, 'docs', 'deploy.md'), 'utf8');
assert.match(doc, /Production branch \| `main`/);
assert.match(doc, /Build output directory \| `landing`/);
assert.match(doc, /Build output directory \| `dist`/);
});

test('every relative import under landing/ resolves inside landing/', () => {
Expand All @@ -48,3 +49,38 @@ test('every relative import under landing/ resolves inside landing/', () => {
}
assert.deepEqual(problems, []);
});

test('landing links the in-site docs, and /py/ and /js/ are left to the build', () => {
const html = readFileSync(join(landing, 'index.html'), 'utf8');
assert.match(html, /href="\/py\/"/);
assert.match(html, /href="\/js\/"/);
assert.doesNotMatch(html, /255bits\.github\.io\/patchling-py/);
for (const dir of ['py', 'js', 'docs']) {
assert.ok(!existsSync(join(landing, dir)), `landing/${dir} would collide with the build output`);
}
});

test('/docs/ redirects to /py/ and the build script wires everything up', () => {
const redirects = readFileSync(join(root, 'scripts', 'site', '_redirects'), 'utf8');
assert.match(redirects, /^\/docs\/\* \/py\/:splat 301$/m);
assert.match(redirects, /^\/docs \/py\/ 301$/m);
const sh = readFileSync(join(root, 'scripts', 'build-site.sh'), 'utf8');
assert.match(sh, /https:\/\/github\.com\/255BITS\/patchling-py\.git/);
assert.match(sh, /-d "\$out\/py"/);
assert.match(sh, /-d "\$out\/js"/);
});

test('every site build records both repos\' commit SHAs', () => {
const sh = readFileSync(join(root, 'scripts', 'build-site.sh'), 'utf8');
assert.match(sh, /PATCHLING_PY_REF:-main/);
assert.match(sh, /\$out\/build-info\.json/);
assert.match(sh, /"patchling":.*"commit": "\$self_sha"/);
assert.match(sh, /"patchling_py":.*"commit": "\$py_sha"/);
assert.match(sh, /echo "patchling-py commit: \$py_sha/);
});

test('site build tooling stays out of the npm package', () => {
const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8'));
assert.deepEqual(pkg.files, ['src']);
assert.equal(pkg.dependencies, undefined);
});
2 changes: 1 addition & 1 deletion test/fixtures/pages/project-ok.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"deployments_enabled": true
}
},
"build_config": { "build_command": "", "destination_dir": "landing", "root_dir": "" },
"build_config": { "build_command": "bash scripts/build-site.sh", "destination_dir": "dist", "root_dir": "" },
"canonical_deployment": {
"environment": "production",
"created_on": "2026-10-13T16:50:51.866675Z",
Expand Down
Loading