diff --git a/.gitignore b/.gitignore index 07c5ad5..d2cc5d8 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ npm-debug.log* *.log .env .env.* +dist/ +.site-build/ diff --git a/docs/deploy.md b/docs/deploy.md index 324c509..3538aba 100644 --- a/docs/deploy.md +++ b/docs/deploy.md @@ -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 @@ -14,11 +36,14 @@ 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 @@ -26,9 +51,9 @@ 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) @@ -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). diff --git a/landing/index.html b/landing/index.html index 704b979..27f9186 100644 --- a/landing/index.html +++ b/landing/index.html @@ -352,6 +352,8 @@ Built with it How it works Any model + JS docs + Py docs GitHub @@ -697,6 +699,7 @@

JavaScript

@@ -710,7 +713,7 @@

Python

diff --git a/scripts/build-site.sh b/scripts/build-site.sh new file mode 100755 index 0000000..225b4a6 --- /dev/null +++ b/scripts/build-site.sh @@ -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" < "$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" < { 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']], diff --git a/test/deploy_config.test.js b/test/deploy_config.test.js index 392d3c3..a09b47f 100644 --- a/test/deploy_config.test.js +++ b/test/deploy_config.test.js @@ -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'; @@ -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/', () => { @@ -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); +}); diff --git a/test/fixtures/pages/project-ok.json b/test/fixtures/pages/project-ok.json index 477ff2e..70ba9fa 100644 --- a/test/fixtures/pages/project-ok.json +++ b/test/fixtures/pages/project-ok.json @@ -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",