diff --git a/Makefile b/Makefile index 8d10259..80ed93e 100644 --- a/Makefile +++ b/Makefile @@ -1,63 +1,42 @@ # The Great Bear — container image builds. # -# Built with Podman/Buildah, not Docker. The image is always built from a fresh -# clone of the git remote, so it matches a pushed commit; commit and push before -# deploying. Registry credentials: `podman login registry.tanshu.com` once per -# machine (nothing here prompts). +# Built with Podman/Buildah, not Docker, from the committed tree +# (`git archive HEAD`) and tagged with that commit, so a deploy ships exactly +# what git knows about. deploy.sh pushes first and refuses a dirty tree. + +COMMIT ?= $(shell git rev-parse --short HEAD) + +# The deploy host (emily) is aarch64, so that is the default target: the image +# is built for the machine it runs on, not the one running make. Podman carries +# no built-in QEMU (unlike docker buildx), so a foreign platform needs binfmt +# handlers on this host — qemu-user-binfmt is installed here. Override with e.g. +# `make build-production PLATFORM=linux/amd64`. +PLATFORM ?= linux/arm64 IMAGE ?= registry.tanshu.com/tanshu/greatbear -REPO ?= git@git.tanshu.com:tanshu/greatbear.git -TAG ?= latest - -# Target platform. Defaults to the architecture of the machine running make, -# which is what a native `podman build` would use anyway. Only cross-build when -# the deploy host differs, e.g.: -# -# make build-production PLATFORM=linux/arm64 -# -# Cross-building needs QEMU binfmt registered on the controller -# (qemu-user-static), the same requirement `docker buildx --platform` had. -PLATFORM ?= linux/$(shell uname -m | sed -e 's/x86_64/amd64/' -e 's/aarch64/arm64/' -e 's/armv7l/arm/') .PHONY: build-production -build-production: ## Build the image with podman from the git remote and push it - @set -euo pipefail; \ - ctx="$$(mktemp -d)"; \ - trap 'rm -rf "$$ctx"' EXIT; \ - echo "==> cloning $(REPO)"; \ - git clone --quiet --depth 1 "$(REPO)" "$$ctx/src"; \ - echo "==> podman build --platform $(PLATFORM) -t $(IMAGE):$(TAG)"; \ - podman build --pull=always --platform "$(PLATFORM)" \ - -t "$(IMAGE):$(TAG)" \ - -f "$$ctx/src/Dockerfile" "$$ctx/src"; \ - echo "==> podman push $(IMAGE):$(TAG)"; \ - podman push "$(IMAGE):$(TAG)"; \ - if [ "$(TAG)" != "latest" ]; then \ - echo "==> podman push $(IMAGE):latest"; \ - podman tag "$(IMAGE):$(TAG)" "$(IMAGE):latest"; \ - podman push "$(IMAGE):latest"; \ - fi +build-production: ## Build HEAD with podman, tag :$(COMMIT) + :latest, and push + @echo "==> podman build --platform $(PLATFORM) → $(IMAGE):$(COMMIT) + :latest" + @git archive --format=tar HEAD | podman build \ + --platform $(PLATFORM) \ + --tag $(IMAGE):$(COMMIT) \ + --tag $(IMAGE):latest \ + --pull=always \ + - || { \ + echo "✗ podman build failed."; \ + echo " Cross-platform builds need binfmt on this host (podman has no built-in QEMU):"; \ + echo " sudo apt install qemu-user-binfmt # or qemu-user-static"; \ + exit 1; \ + } + podman push $(IMAGE):$(COMMIT) + podman push $(IMAGE):latest .PHONY: build-check -build-check: ## Build from the git remote without pushing (compile check) - @set -euo pipefail; \ - ctx="$$(mktemp -d)"; \ - trap 'rm -rf "$$ctx"' EXIT; \ - echo "==> cloning $(REPO)"; \ - git clone --quiet --depth 1 "$(REPO)" "$$ctx/src"; \ - echo "==> podman build --platform $(PLATFORM) greatbear:test"; \ - podman build --pull=always --platform "$(PLATFORM)" \ - -t localhost/greatbear:test \ - -f "$$ctx/src/Dockerfile" "$$ctx/src" - -.PHONY: build-check-local -build-check-local: ## Build the committed local tree (git archive) without pushing - @set -euo pipefail; \ - ctx="$$(mktemp -d)"; \ - trap 'rm -rf "$$ctx"' EXIT; \ - mkdir -p "$$ctx/src"; \ - git archive --format=tar HEAD | tar -x -C "$$ctx/src"; \ - echo "==> podman build --platform $(PLATFORM) greatbear:test (local HEAD)"; \ - podman build --pull=always --platform "$(PLATFORM)" \ - -t localhost/greatbear:test \ - -f "$$ctx/src/Dockerfile" "$$ctx/src" +build-check: ## Compile check from HEAD with podman, nothing pushed + @echo "==> podman build --platform $(PLATFORM) (no push)" + git archive --format=tar HEAD | podman build \ + --platform $(PLATFORM) \ + --tag localhost/greatbear:check \ + --pull=always \ + - diff --git a/README.md b/README.md index bf5297d..c740acc 100644 --- a/README.md +++ b/README.md @@ -88,33 +88,31 @@ podman run --rm -p 3000:3000 -p 8090:8090 \ ## Deploying -The image is built with **Podman/Buildah** (not Docker) and always from the -**committed state of the git remote** — commit and push before deploying. The -Makefile never prompts, so log in to the registry once per machine: -`podman login registry.tanshu.com`. +The image is built with **Podman/Buildah** (not Docker) from the committed tree +(`git archive HEAD`) and tagged with its **commit id**, so what runs on the host +is exactly what git knows about. `deploy.sh` pushes the commit first, refuses a +dirty tracked tree and requires `HEAD == origin/main`. Log in to the registry +once per machine: `podman login registry.tanshu.com`. ```bash -./deploy.sh # build + push :latest, then deploy that tag via Ansible -./deploy.sh v1.2.3 # same, with an explicit version tag +./deploy.sh # push, build , deploy it to emily +./deploy.sh --no-push # skip the push (you already pushed) +./deploy.sh --force # skip the guards ``` Or run the pieces yourself: ```bash -make build-production TAG=v1.2.3 # podman build + push from the git remote -make build-check # build from the git remote, no push -make build-check-local # build the committed local tree (git archive), no push +make build-production # podman build HEAD, push : + :latest +make build-check # compile check from HEAD, no push -cd ansible && ansible-playbook -u tanshu playbook.yml -e "tag=v1.2.3" +cd ansible && ansible-playbook -u tanshu playbook.yml -e "image_tag=$(git rev-parse --short HEAD)" ``` -Images are built for the controller's architecture by default. Only cross-build -when the deploy host differs, and register QEMU binfmt first -(`qemu-user-static`), the same requirement `docker buildx --platform` had: - -```bash -make build-production PLATFORM=linux/arm64 -``` +The image is built for **linux/arm64** by default because `emily` is aarch64. +Podman has no built-in QEMU, so cross-building from an x86_64 controller needs +binfmt there (`qemu-user-binfmt`, already installed on this machine); override +with `PLATFORM=linux/amd64`. ### Ansible deployment @@ -132,17 +130,18 @@ makes `systemctl --user` work. ```bash cd ansible -ansible-playbook -u tanshu playbook.yml -e "tag=v1.2.3" +ansible-playbook -u tanshu playbook.yml -e "image_tag=" ``` -All deployment variables live in `ansible/vars/default.yml` (registry, tag, -domains, PocketBase admin credentials). The play: +All deployment variables live in `ansible/vars/default.yml` (registry, image +tag, domains, PocketBase admin credentials). The play: 1. Writes `~/.config/containers/systemd/greatbear.network` (`NetworkName=greatbear_net`) and a drop-in that attaches the shared Caddy unit to it, so Caddy can resolve `greatbear` by container name. 2. Logs in to the private registry, pulls - `registry.tanshu.com/tanshu/greatbear:{tag}`, writes + `registry.tanshu.com/tanshu/greatbear:{image_tag}` (rollback is the same play + with an older commit), writes `~/podman-data/greatbear/.env`, installs `~/.config/containers/systemd/greatbear.container`, starts it as a **systemd user service** and waits for the image's healthcheck (PocketBase diff --git a/ansible/vars/default.yml b/ansible/vars/default.yml index 31d87a1..ccb396b 100644 --- a/ansible/vars/default.yml +++ b/ansible/vars/default.yml @@ -19,7 +19,9 @@ registry_password: ff28a01f00c0f39315d94cd9dcb1e554968dba25676a8ea5f2be34e96a9a0 image_name: "{{ registry }}/tanshu/greatbear" tag: latest -image: "{{ image_name }}:{{ tag }}" +# deploy.sh passes -e image_tag=; `tag` is the fallback when running the +# play by hand. Rollback = -e image_tag=. +image: "{{ image_name }}:{{ image_tag | default(tag) }}" # --------------------------------------------------------------------------- # The Great Bear diff --git a/deploy.sh b/deploy.sh index e0dec72..bc9d43c 100755 --- a/deploy.sh +++ b/deploy.sh @@ -1,15 +1,63 @@ #!/usr/bin/env bash +# +# The Great Bear deploy — push the current commit, build it with podman, and +# install the Quadlet unit on emily with ansible. +# +# The image is built from the committed tree (`git archive HEAD`) and tagged +# with its commit id, so what runs on the host is exactly what git knows about. +# +# ./deploy.sh deploy HEAD (tracked tree must be clean and pushed) +# ./deploy.sh --force skip the guards (know what you are doing) +# ./deploy.sh --no-push skip the push, if you already pushed +# +# Log in to the registry once beforehand: podman login registry.tanshu.com set -euo pipefail +cd "$(dirname "${BASH_SOURCE[0]}")" -parent_path=$( cd "$(dirname "${BASH_SOURCE[0]}")" || exit ; pwd -P ) -cd "$parent_path" || exit +BRANCH="${BRANCH:-main}" + +FORCE=false +NO_PUSH=false +for arg in "$@"; do + case "$arg" in + --force) FORCE=true ;; + --no-push) NO_PUSH=true ;; + *) + echo "Usage: $0 [--force] [--no-push]" >&2 + exit 1 + ;; + esac +done + +# The build takes HEAD, so push first: otherwise the registry collects an image +# for a commit that exists only on this machine. +if [ "$NO_PUSH" != true ]; then + echo "==> pushing" + git push +fi + +# Guards. Only tracked-file state matters — untracked and ignored files never +# enter a `git archive HEAD` build. HEAD must match origin/$BRANCH so the commit +# the image is tagged with is one the remote actually has. +if [ "$FORCE" != true ]; then + if [ -n "$(git status --porcelain --untracked-files=no)" ]; then + echo "x tracked files modified - commit or stash first (or --force)" >&2 + exit 1 + fi + git fetch origin --quiet + if [ "$(git rev-parse HEAD)" != "$(git rev-parse "origin/$BRANCH")" ]; then + echo "x HEAD != origin/$BRANCH - push or pull first (or --force)" >&2 + exit 1 + fi +fi + +TAG=$(git rev-parse --short HEAD) +echo "==> deploying commit $TAG" echo "========================================================" echo " Building & Uploading Container Image (Podman)" echo "========================================================" -current_version="${1:-latest}" -make build-production TAG="$current_version" - +make build-production COMMIT="$TAG" echo "" echo "========================================================" @@ -19,5 +67,6 @@ echo "========================================================" # connecting user, so it must connect as that user and must NOT become root. # `-u` is the account that owns ~/.config/containers/systemd on the target. deploy_user="${ANSIBLE_USER:-tanshu}" -cd "$parent_path/ansible" || exit -ansible-playbook -u "$deploy_user" playbook.yml -e "tag=$current_version" +cd ansible || exit +# image_tag pins the exact commit; rollback is -e image_tag=. +ansible-playbook -u "$deploy_user" playbook.yml -e "image_tag=$TAG"