Ship the map as a celilo module, with CI/CD and an e2e suite #3

Merged
forgejo-admin merged 12 commits from feat/celilo-module into main 2026-09-09 00:41:22 +00:00

Problem

The map is a blob pasted into Squarespace, so it is only as fresh as the last time somebody pasted it. In 2025 two copies drifted apart with no way to tell which was live.

Solution

Phase one of add-celilo-hosting, plus the build bus and a test suite.

manifest.yml                     public_web required; source_forge +
                                 registry_publish OPTIONAL (see below)
scripts/on_install.ts            publishStaticSite, then register the repo with the forge
scripts/health_check.ts          the page answers AND sales.json holds drawable sales
scripts/register-forge.ts        mints a package-scoped publish token
site/index.template.html         the page
site/build.ts                    renders site/dist, refuses to build an unsafe page
site/data/sales.json             written by the pipeline, fetched by the page
.forgejo/workflows/              pr-validate + release
tests/bna-yard-sale.e2e.test.ts  a real cele2e deploy
pipeline.integration.test.ts     stand-in Google + Nominatim servers

The module was rebuilt against the framework's own guide

The first version of this module was written by copying celilo-website. There is a 3305-line reference/MODULE_DEVELOPMENT_GUIDE.md and an @celilo/mcp-dev-server with a scaffolder and the authoritative capability and hook contracts. Using them found six defects:

  • requires: dns_registrar was never this module's dependency. The public_web contract says it outright: it owns public reachability for the routes it registers and reaches the registrar itself. Copied from celilo-website.
  • public_web was declared optional with a hand-rolled throw. It is a hard requires now, so the framework refuses before the hook runs.
  • Hook files are named for their hooks (on_install.ts, health_check.ts).
  • on_install had no tests; it has five.
  • No cele2e stub existed.
  • celilo/types.d.ts claimed to be generated and was not.
  • scripts/tsconfig.json reached into a sibling celilo checkout, so cloning this repo alone gave a broken typecheck.

CI/CD: a merge produces a .netapp on the registry

on_install calls registerWithForge, which mints a publish token scoped to this one package via registry_publish and hands it to source_forge.registerRepo. That sets the repo's CELILO_PUBLISH_TOKEN secret and applies branch protection. Idempotent, so every deploy reconciles it.

source_forge and registry_publish are optional, not requires. lunacycle's manifest carries the reason and it applies exactly here: under requires they are enforced at import time, so module import fails on any fleet without a forge — which is every fleet the e2e builds. The e2e proves this: it imports and deploys with no forge and no registry present.

release.yml re-runs the PR gates on main before publishing, because pr-validate proves each branch is green and cannot prove main is. After publishing it asks the registry what it actually holds and fails if the manifest version is not there — a publish that reports success and ships nothing is a real outcome the exit code does not distinguish.

