ZDDC/shared/publish-codeberg-release.sh
ZDDC 2dc9ad240c 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>
2026-04-29 21:18:26 -05:00

167 lines
5.8 KiB
Bash
Executable file

#!/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