The catalog page at the release host only listed add-ons from the signed index, and every current app ships inside LibrePortal itself, so a first release would have shown an empty catalog. make_release.sh now also writes the release's app list (apps.json, plus icons) next to latest.json, chosen the same way as the website's app grid. The page shows those apps as "Built in" with a `libreportal app install` command, and add-ons from the index as before. A built-in app hides an add-on with the same name, as it does on a box. The list is display data only: boxes never read it. The Submissions badge no longer shows as an empty dot when there are no submissions. Assisted-by: Claude Opus 5 <noreply@anthropic.com>
200 lines
9.2 KiB
Bash
Executable File
200 lines
9.2 KiB
Bash
Executable File
#!/bin/bash
|
|
#
|
|
# Build a versioned, checksum-verified LibrePortal release artifact.
|
|
#
|
|
# The stable install fetches a release tarball over plain HTTPS (no git, no auth),
|
|
# so it's reproducible and version-pinned. This builds that artifact from the
|
|
# COMMITTED tree via `git archive`, which honours the `export-ignore` rules in
|
|
# .gitattributes — so dev-only trees (scripts/unused, site, .claude, …) never ship.
|
|
#
|
|
# Signing is required. Every install carries the real public key and refuses an
|
|
# unsigned release, so this fails closed unless LP_MINISIGN_SECKEY is set. The
|
|
# output lands in dist/<channel>/ laid out exactly like the host serves it:
|
|
# libreportal-<v>.tar.gz(.sha256,.minisig) the release
|
|
# latest.json the channel pointer boxes follow
|
|
# apps.json + icons/ the built-in apps, for the catalog page
|
|
#
|
|
# For a local test without the key, pass --unsigned. That build goes to
|
|
# dist-unsigned/<channel>/ (never dist/), and the install needs --no-verify-signature:
|
|
# ( cd dist-unsigned && python3 -m http.server 8000 )
|
|
# LP_RELEASE_BASE_URL=http://localhost:8000 ./install.sh --no-verify-signature ...
|
|
#
|
|
# Usage: scripts/release/make_release.sh [--unsigned] [channel] [git-ref]
|
|
# channel stable (default) | edge
|
|
# git-ref HEAD (default) | a tag/commit to build from
|
|
set -euo pipefail
|
|
|
|
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
|
cd "$REPO_ROOT"
|
|
source "$REPO_ROOT/scripts/release/lib/release_index.sh"
|
|
|
|
ARGS=()
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
--unsigned) export LP_RELEASE_UNSIGNED=1 ;;
|
|
-*) echo "make_release: unknown option $arg" >&2; exit 1 ;;
|
|
*) ARGS+=("$arg") ;;
|
|
esac
|
|
done
|
|
CHANNEL="${ARGS[0]:-stable}"
|
|
REF="${ARGS[1]:-HEAD}"
|
|
[[ "$CHANNEL" =~ ^[a-z0-9_-]+$ ]] || { echo "make_release: bad channel name '$CHANNEL'" >&2; exit 1; }
|
|
|
|
releaseRequireSigning make_release
|
|
command -v jq >/dev/null 2>&1 || { echo "make_release: jq is required" >&2; exit 1; }
|
|
|
|
VERSION="$(tr -d ' \t\n\r' < VERSION 2>/dev/null || true)"
|
|
[[ -n "$VERSION" ]] || { echo "make_release: VERSION file is empty or missing" >&2; exit 1; }
|
|
|
|
# Root-owned-footprint version (helpers/wrapper/unit/sudoers). Published in the
|
|
# manifest so the updater can detect when an update needs a root re-install.
|
|
FOOTPRINT_VERSION="$(grep -oE '^footprint_version=[0-9]+' init.sh | head -1 | cut -d= -f2)"
|
|
[[ -n "$FOOTPRINT_VERSION" ]] || FOOTPRINT_VERSION=0
|
|
|
|
# Guard: a release must never ship stale source arrays. Regenerate them; if that
|
|
# changes anything not committed, the tree was stale — abort and tell the dev to
|
|
# commit, so `git archive` (committed files only) can't bake in a mismatch. Only
|
|
# enforced when building HEAD (an explicit old tag/ref is taken as-is).
|
|
if [[ "$REF" == "HEAD" && -x scripts/source/files/generate_arrays.sh ]]; then
|
|
scripts/source/files/generate_arrays.sh run >/dev/null 2>&1 || true
|
|
if ! git diff --quiet -- scripts/source/files/arrays 2>/dev/null; then
|
|
echo "make_release: source arrays were stale — regenerated them now." >&2
|
|
echo " Commit the changes under scripts/source/files/arrays/ and re-run." >&2
|
|
exit 1
|
|
fi
|
|
fi
|
|
|
|
TARBALL="libreportal-${VERSION}.tar.gz"
|
|
PREFIX="libreportal-${VERSION}/" # top-level dir inside the tarball
|
|
OUT="$(releaseDistDir)/$CHANNEL"
|
|
mkdir -p "$OUT"
|
|
|
|
# A published version is immutable: the host serves tarballs with a one-year
|
|
# immutable cache, and boxes pin its sha256. Rebuilding the same version would
|
|
# swap the bytes under everyone, so a signed build refuses; bump VERSION instead.
|
|
# Unsigned test builds may overwrite (they never leave this machine).
|
|
if ! releaseUnsigned && [[ -e "$OUT/$TARBALL" ]]; then
|
|
echo "make_release: $OUT/$TARBALL already exists — bump VERSION for a new release." >&2
|
|
exit 1
|
|
fi
|
|
|
|
echo "Building $TARBALL (channel=$CHANNEL, ref=$REF) ..."
|
|
|
|
# Build into a staging tree (not a streamed tarball) so we can drop a per-file
|
|
# integrity manifest INSIDE the release. `git archive --format=tar` still honours
|
|
# .gitattributes export-ignore, so dev-only trees stay out of the staging tree.
|
|
STAGE="$(mktemp -d)"
|
|
trap 'rm -rf "$STAGE"' EXIT
|
|
git archive --format=tar --prefix="$PREFIX" "$REF" | tar -x -C "$STAGE"
|
|
|
|
# SHA256SUMS: one `sha256sum`-format line per shipped file (relative ./paths,
|
|
# stable sort), excluding the manifest + its signature themselves. The running
|
|
# install re-hashes against this to prove its files match the signed release
|
|
# (see lpVerifyInstall in scripts/source/verify.sh). The manifest's own trust
|
|
# comes from SHA256SUMS.minisig below, not from being in the list.
|
|
(
|
|
cd "$STAGE/$PREFIX"
|
|
find . -type f ! -name SHA256SUMS ! -name SHA256SUMS.minisig -print0 \
|
|
| LC_ALL=C sort -z | xargs -0 sha256sum > SHA256SUMS
|
|
)
|
|
MANIFEST_FILES="$(wc -l < "$STAGE/$PREFIX/SHA256SUMS" | tr -d ' ')"
|
|
|
|
# Sign the manifest with the same offline minisign key used for the tarball, so
|
|
# the install can verify it against the root-owned public key. Keep
|
|
# LP_MINISIGN_SECKEY on the release machine only.
|
|
releaseSignIfKeyed "$STAGE/$PREFIX/SHA256SUMS" "libreportal $VERSION manifest ($CHANNEL)"
|
|
MANIFEST_SIGNED=" SHA256SUMS: $(releaseSignedNote) (inside tarball)"
|
|
|
|
# Everything is built and signed in BUILD first and moved into OUT at the end,
|
|
# latest.json last. A failure part-way (a wrong key, a full disk) then leaves the
|
|
# channel exactly as it was, instead of a latest.json naming a tarball that has
|
|
# no signature yet.
|
|
BUILD="$STAGE/.out"
|
|
mkdir -p "$BUILD"
|
|
|
|
# Pack the staging tree (manifest + signature included). --sort/--owner/--group
|
|
# keep the archive deterministic for a given commit.
|
|
tar --sort=name --owner=0 --group=0 --numeric-owner -czf "$BUILD/$TARBALL" -C "$STAGE" "$PREFIX"
|
|
|
|
( cd "$BUILD" && sha256sum "$TARBALL" > "$TARBALL.sha256" )
|
|
SHA="$(cut -d' ' -f1 < "$BUILD/$TARBALL.sha256")"
|
|
|
|
# apps.json + icons/: the apps BUILT INTO this release, for the catalog page at
|
|
# the release host (containers/libreportal_catalog), which otherwise only knows
|
|
# the add-ons in index.json. Display data only: boxes never read it, and the
|
|
# page trusts nothing it shows (the tarball's signature covers the real thing).
|
|
# Same selection as the website's app grid: a TITLE, not DEV_ONLY, not an
|
|
# instance, not the WebUI itself. Metadata is parsed line-wise, never sourced.
|
|
mkdir -p "$BUILD/icons"
|
|
APPS_JSON='[]'
|
|
for cfg in "$STAGE/$PREFIX"containers/*/*.config; do
|
|
slug="$(basename "$(dirname "$cfg")")"
|
|
[[ "$cfg" == */"$slug/$slug.config" && "$slug" != "libreportal" ]] || continue
|
|
title=""; category=""; description=""; long_description=""; skip=0
|
|
while IFS='=' read -r key val || [[ -n "$key" ]]; do
|
|
[[ "$key" == "CFG_${slug^^}_"* ]] || continue
|
|
val="${val%$'\r'}"
|
|
if [[ "$val" == \"* ]]; then val="${val#\"}"; val="${val%%\"*}"
|
|
else val="${val%%[[:space:]]#*}"; val="${val%"${val##*[![:space:]]}"}"; fi
|
|
case "${key#CFG_"${slug^^}"_}" in
|
|
TITLE) title="$val" ;;
|
|
CATEGORY) category="${val%%,*}" ;;
|
|
DESCRIPTION) description="$val" ;;
|
|
LONG_DESCRIPTION) long_description="$val" ;;
|
|
DEV_ONLY) [[ "${val,,}" == "true" ]] && skip=1 ;;
|
|
INSTANCE_OF) [[ -n "$val" ]] && skip=1 ;;
|
|
esac
|
|
done < "$cfg"
|
|
[[ -n "$title" && "$skip" == 0 ]] || continue
|
|
icon=""
|
|
for ext in svg png; do
|
|
if [[ -f "$(dirname "$cfg")/$slug.$ext" ]]; then
|
|
cp "$(dirname "$cfg")/$slug.$ext" "$BUILD/icons/$slug.$ext"
|
|
icon="$CHANNEL/icons/$slug.$ext"
|
|
break
|
|
fi
|
|
done
|
|
APPS_JSON="$(jq -c --arg slug "$slug" --arg title "$title" --arg category "${category,,}" \
|
|
--arg description "$description" --arg long_description "$long_description" --arg icon "$icon" \
|
|
'. + [{slug:$slug, title:$title, category:$category, description:$description,
|
|
long_description:$long_description} + (if $icon != "" then {icon:$icon} else {} end)]' <<<"$APPS_JSON")"
|
|
done
|
|
jq --arg version "$VERSION" --arg channel "$CHANNEL" \
|
|
'{schema:1, version:$version, channel:$channel, apps:(sort_by(.title | ascii_downcase))}' \
|
|
<<<"$APPS_JSON" > "$BUILD/apps.json"
|
|
APP_COUNT="$(jq '.apps | length' "$BUILD/apps.json")"
|
|
|
|
cat > "$BUILD/latest.json" <<EOF
|
|
{
|
|
"version": "$VERSION",
|
|
"channel": "$CHANNEL",
|
|
"url": "$TARBALL",
|
|
"sha256": "$SHA",
|
|
"footprint_version": $FOOTPRINT_VERSION,
|
|
"notes": ""
|
|
}
|
|
EOF
|
|
|
|
# Sign the tarball (and check the signature against libreportal.pub, the key
|
|
# install.sh verifies with). Keep LP_MINISIGN_SECKEY OFFLINE, on the release
|
|
# machine only. Produces <tarball>.minisig.
|
|
releaseSignIfKeyed "$BUILD/$TARBALL" "libreportal $VERSION ($CHANNEL)"
|
|
SIGNED=" tarball: $(releaseSignedNote)"
|
|
|
|
# Into place: the immutable files first, the channel pointer last.
|
|
mv -f "$BUILD/$TARBALL" "$OUT/$TARBALL"
|
|
[[ -f "$BUILD/$TARBALL.minisig" ]] && mv -f "$BUILD/$TARBALL.minisig" "$OUT/$TARBALL.minisig"
|
|
mv -f "$BUILD/$TARBALL.sha256" "$OUT/$TARBALL.sha256"
|
|
mkdir -p "$OUT/icons"
|
|
cp -rf "$BUILD/icons/." "$OUT/icons/"
|
|
mv -f "$BUILD/apps.json" "$OUT/apps.json"
|
|
mv -f "$BUILD/latest.json" "$OUT/latest.json"
|
|
|
|
echo "✓ $OUT/$TARBALL"
|
|
echo "✓ $OUT/$TARBALL.sha256 ($SHA)"
|
|
echo "✓ $OUT/apps.json ($APP_COUNT built-in apps, for the catalog page)"
|
|
echo "✓ $OUT/latest.json"
|
|
echo "$SIGNED"
|
|
echo " ✓ SHA256SUMS ($MANIFEST_FILES files, inside tarball)"
|
|
echo "$MANIFEST_SIGNED"
|