Both workflows run the CLI this repo pins, not bunx @celilo/cli. That is not tidiness. The CLI version silently decides whether the artifact is usable at all: 2.2.0 stripped scripts/node_modules out of any module with a root package.json, so it built a .netapp that packaged, published, imported and deployed and then died in on_install (celilo#1310, fixed in 2.2.1). bunx resolves latest at run time, so a future regression would ship the same way. CI already runs bun install --frozen-lockfile, so the lockfile is the single source of truth for which packager built the artifact, with no version literal duplicated into the workflows. @celilo/cli and @celilo/e2e are pinned exactly rather than caret ranges for the same reason.

The pipeline ends at a .netapp in the registry. Nothing in it deploys. That needs auto_upgrade and upgrade_policy, which are operator state and deliberately not in this repo.

Why this repo moved to the celilo org

The fleet runner is scoped org/celilo and advertises native:host. This repo was at forgejo-admin/bna-yard-sale, a user namespace. Forgejo scopes are a tree where org and user are siblings, so the runner could never claim these jobs: every run sat in waiting forever, with no error anywhere, even though the runner existed and advertised exactly the requested label. That is celilo#627. manifest.test.ts now pins forge_repo to the org so it cannot regress quietly.

Tests

pipeline.integration.test.ts — real HTTP servers on localhost, not a mocked fetch, because the bugs worth catching here live in the wire format. It serves the exact header Google publishes for this form, where the address question wraps onto a second line inside quotes. The case that matters most: an unpublished sheet answers 200 with a sign-in page, which without a guard parses into one very strange sale. Verified that guard fails when removed.

tests/bna-yard-sale.e2e.test.ts — a real cele2e deploy, ten stages. Beyond serving a 200 it fetches sales.json and asserts every pin lands inside the neighborhood bounding box; a geocoder misfire that shipped would put a marker on a stranger's house. It also asserts the served page renders seller text as text, and asks the module's own health check for an independent second reading.

e2e does not run in CI. The rig is a single machine-global resource behind a run-lock; it is bun run test:e2e locally, and cele2e build-infra has to have been run once on the machine to populate the harness's netapp cache.

The e2e suite is green on the released toolchain

Ten stages, 142s, against @celilo/cli 2.2.1 and @celilo/e2e 0.20.1 from npm — nothing unreleased anywhere in the chain. site verified: 33 sales served / served page renders seller text as text / module reports healthy.

Two of the defects it found were ours:

  • The manifest required public_web 3.2.0, a version nothing has ever provided. The fleet's caddy provides 3.1.0, the current contract. This would have blocked the production deploy too, and no local gate can see it — celilo module check reads the manifest, not the fleet. The deploy is what fails.
  • The suite used bna-yard-sale.org. cele2e's simulated internet is authoritative only for the zones its DNS containers are seeded with, so the fake registrar answered ENOENT ... /config/bna-yard-sale.org.zone. It now uses iamtheinternet.org, which also proves the module takes its hostname from config, since the manifest's default is the real domain and this is not it.

Four were in the framework, filed as celilo#1309 and celilo#1310. celilo#1310 is fixed and released. The three in #1309 are still present in 0.20.1, so the suite works around each:

  • publishModule shells out to a CLI path that exists only in the celilo monorepo, so packaging fails for every npm consumer. That kills registerControlPlane(), which every suite calls. We package with the pinned @celilo/cli and hand it a .netapp.
  • It also takes the module id from the DIRECTORY basename. The .netapp form takes the id from the filename instead.
  • Publishing a staged netapp runs copyFileSync(path, path) and destroys it — the harness's netapps/ cache went from 39 files to 38. stagedNetappCopy copies into a temp dir first.
  • registerControlPlane defaults to a modules/celilo-mgmt that a consumer install ships empty, so it is handed the staged netapp explicitly.

assertStandardModulesStaged turns the resulting failure — which otherwise arrives four stages later as Module 'iptables' not found in registry and reads as a broken registry — into the one instruction that fixes it. It has already earned its keep once: a dependency reinstall wiped the netapp cache, and the suite failed in 0s with the remedy instead of 90s into a rig run.

Verified on the running system, not just by assertions

Inspected a live deploy with --keep:

  • The served sales.json is byte-identical to the build output and matches the committed source at 33 entries (closes task 5.5 of add-celilo-hosting).
  • The hook runtime lands correctly on a real target: @celilo/capabilities, drizzle-orm, tldts, tldts-core.
  • on_install is genuinely idempotent. Three publishes of identical content produced one content-addressed release directory with an unchanged timestamp, so redeploys do not accumulate on the host.
  • sales.json is served cache-control: no-cache. This matters: the capability's Caddy template gives /assets/* a one-year immutable cache, and a data file in that bucket would have frozen the sale list.
  • public_web renders try_files {path} {path}/index.html /index.html, so every path returns 200/does-not-exist serves the map. That comes from the capability, not this module. For a one-page map it is arguably better than a 404, so it is recorded rather than changed.

Verification

gate exit
bun run typecheck 0
bun run typecheck:hooks 0
bun run typecheck:e2e 0
bun run test:unit 0 — 56 pass, 0 fail
bun run build:site 0
celilo module check . 0 — 7 ok, 0 warn, 0 fail
bun run test:e2e 0 — 1 passed, 0 failed, ten stages, 142s

The built site was served and driven in real Chrome: 33 sales fetched, 33 rows, 33 flowers, 18 street labels, map fills its pane, all three derived date strings correct.

What this does not cover

The forge registration path never executes in the e2e, because no e2e fleet has a forge — registerWithForge warns and returns. On the production fleet both capabilities exist, so the token-minting code runs for the first time on the real deploy. The logic is unit-tested against the capability contract with fakes (register-forge.test.ts), so what is untested is the real capability implementation, not the branch. Covering it properly would mean deploying forgejo, roughly tripling the suite.

Still blocked on one thing

CELILO_PUBLISH_TOKEN does not exist, so merging this runs release.yml and fails at the publish step. Two ways to create it:

  1. Deploy the module to the fleet, so registerWithForge mints it as designed. Gated on confirming namecheap manages bna-yard-sale.org.
  2. Mint a package-scoped token directly against the registry and set it as the repo secret. This decouples the first .netapp from the domain question.

Nothing in this PR has touched the fleet.

🤖 Generated with Claude Code

https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee

## Problem The map is a blob pasted into Squarespace, so it is only as fresh as the last time somebody pasted it. In 2025 two copies drifted apart with no way to tell which was live. ## Solution Phase one of `add-celilo-hosting`, plus the build bus and a test suite. ``` manifest.yml public_web required; source_forge + registry_publish OPTIONAL (see below) scripts/on_install.ts publishStaticSite, then register the repo with the forge scripts/health_check.ts the page answers AND sales.json holds drawable sales scripts/register-forge.ts mints a package-scoped publish token site/index.template.html the page site/build.ts renders site/dist, refuses to build an unsafe page site/data/sales.json written by the pipeline, fetched by the page .forgejo/workflows/ pr-validate + release tests/bna-yard-sale.e2e.test.ts a real cele2e deploy pipeline.integration.test.ts stand-in Google + Nominatim servers ``` ## The module was rebuilt against the framework's own guide The first version of this module was written by copying `celilo-website`. There is a 3305-line `reference/MODULE_DEVELOPMENT_GUIDE.md` and an `@celilo/mcp-dev-server` with a scaffolder and the authoritative capability and hook contracts. Using them found six defects: - **`requires: dns_registrar` was never this module's dependency.** The `public_web` contract says it outright: it owns public reachability for the routes it registers and reaches the registrar itself. Copied from celilo-website. - **`public_web` was declared optional with a hand-rolled throw.** It is a hard `requires` now, so the framework refuses before the hook runs. - Hook files are named for their hooks (`on_install.ts`, `health_check.ts`). - `on_install` had **no tests**; it has five. - No cele2e stub existed. - `celilo/types.d.ts` **claimed to be generated and was not.** - `scripts/tsconfig.json` reached into a sibling `celilo` checkout, so cloning this repo alone gave a broken typecheck. ## CI/CD: a merge produces a .netapp on the registry `on_install` calls `registerWithForge`, which mints a publish token scoped to this one package via `registry_publish` and hands it to `source_forge.registerRepo`. That sets the repo's `CELILO_PUBLISH_TOKEN` secret and applies branch protection. Idempotent, so every deploy reconciles it. **`source_forge` and `registry_publish` are `optional`, not `requires`.** lunacycle's manifest carries the reason and it applies exactly here: under `requires` they are enforced at import time, so `module import` fails on any fleet without a forge — which is every fleet the e2e builds. The e2e proves this: it imports and deploys with no forge and no registry present. `release.yml` re-runs the PR gates on main before publishing, because pr-validate proves each *branch* is green and cannot prove main is. After publishing it asks the registry what it actually holds and fails if the manifest version is not there — a publish that reports success and ships nothing is a real outcome the exit code does not distinguish. **Both workflows run the CLI this repo pins, not `bunx @celilo/cli`.** That is not tidiness. The CLI version silently decides whether the artifact is usable at all: 2.2.0 stripped `scripts/node_modules` out of any module with a root `package.json`, so it built a `.netapp` that packaged, published, imported and deployed and then died in `on_install` (celilo#1310, fixed in 2.2.1). `bunx` resolves `latest` at run time, so a future regression would ship the same way. CI already runs `bun install --frozen-lockfile`, so the lockfile is the single source of truth for which packager built the artifact, with no version literal duplicated into the workflows. `@celilo/cli` and `@celilo/e2e` are pinned exactly rather than caret ranges for the same reason. The pipeline **ends at a .netapp in the registry**. Nothing in it deploys. That needs `auto_upgrade` and `upgrade_policy`, which are operator state and deliberately not in this repo. ## Why this repo moved to the celilo org The fleet runner is scoped `org`/`celilo` and advertises `native:host`. This repo was at `forgejo-admin/bna-yard-sale`, a **user** namespace. Forgejo scopes are a tree where org and user are *siblings*, so the runner could never claim these jobs: every run sat in `waiting` forever, with no error anywhere, even though the runner existed and advertised exactly the requested label. That is celilo#627. `manifest.test.ts` now pins `forge_repo` to the org so it cannot regress quietly. ## Tests **`pipeline.integration.test.ts`** — real HTTP servers on localhost, not a mocked fetch, because the bugs worth catching here live in the wire format. It serves the exact header Google publishes for this form, where the address question wraps onto a second line inside quotes. The case that matters most: an unpublished sheet answers **200 with a sign-in page**, which without a guard parses into one very strange sale. Verified that guard fails when removed. **`tests/bna-yard-sale.e2e.test.ts`** — a real cele2e deploy, ten stages. Beyond serving a 200 it fetches `sales.json` and asserts every pin lands inside the neighborhood bounding box; a geocoder misfire that shipped would put a marker on a stranger's house. It also asserts the *served* page renders seller text as text, and asks the module's own health check for an independent second reading. e2e does not run in CI. The rig is a single machine-global resource behind a run-lock; it is `bun run test:e2e` locally, and `cele2e build-infra` has to have been run once on the machine to populate the harness's netapp cache. ## The e2e suite is green on the released toolchain **Ten stages, 142s**, against `@celilo/cli` 2.2.1 and `@celilo/e2e` 0.20.1 from npm — nothing unreleased anywhere in the chain. `site verified: 33 sales served` / `served page renders seller text as text` / `module reports healthy`. Two of the defects it found were ours: - **The manifest required `public_web` 3.2.0, a version nothing has ever provided.** The fleet's caddy provides 3.1.0, the current contract. This would have blocked the production deploy too, and no local gate can see it — `celilo module check` reads the manifest, not the fleet. The deploy is what fails. - **The suite used `bna-yard-sale.org`.** cele2e's simulated internet is authoritative only for the zones its DNS containers are seeded with, so the fake registrar answered `ENOENT ... /config/bna-yard-sale.org.zone`. It now uses `iamtheinternet.org`, which also proves the module takes its hostname from config, since the manifest's default is the real domain and this is not it. Four were in the framework, filed as celilo#1309 and celilo#1310. celilo#1310 is fixed and released. The three in #1309 are **still present in 0.20.1**, so the suite works around each: - `publishModule` shells out to a CLI path that exists only in the celilo monorepo, so packaging fails for every npm consumer. That kills `registerControlPlane()`, which every suite calls. We package with the pinned `@celilo/cli` and hand it a `.netapp`. - It also takes the module id from the DIRECTORY basename. The `.netapp` form takes the id from the filename instead. - Publishing a staged netapp runs `copyFileSync(path, path)` and **destroys** it — the harness's `netapps/` cache went from 39 files to 38. `stagedNetappCopy` copies into a temp dir first. - `registerControlPlane` defaults to a `modules/celilo-mgmt` that a consumer install ships empty, so it is handed the staged netapp explicitly. `assertStandardModulesStaged` turns the resulting failure — which otherwise arrives four stages later as `Module 'iptables' not found in registry` and reads as a broken registry — into the one instruction that fixes it. It has already earned its keep once: a dependency reinstall wiped the netapp cache, and the suite failed in 0s with the remedy instead of 90s into a rig run. ## Verified on the running system, not just by assertions Inspected a live deploy with `--keep`: - The served `sales.json` is **byte-identical** to the build output and matches the committed source at 33 entries (closes task 5.5 of `add-celilo-hosting`). - The hook runtime lands correctly on a real target: `@celilo/capabilities`, `drizzle-orm`, `tldts`, `tldts-core`. - `on_install` is genuinely idempotent. Three publishes of identical content produced **one** content-addressed release directory with an unchanged timestamp, so redeploys do not accumulate on the host. - `sales.json` is served `cache-control: no-cache`. This matters: the capability's Caddy template gives `/assets/*` a one-year immutable cache, and a data file in that bucket would have frozen the sale list. - `public_web` renders `try_files {path} {path}/index.html /index.html`, so **every path returns 200** — `/does-not-exist` serves the map. That comes from the capability, not this module. For a one-page map it is arguably better than a 404, so it is recorded rather than changed. ## Verification | gate | exit | | --- | --- | | `bun run typecheck` | 0 | | `bun run typecheck:hooks` | 0 | | `bun run typecheck:e2e` | 0 | | `bun run test:unit` | 0 — 56 pass, 0 fail | | `bun run build:site` | 0 | | `celilo module check .` | 0 — 7 ok, 0 warn, 0 fail | | `bun run test:e2e` | 0 — 1 passed, 0 failed, ten stages, 142s | The built site was served and driven in real Chrome: 33 sales fetched, 33 rows, 33 flowers, 18 street labels, map fills its pane, all three derived date strings correct. ## What this does not cover The forge registration path never executes in the e2e, because no e2e fleet has a forge — `registerWithForge` warns and returns. On the production fleet both capabilities exist, so the token-minting code runs for the first time on the real deploy. The logic is unit-tested against the capability contract with fakes (`register-forge.test.ts`), so what is untested is the real capability implementation, not the branch. Covering it properly would mean deploying `forgejo`, roughly tripling the suite. ## Still blocked on one thing **`CELILO_PUBLISH_TOKEN` does not exist**, so merging this runs `release.yml` and fails at the publish step. Two ways to create it: 1. Deploy the module to the fleet, so `registerWithForge` mints it as designed. Gated on confirming `namecheap` manages `bna-yard-sale.org`. 2. Mint a package-scoped token directly against the registry and set it as the repo secret. This decouples the first `.netapp` from the domain question. Nothing in this PR has touched the fleet. 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
Phase one of add-celilo-hosting. The map becomes a static site published
through public_web at bna-yard-sale.org, and Squarespace links to it once
instead of carrying a pasted blob that drifts.

The page now fetches sales.json from its own origin rather than having the
sales baked into the markup (design D3). That is what makes the phase-two
refresh reviewable later: a new sale is a one-line diff in a data file, not
a re-rendered page. It also means the page has to survive the data not
arriving, so it does: the header, the date and an honest message render, and
it says the list could not be loaded rather than claiming nobody signed up.

The date is computed from the calendar rule instead of typed. The sale is
the last Saturday in September, so the page derives it and rolls to next
year the day after this year's. It was hardcoded in three places (the blade,
the header line, the phone bar) and would have gone stale in all three.
Checked against 2024 through 2032 and across the rollover.

