Multi-stage app builds left an untagged image for every build stage after each deploy, and nothing could safely remove them. upaas now asks Docker for a BuildKit build, which keeps stages in Docker's build cache instead of as images; Docker limits and cleans that cache itself. The final image is still tagged `upaas-<app>:<N>`, so old-image cleanup is unchanged. BuildKit's progress messages are decoded and written to the deployment log as plain text.
Deviation: adds `github.com/moby/buildkit` v0.16.0 (matching the pinned Docker client), added with `go get` since no make target adds dependencies.
Judgement call: the cache limit is on by default only from Docker Engine 28.2; older engines need the `daemon.json` setting the README now names.
Model: opus-5-5
Every deploy built and tagged a new image and nothing removed the old ones, so disk use grew with each push. After a successful deploy, upaas now removes the app's old `upaas-<app>:<N>` tags by name, without force, keeping the current image and the previous one that rollback starts. Docker deletes an image only when no other tag or container still uses it, so an image shared with another app stays. A failed removal is a warning in the deployment log, not a failed deploy. The post-deploy step is one function, tested against a fake Docker API.
Judgement call: untagged images from other stages of a multi-stage build stay; they are the build cache.
On the first deploy after upgrading, all older images of that app are removed at once.
Model: opus-5-5
The pinned `alpine/git` image declares a volume at `/git`, so every clone container got an anonymous volume, and the container was removed without its volumes, leaving one volume behind per deploy. The clone container is now removed together with its volumes, whether the clone succeeds, fails or is cancelled; the removal uses a context that outlives cancellation. Tests cover success, failure and cancellation against a fake Docker API, and a manual check against real Docker is recorded on the PR. The old app container's own anonymous volumes are still kept on redeploy, since deleting them could discard app data.
Model: opus-5-5
Deployment logs were stored under a directory named after the container's hostname, and the path was worked out again from the current hostname on download. Docker gives a recreated container a new hostname, so every older log download returned 404. New logs now go to `logs/<appname>/` with no hostname in the path. Logs written by older versions under an old hostname directory are still found by looking one directory deeper, inside the same confined log root. Tests cover both the hostname-free path and downloading an old-layout log.
Model: opus-5-5
`TestHandleWebhookProcessesValidWebhook` waited for the background deployment with a 100 ms sleep, so its temp directory could be removed while the deployment was still writing its log there, and the test failed now and then. It now waits with the webhook service's `WaitForDeployments`, the fix already applied to the webhook service tests for #198. The test context keeps the webhook service it already built. No other handlers test can start a deployment. No production code change.
Model: opus-5-5
Replaces the local `.golangci.yml` with the canonical file from `sneak/prompts`, fetched unchanged. This turns off the deprecated `gomodguard` (removing its warning from every lint run), turns on `depguard`, and gives the already-running `gomodguard_v2` its module block list. The new config finds nothing in upaas, so no source changes. The `test-support` deny list stays canonical because upaas has no separate test-support packages.
Model: opus-5-5
script/fmt ran unpinned npx prettier with an inline --tab-width and no config, so formatting was not reproducible. This vendors the canonical format toolchain from the prompts scaffold: .prettierrc (tabWidth 4, proseWrap always), package.json and yarn.lock pinning prettier 3.8.1, and a script/bootstrap that installs node/yarn from a hash-verified nvm archive. script/fmt now reads the config over static/js and markdown; gofmt/goimports unchanged. .prettierignore keeps the *.min.js rule protecting the vendored alpine.min.js. Existing markdown was reflowed to house style.
Disclosure: make check does not gate prettier; the linter's pre-existing gomodguard deprecation warning is unrelated and left as-is.
Model: opus-4-8 (implementation and review)