refactor: distribute via Codeberg release assets, drop the upstream image

Removes the codeberg.org/varasys/zddc-server registry image, which had
no remaining consumer outside this shop. The two chart Dockerfiles
(tnd-zddc-chart) now compile zddc-server from source at build time,
fetching the right tag from a Codeberg release. release-image.sh,
zddc/Containerfile, and zddc/podman-compose.yaml are gone.

Build artifacts (HTML tools + zddc-server binaries) move from
website/releases/ in this repo to Codeberg release assets attached to
git tags. The website at zddc.varasys.io serves them by reverse-
proxying /releases/<tag>/<asset> to the corresponding Codeberg URL,
so consumers (zddc-use, level-2 bootstrap stubs, the chart
Dockerfiles) only ever talk to zddc.varasys.io.

Releases page becomes server-rendered static HTML regenerated on each
build via a single Codeberg API call. A small website/releases/manifest.json
maps <tool>-<channel> → tag for runtime channel resolution by zddc-use
and the level-2 stubs.

Files added:
- shared/publish-codeberg-release.sh — POSIX-sh helper that creates a
  Codeberg release for a tag (sets prerelease flag from tag suffix)
  and uploads/replaces release assets idempotently. Sourced by
  build-lib.sh and zddc/release.sh.
- zddc/release.sh — replaces release-image.sh. Tags + cross-compiles
  binaries via native Go (no podman needed; install Go) + uploads to
  Codeberg release assets. No image build, no registry push.

Files modified:
- shared/build-lib.sh — promote_release tags + uploads via the helper
  for stable AND alpha/beta now (alpha/beta were untagged before).
  update_alpha removed; per-tool build.sh files no longer mirror to
  website/releases/<tool>_alpha.html on plain dev builds.
- build.sh — prefers native go build over the old podman-based
  cross-compile (which is gone with Containerfile). build_releases_index
  queries the Codeberg API once and writes static HTML + manifest.json,
  with graceful fallback when the API is unreachable.
- bootstrap/level2.html.tmpl — fetches manifest.json to resolve
  channel → tag, then fetches the asset from /releases/<tag>/<asset>
  (Caddy proxy). Replaces the old /releases/<tool>_<channel>.html flat
  URL pattern. Operators with curl'd level-2 stubs need to re-issue
  them — this is a breaking change.
- AGENTS.md, CLAUDE.md — rewritten to describe the new flow.
- .gitignore — releases/ artifacts now expected to be on Codeberg, not
  committed locally.

NOT in this commit (deferred until $CODEBERG_TOKEN is provisioned):
- Backfilling existing tags as Codeberg releases.
- Cleanup commit: git rm-ing the existing artifacts in website/releases/.
  Until backfill happens, those files are how operators with old
  bootstrap stubs still get content. Once Codeberg has the assets,
  drop them.
- The Caddy reverse-proxy config on zddc.varasys.io.

Operator-side changes (not in this repo):
- tnd-zddc-chart Dockerfile.prod and Dockerfile (dev) need updating
  to compile from source rather than `FROM codeberg.org/...:stable`.
  Done in a separate commit on that repo.
- Caddyfile rule for the /releases/<tag>/<asset> reverse-proxy.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
ZDDC 2026-04-29 21:18:26 -05:00
parent 6e1777e9c2
commit 2dc9ad240c
31 changed files with 1173 additions and 814 deletions

10
.gitignore vendored
View file