DEVIATION FROM THE DESIGN, for review. D2 says Astro, mirroring
celilo-website. This ships a 100-line site/build.ts instead. D2's stated
reasons were "no build step to catch a mistake and no component to hold the
date", and both are met: the date is derived, and the build refuses to
publish a page that uses innerHTML, reaches a geocoder at runtime, or stops
routing seller descriptions through the textContent helper. Those are the
ways this page could hurt somebody, and each refusal was verified by
breaking the page and watching the build fail. Astro would have added a
framework, a node_modules and a config to render one static file. Say the
word and I will swap it; the module shape does not change.

The health check asks the question that matters. A 200 from the front page
only proves Caddy is answering, because the page is a shell that fetches its
own data. So it also fetches sales.json and asserts it parses and holds at
least one sale the map can draw. Seven tests cover the malformed cases,
including partially-usable data, which warns rather than passes.

The hook scripts are their own package with their own tsconfig, matching how
modules resolve @celilo/capabilities on the fleet. The root tsconfig no
longer reaches into them, because the published package ships raw .ts and
typechecking it from here reports errors in somebody else's source.

Manifest validated against celilo/schemas/module-manifest.schema.json:
required keys, no unknown keys, hook scripts exist, build artifacts exist.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fVMptFeMs2vUR31ByWYYF
Task 6.1. The Squarespace fallback had never actually been rendered. It works:
the snippet was assembled the way the README says to paste it, served, and
loaded in a real browser. Leaflet came up, 33 markers landed on the right
blocks, OpenStreetMap tiles arrived. That basemap also independently confirms
the street grid derived for the celilo site is right, since the two agree.

