refactor: separate website repo + deploy-host model

Migrates from in-repo orphan `website` branch + LFS to a two-repo +
deploy-host model so source editing is fully decoupled from live state.

  - Source code stays here (codeberg.org/VARASYS/ZDDC).
  - Hand-edited website content moves to a separate Codeberg repo
    (codeberg.org/VARASYS/ZDDC-website, cloned at ~/src/zddc-website/).
  - Live site is /srv/zddc/ on the deploy host (Caddy bind-mount),
    populated by ./deploy from this repo's dist/release-output/ plus
    ~/src/zddc-website/.
  - Releases are no longer in any git history — reproducible from
    <tool>-vX.Y.Z tags via `./build release X.Y.Z`. No LFS, no
    Codeberg release assets.

Build/deploy split:
  - ./build (no arg) is source-only; nothing in dist/release-output/
    or /srv/zddc/ is touched.
  - ./build alpha|beta|release seeds dist/release-output/ from
    /srv/zddc/releases/ (preserving symlinks), then mutates the
    channel(s) being cut on top. The bundle is always a complete
    intended-live snapshot, so the verifier sees a complete world
    and ./deploy --releases (rsync --delete-after) replaces live
    state cleanly.
  - New ./deploy wraps the rsync flow with --content / --releases
    subcommands.

Docs updated to reflect the new model: CLAUDE.md, AGENTS.md,
ARCHITECTURE.md, zddc/README.md, README.md, .gitignore, shared/
build-lib.sh comments, deprecated zddc/release.sh message.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-05-02 09:14:40 -05:00
parent 76e1e78c55
commit 7570fb7494
18 changed files with 349 additions and 187 deletions

26
.gitignore vendored
View file

@ -16,22 +16,20 @@ node_modules/
playwright-report/
test-results/
# Build artifacts
# NOTE: dist/ is listed here but each tool's dist/*.html is force-tracked in git
# (added with `git add -f tool/dist/tool.html`). This is intentional — built artifacts
# are committed alongside source so users can download them directly from the repo.
# New tool dist files must be force-added: git add -f tool/dist/tool.html
# Build artifacts. dist/ is ignored everywhere: per-tool dist/<tool>.html
# is a transient build output (and the canonical thing tests open via
# file://), and dist/release-output/ is the local-only release bundle
# produced by `./build alpha|beta|release`, then rsync'd to the live
# site by `./deploy`. Nothing in dist/ should be committed.
#
# Hand-edited website content (index.html, reference.html, css/, js/,
# img/) lives in a SEPARATE Codeberg repo at codeberg.org/VARASYS/
# ZDDC-website, typically cloned at ~/src/zddc-website/. Release
# artifacts are NOT in git history at all — they're produced by this
# repo's build, rsync'd to /srv/zddc/releases/ on the deploy host,
# and reproducible from any <tool>-vX.Y.Z tag.
dist/
# The website (hand-edited index.html / reference.html / css/ / js/ / img/
# plus all release artifacts) lives in the orphan `website` branch of this
# same Codeberg repo. A `git worktree` of that branch is typically checked
# out at ~/src/zddc-website/ and is what the system Caddy serves at
# zddc.varasys.io. The lockstep build pipeline writes release artifacts
# directly to ~/src/zddc-website/releases/ (override with
# $ZDDC_DEPLOY_RELEASES_DIR). zddc-server binaries are LFS-tracked on
# the website branch; HTML tools + symlinks stay in regular git.
# IDE and project files
.opencode/
opencode.json

View file

