Files
kvitt/.gitea/workflows/build-images.yaml
T
jacobh2 5d948f9e52
Build Kvitt images / build (push) Has been cancelled
SPEC v0.34: L-10 implemented (D-156), W0b recorded, web version code in the image build
- AC-3.2, E-16 (§14.3.4), §11.4: the "to implement" notes removed; D-156 says
  implemented in 07c36d2; security review L-10 note: fixed in 07c36d2. ctutils
  0.4.4 (RUSTSEC-2026-0385) in the v0.34 row. Server tests 300, contracts-check 136.
- §15.3: W0b as built (5305f37): shared pieces, no cross-feature imports or web_
  keys, per-locale amount fields, Recharts chunk, Vitest cache off, version code.
  Web gate 609 tests.
- server/Dockerfile: ARG KVITT_VERSION_CODE=1 in the Node stage, passed to
  npm run build; the Gitea workflow computes git rev-list --count HEAD and passes
  it as a build argument (§10.3).
- HANDOFF: main 5305f37 pushed 2026-10-24; next image build, deploy, laptop login,
  browser checks; Web follow-ups closed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TQd3pEP9UeN2uifbHknQ7B
2026-10-24 11:29:42 +02:00

144 lines
7.5 KiB
YAML

# Kvitt image build on Gitea Actions (SPEC §10.3, D-104). NOT YET RUN (written 2026-10-18 from the owner's working
# example workflow and the Gitea Actions documentation; the first run is an owner step, infra/README.md).
#
# Runner: act_runner with the label `host` on the owner's Raspberry Pi (Alpine-based host, docker builds on the
# Pi itself). Pushes two linux/arm64 images to the Gitea container registry of the same instance (gitea.lan,
# plain HTTP), each tagged with the version vMAJOR.MINOR.PATCH and `latest`, then pushes the git tag of that version.
# On the Kvitt Pi: cd ~/kvitt, set KVITT_TAG=<version> in .env, then docker compose pull && docker compose up -d.
#
# Secret: `kvitt_cicd`, a user-level secret of the repository owner (Gitea gives a repository its owner's secrets)
# holding a Gitea access token with scope write:package of the user that pushes (owner step, infra/README.md). It is used only for `docker login`; the git tag push at the end goes through
# the credentials actions/checkout stored (the job's own token), not this secret, so it needs no write:repository.
#
# RISK: the runner is a Raspberry Pi 3 with 1 GB RAM and the server image compiles about 232 Rust crates
# (aws-lc-sys's C code included), after an npm ci and Vite build of the web client (D-132; not yet measured on the
# Pi). The build passes CARGO_BUILD_JOBS=1 to keep memory down; server/Dockerfile honours it (`ARG CARGO_BUILD_JOBS`,
# SPEC §10.3). The Dockerfile keeps the cargo registry and the target directory
# in BuildKit cache mounts, which persist in the reused builder `local-http-builder`, so later builds are
# incremental. First-build duration on the Pi: unknown.
# The builder's buildkitd.toml is read only when the builder is created; after changing it, remove the builder on
# the runner once (docker buildx rm local-http-builder), which also drops its cache.
# Fallback: infra/build-images.sh on the Mac.
name: Build Kvitt images
env:
MAJOR: 1
MINOR: 0
REGISTRY_HOST: gitea.lan
# ${KVITT_REGISTRY} of SPEC §10.3 (D-104).
REGISTRY: gitea.lan/jacobh2
defaults:
run:
shell: sh
on:
push:
paths:
- "server/**"
- "web/**"
- "contracts/**"
- "infra/backup/**"
- ".gitea/workflows/**"
workflow_dispatch: {}
jobs:
build:
runs-on: host
steps:
- name: Install Git and Docker CLI
run: apk add --no-cache git docker-cli docker-cli-buildx
- name: Log in to Gitea Container Registry
# The owner's proven form: the secret interpolated into the run line (act_runner masks it in the log).
# The env-based form (env: REGISTRY_TOKEN: ${{ secrets.kvitt_cicd }}) came through empty on act_runner 0.6.1.
run: printf '%s' "${{ secrets.kvitt_cicd }}" | docker login "$REGISTRY_HOST" -u "${{ github.actor }}" --password-stdin
- name: Create Custom Buildx Builder for HTTP
run: |
# Create the builder once and reuse it, so its BuildKit cache (cargo registry, target) survives runs.
if docker buildx inspect local-http-builder >/dev/null 2>&1; then
docker buildx use local-http-builder
else
# A configuration rule forcing plain HTTP for the local registry.
cfg=$(mktemp)
printf '[registry."%s"]\n http = true\n' "$REGISTRY_HOST" > "$cfg"
docker buildx create --use --name local-http-builder --config "$cfg"
rm -f "$cfg"
fi
- name: Checkout
uses: actions/checkout@v4
with:
# Tells the action to use the local 'git' binary installed above
set-safe-directory: 'true'
# Only what the two builds read: server/Dockerfile builds from the repository root (D-132) and copies
# server/ (Cargo workspace, migrations, the import's category table), web/ (the web client, built by its
# Node stage) and contracts/; the root .dockerignore keeps everything else out. infra/backup/Dockerfile
# copies backup.sh and healthcheck.sh. Cone mode also checks out the files at the root (.dockerignore
# included) and directly in infra/. A sparse checkout makes the action fetch with --filter=blob:none, so
# only the blobs of these paths are downloaded (not the Android screenshot goldens).
sparse-checkout: |
server
web
contracts
infra/backup
sparse-checkout-cone-mode: true
# Full commit and tag history (trees and commits only, no blobs, because of the filter above): "Determine
# Version" needs every v1.0.* tag. With fetch-depth 1, fetch-tags: true only drops --no-tags, so git
# fetches just the tags that point at the fetched commit, not the older v1.0.N.
fetch-depth: 0
- name: Determine Version
run: |
# git's own version sort: busybox sort on the Alpine runner may lack -V.
LATEST_TAG=$(git tag -l "v$MAJOR.$MINOR.*" --sort=-v:refname | head -n 1)
if [ -z "$LATEST_TAG" ]; then
VERSION="v$MAJOR.$MINOR.0"
else
# The patch number is the part after the last dot.
VERSION="v$MAJOR.$MINOR.$((${LATEST_TAG##*.} + 1))"
fi
echo "Version: $VERSION"
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
# The web's version code: the commit count, as the app's release builds count (fetch-depth 0 above).
VERSION_CODE=$(git rev-list --count HEAD)
echo "Version code: $VERSION_CODE"
echo "VERSION_CODE=$VERSION_CODE" >> "$GITHUB_ENV"
# --provenance=false on every build: one plain linux/arm64 manifest per tag, as Podman and infra/build-images.sh
# push. Without it buildx pushes an index with an attestation entry (platform unknown/unknown), which the Gitea
# package page lists as an extra platform; nothing here reads attestations.
- name: Build backup image
run: |
docker buildx build \
--platform linux/arm64 \
--provenance=false \
-t "$REGISTRY/kvitt-backup:$VERSION" \
-t "$REGISTRY/kvitt-backup:latest" \
--metadata-file /tmp/kvitt-backup.metadata.json \
./infra/backup \
--push
- name: Build server image
run: |
# Context: the repository root (D-132). server/Dockerfile builds the web client first (Node stage), then
# the Rust binaries, one after the other, so npm and rustc never share the Pi's 1 GB.
docker buildx build \
--platform linux/arm64 \
--provenance=false \
--build-arg CARGO_BUILD_JOBS=1 \
--build-arg "KVITT_VERSION_CODE=$VERSION_CODE" \
-f ./server/Dockerfile \
-t "$REGISTRY/kvitt-server:$VERSION" \
-t "$REGISTRY/kvitt-server:latest" \
--metadata-file /tmp/kvitt-server.metadata.json \
. \
--push
- name: Print image digests
# The digests the Kvitt Pi pulls (cd ~/kvitt && docker compose images, or docker image inspect) must match.
run: |
for name in backup server; do
digest=$(sed -n 's/.*"containerimage.digest": *"\([^"]*\)".*/\1/p' "/tmp/kvitt-$name.metadata.json")
echo "$REGISTRY/kvitt-$name:$VERSION ${digest:-unknown}"
rm -f "/tmp/kvitt-$name.metadata.json"
done
- name: Tag and Push Version
# The tag points at HEAD, which the remote already has; works with the sparse, blob-filtered checkout.
run: |
git tag "$VERSION"
git push origin "$VERSION"