Doing it found a defect reading would not have. The Freeya sponsor logo had no
size on it. index.css was deleted months ago when the snippet became generated,
and the block had been relying on it, so the logo rendered at full size and
filled the page below the map. The sponsor block now carries its own styles
inline, which is correct regardless: it lands in a Squarespace Code block with
whatever CSS that page happens to have, so it cannot depend on any.

Also records in design.md that D2 shipped as site/build.ts rather than Astro,
with the reasoning, so the doc matches what exists and the reviewer can rule on
it in one place. Marks the phase-one tasks that are genuinely done, and marks
5.4 as blocked on 1.5, which is not mine.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018fVMptFeMs2vUR31ByWYYF
I built this module by copying celilo-website. There is a 3305-line
reference/MODULE_DEVELOPMENT_GUIDE.md and an @celilo/mcp-dev-server with a
scaffolder, a manifest validator, and the authoritative capability and hook
contracts. I used none of them, because I searched docs/ for "module" and
never looked in reference/, and never asked whether tooling existed.

Scaffolding a module properly and reading describe_capability('public_web')
turned up six things.

requires no longer lists dns_registrar. The public_web contract says it
outright: public_web owns public reachability for the routes it registers
and reaches the registrar itself, so "you do NOT requires: dns_registrar
just to reach clients". That was copied from celilo-website and was never
this module's dependency. public_web is also pinned at the current 3.2.0
rather than 3.0.0.