@ -5,12 +5,12 @@
```bash
# ── ./build subcommands ────────────────────────────────────────────────────
# `./build` (no arg) is a source-side dev build only — assembles tool/dist/
# + cross-compiles zddc-server. The website worktree is left alone.
# Channel + release subcommands deploy artifacts to the website worktree
# under $ZDDC_DEPLOY_RELEASES_DIR (default ~/src/zddc-website/releases).
# + cross-compiles zddc-server. dist/release-output/ and the live site are
# left alone. Channel + release subcommands produce a complete release
# bundle in dist/release-output/ (gitignored). Run `./deploy` to publish.
# Workflow: alpha = active dev → beta = ready for testing → release = ship.
./build # dev build (no website worktree write)
./build # dev build (no release bundle)
./build alpha # cut alpha (cascades nothing)
./build beta # cut beta (cascades alpha → beta)
./build release # cut stable, coordinated next version
@ -18,7 +18,15 @@
./build release 1.2.0 # cut stable at explicit version
./build help
# Single-tool dev build for testing (does NOT touch the website worktree):
# ── ./deploy subcommands ────────────────────────────────────────────────────
# rsync the build output and content repo to /srv/zddc/ (Caddy's bind-mount).
# --delete-after — the live tree exactly mirrors source.
./deploy # full sync (content + releases)
./deploy --content # only ~/src/zddc-website/ → /srv/zddc/
./deploy --releases # only dist/release-output/ → /srv/zddc/releases/
# Single-tool dev build for testing (does NOT touch dist/release-output/):
sh tool/build.sh # archive|transmittal|classifier|mdedit|landing
# Single-tool release (rare; prefer ./build alpha|beta|release so versions
@ -39,13 +47,16 @@ npx playwright test tool # archive | transmittal | classifier | mded
No lint, typecheck, or format commands exist — the project is plain sh + vanilla JS.
Channel/release cuts seed `dist/release-output/` from the current
`/srv/zddc/releases/` (preserving symlinks) before running per-tool
promote, then mutate the channels being cut on top. The bundle is
therefore always a complete intended-live snapshot, not a sparse diff.
The build ends with a **channel-link verifier** that asserts every
`<tool>_{stable,beta,alpha}.html` (and zddc-server's per-platform binary
mirrors + stub pages) resolves. Build fails if any link is dangling.
Output goes to `${ZDDC_DEPLOY_RELEASES_DIR:-$HOME/src/zddc-website/releases}`
— the website branch's worktree, what Caddy serves. **Nothing is
pushed automatically;** review with `git -C ~/src/zddc-website status`,
commit + push the website branch yourself when ready.
mirrors + stub pages) resolves. Build fails if any link is dangling —
because the bundle is complete, dangling-link errors mean a real bug.
**Nothing is pushed automatically.** Run `./deploy` to publish; commit
+ push source changes to `main` separately.
## Architecture
@ -68,19 +79,26 @@ shared/
build-lib.sh POSIX sh helpers (ensure_exists, concat_files, build_timestamp)
sourced by every tool's build.sh via: . "$root_dir/../shared/build-lib.sh"
# Website lives on the `website` orphan branch of this same repo
# (NOT in main's tree). Worktree typically at ~/src/zddc-website/:
# Hand-edited website content lives in a SEPARATE Codeberg repo
# (codeberg.org/VARASYS/ZDDC-website), typically cloned at
# ~/src/zddc-website/. Just content — no releases, no LFS:
# index.html, reference.html, css/, js/, img/ hand-edited content
# README.md, LICENSE repo housekeeping
#
# This repo's ./build produces a release bundle in dist/release-output/
# (gitignored, local-only). ./deploy rsyncs both into /srv/zddc/ on
# the deploy host (Caddy's bind-mount):
# /srv/zddc/
# index.html, reference.html, css/, js/, img/ ← from ~/src/zddc-website
# releases/
# index.html regenerated by `./build`
# <tool>_v<X.Y.Z>.html per-version (committed, immutable)
# <tool>_v<X.Y>.html -> ... symlink chain (regular git symlinks)
# <tool>_v<X.Y.Z>.html per-version (immutable)
# <tool>_v<X.Y>.html -> ... symlink chain
# <tool>_stable.html -> ... channel mirror, follows latest stable
# <tool>_{beta,alpha}.html -> ... channels (cascade to stable when idle)
# zddc-server_v<X.Y.Z>_<platform> per-platform binary (LFS-tracked)
# zddc-server_v<X.Y.Z>_<platform> per-platform binary (raw bytes, no LFS)
# zddc-server_<channel>_<platform> channel binary mirror (symlink)
# zddc-server_<X>.html stub page surfacing 4 platform DLs
# .gitattributes LFS rules for binaries
helm/
zddc-server-prod/ production-shaped Helm chart (compiles from source via init container)
@ -88,9 +106,9 @@ helm/
README.md chart design rationale + quick-start
```
**Critical:** `dist/` files are gitignored. They're the canonical built artifact for testing and the source for `--release` writes into `~/src/zddc-website/releases/`, but they aren't checked in. Never edit them directly.
**Critical:** `dist/` files are gitignored. `tool/dist/<tool>.html` is the canonical built artifact for testing and the source for `--release` writes into `dist/release-output/`. `dist/release-output/` is the local-only release bundle. Neither is in git. Never edit them directly.
**Release artifacts live on the `website` orphan branch** of this same Codeberg repo, not on `main`. A `git worktree` of that branch — typically at `~/src/zddc-website/` — is what the system Caddy bind-mounts and serves at `zddc.varasys.io/`. The build pipeline writes its release outputs to `${ZDDC_DEPLOY_RELEASES_DIR:-$HOME/src/zddc-website/releases}` directly, then the operator commits + pushes that worktree separately from `main`. Per-version HTML and per-version zddc-server binaries are real bytes (binaries are LFS-tracked; HTML stays regular git); partial-version pins (`_v<X.Y>`, `_v<X>`) and channel mirrors (`_stable`, `_beta`, `_alpha`) are symlinks. `shared/build-lib.sh` provides `promote_release` (HTML tools) and `promote_zddc_server` (binaries + matching stub pages); the top-level `./build` calls them in lockstep. No Codeberg release-asset publication anymore; everything serves from `zddc.varasys.io/releases/`.
**Two-repo + deploy-host model.** Source code lives here (`codeberg.org/VARASYS/ZDDC`); hand-edited website content lives in a separate repo (`codeberg.org/VARASYS/ZDDC-website`, typically cloned at `~/src/zddc-website/`). The live site at `zddc.varasys.io` is `/srv/zddc/` on the deploy host (Caddy bind-mount), populated by `./deploy`. Release artifacts are NOT in git — they're produced by `./build alpha|beta|release` into `dist/release-output/` and rsync'd to `/srv/zddc/releases/` by `./deploy --releases`. Per-version files (HTML and zddc-server binaries) are real immutable bytes; partial-version pins (`_v<X.Y>`, `_v<X>`) and channel mirrors (`_stable`, `_beta`, `_alpha`) are symlinks. `shared/build-lib.sh` provides `promote_release` (HTML tools) and `promote_zddc_server` (binaries + matching stub pages); the top-level `./build` seeds from live state, then calls them in lockstep. Older releases are reproducible from any `<tool>-vX.Y.Z` tag in this repo (`git checkout zddc-server-v0.0.8 && ./build release 0.0.8`). No Codeberg release assets, no LFS.
## Shared CSS (`shared/base.css`)
@ -175,14 +193,15 @@ Format: `trackingNumber_revision (status) - title.extension`
- Feature-branch workflow; squash-merge feature branches to `main`
- Conventional commits: `feat(archive): ...`, `fix(transmittal): ...`
- Release tags: `<tool>-v<X.Y.Z>` per tool, all six sharing the same X.Y.Z on a coordinated cut (e.g. `archive-v0.0.8`, `transmittal-v0.0.8`, `classifier-v0.0.8`, `mdedit-v0.0.8`, `landing-v0.0.8`, `zddc-server-v0.0.8`)
- Commit dist files: `git add -f tool/dist/tool.html`
- Commit zddc-server binaries (per-version + symlinks) on the `website` branch's worktree (`~/src/zddc-website/releases/`) — they're LFS-tracked there alongside the HTML tool releases. Push that branch separately from `main`
- `dist/` is gitignored. Build artifacts (per-tool `dist/<tool>.html` and `dist/release-output/`) are NOT committed to this repo. Reproduce them from a tag with `./build release X.Y.Z`
- Hand-edited website content lives in a separate Codeberg repo (`codeberg.org/VARASYS/ZDDC-website`, cloned at `~/src/zddc-website/`). Source-code commits go to `main` here; content commits go to that repo
- Release artifacts live on the deploy host (`/srv/zddc/`), not in any git history. Use `./deploy` to publish
### Releasing — lockstep, channels, layout
**Lockstep convention.** Every release cut bumps all six artifacts (5 HTML tools + zddc-server) to the same version, even if a tool didn't change. Per-tool independent versions are gone. The coordinated next-stable target is `max(latest tag across all six tools) + 1``_coordinated_next_stable` in `shared/build-lib.sh`. Channel cuts (alpha/beta) follow the same lockstep — every tool's channel mirror is overwritten in step. Three channels, ordered: **alpha** (dev iteration) → **beta** (general testing) → **stable** (ship).
**Storage model.** All release artifacts live under the `website` orphan branch of this repo (worktree typically at `~/src/zddc-website/releases/`) and are served from `zddc.varasys.io/releases/`. The `website` branch's history is independent of `main`. No Codeberg release assets, no third-party mirrors.
**Storage model.** All release artifacts live on the deploy host at `/srv/zddc/releases/` (Caddy bind-mount, served as `https://zddc.varasys.io/releases/`). Locally they materialize in this repo's `dist/release-output/` (gitignored) when `./build alpha|beta|release` runs; `./deploy` rsyncs them out. **No git history holds release artifacts** — older versions are reproducible from any `<tool>-vX.Y.Z` tag (`git checkout zddc-server-v0.0.8 && ./build release 0.0.8`). No Codeberg release assets, no LFS, no third-party mirrors.
| Artifact | Type | Layout |
|---|---|---|
@ -194,12 +213,12 @@ Format: `trackingNumber_revision (status) - title.extension`
| `zddc-server_<X>.html` | generated stub page | per-version / per-channel; lists the four platform downloads. This is what the matrix-cell link points at — one stub fans out to four binaries |
| `index.html` | regenerated by `build.sh` | matrix table, one column per tool, one row per release |
**Single point of truth.** `./build release` is the canonical lockstep cut. It forwards each HTML tool's build with the agreed version, then `promote_zddc_server` (in `shared/build-lib.sh`) copies the freshly cross-compiled binaries into `~/src/zddc-website/releases/` with the matching symlink chain, then `write_zddc_server_stubs_all` regenerates every stub page, then `build_releases_index` rewrites the matrix, then `verify_channel_links` asserts nothing dangles.
**Single point of truth.** `./build release` is the canonical lockstep cut. It seeds `dist/release-output/` from `/srv/zddc/releases/` (so cascades and the verifier see a complete world), forwards each HTML tool's build with the agreed version, then `promote_zddc_server` (in `shared/build-lib.sh`) copies the freshly cross-compiled binaries into `dist/release-output/` with the matching symlink chain, then `write_zddc_server_stubs_all` regenerates every stub page, then `build_releases_index` rewrites the index, then `verify_channel_links` asserts nothing dangles. `./deploy --releases` then publishes the bundle.
- **Stable** (`./build release` or `--release X.Y.Z`): Writes per-version HTML for the five HTML tools + per-version binaries for zddc-server (real bytes, immutable). Refreshes 5 symlinks per HTML tool + 5 symlinks per zddc-server platform → the new version. Tags all six: `<tool>-v<X.Y.Z>`. Cascade: stable cut means beta and alpha both reset to stable for every tool. Skips silently if source for an HTML tool hasn't changed since the latest stable tag (the binary always builds).
- **Beta** (`./build beta`): Overwrites `<tool>_beta.html` with dist bytes for each HTML tool, and `zddc-server_beta_<platform>` with each platform's binary. Cascade: `<tool>_alpha.html``<tool>_beta.html` and `zddc-server_alpha_<platform>``zddc-server_beta_<platform>` (symlinks). No tag.
- **Alpha** (`./build`): Overwrites only the alpha mirrors, all six tools. No tag, no other side-effects.
- **Plain dev builds** (no `--release`): produce `tool/dist/<tool>.html` for HTML tools and `zddc/dist/zddc-server-<platform>` binaries; do NOT touch `~/src/zddc-website/releases/`. The matrix index and stub pages still get regenerated from whatever `~/src/zddc-website/releases/` contains, so the build is idempotent for repeated dev runs.
- **Plain dev builds** (`./build` with no arg): produce `tool/dist/<tool>.html` for HTML tools and `zddc/dist/zddc-server-<platform>` binaries; do NOT touch `dist/release-output/` or the live site. Use it to iterate without affecting deployable state.
On-page `{{BUILD_LABEL}}` format (HTML tools only — zddc-server's version comes from the binary itself):
@ -304,7 +323,7 @@ zddc-server ships as a cross-compiled binary, not a container image. There's no
(cd zddc && go run ./cmd/zddc-server)
```
The repo's top-level `./build` cross-compiles the four release binaries (linux/amd64, darwin/amd64, darwin/arm64, windows/amd64) into `zddc/dist/` via a containerized Go toolchain (podman or docker). On `--release` it also promotes those binaries to `~/src/zddc-website/releases/` with the matching symlink chain and stub pages — same lockstep flow as the HTML tools.
The repo's top-level `./build` cross-compiles the four release binaries (linux/amd64, darwin/amd64, darwin/arm64, windows/amd64) into `zddc/dist/` via a containerized Go toolchain (podman or docker). On `./build alpha|beta|release` it also promotes those binaries to `dist/release-output/` with the matching symlink chain and stub pages — same lockstep flow as the HTML tools. `./deploy` rsyncs the bundle to `/srv/zddc/releases/`.
### Run (development)
@ -335,20 +354,21 @@ ZDDC_ROOT=/path/to/your/archive ZDDC_TLS_CERT=none ZDDC_ADDR=:8080 \
### Release tagging
zddc-server has no separate release script anymore. The top-level `./build release [version|alpha|beta]` is the canonical path: it cross-compiles the binaries inside the containerized Go toolchain, copies them into `~/src/zddc-website/releases/` with the lockstep symlink chain (one set of symlinks per platform), regenerates the per-version + per-channel stub pages, refreshes the matrix, and tags `zddc-server-v<X.Y.Z>` alongside the five HTML-tool tags.
zddc-server has no separate release script. The top-level `./build alpha|beta|release [version]` is the canonical path: it cross-compiles the binaries inside the containerized Go toolchain, copies them into `dist/release-output/` with the lockstep symlink chain (one set of symlinks per platform), regenerates the per-version + per-channel stub pages, refreshes the index, and (on stable) tags `zddc-server-v<X.Y.Z>` alongside the five HTML-tool tags.
```sh
./build release # lockstep stable, coordinated next version
./build release 1.2.0 # lockstep stable, explicit version
./build # lockstep alpha cut for everything
./build alpha # lockstep alpha cut for everything
./build beta # lockstep beta cut for everything
./deploy --releases # publish the bundle to /srv/zddc/releases/
```
The script tags every tool but does NOT push — finish with `git push origin main && git push origin --tags`.
The script tags every tool but does NOT push — finish with `git push origin main && git push origin --tags` (and run `./deploy` to put the artifacts on the live site).
**Versioning** — clean semver. Stable cuts emit one `<tool>-vX.Y.Z` tag per tool, all six sharing the same X.Y.Z. No `-alpha.N` / `-beta.N` counter tags — channel URLs are stable URLs by design. Historical per-tool independent tags (`archive-v0.0.2`, `zddc-server-v0.0.7`, etc.) stay as artifacts; the next coordinated cut jumps every tool to the same number.
**Binary distribution** — `website/releases/zddc-server_<X>_<platform>` are real static files served from `zddc.varasys.io/releases/`. No Codeberg release assets, no `$CODEBERG_TOKEN`, no third-party mirror. The matrix-cell link points at `zddc-server_<X>.html`, a generated stub page that surfaces the four platform downloads in one click.
**Binary distribution** — `/srv/zddc/releases/zddc-server_<X>_<platform>` (on the deploy host) are real static files served from `zddc.varasys.io/releases/`. No Codeberg release assets, no `$CODEBERG_TOKEN`, no third-party mirror, no LFS. The matrix-cell link points at `zddc-server_<X>.html`, a generated stub page that surfaces the four platform downloads in one click.
There is no CI for this — solo workflow benefits from one canonical
local path that fails loudly and visibly on the developer's terminal.

View file

@ -33,36 +33,42 @@ tool/
tool.html # Generated output — never edit this manually
```
Website files (what `zddc.varasys.io` serves) live on the **`website` orphan branch** of this same Codeberg repo, separate from `main`. A `git worktree` of that branch — typically at `~/src/zddc-website/` — is what the system Caddy bind-mounts and serves. The build pipeline writes release artifacts directly to that worktree's `releases/` subdir:
Website files (what `zddc.varasys.io` serves) live on a **separate Codeberg repo** (`codeberg.org/VARASYS/ZDDC-website`, typically cloned at `~/src/zddc-website/`) for hand-edited content, plus the **deploy host's `/srv/zddc/`** for the assembled live site. The system Caddy bind-mounts `/srv/zddc/`. `./deploy` rsyncs both into it.
```
~/src/zddc-website/ (git worktree of `website` branch)
~/src/zddc-website/ (clone of codeberg.org/VARASYS/ZDDC-website)
index.html # hand-edited intro page + install snippets (root URL)
reference.html # hand-edited file-naming convention spec
css/, js/, img/ # hand-edited static assets
.gitattributes # LFS rules: zddc-server_*-{amd64,arm64,*.exe}
releases/
index.html # matrix-style download page, regenerated by build.sh
<tool>_v<X.Y.Z>.html # real per-version HTML (committed, immutable)
README.md, LICENSE # repo housekeeping
# NO releases/ — release artifacts are NOT in any git history.
~/src/zddc/dist/release-output/ (gitignored, produced by ./build alpha|beta|release)
index.html # download page, regenerated by build
<tool>_v<X.Y.Z>.html # real per-version HTML (immutable)
<tool>_v<X.Y>.html → ... # symlink: latest patch within X.Y.*
<tool>_v<X>.html → ... # symlink: latest within X.*.*
<tool>_stable.html → ... # symlink: current stable HTML
<tool>_beta.html → ... # symlink to stable (or real bytes when active beta dev)
<tool>_alpha.html → ... # symlink to beta/stable (or real bytes when active alpha dev)
zddc-server_v<X.Y.Z>_<platform> # real per-version cross-compiled binary (LFS-tracked)
zddc-server_v<X.Y.Z>_<platform> # real per-version cross-compiled binary (raw bytes, no LFS)
zddc-server_v<X.Y>_<platform> → ... # symlink chain (mirrors the HTML cascade per platform)
zddc-server_v<X>_<platform> → ...
zddc-server_<channel>_<platform> → ... # channel mirror per platform
zddc-server_<X>.html # generated stub: matrix-cell link → fans out 4 platform downloads
zddc-server_<X>.html # generated stub: cell link → fans out 4 platform downloads
/srv/zddc/ (deploy host; Caddy bind-mount)
index.html, reference.html, css/, js/, img/ ← rsync'd from ~/src/zddc-website/
releases/ ← rsync'd from ~/src/zddc/dist/release-output/
```
`<tool>` ∈ {archive, transmittal, classifier, mdedit, landing}. `<platform>` ∈ {linux-amd64, darwin-amd64, darwin-arm64, windows-amd64.exe}.
Every URL under `/releases/` resolves directly via the symlink chain — no `manifest.json`, no Caddy regex-rewrite, no JavaScript indirection, no third-party mirror. Caddy serves these as plain static files. The Docker-tag pattern: `:1.2.3` is pinned, `:1.2` floats, `:1` floats further, `:stable` floats furthest, and `:beta` / `:alpha` are mutable channel mirrors that overwrite in place.
**zddc-server binaries live in this repo too** — committed under `~/src/zddc-website/releases/`, served from `zddc.varasys.io/releases/`. No Codeberg release assets, no separate distribution channel. The `helm/zddc-server-{prod,dev}/` charts build from source via init container, but operators who want a prebuilt binary just `curl -O https://zddc.varasys.io/releases/zddc-server_stable_linux-amd64`. The single matrix-cell link per release points at `zddc-server_<X>.html`, a small generated stub that surfaces all four platform downloads.
**zddc-server binaries are reproducible from a tag, not in git** — `./build alpha|beta|release` cross-compiles them into `dist/release-output/`, `./deploy` rsyncs them to `/srv/zddc/releases/`, Caddy serves from there. Older versions: `git checkout zddc-server-v0.0.8 && ./build release 0.0.8`. The `helm/zddc-server-{prod,dev}/` charts build from source via init container, but operators who want a prebuilt binary just `curl -O https://zddc.varasys.io/releases/zddc-server_stable_linux-amd64`. The single cell link per release points at `zddc-server_<X>.html`, a small generated stub that surfaces all four platform downloads.
There is no `website/dev/`. To preview a build locally, open `dist/tool.html` directly via the dev server. To publish on `zddc.varasys.io`, cut a release.
To preview a build locally, open `dist/tool.html` directly via the dev server. To publish on `zddc.varasys.io`, cut a release with `./build alpha|beta|release` and then `./deploy`.
Vendor dependencies (bundled third-party libraries) live in `tool/vendor/` if present. The build script is responsible for inlining them into the output.
@ -74,16 +80,16 @@ Each topic has exactly one authoritative home; everything else links to it.
| Topic | Single home | Linked from |
|---|---|---|
| What ZDDC is + tool channel links + dual-mode (local/server) overview + install snippets | `website/index.html` (hand-edited intro for `zddc.varasys.io/`) | repo `README.md`, `zddc/README.md` |
| File-naming convention spec (status codes, modifiers, folder format) | `website/reference.html` | repo `README.md`, in-tool help text |
| Versions + channel builds index of every tool | `~/src/zddc-website/releases/index.html` (regenerated by `build.sh`) | website intro nav, "Browse all versions" link |
| What ZDDC is + tool channel links + dual-mode (local/server) overview + install snippets | `~/src/zddc-website/index.html` (hand-edited intro for `zddc.varasys.io/`, in the `ZDDC-website` repo) | repo `README.md`, `zddc/README.md` |
| File-naming convention spec (status codes, modifiers, folder format) | `~/src/zddc-website/reference.html` | repo `README.md`, in-tool help text |
| Versions + channel builds index of every tool | `dist/release-output/index.html` (regenerated by `./build`; deployed to `/srv/zddc/releases/index.html`) | website intro nav, "Browse all versions" link |
| Customer-deployment install (`zddc-server` binary embeds current-stable tools; `.zddc apps:` cascade overrides; cache at `<root>/_app/`) | `zddc/README.md` "Apps: virtual tool HTMLs" section | website intro, `AGENTS.md` |
| zddc-server operations: env vars, ACL syntax, `.archive` URLs, container vs binary | `zddc/README.md` | `AGENTS.md`, website intro |
| Build / release / channel commands | `AGENTS.md` | repo `README.md` ("see AGENTS.md") |
| Architecture & internal patterns | `ARCHITECTURE.md` (this file) | `AGENTS.md` |
| Per-tool internal design quirks | `<tool>/README.md` | (linked from website intro tool cards) |
`index.html` on the `website` branch (working dir `~/src/zddc-website/index.html`) is **hand-edited static content** (analogous to `reference.html`), not the landing-tool output. The install section points operators at two paths: **local** (download a `.html` file from `/releases/`) and **server** (run `zddc-server`; current-stable builds of all five tools are baked into the binary at compile time via `//go:embed`). The landing tool's released bytes live at `~/src/zddc-website/releases/landing_v<X.Y.Z>.html`; the embedded copy serves at the deployment root by default. The public website at `zddc.varasys.io/` is the same hand-edited `index.html` — its root URL is the introduction page, not the project picker (because there are no projects to pick from a static site).
`index.html` in the `ZDDC-website` repo (working dir `~/src/zddc-website/index.html`) is **hand-edited static content** (analogous to `reference.html`), not the landing-tool output. The install section points operators at two paths: **local** (download a `.html` file from `/releases/`) and **server** (run `zddc-server`; current-stable builds of all five tools are baked into the binary at compile time via `//go:embed`). The landing tool's released bytes live at `/srv/zddc/releases/landing_v<X.Y.Z>.html` (rsync'd from `dist/release-output/`); the embedded copy serves at the deployment root by default. The public website at `zddc.varasys.io/` is the same hand-edited `index.html` — its root URL is the introduction page, not the project picker (because there are no projects to pick from a static site).
When updating documentation, prefer linking over duplicating. If you find yourself rewriting the file-naming convention in a tool's README, link to `reference.html` instead.
@ -99,16 +105,19 @@ Each HTML tool's `build.sh`:
2. Reads JS files in declaration order, concatenates them
3. Processes `template.html` with `awk`, replacing `{{PLACEHOLDER}}` markers with the concatenated content and stripping CDN `<script>`/`<link>` tags
4. Writes the result to `dist/tool.html`
5. If `--release <channel-or-version>` was passed, calls `promote_release` to write into `~/src/zddc-website/releases/` (per-version file + symlink updates for stable; channel mirror overwrite for alpha/beta).
5. If `--release <channel-or-version>` was passed, calls `promote_release` to write into `dist/release-output/` (per-version file + symlink updates for stable; channel mirror overwrite for alpha/beta).
The top-level `build.sh` at the repository root is the canonical lockstep entry point. It:
The top-level `./build` at the repository root is the canonical lockstep entry point. It:
1. Forwards `--release [version|alpha|beta]` to every HTML tool's build, computing a coordinated next-stable target via `_coordinated_next_stable` (max of every tool's latest tag + 1) when no explicit version is given.
2. Cross-compiles zddc-server for the four target platforms inside a containerized Go toolchain (podman/docker).
3. On `--release`, calls `promote_zddc_server` to copy the freshly cross-compiled binaries into `~/src/zddc-website/releases/` with the matching symlink chain (one set per platform) and tag `zddc-server-v<X.Y.Z>` alongside the five HTML-tool tags.
4. Always calls `write_zddc_server_stubs_all` to refresh the per-version + per-channel stub HTML pages from whatever artifacts are in `~/src/zddc-website/releases/`.
5. Regenerates `~/src/zddc-website/releases/index.html` as a matrix table (rows = versions, columns = tools).
6. Calls `verify_channel_links` — fails the build if any channel link is dangling.
1. On a channel/release cut, **seeds `dist/release-output/` from `/srv/zddc/releases/`** (preserving symlinks) so the bundle is a complete intended-live snapshot, not a sparse one-channel diff. Cascades and the verifier downstream see the same world the live site has.
2. Forwards `--release [version|alpha|beta]` to every HTML tool's build, computing a coordinated next-stable target via `_coordinated_next_stable` (max of every tool's latest tag + 1) when no explicit version is given.
3. Cross-compiles zddc-server for the four target platforms inside a containerized Go toolchain (podman/docker).
4. On a channel/release cut, calls `promote_zddc_server` to copy the freshly cross-compiled binaries into `dist/release-output/` with the matching symlink chain (one set per platform) and tag `zddc-server-v<X.Y.Z>` alongside the five HTML-tool tags (stable cuts only).
5. Calls `write_zddc_server_stubs_all` to refresh the per-version + per-channel stub HTML pages from whatever artifacts are in `dist/release-output/`.
6. Regenerates `dist/release-output/index.html` as the action-first download page.
7. Calls `verify_channel_links` — fails the build if any channel link is dangling.
Then `./deploy --releases` rsyncs `dist/release-output/``/srv/zddc/releases/` with `--delete-after`.
### Channels
@ -118,7 +127,7 @@ Three release channels, applied in lockstep across all six tools (5 HTML + zddc-
- **Beta**`./build beta` overwrites `<tool>_beta.html` for each HTML tool and `zddc-server_beta_<platform>` for each platform with fresh bytes. Cascades alpha → beta for both HTML and binaries (one symlink per platform). No tag — channel URLs are stable URLs by design.
- **Alpha**`./build` overwrites only the alpha mirrors, all six tools. No tag, no other side-effects.
A plain `./build` (no `--release`) is a dev build: it produces `dist/<tool>.html` and `zddc/dist/zddc-server-<platform>` binaries; doesn't touch `~/src/zddc-website/releases/`. The matrix index and stub pages still get regenerated from whatever's in `~/src/zddc-website/releases/`, so dev builds remain idempotent and don't break the channel-link verifier.
A plain `./build` (no arg) is a dev build: it produces `dist/<tool>.html` and `zddc/dist/zddc-server-<platform>` binaries; doesn't touch `dist/release-output/` or the live site. The download index, stub pages, and verifier only run when a channel/release is being cut.
The cascade rule (stable cut → beta + alpha mirrors reset to stable; beta cut → alpha resets to beta) means downstream channels are never stale across either HTML or binaries. "No active beta" silently shows current stable; "no active alpha" silently shows current beta or stable. Operators don't need to run a freshen step after each stable release.

View file

@ -16,9 +16,9 @@ If something in this CLAUDE.md conflicts with those, those win — and please up
This is a **monorepo of independent tools**, not one application:
- `archive/`, `transmittal/`, `classifier/`, `mdedit/`, `landing/` — five self-contained HTML tools, each compiled to a single inlined HTML file in its own `dist/`. Naming: the first four output `dist/tool.html`; **`landing/` outputs `dist/index.html`** (it's the project picker served at the root of `zddc-server`).
- `zddc/` — Go HTTP server (separate sub-project; Go 1.24+). Serves `ZDDC_ROOT/index.html` at `GET /` as the landing page; `Accept: application/json` on `/` returns the ACL-filtered project list. Cross-compiled binaries are committed to the `website` orphan branch (LFS-tracked) and served from `zddc.varasys.io/releases/` (no Codeberg release assets); the `helm/` charts in this repo build from source at deploy time.
- `zddc/` — Go HTTP server (separate sub-project; Go 1.24+). Serves `ZDDC_ROOT/index.html` at `GET /` as the landing page; `Accept: application/json` on `/` returns the ACL-filtered project list. Cross-compiled binaries are produced by `./build` and live in `dist/release-output/` (gitignored); `./deploy` rsyncs them to `/srv/zddc/releases/` on the deploy host (Caddy serves them at `https://zddc.varasys.io/releases/`). The `helm/` charts in this repo build from source at deploy time.
- `shared/``base.css` plus shared JS modules (`zddc.js`, `hash.js`, `zddc-filter.js`, `theme.js`, `help.js`) included by every tool's build, and `build-lib.sh` (POSIX sh helpers sourced by every tool's `build.sh` AND by the top-level `build` for lockstep release helpers).
- **`website` orphan branch** (same Codeberg repo) — committed static site: `index.html` (root URL, hand-edited intro), `releases/<tool>_v<X.Y.Z>.html` (immutable per-version archives), `releases/<tool>_v<X.Y>.html` and `_v<X>.html` (symlinks), `releases/<tool>_{stable,beta,alpha}.html` (channel mirrors), `releases/zddc-server_v<X.Y.Z>_<platform>` (per-version cross-compiled binaries; LFS-tracked), `releases/zddc-server_<channel>_<platform>` (binary symlinks following the same cascade), `releases/zddc-server_<X>.html` (per-version / per-channel stub pages that fan out the four platform downloads in one matrix-cell link), `releases/index.html` (matrix table regenerated by `build.sh`). **Working dir:** `~/src/zddc-website/` (a `git worktree` of the `website` branch — `git -C ~/src/zddc worktree add ~/src/zddc-website website`). **Caddy:** the `zddc.varasys.io:8443` vhost bind-mounts `~/src/zddc-website` and serves from there. **Install model:** local use is a download from `/releases/`. Server use is `zddc-server`, which has the current-stable build of all five tools baked in via `//go:embed` (compile-time default). Tools auto-served at folder-name-driven paths: `archive` everywhere, `classifier` in `Incoming`/`Working`/`Staging` subtrees, `mdedit` in `Working` subtrees, `transmittal` in `Staging` subtrees, `landing` only at root. Override via `.zddc apps:` cascade entry (channel/version/URL/path) — fetched once, cached at `<ZDDC_ROOT>/_app/`. Drop a real `.html` file at any path to override.
- **Two-repo + deploy-host model.** Source code lives here (`codeberg.org/VARASYS/ZDDC`). Hand-edited website content lives in a separate repo (`codeberg.org/VARASYS/ZDDC-website`, typically cloned at `~/src/zddc-website/` — just `index.html`, `reference.html`, `css/`, `js/`, `img/`; no releases, no LFS). The live site at `zddc.varasys.io` is served from `/srv/zddc/` on the deploy host: Caddy bind-mounts that path, and it's populated by `./deploy` from this repo's `dist/release-output/` plus `~/src/zddc-website/`. **Releases are NOT in any git history** — they're reproducible from this repo's `<tool>-vX.Y.Z` tags by checking out the tag and running `./build release X.Y.Z`. Per-version files (`<tool>_v<X.Y.Z>.html`) are immutable; partial-version pins (`<tool>_v<X.Y>.html`, `<tool>_v<X>.html`) and channel mirrors (`<tool>_{stable,beta,alpha}.html`) are symlinks; zddc-server has analogous `zddc-server_v<X.Y.Z>_<platform>` per-version binaries plus channel/partial-version symlinks plus `zddc-server_<X>.html` stub pages that fan out the four-platform download in one cell. **Install model:** local use is a download from `/releases/`. Server use is `zddc-server`, which has the current-stable build of all five tools baked in via `//go:embed` (compile-time default). Tools auto-served at folder-name-driven paths: `archive` everywhere, `classifier` in `Incoming`/`Working`/`Staging` subtrees, `mdedit` in `Working` subtrees, `transmittal` in `Staging` subtrees, `landing` only at root. Override via `.zddc apps:` cascade entry (channel/version/URL/path) — fetched once, cached at `<ZDDC_ROOT>/_app/`. Drop a real `.html` file at any path to override.
- `helm/` — example Helm charts for zddc-server (`zddc-server-prod/`, `zddc-server-dev/`). Both compile from source via init container. Operators copy `values.yaml.example` and customize. No secrets in repo.
- `tests/` — Playwright specs (Chromium only, requires File System Access API). `tests/schema.spec.js` validates `transmittal.schema.json` against canonical fixtures via `ajv` (only dev dep besides Playwright)
@ -26,10 +26,13 @@ This is a **monorepo of independent tools**, not one application:
```bash
# Source-side dev build only — assembles tool/dist/ + cross-compiles
# zddc-server. Does NOT write to the website worktree.
# zddc-server. Does NOT touch dist/release-output/ or the live site.
./build
# Channel/release cuts — write into ~/src/zddc-website/releases/
# Channel/release cuts — produce a complete release bundle in
# dist/release-output/ (gitignored). Cuts seed from the live site
# (/srv/zddc/releases/) so the bundle is a complete intended-live
# snapshot, not a sparse diff. Run ./deploy to publish.
./build alpha # cut alpha (cascades nothing)
./build beta # cut beta (cascades alpha → beta)
./build release # cut stable, coordinated next version
@ -37,6 +40,12 @@ This is a **monorepo of independent tools**, not one application:
./build release X.Y.Z # cut stable at explicit version
./build help # usage
# Deploy — atomic-ish rsync of the build output + content repo to
# /srv/zddc/, where Caddy serves it. The build does NOT auto-deploy.
./deploy # full sync: content + releases
./deploy --content # only ~/src/zddc-website/ → /srv/zddc/
./deploy --releases # only dist/release-output/ → /srv/zddc/releases/
sh tool/build.sh # iterate on one HTML tool's dist/
sh tool/build.sh --release [...] # single-tool release (rare; prefer the lockstep ./build)
./freshen-channel <tool> <channel> # rebuild one tool's alpha/beta from its current stable tag
@ -53,13 +62,15 @@ No lint/typecheck/format commands exist for the HTML tools — vanilla JS + POSI
## Things that bite if you forget
- **`dist/` is gitignored.** `tool/dist/<tool>.html` is the canonical built artifact for testing and as the source for `--release` writes. Never hand-edit a `dist/` file.
- **`dist/` is gitignored.** `tool/dist/<tool>.html` is the canonical built artifact for testing and as the source for `--release` writes. `dist/release-output/` is the local-only release bundle written by `./build alpha|beta|release`. Never hand-edit a `dist/` file.
- **Build vs deploy are separate verbs.** `./build` and `./build alpha|beta|release` produce artifacts under `dist/release-output/`. Nothing escapes the source tree until the operator runs `./deploy`, which rsyncs into `/srv/zddc/` (Caddy's bind-mount). This decouples local iteration from live state.
- **Channel/release cuts seed from live state.** Before running per-tool promote, `./build alpha|beta|release` clears `dist/release-output/` and copies `/srv/zddc/releases/` into it (preserving symlinks). The cut then mutates the channels being cut on top. Result: `dist/release-output/` is always a complete intended-live snapshot, the verifier sees a complete world, and `./deploy --releases` (rsync `--delete-after`) replaces live state cleanly.
- **Lockstep releases.** Every release cut bumps all six artifacts (5 HTML tools + zddc-server) to the same version, even if a tool didn't change. The coordinated next-stable target is `max(latest tag across all tools) + 1`. Per-tool independent versions are no longer the norm — `./build release` is the canonical path. Workflow: alpha = active dev, beta = ready for general testing, stable = ready to ship.
- **Release artifacts live on the `website` orphan branch.** The build pipeline writes them to `${ZDDC_DEPLOY_RELEASES_DIR:-$HOME/src/zddc-website/releases}` — a git worktree of that branch, served by Caddy directly. HTML tools: per-version `<tool>_v<X.Y.Z>.html` (real immutable files) + partial-version pins + channel mirrors (symlinks). zddc-server: `zddc-server_v<X.Y.Z>_<platform>` per-version binaries (LFS), `zddc-server_v<X.Y>_<platform>` / `_v<X>_<platform>` / `_<channel>_<platform>` symlinks, plus `zddc-server_<X>.html` stub pages that surface the four platform downloads in one matrix-cell link. Same cascade rule for both: stable cut → beta + alpha both reset to stable; beta cut → alpha cascades to beta.
- **Release artifact layout** (in `dist/release-output/`, mirrored to `/srv/zddc/releases/`). HTML tools: per-version `<tool>_v<X.Y.Z>.html` (real immutable files) + partial-version pins (`<tool>_v<X.Y>.html`, `_v<X>.html`) + channel mirrors (`<tool>_{stable,beta,alpha}.html`) — all symlinks except per-version. zddc-server: `zddc-server_v<X.Y.Z>_<platform>` per-version binaries (raw bytes, no LFS), `_v<X.Y>_<platform>` / `_v<X>_<platform>` / `_<channel>_<platform>` symlinks, plus `zddc-server_<X>.html` stub pages that surface the four platform downloads in one matrix-cell link. Same cascade rule for both: stable cut → beta + alpha both reset to stable; beta cut → alpha cascades to beta.
- **No tags for alpha/beta.** Channel URLs are stable URLs by design — appending counter tags would defeat the purpose. The on-page label encodes `<date> · <sha>` for traceability. Stable cuts get clean `<tool>-vX.Y.Z` tags for every tool (six tags per cut, all sharing the same X.Y.Z).
- **Pre-release semver in the on-page label.** Plain dev builds and `--release alpha|beta` cuts embed `vX.Y.Z-{alpha,beta}` in `{{BUILD_LABEL}}` where X.Y.Z is the next-stable target. Plain dev adds a full timestamp + `-dirty` marker; `--release alpha|beta` is date-only.
- **Channel-link verifier.** Every `./build` ends with a check that every `<tool>_{stable,beta,alpha}.html` (and zddc-server's per-platform binary mirrors + stub pages) resolves. Bootstrap-friendly: skips zddc-server checks until the first `--release` cut materializes the binaries.
- **`./build` (no arg) is a source-side dev build.** Assembles `tool/dist/` + cross-compiled binaries; does NOT write to the website worktree. Use it to iterate without affecting the live site. To deploy, run `./build alpha|beta|release` — those promote to `${ZDDC_DEPLOY_RELEASES_DIR:-~/src/zddc-website/releases}`. Nothing is pushed to Codeberg automatically; commit + push the website branch when you want to publish.
- **Channel-link verifier.** Every `./build alpha|beta|release` ends with a check that every `<tool>_{stable,beta,alpha}.html` (and zddc-server's per-platform binary mirrors + stub pages) resolves. Because cuts seed from live state, the verifier always sees a complete world; missing-link errors mean a real problem, not a sparse-bundle artifact.
- **`./build` (no arg) is a source-side dev build.** Assembles `tool/dist/` + cross-compiled binaries; does NOT touch `dist/release-output/` or the live site. Use it to iterate without affecting anything. To produce a deployable bundle, run `./build alpha|beta|release`. To publish, run `./deploy`. Nothing is pushed to Codeberg automatically.
- **Always build before running tests** — Playwright opens `dist/tool.html` via `file://`.
- **`</` in JS string/template literals breaks inline `<script>`** embedding. `shared/build-lib.sh` provides `escape_js_close_tags`; every tool's `build.sh` runs JS through it before inlining.
- **All ZDDC parsing/formatting/hashing goes through `window.zddc`** (from `shared/zddc.js` + `shared/hash.js` + `shared/zddc-filter.js`). API: `parseFilename`, `parseFolder`, `parseRevision`, `formatFilename`, `formatFolder`, `compareRevisions`, `isValidStatus`, `splitExtension`, `joinExtension`, `crypto.{sha256Hex, sha256String, sha256File, bytesToHex}`, `filter.{parse, matches}`. File objects across tools use `trackingNumber` (string) and `extension` (string, **no leading dot** — use `zddc.joinExtension(name, ext)` to build a filename). Add edge cases to `tests/zddc.spec.js`, not per-tool tests.

View file

@ -17,7 +17,7 @@ The name "Zero Day Document Control" comes from the convention itself — adopt
| **Document Classifier** | Spreadsheet-like bulk-renamer that copy/pastes with Excel and writes back to disk. |
| **Markdown Editor** | Browser-based markdown editor with YAML front matter, TOC, and direct local file access. |
Each tool is published in three channels (stable, beta, alpha) as static files committed under `website/releases/`, browsable at <https://zddc.varasys.io/releases/>. **Local use:** download a `.html` file from `releases/` and open it in a browser. **Server use:** run `zddc-server` — the current-stable build of every tool is baked into the binary at compile time, so a fresh deployment Just Works with zero config. Tools auto-appear at folder-name-driven paths (archive everywhere; classifier in `Incoming`/`Working`/`Staging`; mdedit in `Working`; transmittal in `Staging`). Override per-directory by writing an `apps:` entry in any `.zddc` file (channel/version/URL/path). URL overrides are fetched once and cached in `<ZDDC_ROOT>/_app/`; drop a real `.html` file at any path to override entirely.
Each tool is published in three channels (stable, beta, alpha) as static files served from <https://zddc.varasys.io/releases/>. **Local use:** download a `.html` file from `releases/` and open it in a browser. **Server use:** run `zddc-server` — the current-stable build of every tool is baked into the binary at compile time, so a fresh deployment Just Works with zero config. Tools auto-appear at folder-name-driven paths (archive everywhere; classifier in `Incoming`/`Working`/`Staging`; mdedit in `Working`; transmittal in `Staging`). Override per-directory by writing an `apps:` entry in any `.zddc` file (channel/version/URL/path). URL overrides are fetched once and cached in `<ZDDC_ROOT>/_app/`; drop a real `.html` file at any path to override entirely.
## File-naming convention

79
build
View file

@ -4,23 +4,25 @@ set -eu
# build — ZDDC source build + lockstep release driver.
#
# ./build dev build: assemble tool dist/, cross-compile
# zddc-server binaries. NO write to the website
# worktree; nothing gets deployed.
# ./build alpha dev build + copy alpha mirrors to the website
# worktree (cascades nothing).
# ./build beta dev build + copy beta mirrors (cascades alpha → beta).
# ./build release dev build + cut coordinated stable
# (cascades alpha + beta → new stable; tags all six tools).
# zddc-server binaries. Nothing else is touched
# — no release artifacts produced, no deploy.
# ./build alpha cut alpha: produce a complete release bundle
# in dist/release-output/ (cascades nothing).
# ./build beta cut beta (cascades alpha → beta).
# ./build release cut coordinated stable (cascades alpha + beta
# → new stable; tags all six tools).
# ./build release X.Y.Z same, explicit version.
# ./build help this message.
#
# Lockstep: every channel/release cut bumps all six tools (5 HTML +
# zddc-server) together. Coordinated next-stable = max(latest tag) + 1.
#
# Channel/release cuts write to the website worktree at
# ${ZDDC_DEPLOY_RELEASES_DIR:-$HOME/src/zddc-website/releases}. Caddy serves
# that path live; nothing is pushed to Codeberg automatically. To publish:
# cd ~/src/zddc-website && git add -A && git commit && git push origin website
# Channel/release cuts write a complete intended-live snapshot to
# ${ZDDC_DEPLOY_RELEASES_DIR:-$SCRIPT_DIR/dist/release-output}. The build
# does NOT touch the live site — run `./deploy` (or `./deploy --releases`)
# to rsync the snapshot into /srv/zddc/. The snapshot is built by seeding
# from the current live state (so cascades and the verifier see a
# complete world), then mutating the channel(s) being cut on top.
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
@ -80,16 +82,44 @@ else
TOOL_RELEASE_ARGS="--release $RELEASE_CHANNEL"
fi
# Deploy directory for release artifacts. The website lives in the
# orphan `website` branch served by Caddy from a fixed path; this dir
# is the worktree of that branch (default ~/src/zddc-website/releases).
# Override with $ZDDC_DEPLOY_RELEASES_DIR for testing or alternate
# deploy targets. Exported so child per-tool build.sh invocations see
# the same path.
export ZDDC_DEPLOY_RELEASES_DIR="${ZDDC_DEPLOY_RELEASES_DIR:-$HOME/src/zddc-website/releases}"
# Local-only build output. The release pipeline writes here; nothing
# escapes the source tree until the operator runs `./deploy`. Default
# is $SCRIPT_DIR/dist/release-output; override with
# $ZDDC_DEPLOY_RELEASES_DIR. Exported so child per-tool build.sh
# invocations see the same path.
export ZDDC_DEPLOY_RELEASES_DIR="${ZDDC_DEPLOY_RELEASES_DIR:-$SCRIPT_DIR/dist/release-output}"
RELEASES_DIR="$ZDDC_DEPLOY_RELEASES_DIR"
mkdir -p "$RELEASES_DIR"
# When cutting a channel/release, seed RELEASES_DIR from the current live
# site so the resulting bundle is a complete intended-live snapshot, not
# a sparse one-channel diff. Two reasons:
# 1. Per-tool promote_release does cascade writes (beta cut → also
# rewrites alpha to track beta; stable cut → resets alpha + beta).
# The cascade itself is deterministic, but downstream artifacts that
# were NOT touched by this cut (e.g. older versioned files, the
# other channel mirrors, partial-version symlinks) still need to be
# present in the bundle so `./deploy --releases` (rsync
# --delete-after) doesn't wipe them off the live site.
# 2. verify_channel_links cross-checks the full release tree; it
# flags absent channels as missing. With seeding, a fresh
# `dist/release-output/` matches live state, the cut mutates on
# top, and the verifier sees a complete world.
# Bootstrap case (no live site yet, or live releases dir empty) is
# silently skipped — the very first stable cut populates everything.
if [ -n "$RELEASE_CHANNEL" ]; then
LIVE_RELEASES="${ZDDC_LIVE_DIR:-/srv/zddc}/releases"
if [ -d "$LIVE_RELEASES" ] && [ -n "$(ls -A "$LIVE_RELEASES" 2>/dev/null)" ]; then
echo "=== Seeding $RELEASES_DIR from $LIVE_RELEASES ==="
rm -rf "$RELEASES_DIR"
mkdir -p "$RELEASES_DIR"
# cp -a preserves the symlink graph (channel mirrors +
# _v<X.Y> / _v<X> partial-version pins) so cascade decisions
# downstream see the same world the live site has.
cp -a "$LIVE_RELEASES/." "$RELEASES_DIR/"
fi
fi
echo "=== Building ZDDC tools ==="
# Each tool's compute_build_label writes a sidecar `<tool>.label` here so
@ -650,11 +680,11 @@ echo ""
echo "=== Build done ==="
echo ""
if [ -z "$RELEASE_CHANNEL" ]; then
echo "Mode: dev (source-only build, website worktree untouched)"
echo "Mode: dev (source-only build; live site untouched)"
echo " tool/dist/*.html ready"
echo " zddc/dist/zddc-server-* binaries ready"
echo ""
echo "To deploy alpha mirrors to the website: ./build alpha"
echo "To cut alpha into a deployable bundle: ./build alpha"
else
echo "Cut: $RELEASE_CHANNEL"
if [ -n "$RELEASE_VERSION" ]; then
@ -667,8 +697,9 @@ else
echo " git push origin main && git push origin --tags"
fi
echo ""
echo "Artifacts written to $RELEASES_DIR/"
echo " cd $(dirname "$RELEASES_DIR") && git status # to review the deploy"
echo " cd $(dirname "$RELEASES_DIR") && git add -A && git commit && git push origin website"
echo " ↑ commits + pushes the website branch when you're ready to publish"
echo "Snapshot ready at $RELEASES_DIR/"
echo ""
echo "To publish to the live site:"
echo " ./deploy --releases # rsync the snapshot to /srv/zddc/releases/"
echo " ./deploy # full sync (content + releases)"
fi

91
deploy Executable file
View file

@ -0,0 +1,91 @@
#!/bin/sh
set -eu
# deploy — sync built artifacts and/or hand-edited content to the live site.
#
# The build pipeline (`./build alpha|beta|release`) produces self-contained
# bundles in dist/release-output/ but does NOT touch the live site. This
# script is the explicit deploy step. Two sync paths, independent:
#
# ./deploy push everything: content + releases
# ./deploy --content push only ~/src/zddc-website/ → /srv/zddc/
# (excludes /releases/ so releases stay intact)
# ./deploy --releases push only dist/release-output/ → /srv/zddc/releases/
#
# Both paths use rsync with --delete-after, so the live tree exactly
# mirrors the source — files removed locally go away on the live site.
# Mostly-atomic per-file; brief mixed-state during a sync is acceptable
# for a low-traffic static site. Caddy bind-mounts /srv/zddc as :ro and
# serves whatever is there at request time.
#
# Override the source paths via env if you want:
# ZDDC_CONTENT_DIR default: ~/src/zddc-website
# ZDDC_DEPLOY_RELEASES_DIR default: <this-script-dir>/dist/release-output
# ZDDC_LIVE_DIR default: /srv/zddc
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
CONTENT_SRC="${ZDDC_CONTENT_DIR:-$HOME/src/zddc-website}"
RELEASES_SRC="${ZDDC_DEPLOY_RELEASES_DIR:-$SCRIPT_DIR/dist/release-output}"
LIVE="${ZDDC_LIVE_DIR:-/srv/zddc}"
case "${1:-all}" in
-h|--help|help)
sed -n '4,21p' "$0" | sed 's/^# \{0,1\}//'
exit 0
;;
--content|content)
WHAT=content
;;
--releases|releases)
WHAT=releases
;;
all|"")
WHAT=all
;;
*)
echo "deploy: unknown subcommand '$1'. Try './deploy help'." >&2
exit 1
;;
esac
if [ ! -d "$LIVE" ]; then
echo "deploy: $LIVE does not exist. Create it and chown to your user first:" >&2
echo " sudo mkdir -p $LIVE && sudo chown -R \$USER:\$USER $LIVE" >&2
exit 1
fi
if [ "$WHAT" = content ] || [ "$WHAT" = all ]; then
if [ ! -d "$CONTENT_SRC" ]; then
echo "deploy: content source $CONTENT_SRC does not exist" >&2
exit 1
fi
echo "=== Syncing content: $CONTENT_SRC/ → $LIVE/ ==="
# --exclude=/releases/ keeps the live site's releases dir untouched
# by content syncs. --exclude=.git so the .git dir doesn't end up
# under /usr/share/caddy.
rsync -av --delete-after \
--exclude='/releases/' \
--exclude='/.git*' \
--exclude='/README.md' \
--exclude='/LICENSE' \
"$CONTENT_SRC/" "$LIVE/"
fi
if [ "$WHAT" = releases ] || [ "$WHAT" = all ]; then
if [ ! -d "$RELEASES_SRC" ] || [ -z "$(ls -A "$RELEASES_SRC" 2>/dev/null)" ]; then
echo "deploy: releases source $RELEASES_SRC is empty or missing" >&2
echo " Run ./build alpha|beta|release first to populate it." >&2
if [ "$WHAT" = all ]; then
echo " (Skipping releases sync; content was synced.)" >&2
exit 0
fi
exit 1
fi
mkdir -p "$LIVE/releases"
echo "=== Syncing releases: $RELEASES_SRC/ → $LIVE/releases/ ==="
rsync -av --delete-after "$RELEASES_SRC/" "$LIVE/releases/"
fi
echo ""
echo "=== Deploy done ==="
echo "Live: https://zddc.varasys.io/"