@ -20,6 +20,16 @@ test-results/
# New tool dist files must be force-added: git add -f tool/dist/tool.html # New tool dist files must be force-added: git add -f tool/dist/tool.html
dist/ dist/
# Release artifacts — published to Codeberg release assets, not committed.
# website/releases/index.html and website/releases/manifest.json are
# regenerated by build.sh and DO get committed (they're tiny indices).
# Everything else under website/releases/ — versioned HTML tool files,
# zddc-server binaries — lives on Codeberg; the website's Caddy reverse-
# proxies download URLs to Codeberg release-asset URLs.
website/releases/*.html
!website/releases/index.html
website/releases/zddc-server-*
# IDE and project files # IDE and project files
.opencode/ .opencode/
opencode.json opencode.json

111
AGENTS.md
View file

@ -162,21 +162,22 @@ Format: `trackingNumber_revision (status) - title.extension`
### Releasing — channels and layout ### Releasing — channels and layout
Three channels. Versioning is **pre-release semver**: stable owns clean `vX.Y.Z`; alpha and beta carry `vX.Y.Z-alpha` / `vX.Y.Z-beta` suffixes (with a `.N` counter on the zddc-server image only — see "zddc-server release flow" below). The next-stable target X.Y.Z is patch-bumped from the latest clean `<tool>-vX.Y.Z` tag, so the on-page label always communicates which stable the alpha/beta is working toward. Three channels. Versioning is **pre-release semver**: stable owns clean `vX.Y.Z`; alpha and beta carry `vX.Y.Z-alpha.N` / `vX.Y.Z-beta.N`. The next-stable target X.Y.Z is patch-bumped from the latest clean `<tool>-vX.Y.Z` tag.
- **Stable**: versioned, immutable. `sh tool/build.sh --release [version]` writes `website/releases/<tool>_v<version>.html`, refreshes the `<tool>_stable.html` symlink, and tags `<tool>-v<version>`. Skips automatically if source has not changed since the latest tag. Pass an explicit version to override auto-increment. Label: `vX.Y.Z` (black). **Storage model.** Built artifacts live on Codeberg as release assets attached to git tags — *not* committed to this repo. The website at zddc.varasys.io serves them by reverse-proxying `/releases/<tag>/<asset>` to the corresponding Codeberg URL, so consumers (operators' bootstrap stubs, `zddc-use`) only ever talk to zddc.varasys.io. Channel resolution is via `website/releases/manifest.json` — a small file `build.sh` regenerates from the Codeberg API and commits.
- **Beta**: mutable. `sh tool/build.sh --release beta` overwrites `website/releases/<tool>_beta.html` in place. No tag — alpha and beta artifacts are mutable files, not immutable per-build snapshots. Label: `vX.Y.Z-beta · <date> · <sha>` (red), where `X.Y.Z` is the next-stable target.
- **Alpha**: mutable, analogous. `sh tool/build.sh --release alpha`. **Also**: every plain (non-release) `tool/build.sh` invocation copies the just-built dist file into `website/releases/<tool>_alpha.html`, so the alpha hyperlinks on the website always serve the latest dev build. Plain copy (not symlink) so the file is reachable regardless of how the web server mounts the working tree (the canonical Caddy setup at `/etc/containers/systemd/caddy.container` mounts only `website/` read-only and cannot follow `../<tool>/dist/` symlinks). Side-effect: every dev build dirties the corresponding `_alpha.html` in `website/releases/`; commit those alongside the source change or `git checkout` them before pushing. Plain-build alpha pages carry the dev label `vX.Y.Z-alpha · <ts> · <sha>[-dirty]` (red, with the full timestamp + dirty marker so iterative dev builds are distinguishable from formal `--release alpha` cuts, which use `<date>` only and never carry `-dirty`).
Stable releases do **not** automatically clobber `<tool>_alpha.html` / `<tool>_beta.html` — those keep whatever was last built into them. Use `./freshen-channel <tool> <channel>` (see "Freshen helper" below) to drag a channel forward to current stable; never `git checkout` the main worktree by hand for this. - **Stable**: `sh tool/build.sh --release [version]` (or just `--release` to auto-bump patch from the latest stable tag). Tags `<tool>-v<version>`, uploads `<tool>_v<version>.html` as a release asset on Codeberg. Label: `vX.Y.Z` (black). Skips silently if source has not changed since the latest stable tag (HEAD-vs-tag diff).
- **Beta**: `sh tool/build.sh --release beta`. Tags `<tool>-v<next-patch>-beta.N`, uploads `<tool>_v<next-patch>-beta.N.html`. Label: `vX.Y.Z-beta · <date> · <sha>` (red).
- **Alpha**: `sh tool/build.sh --release alpha`. Tags `<tool>-v<next-patch>-alpha.N`, uploads. Label: `vX.Y.Z-alpha · <date> · <sha>` (red).
- **Plain dev builds** (no `--release`): produce `tool/dist/<tool>.html` only. No website/releases side-effect, no Codeberg upload. To publish, re-run with `--release alpha`.
After cutting a stable release, run `git push --tags` to publish the tag. After any release run, `sh build.sh` queries the Codeberg API once and rewrites `website/releases/index.html` and `manifest.json`. Commit those alongside the release.
The "skip if no source change since last tag" guard for stable releases compares **HEAD** to the latest tag — uncommitted working-tree changes are invisible. If you edit a tool and want a stable release to actually fire, commit the change first; otherwise the build prints `no source changes since <tool>-vX.Y.Z — skipping` and exits 0. Alpha and beta channel builds always rebuild (no skip check). After cutting a release, run `git push --tags` to publish the tag.
Agents must **never** write to `website/releases/<tool>_v*.html`, `website/releases/<tool>_stable.html`, `website/releases/<tool>_beta.html`, or `website/index.html` directly — always go through `--release` or `./freshen-channel`. (The `<tool>_alpha.html` files are an exception: every plain build rewrites them with a copy of the current dist, as described above.) `$CODEBERG_TOKEN` must be exported before any `--release` invocation. The `promote_release` helper calls `publish_codeberg_release` which uses the token to create the release and upload the asset.
`landing/build.sh --release <version>` additionally writes `website/index.html` (the root URL of zddc.varasys.io). `landing/build.sh --release <version>` additionally writes `website/index.html` (the root URL of zddc.varasys.io) as a regular committed file — that page is hand-edited intro copy, not a release asset.
### Channel discipline (MUST rules) ### Channel discipline (MUST rules)
@ -191,7 +192,7 @@ The build system does not enforce these. Treating channels carelessly defeats th
### Freshen helper ### Freshen helper
`./freshen-channel <tool> <channel>` rebuilds the alpha or beta channel of a tool from its current stable tag. Use it after every stable release (rule 4 above) and any other time alpha/beta has fallen behind stable. `./freshen-channel <tool> <channel>` rebuilds the alpha or beta channel of a tool from its current stable tag, cutting a new pre-release tag (e.g., `<tool>-v<next-patch>-alpha.N`) and uploading the asset to Codeberg. Use it after every stable release (rule 4 above) and any other time alpha/beta has fallen behind stable.
```sh ```sh
./freshen-channel archive alpha ./freshen-channel archive alpha
@ -200,13 +201,12 @@ The build system does not enforce these. Treating channels carelessly defeats th
What it does: What it does:
1. Finds the latest `<tool>-v*` tag. 1. Finds the latest `<tool>-v*` clean stable tag.
2. Creates a temporary git worktree at that tag — does **not** touch the main worktree's HEAD or working tree. 2. Creates a temporary git worktree at that tag — does **not** touch the main worktree's HEAD or working tree.
3. Runs `<tool>/build.sh --release <channel>` inside the worktree. 3. Runs `<tool>/build.sh --release <channel>` inside the worktree, which tags `<tool>-v<next-patch>-<channel>.N` and uploads to Codeberg.
4. Copies the resulting `<tool>_<channel>.html` into the main repo's `website/releases/`. 4. Removes the worktree.
5. Removes the worktree.
The on-page label of the freshened build is `v<next-stable>-<channel> · <today> · <stable-tag-sha>` the version reflects the next-stable target (patch+1 from current stable, since the freshen establishes the baseline for the upcoming pre-release window) and the SHA pins which stable was used as the source, recoverable via `git checkout`. The on-page label of the freshened build is `v<next-stable>-<channel> · <today> · <stable-tag-sha>` — the SHA pins which stable was used as the source, recoverable via `git checkout`.
Note: the build pipeline used is the one **at the tag**, not on `main`. That is intentional (pure reproducibility). If you have made build-system improvements since stable was cut and want the freshen to use them, cut a new stable first. Note: the build pipeline used is the one **at the tag**, not on `main`. That is intentional (pure reproducibility). If you have made build-system improvements since stable was cut and want the freshen to use them, cut a new stable first.
@ -215,9 +215,9 @@ Note: the build pipeline used is the one **at the tag**, not on `main`. That is
`build.sh` regenerates `website/bootstrap/` on every invocation: `build.sh` regenerates `website/bootstrap/` on every invocation:
- `bootstrap/level1/<tool>.html` — 4 same-origin level-1 stubs (archive, transmittal, classifier, mdedit; landing has no level-1 stub since it only lives at deployment root). - `bootstrap/level1/<tool>.html` — 4 same-origin level-1 stubs (archive, transmittal, classifier, mdedit; landing has no level-1 stub since it only lives at deployment root).
- `bootstrap/track-{alpha,beta,stable}/<tool>.html` — 5 level-2 stubs per channel, hardcoded to fetch the named channel from `zddc.varasys.io/releases/`. - `bootstrap/track-{alpha,beta,stable}/<tool>.html` — 5 level-2 stubs per channel that resolve the channel via `zddc.varasys.io/releases/manifest.json` and fetch the asset via `zddc.varasys.io/releases/<tag>/<tool>_v<version>.html` (Caddy proxies to Codeberg).
End users install via copy-paste shell snippets on the home page's "Install on your server" section — each snippet `curl`s the relevant stubs (or stable HTMLs, for the self-contained option) into the operator's deployment directory. End users install via copy-paste shell snippets on the home page's "Install on your server" section — each snippet `curl`s the relevant stubs (or a one-shot version-pinned HTML, for the self-contained option) into the operator's deployment directory.
See `bootstrap/README.md` for the install / pin / audit story. See `bootstrap/README.md` for the install / pin / audit story.
@ -278,28 +278,33 @@ ZDDC_DATA_DIR=/path/to/your/archive podman-compose -f zddc/podman-compose.yaml u
### Release tagging ### Release tagging
`release-image.sh` auto-derives the version (from the latest clean `zddc/release.sh` is the canonical path. It tags the commit, compiles
`zddc-server-vX.Y.Z` tag) and creates the git tag itself, so the the binaries (native Go), and uploads them as Codeberg release assets.
operator no longer cuts a tag manually before running the script: There's no container image build / push anymore — the chart's
`Dockerfile.prod` and `Dockerfile` (dev) compile zddc-server from
source at build time, fetching the right tag from Codeberg directly.
The upstream `codeberg.org/varasys/zddc-server` registry is frozen
(historical tags only).
```sh ```sh
sh release-image.sh # alpha cut, version auto-derived sh zddc/release.sh # alpha cut, version auto-derived
sh release-image.sh alpha # same sh zddc/release.sh alpha # same
sh release-image.sh beta # beta cut sh zddc/release.sh beta # beta cut
sh release-image.sh stable # stable cut, patch++ from latest stable sh zddc/release.sh stable # stable cut, patch++ from latest stable
sh release-image.sh stable 0.1.0 # stable cut, explicit version sh zddc/release.sh stable 0.1.0 # stable cut, explicit version
``` ```
**Default channel is `alpha`** so `:stable` never advances by accident **Default channel is `alpha`** so a stable-equivalent tag never
during active development. Pass `beta` to soak; pass `stable` only when appears by accident during active development. Pass `beta` to soak;
deliberately promoting. The script tags the commit but does NOT push pass `stable` only when deliberately promoting. The script tags the
git history — finish with `git push origin <branch>` and `git push --tags` (or push the specific tag). commit but does NOT push — finish with `git push origin <branch>` and
`git push origin <tag>`.
**Versioning** — pre-release semver. Stable cuts get clean `vX.Y.Z` **Versioning** — pre-release semver. Stable cuts get clean `vX.Y.Z`
tags and `:vX.Y.Z` image tags. Alpha and beta cuts get tags. Alpha and beta cuts get `vX.Y.Z-alpha.N` / `vX.Y.Z-beta.N`
`vX.Y.Z-alpha.N` / `vX.Y.Z-beta.N` where `X.Y.Z` is the next patch where `X.Y.Z` is the next patch of the latest clean stable and `N`
of the latest clean stable, and `N` is a per-channel counter that is a per-channel counter that resets when stable advances. Example
resets when stable advances. Example sequence (current stable v0.0.7): sequence (current stable v0.0.7):
``` ```
alpha → v0.0.8-alpha.1 alpha → v0.0.8-alpha.1
@ -310,35 +315,29 @@ stable → v0.0.8 (counter resets at next-patch advance)
alpha → v0.0.9-alpha.1 alpha → v0.0.9-alpha.1
``` ```
Cascades (unchanged):
- `alpha``:<version> :alpha`
- `beta``:<version> :beta :alpha`
- `stable``:<version> :stable :beta :alpha`
Pre-release semver ordering (`0.0.8-alpha.1 < 0.0.8-alpha.2 < Pre-release semver ordering (`0.0.8-alpha.1 < 0.0.8-alpha.2 <
0.0.8-beta.1 < 0.0.8`) is honored by all standard tooling registry 0.0.8-beta.1 < 0.0.8`) is honored by all standard tooling Codeberg
tag sorting, `git tag --sort=-v:refname`, `sort -V`, npm, cargo — release sorting, `git tag --sort=-v:refname`, `sort -V`, npm, cargo —
so consumers can pin or compare versions without surprises. so consumers can pin or compare versions without surprises.
**Binary publishing** — `release-image.sh` also mirrors the **Binary publishing** — release.sh uploads the four cross-compiled
cross-compiled binaries from `zddc/dist/zddc-server-<os>-<arch>` into binaries (`zddc-server-{linux,darwin,windows}-{amd64,arm64}`) as
`website/releases/zddc-server-<os>-<arch>-<channel>` for every channel release assets attached to the new git tag on Codeberg. The website
in the cascade. These are mutable channel pointers (no immutable at zddc.varasys.io reverse-proxies `/releases/<tag>/<asset>` URLs to
per-version files — those live in the container registry to keep the the corresponding Codeberg release-asset URL, so consumers
git tree from growing 40MB per release). Plain `sh build.sh` does (`zddc-use`, the level-2 bootstrap stubs, the dynamic chart
NOT mirror the binaries; only `release-image.sh` does, deliberately, Dockerfiles) only ever talk to zddc.varasys.io.
so the website's binaries always match a published image.
The mirrored files at `https://zddc.varasys.io/releases/zddc-server-…` After publishing: run `sh build.sh` to refresh
are what the dev shell's `zddc-use` helper fetches by default. `website/releases/index.html` and `manifest.json` against the new
release list, and commit those.
Prerequisite: `podman login codeberg.org` (one-time, with a Codeberg Prerequisites:
personal token scoped `package:write`). - Go 1.24+ on PATH (or run from a Go container).
- `$CODEBERG_TOKEN` exported, scoped to write the VARASYS/ZDDC repo.
There is no CI for this — `.woodpecker.yml` was removed in favor of There is no CI for this — solo workflow benefits from one canonical
local-build-and-push because solo workflow benefits from one canonical local path that fails loudly and visibly on the developer's terminal.
path that fails loudly and visibly on the developer's terminal.
### Notes ### Notes

View file

@ -36,7 +36,7 @@ npx playwright test <tool> # one spec
# zddc/ Go server (separate sub-project, not part of sh build.sh) # zddc/ Go server (separate sub-project, not part of sh build.sh)
(cd zddc && go test ./...) # unit tests (Go 1.24+) (cd zddc && go test ./...) # unit tests (Go 1.24+)
podman build -t zddc-server zddc/ # build container image podman build -t zddc-server zddc/ # build container image
sh release-image.sh [alpha|beta|stable] [<version>] # canonical image release; alpha/beta auto-derive version (default: alpha) sh zddc/release.sh [alpha|beta|stable] [<version>] # cut + publish zddc-server binaries to Codeberg release assets (default: alpha; auto-derives version)
``` ```
No lint/typecheck/format commands exist for the HTML tools — vanilla JS + POSIX sh by design. No lint/typecheck/format commands exist for the HTML tools — vanilla JS + POSIX sh by design.

View file

@ -90,6 +90,4 @@ echo "Wrote $output_html"
if [ "$is_release" = "1" ]; then if [ "$is_release" = "1" ]; then
promote_release "archive" promote_release "archive"
else
update_alpha "archive"
fi fi

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the {{CHANNEL}} default // (none) uses the {{CHANNEL}} default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = '{{TOOL}}'; const tool = '{{TOOL}}';
const defaultChannel = '{{CHANNEL}}'; const defaultChannel = '{{CHANNEL}}';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

273
build.sh
View file

@ -29,17 +29,25 @@ echo "Wrote zddc/dist/web/archive.html"
# Cross-compiled zddc-server binaries — only relevant if you're shipping # Cross-compiled zddc-server binaries — only relevant if you're shipping
# standalone Linux/macOS/Windows binaries to users. Skipped silently when # standalone Linux/macOS/Windows binaries to users. Skipped silently when
# podman isn't on PATH; the runtime container release path # Go isn't on PATH. (zddc/release.sh handles the publish flow that
# (release-image.sh) does its own internal build via Containerfile's # uploads these to Codeberg release assets.)
# multi-stage flow and doesn't need these host-side binaries.
echo "" echo ""
echo "=== Building zddc-server binaries ===" echo "=== Building zddc-server binaries ==="
if command -v podman >/dev/null 2>&1; then if command -v go >/dev/null 2>&1; then
podman build --target binaries -o "$SCRIPT_DIR/zddc/dist/" "$SCRIPT_DIR/zddc/" 2>&1 | grep -v "^-->" cd "$SCRIPT_DIR/zddc"
mkdir -p dist
for target in linux/amd64 darwin/amd64 darwin/arm64 windows/amd64; do
os="${target%/*}"; arch="${target#*/}"
out="zddc-server-${os}-${arch}"
case "$os" in windows) out="${out}.exe" ;; esac
echo " building $out"
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" \
go build -trimpath -ldflags="-s -w" -o "dist/$out" ./cmd/zddc-server
done
cd "$SCRIPT_DIR"
else else
echo "podman not found — skipping cross-compiled binary build." echo "go not found — skipping cross-compiled binary build."
echo " (Standalone-binary distribution only; the runtime container" echo " (Install Go 1.24+ to build standalone binaries.)"
echo " release path builds its own copy via Containerfile.)"
fi fi
# ─── Bootstrap stubs ───────────────────────────────────────────────────────── # ─── Bootstrap stubs ─────────────────────────────────────────────────────────
@ -116,11 +124,71 @@ EOF
done done
} }
# Regenerate website/releases/index.html — a static directory listing of # Regenerate website/releases/index.html and manifest.json from the
# all tool releases with channel rows + per-version archive links. Read # Codeberg release list. Single API call at build time, no runtime
# from the filesystem so the page is always consistent with what's there. # dependency on Codeberg from the page (it's static HTML when served).
#
# Page links use /releases/<tag>/<asset> URLs which the website's Caddy
# reverse-proxies to codeberg.org/.../releases/download/<tag>/<asset>.
# Operators see one origin (zddc.varasys.io); Codeberg is the storage
# backend.
#
# Skips silently if curl/jq aren't on PATH (offline dev shouldn't blow
# up). Skips with a warning if the API call fails (network down, rate-
# limited, etc.) — the existing index.html stays as-is.
build_releases_index() { build_releases_index() {
_out="$RELEASES_DIR/index.html" _out="$RELEASES_DIR/index.html"
_manifest="$RELEASES_DIR/manifest.json"
mkdir -p "$RELEASES_DIR"
if ! command -v curl >/dev/null 2>&1 || ! command -v jq >/dev/null 2>&1; then
echo " (curl or jq missing — skipping releases index regeneration)"
return 0
fi
_api_resp=$(curl -fsSL --max-time 30 \
'https://codeberg.org/api/v1/repos/VARASYS/ZDDC/releases?limit=100' \
2>/dev/null) || _api_resp=""
if [ -z "$_api_resp" ]; then
echo " (Codeberg API unreachable — leaving releases/index.html as-is)"
return 0
fi
# Build manifest.json: for each tool/channel, find the latest matching
# release and emit "<tool>-<channel>": "<tag>". Channel resolution:
# alpha = latest tag matching <tool>-vX.Y.Z-alpha.N
# beta = latest tag matching <tool>-vX.Y.Z-beta.N
# stable = latest tag matching <tool>-vX.Y.Z (no suffix)
# "Latest" via sort -V on the version part.
_tools="archive transmittal classifier mdedit landing zddc-server"
{
printf '{\n'
_first=1
for _tool in $_tools; do
for _ch in stable beta alpha; do
if [ "$_ch" = "stable" ]; then
_re="^${_tool}-v[0-9]+\\.[0-9]+\\.[0-9]+\$"
else
_re="^${_tool}-v[0-9]+\\.[0-9]+\\.[0-9]+-${_ch}\\.[0-9]+\$"
fi
_tag=$(printf '%s' "$_api_resp" \
| jq -r --arg re "$_re" '
[.[] | select(.tag_name | test($re)) | .tag_name]
| sort
| last // empty
')
if [ -n "$_tag" ]; then
[ "$_first" = "1" ] || printf ',\n'
printf ' "%s-%s": "%s"' "$_tool" "$_ch" "$_tag"
_first=0
fi
done
done
printf '\n}\n'
} > "$_manifest"
echo "Wrote $_manifest"
{ {
cat <<'HEAD' cat <<'HEAD'
<!DOCTYPE html> <!DOCTYPE html>
@ -184,113 +252,104 @@ build_releases_index() {
<main class="container" style="margin-bottom: var(--spacing-2xl);"> <main class="container" style="margin-bottom: var(--spacing-2xl);">
HEAD HEAD
printf '%s\n' "$TOOL_TABLE" | while IFS='|' read -r _tool _file _title; do # Render one section per tool. The HTML tool releases publish a
_stable_target="" # single asset per tag (the inlined HTML); zddc-server publishes
if [ -L "$RELEASES_DIR/${_tool}_stable.html" ]; then # per-platform binaries. The renderer handles both.
_stable_target=$(readlink "$RELEASES_DIR/${_tool}_stable.html") for _tool_entry in 'archive|Archive' \
'transmittal|Transmittal' \
'classifier|Classifier' \
'mdedit|Markdown Editor' \
'landing|Landing (project picker)' \
'zddc-server|zddc-server (Go file server)'; do
_tool="${_tool_entry%%|*}"
_title="${_tool_entry#*|}"
_re_stable="^${_tool}-v[0-9]+\\.[0-9]+\\.[0-9]+\$"
_re_alpha="^${_tool}-v[0-9]+\\.[0-9]+\\.[0-9]+-alpha\\.[0-9]+\$"
_re_beta="^${_tool}-v[0-9]+\\.[0-9]+\\.[0-9]+-beta\\.[0-9]+\$"
_latest_stable=$(printf '%s' "$_api_resp" | jq -r --arg re "$_re_stable" \
'[.[] | select(.tag_name | test($re)) | .tag_name] | sort | last // empty')
_latest_beta=$(printf '%s' "$_api_resp" | jq -r --arg re "$_re_beta" \
'[.[] | select(.tag_name | test($re)) | .tag_name] | sort | last // empty')
_latest_alpha=$(printf '%s' "$_api_resp" | jq -r --arg re "$_re_alpha" \
'[.[] | select(.tag_name | test($re)) | .tag_name] | sort | last // empty')
_all_stables=$(printf '%s' "$_api_resp" | jq -r --arg re "$_re_stable" \
'[.[] | select(.tag_name | test($re)) | .tag_name] | sort | reverse | .[]')
# Skip the section entirely if no releases exist for this tool.
if [ -z "$_latest_stable$_latest_beta$_latest_alpha" ]; then
continue
fi fi
printf ' <section class="rel-tool">\n' printf ' <section class="rel-tool">\n'
printf ' <h2>%s</h2>\n' "$_title" printf ' <h2>%s</h2>\n' "$_title"
# Channel chips. Each link uses /releases/<tag>/<asset>
# which Caddy proxies to the Codeberg release-asset URL.
# Asset name is <tool>_v<version>.html for HTML tools; for
# zddc-server we link to the Codeberg release page since
# the asset is per-platform (operator picks one).
printf ' <div class="rel-channels">\n' printf ' <div class="rel-channels">\n'
[ -e "$RELEASES_DIR/${_tool}_stable.html" ] && printf ' <a class="stable" href="%s_stable.html">stable</a>\n' "$_tool" for _row in "stable|$_latest_stable" "beta|$_latest_beta" "alpha|$_latest_alpha"; do
[ -e "$RELEASES_DIR/${_tool}_beta.html" ] && printf ' <a class="beta" href="%s_beta.html">beta</a>\n' "$_tool" _ch="${_row%%|*}"
[ -e "$RELEASES_DIR/${_tool}_alpha.html" ] && printf ' <a class="alpha" href="%s_alpha.html">alpha</a>\n' "$_tool" _tag="${_row#*|}"
[ -n "$_tag" ] || continue
if [ "$_tool" = "zddc-server" ]; then
printf ' <a class="%s" href="https://codeberg.org/VARASYS/ZDDC/releases/tag/%s">%s</a>\n' \
"$_ch" "$_tag" "$_ch"
else
_ver="${_tag#${_tool}-v}"
_asset="${_tool}_v${_ver}.html"
printf ' <a class="%s" href="/releases/%s/%s">%s</a>\n' \
"$_ch" "$_tag" "$_asset" "$_ch"
fi
done
printf ' </div>\n' printf ' </div>\n'
_versions=$(ls -1 "$RELEASES_DIR" 2>/dev/null | grep -E "^${_tool}_v[0-9]" | sort -V -r)
if [ -n "$_versions" ]; then # zddc-server: per-platform binary table, one row per channel.
if [ "$_tool" = "zddc-server" ]; then
_platforms="linux-amd64 darwin-amd64 darwin-arm64 windows-amd64"
printf ' <h3 style="font-size:1rem;margin:0.75rem 0 0.4rem;">Standalone binaries</h3>\n'
printf ' <table class="rel-bin-table"><thead><tr><th>Channel</th>'
for _p in $_platforms; do printf '<th>%s</th>' "$_p"; done
printf '</tr></thead><tbody>\n'
for _row in "stable|$_latest_stable" "beta|$_latest_beta" "alpha|$_latest_alpha"; do
_ch="${_row%%|*}"
_tag="${_row#*|}"
printf ' <tr><td class="ch-%s">%s</td>' "$_ch" "$_ch"
if [ -z "$_tag" ]; then
for _p in $_platforms; do printf '<td class="empty">—</td>'; done
else
for _p in $_platforms; do
_ext=""; case "$_p" in windows-*) _ext=".exe" ;; esac
_asset="zddc-server-${_p}${_ext}"
printf '<td><a href="/releases/%s/%s">download</a></td>' "$_tag" "$_asset"
done
fi
printf '</tr>\n'
done
printf ' </tbody></table>\n'
fi
# Pin-to-version row. All stables (semver-descending). HTML
# tools link to the asset directly; zddc-server links to the
# Codeberg release page (per-platform asset choice).
if [ -n "$_all_stables" ]; then
printf ' <div class="rel-versions"><strong>Pin to version:</strong>\n' printf ' <div class="rel-versions"><strong>Pin to version:</strong>\n'
printf '%s\n' "$_versions" | while read -r _v; do printf '%s\n' "$_all_stables" | while read -r _t; do
_ver=${_v#${_tool}_v}; _ver=${_ver%.html} [ -n "$_t" ] || continue
printf ' <a href="%s">v%s</a>\n' "$_v" "$_ver" _v="${_t#${_tool}-v}"
if [ "$_tool" = "zddc-server" ]; then
printf ' <a href="https://codeberg.org/VARASYS/ZDDC/releases/tag/%s">v%s</a>\n' "$_t" "$_v"
else
printf ' <a href="/releases/%s/%s_v%s.html">v%s</a>\n' "$_t" "$_tool" "$_v" "$_v"
fi
done done
printf ' </div>\n' printf ' </div>\n'
fi fi
if [ -n "$_stable_target" ]; then
printf ' <div class="rel-meta">stable currently → %s</div>\n' "$_stable_target"
fi
printf ' </section>\n' printf ' </section>\n'
done done
# zddc-server section — different artifact shape than the HTML tools.
# Two artifact families:
# 1. Downloadable binaries at website/releases/zddc-server-<os>-<arch>-<channel>
# (mutable channel pointers; release-image.sh writes them).
# 2. Container images at codeberg.org/varasys/zddc-server:<channel> and
# :<version> (the registry; we don't proxy them through the website,
# but we list the recent tags and a `podman pull` snippet for each).
printf ' <section class="rel-tool">\n'
printf ' <h2>zddc-server</h2>\n'
printf ' <p style="margin-top:0;color:var(--color-text-muted);font-size:0.95rem;">Go HTTP file server with ACL, .archive index, and a delegated-admin .zddc editor. Distributed as a container image (preferred) or a standalone binary.</p>\n'
# Binaries table.
_platforms="linux-amd64 darwin-amd64 darwin-arm64 windows-amd64"
_have_any_bin=0
for _ch in stable beta alpha; do
for _p in $_platforms; do
_ext=""; case "$_p" in windows-*) _ext=".exe" ;; esac
if [ -f "$RELEASES_DIR/zddc-server-${_p}-${_ch}${_ext}" ]; then
_have_any_bin=1; break 2
fi
done
done
if [ "$_have_any_bin" = "1" ]; then
printf ' <h3 style="font-size:1rem;margin:0.75rem 0 0.4rem;">Standalone binaries</h3>\n'
printf ' <table class="rel-bin-table"><thead><tr><th>Channel</th>'
for _p in $_platforms; do printf '<th>%s</th>' "$_p"; done
printf '</tr></thead><tbody>\n'
for _ch in stable beta alpha; do
printf ' <tr><td class="ch-%s">%s</td>' "$_ch" "$_ch"
for _p in $_platforms; do
_ext=""; case "$_p" in windows-*) _ext=".exe" ;; esac
_f="zddc-server-${_p}-${_ch}${_ext}"
if [ -f "$RELEASES_DIR/$_f" ]; then
printf '<td><a href="%s">download</a></td>' "$_f"
else
printf '<td class="empty">—</td>'
fi
done
printf '</tr>\n'
done
printf ' </tbody></table>\n'
fi
# Container image: pull-snippet box plus the recent git tag list.
printf ' <h3 style="font-size:1rem;margin:0.75rem 0 0.4rem;">Container image</h3>\n'
printf ' <p style="margin:0 0 0.4rem;font-size:0.9rem;">Pull from <a href="https://codeberg.org/VARASYS/-/packages/container/zddc-server">codeberg.org/varasys/zddc-server</a>:</p>\n'
printf ' <div class="rel-channels">\n'
printf ' <a class="stable" href="https://codeberg.org/VARASYS/-/packages/container/zddc-server/stable">:stable</a>\n'
printf ' <a class="beta" href="https://codeberg.org/VARASYS/-/packages/container/zddc-server/beta">:beta</a>\n'
printf ' <a class="alpha" href="https://codeberg.org/VARASYS/-/packages/container/zddc-server/alpha">:alpha</a>\n'
printf ' </div>\n'
printf ' <code class="rel-pull">podman pull codeberg.org/varasys/zddc-server:stable</code>\n'
# Recent zddc-server tags from git: clean stables first (top), then
# the most recent pre-releases. Dropping older pre-releases keeps the
# list readable since alpha/beta cuts proliferate.
_server_stables=$(git -C "$SCRIPT_DIR" tag --list 'zddc-server-v*' 2>/dev/null \
| grep -E '^zddc-server-v[0-9]+\.[0-9]+\.[0-9]+$' \
| sed 's|^zddc-server-v||' \
| sort -V -r)
_server_prereleases=$(git -C "$SCRIPT_DIR" tag --list 'zddc-server-v*' 2>/dev/null \
| grep -E '^zddc-server-v[0-9]+\.[0-9]+\.[0-9]+-' \
| sed 's|^zddc-server-v||' \
| sort -V -r \
| head -10)
if [ -n "$_server_stables" ] || [ -n "$_server_prereleases" ]; then
printf ' <div class="rel-versions" style="margin-top:0.6rem;"><strong>Pin to version:</strong>\n'
printf '%s\n' "$_server_stables" | while read -r _v; do
[ -n "$_v" ] || continue
printf ' <a href="https://codeberg.org/VARASYS/-/packages/container/zddc-server/%s">v%s</a>\n' "$_v" "$_v"
done
if [ -n "$_server_prereleases" ]; then
printf ' <span style="color:var(--color-text-muted);">recent pre-releases:</span>\n'
printf '%s\n' "$_server_prereleases" | while read -r _v; do
[ -n "$_v" ] || continue
printf ' <a href="https://codeberg.org/VARASYS/-/packages/container/zddc-server/%s">v%s</a>\n' "$_v" "$_v"
done
fi
printf ' </div>\n'
fi
printf ' </section>\n'
cat <<'TAIL' cat <<'TAIL'

View file

@ -88,6 +88,4 @@ echo "Wrote $output_html"
if [ "$is_release" = "1" ]; then if [ "$is_release" = "1" ]; then
promote_release "classifier" promote_release "classifier"
else
update_alpha "classifier"
fi fi

View file

@ -72,11 +72,7 @@ if [ "$is_release" = "1" ]; then
promote_release "landing" promote_release "landing"
# NOTE: website/index.html is a hand-edited intro page for # NOTE: website/index.html is a hand-edited intro page for
# zddc.varasys.io, not the landing tool. The landing tool ships # zddc.varasys.io, not the landing tool. The landing tool ships
# only via website/releases/ — the self-contained install snippet # only via Codeberg release assets (uploaded by promote_release) —
# on the home page copies landing_stable.html to <deployment-root>/ # the self-contained install snippet on the home page fetches
# index.html, where the project picker UI is useful (it queries # landing_v<version>.html via the zddc.varasys.io Caddy proxy.
# zddc-server for the project list). See AGENTS.md "Releasing —
# channels and layout".
else
update_alpha "landing"
fi fi

View file

@ -130,6 +130,4 @@ echo "Wrote $output_html ($(wc -c < "$output_html") bytes)"
if [ "$is_release" = "1" ]; then if [ "$is_release" = "1" ]; then
promote_release "mdedit" promote_release "mdedit"
else
update_alpha "mdedit"
fi fi

View file

@ -1,214 +0,0 @@
#!/bin/sh
# release-image.sh — build the zddc-server runtime image locally and push it
# to codeberg.org/varasys/zddc-server with cascading channel tags.
#
# Usage:
# sh release-image.sh # alpha cut, version auto-derived
# sh release-image.sh alpha # same
# sh release-image.sh beta # beta cut, version auto-derived
# sh release-image.sh stable # stable cut, patch++ from latest stable
# sh release-image.sh stable 0.1.0 # stable cut, explicit version
#
# Channel cascade rules (unchanged from earlier):
# alpha → :<version> :alpha
# beta → :<version> :beta :alpha
# stable → :<version> :stable :beta :alpha
#
# Versioning: pre-release semver. Stable releases own clean vX.Y.Z; alpha
# and beta carry vX.Y.Z-alpha.N / vX.Y.Z-beta.N suffixes, where X.Y.Z is
# the next patch of the latest clean stable tag and N is a per-channel
# counter that resets when stable advances.
#
# The script does ONE thing the old version did not: it `git tag`s the
# release before pushing the image, since auto-derivation means the
# operator can no longer predict the version up-front. The tag is pushed
# to origin only if the operator runs `git push --tags` afterwards
# (intentional — the script never pushes git history on its own).
#
# Prerequisites:
# - podman (or docker — commands are identical)
# - logged in to codeberg.org: `podman login codeberg.org`
# (Codeberg username + a token with `package:write` scope, generated at
# https://codeberg.org/user/settings/applications)
#
# What it does:
# 1. Compute the version (auto for alpha/beta, auto-or-explicit for stable).
# 2. Refresh dist/web by running sh build.sh — the Containerfile's
# server stage COPYs those files in.
# 3. Tag the current commit zddc-server-v<version>.
# 4. Build zddc/Containerfile's `server` stage as a single local image.
# 5. Tag and push each cascade tag.
set -eu
usage() {
cat >&2 <<'EOF'
usage: release-image.sh [alpha|beta|stable] [<version>]
alpha (default) cut alpha, version auto-derived from the latest
clean stable tag (vX.Y.Z + patch++ + -alpha.N).
beta cut beta, version auto-derived (vX.Y.Z + -beta.N).
stable cut stable. Without <version>, patch-bump from the
latest clean stable tag. With <version>, use it
verbatim (must be a clean X.Y.Z).
EOF
exit 1
}
CHANNEL="${1:-alpha}"
case "$CHANNEL" in
alpha | beta | stable) ;;
-h | --help) usage ;;
*) echo "error: unknown channel '$CHANNEL'" >&2; usage ;;
esac
EXPLICIT_VERSION="${2:-}"
if [ -n "$EXPLICIT_VERSION" ] && [ "$CHANNEL" != "stable" ]; then
echo "error: an explicit <version> is only valid with the 'stable' channel" >&2
echo " alpha and beta versions are auto-derived." >&2
exit 1
fi
REPO="codeberg.org/varasys/zddc-server"
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
TAG_PREFIX="zddc-server-v"
# Source build-lib.sh so we can call next_prerelease for alpha/beta and
# share the validation helpers used by the HTML tools.
root_dir="$SCRIPT_DIR"
. "$SCRIPT_DIR/shared/build-lib.sh"
# --- Determine the version --------------------------------------------------
case "$CHANNEL" in
alpha | beta)
VERSION=$(next_prerelease "$CHANNEL" "$TAG_PREFIX")
;;
stable)
if [ -n "$EXPLICIT_VERSION" ]; then
_validate_semver "$EXPLICIT_VERSION"
VERSION="$EXPLICIT_VERSION"
else
# Auto-bump patch from the latest clean stable tag.
_latest=$(git -C "$SCRIPT_DIR" tag --list "${TAG_PREFIX}*" 2>/dev/null \
| grep -E "^${TAG_PREFIX}[0-9]+\.[0-9]+\.[0-9]+\$" \
| sed "s|^${TAG_PREFIX}||" \
| sort -V \
| tail -1)
[ -n "$_latest" ] || _latest="0.0.0"
_major="${_latest%%.*}"
_rest="${_latest#*.}"
_minor="${_rest%%.*}"
_patch="${_rest#*.}"
VERSION="${_major}.${_minor}.$((_patch + 1))"
fi
;;
esac
case "$CHANNEL" in
alpha) TAGS="$VERSION alpha" ;;
beta) TAGS="$VERSION beta alpha" ;;
stable) TAGS="$VERSION stable beta alpha" ;;
esac
# Pick podman or docker, whichever is on PATH.
if command -v podman >/dev/null 2>&1; then
OCI=podman
elif command -v docker >/dev/null 2>&1; then
OCI=docker
else
echo "error: neither podman nor docker found on PATH" >&2
exit 1
fi
GIT_TAG="${TAG_PREFIX}${VERSION}"
echo "=== Building $REPO ==="
echo "Channel: $CHANNEL"
echo "Version: $VERSION"
echo "Git tag: $GIT_TAG"
echo "Image tags: $TAGS"
echo "OCI CLI: $OCI"
echo
# --- Refresh HTML dist (Containerfile COPYs from dist/web) ------------------
sh "$SCRIPT_DIR/build.sh"
# --- Mirror cross-compiled binaries to website/releases/ -------------------
# Channel pointers, mutable: cascade rules mirror this image's binaries to
# every channel in the cascade so a `zddc-use :stable` curl pulls bytes that
# match the just-published stable image. Per-version pinning is provided by
# the container registry's :vX.Y.Z-alpha.N tag, not by an immutable file
# here — keeps the repo from growing 40MB per release. Plain `sh build.sh`
# does NOT mirror binaries, deliberately: it would dirty 40MB of files on
# every dev iteration. Use release-image.sh as the publish trigger.
publish_binary() {
_src="$1" # path under zddc/dist/, e.g. zddc-server-linux-amd64
_basename=$(basename "$_src")
if [ ! -f "$SCRIPT_DIR/zddc/dist/$_src" ]; then
echo "warn: $_src not found in zddc/dist/; skipping mirror" >&2
return 0
fi
# The version-style tag (e.g. 0.0.8-alpha.1) is in $TAGS too — skip it;
# only iterate the channel-name tags.
for _tag in $TAGS; do
case "$_tag" in
alpha | beta | stable)
_dest="$SCRIPT_DIR/website/releases/${_basename%.exe}-${_tag}"
case "$_basename" in *.exe) _dest="${_dest}.exe" ;; esac
cp -f "$SCRIPT_DIR/zddc/dist/$_src" "$_dest"
echo "mirrored $_src → website/releases/$(basename "$_dest")"
;;
esac
done
}
publish_binary zddc-server-linux-amd64
publish_binary zddc-server-darwin-amd64
publish_binary zddc-server-darwin-arm64
publish_binary zddc-server-windows-amd64.exe
# --- Tag the commit (idempotent: skip if the tag already points here) -------
if git -C "$SCRIPT_DIR" rev-parse -q --verify "refs/tags/$GIT_TAG" >/dev/null; then
_existing=$(git -C "$SCRIPT_DIR" rev-list -n 1 "$GIT_TAG")
_head=$(git -C "$SCRIPT_DIR" rev-parse HEAD)
if [ "$_existing" != "$_head" ]; then
echo "error: tag $GIT_TAG already exists at $_existing, but HEAD is $_head" >&2
echo " refusing to overwrite. Resolve manually." >&2
exit 1
fi
echo "(tag $GIT_TAG already at HEAD)"
else
git -C "$SCRIPT_DIR" tag "$GIT_TAG"
echo "tagged $GIT_TAG (run 'git push --tags' to publish)"
fi
# --- Build + push -----------------------------------------------------------
"$OCI" build --target server -t zddc-server:build "$SCRIPT_DIR/zddc/"
echo
echo "=== Pushing tags ==="
for tag in $TAGS; do
"$OCI" tag zddc-server:build "$REPO:$tag"
"$OCI" push "$REPO:$tag"
echo "pushed $REPO:$tag"
done
echo
echo "=== Done ==="
echo "Image: $REPO:$VERSION"
echo "Cascading tags: $TAGS"
echo "Git tag: $GIT_TAG (publish with: git push origin $GIT_TAG)"
echo
case "$CHANNEL" in
stable)
echo "Reminder (channel discipline rule 4): freshen alpha + beta now"
echo "so users tracking those channels are not on stale code:"
echo " ./freshen-channel <tool> alpha"
echo " ./freshen-channel <tool> beta"
echo
echo "Bump tnd-zddc-chart's ZDDC_SERVER_TAG (push to develop) so the"
echo "chart's CI rebuilds the dev-shell image and helm-rolls."
;;
beta)
echo "Beta cut. Soak before promoting to stable."
;;
esac

