A site's publish (SITE-FRONT-BOUNDARY unit 9): one function, publish::run, runs every publish. It asks the site's main runtime for its build (#24), then does each step with credentials the main runtime and its children never hold: the standard.site records from the build's manifest, the push of the source, and the deploy of refs/dist/latest to GitHub Pages, Codeberg Pages or wisp.place. Since FULL-SITE-PROCESS this code is lichen-site's and runs on the site process's admin thread, under its own publish lock; the front server keeps only the forwarding of the publish routes (publish/front.rs). On 09-23 the front server published through a hook in the site's runtime with per-target publishers; those publishers are gone, and the deploy works from the hub.
AST-verified changes
- The records code moved from the site to the front server (SITE-FRONT-BOUNDARY 9b): six functions of
lichen-site/src/publish/standard_site/publish.rs(normalize_datetime,is_lexicon_datetime,record_hash,is_locally_deleted,document_candidates,publish_with_tree) topublish/records.rs, andstandard_site/remote.rstopublish/records/remote.rs. The landed commit, 98c16447, mixes the move with the change, so the move was checked on a rebuilt pure move (80c41e47 → 50f87658): run notify-19d4ab25, "AST_MOVE_RC=0 with no errors (6 items moved, 1 file renamed, 5146 stayed)"; run notify-8b2d0074, "AST-equal, both plants caught"; adversary rerun notify-b2a7765c, equal. No rewrites allowed. The evidence (machine-docs/evidence/sfb-9b-records-move/) puts the change after the move atrecords.rs+604/−235 andremote.rs+72/−54. - Before that, the same code had gone the other way: P2c-2b commit 3 (cf1f8e96) moved the front server's whole publish module,
standard_site/publish.rsandstandard_site/remote.rsincluded, into lichen-site. Checked (run id not recorded): "commit 3 move ok: 1 moved, 241 carried, 24 renamed; plant refused"; adversary rerun (A11333) equal=true. So in this difflichen-server/src/publish/standard_site/publish.rs(−1069) andrecords.rs(+1083, now in lichen-site) read against each other, as dostandard_site/remote.rs(−247) andrecords/remote.rs(+341). The rest of commit 3 is in #24. publish/mod.rsrenamed topublish.rs(OPERATOR-REVIEW-FIXES U1, N8, f6a10c32). Checked over 8b30050a..f6a10c32 (run id not recorded): "3004 items, 48 moved, 69 carried, 19 renamed, 2 visibility as mapped, 233 module declarations"; adversary rerun notify-abab368a, the same counts. Both halves are here:publish.rs, and the deletedpublish/mod.rs(−65).- SECURITY-FOLDER-AND-PRINCIPALS unit 2 (b603dbbe):
site_area/publish.rs→admin_area/publish.rs, part ofsite_area/→admin_area/(13 files renamed,crate::site_area→crate::admin_areain all lichen-server Rust). Checked by the builder: "AST equal three ways (8da73502 → b603dbbe, → bdaa795b, → 067dbe37), plants caught"; adversary rerun notify-020eb199: "all equal=True (2471/2471, renamed 13, carried 426, visibility 0, errors 0)". The non-move edits (d85a66d2:include_str!paths, literals) and the rustfmt (bdaa795b) are separate commits. - The path rewrites of SECURITY-FOLDER-AND-PRINCIPALS units 1 and 2 in this PR's files (
crate::site_area→crate::admin_area,crate::access→crate::security::access, and the othersecurity::paths), which those checks cover (unit 1 notify-eebdd32e, unit 2 adversary notify-020eb199, both equal, errors 0), with the rustfmt rewraps after them (d6c25912, bdaa795b), proven bycargo fmt. - FULL-SITE-PROCESS 5b (#1649 on lichen.page.review, 26779036 → 0e8871fe, map
machine-docs/reports/fsp-5b/move-map.json): one move commit (85d6303c) renamed 34 files whole from lichen-server to lichen-site (19 modules and their 15#[path]test files); lichen-server re-exports each where it was. Here:publish/{run, records, records/remote, dns}.rs,git/deploy.rsandadmin_area/publish.rs, now undercrates/lichen-site/src/, and the re-exports in lichen-server'spublish.rs. Checked by run notify-981f7470: "equal=True; items 5557/5557, stayed 4752, carried 805 in the 34 renamed files; visibility_changes 99, every one listed; comments compared 4991; module declarations checked 352"; plants item_edited and visibility_unlisted caught (a dropped declaration gate is not caught, because the checker lets declarations change; the PR's gate table covers it); adversary reruns equal (notify-c53fbd02). Allowed: the 99 listed visibility changes,pubfor what the front server calls (6a2b574d, c04bebac, 0e8871fe, inside the checked range). Outside the check: 26779036 (path-only, before the move), e0dc7411 (FSP-NOMDBOOK1), thefor_testsaccessors and test hooks, ad03def8 (rustfmt).
Roughly 1,230 of this PR's 4,537 changed lines are code these moves carried verbatim, nearly all of it the records code that standard_site/publish.rs held on 09-23. run.rs, deploy.rs and admin_area/publish.rs were written after 09-23, so they read as new code, though 5b moved them.
On this forge the renamed file here shows as a whole deletion and a whole addition, so its diff reads 5,011 changed lines; with rename detection (git diff -M full/08-publish-site full/09-publish-front) it is 4,537.
A publish now (publish/run.rs, the site process's admin thread's since FULL-SITE-PROCESS 6e), the same for the editor's Publish (POST /cms/api/publish on the site's host, which the front server forwards as the client's identity), the dashboard's (trigger-publish, forwarded as an admin act) and a save with auto_publish (the site's own ask):
- One publish at a time per site, under the admin thread's lock: a publish asked for while one runs is answered busy and runs nothing; a site's ask waits for it.
- The site's main runtime is asked for its build, within a time bound: it commits, builds with its records' links, and pushes
mainand a parentlessrefs/dist/latestcommit into the hub. What is deployed is what the hub holds. - Then each step, whatever the one before did (operator note N4): the records, the source's push when
push_on_publish(git::outbound::now, #23), the deploy. The report keeps thePublicationReportshape, one outcome per destination. - The record of the publish is in the site's area (
admin_area/publish.rs), andGET /cms/api/publishanswers it. A publish's end goes to the site's frontends and, told to the front server, to the dashboard's stream (PublishFinished, coalesced per site).
The records (publish/records.rs): the admin thread's own record of the site's keys (standard-site.json in the area) is the authority, migrated once from the site's state. Only records of this site's publication are touched (several sites can share one PDS). The manifest is checked before any write: every key a TID, a recorded path keeps its key, a new key is created with createRecord, which the PDS refuses where a record exists, and a delete names only recorded keys. They are written with the owner's session the front server lends the site process.
The deploy (git/deploy.rs): for Pages, a parentless commit of the build's tree with the domain file, made in the hub and pushed as the source's push is (unit 6's IP rule, no redirects, confined, time-bounded); the target is the site's own setting, so the push carries none of the owner's credentials: over SSH it uses the site's own Pages key from the area (POST /api/sites/{d}/publish-key), and a pds:// target is refused. For wisp.place, the tree is written out by git and wisp-cli deploy runs confined, with its home in the site's area. The Pages key and wisp's credential reach the main runtime and its children only with the owner's opt-in.
What stays in the front server (publish/front.rs, new): it admits GET/POST /cms/api/publish and the dashboard's trigger-publish and forwards them to the site process's admin thread; a site's push that moved its main asks its own admin thread; a publish's end is told here for the dashboard's event.
Files
Moved into crates/lichen-site/src/ (FULL-SITE-PROCESS 5b), run by the admin thread:
publish/run.rs— the one publish path. Read this first.publish/records.rs— the record of keys, the manifest check, writing and sweeping records. Its 09-23 counterpart,lichen-server/src/publish/standard_site/publish.rs(−1069), is deleted here.publish/records/remote.rs— remote inventories; a publication is this site's only when the record holds it. Its 09-23 counterpart,lichen-server/src/publish/standard_site/remote.rs(−247), is deleted here.git/deploy.rs— the Pages and wisp.place deploy.admin_area/publish.rs— the publish record, the Pages deploy key and wisp's credential, in the area; no route answers the key or the credential.publish/dns.rs— the DNS check of a publish target, renamed from lichen-server;digstarts through the sandbox's command builder.
Still in crates/lichen-server/src/:
publish/front.rs— new: the front server's forwarding of a publish.publish.rs— the module list; re-exports the moved modules and the site'shelpers,routerandsettings.publish/mod.rs(−65), the 09-23 root it was renamed from, is deleted here.
Moved in: publish/standard_site/publish.rs's and standard_site/remote.rs's deletions, placed with records.rs and records/remote.rs (git pairs them only below its default similarity), publish/mod.rs's deletion, with the publish.rs it became, and publish/dns.rs (by an explicit rule). git/deploy.rs and admin_area/publish.rs were already here in round 1.
Lives elsewhere
- The site's build, manifest, settings and the PDS client: #24. The rebuild lane: #27.
- The source's push (
git/outbound.rs): #23. The hub's receive: #21. - The dashboard's
trigger-publishroute:api.rsin #32. The owner's session as the front server holds it:security/credentials.rsin #30; as the site process holds it: #19. - The admin thread the publish runs on: #38.
- Tests (
tests/publish__records*.rs,tests/publish__run.rs, lichen-server'stests/moved/run_tested.rs,publish/tests/dns__*.rs, e2e publish tests) are in the base commit.
Category: Publishing: runs, records and deploys, on the site process's admin thread
Size: 11 files, +3,153/−1,384
Rounds: round 0 at stack ae42e3c0; round 1 at 6b650b2a (SECURITY-FOLDER-AND-PRINCIPALS): publish.rs renamed into admin_area/ (unit 2), and path rewrites and rustfmt in git/deploy.rs, publish/records.rs and publish/run.rs; round 2 at b66397c0 (FULL-SITE-PROCESS units 4, 5b, 6e): the publish run, the records, the deploy, the DNS check and the publish record moved into lichen-site (5b, AST-checked) and now run on the site process's admin thread, under its lock, with the owner's lent session; the front server keeps publish/front.rs (new), which forwards the publish routes. Read this round in its interdiff: https://tangled.org/notplants-bot.bsky.social/lichen.page.october/pulls/25/round/2/interdiff
Plans and ledger: FULL-SITE-PROCESS-2026-10-25.md (part 7; units 4, 5b, 6e) with FULL-SITE-PROCESS-U0-DESIGN-2026-10-26.md; SITE-FRONT-BOUNDARY-2026-10-17.md ("Publishing", decision 10, units 9a and 9b) with SITE-FRONT-BOUNDARY-U9-DESIGN-2026-10-23.md (§4, §9, §11, §12); OPERATOR-REVIEW-REVISIONS-2026-10-13.md (N4); PER-SITE-PROCESS-2026-10-09.md (P2c-2b commit 3); OPERATOR-REVIEW-FIXES-2026-10-07.md (U1, N8). Ledger: lichen-builder B2363, B3310, B3338; lichen-adversary A11224, A11333, A11659.
Inline #[cfg(test)] modules: none inline; 4 files point to a test file with #[path]; publish/front.rs has test-only items.
Bucket moves: round 1's Publishing PR is split: the publish run, records and deploys here (the front server's until FULL-SITE-PROCESS, the admin thread's now), the site's build and settings in #24. standard_site/publish.rs's and standard_site/remote.rs's deletions are placed with records.rs and records/remote.rs. Every file kept its PR when FULL-SITE-PROCESS moved it into lichen-site: the subject did not change, its home did.
Round 1: #7 (Publishing: the front server publishes, the site builds) covered these files from 0b92cb61; publish/dns.rs, publish/front.rs and the standard_site/publish.rs, standard_site/remote.rs and publish/mod.rs deletions are new to the set.