View file

@ -80,11 +80,10 @@ echo "Freshening ${TOOL} ${CHANNEL} from ${LATEST_TAG}"
git -C "$REPO" worktree add --quiet --detach "$WT" "$LATEST_TAG"
# Build in the worktree. The tool's build.sh resolves its release dir
# from $ZDDC_DEPLOY_RELEASES_DIR (default ~/src/zddc-website/releases),
# writing the channel artifact directly there. Pass through whatever the
# parent process has set so freshen-channel honors the same target as
# the regular build.
DEPLOY_DIR="${ZDDC_DEPLOY_RELEASES_DIR:-$HOME/src/zddc-website/releases}"
# from $ZDDC_DEPLOY_RELEASES_DIR (default $REPO/dist/release-output);
# pass through whatever the parent process has set so freshen-channel
# honors the same target as the regular build.
DEPLOY_DIR="${ZDDC_DEPLOY_RELEASES_DIR:-$REPO/dist/release-output}"
mkdir -p "$DEPLOY_DIR"
ZDDC_DEPLOY_RELEASES_DIR="$DEPLOY_DIR" \
sh "$WT/${TOOL}/build.sh" --release "$CHANNEL"
@ -97,5 +96,4 @@ fi
echo "Wrote $DST"
echo "Done. ${CHANNEL} channel for ${TOOL} now reflects ${LATEST_TAG}."
echo "Commit the change in the website worktree:"
echo " cd $(dirname "$DEPLOY_DIR") && git add $(basename "$DEPLOY_DIR")/$(basename "$DST") && git commit"
echo "Run ./deploy --releases to push it to the live site."

