diff --git a/.gitea/workflows/build-images.yaml b/.gitea/workflows/build-images.yaml index bb707be..902b2d2 100644 --- a/.gitea/workflows/build-images.yaml +++ b/.gitea/workflows/build-images.yaml @@ -10,16 +10,20 @@ # write:repository for the tag push) of the user that pushes (owner step, infra/README.md). # # 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 must -# declare `ARG CARGO_BUILD_JOBS` in its build stage for it to take effect (contract request in the M2c-pi report; -# until then buildx warns that the argument is unused and cargo uses every core). The Dockerfile already 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. +# (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: @@ -41,17 +45,22 @@ jobs: - name: Install Git and Docker CLI run: apk add --no-cache git docker-cli docker-cli-buildx - name: Log in to Gitea Container Registry - run: echo "${{ secrets.kvitt_cicd }}" | docker login gitea.lan -u ${{ github.actor }} --password-stdin + # 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: | - # 1. Write a configuration rule forcing plain HTTP for the local registry - echo '[registry."gitea.lan"]' > buildkitd.toml - echo ' http = true' >> buildkitd.toml - # 2. Create the builder once and reuse it, so its BuildKit cache (cargo registry, target) survives runs + # 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 - docker buildx create --use --name local-http-builder --config buildkitd.toml + # 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 @@ -61,30 +70,35 @@ jobs: fetch-depth: 0 - name: Determine Version run: | - LATEST_TAG=$(git tag -l "v${{ env.MAJOR }}.${{ env.MINOR }}.*" | sort -V | tail -n 1) + # 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${{ env.MAJOR }}.${{ env.MINOR }}.0" + VERSION="v$MAJOR.$MINOR.0" else - # Strip the 'v' and get the patch version - PATCH=$(echo "$LATEST_TAG" | sed 's/v//' | cut -d'.' -f3) - NEW_PATCH=$((PATCH + 1)) - VERSION="v${{ env.MAJOR }}.${{ env.MINOR }}.$NEW_PATCH" + # 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 \ - -t gitea.lan/jacobh2/kvitt-cert-sync:${{ env.VERSION }} \ - -t gitea.lan/jacobh2/kvitt-cert-sync:latest \ + --provenance=false \ + -t "$REGISTRY/kvitt-cert-sync:$VERSION" \ + -t "$REGISTRY/kvitt-cert-sync:latest" \ ./infra/cert-sync \ --push - name: Build backup image run: | docker buildx build \ --platform linux/arm64 \ - -t gitea.lan/jacobh2/kvitt-backup:${{ env.VERSION }} \ - -t gitea.lan/jacobh2/kvitt-backup:latest \ + --provenance=false \ + -t "$REGISTRY/kvitt-backup:$VERSION" \ + -t "$REGISTRY/kvitt-backup:latest" \ ./infra/backup \ --push - name: Build server image @@ -92,10 +106,11 @@ jobs: # 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 gitea.lan/jacobh2/kvitt-server:${{ env.VERSION }} \ - -t gitea.lan/jacobh2/kvitt-server:latest \ + -t "$REGISTRY/kvitt-server:$VERSION" \ + -t "$REGISTRY/kvitt-server:latest" \ ./server \ --push - name: Tag and Push Version diff --git a/infra/build-images.sh b/infra/build-images.sh index ed4bbe8..5b82cce 100755 --- a/infra/build-images.sh +++ b/infra/build-images.sh @@ -15,10 +15,11 @@ 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; } -lock=/tmp/kvitt-gradle.lock -if [[ -d "$lock" ]] && pid="$(cat "$lock/pid" 2>/dev/null)" && [[ -n "$pid" ]] && kill -0 "$pid" 2>/dev/null; then +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 @@ -27,10 +28,11 @@ 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" -registry="gitea.lan/jacobh2"; user=""; token=""; push=0 +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:-gitea.lan/jacobh2}" + registry="$(value KVITT_REGISTRY)"; registry="${registry:-$default_registry}" user="$(value KVITT_REGISTRY_USER)"; token="$(value KVITT_REGISTRY_TOKEN)" push=1 fi @@ -58,9 +60,11 @@ 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 @@ -75,7 +79,9 @@ for spec in "${images[@]}"; do podman push --tls-verify=false "$ref:latest" fi elif [[ $push == 1 ]]; then - docker buildx build --builder kvitt-http-builder --platform linux/arm64 -f "$dockerfile" \ + # --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"