View file

@ -21,36 +21,28 @@
# — sets globals: build_label, build_version, # — sets globals: build_label, build_version,
# is_release, is_red, channel. # is_release, is_red, channel.
# See "Channels and release args" below. # See "Channels and release args" below.
# promote_release <tool> — write to website/releases/ in the layout # promote_release <tool> — for stable / alpha / beta, tag the
# driven by $channel and $build_version. For # commit and upload the dist HTML as a
# stable, also update the _stable.html # Codeberg release asset. No local mirror
# symlink and create the git tag. # under website/releases/ — the website
# update_alpha <tool> — copy the just-built dist file to # reverse-proxies download URLs to Codeberg
# website/releases/<tool>_alpha.html so the # release-asset URLs. Stable cuts skip when
# site's alpha hyperlinks always serve the # the tool's source is unchanged since the
# latest dev build. Called automatically by # latest stable tag.
# each tool's build.sh on plain (non-release)
# builds. Plain copy (not symlink) so it
# survives deployments whose web server only
# mounts website/. `--release alpha` writes
# the same file with the formal
# "alpha · <date> · <sha>" label; the next
# plain build clobbers it.
# #
# Channels and release args: # Channels and release args:
# <none> dev build, dist/ + alpha mirror, label # <none> dev build, dist/ only, label
# "v<next-stable>-alpha · <ts> · <sha>[-dirty]" (red). # "v<next-stable>-alpha · <ts> · <sha>[-dirty]" (red).
# Plain builds ARE alpha builds — every dev build cascades # No website/releases/ side-effect. To publish, re-run
# to the alpha channel via update_alpha. The version embeds # with `--release alpha`.
# the next-stable target (patch-bump of the latest clean
# <tool>-vX.Y.Z tag).
# --release stable, auto-bump patch from latest tag (or 0.0.1). # --release stable, auto-bump patch from latest tag (or 0.0.1).
# Label "vX.Y.Z" (black). # Label "vX.Y.Z" (black). Tags + uploads.
# --release X.Y.Z stable, explicit version. # --release X.Y.Z stable, explicit version. Tags + uploads.
# --release alpha alpha channel cut at HEAD; # --release alpha alpha channel cut at HEAD;
# label "v<next-stable>-alpha · <date> · <sha>" (red). # label "v<next-stable>-alpha · <date> · <sha>" (red).
# Tags <tool>-vX.Y.Z-alpha.N + uploads.
# --release beta beta channel; label "v<next-stable>-beta · <date> · <sha>". # --release beta beta channel; label "v<next-stable>-beta · <date> · <sha>".
# Opt-in soak channel before stable promotion. # Tags + uploads.
# --release <other> error. # --release <other> error.
# ============================================================================= # =============================================================================
@ -60,6 +52,11 @@ if [ -z "${root_dir:-}" ]; then
exit 1 exit 1
fi fi
# Pull in the Codeberg release-publish helper so promote_release can call
# its publish_codeberg_release function. Sourced unconditionally — the
# helper has no side effects when sourced (only defines functions).
. "$root_dir/../shared/publish-codeberg-release.sh"
# Fail hard on any missing source file # Fail hard on any missing source file
ensure_exists() { ensure_exists() {
_path="$1" _path="$1"
@ -265,80 +262,83 @@ _next_stable_for_tool() {
echo "${_major}.${_minor}.$((_patch + 1))" echo "${_major}.${_minor}.$((_patch + 1))"
} }
# Promote a built dist file to the appropriate slot under website/releases/. # Promote a built dist file to a Codeberg release.
# Reads from caller scope: $channel, $build_version, $output_html, $root_dir. # Reads from caller scope: $channel, $build_version, $output_html, $root_dir.
# #
# Stable releases write website/releases/<tool>_v<version>.html, refresh the # All three channels (alpha, beta, stable) follow the same shape now:
# website/releases/<tool>_stable.html symlink, and tag <tool>-v<version> in # 1. Compute the version (already done by compute_build_label for stable;
# git. Skips silently when the source has not changed since the latest tag. # for alpha/beta we compute next_prerelease here).
# 2. Tag the commit <tool>-v<version> (or <tool>-v<version>-CHANNEL.N).
# 3. Upload the built dist HTML as a release asset to Codeberg.
# #
# Alpha and beta channel releases overwrite website/releases/<tool>_<channel>.html # Idempotent: the publish helper replaces a same-named asset on re-upload,
# in place with no tag (the embedded label carries date + commit SHA, so the # and the tag step is a no-op if the tag already points at HEAD.
# source is recoverable from git directly). #
# For stable: the original "skip if no source change since latest stable
# tag" guard still applies — pointless re-releases are silently no-op'd.
# For alpha/beta: the auto-incrementing counter already differentiates
# successive cuts, so no skip check.
#
# Requires $CODEBERG_TOKEN exported. publish_codeberg_release surfaces a
# clear error if it isn't.
promote_release() { promote_release() {
_tool="$1" _tool="$1"
_releases_dir="$root_dir/../website/releases"
mkdir -p "$_releases_dir"
if [ "$channel" = "alpha" ] || [ "$channel" = "beta" ]; then case "$channel" in
_dest="${_releases_dir}/${_tool}_${channel}.html" stable)
cp "$output_html" "$_dest" if [ -z "$build_version" ]; then
echo "Released $channel to $_dest" echo "promote_release: stable channel but no build_version" >&2
return 0 exit 1
fi
_latest=$(git -C "$root_dir" tag --list "${_tool}-v*" 2>/dev/null \
| grep -E "^${_tool}-v[0-9]+\.[0-9]+\.[0-9]+\$" \
| sort -V | tail -1)
if [ -n "$_latest" ] && git -C "$root_dir" diff --quiet "$_latest" HEAD -- . ../shared 2>/dev/null; then
echo "${_tool}: no source changes since $_latest — skipping"
return 0
fi
_version="$build_version"
;;
alpha | beta)
_version=$(next_prerelease "$channel" "${_tool}-v")
;;
*)
echo "promote_release: unknown channel '$channel'" >&2
exit 1
;;
esac
_tag="${_tool}-v${_version}"
# Tag the commit (idempotent: skip if already at HEAD).
if git -C "$root_dir" rev-parse -q --verify "refs/tags/$_tag" >/dev/null; then
_existing=$(git -C "$root_dir" rev-list -n 1 "$_tag")
_head=$(git -C "$root_dir" rev-parse HEAD)
if [ "$_existing" != "$_head" ]; then
echo "promote_release: tag $_tag already exists at $_existing, but HEAD is $_head" >&2
return 1
fi
echo "(tag $_tag already at HEAD)"
else
git -C "$root_dir" tag "$_tag"
echo "tagged $_tag"
fi fi
if [ "$channel" != "stable" ] || [ -z "$build_version" ]; then # Upload to Codeberg. The asset name embeds the version so consumers
echo "promote_release: refusing to promote — channel=$channel build_version=$build_version" >&2 # can pin to a specific build (e.g. <tool>_v0.0.3-alpha.1.html).
exit 1 _asset="${_tool}_v${_version}.html"
_staged="$root_dir/$_tool/dist/$_asset"
cp "$output_html" "$_staged"
if ! command -v publish_codeberg_release >/dev/null 2>&1; then
# build-lib.sh is sourced before publish-codeberg-release.sh in the
# canonical wrapper scripts; if the helper isn't loaded yet, bail
# with a clear pointer.
echo "promote_release: publish_codeberg_release not available; source shared/publish-codeberg-release.sh first" >&2
return 1
fi fi
publish_codeberg_release "VARASYS/ZDDC" "$_tag" "$_staged"
rm -f "$_staged"
_latest=$(git -C "$root_dir" tag --list "${_tool}-v*" --sort=-v:refname 2>/dev/null | head -1) echo "Released $_tag (channel: $channel, version: $_version)"
if [ -n "$_latest" ] && git -C "$root_dir" diff --quiet "$_latest" HEAD -- . ../shared 2>/dev/null; then echo " publish git tag with: git push origin $_tag"
echo "${_tool}: no source changes since $_latest — skipping"
return 0
fi
_versioned="${_releases_dir}/${_tool}_v${build_version}.html"
cp "$output_html" "$_versioned"
echo "Released $_versioned"
# Symlink target is relative to its own directory so the link survives
# path moves and works regardless of where the website is mounted.
(cd "$_releases_dir" && ln -sfn "${_tool}_v${build_version}.html" "${_tool}_stable.html")
echo "Updated ${_tool}_stable.html -> ${_tool}_v${build_version}.html"
git -C "$root_dir" tag "${_tool}-v${build_version}"
echo "Tagged ${_tool}-v${build_version} — run: git push --tags"
}
# Mirror the just-built dist file to website/releases/<tool>_alpha.html so
# the website's alpha hyperlinks always serve whatever dist currently holds.
# Plain copy (not symlink): symlinks pointing outside website/ break under
# deployments whose web server only mounts website/ (notably the canonical
# Caddy setup at /etc/containers/systemd/caddy.container, which mounts
# /home/user/src/zddc/website read-only and cannot follow ../ paths to
# landing/dist or archive/dist on the host filesystem).
#
# Trade-off: every dev build that touches a tool's source dirties the
# corresponding _alpha.html file in git. Commit those alongside the source
# change (or `git checkout` them before pushing) since the alpha channel is
# explicitly mutable.
#
# `--release alpha` overwrites the same file with a "alpha · date · sha"
# labeled build; the next plain build clobbers it again. That's the alpha-
# is-mutable contract.
#
# Reads $output_html and $root_dir from caller scope.
update_alpha() {
_tool="$1"
_releases_dir="$root_dir/../website/releases"
_dest="${_releases_dir}/${_tool}_alpha.html"
mkdir -p "$_releases_dir"
# rm first: if the dest is currently a symlink to dist (legacy from the
# earlier symlink approach), `cp` would follow the symlink and try to
# write to the same file it's reading from. Removing first replaces the
# symlink with a plain regular file copy.
rm -f "$_dest"
cp "$output_html" "$_dest"
echo "Mirrored to $_dest"
} }