public_web is now a hard `requires` on the hook rather than `optional` with
a hand-rolled throw. Optional is the shape for a capability the module can
work without; with no public_web there is nowhere to publish, so the
framework should refuse before the hook runs. It also makes the capability
non-nullable at the type level instead of guarded at runtime.

Hook scripts are named for their hooks: on_install.ts and health_check.ts.

on_install has unit tests for the first time — five, covering the domain
fallback, the missing-capability path, a failed publish, and that the
request carries no sourceDir, which the capability removed in D10.

tests/bna-yard-sale.e2e.test.ts is the cele2e stub the convention calls for,
skipped until @celilo/e2e is consumable from outside the monorepo. Its
assertions are written against the artifact: the page answers AND sales.json
holds sales the map can draw.

celilo/types.d.ts stops claiming to be generated. It carried a "Generated by
celilo module types generate — do not edit" banner it had not earned. It now
says it is hand-written and mirrors the manifest.

scripts/tsconfig.json no longer extends a path inside a sibling celilo
checkout. That only resolved because celilo happens to sit next to this repo;
anyone cloning this alone got a broken typecheck. It is standalone now.

Version bumped 0.1.0 -> 0.2.0: new manifest surface and changed hook
contracts, backward-compatible, which the guide's versioning section makes a
minor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
CI/CD: forgejo builds and publishes the .netapp to celilo-registry
Some checks are pending
pr-validate / validate (pull_request) Waiting to run
bc5862c69d
Wires this repo into the build bus so a merge to main produces a .netapp on
the production registry, following celilo/reference/APP_CI_REFERENCE.md and
lunacycle, which is the app already on it.