View file

@ -1774,7 +1774,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Markdown</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 · 6167e99</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 14:02:00 · 76e1e78-dirty</span></span>
</div>
<button id="select-directory" class="btn btn-primary" title="Select a Directory">Select Directory</button>
</div>

View file

@ -22,22 +22,28 @@
# is_release, is_red, channel.
# See "Channels and release args" below.
# promote_release <tool> — for stable / alpha / beta, copy the dist
# HTML into website/releases/. Stable cuts
# write the immutable per-version file +
# HTML into the release-output bundle
# (default $root_dir/../dist/release-output;
# override $ZDDC_DEPLOY_RELEASES_DIR). Stable
# cuts write the immutable per-version file +
# refresh five symlinks (_v<X.Y>, _v<X>,
# _stable, _beta, _alpha) and tag
# <tool>-v<X.Y.Z>. Alpha/beta cuts
# overwrite the channel mirror in place
# and cascade alpha → beta. No git tags
# for alpha/beta cuts; no Codeberg upload
# for HTML tools. See ARCHITECTURE.md
# for alpha/beta cuts. The bundle is a
# complete intended-live snapshot — the
# top-level ./build seeds it from
# /srv/zddc/releases/ before per-tool
# promote runs, then ./deploy --releases
# rsyncs it back. See ARCHITECTURE.md
# "Channels" for the full table.
#
# Channels and release args:
# <none> dev build, dist/ only, label
# <none> dev build, tool/dist/ only, label
# "v<next-stable>-alpha · <ts> · <sha>[-dirty]" (red).
# No website/releases/ side-effect. To publish, re-run
# with `--release alpha`.
# No release-output side-effect. To produce a deployable
# bundle, re-run with `--release alpha`.
# --release stable, auto-bump patch from latest tag (or 0.0.1).
# Writes per-version file + symlinks; tags vX.Y.Z.
# --release X.Y.Z stable, explicit version.
@ -56,9 +62,11 @@ if [ -z "${root_dir:-}" ]; then
exit 1
fi
# NOTE: shared/publish-codeberg-release.sh is no longer sourced here.
# HTML tools publish to website/releases/ as committed static files; only
# zddc-server/release.sh uploads to Codeberg (it sources the helper directly).
# NOTE: there's no Codeberg release-asset publication path anymore. All
# release artifacts (HTML tools + zddc-server binaries) materialize in
# dist/release-output/ via the lockstep ./build, then ./deploy rsyncs
# them to /srv/zddc/ on the deploy host. The deprecated zddc/release.sh
# is now a no-op guard that prints a redirection message.
# Fail hard on any missing source file
ensure_exists() {
@ -261,20 +269,21 @@ _coordinated_next_stable() {
echo "${_major}.${_minor}.$((_patch + 1))"
}
# Promote a built dist file to website/releases/. Reads from caller scope:
# $channel ("stable" / "alpha" / "beta"), $build_version (stable only),
# $output_html, $root_dir.
# Promote a built dist file to the release-output bundle. Reads from caller
# scope: $channel ("stable" / "alpha" / "beta"), $build_version (stable only),
# $output_html, $root_dir. Bundle path resolves from $ZDDC_DEPLOY_RELEASES_DIR
# (default $root_dir/../dist/release-output).
#
# Stable cuts:
# 1. Skip if source unchanged since latest stable tag.
# 2. Copy dist HTML → website/releases/<tool>_v<X.Y.Z>.html (immutable).
# 2. Copy dist HTML → <bundle>/<tool>_v<X.Y.Z>.html (immutable).
# 3. Refresh symlinks: _v<X.Y>, _v<X>, _stable, _beta, _alpha all → the
# new versioned file. Cascade rule: stable cut means beta and alpha
# reset to stable (no active dev on either downstream channel).
# 4. Tag the commit <tool>-v<X.Y.Z>.
#
# Alpha/beta cuts:
# 1. Overwrite website/releases/<tool>_<channel>.html with dist HTML
# 1. Overwrite <bundle>/<tool>_<channel>.html with dist HTML
# (replaces a symlink with real bytes if one was there).
# 2. For beta: cascade <tool>_alpha.html → <tool>_beta.html (symlink),
# since alpha defaults to beta when no active alpha.
@ -287,11 +296,12 @@ _coordinated_next_stable() {
# handles binary uploads to Codeberg directly (different distribution model).
promote_release() {
_tool="$1"
# Honor $ZDDC_DEPLOY_RELEASES_DIR (set by the top-level build.sh and
# documentable for one-off CI / test invocations). Fall back to the
# legacy in-repo path so single-tool standalone invocations from a
# checkout that still has website/ on disk continue to work.
_releases_dir="${ZDDC_DEPLOY_RELEASES_DIR:-$root_dir/../website/releases}"
# The top-level `./build` exports $ZDDC_DEPLOY_RELEASES_DIR pointing
# at $SCRIPT_DIR/dist/release-output. Single-tool standalone
# invocations fall back to the same default — no inheritance from a
# parent build run.
_releases_dir="${ZDDC_DEPLOY_RELEASES_DIR:-$root_dir/../dist/release-output}"
mkdir -p "$_releases_dir"
if [ ! -d "$_releases_dir" ]; then
echo "promote_release: $_releases_dir not found" >&2
@ -475,12 +485,10 @@ TAIL
} > "$_out"
}
# Refresh every zddc-server stub page based on what's currently in
# website/releases/. Driven by the existing per-version binary files +
# Refresh every zddc-server stub page based on what's currently in the
# release-output bundle. Driven by the existing per-version binary files +
# symlinks that the release flow already maintains; just emits the HTML
# wrappers for them. Safe to run on every build (idempotent), so plain
# `sh build.sh` keeps the stub pages in sync if a release file was added
# out of band.
# wrappers for them. Safe to run on every cut (idempotent).
#
# $1 — releases dir (absolute)
write_zddc_server_stubs_all() {
@ -534,8 +542,8 @@ write_zddc_server_stubs_all() {
fi
}
# Promote a freshly-cross-compiled set of zddc-server binaries to
# website/releases/. Called by the top-level build.sh on a release cut.
# Promote a freshly-cross-compiled set of zddc-server binaries to the
# release-output bundle. Called by the top-level ./build on a release cut.
#
# $1 — channel ("stable" | "alpha" | "beta")
# $2 — version (X.Y.Z; required for stable; ignored for alpha/beta but

View file

@ -500,32 +500,28 @@ To run unit tests:
## Release tagging
`sh zddc/release.sh` is the canonical path. **Stable cuts only.** The script tags the commit, cross-compiles the four binaries (native Go), and uploads them as Codeberg release assets via the shared `publish-codeberg-release.sh` helper.
zddc-server has no separate release script. The repo's top-level `./build alpha|beta|release [version]` is the canonical path: it cross-compiles the four binaries inside the containerized Go toolchain, copies them into `dist/release-output/` with the lockstep symlink chain (one set per platform), regenerates the per-version + per-channel stub pages, refreshes the index, and (on stable cuts) tags `zddc-server-v<X.Y.Z>` alongside the five HTML-tool tags.
```sh
sh zddc/release.sh # patch-bump from latest clean stable tag
sh zddc/release.sh 0.1.0 # explicit version
./build release # lockstep stable, coordinated next version
./build release 1.2.0 # lockstep stable, explicit version
./build alpha # lockstep alpha cut
./build beta # lockstep beta cut
./deploy --releases # publish dist/release-output/ to /srv/zddc/releases/
```
The script tags the commit but does NOT push — finish with `git push origin main` and `git push origin <tag>`.
The script tags every tool but does NOT push — finish with `git push origin main && git push origin --tags` (and run `./deploy` to put the artifacts on the live site).
Prerequisites:
- Go 1.24+ on PATH.
- `$CODEBERG_TOKEN` exported, scoped to write the VARASYS/ZDDC repo. Generate one at <https://codeberg.org/user/settings/applications>.
After the script returns successfully, the website's versions index doesn't need updating for zddc-server (it links out to the Codeberg release page directly). Just push:
```sh
git push origin main
git push origin zddc-server-vX.Y.Z
```
- Go 1.24+ available inside the build container (downloaded automatically — `docker.io/golang:1.24-alpine`).
- `podman` (preferred) or `docker` on PATH.
Single-developer / solo-release flow by design — no CI babysitting, no separate dashboard to debug. The script fails loudly and visibly on the developer's terminal if anything goes wrong.
### Versioning
Clean semver. Stable cuts get `<tool>-vX.Y.Z` tags. There are no alpha/beta channel tags for zddc-server — channel URLs are stable URLs by design (counters defeat that), and zddc-server has no static-asset distribution layer where channel mirrors would matter. Active dev runs via `helm/zddc-server-dev/`, which builds from source on each rollout.
Clean semver, lockstep across all six tools (5 HTML + zddc-server). Stable cuts get `<tool>-vX.Y.Z` tags for every tool, all six sharing the same X.Y.Z. There are no alpha/beta tags — channel URLs are stable URLs by design (counters defeat that). Active dev runs via `helm/zddc-server-dev/`, which builds from source on each rollout.
The two existing `zddc-server-v0.0.8-alpha.1` and `zddc-server-v0.0.8-alpha.2` tags from a previous experiment stay as historical artifacts; no new alpha/beta tags are created going forward.

View file

@ -2113,7 +2113,7 @@ td[data-field="trackingNumber"] {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 · 6167e99</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 14:02:00 · 76e1e78-dirty</span></span>
</div>
<button id="addDirectoryBtn" class="btn btn-primary">Add Local Directory</button>
<button id="refreshHeaderBtn" class="btn btn-secondary hidden" title="Refresh Data" style="font-size:1.1rem;"></button>

View file

@ -1376,7 +1376,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Classifier</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 · 6167e99</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 14:02:00 · 76e1e78-dirty</span></span>
</div>
<button id="selectDirectoryBtn" class="btn btn-primary">Select Directory</button>
<button id="refreshBtn" class="btn btn-secondary hidden" title="Refresh and rescan directory">Refresh</button>

View file

@ -866,7 +866,7 @@ body {
</g>
</svg>
<span class="app-header__title">ZDDC Archive</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 · 6167e99</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 14:02:00 · 76e1e78-dirty</span></span>
</div>
<div class="header-right">
<button id="theme-btn" class="btn btn-secondary" title="Theme: auto (follows OS)" aria-label="Theme: auto (follows OS)"></button>

View file

@ -1774,7 +1774,7 @@ body.help-open .app-header {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Markdown</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 · 6167e99</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 14:02:00 · 76e1e78-dirty</span></span>
</div>
<button id="select-directory" class="btn btn-primary" title="Select a Directory">Select Directory</button>
</div>

View file

@ -2210,7 +2210,7 @@ dialog.modal--narrow {
</svg>
<div class="header-title-group">
<span class="app-header__title">ZDDC Transmittal</span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 · 6167e99</span></span>
<span class="build-timestamp"><span style="color:red;font-weight:bold">v0.0.9-alpha · 2026-05-02 14:02:00 · 76e1e78-dirty</span></span>
</div>
<div class="app-header__spacer"></div>
<div class="app-header__icons">

View file

@ -1,6 +1,6 @@
# Generated by build.sh — do not edit. One <app>=<build label> per line.
archive=v0.0.9-alpha · 2026-05-02 · 6167e99
transmittal=v0.0.9-alpha · 2026-05-02 · 6167e99
classifier=v0.0.9-alpha · 2026-05-02 · 6167e99
mdedit=v0.0.9-alpha · 2026-05-02 · 6167e99
landing=v0.0.9-alpha · 2026-05-02 · 6167e99
archive=v0.0.9-alpha · 2026-05-02 14:02:00 · 76e1e78-dirty
transmittal=v0.0.9-alpha · 2026-05-02 14:02:00 · 76e1e78-dirty
classifier=v0.0.9-alpha · 2026-05-02 14:02:00 · 76e1e78-dirty
mdedit=v0.0.9-alpha · 2026-05-02 14:02:00 · 76e1e78-dirty
landing=v0.0.9-alpha · 2026-05-02 14:02:00 · 76e1e78-dirty

View file

@ -3,12 +3,11 @@
# do nothing.
#
# zddc-server is no longer released independently. The top-level
# `sh build.sh --release [version|alpha|beta]` is the canonical lockstep
# cut: it bumps every tool (5 HTML + zddc-server) to the same version,
# cross-compiles the binaries, copies them into website/releases/ alongside
# the HTML tool artifacts, regenerates the matrix index, and tags every
# tool. No more Codeberg release-asset uploads — everything serves from
# zddc.varasys.io/releases/.
# `./build alpha|beta|release [version]` is the canonical lockstep cut:
# it bumps every tool (5 HTML + zddc-server) to the same version,
# cross-compiles the binaries, writes a complete release bundle to
# dist/release-output/, regenerates the index, and tags every tool.
# Run ./deploy to publish to /srv/zddc/.
#
# See AGENTS.md "Releasing — lockstep, channels, layout" for the full
# release process.
@ -18,17 +17,18 @@ set -eu
cat >&2 <<'EOF'
zddc/release.sh is deprecated.
Use the top-level lockstep release instead:
Use the top-level lockstep release from the repo root instead:
sh build.sh --release # stable, coordinated next version
sh build.sh --release X.Y.Z # stable, explicit version
sh build.sh --release alpha # alpha cut for everything
sh build.sh --release beta # beta cut for everything
./build release # stable, coordinated next version
./build release X.Y.Z # stable, explicit version
./build alpha # alpha cut for everything
./build beta # beta cut for everything
./deploy --releases # publish dist/release-output/ to /srv/zddc/
zddc-server binaries now ship under website/releases/ alongside the HTML
tools and serve from zddc.varasys.io/releases/. There's no Codeberg
release-asset publication anymore. See AGENTS.md "Releasing — lockstep,
channels, layout" for details.
zddc-server binaries now ship in dist/release-output/ (gitignored,
local-only) and are deployed to /srv/zddc/releases/ on the live host.
No Codeberg release-asset publication, no LFS. See AGENTS.md
"Releasing — lockstep, channels, layout" for details.
EOF
exit 1