View file

@ -0,0 +1,167 @@
#!/bin/sh
# publish-codeberg-release.sh — upload assets to a Codeberg release.
#
# Usage:
# publish_codeberg_release <repo> <tag> <asset-path>...
#
# Where:
# <repo> e.g. VARASYS/ZDDC
# <tag> e.g. zddc-server-v0.0.8-alpha.3 or archive-v0.0.3
# <asset-path> one or more files to attach to the release
#
# Prerequisites:
# - $CODEBERG_TOKEN exported in the environment, with scope sufficient
# to create/update releases on the target repo. (Codeberg/Gitea
# terminology: "Application token with `write:repository` access".)
# - curl, jq.
#
# Behavior:
# - If a release for $tag does not exist on Codeberg, create it. The
# prerelease flag is derived from the tag itself: if the version
# part (text after the last 'v') contains a '-', it is a pre-release
# (e.g. 'zddc-server-v0.0.8-alpha.2' → prerelease=true);
# 'zddc-server-v0.0.7' → prerelease=false. Codeberg orders releases
# by published-at and sets a "Latest" badge against the latest
# non-prerelease, so this matters.
# - For each asset: if a same-named asset already exists, delete it
# first (Codeberg/Gitea API doesn't support in-place replacement).
# Then upload the new bytes.
#
# Idempotent: re-running with the same args leaves the release with the
# same set of assets.
#
# This file is meant to be sourced and invoked via the function name, but
# it's also runnable directly as a script for quick testing — when run
# directly (i.e., $0 ends in publish-codeberg-release.sh), the function
# is called with the script's argv.
#
# NOTE: We do NOT `set -eu` at the top, because that would leak into any
# caller that sources this file. The direct-run dispatch at the bottom
# turns -eu on for that path only.
CODEBERG_API="${CODEBERG_API:-https://codeberg.org/api/v1}"
# True iff $1's "version part" (text after last 'v') contains '-'.
# Tags with a '-' in the version part are pre-releases per the
# pre-release-semver scheme (see AGENTS.md "Releasing").
_is_prerelease() {
_ver="${1##*v}"
case "$_ver" in *-*) return 0 ;; *) return 1 ;; esac
}
# Fetch a release by tag. Echoes the numeric release ID, or empty on 404.
# Suppresses 404-on-stderr; other errors propagate.
_get_release_id() {
_repo="$1"
_tag="$2"
_resp=$(curl -fsS -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/repos/$_repo/releases/tags/$_tag" 2>/dev/null) || _resp=""
[ -z "$_resp" ] && return 0
printf '%s' "$_resp" | jq -r '.id // empty'
}
# Create a release for the given tag. Echoes the new release ID. Bombs
# out on any error (the caller relies on stable behavior — releases
# don't get half-created).
_create_release() {
_repo="$1"
_tag="$2"
if _is_prerelease "$_tag"; then
_prerelease=true
else
_prerelease=false
fi
# Inline JSON; tag/name don't contain quotes per our naming rules.
_body=$(printf '{"tag_name":"%s","name":"%s","prerelease":%s,"draft":false}' \
"$_tag" "$_tag" "$_prerelease")
curl -fsS \
-X POST \
-H "Authorization: token $CODEBERG_TOKEN" \
-H "Content-Type: application/json" \
-d "$_body" \
"$CODEBERG_API/repos/$_repo/releases" \
| jq -r '.id'
}
# Echo the asset ID for an asset of the given filename in the given
# release, or empty if no such asset.
_find_asset_id() {
_repo="$1"
_release_id="$2"
_name="$3"
curl -fsS -H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/repos/$_repo/releases/$_release_id" \
| jq -r --arg n "$_name" '.assets[] | select(.name == $n) | .id' \
| head -1
}
_delete_asset() {
_repo="$1"
_asset_id="$2"
curl -fsS -X DELETE \
-H "Authorization: token $CODEBERG_TOKEN" \
"$CODEBERG_API/repos/$_repo/releases/assets/$_asset_id" >/dev/null
}
_upload_asset() {
_repo="$1"
_release_id="$2"
_asset_path="$3"
_name=$(basename "$_asset_path")
# Codeberg/Gitea expects the file under field name "attachment", and
# the desired display name as the ?name= query parameter (otherwise
# the original filename is used; we set both for clarity).
curl -fsS -X POST \
-H "Authorization: token $CODEBERG_TOKEN" \
-F "attachment=@${_asset_path}" \
"$CODEBERG_API/repos/$_repo/releases/$_release_id/assets?name=$(printf '%s' "$_name" | jq -sRr @uri)" \
>/dev/null
}
publish_codeberg_release() {
if [ $# -lt 3 ]; then
echo "usage: publish_codeberg_release <repo> <tag> <asset-path>..." >&2
return 2
fi
if [ -z "${CODEBERG_TOKEN:-}" ]; then
echo "publish_codeberg_release: CODEBERG_TOKEN not set" >&2
return 2
fi
_repo="$1"
_tag="$2"
shift 2
_release_id=$(_get_release_id "$_repo" "$_tag")
if [ -z "$_release_id" ]; then
echo " creating release for $_tag"
_release_id=$(_create_release "$_repo" "$_tag")
if [ -z "$_release_id" ] || [ "$_release_id" = "null" ]; then
echo "publish_codeberg_release: failed to create release for $_tag" >&2
return 1
fi
fi
echo " release id: $_release_id"
for _asset_path do
if [ ! -f "$_asset_path" ]; then
echo "publish_codeberg_release: asset not readable: $_asset_path" >&2
return 1
fi
_name=$(basename "$_asset_path")
_existing=$(_find_asset_id "$_repo" "$_release_id" "$_name")
if [ -n "$_existing" ]; then
echo " replacing existing asset $_name (id $_existing)"
_delete_asset "$_repo" "$_existing"
fi
echo " uploading $_name"
_upload_asset "$_repo" "$_release_id" "$_asset_path"
done
}
# When invoked directly (not sourced), call the function with argv.
case "${0##*/}" in
publish-codeberg-release.sh)
set -eu
publish_codeberg_release "$@"
;;
esac

View file

@ -172,6 +172,4 @@ echo "Wrote $output_html"
if [ "$is_release" = "1" ]; then if [ "$is_release" = "1" ]; then
promote_release "transmittal" promote_release "transmittal"
else
update_alpha "transmittal"
fi fi

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the alpha default // (none) uses the alpha default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'archive'; const tool = 'archive';
const defaultChannel = 'alpha'; const defaultChannel = 'alpha';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the alpha default // (none) uses the alpha default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'classifier'; const tool = 'classifier';
const defaultChannel = 'alpha'; const defaultChannel = 'alpha';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the alpha default // (none) uses the alpha default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'landing'; const tool = 'landing';
const defaultChannel = 'alpha'; const defaultChannel = 'alpha';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the alpha default // (none) uses the alpha default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'mdedit'; const tool = 'mdedit';
const defaultChannel = 'alpha'; const defaultChannel = 'alpha';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the alpha default // (none) uses the alpha default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'transmittal'; const tool = 'transmittal';
const defaultChannel = 'alpha'; const defaultChannel = 'alpha';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the beta default // (none) uses the beta default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'archive'; const tool = 'archive';
const defaultChannel = 'beta'; const defaultChannel = 'beta';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the beta default // (none) uses the beta default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'classifier'; const tool = 'classifier';
const defaultChannel = 'beta'; const defaultChannel = 'beta';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the beta default // (none) uses the beta default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'landing'; const tool = 'landing';
const defaultChannel = 'beta'; const defaultChannel = 'beta';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the beta default // (none) uses the beta default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'mdedit'; const tool = 'mdedit';
const defaultChannel = 'beta'; const defaultChannel = 'beta';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the beta default // (none) uses the beta default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'transmittal'; const tool = 'transmittal';
const defaultChannel = 'beta'; const defaultChannel = 'beta';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the stable default // (none) uses the stable default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'archive'; const tool = 'archive';
const defaultChannel = 'stable'; const defaultChannel = 'stable';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the stable default // (none) uses the stable default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'classifier'; const tool = 'classifier';
const defaultChannel = 'stable'; const defaultChannel = 'stable';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the stable default // (none) uses the stable default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'landing'; const tool = 'landing';
const defaultChannel = 'stable'; const defaultChannel = 'stable';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the stable default // (none) uses the stable default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'mdedit'; const tool = 'mdedit';
const defaultChannel = 'stable'; const defaultChannel = 'stable';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -18,32 +18,51 @@
// ?v=0.0.4 (or v0.0.4) pins to that exact stable version // ?v=0.0.4 (or v0.0.4) pins to that exact stable version
// (none) uses the stable default // (none) uses the stable default
// //
// Requires zddc.varasys.io to serve Access-Control-Allow-Origin: *. // Resolution path:
// 1. Channel mode (no ?v= or ?v={alpha,beta,stable}): fetch
// manifest.json from the same origin, look up
// "<tool>-<channel>" → tag, then fetch the asset.
// 2. Version pin: build the URL directly (skip manifest).
//
// The asset URL is /releases/<tag>/<tool>_v<version>.html. Caddy on
// zddc.varasys.io reverse-proxies that to the Codeberg release-asset
// URL — neither this stub nor the user's browser hits codeberg.org
// directly.
(async function () { (async function () {
const params = new URLSearchParams(location.search); const params = new URLSearchParams(location.search);
const v = params.get('v'); const v = params.get('v');
const tool = 'transmittal'; const tool = 'transmittal';
const defaultChannel = 'stable'; const defaultChannel = 'stable';
const channels = { alpha: '_alpha', beta: '_beta', stable: '_stable' }; const channels = ['alpha', 'beta', 'stable'];
const base = 'https://zddc.varasys.io/releases/';
function suffixFor(value) { function isChannel(s) { return channels.indexOf(s) >= 0; }
if (!value) return '_' + defaultChannel;
if (value in channels) return channels[value];
const ver = value.startsWith('v') ? value.slice(1) : value;
return '_v' + ver;
}
const upstream = 'https://zddc.varasys.io/releases/' + tool + suffixFor(v) + '.html';
try { try {
const resp = await fetch(upstream, { cache: 'no-cache', credentials: 'omit' }); let assetUrl;
if (v && !isChannel(v)) {
// Explicit version pin (e.g. ?v=0.0.4 or ?v=v0.0.4).
const ver = v.replace(/^v/, '');
const tag = tool + '-v' + ver;
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
} else {
// Channel mode: resolve via manifest.
const channel = (v && isChannel(v)) ? v : defaultChannel;
const manifest = await (await fetch(base + 'manifest.json', { cache: 'no-cache', credentials: 'omit' })).json();
const tag = manifest[tool + '-' + channel];
if (!tag) throw new Error('manifest has no entry for ' + tool + '-' + channel);
const ver = tag.replace(tool + '-v', '');
assetUrl = base + tag + '/' + tool + '_v' + ver + '.html';
}
const resp = await fetch(assetUrl, { cache: 'no-cache', credentials: 'omit' });
if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText); if (!resp.ok) throw new Error(resp.status + ' ' + resp.statusText);
const html = await resp.text(); const html = await resp.text();
document.open(); document.open();
document.write(html); document.write(html);
document.close(); document.close();
} catch (err) { } catch (err) {
document.body.textContent = 'Failed to load from ' + upstream + ': ' + err.message; document.body.textContent = 'Failed to load ' + tool + ': ' + err.message;
} }
})(); })();
</script> </script>

