Files
kvitt/infra/build-images.sh
T
jacobh2 50a0d6792a
Build Kvitt images / build (push) Failing after 1m20s
Server: web client under /app/ (D-132), analytics E-34 trends and E-35 forecast (D-133)
Part A (D-132): KVITT_WEB_DIR (default /app) served under /app/ by a small handler: /app redirects to
/app/, app routes fall back to index.html, a missing assets/* file is 404, assets cached immutable,
index.html no-store, the app's own CSP plus nosniff, DENY and no-referrer. Unsafe paths never reach the
file system. /app paths count against the unauthenticated per-IP limit. Without the directory every /app
path stays 404 not_found. server/Dockerfile gains a Node 24 LTS stage (pinned by digest) building web/,
passed through the Rust stage so npm and rustc never run at once on the Pi 3; the build context is the
repository root with an allowlist .dockerignore (server/.dockerignore removed); workflow, build-images.sh
and infra/dev/build.sh follow.

Part B (D-133): GET /analytics/trends (E-34) and GET /analytics/forecast (E-35), authenticated, read-only,
SQL month buckets; rules (half-up average, trailing 3-month moving average, percent against the rounded
average, template occurrence per month) in kvitt-domain::analytics. OpenAPI, examples, contracts-check
and tests updated (35 operations).

Cargo.lock: rustls-webpki 0.103.16 and webpki-root-certs 1.0.10 (RUSTSEC-2026-0382, -0383) so that
cargo audit passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-21 20:54:21 +02:00

105 lines
4.8 KiB
Bash
Executable File

#!/usr/bin/env bash
# Build the two Kvitt images for linux/arm64 on the Mac and push them to the Gitea registry (SPEC §10.3).
# The Mac fallback for the Gitea Actions workflow .gitea/workflows/build-images.yaml (which is the normal path).
# Usage: infra/build-images.sh [tag] tag defaults to the short git SHA of HEAD; `latest` is always tagged too.
#
# Images (D-104, D-120): ${KVITT_REGISTRY}/kvitt-server (context: the repository root, D-132; server/Dockerfile
# builds the web client too) and kvitt-backup (infra/backup).
# KVITT_REGISTRY and the optional credentials come from infra/secrets/registry.env (git-ignored):
# KVITT_REGISTRY=gitea.lan/jacobh2
# KVITT_REGISTRY_USER=<gitea user> optional
# KVITT_REGISTRY_TOKEN=<token, write:package> optional
# Without that file the images are built locally and the exact push commands are printed instead.
# Refuses a dirty working tree, and refuses to run while a Gradle build holds the gradle-serial lock (CLAUDE.md).
# Works with Docker (buildx) and with Podman (this Mac's `docker`); the registry is plain HTTP on the home Wi-Fi.
set -euo pipefail
repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$repo_root"
# shellcheck source=../tools/gradle-lock.sh
. "$repo_root/tools/gradle-lock.sh"
[[ -z "$(git status --porcelain)" ]] || { echo "build-images.sh: the working tree is dirty; commit or stash first." >&2; exit 1; }
if pid="$(gradle_lock_holder)"; then
echo "build-images.sh: a Gradle build is running (pid $pid); try again when it is done." >&2
exit 1
fi
tag="${1:-$(git rev-parse --short HEAD)}"
[[ "$tag" =~ ^[A-Za-z0-9_][A-Za-z0-9_.-]{0,127}$ ]] || { echo "build-images.sh: not a valid tag: $tag" >&2; exit 2; }
reg_file="$repo_root/infra/secrets/registry.env"
default_registry="gitea.lan/jacobh2" # SPEC §10.3, D-104
registry="$default_registry"; user=""; token=""; push=0
if [[ -f "$reg_file" ]]; then
value() { sed -n "s/^$1=//p" "$reg_file" | tail -n 1; }
registry="$(value KVITT_REGISTRY)"; registry="${registry:-$default_registry}"
user="$(value KVITT_REGISTRY_USER)"; token="$(value KVITT_REGISTRY_TOKEN)"
push=1
fi
registry_host="${registry%%/*}"
# name|context|dockerfile
images=(
"kvitt-server|.|server/Dockerfile"
"kvitt-backup|infra/backup|infra/backup/Dockerfile"
)
if docker version 2>/dev/null | grep -qi podman; then engine=podman; else engine=docker; fi
echo "Engine: $engine; registry: $registry; tag: $tag (and latest); push: $([[ $push == 1 ]] && echo yes || echo no)"
if [[ $push == 1 && -n "$user" && -n "$token" ]]; then
if [[ $engine == podman ]]; then
printf '%s' "$token" | podman login --tls-verify=false "$registry_host" -u "$user" --password-stdin
else
printf '%s' "$token" | docker login "$registry_host" -u "$user" --password-stdin
fi
fi
if [[ $engine == docker && $push == 1 ]]; then
# A buildx builder that may push to the plain-HTTP registry (same rule as the Gitea workflow).
if ! docker buildx inspect kvitt-http-builder >/dev/null 2>&1; then
cfg="$(mktemp)"
trap 'rm -f "$cfg"' EXIT
printf '[registry."%s"]\n http = true\n' "$registry_host" > "$cfg"
docker buildx create --name kvitt-http-builder --config "$cfg" >/dev/null
rm -f "$cfg"
trap - EXIT
fi
fi
for spec in "${images[@]}"; do
IFS='|' read -r name context dockerfile <<< "$spec"
ref="$registry/$name"
echo "== $ref:$tag"
if [[ $engine == podman ]]; then
podman build --platform linux/arm64 -f "$dockerfile" -t "$ref:$tag" -t "$ref:latest" "$context"
if [[ $push == 1 ]]; then
podman push --tls-verify=false "$ref:$tag"
podman push --tls-verify=false "$ref:latest"
fi
elif [[ $push == 1 ]]; then
# --provenance=false: push a plain image manifest, as Podman does, rather than an index with an attestation
# entry (same choice as the Gitea workflow).
docker buildx build --builder kvitt-http-builder --platform linux/arm64 --provenance=false -f "$dockerfile" \
-t "$ref:$tag" -t "$ref:latest" --push "$context"
else
docker buildx build --platform linux/arm64 -f "$dockerfile" -t "$ref:$tag" -t "$ref:latest" --load "$context"
fi
done
if [[ $push == 1 ]]; then
echo "Pushed. On the Pi: cd ~/kvitt, set KVITT_TAG=$tag in .env, then docker compose pull && docker compose up -d"
else
echo
echo "Built locally, not pushed: $reg_file does not exist. To push, create it (KVITT_REGISTRY=$registry, plus"
echo "KVITT_REGISTRY_USER and KVITT_REGISTRY_TOKEN if Gitea needs them) and run this script again, or push by hand:"
for spec in "${images[@]}"; do
IFS='|' read -r name _ _ <<< "$spec"
if [[ $engine == podman ]]; then
echo " podman push --tls-verify=false $registry/$name:$tag && podman push --tls-verify=false $registry/$name:latest"
else
echo " docker push $registry/$name:$tag && docker push $registry/$name:latest # needs $registry_host in insecure-registries"
fi
done
fi