on_install now registers the repo with the forge: registerWithForge mints a
publish token scoped to this one package via registry_publish and hands it to
source_forge.registerRepo, which sets it as the repo's CELILO_PUBLISH_TOKEN
Actions secret and applies branch protection to main. Re-minting rotates and
the secret is upserted, so running it every deploy IS the reconcile.

source_forge and registry_publish are declared OPTIONAL, not required.
lunacycle's manifest carries the reason in a comment and it applies exactly
here: under `requires` they are enforced at import time, so `module import`
fails outright on any fleet without a forge, which is every fleet the e2e
builds. The hook already warns and skips when they are absent, and there are
tests for both skip paths.

.forgejo/workflows/pr-validate.yml gates every PR; release.yml re-runs those
same gates on main before publishing, because pr-validate proves each BRANCH
is green and cannot prove main is. Both use git-native checkout and a mise
toolchain, because the celilo light runner has no node and actions/checkout
is a JS action.

The release ends at a .netapp in the registry. Nothing in it deploys. Rolling
a version onto the fleet is celilo-mgr's registry poll, and it needs two
pieces of operator state that deliberately are not in this repo:
auto_upgrade and upgrade_policy. Both default off.

e2e does not gate publish. It needs the Docker builder runner, and gating
every release on it means a runner outage blocks shipping.

The publish step is followed by asking the registry what it actually holds
and failing if the manifest version is not there. A publish that reports
success and ships nothing is a real outcome, and the exit code does not
distinguish them.

manifest.test.ts is new: celilo/types.d.ts is hand-written here, so nothing
enforced that it matched the manifest. It had already drifted — adding
forge_repo and publishes left the type behind, and only the hook typecheck
caught it. The test checks presence, optionality against the guide's rule,
that no field exists in the type alone, that every hook script exists, and
that the publish scope matches the module id. Verified it fails when a
required field is made optional and when a field is deleted.

