The site's half of publishing. On 09-23 the front server held all of it: the publish hook, the GitHub Pages, Codeberg Pages and wisp.place publishers, the standard.site records and the PDS client. PER-SITE-PROCESS moved that code into lichen-site. SITE-FRONT-BOUNDARY unit 9 then gave the credentialed steps to the front server, and FULL-SITE-PROCESS gave them to the site process's admin thread (#25), so what is here is what the site's main runtime does: it reads its publish settings, discovers and keys its standard.site posts, and makes a publish's build (the source and a refs/dist/latest commit of dist) in its hub.
AST-verified changes
- The front server's publish module moved into lichen-site (PER-SITE-PROCESS P2c-2b commit 3, cf1f8e96): 24 files renamed from
lichen-server/src/publish/tolichen-site/src/publish/(settings, the three publishers,standard_site/*and their tests), andbuild_publish_hookintolichen-site/src/publish.rs. Checked by ast-checker 9a9c4144 (run id not recorded): "commit 3 move ok: 1 moved, 241 carried, 24 renamed; plant refused"; adversary rerun (A11333): equal=true, "3 1/0" (moved/visibility). Allowed:lichen_site::→crate::in the 24 moved files, one path rewrite (get_url_path_from_dist_path),lichen-site/Cargo.tomlas an other file. publish/router.rsmoved whole to lichen-site (P2c-2b commit 4b, c2eabe5a). Checked (run id not recorded): "4b ok"; adversary rerun (A11334): "equal=true whole-file rename". Allowed:lichen_site::→crate::in the file and one path rewrite (publish_record::run).publish/helpers.rs(with its tests) moved to lichen-site (P2c-2b commit 1d, 10a747b0), withgit/settings.rs(#22). Checked (run id not recorded): "3 moved, 32 carried, 5 visibility"; adversary rerun (A11333): equal=true, "1d 3/5".- The PDS XRPC core moved from
lichen-server/src/auth/atproto/pds.rstolichen-site/src/atproto/pds.rs(P2c-2b commit 1e, e3ed8ab6): 17 items,send_dpop_request,put_record,create_record,delete_record,list_records,upload_bloband their helpers. Builder count not recorded; adversary rerun (A11333): equal=true, "1e 17/1". Allowed: one visibility change (delete_record_with_contexttopub); both crates'Cargo.tomlas other files. publish/standard_site/mod.rsrenamed tostandard_site.rs(OPERATOR-REVIEW-FIXES U1, N8, f6a10c32), before commit 3 moved it. 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. The same commit renamedpublish/mod.rstopublish.rs; both halves of that one are in #25.- The publication renames of OPERATOR-REVIEW-REVISIONS R7 (d52675cd):
PublishResult→StandardSitePublication,PublishHook/Attempt/Record→Publication*,publish_staged→install_dist, and others. Checked by substituting the names back: run notify-392a0b96, "names substituted back AST EQUAL, control RED"; adversary (A11506): "rename AST-equal (control RED)". - Leaving this code: six functions of
standard_site/publish.rsandstandard_site/remote.rswent to the front server'spublish/records.rs(SITE-FRONT-BOUNDARY 9b, inside 98c16447, checked on a rebuilt pure move); see #25.
Roughly 920 of this PR's 3,293 changed lines are code these moves carried verbatim, mostly the PDS client and the deleted publish module's lines that reappear here; the manifest, the publish build and the publication report are new code.
On this forge the 6 renamed files here show as a whole deletion and a whole addition, so its diff reads 6,689 changed lines; with rename detection (git diff -M full/07-git-sync full/08-publish-site) it is 3,293.
What the site does now
- The publish build (
git/publish_build.rs, unit 9a): asked by the site process's admin thread, which runs the publish since FULL-SITE-PROCESS 6e (POST /cms/api/git/publish-build, admitted only on the admin thread's own connection:access::admin_host, D6), the site commits what is saved, buildsdistwith the standard.site links in it, makes a parentlessrefs/dist/latestcommit of the build with each link resolved to its bytes (a link that resolves outside the site refuses the publish), and pushesmainand that commit into the hub through the admin thread's one receive, which the front server relays to. It sends the editor'spublishevents; the admin thread asks it to sendfinishedorfailed(/cms/api/git/publish-event). - standard.site, the site's part (
publish/standard_site.rs,standard_site/manifest.rs, unit 9b): the site discovers its posts and gives each a record key before the build (so one build carries every link), then writes a manifest of the records it wants into the build's commit. The admin thread checks the manifest and writes the records with the owner's session; the main runtime that builds holds no session.standard_site/publish.rskeeps the site's keys, its publication identity and the counts, not the writing. - Settings and checks:
publish/settings.rs(the[publish]and[standard_site]sections),publish/helpers.rs(the checks of a remote URL and of a Pages repository and branch, which the admin thread uses before it pushes),publish/router.rs(GET /cms/api/publish/config, whether publishing is configured). - The report and record formats the admin thread fills:
publication.rs(one outcome per destination, operator note N4) andpublish_record.rs(the last publish and the recent ones). - The PDS client (
atproto/pds.rs): DPoP-signed record CRUD and blob upload, now shared by both crates;create_record_atwrites a chosen key withcreateRecord, which the PDS refuses where a record exists, so it never replaces one. - Deleted from the front server:
publish/github_pages.rs,codeberg_pages.rs,wisp.rs(the deploy is nowgit/deploy.rs, #25),router.rs,helpers.rs,standard_site/mod.rs;auth/atproto/pds.rskeeps the site-index records, the git-state record and the session check, and re-exports the client from lichen-site (+5/−393).
Files The build, read this first:
crates/lichen-site/src/git/publish_build.rs— new: the site's half of a publish.crates/lichen-site/src/publish/standard_site/manifest.rs— new: the keys and the manifest.
standard.site (crates/lichen-site/src/publish/):
standard_site.rs—prepare: discovery and keys for the build.standard_site/publish.rs— the site's standard.site state; the diff-and-sync ispublish/records.rs's, the admin thread's (#25).standard_site/discovery.rs,inject.rs,markdown.rs,identity.rs,types.rs— renamed fromlichen-server/src/publish/standard_site/; small changes (crate paths, test-support gates).
Settings, checks and formats:
crates/lichen-site/src/publish.rs,publish/settings.rs(renamed from lichen-server),publish/helpers.rs,publish/router.rs.crates/lichen-site/src/publication.rs,crates/lichen-site/src/publish_record.rs.crates/lichen-site/src/atproto/pds.rs— the PDS client;crates/lichen-server/src/auth/atproto/pds.rs— what the front server kept.
Deleted: crates/lichen-server/src/publish/{router,helpers,github_pages,codeberg_pages,wisp}.rs, publish/standard_site/mod.rs.
Moved in: the lichen-server publish deletions above (their code went to lichen-site in P2c-2b), and lichen-server/src/auth/atproto/pds.rs (round 1's map would put it under front-server auth; most of its change is the PDS client leaving for lichen-site/src/atproto/pds.rs).
Lives elsewhere
- The publish run, the records, the deploy and the publish record in the area (lichen-site's
publish/run.rs,publish/records.rs,git/deploy.rs,admin_area/publish.rs, run by the admin thread), and the front server's forwarding of the publish routes: #25. - The build itself (the rebuild lane, SSGs) and its scripts: #27, #26.
- The hub's receive that takes the build: #21.
lichen-site/src/ipc/publish.rs, the site process's publish ask: #19.- Tests (
publish/tests/,standard_site/tests/,tests/publish_record.rs, e2e publish tests) are in the base commit.
Category: Publishing in the site: standard.site, the publish build and the publish settings
Size: 23 files, +2,268/−1,025
Rounds: round 0 at stack ae42e3c0; at 29a7c15b and at 6b650b2a none of this PR's files changed; round 1 at b66397c0 (FULL-SITE-PROCESS 6e): the publish build is asked by the site process's admin thread, which now runs the publish, and is admitted only on its own connection (access::admin_host); the build's own push holds TriggerPushToRemote (the O4 split) where it held PushToRemote; publish.rs declares the moved dns, records and run; comments name the admin thread where they named the front server. Read this round in its interdiff: https://tangled.org/notplants-bot.bsky.social/lichen.page.october/pulls/24/round/1/interdiff
Plans and ledger: PER-SITE-PROCESS-2026-10-09.md (P2c-2b commits 1d, 1e, 3, 4b); SITE-FRONT-BOUNDARY-2026-10-17.md ("Publishing", decision 10, units 9a and 9b) with SITE-FRONT-BOUNDARY-U9-DESIGN-2026-10-23.md; OPERATOR-REVIEW-REVISIONS-2026-10-13.md (N4, R7); OPERATOR-REVIEW-FIXES-2026-10-07.md (U1, N8). Ledger: lichen-builder B2352, B2363, B2367, B2777, B3310; lichen-adversary A11224, A11333, A11334, A11506.
Inline #[cfg(test)] modules: none inline; 11 files point to a test file with #[path], 1 has test-only items (publish/standard_site.rs).
Bucket moves: round 1's Publishing PR is split: the site's half here, the front server's in #25. The deleted lichen-server publish files and auth/atproto/pds.rs are placed here with the code they became.
Round 1: #7 (Publishing: the front server publishes, the site builds) covered most of these files from 0b92cb61; standard_site/discovery.rs, inject.rs and markdown.rs and the lichen-server deletions are new to the set.