Map build delivery, and narrow the Idunn invariant to services
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
6dc1860770
commit
2c2b92579f
2 changed files with 547 additions and 2 deletions
542
docs/build-delivery-cut.md
Normal file
542
docs/build-delivery-cut.md
Normal file
|
|
@ -0,0 +1,542 @@
|
|||
# Build Delivery: Cut Map
|
||||
|
||||
Date: 2026-09-17
|
||||
|
||||
Status: Imagination pass, cut map. Nothing here has landed. Anchors are against
|
||||
`codex/item-provenance` HEAD `6dc18607`. Ends are owned by
|
||||
`docs/build-delivery-target.md`; this document owns the means.
|
||||
|
||||
Claims marked **(probe)** were run, not read off names. What was run:
|
||||
|
||||
- A real batchmode player build of the main tree, Unity closed, output to a
|
||||
scratch directory outside the repo:
|
||||
`Unity.exe -batchmode -quit -nographics -silent-crashes -projectPath F:\Projects\Aetheria -buildWindows64Player <scratch>\playerbuild\Aetheria.exe -logFile <scratch>\unity-player-build.log`.
|
||||
No Editor script was added; `-buildWindows64Player` needs none. Exit code 0.
|
||||
It dirtied one tracked file, `ProjectSettings/UnityConnectSettings.asset`,
|
||||
which was restored to `6dc18607`; the other 16 modified files in the worktree
|
||||
predate this pass. Build output and the zip were deleted afterwards.
|
||||
- A headless run of the produced player:
|
||||
`Aetheria.exe -batchmode -nographics -logFile <scratch>\player-run.log`, killed
|
||||
at 25s.
|
||||
- Reads of `gamecult-ops` inventory, runbooks, `nginx/`, and its deploy scripts;
|
||||
Idunn `src/` and `docs/`; `gamecult-site`, `AetheriaLore` and `GameCult-Quartz`
|
||||
configs and workflows; the `CultLib-semver` worktree.
|
||||
|
||||
No remote host was contacted. No `ssh` was run.
|
||||
|
||||
## 0. What the probes changed about the target
|
||||
|
||||
Four findings move the design. Read these before the cuts.
|
||||
|
||||
### 0.1 A player build already works **(probe)**
|
||||
|
||||
The target says Aetheria "has no player build pipeline"
|
||||
(`docs/build-delivery-target.md:11`). Half true: there is no build *code* — zero
|
||||
`BuildPipeline`/`BuildPlayer`/`BuildTarget` references in any tracked `.cs`, and
|
||||
`Build/` holds only Eve asset bundles and logs, never a player. But the project
|
||||
itself builds clean today with the stock command-line switch.
|
||||
|
||||
- Scripts compiled for StandaloneWindows64 with warnings only (`CS0067` on
|
||||
`Assets/Scripts/ServerShared/Behaviors/LockWeapon.cs:44-46`).
|
||||
- ~9 minutes wall on Starfire with a warm `Library/`.
|
||||
- Output 212 files, **352.4 MB** on disk; **189.8 MB** as a zip
|
||||
(`Compress-Archive -CompressionLevel Optimal`, 35s).
|
||||
- Scenes come from `ProjectSettings/EditorBuildSettings.asset:8-13`:
|
||||
`Assets/Scenes/Main Menu.unity`, `Assets/Scenes/ARPG.unity`, both enabled.
|
||||
- Scripting backend is Mono, not IL2CPP: `ProjectSettings/ProjectSettings.asset:660-661`
|
||||
sets `scriptingBackend` for Android only, so Standalone takes the default.
|
||||
- `Aetheria_BurstDebugInformation_DoNotShip/` is emitted beside the exe and must
|
||||
not ship.
|
||||
|
||||
So Cut 1 is not "make a build possible". It is "make a build repeatable,
|
||||
identified, and complete", which is a much smaller cut.
|
||||
|
||||
### 0.2 The catalog gap is silent, not loud **(probe)**
|
||||
|
||||
`ActionGameManager.cs:38-41` resolves `GameDataDirectory` as
|
||||
`new DirectoryInfo(Application.dataPath).Parent.CreateSubdirectory("GameData")`,
|
||||
and `:45` builds `CatalogPath` from it. In a player that is
|
||||
`<build>\GameData\Aetheria.cc`. Nothing in the repo copies the catalog there.
|
||||
|
||||
The dangerous part is what happens next. `AetheriaStores.cs:13-15` promises the
|
||||
open "Throws when a `[CultGlobal]` type routed to the catalog has no record", and
|
||||
`:26-31` implements exactly that. But none of the seven catalog types at
|
||||
`AetheriaStores.cs:9` is `[CultGlobal]`, so the guard selects nothing and the
|
||||
open succeeds over a catalog file that does not exist.
|
||||
|
||||
Proven, not inferred: the built player launched, reached Main Menu, ran for 25s,
|
||||
and wrote `<build>\GameData\player.cc` (1,964 bytes) — while `Aetheria.cc` was
|
||||
absent the whole time. No exception, no log line about the catalog. The only
|
||||
errors were `Stardust` compute-shader nulls, which are `-nographics` artefacts.
|
||||
|
||||
A player shipped without its catalog therefore does not refuse. It boots to a
|
||||
menu and fails later, in the ARPG scene, with an empty world. That is worse than
|
||||
a crash, and Cut 1 has to close it.
|
||||
|
||||
### 0.3 Idunn structurally cannot carry a file artifact
|
||||
|
||||
The target invariant reads "Idunn owns deployment to Yggdrasil"
|
||||
(`docs/build-delivery-target.md:44`). Idunn cannot own this one.
|
||||
|
||||
- `Idunn/src/deployment.rs:33` — `TargetDeclaration.service` is a plain
|
||||
`ServiceDeclaration`, not an `Option`. Every deployment unit is a service.
|
||||
- `Idunn/src/deployment.rs:110-141` — a `ServiceDeclaration` requires an
|
||||
`executable_artifact`, a transport, and a mandatory `health` declaration;
|
||||
`:851` refuses a plan whose service executable is not a declared artifact.
|
||||
- Admission does not complete until the workload publishes its own signed
|
||||
presence and health (`Idunn/docs/deployment-authority.md:84-92`). A zip cannot
|
||||
sign presence.
|
||||
- The only workload drivers are `SystemdTransient` and `HostActuator`
|
||||
(`Idunn/src/deployment.rs:442-450`) — both process supervisors. There is no
|
||||
oneshot path.
|
||||
- The only route drivers are `NginxStreamTcp`/`NginxStreamUdp`
|
||||
(`Idunn/src/deployment.rs:698-701`), writing stream fragments to
|
||||
`/etc/nginx/idunn-stream-routes/`. Idunn has no concept of an HTTP `root`,
|
||||
`try_files`, or a web root anywhere in `src/`.
|
||||
- Idunn has no push transport at all. It pulls: `GitSourceDriver`
|
||||
(`Idunn/src/drivers.rs:682,714`) fetches the admitted ref on the host. Even the
|
||||
remote Windows actuator fetches its own source
|
||||
(`Idunn/docs/host-actuator.md:27-47`). Bytes never travel *from* Idunn.
|
||||
|
||||
`ArtifactOutput` (`Idunn/src/deployment.rs:91-101`) can carry an arbitrary blob
|
||||
with an `expected_sha256`, and `required_adjacent_artifacts` (`:115`) ships
|
||||
non-executable files — but only alongside a supervised executable. There is no
|
||||
unit shaped like "a file at a URL".
|
||||
|
||||
Meanwhile Yggdrasil already serves static files, twice, through a different and
|
||||
proven authority (see 0.4). The target invariant should be narrowed to "Idunn
|
||||
owns *service* deployment to Yggdrasil"; static bytes already have an owner, and
|
||||
it is not a second deployment authority invented for this migration. See **F1**.
|
||||
|
||||
### 0.4 The site is GitHub Pages; only the bytes can live on Yggdrasil
|
||||
|
||||
`gamecult.org` is GitHub Pages out of `GameCult/gamecult-site`
|
||||
(`gamecult-ops/inventory.md:728-738`, `runbooks/gamecult-pages-cutover.md:1-19`),
|
||||
built by the shared engine `GameCult/GameCult-Quartz` through a reusable workflow
|
||||
(`gamecult-site/.github/workflows/deploy-quartz.yml`,
|
||||
`GameCult-Quartz/.github/workflows/quartz-pages.yml:24-72`, ending in
|
||||
`actions/deploy-pages@v4`). `aetheria.gamecult.org` is the same mechanism out of
|
||||
`GameCult/AetheriaLore` (`AetheriaLore/site/quartz.config.ts:66`,
|
||||
`gamecult-ops/inventory.md:801-806`).
|
||||
|
||||
A 190 MB zip cannot go in either repo: GitHub blocks files over 100 MB, and
|
||||
Pages has its own soft limits. So the split is forced and is exactly what the
|
||||
operator asked for — the *page* is on the site, the *bytes* are on Yggdrasil.
|
||||
|
||||
What Yggdrasil already has:
|
||||
|
||||
- nginx with real static vhosts: `gamecult-ops/nginx/erycina.org.conf:4`
|
||||
(`root /srv/erycina-site/site/current`), `nginx/velvet.erycina.org.conf:4`.
|
||||
- An immutable-release store pattern:
|
||||
`scripts/deploy-erycina-site-yggdrasil.sh:33-50` unpacks into
|
||||
`/srv/<site>/site/releases/<UTC-stamp>` and moves `current` atomically.
|
||||
- A generic TLS publisher: `scripts/publish-static-site-tls-yggdrasil.sh:1-60`,
|
||||
parameterised by `SITE_HOST`/`SITE_ROOT`, refusing to run unless DNS already
|
||||
resolves to this host (`:40-56`). certbot/Let's Encrypt throughout.
|
||||
- Transport: `scp ... ygg:` then `ssh ygg 'sudo -n bash /tmp/...'`
|
||||
(`runbooks/erycina-org-site-deploy.md:78-81`).
|
||||
|
||||
One thing does **not** transfer. The site-release pattern rotates: a `current`
|
||||
symlink plus `KEEP_RELEASES=5` pruning
|
||||
(`scripts/deploy-erycina-site-yggdrasil.sh:50-56`). The target requires the
|
||||
opposite — "A published version stays published"
|
||||
(`docs/build-delivery-target.md:32`). The artifact store is append-only, with no
|
||||
`current` symlink and no pruning. Cut 4 says so explicitly, because copying the
|
||||
erycina script wholesale would silently delete published downloads.
|
||||
|
||||
## 1. Authority map (whole migration)
|
||||
|
||||
- **Owner of the release record:** a new CultCache store
|
||||
`Releases/aetheria.releases.cc` in this repo, one `aetheria.release` document
|
||||
per published build. The Aetheria repo owns the facts about Aetheria builds.
|
||||
- **Inputs:** the built artifact's bytes (size, sha256), the commit it was built
|
||||
from, the platform, the build host, the version, and a hand-authored changelog
|
||||
entry.
|
||||
- **Outputs:** the artifact zip; the release document; a derived feed and derived
|
||||
changelog pages in the site repo.
|
||||
- **Derived state, never authored:** the browser-facing JSON feed, the download
|
||||
page, and every changelog page. All are projections of the release store,
|
||||
regenerated wholesale. Editing them by hand is the failure the target names at
|
||||
`docs/build-delivery-target.md:30-31`.
|
||||
- **Forbidden writers:**
|
||||
- The *game* must never read or write the release store. It is not added to
|
||||
`AetheriaStores.cs:9-11`, and `ActionGameManager.cs:56` keeps opening exactly
|
||||
three stores. A release record is publishing state, not game state.
|
||||
- Idunn does not write the artifact, the feed, or the pages (0.3).
|
||||
- The site repos never author release facts; their content files for downloads
|
||||
and changelog are generated and overwritten.
|
||||
- `Build/` stays gitignored (`.gitignore:8`) and never becomes an artifact
|
||||
store.
|
||||
- **Shared paths:** there is exactly one publish primitive. A manual publish, a
|
||||
re-publish, and any future automated publish all go through the same
|
||||
`release publish` verb; none of them writes the feed or the pages directly.
|
||||
- **Deletion line:** the vacuous catalog guard at `AetheriaStores.cs:26-31` is
|
||||
replaced before Cut 1 adds any build code, and
|
||||
`docs/build-delivery-target.md:11-14` is corrected once Cut 1 lands.
|
||||
|
||||
**Atomicity, honestly.** The target asks that publishing "publishes the artifact,
|
||||
the feed entry and the changelog entry together, or it publishes nothing"
|
||||
(`:28-29`). Three destinations — a remote filesystem, a git-tracked store, and a
|
||||
second git repo — admit no real two-phase commit. The invariant that actually
|
||||
protects a visitor is narrower and is the one to enforce: **no feed entry exists
|
||||
without verified bytes already fetchable at its URL.** Order is artifact → verify
|
||||
by re-downloading and re-hashing → release document → projection → site push.
|
||||
A failure before the release document leaves unreferenced bytes, which are inert
|
||||
and which the next publish of the same version may overwrite only when the hash
|
||||
matches. A failure after it leaves a pending page, which the next site build
|
||||
fixes. Neither produces a download link that 404s.
|
||||
|
||||
## 2. Cuts
|
||||
|
||||
### Cut 1. A player build that is repeatable, identified, and carries its catalog
|
||||
|
||||
- **Repo/branch:** Aetheria, `codex/build-delivery` from `6dc18607`. Depends on
|
||||
nothing.
|
||||
- **First:** keep the probe numbers in 0.1 as the baseline — 212 files,
|
||||
352.4 MB, 189.8 MB zipped, ~9 min warm. A first landed build that differs
|
||||
wildly means the build options diverged from the stock switch.
|
||||
- **Deletes first:**
|
||||
- `Assets/Scripts/ServerShared/AetheriaStores.cs:13-15` (the comment promising
|
||||
a guard that does not fire) and the guard body at `:26-31`, replaced rather
|
||||
than supplemented.
|
||||
- **Adds:**
|
||||
- `Assets/Editor/AetheriaBuild.cs` — one static entry point for
|
||||
`-executeMethod`, taking version and output root from the command line. It
|
||||
goes under the existing `Assets/Editor/` (whose only current file is
|
||||
`Epiphany/EpiphanyEditorBridge.cs`) and compiles into the existing
|
||||
`Assembly-CSharp-Editor`; no new asmdef, no new assembly.
|
||||
- A catalog staging step in that entry point.
|
||||
- **Per-file changes** (against `6dc18607`):
|
||||
- `Assets/Scripts/ServerShared/AetheriaStores.cs:16-32`: `Open` refuses a
|
||||
`catalogPath` that does not exist, unless `catalogWritable` (the seed case
|
||||
already exempted at `:24-25`). Keep the existing global check as a second,
|
||||
narrower guard; it is not wrong, it is merely inert for today's type set, and
|
||||
it will bite the moment a catalog type is marked `[CultGlobal]`. The new
|
||||
check is the one that protects a shipped player.
|
||||
- `Assets/Editor/AetheriaBuild.cs` (new): `BuildPlayerOptions` with
|
||||
`scenes` read from `EditorBuildSettings.scenes` filtered on `enabled` — the
|
||||
same two scenes at `ProjectSettings/EditorBuildSettings.asset:8-13`;
|
||||
`target = BuildTarget.StandaloneWindows64`. Sets
|
||||
`PlayerSettings.bundleVersion` from the passed version, superseding the
|
||||
stale `0.1` at `ProjectSettings/ProjectSettings.asset:152`. After the build:
|
||||
copy `GameData/Aetheria.cc`, `GameData/Narrative/**` and
|
||||
`GameData/SoundbanksInfo.json` next to the exe; delete
|
||||
`Aetheria_BurstDebugInformation_DoNotShip/`; fail the run on a non-empty
|
||||
`BuildReport` error list.
|
||||
- **Authority map:**
|
||||
- Owner: `AetheriaBuild` owns what a player build contains.
|
||||
- Inputs: `EditorBuildSettings.scenes`, `GameData/`, a version string, an
|
||||
output root.
|
||||
- Outputs: a complete player directory, and the build report.
|
||||
- Derived: `PlayerSettings.bundleVersion` is now set by the build, not authored
|
||||
in `ProjectSettings.asset`.
|
||||
- Forbidden writers: nothing else sets `bundleVersion`; nothing else decides
|
||||
the scene list.
|
||||
- Deletion line: the inert-guard comment goes before the entry point is added.
|
||||
- **Verification:**
|
||||
- builds: the entry point produces a player; size within ~10% of 352.4 MB.
|
||||
- negative: `git grep -n "Aetheria_BurstDebugInformation"` finds only the
|
||||
deletion in `AetheriaBuild.cs`.
|
||||
- test: a `Aetheria.Shared.Tests` case pins that `AetheriaStores.Open` throws
|
||||
on a nonexistent read-only catalog path, and still opens a writable empty one
|
||||
(the seed case at `AetheriaStores.cs:24-25`). This is the regression that 0.2
|
||||
proves is live today.
|
||||
- operator: launch the staged build by double-click, reach Main Menu, start a
|
||||
game, confirm the world is populated. Then rename `GameData/` beside the exe
|
||||
and relaunch: it must refuse loudly, not boot to a menu.
|
||||
|
||||
### Cut 2. The release record
|
||||
|
||||
- **Repo/branch:** Aetheria, same branch. Depends on Cut 1 for the fields it
|
||||
records.
|
||||
- **Adds:**
|
||||
- An `aetheria.release` `[CultDocument]` type, declared in the tools assembly,
|
||||
not in `ServerShared` — the game must not link it.
|
||||
- `Releases/aetheria.releases.cc`, tracked in git. Small: a few KB per release.
|
||||
- **Shape.** Model the fields on the one existing GameCult type that already
|
||||
describes a versioned build artifact,
|
||||
`CultLib/src/GameCult.Mesh/CultMeshCdn.cs:130-171`
|
||||
(`gamecult.mesh.cdn_artifact_manifest`): `Version` (`:145`), `ContentHash`
|
||||
sha256 (`:151`), `SizeBytes` (`:156`), `MimeType` (`:160`), `CreatedAtUtc`
|
||||
(`:164`). Note `CultMeshCdnArtifactKinds.Build = "build"` at `CultMeshCdn.cs:34`
|
||||
is documented as "Executable build or install/update payload" — this exact case.
|
||||
Add what that type lacks and the target requires: `Commit`, `Platform`,
|
||||
`BuildHost`, `Url`, and the changelog body. See **F7** on reuse vs a new type.
|
||||
- **Authority map:**
|
||||
- Owner: the release store is the authority on what exists
|
||||
(`docs/build-delivery-target.md:30`).
|
||||
- Inputs: Cut 3's publish verb only.
|
||||
- Derived: everything a browser sees.
|
||||
- Forbidden writers: the game (`AetheriaStores.cs:9-11` unchanged,
|
||||
`ActionGameManager.cs:56` unchanged); the site repos; any hand edit.
|
||||
- Deletion line: none — nothing exists to replace.
|
||||
- **Verification:**
|
||||
- negative: `git grep -n "aetheria.release"` must not match anything under
|
||||
`Assets/Scripts/ServerShared/`. Checked against the `aetheria.*` schema names
|
||||
already in that tree so it cannot collide.
|
||||
- test: reopening the store yields the same documents; a published version is
|
||||
refused on re-publish with different bytes.
|
||||
|
||||
### Cut 3. One publish primitive
|
||||
|
||||
- **Repo/branch:** Aetheria, same branch. Depends on Cuts 1 and 2.
|
||||
- **Keeps and reuses:** `tools/AetherDb` — the existing headless .NET console over
|
||||
Aetheria's CultCache stores, with store-path resolution already written at
|
||||
`tools/AetherDb/AetherDb.cs:15-32`. A `release` verb group there adds **zero**
|
||||
build targets and reuses the csproj, the `Aetheria.Shared` reference, and the
|
||||
path walk. A second console would add a target for nothing. See **F5** on who
|
||||
runs it.
|
||||
- **Adds:** `release build`, `release publish`, `release project` verbs, and
|
||||
`CHANGELOG.md` at the repo root in the Keep-a-Changelog shape CultLib just
|
||||
adopted (`CultLib-semver/unity/org.gamecult.cultlib/CHANGELOG.md:1`).
|
||||
- **Per-file changes:**
|
||||
- `tools/AetherDb/Program.cs`: new verb dispatch. `release build` shells the
|
||||
Unity entry point from Cut 1 and zips the result, excluding nothing further
|
||||
(Cut 1 already removed the Burst directory). `release publish` computes
|
||||
sha256 and size, reads the `CHANGELOG.md` section for the version, writes the
|
||||
`aetheria.release` document, and emits the projection. `release project`
|
||||
regenerates the projection alone from the store, so the pages can be rebuilt
|
||||
without a build.
|
||||
- Follow the deterministic-packaging precedent in
|
||||
`CultLib-semver/scripts/build-unity-package.ps1:41-52`, which clears
|
||||
intermediates and disables source-revision stamping so a rebuild is
|
||||
byte-comparable. The target asks for reproducibility from a commit
|
||||
(`docs/build-delivery-target.md:36-37`); say plainly in the doc what Unity
|
||||
does *not* guarantee there rather than claim byte-identity that Cut 1 has not
|
||||
demonstrated.
|
||||
- **Authority map:**
|
||||
- Owner: `release publish` is the only writer of the release store.
|
||||
- Derived: the feed JSON and the markdown pages, regenerated wholesale, never
|
||||
merged into.
|
||||
- Shared paths: a first publish and a re-publish use the same verb.
|
||||
- Forbidden writers: no script writes the feed or pages directly.
|
||||
- **Verification:**
|
||||
- test: `release project` run twice produces byte-identical output.
|
||||
- negative: nothing outside `tools/AetherDb` opens `aetheria.releases.cc`.
|
||||
- operator: none yet; Cut 5 carries the smoke.
|
||||
|
||||
### Cut 4. The artifact host on Yggdrasil
|
||||
|
||||
- **Repo/branch:** `gamecult-ops`, its own branch. Depends on Cut 3 producing a
|
||||
zip. Independent of Cuts 1-3 landing.
|
||||
- **Adds:**
|
||||
- `nginx/<host>.conf` modelled on `nginx/erycina.org.conf`, keeping the ACME
|
||||
location (`:11-14`) and the security headers (`:7-9`), but with
|
||||
`root /srv/aetheria-builds`, `autoindex off`, and **no** `try_files`
|
||||
fallback and **no** `error_page` rewrite — a missing version must 404.
|
||||
- `scripts/deploy-aetheria-build-yggdrasil.sh`. Modelled on
|
||||
`scripts/deploy-erycina-site-yggdrasil.sh` but with the rotation removed:
|
||||
the release path is `/srv/aetheria-builds/<version>/`, it **refuses** to
|
||||
write into a version directory that already exists, there is **no** `current`
|
||||
symlink, and there is **no** pruning. Copying `:50-56` unchanged would delete
|
||||
published downloads and break
|
||||
`docs/build-delivery-target.md:32`.
|
||||
- A runbook alongside `runbooks/erycina-org-site-deploy.md`.
|
||||
- **Reuses unchanged:** `scripts/publish-static-site-tls-yggdrasil.sh` for the
|
||||
vhost install and certbot run — it is already parameterised by
|
||||
`SITE_HOST`/`SITE_ROOT` (`:1-60`) and already refuses to run before DNS
|
||||
resolves to this host (`:40-56`).
|
||||
- **Hazard to carry into the runbook:** five existing `*.gamecult.org` records in
|
||||
`gamecult-ops/inventory.md` still say `A -> 94.130.223.131`
|
||||
(`:746, :764, :771, :777, :785`) while Yggdrasil is at `159.195.114.249`
|
||||
(`:363, :387`). The new record must use the current address, and the stale rows
|
||||
are a follow-up outside this migration.
|
||||
- **Authority map:**
|
||||
- Owner: the release directory tree on Yggdrasil is append-only and root-owned.
|
||||
- Inputs: a zip and its sha256, scp'd by the operator.
|
||||
- Forbidden writers: nothing overwrites a published version directory; Idunn
|
||||
does not touch this tree (0.3); no pruning job exists.
|
||||
- Deletion line: the rotation and pruning logic is removed when the script is
|
||||
adapted, not left in and disabled.
|
||||
- **Verification:**
|
||||
- operator: `curl -I` the artifact URL returns 200 with the right
|
||||
`Content-Length`; TLS valid; the parent directory does not list;
|
||||
a nonexistent version returns 404.
|
||||
- negative: re-running the deploy script for an existing version exits nonzero
|
||||
and changes nothing.
|
||||
- budget: ~190 MB per release on a ~2 TB root
|
||||
(`gamecult-ops/inventory.md:355-392`). Ten releases is ~1.9 GB. Retention
|
||||
owner is the operator; append-only is the default and nothing prunes.
|
||||
|
||||
### Cut 5. The download page and the changelog pages
|
||||
|
||||
- **Repo/branch:** the chosen site repo (see **F2**), its own branch. Depends on
|
||||
Cut 3's projection and Cut 4's URL.
|
||||
- **Adds:** generated content files under the site's content dir — one downloads
|
||||
page, one page per release — plus the feed JSON under `static/`.
|
||||
- **Mechanism.** Do not write a Quartz emitter plugin. The established precedent
|
||||
in these repos is pre-build generation into committed files:
|
||||
`gamecult-site/scripts/generate-vn-repo-doc-tree.mjs` and
|
||||
`generate-vn-eve-surface.mjs` write JSON into `GameCult/static/...`, invoked by
|
||||
the local launcher (`scripts/quartz/quartz.ps1:38-52`) and **not** by CI, so the
|
||||
outputs are committed artifacts. Cut 3's `release project` fills that same role.
|
||||
The site's existing workflow then publishes with no change at all.
|
||||
- **Brand.** Follow `gamecult-site/docs/brand-design-language.md` and the config
|
||||
it describes, not invention: Montserrat 100-300 for titles, Ubuntu 300 body,
|
||||
IBM Plex Mono uppercase and letter-spaced for the structural labels a download
|
||||
page is mostly made of (version, date, platform, size, hash). Ground `#07111a`,
|
||||
panels `#16212c`, body `#b7c7d9`, headings `#eef5ff`, accent `#ff8a2a`, links
|
||||
`#59b7ff`. Reuse the existing component classes rather than adding a page-type
|
||||
variant: `.gamecult-repo-facts` / `-fact-label` / `-fact-value`
|
||||
(`gamecult-site/site/quartz/styles/custom.scss:953-1008`) is already a
|
||||
label/value fact table, and `.gamecult-action`
|
||||
(`custom.scss:2246-2360+`) is already the button. Note the brand defines no
|
||||
success/warning/error colour (`brand-design-language.md:135-138`); a download
|
||||
page needs none.
|
||||
- **Authority map:**
|
||||
- Owner: Cut 3's projector owns every byte of these files.
|
||||
- Derived: all of them. A hand edit is overwritten on the next projection and
|
||||
that is the intended behaviour.
|
||||
- Forbidden writers: no human edits the generated pages; changelog prose is
|
||||
authored in the Aetheria repo's `CHANGELOG.md` and flows through Cut 3.
|
||||
- Shared paths: the site's existing Pages workflow is untouched.
|
||||
- **Verification:**
|
||||
- builds: the site's Quartz build succeeds with the generated pages present.
|
||||
- negative: re-running `release project` leaves the working tree clean.
|
||||
- **operator, the real check:** from a machine that has never held the repo —
|
||||
open the download page on the public site, click the link, let it download,
|
||||
verify the sha256 shown on the page against the file, unzip it, run
|
||||
`Aetheria.exe`, start a game, confirm the world is populated. Then confirm
|
||||
the changelog page for that version says what actually changed. This is the
|
||||
only check that proves the whole path; every other verification above is a
|
||||
component test.
|
||||
|
||||
## 3. Operator forks
|
||||
|
||||
**F1. Does Idunn carry the artifact?** *Most blocking — it decides Cut 4.*
|
||||
- **A:** Extend Idunn with a static-artifact deployment unit (optional `service`,
|
||||
a file workload driver, an HTTP root route driver).
|
||||
- **B:** Use the existing `gamecult-ops` static release path — scp, a root-owned
|
||||
append-only release script, an nginx vhost, certbot — the same authority shape
|
||||
that already serves `erycina.org` and `velvet.erycina.org`.
|
||||
- **Recommended: B.** A is a large change in Rust to the one daemon whose whole
|
||||
design premise is that it depends on no managed target and supervises signed,
|
||||
health-reporting processes; it would mean making `service` optional
|
||||
(`Idunn/src/deployment.rs:33`) and inventing an HTTP-root route driver, to make
|
||||
Idunn do the thing it is deliberately not shaped for. B is not a second
|
||||
deployment authority — it is the *existing* file-serving authority on that
|
||||
host. This does mean narrowing the target invariant at
|
||||
`docs/build-delivery-target.md:44` to name service deployment; that edit
|
||||
belongs to whoever lands Cut 4.
|
||||
|
||||
**F2. Which site owns the download page?**
|
||||
- **A:** `gamecult.org` (`gamecult-site`) — what the target's invariant names
|
||||
(`docs/build-delivery-target.md:46`).
|
||||
- **B:** `aetheria.gamecult.org` (`AetheriaLore`) — the game's own live site,
|
||||
same engine, same brand family.
|
||||
- **Recommended: B, with A linking to it.** A download for Aetheria belongs on
|
||||
Aetheria's site, and per-release changelog pages will accumulate; parking
|
||||
dozens of them in the studio site's tree is noise there and natural there. The
|
||||
honest counter: `AetheriaLore` is a *lore vault* (`Aetheria/Lore`, `Fiction`,
|
||||
`Worldbuilding`) and release notes are not lore, so B needs a new top-level
|
||||
section and a masthead entry
|
||||
(`AetheriaLore/site/quartz/components/AetheriaMasthead.tsx:19-39` is a
|
||||
hardcoded route list). This one is close; the target's wording points at A and
|
||||
only the operator can say whether that wording was deliberate.
|
||||
|
||||
**F3. Versioning scheme for game builds.**
|
||||
- **A:** semver, next is `0.2.0`, commit recorded as a separate field.
|
||||
- **B:** CalVer, `2026.09.17`.
|
||||
- **C:** commit-derived, `0.1.0+g6dc1860`.
|
||||
- **Recommended: A.** It reuses the policy work already in flight:
|
||||
`CultLib-semver/scripts/check-changelog-semver.mjs` demands exactly one
|
||||
major/minor/patch step per release (`:39,:62`), refuses a version with no
|
||||
changelog entry (`:79`), and treats `0.y.z` minor as the breaking lane
|
||||
(`:98,:139`) — which is exactly right for a game whose saves will break.
|
||||
Caveat the operator should know: that script is **untracked** on its branch,
|
||||
and the `docs/semver-policy.md` it cites at `:4` does not exist in any CultLib
|
||||
worktree. It is package-only and says nothing about builds, so adopting it here
|
||||
is a decision, not an inheritance. B tells a player nothing about whether their
|
||||
save survives. C puts a commit in the version string where the target already
|
||||
wants the commit as its own field (`docs/build-delivery-target.md:26-27`).
|
||||
|
||||
**F4. Hand-written or commit-derived changelog?**
|
||||
- **A:** Hand-written per release in `CHANGELOG.md`, ingested at publish.
|
||||
- **B:** Derived from commit subjects between tags.
|
||||
- **Recommended: A.** B produces a build log, not release notes — and GameCult
|
||||
already has that experiment running by hand:
|
||||
`gamecult-site/GameCult/Projects/VoidBot.md:53-57` and `Ghostlight.md:56-57`
|
||||
are dated commit-subject lists, and they read like machine output. A also
|
||||
matches what CultLib just adopted for its own packages. The cost is that
|
||||
publishing blocks on someone writing prose, which is the correct thing for it
|
||||
to block on.
|
||||
|
||||
**F5. Agent-run or operator-run publishing?**
|
||||
- **A:** Fully agent-run, including the Yggdrasil push.
|
||||
- **B:** Agent builds, hashes, writes the record and the projection, and opens
|
||||
the PRs; the operator runs the two remote scripts.
|
||||
- **Recommended: B.** `gamecult-ops` already draws this exact line, in a comment
|
||||
worth quoting: "putting a name on the public internet is a separate decision
|
||||
from having bytes on disk"
|
||||
(`scripts/deploy-erycina-site-yggdrasil.sh:9-11`). A published version is
|
||||
permanent by the target's own rule, so the irreversible step is the one a human
|
||||
should take. Everything expensive and mechanical stays automated.
|
||||
|
||||
**F6. Zip or installer?**
|
||||
- **A:** Zip. **B:** An installer (NSIS/Inno/MSI).
|
||||
- **Recommended: A.** Measured at 189.8 MB **(probe)**. No installer toolchain
|
||||
exists anywhere in GameCult, and B drags in code signing, an uninstaller, and a
|
||||
new build dependency for no player benefit while launchers and auto-update are
|
||||
already out of scope (`docs/build-delivery-target.md:55`). Revisit when there
|
||||
is a reason, not before.
|
||||
|
||||
**F7. Reuse `gamecult.mesh.cdn_artifact_manifest` or declare `aetheria.release`?**
|
||||
- **A:** Reuse the CultLib type (`CultLib/src/GameCult.Mesh/CultMeshCdn.cs:130`),
|
||||
which already has version, sha256, size, mime, created-at, and a `Build` kind.
|
||||
- **B:** A new `aetheria.release` type that borrows those field names.
|
||||
- **Recommended: B, citing A.** The CultLib type is CDN-shaped: its `Chunks` list
|
||||
(`:168`) is the point of it, and content-addressed chunking
|
||||
(`CultMeshCdn.cs:96-120`) is machinery this migration does not use and should
|
||||
not fake. A release also needs `Commit`, `BuildHost`, and changelog prose,
|
||||
which do not belong bolted onto a CDN manifest consumed elsewhere. Borrow the
|
||||
field shape, not the type. If a CultMesh CDN path ever carries these builds, A
|
||||
becomes the transport and B stays the record.
|
||||
|
||||
## 4. Subtraction and build budget
|
||||
|
||||
This migration is net additive and cannot honestly be made otherwise: it buys a
|
||||
capability that does not exist today — nobody outside Starfire can play the game.
|
||||
What it must not do is buy it twice.
|
||||
|
||||
| Cut | Removes | Adds | Targets |
|
||||
|---|---|---|---|
|
||||
| 1 | inert guard + stale comment, `AetheriaStores.cs:13-15,26-31`; the shipped Burst debug dir | one Editor file under the existing `Assets/Editor/` | +0 assemblies (no asmdef; joins `Assembly-CSharp-Editor`) |
|
||||
| 2 | — | one document type, one tracked `.cc` | +0 |
|
||||
| 3 | — | verbs inside the existing `tools/AetherDb`; one root `CHANGELOG.md` | **+0 executable targets** |
|
||||
| 4 | the rotation and pruning logic while adapting the erycina script | one nginx conf, one shell script, one runbook | +0 (no compilation in `gamecult-ops`) |
|
||||
| 5 | — | generated markdown/JSON in a site repo | +0 (no workflow change) |
|
||||
|
||||
Explicit budget statements:
|
||||
|
||||
- **No new executable target anywhere.** Cut 3 reuses `tools/AetherDb` precisely
|
||||
so the repo does not gain a second console. If that reuse is rejected, the
|
||||
budget changes and the fork should be raised.
|
||||
- **No new Unity assembly**, no new package dependency, no Addressables
|
||||
dependency — `docs/addressables-cut.md` has not landed and this migration does
|
||||
not wait on it. The build probed in 0.1 is the pre-Addressables `Resources`
|
||||
path, and it works.
|
||||
- **Build host is Starfire, Windows, Unity 6000.3.24f1**
|
||||
(`ProjectSettings/ProjectVersion.txt:1`). The serving host is Debian 13. The
|
||||
artifact is Windows-only and produced only on Windows; nothing about Cut 4
|
||||
proves anything about the player, and nothing about Cut 1 proves anything about
|
||||
the server.
|
||||
- **Disk.** ~190 MB per release on Yggdrasil, append-only, no pruning. The
|
||||
operator owns retention.
|
||||
- **A fresh clone is not required per release.** The checkout is 1.78 GiB of
|
||||
`.git` plus a 2.6 GB `Library/`; the ~9 min build in 0.1 was warm, and a cold
|
||||
`Library/` import is far worse. Publishing builds from the working Starfire
|
||||
checkout is the assumption; if that ever moves to a clean builder, the import
|
||||
cost has to be budgeted then, not assumed away now.
|
||||
|
||||
## 5. Open
|
||||
|
||||
- Every fork in section 3 is unruled.
|
||||
- `docs/build-delivery-target.md:11-14` and `:44` both need correcting once F1
|
||||
and Cut 1 are ruled; the target currently says builds happen by hand into
|
||||
`Build/` (they do not — nothing produces a player) and that Idunn owns this
|
||||
deployment (it structurally cannot).
|
||||
- Follow-up outside this migration: the five stale `A -> 94.130.223.131` DNS rows
|
||||
in `gamecult-ops/inventory.md:746,764,771,777,785`.
|
||||
- Follow-up outside this migration: `CultLib-semver/scripts/check-changelog-semver.mjs`
|
||||
is untracked and cites a `docs/semver-policy.md` that does not exist. F3 leans
|
||||
on that work; someone should land it there.
|
||||
|
|
@ -41,8 +41,11 @@ artifact: Idunn deploys services to Yggdrasil, not files.
|
|||
- **State is CultCache, not JSON** (`F:\Projects\CLAUDE.md`). The release
|
||||
record and the changelog are typed documents. Anything a browser eats is a
|
||||
projection at the xenos boundary, and is derived, never authored.
|
||||
- **Idunn owns deployment to Yggdrasil.** Publishing does not invent a second
|
||||
deployment authority, and does not require Idunn to be up in order to build.
|
||||
- **Idunn owns *service* deployment to Yggdrasil**, and only that. Probed
|
||||
2026-09-17 (`docs/build-delivery-cut.md`): every Idunn unit is a supervised
|
||||
process that must publish signed health, and Idunn pulls rather than pushes, so
|
||||
it cannot carry a file artifact. Publishing uses the host's existing static-file
|
||||
path and does not invent a second *service* deployment authority.
|
||||
- **The site's brand is the site's** (`F:\Projects\gamecult-site`), and the
|
||||
download and changelog pages follow it.
|
||||
- **The build host is Windows.** Unity 6000.3.24f1 builds on Starfire; the
|
||||
|
|
|
|||
Loading…
Reference in a new issue