View file

@ -1,107 +0,0 @@
# syntax=docker/dockerfile:1
#
# Multi-stage build with three useful targets:
#
# --target binaries — scratch image holding cross-compiled binaries.
# Use `podman build --target binaries -o dist/ .`
# to extract zddc-server-{linux,darwin,windows}-*
# to the host. No image published from this stage.
#
# --target server — alpine-based runtime. Default target. Published
# as codeberg.org/varasys/zddc-server:vX.Y.Z.
#
# Build context expectations (when targeting `server`):
# dist/web/index.html and dist/web/archive.html must exist —
# produced by `sh build.sh` from the repo root. The release flow
# (`release-image.sh` at the repo root) runs build.sh first and
# then invokes `podman build --target server`.
#
# ─── Stage 1: build ──────────────────────────────────────────────────────────
FROM docker.io/library/golang:1.24-alpine AS builder
WORKDIR /src
# git is required by go mod for VCS dependencies
RUN apk add --no-cache git
# Skip sum DB checks (allows building with empty/partial go.sum)
ENV GONOSUMDB=* GOPRIVATE=* GOPROXY=direct
# Copy source
COPY . .
# Build linux/amd64 (used by the runtime image and Linux hosts)
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
-o /out/zddc-server-linux-amd64 ./cmd/zddc-server
# Cross-compile for macOS (Intel and Apple Silicon)
RUN CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
-o /out/zddc-server-darwin-amd64 ./cmd/zddc-server
RUN CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -trimpath -ldflags="-s -w" \
-o /out/zddc-server-darwin-arm64 ./cmd/zddc-server
# Cross-compile for Windows
RUN CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags="-s -w" \
-o /out/zddc-server-windows-amd64.exe ./cmd/zddc-server
# ─── Stage 2: export binaries ─────────────────────────────────────────────────
# Use `podman build --target binaries -o dist/ .` to extract binaries to the host.
# No base image needed — this stage only exists to hold the output files.
FROM scratch AS binaries
COPY --from=builder /out/ /
# ─── Stage 3: runtime (published image) ─────────────────────────────────────
FROM docker.io/library/alpine:3.20 AS server
LABEL org.opencontainers.image.title="zddc-server" \
org.opencontainers.image.description="HTTP server for ZDDC archives — ACL via .zddc files, virtual archive index, audit logging" \
org.opencontainers.image.source="https://codeberg.org/VARASYS/ZDDC" \
org.opencontainers.image.documentation="https://zddc.varasys.io/zddc-server.html" \
org.opencontainers.image.licenses="AGPL-3.0-only" \
org.opencontainers.image.vendor="VARASYS"
# wget is in the base image (busybox); explicitly install ca-certificates
# (outbound HTTPS for any future upstream auth check) and tini (PID-1
# orphan reaper + signal forwarder). zddc-server itself only spawns
# subprocesses transitively — e.g. once a future render path shells out
# to pandoc, which in turn shells out to xelatex / lua filters / dot —
# and any of those grandchildren orphaned by a mid-run crash get
# reparented to PID 1. Without an init that knows to wait(2) on them,
# they accumulate as zombies. tini is ~24KB and does exactly this.
RUN apk add --no-cache ca-certificates tini && rm -rf /var/cache/apk/*
# Non-root user. UID/GID are deliberately fixed so volume permissions are
# predictable across hosts.
RUN addgroup -S -g 1000 zddc && adduser -S -u 1000 -G zddc zddc
# Binary
COPY --from=builder /out/zddc-server-linux-amd64 /usr/local/bin/zddc-server
# Bundled landing + archive tools — useful for self-contained demos and as
# a fallback web root. Set ZDDC_ROOT=/opt/zddc-server/web to serve only
# these (no external data). For real archives, mount the data tree at
# /srv (the default ZDDC_ROOT below).
COPY dist/web/index.html /opt/zddc-server/web/index.html
COPY dist/web/archive.html /opt/zddc-server/web/archive.html
# Conventional mount point for the served archive. Operators mount their
# data here (Azure Files, NFS, hostPath, …). Override with ZDDC_ROOT.
VOLUME /srv
USER zddc
# Default config: data mount at /srv. Override at run time as needed.
# Other env vars (ZDDC_TLS_CERT, ZDDC_EMAIL_HEADER, ZDDC_CORS_ORIGIN, …)
# are intentionally not defaulted — see zddc/README.md.
ENV ZDDC_ROOT=/srv
EXPOSE 8443
# Liveness probe for `docker run` users. Kubernetes deployments override
# this with their own livenessProbe / readinessProbe.
HEALTHCHECK --interval=30s --timeout=5s --start-period=5s --retries=3 \
CMD wget --no-check-certificate -q --spider https://localhost:8443/ || exit 1
ENTRYPOINT ["/sbin/tini", "--", "/usr/local/bin/zddc-server"]

View file

@ -1,33 +0,0 @@
# podman-compose.yaml — local development environment
# Usage: podman-compose up --build
#
# Requires a directory to serve. Set ZDDC_DATA_DIR to the path of your
# archive root, or mount it manually.
#
# Note: Use :z volume suffix for SELinux-enabled hosts (RHEL, Fedora, CentOS).
services:
zddc-server:
build:
context: .
dockerfile: Containerfile
image: zddc-server:dev
ports:
- "8443:8443"
# TLS modes (set via ZDDC_TLS_CERT):
# empty (default) — self-signed cert generated in memory at startup
# ZDDC_TLS_CERT=none — plain HTTP, no TLS (use when reverse proxy handles TLS)
# ZDDC_TLS_CERT=/path — load cert from file (also set ZDDC_TLS_KEY)
environment:
ZDDC_ROOT: /data
ZDDC_ADDR: ":8443"
ZDDC_LOG_LEVEL: debug
ZDDC_INDEX_PATH: .archive
# ZDDC_EMAIL_HEADER defaults to X-Auth-Request-Email — uncomment to override.
# ZDDC_EMAIL_HEADER: X-Auth-Request-Email
volumes:
- type: bind
source: ${ZDDC_DATA_DIR:-./testdata}
target: /data
options: z
restart: unless-stopped

186
zddc/release.sh Executable file
View file

@ -0,0 +1,186 @@
#!/bin/sh
# release.sh — cut a zddc-server release: tag, compile binaries,
# publish them as assets to a Codeberg release.
#
# Usage:
# sh zddc/release.sh # alpha cut (default), version auto-derived
# sh zddc/release.sh alpha # same
# sh zddc/release.sh beta # beta cut, version auto-derived
# sh zddc/release.sh stable # stable cut, patch++ from latest stable
# sh zddc/release.sh stable 0.1.0 # stable cut, explicit version
#
# What this is NOT: there's no container image build, no registry push.
# Those went away when the upstream codeberg.org/varasys/zddc-server
# image lost its only consumers (tnd-zddc-chart's two Dockerfiles now
# compile from source directly, fetching this Codeberg release tarball
# / binary). See AGENTS.md "Releasing" for the full flow.
#
# Prerequisites:
# - Go 1.24+ on PATH.
# - $CODEBERG_TOKEN exported, scoped to write the VARASYS/ZDDC repo.
# - curl, jq, git.
#
# What it does:
# 1. Determine version per the pre-release semver scheme:
# alpha/beta → next_prerelease (vX.Y.Z-CHANNEL.N from latest stable)
# stable → operator-supplied or patch-bumped from latest stable
# 2. Tag the current commit zddc-server-v<version>.
# 3. Cross-compile binaries (linux/darwin/windows × amd64/arm64)
# into zddc/dist/zddc-server-<os>-<arch>[.exe]. Native Go preferred.
# 4. Upload each binary as an asset to the new Codeberg release.
# 5. Print the operator's next steps (push the tag).
#
# The script does NOT push the tag itself — that's a deliberate `git push`
# you do after reviewing.
set -eu
usage() {
cat >&2 <<'EOF'
usage: release.sh [alpha|beta|stable] [<version>]
alpha (default) cut alpha. Auto-derive version from the latest clean
zddc-server-vX.Y.Z tag plus next-patch + -alpha.N.
beta cut beta. Auto-derive version (vX.Y.Z + -beta.N).
stable cut stable. Without <version>, patch-bump from the
latest clean stable tag. With <version>, use it
verbatim (must be a clean X.Y.Z).
EOF
exit 1
}
CHANNEL="${1:-alpha}"
case "$CHANNEL" in
alpha | beta | stable) ;;
-h | --help) usage ;;
*) echo "error: unknown channel '$CHANNEL'" >&2; usage ;;
esac
EXPLICIT_VERSION="${2:-}"
if [ -n "$EXPLICIT_VERSION" ] && [ "$CHANNEL" != "stable" ]; then
echo "error: an explicit <version> is only valid with the 'stable' channel" >&2
echo " alpha and beta versions are auto-derived." >&2
exit 1
fi
if [ -z "${CODEBERG_TOKEN:-}" ]; then
echo "error: CODEBERG_TOKEN must be exported in the environment" >&2
echo " (Codeberg user → Settings → Applications → generate a token" >&2
echo " with scope sufficient to create releases on VARASYS/ZDDC.)" >&2
exit 1
fi
SCRIPT_DIR=$(cd "$(dirname "$0")/.." && pwd)
TAG_PREFIX="zddc-server-v"
REPO="VARASYS/ZDDC"
# Source build-lib.sh for next_prerelease + _validate_semver. It
# requires root_dir set; pointing at the repo root works.
root_dir="$SCRIPT_DIR"
. "$SCRIPT_DIR/shared/build-lib.sh"
. "$SCRIPT_DIR/shared/publish-codeberg-release.sh"
# --- Determine version -----------------------------------------------------
case "$CHANNEL" in
alpha | beta)
VERSION=$(next_prerelease "$CHANNEL" "$TAG_PREFIX")
;;
stable)
if [ -n "$EXPLICIT_VERSION" ]; then
_validate_semver "$EXPLICIT_VERSION"
VERSION="$EXPLICIT_VERSION"
else
_latest=$(git -C "$SCRIPT_DIR" tag --list "${TAG_PREFIX}*" 2>/dev/null \
| grep -E "^${TAG_PREFIX}[0-9]+\.[0-9]+\.[0-9]+\$" \
| sed "s|^${TAG_PREFIX}||" \
| sort -V \
| tail -1)
[ -n "$_latest" ] || _latest="0.0.0"
_major="${_latest%%.*}"
_rest="${_latest#*.}"
_minor="${_rest%%.*}"
_patch="${_rest#*.}"
VERSION="${_major}.${_minor}.$((_patch + 1))"
fi
;;
esac
GIT_TAG="${TAG_PREFIX}${VERSION}"
echo "=== zddc-server release ==="
echo "Channel: $CHANNEL"
echo "Version: $VERSION"
echo "Git tag: $GIT_TAG"
echo
# --- Tag the commit (idempotent: skip if the tag already points here) -----
if git -C "$SCRIPT_DIR" rev-parse -q --verify "refs/tags/$GIT_TAG" >/dev/null; then
_existing=$(git -C "$SCRIPT_DIR" rev-list -n 1 "$GIT_TAG")
_head=$(git -C "$SCRIPT_DIR" rev-parse HEAD)
if [ "$_existing" != "$_head" ]; then
echo "error: tag $GIT_TAG already exists at $_existing, but HEAD is $_head" >&2
echo " refusing to overwrite. Resolve manually." >&2
exit 1
fi
echo "(tag $GIT_TAG already at HEAD)"
else
git -C "$SCRIPT_DIR" tag "$GIT_TAG"
echo "tagged $GIT_TAG"
fi
# --- Cross-compile binaries (native Go) ------------------------------------
if ! command -v go >/dev/null 2>&1; then
echo "error: go not found on PATH" >&2
echo " (install Go 1.24+, or run this script from inside a Go" >&2
echo " container — there's no podman fallback anymore.)" >&2
exit 1
fi
DIST="$SCRIPT_DIR/zddc/dist"
mkdir -p "$DIST"
echo
echo "=== Cross-compiling ==="
cd "$SCRIPT_DIR/zddc"
LDFLAGS="-s -w -X main.version=${VERSION}"
for target in linux/amd64 darwin/amd64 darwin/arm64 windows/amd64; do
os="${target%/*}"
arch="${target#*/}"
out="zddc-server-${os}-${arch}"
case "$os" in windows) out="${out}.exe" ;; esac
echo " building $out"
CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" \
go build -trimpath -ldflags="$LDFLAGS" -o "$DIST/$out" ./cmd/zddc-server
done
cd "$SCRIPT_DIR"
# --- Publish to Codeberg ---------------------------------------------------
echo
echo "=== Publishing to Codeberg release $GIT_TAG ==="
publish_codeberg_release "$REPO" "$GIT_TAG" \
"$DIST/zddc-server-linux-amd64" \
"$DIST/zddc-server-darwin-amd64" \
"$DIST/zddc-server-darwin-arm64" \
"$DIST/zddc-server-windows-amd64.exe"
echo
echo "=== Done ==="
echo "Release: https://codeberg.org/$REPO/releases/tag/$GIT_TAG"
echo "Git tag: $GIT_TAG (publish with: git push origin $GIT_TAG)"
echo
case "$CHANNEL" in
stable)
echo "Reminder (channel discipline rule 4): freshen alpha + beta now"
echo "so the floating channels resolve to at-least-current code:"
echo " ./freshen-channel zddc-server alpha"
echo " ./freshen-channel zddc-server beta"
;;
beta)
echo "Beta cut. Soak before promoting to stable."
;;
esac
echo
echo "Don't forget to also regenerate the website releases page:"
echo " sh build.sh"
echo " git add website/releases/index.html website/releases/manifest.json"
echo " git commit && git push"