site: serve bench.12vectors.com from a Cloudflare Worker

site/wrangler.jsonc puts site/dist/ behind bench.12vectors.com as static
assets. No `main`: the site is files, and a Worker with no script is the
cheapest correct way to serve them.

  html_handling      force-trailing-slash, so /x redirects to /x/ — the
                     url the pages link and rel=canonical names. One
                     page, one address; no url ends in .html.
  not_found_handling 404-page, so an unknown path gets dist/404.html
                     with a 404 status rather than the landing page
                     with a 200.
  routes             bench.12vectors.com as a custom domain. Cloudflare
                     takes the hostname at the zone level and makes the
                     DNS record; nothing else on 12vectors.com moves.

site/root/_headers carries the response policy. HTML revalidates on
every view, so a deploy is visible on the next reload without anyone
clearing a cache; /static/* is kept for a year and never re-checked,
which is safe because the stylesheet and icon urls carry a hash of their
contents. The general rule is written first and the specific one second,
so a host that merged the two instead of overriding would still land on
max-age=0 — the safe side. Alongside it the baseline a public page owes:
nosniff, a referrer policy, a year of HSTS without preload,
X-Frame-Options, and a default-src 'none' CSP that makes "no analytics,
no third-party anything" something the browser enforces rather than
something a test asserted once.

Deploys are run by hand, as releases already are — no Cloudflare token
in repository secrets, no first deploy pipeline. site/README.md names
the account, the Worker, the route and the four-command sequence, plus
the four things to check after a deploy that no test here can reach.

The tests cover everything before Cloudflare: that the config says what
the site needs, that the build writes the files it names, and that
wrangler.jsonc, pages.json and README.md cannot drift apart about which
domain this is. A live response is not among them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
istos
2026-07-31 11:47:12 +02:00
co-authored by Claude Opus 5
parent ad639a33d3
commit 6c06583109
4 changed files with 595 additions and 5 deletions
+96 -5
View File
@@ -18,13 +18,98 @@ python3 site/build.py # → site/dist/
```
`site/dist/` is gitignored — it is output, rebuilt on every deploy.
Deployment is task 32's; this directory only has to produce the files.
`site/` never reaches a host repo: releases ship exactly what
`../manager/core/release-manifest` lists, and it does not list this
directory. `tests/test_release_artifact.py` asserts that out loud rather
than leaving it to inference.
## Where the site lives
| | |
| --- | --- |
| Host | Cloudflare Workers, [static assets](https://developers.cloudflare.com/workers/static-assets/) — no script, no KV, no database |
| Account | the Cloudflare account holding the `12vectors.com` zone. `npx wrangler whoami` must list it before a deploy will work |
| Worker | `bench-site` |
| Route | `bench.12vectors.com`, a **custom domain** — Cloudflare owns the hostname at the zone level and creates the DNS record itself |
| Served from | `site/dist/`, uploaded whole on every deploy |
| Config | `site/wrangler.jsonc` (routing) and `site/root/_headers` (caching, security) |
Deploys are run by hand, by a person, exactly as releases are
(`../release.sh`). There is no deploy pipeline and no Cloudflare token in
repository secrets; adding a GitHub Action on merge to `main` is a
separate decision with a separate cost, and a follow-up card.
## Deploying
From a clean checkout, four commands. Re-running the whole sequence is
safe: the build empties and rewrites `dist/`, and `wrangler deploy`
replaces the Worker's assets rather than adding to them.
```bash
python3 -m pip install -r site/requirements.txt # once per machine
python3 site/fetch-fonts.py # once per checkout
python3 site/build.py # → site/dist/
npx wrangler@4 deploy --config site/wrangler.jsonc
```
`npx wrangler@4 login` first, once per machine, against an account that
can see the `12vectors.com` zone. Paths inside `wrangler.jsonc` are
relative to that file, so the command works from anywhere in the repo.
The first deploy is the one that takes the hostname over. It creates the
DNS record for `bench.12vectors.com` and routes it to the Worker;
anything else answering on that name stops answering. Every deploy after
it is an asset upload.
### Preview it locally
```bash
python3 site/build.py
npx wrangler@4 dev --config site/wrangler.jsonc # → http://localhost:8787
```
`dev` serves `dist/` through the same static-assets router as production,
so trailing-slash redirects and the 404 page behave as they will live.
The custom domain is ignored locally.
### After a deploy, check these four
The things this repository's tests cannot reach, because they are
answers rather than files:
1. `https://bench.12vectors.com/` serves the landing page over TLS.
2. `https://bench.12vectors.com/concepts/claiming-a-card` redirects to
the same path with a trailing slash, and no url anywhere ends in
`.html`.
3. A path that does not exist — `/nope/` — renders the site's own 404
page **with a 404 status**, not the landing page with a 200.
4. `curl -sI https://bench.12vectors.com/` shows
`x-content-type-options`, `referrer-policy`,
`strict-transport-security` and a `cache-control` that revalidates.
## How it is served
- **One url per page.** `html_handling: "force-trailing-slash"` redirects
`/concepts/claiming-a-card` to `/concepts/claiming-a-card/`, which is
what the pages link and what `<link rel=canonical>` names. Pages are
written as `<route>/index.html`, so no url ends in `.html`.
- **A real 404.** `not_found_handling: "404-page"` serves `dist/404.html`
with a 404 status. That page is a normal manifest entry (`/404.html`,
layout `notfound`) — the site's own design, its own nav, and a link
back to the landing page.
- **Two caching policies, because there are two kinds of file.** HTML
revalidates on every view, so a deploy is visible on the next reload
with nobody clearing anything. Everything under `/static/` is kept for
a year and never re-checked, which is only safe because the stylesheet
and the icon are linked with a `?v=<hash>` of their own contents:
change the file and the url changes with it.
- **Baseline headers, no third parties.** `nosniff`, a referrer policy,
a year of HSTS, `X-Frame-Options`, and a Content-Security-Policy of
`default-src 'none'` with `'self'` for styles, fonts and images. The
site collects nothing and loads nothing from anywhere else; the CSP is
that promise in a form the browser enforces.
## What is where
| Path | What it is |
@@ -33,6 +118,8 @@ than leaving it to inference.
| `build.py` | The generator — slicing, drift detection, rendering, links |
| `templates/` | One `string.Template` per layout (`$name`, `$$` for a literal dollar) |
| `static/` | Copied to `dist/static/` verbatim: the stylesheet, the icon, the fonts |
| `root/` | Copied to the **top** of `dist/` verbatim: `_headers`, which the host reads and never serves |
| `wrangler.jsonc` | The Worker: assets directory, url handling, 404, custom domain |
| `requirements.txt` | `markdown-it-py`, pinned. The only dependency |
| `fetch-fonts.py` | Downloads the self-hosted woff2 files, once |
@@ -51,12 +138,15 @@ than leaving it to inference.
}
```
- **`path`** starts and ends with `/`; `/x/y/` is written to
`dist/x/y/index.html`.
- **`path`** starts with `/` and ends with `/`; `/x/y/` is written to
`dist/x/y/index.html`. A route that names an `.html` file instead is
written to exactly that path — `/404.html` is the only one, and it
exists because the host looks for that literal filename.
- **`layout`** names a file in `templates/`.
- **`section`** groups the page in the nav and the sidebar. The IA is
read out of this file in this file's order — nothing is derived from
the directory layout.
the directory layout. A `null` section keeps the page out of both,
which is how the 404 page stays off the nav.
- **`source`** is repo-relative, or `null` for a landing page whose body
is authored in its template rather than sliced.
- **`from`** is the heading the slice starts at. It is matched on the
@@ -78,7 +168,8 @@ Each of these exits non-zero with a message naming the route:
- a slice that comes out empty;
- a markdown link to a repo path that does not exist, or that escapes
the repo — a dead relative link must never reach the site;
- a template placeholder the builder does not supply.
- a template placeholder the builder does not supply;
- a file in `root/` that a route would also write.
Repo-relative links that *do* resolve are rewritten: to a site route if
`link_routes` maps the file to one, otherwise to the file on GitHub.
+42
View File
@@ -0,0 +1,42 @@
# Response headers for bench.12vectors.com.
#
# Read by Cloudflare Workers static assets at deploy time — the file is
# consumed, never served — and copied here from site/root/ by
# site/build.py, because the host looks for it at the root of the build
# and nowhere else.
#
# Rules apply in order and a later rule wins on a header it repeats. The
# two blocks below are written so that even a host that merged them
# instead would land on the safe side: HTML would still revalidate.
# Everything, so that no page can ever forget one of these.
#
# nosniff a text/plain file must not become a script
# Referrer-Policy a full url is never sent to another origin
# HSTS one year, this host and anything below it. No
# `preload`: that is a submission to browser
# vendors and a commitment this card did not make
# X-Frame-Options nothing here is meant to be framed
# CSP the runtime form of the site's own promise —
# no analytics, no font CDN, no third-party
# anything. `default-src 'none'` means an asset
# must be named below to load at all, and no
# 'unsafe-inline' anywhere means an injected
# <script> does not run
# Cache-Control HTML revalidates on every view, so a deploy is
# visible on the next reload. The ETag makes that
# a 304 rather than a re-download
/*
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY
Content-Security-Policy: default-src 'none'; style-src 'self'; font-src 'self'; img-src 'self'; connect-src 'self'; base-uri 'none'; form-action 'none'; frame-ancestors 'none'
Cache-Control: public, max-age=0, must-revalidate
# The stylesheet and the icon are linked with a ?v=<hash> of their own
# contents (site/build.py, stamp()), and the fonts never change under a
# given filename. All of it is safe to keep for a year and never check:
# a deploy that changes the stylesheet changes the url that asks for it.
/static/*
Cache-Control: public, max-age=31536000, immutable
+43
View File
@@ -0,0 +1,43 @@
// bench.12vectors.com — the built minisite, served as static assets.
//
// There is no `main`: this Worker has no script at all. site/dist/ is
// plain files and Cloudflare's static-assets router serves them, so the
// cheapest correct thing is to give it nothing to run. Anything that
// would need a fetch handler (an api, a redirect that depends on state)
// is a different card.
//
// npx wrangler@4 deploy --config site/wrangler.jsonc
//
// See site/README.md for the whole sequence, the account, and what the
// domain currently points at.
{
"name": "bench-site",
"compatibility_date": "2026-07-01",
"assets": {
// Relative to this file. site/build.py writes it; it is gitignored,
// so a deploy from a clean checkout builds first.
"directory": "./dist",
// /concepts/claiming-a-card -> 301 -> /concepts/claiming-a-card/,
// which is the url the pages link and the one <link rel=canonical>
// names. One page, one address: the slashless form redirects rather
// than serving a second copy, and no url ever ends in .html.
"html_handling": "force-trailing-slash",
// An unknown path gets dist/404.html with a 404 status — the site's
// own not-found page, in the site's own design. Not "single-page-
// application", which would answer 200 with the landing page and
// tell a crawler every typo is a real url.
"not_found_handling": "404-page"
},
// The hostname, taken over at the zone level: Cloudflare points
// bench.12vectors.com at this Worker and creates the DNS record. It
// touches nothing else on 12vectors.com.
"routes": [
{ "pattern": "bench.12vectors.com", "custom_domain": true }
],
"observability": { "enabled": true }
}
+414
View File
@@ -0,0 +1,414 @@
"""bench.12vectors.com is a Cloudflare Worker serving site/dist/ as
static assets. What can be tested here is everything the deploy depends
on *before* it reaches Cloudflare: that the config says what the site
needs it to say, that the build produces the files that config names,
and that the three of them — wrangler.jsonc, pages.json, README.md —
cannot drift apart about which domain this is.
What cannot be tested here is a live response. `not_found_handling` is
asserted as configuration plus the 404.html it points at; that a request
to a missing path comes back 404 is Cloudflare's half, and the deploy
checklist in site/README.md is where a person confirms it.
python3 -m unittest discover -s tests
"""
import json
import re
import shutil
import subprocess
import sys
import tempfile
import unittest
from pathlib import Path
from urllib.parse import urlparse
REPO = Path(__file__).resolve().parents[1]
SITE = REPO / "site"
WRANGLER = SITE / "wrangler.jsonc"
HEADERS = SITE / "root" / "_headers"
README = SITE / "README.md"
# Set on every response, whatever it is. The task's three, plus the two
# that come free with them.
BASELINE = ("X-Content-Type-Options", "Referrer-Policy",
"Strict-Transport-Security")
try:
import markdown_it # noqa: F401
HAS_MARKDOWN_IT = True
except ImportError: # pragma: no cover - environment
HAS_MARKDOWN_IT = False
# ── reading the two config formats by hand ────────────────────────────
COMMENT = re.compile(r'"(?:\\.|[^"\\])*"|//[^\n]*|/\*.*?\*/', re.DOTALL)
def read_jsonc(path: Path) -> dict:
"""wrangler.jsonc is JSON with comments, and comments are how that
file explains itself. Strings are matched first so a `//` inside one
survives."""
def keep(match):
text = match.group(0)
return text if text.startswith('"') else " "
return json.loads(COMMENT.sub(keep, path.read_text(encoding="utf-8")))
def read_headers(path: Path) -> list:
"""`_headers` as [(pattern, {header: value})], in file order — an
unindented line opens a rule, an indented one adds a header to it."""
rules, current = [], None
for line in path.read_text(encoding="utf-8").splitlines():
if not line.strip() or line.lstrip().startswith("#"):
continue
if not line.startswith((" ", "\t")):
current = (line.strip(), {})
rules.append(current)
elif current is not None:
name, _, value = line.strip().partition(":")
current[1][name.strip()] = value.strip()
return rules
def readme_text() -> str:
return README.read_text(encoding="utf-8")
def rule(pattern: str) -> dict:
for found, headers in read_headers(HEADERS):
if found == pattern:
return headers
raise AssertionError(f"_headers has no rule for {pattern}")
def run_build(repo: Path, out: Path) -> subprocess.CompletedProcess:
return subprocess.run(
[sys.executable, str(repo / "site" / "build.py"), "--out", str(out)],
capture_output=True, text=True, cwd=repo)
class Built(unittest.TestCase):
"""The real site, built once into a scratch directory."""
@classmethod
def setUpClass(cls):
if not HAS_MARKDOWN_IT:
raise unittest.SkipTest("markdown-it-py is not installed")
cls.out = Path(tempfile.mkdtemp(prefix="bench-deploy-")).resolve()
result = run_build(REPO, cls.out)
if result.returncode != 0: # not assert: must survive python -O
raise RuntimeError(
f"site/build.py failed:\n{result.stdout}{result.stderr}")
@classmethod
def tearDownClass(cls):
if hasattr(cls, "out"):
shutil.rmtree(cls.out, ignore_errors=True)
# ── the worker config ─────────────────────────────────────────────────
class TheWorkerServesTheBuild(unittest.TestCase):
"""site/wrangler.jsonc, read the way wrangler reads it."""
def setUp(self):
self.config = read_jsonc(WRANGLER)
self.assets = self.config.get("assets", {})
def test_the_assets_directory_is_what_the_builder_writes(self):
"""Paths in wrangler config are relative to the config file, so
this one has to resolve to site/dist and not to a sibling of the
repo root."""
directory = (WRANGLER.parent / self.assets["directory"]).resolve()
self.assertEqual(directory, (SITE / "dist").resolve())
def test_there_is_no_worker_script(self):
"""The site is files. A `main` would mean a fetch handler to
maintain, and nothing here needs one."""
self.assertNotIn("main", self.config)
def test_a_url_has_exactly_one_form(self):
"""force-trailing-slash: /x redirects to /x/, which is the url
the pages link and the one rel=canonical names. Anything else
leaves two urls serving one page."""
self.assertEqual("force-trailing-slash",
self.assets.get("html_handling"))
def test_an_unknown_path_gets_the_sites_own_404(self):
"""Not single-page-application, which answers 200 with the
landing page and tells a crawler every typo is a real url."""
self.assertEqual("404-page", self.assets.get("not_found_handling"))
def test_the_route_is_a_custom_domain(self):
routes = self.config.get("routes", [])
self.assertEqual(1, len(routes), "expected exactly one route")
self.assertTrue(routes[0].get("custom_domain"),
"the route is not a custom domain")
def test_the_worker_is_named(self):
self.assertTrue(self.config.get("name"))
def test_a_compatibility_date_is_pinned(self):
self.assertRegex(self.config.get("compatibility_date", ""),
r"^\d{4}-\d{2}-\d{2}$")
class TheDomainIsWrittenDownOnce(unittest.TestCase):
"""Three files name this site's address. They are allowed to say it
three times; they are not allowed to disagree."""
def setUp(self):
self.config = read_jsonc(WRANGLER)
self.manifest = json.loads(
(SITE / "pages.json").read_text(encoding="utf-8"))
def test_the_route_is_the_host_the_pages_canonicalise_to(self):
"""A rel=canonical pointing somewhere the Worker does not answer
is worse than none at all."""
canonical = urlparse(self.manifest["site"]["base_url"])
self.assertEqual("https", canonical.scheme)
self.assertEqual(canonical.netloc,
self.config["routes"][0]["pattern"])
def test_the_readme_names_the_worker_and_the_route(self):
"""Acceptance: the next person should not have to guess where
the site lives."""
readme = README.read_text(encoding="utf-8")
self.assertIn(self.config["name"], readme)
self.assertIn(self.config["routes"][0]["pattern"], readme)
def test_the_readme_names_the_deploy_command(self):
self.assertRegex(readme_text(), r"wrangler[^\n]*\bdeploy\b")
def test_the_readme_names_the_account(self):
self.assertRegex(readme_text(), r"(?i)\baccount\b")
# ── the 404 page ──────────────────────────────────────────────────────
class TheNotFoundPageIsAPage(Built):
"""`not_found_handling: "404-page"` is half of it. The other half is
that a 404.html exists, looks like the rest of the site, and offers a
way out."""
def setUp(self):
self.page = (self.out / "404.html").read_text("utf-8")
def test_it_is_written_where_the_host_looks(self):
"""A 404/index.html would never be found: the host wants a file
called 404.html at the root of the assets directory."""
self.assertTrue((self.out / "404.html").is_file())
self.assertFalse((self.out / "404" / "index.html").exists())
def test_it_wears_the_sites_design(self):
self.assertIn('rel="stylesheet" href="/static/site.css', self.page)
self.assertIn("404", self.page)
def test_it_offers_the_way_back(self):
self.assertIn('href="/"', self.page)
def test_it_claims_no_canonical_url(self):
"""A 404 is not a page to canonicalise to, and telling a crawler
to index it is worse still."""
self.assertNotIn('rel="canonical"', self.page)
self.assertIn('name="robots" content="noindex"', self.page)
def test_it_stays_out_of_the_nav_and_the_sidebar(self):
home = (self.out / "index.html").read_text("utf-8")
article = (self.out / "concepts" / "claiming-a-card"
/ "index.html").read_text("utf-8")
for html in (home, article):
self.assertNotIn("404.html", html)
# ── headers ───────────────────────────────────────────────────────────
class TheHeaderPolicyTravelsWithTheBuild(Built):
"""site/root/_headers is the policy; the build has to carry it to
the root of dist/ or the host never reads it."""
def test_it_reaches_the_root_of_the_build(self):
shipped = self.out / "_headers"
self.assertTrue(shipped.is_file())
self.assertEqual(HEADERS.read_text("utf-8"), shipped.read_text("utf-8"))
def test_it_is_not_also_served_as_a_page(self):
"""The host consumes _headers rather than serving it, but the
build must not have made a route out of it either."""
self.assertFalse((self.out / "_headers" / "index.html").exists())
def test_no_page_needs_what_the_policy_forbids(self):
"""The CSP carries no 'unsafe-inline', so an inline <script> or a
style="" attribute is a page that renders right in every test
here and wrong in production. The only place that can be caught
before a deploy is the build output."""
for path in sorted(self.out.rglob("*.html")):
html = path.read_text("utf-8")
where = path.relative_to(self.out)
self.assertNotIn("<script", html.lower(),
f"{where} has a script the CSP would block")
self.assertNotRegex(
html, r"""\sstyle\s*=\s*["']""",
f"{where} has an inline style the CSP would block")
class EveryResponseCarriesTheBaseline(unittest.TestCase):
def setUp(self):
self.everything = rule("/*")
def test_the_three_headers_a_public_page_owes(self):
for name in BASELINE:
self.assertIn(name, self.everything)
def test_hsts_is_a_year_and_does_not_preload(self):
value = self.everything["Strict-Transport-Security"]
self.assertIn("max-age=31536000", value)
self.assertNotIn("preload", value,
"preload is a submission to browser vendors, "
"not a header to set in passing")
def test_nothing_third_party_can_load(self):
"""The site's promise — no analytics, no font CDN — as something
the browser enforces rather than something a test asserted once
at build time."""
policy = self.everything["Content-Security-Policy"]
self.assertIn("default-src 'none'", policy)
self.assertNotIn("unsafe-inline", policy)
self.assertNotIn("unsafe-eval", policy)
self.assertNotIn("*", policy)
self.assertNotIn("//", policy, "the policy names another origin")
class TheTwoKindsOfFileAreCachedDifferently(unittest.TestCase):
"""The edge case in the task: a stale HTML page must not survive a
deploy, while the assets it links may live for a year."""
def test_html_revalidates_on_every_view(self):
value = rule("/*")["Cache-Control"]
self.assertIn("max-age=0", value)
self.assertIn("must-revalidate", value)
self.assertNotIn("immutable", value)
def test_fingerprinted_assets_are_kept_for_a_year(self):
value = rule("/static/*")["Cache-Control"]
self.assertIn("max-age=31536000", value)
self.assertIn("immutable", value)
def test_the_general_rule_comes_first(self):
"""Order is the whole argument: /static/* overrides /* on
Cache-Control, and a host that merged them instead of overriding
would land on max-age=0 — the safe side."""
patterns = [pattern for pattern, _ in read_headers(HEADERS)]
self.assertLess(patterns.index("/*"), patterns.index("/static/*"))
# ── fingerprinting ────────────────────────────────────────────────────
class Scratch(unittest.TestCase):
"""A copy of site/ plus the markdown it reads, so a test can edit the
stylesheet without touching the real one."""
def setUp(self):
if not HAS_MARKDOWN_IT:
raise unittest.SkipTest("markdown-it-py is not installed")
self.root = Path(tempfile.mkdtemp(prefix="bench-deploy-")).resolve()
self.addCleanup(shutil.rmtree, self.root, True)
shutil.copytree(SITE, self.root / "site",
ignore=shutil.ignore_patterns("dist", "__pycache__"))
for name in ("AGENTS.md", "README.md"):
shutil.copy(REPO / name, self.root / name)
self.out = self.root / "site" / "dist"
def build(self):
return run_build(self.root, self.out)
def home(self) -> str:
return (self.out / "index.html").read_text("utf-8")
class TheStylesheetUrlFollowsTheStylesheet(Scratch):
"""`immutable` for a year is only safe because the url changes when
the file does. That is the claim under test."""
STAMP = re.compile(r'href="(/static/site\.css\?v=([0-9a-f]{10}))"')
def test_the_pages_link_a_stamped_url(self):
self.assertEqual(0, self.build().returncode)
self.assertRegex(self.home(), self.STAMP.pattern)
def test_the_file_still_sits_at_its_plain_path(self):
"""Only the link carries the stamp — nothing in static/ is
renamed, so the /static/* glob in _headers still matches and the
tree stays readable."""
self.assertEqual(0, self.build().returncode)
self.assertTrue((self.out / "static" / "site.css").is_file())
def test_editing_the_stylesheet_changes_the_stamp(self):
self.assertEqual(0, self.build().returncode)
before = self.STAMP.search(self.home()).group(2)
css = self.root / "site" / "static" / "site.css"
css.write_text(css.read_text("utf-8") + "\n.lost{color:red}\n",
encoding="utf-8")
self.assertEqual(0, self.build().returncode)
after = self.STAMP.search(self.home()).group(2)
self.assertNotEqual(before, after,
"a changed stylesheet kept its url, so a "
"year-long cache would keep the old one")
def test_a_rebuild_that_changes_nothing_keeps_the_stamp(self):
"""The other half: a deploy that did not touch the stylesheet
must not invalidate everyone's cache."""
self.assertEqual(0, self.build().returncode)
before = self.STAMP.search(self.home()).group(2)
self.assertEqual(0, self.build().returncode)
self.assertEqual(before, self.STAMP.search(self.home()).group(2))
class TheRootTreeIsCopiedVerbatim(Scratch):
def test_a_root_file_lands_at_the_top_of_the_build(self):
(self.root / "site" / "root" / "robots.txt").write_text(
"User-agent: *\n", encoding="utf-8")
self.assertEqual(0, self.build().returncode)
self.assertEqual("User-agent: *\n",
(self.out / "robots.txt").read_text("utf-8"))
def test_a_root_file_a_route_also_claims_stops_the_build(self):
"""Both would be written, one would win, and which one is not
something a person should have to work out from a diff."""
(self.root / "site" / "root" / "404.html").write_text(
"mine", encoding="utf-8")
result = self.build()
self.assertNotEqual(0, result.returncode)
self.assertIn("404.html", result.stderr)
class TheDeployIsInTheRepository(unittest.TestCase):
"""The two facts about deployment that live outside a config file."""
def test_the_worker_config_and_the_headers_are_tracked(self):
tracked = subprocess.run(
["git", "check-ignore", "site/wrangler.jsonc", "site/root/_headers"],
capture_output=True, text=True, cwd=REPO)
self.assertNotEqual(tracked.returncode, 0,
"the deploy config is gitignored")
def test_the_release_artifact_still_does_not_ship_the_site(self):
"""A Worker config in site/ must not start travelling into host
projects with a release."""
manifest = (REPO / "manager" / "core" / "release-manifest"
).read_text("utf-8")
for line in manifest.splitlines():
if line.strip().startswith("#") or not line.strip():
continue
self.assertNotIn("site", line.split(None, 1)[1],
f"the release manifest names site/: {line}")
if __name__ == "__main__":
unittest.main()