#!/usr/bin/env bash # Root-owned publish step for storebound desktop releases (install as /usr/local/sbin/storebound-desktop-publish, # root:root 755; see docs/desktop-release.md). tools/desktop-release.ts publish uploads one version folder to # /var/lib/storebound-desktop/incoming// and then runs: storebound-desktop-publish [stable|beta|rehearsal] # (rehearsal: tools/release.ts --rehearsal, a throwaway prerelease at /desktop/rehearsal/latest.json that no app reads) # # It checks the upload (regular files with plain names only, SHA256SUMS, latest.json of that version whose every URL # points at an uploaded file of /desktop/dl//), copies the files into /var/www/storebound-desktop/dl// # (a new version: a temp folder renamed into place; an existing version: only new files, a file already published is # never replaced by different bytes), writes the version's own dl//latest.json (what `storebound desktop` of # the same CLI version installs), and only then swaps latest.json of the channel atomically (temp file + rename). # Apache serves that folder under https://storebound.dev/desktop/ (tools/vds/apache-storebound-desktop.conf), apart # from the site's own rsync, which deletes what is not in the repository. Nothing from the upload is executed. set -euo pipefail umask 022 VERSION="${1:-}" CHANNEL="${2:-stable}" INCOMING=/var/lib/storebound-desktop/incoming ROOT=/var/www/storebound-desktop SITE=https://storebound.dev/desktop die() { echo "storebound-desktop-publish: $*" >&2; exit 1; } [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.]+)?$ ]] || die "usage: storebound-desktop-publish [stable|beta|rehearsal]" [[ "$CHANNEL" == stable || "$CHANNEL" == beta || "$CHANNEL" == rehearsal ]] || die "channel: stable, beta or rehearsal" # a stable or beta version is never a rehearsal, and a rehearsal never reaches stable or beta if [[ "$VERSION" == *-rehearsal.* ]]; then [ "$CHANNEL" = rehearsal ] || die "a rehearsal version goes to the rehearsal channel only"; else [ "$CHANNEL" != rehearsal ] || die "the rehearsal channel takes rehearsal versions only"; fi IN="$INCOMING/$VERSION" DL="$ROOT/dl/$VERSION" [ -d "$IN" ] && [ ! -L "$IN" ] || die "nothing uploaded at $IN" # only regular files with plain names, nothing hidden, no folders, no links if find "$IN" -mindepth 1 \( ! -type f -o -name '.*' \) -print | grep -q .; then die "the upload holds something other than plain files"; fi if find "$IN" -mindepth 1 -type f -printf '%f\n' | grep -Ev '^[A-Za-z0-9][A-Za-z0-9._-]*$' | grep -q .; then die "a file name has unexpected characters"; fi [ -f "$IN/latest.json" ] && [ -f "$IN/SHA256SUMS" ] || die "latest.json and SHA256SUMS are required" (cd "$IN" && sha256sum --quiet --strict -c SHA256SUMS) || die "SHA256SUMS does not match the files" python3 - "$IN" "$VERSION" "$SITE" <<'PY' || die "latest.json does not describe this upload" import json, os, re, sys src, version, site = sys.argv[1], sys.argv[2], sys.argv[3] m = json.load(open(os.path.join(src, "latest.json"))) assert m.get("version") == version, "version" files = set(os.listdir(src)) listed = {l.split()[1] for l in open(os.path.join(src, "SHA256SUMS")) if l.strip()} assert files - {"SHA256SUMS"} <= listed, "files missing from SHA256SUMS" prefix = f"{site}/dl/{version}/" entries = list((m.get("platforms") or {}).values()) + list((m.get("downloads") or {}).values()) assert entries, "no platforms" for e in entries: url = e.get("url", "") assert url.startswith(prefix), url name = url[len(prefix):] assert re.fullmatch(r"[A-Za-z0-9][A-Za-z0-9._-]*", name) and name in files, url assert e.get("signature"), url PY install -d -m 755 "$ROOT" "$ROOT/dl" "$ROOT/beta" "$ROOT/rehearsal" if [ ! -e "$DL" ]; then TMP="$(mktemp -d "$ROOT/dl/.tmp-$VERSION.XXXXXX")" for f in "$IN"/*; do n="$(basename "$f")"; [ "$n" = latest.json ] || install -m 644 "$f" "$TMP/$n"; done chmod 755 "$TMP" mv -T "$TMP" "$DL" else for f in "$IN"/*; do n="$(basename "$f")" [ "$n" = latest.json ] && continue if [ -e "$DL/$n" ]; then [ "$n" = SHA256SUMS ] && continue cmp -s "$f" "$DL/$n" || die "$n is already published with other bytes: a published file is never replaced (bump the version)" else install -m 644 "$f" "$DL/.$n.tmp" && mv -f "$DL/.$n.tmp" "$DL/$n" fi done install -m 644 "$IN/SHA256SUMS" "$DL/.SHA256SUMS.tmp" && mv -f "$DL/.SHA256SUMS.tmp" "$DL/SHA256SUMS" fi # the version's own manifest: dl//latest.json. `storebound desktop` of CLI installs exactly that # version from it (the CLI and the app share one version number); it may be updated when installers are added install -m 644 "$IN/latest.json" "$DL/.latest.json.tmp" && mv -f "$DL/.latest.json.tmp" "$DL/latest.json" # latest.json last: installed apps see the new version only once every file it names is in place if [ "$CHANNEL" = stable ]; then LJ="$ROOT/latest.json"; URL="$SITE/latest.json"; else LJ="$ROOT/$CHANNEL/latest.json"; URL="$SITE/$CHANNEL/latest.json"; fi install -m 644 "$IN/latest.json" "$LJ.tmp" mv -f "$LJ.tmp" "$LJ" rm -rf "$IN" curl -fsS -o /dev/null --resolve storebound.dev:443:127.0.0.1 "$URL" && echo "storebound desktop $VERSION published ($CHANNEL): $URL"