Version 0.2.0 -> 0.3.0: new manifest surface, backward-compatible.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
e2e: a real cele2e deploy, and stand-in Google and Nominatim servers
Some checks are pending
pr-validate / validate (pull_request) Waiting to run
4f1495d04e
Two suites, split by what they actually need.

tests/bna-yard-sale.e2e.test.ts is a real cele2e deploy — real Caddy, real
ACME through pebble, real DNS through the fake registrar, on a host celilo
provisions. Ten stages following modules/celilo-website/e2e as the reference.
What it asserts beyond that reference is the part specific to this app: a 200
on the page proves only that Caddy is answering, because the page is a shell
that fetches its own data. So it fetches sales.json too, asserts it parses,
holds sales, and that every pin lands inside the neighborhood bounding box —
a geocoder misfire that shipped would put a marker on a stranger's house.

It also asserts the SERVED page renders seller text as text. site/build.ts
already refuses to build a page that fails that, but the build gate reads the
template; this reads what is actually on the wire. And stage 10 asks the
module's own health_check, which is an independent second reading from inside
the fleet.

The deploy imports the module onto a fleet with no forge and no registry,
which is the case that proves source_forge and registry_publish had to be
optional rather than required.

pipeline.integration.test.ts is where the Google stand-in lives, and it needs
no fleet. Real HTTP servers on localhost, not a mocked fetch, because the
bugs worth catching here live in the wire format. It serves the exact header
Google publishes for this form, where the address question wraps onto a second
line inside quotes, and proves findColumn still matches it. It covers the
redirect the published URL performs, and the case that matters most: an
unpublished sheet answers 200 with a sign-in page, which without a guard
parses into one very strange sale. Verified that guard fails when removed.

csv-to-json gained readResponses, so it reads either the local export or a
published-sheet URL, and NOMINATIM_URL is overridable so tests point at a
stand-in rather than hammering a free service on donated hardware.

tests/ is scoped to its own tsconfig for the same reason scripts/ is: the
harness ships raw .ts and checking it under this repo's settings reports
errors in somebody else's source. Both workflows typecheck it.

The deploy has NOT been run yet. The rig went busy between checking it and
using it — another session holds the run-lock. Everything static is green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
Take e2e out of CI entirely; it is a local command
Some checks are pending
pr-validate / validate (pull_request) Waiting to run
2fdf1fb5b9
The deploy suite runs against the shared cele2e rig, which is a single
machine-global resource behind a run-lock. A CI job firing at it would queue
behind whoever is at the keyboard.

Removed the e2e typecheck step from both workflows — CI now has no reason to
install the harness at all — and folded it into `bun run check`, which is what
runs before a commit. Also fixed a comment in release.yml that pointed at an
e2e.yml that never existed.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
Point the repo at the celilo org, so the runner can see its jobs
All checks were successful
pr-validate / validate (pull_request) Successful in 42s
95892faa63
The fleet's forgejo-runner is scoped org/celilo. This repo lived at
forgejo-admin/bna-yard-sale, a USER namespace, and Forgejo's scopes are a tree
where org and user are siblings rather than a hierarchy — so the runner could
never claim these jobs. The symptom is exactly what celilo documents as #627:
"Waiting for a runner with the following label: native", forever, with no error
anywhere, even though the runner exists and advertises that label.

forge_repo now defaults to celilo/bna-yard-sale, the register-forge tests
assert that path, and manifest.test.ts pins the org so this cannot regress
quietly. docs/one-time-setup.md gains a section on why the namespace is
load-bearing, including the alternative (runner_scope = instance, the only
scope spanning orgs and users) and why moving the repo is the smaller change.

The transfer itself is not in this commit — see the note in the PR.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
forgejo-admin changed title from Ship the map as a celilo module to Ship the map as a celilo module, with CI/CD and an e2e suite 2026-09-08 03:27:34 +00:00
ci: trigger a run now the repo sits where the runner can see it
All checks were successful
pr-validate / validate (pull_request) Successful in 28s
33f5e17bbf
The transfer to the celilo org carried the PR but not the run history, so
there is nothing queued at the new path. This empty commit is a pull_request
synchronize event and nothing else — the first honest test of whether the
org-scoped runner claims a job from this repo.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
test(e2e): get the cele2e deploy suite running, and fix what it caught
All checks were successful
pr-validate / validate (pull_request) Successful in 32s
0e0fc462de
The suite existed but had never been run. Running it found two defects in
this module and four in the harness.

