# 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 three 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: /opt/kvitt/deploy.sh . # # Secret: `kvitt_cicd`, a repository secret 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). 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/**" - "infra/cert-sync/**" - "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 token reaches the shell as an environment variable, never as text inside the script. env: REGISTRY_TOKEN: ${{ secrets.kvitt_cicd }} REGISTRY_USER: ${{ github.actor }} run: printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY_HOST" -u "$REGISTRY_USER" --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' 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" # --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 cert-sync image run: | docker buildx build \ --platform linux/arm64 \ --provenance=false \ -t "$REGISTRY/kvitt-cert-sync:$VERSION" \ -t "$REGISTRY/kvitt-cert-sync:latest" \ --metadata-file /tmp/kvitt-cert-sync.metadata.json \ ./infra/cert-sync \ --push - 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 server/: server/Dockerfile copies the Cargo workspace from its own directory. docker buildx build \ --platform linux/arm64 \ --provenance=false \ --build-arg CARGO_BUILD_JOBS=1 \ -f ./server/Dockerfile \ -t "$REGISTRY/kvitt-server:$VERSION" \ -t "$REGISTRY/kvitt-server:latest" \ --metadata-file /tmp/kvitt-server.metadata.json \ ./server \ --push - name: Print image digests # The digests deploy.sh prints and records on the Kvitt Pi (/opt/kvitt/deployed-digests, D-117) must match. run: | for name in cert-sync 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 run: | git tag "$VERSION" git push origin "$VERSION"