Ours:

- The manifest required `public_web` 3.2.0, a version nothing has ever
  provided. The fleet's caddy provides 3.1.0, which is the current contract.
  No local gate can catch this: `celilo module check` reads the manifest, not
  the fleet. The deploy is what fails.
- The suite used `bna-yard-sale.org`. cele2e's simulated internet is
  authoritative only for the zones its DNS containers are seeded with, so the
  fake registrar answered `ENOENT ... /config/bna-yard-sale.org.zone` — there
  is no zone to write into. It now uses `iamtheinternet.org`, which also
  proves the module takes its hostname from config, since the manifest's
  default is the real domain and this is not it.

The harness ones are filed as celilo#1309 and celilo#1310, and the suite works
around the first three:

- `publishModule` shells out to a CLI path that exists only in the celilo
  monorepo, so packaging fails for every npm consumer. We package with the
  installed `@celilo/cli` and hand it the `.netapp`.
- It also takes the module id from the DIRECTORY basename. The `.netapp` form
  takes the id from the filename instead.
- Publishing a staged netapp runs `copyFileSync(path, path)` and DESTROYS it.
  `stagedNetappCopy` copies into a temp dir first.
- `registerControlPlane` defaults to a `modules/celilo-mgmt` that a consumer
  install ships empty, so it is handed the staged netapp explicitly.

`assertStandardModulesStaged` turns the resulting failure — which otherwise
arrives four stages later as `Module 'iptables' not found in registry` and
reads as a broken registry — into the one instruction that fixes it.

package.json gains a name and a version because `celilo package` stages
through `bun pm pack`, which refuses a package.json without them, and a
`files` list so the artifact stops carrying openspec/ and .claude/. Two tests
pin those to the manifest.

Not yet green on the published CLI: celilo#1310 strips this module's hook
runtime during packaging. Verified green against the fix (celilo#1311) — all
ten stages, 134s, 33 sales served and the module's own health check agreeing.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
docs(readme): record how to run the gates, and drop a question we answered
Some checks failed
pr-validate / validate (pull_request) Has been cancelled
2e89c3adf1
The Checks section listed raw `bunx tsc` invocations from before the module
existed, so it missed the hook and e2e typechecks and told you to run four
things by hand when `bun run check` runs all five in order.

It also had no word on the deploy suite, which is the gate that actually
proves this module works. Adds it, with the two things that will otherwise
cost an hour: `cele2e build-infra` must have run once on the machine, or the
suite fails four stages in with `Module 'iptables' not found in registry` and
reads as a broken registry rather than an empty cache; and the rig is one
machine-global resource behind a run lock, which is why this does not run in
CI.

Removes "whether to keep pasting into Squarespace at all" from Open questions.
That was decided — the map is a celilo module served at its own domain, and
the snippet is the contingency. Leaving a settled decision under "decisions
nobody has made yet" is worse than not writing it down.

Task 6.3 of add-celilo-hosting.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
docs(openspec): mark task 6.3 done
All checks were successful
pr-validate / validate (pull_request) Successful in 31s
c456ab0931
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
ci: pin the packaging CLI, and confirm the suite on released toolchain
All checks were successful
pr-validate / validate (pull_request) Successful in 28s
1e16f02ad9
@celilo/cli 2.2.1 carries the celilo#1310 fix, so the deploy suite now passes
with no unreleased code anywhere: ten stages, 142s, 33 sales served, module
reports healthy. The previous green runs were measured against an unmerged
branch; this one is against what anybody else would install.

Both workflows now run the CLI this repo pins rather than `bunx @celilo/cli`,
which resolves `latest` at run time. That is not tidiness. The CLI version
silently decides whether the artifact is usable at all — 2.2.0 stripped
scripts/node_modules out of any module with a root package.json, so it built a
.netapp that packaged, published, imported and deployed and then died in
on_install. CI already runs `bun install --frozen-lockfile`, so the lockfile is
now the single source of truth for which packager built the thing we ship, with
no version literal duplicated into the workflows.

@celilo/cli and @celilo/e2e are pinned exactly for the same reason.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018Qbb67pVLEyTPKMo9T8Pee
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
celilo/bna-yard-sale!3
No description provided.