build: move releases to CI and trim the Makefile
check / check (push) Successful in 6m23s

Merging a pull request into main is now the whole release. A Gitea
Actions workflow derives the CalVer tag, builds the image and pushes
it with :latest, so nothing is built locally any more.

That made image/push/release redundant, and with them the .release-tag
state file and the main-branch guard — the workflow only runs on main,
which is protected, so the guard had nothing left to catch. The digest
verification went too: it guarded a `make -j` race between image and
push that cannot happen in a single CI job.

seed, icons and vendor ran a few times a year and are written out in
the README instead. Makefile: 151 lines to 71.

Docs referenced the removed targets in sixteen places, including a
CONTRIBUTING note claiming the branch check "has to be local".
This commit is contained in:
Esa Kataja
2026-09-06 10:54:22 +03:00
parent c0b48ee71a
commit 6590c18412
8 changed files with 158 additions and 151 deletions
+75 -49
View File
@@ -11,35 +11,35 @@ See [PRD.md](PRD.md) for the full specification.
## Status
**Stage 1 — eating history: in development.** The meal catalog and the daily
log come first, because the suggester is worthless until there are a few
weeks of real history to weight against.
**Stage 1 — eating history: in use.** The meal catalog and the daily log came
first, because the suggester is worthless until there are a few weeks of real
history to weight against.
Working:
- **Kirjaa** — log a dinner: pick a dish, tick sides, save. Dishes are ordered
and sized by how often they are eaten, so the likely answer is the biggest
target. The history sits on the same page underneath: every day back to the
first entry, unlogged days shown as explicit gaps, and every row a link that
loads that day into the logger above it.
- **Kirjaa** — log a dinner: pick a dish, tick sides, save. Dishes are grouped
by category, then ordered and sized by how often they are eaten, so the
likely answer is the biggest target. The history sits on the same page
underneath: every day back to the first entry, unlogged days shown as
explicit gaps, and every row opening that day's logger in place. Older days
arrive a window at a time.
- **Ruuat** — add, edit and delete mains and sides, or import a whole bundle
by paste or file upload. Grouped by category and alphabetical inside, since
this is a list you manage rather than one you pick from. Deletes are soft,
so old log entries keep showing the dish they used.
this is a list you manage rather than one you pick from. Edit and delete are
row icons, and a delete asks first. Deletes are soft, so old log entries
keep showing the dish they used.
- **Search as you type** on both tabs, debounced, patching just the list.
- **Category icons**, not colour dots: shape and colour together, so two marks
are told apart by more than hue.
- **Light / dark**, remembered per device, dark by default. The button shows
the theme that is on — moon while dark, sun while light — not the one a
click would bring.
Nothing navigates. Every interaction patches the page through Datastar, so
the scroll position survives; links and forms still work with JavaScript off.
Still to build:
- Live search as you type, and paging for the history and catalog lists once
years of entries make them long. Both via Datastar.
- Category icons instead of plain colour dots — colour and shape together, so
a red blob and a yellow blob are told apart by more than hue.
- Edit and delete as icons in the catalog rows, and a confirmation step before
a delete actually happens.
- A background for the header. Something subtle; the palette gets overhauled
later.
- Stage 2: the seven-meal suggester, which starts once there is history to
weight against.
@@ -74,20 +74,20 @@ no direct pushes, so a release arrives through a pull request.
git switch dev # where the work happens
# ... commits ...
make check # lint, unit tests, smoke
git push origin dev
git push origin dev # CI runs make check too
tea pr create --base main --head dev # or open it in the forge
# merge the pull request, then:
git switch main && git pull --ff-only
make release # builds, tags vYYYYMMDD-N, pushes the image
git push origin --tags
# squash-merge the pull request — that is the whole release
```
`make image` additionally refuses to run from any branch but `main`, so a
release tag can never point at a commit that was not released. That check
lives locally because it has to: tags and images are built before anything
reaches the remote, so protection there cannot catch it.
Merging is the release. CI builds the image, tags it `vYYYYMMDD-N` and
`latest`, pushes both to the registry, and creates the matching git tag. There
is nothing to run locally afterwards; pull the new image on the server when
you are ready.
A release tag can therefore never point at a commit that was not released:
the workflow only runs on `main`, and `main` only moves through a pull
request.
## Quick start
@@ -102,15 +102,39 @@ make run # http://localhost:8080
make fix gofmt, templ fmt, go mod tidy
make lint go vet, gofmt check, golangci-lint when installed
make test go test ./...
make smoke end-to-end check against a scratch server
make check lint + test + smoke — run before every commit
make build ./foodster
make seed import a dish bundle (SEED=seeds/testi.json)
make vendor re-download the Datastar client
make image build and tag vYYYYMMDD-N (creates a git tag)
make push push the newest tag and :latest
make release image + push
make up/down/logs compose
```
Images are built by CI, not here — see [Deployment](#deployment).
### Occasional commands
Rare enough not to earn a `make` target. Both write into
`cmd/foodster/static/`, and the results are committed.
Re-download the vendored Datastar client after bumping the version:
```sh
curl -sSfL -o cmd/foodster/static/datastar.js \
"https://cdn.jsdelivr.net/gh/starfederation/[email protected]/bundles/datastar.js"
```
Re-rasterise the home-screen icons after editing `assets/icon.svg`:
```sh
cd cmd/foodster/static
rsvg-convert -w 180 -h 180 ../../../assets/icon.svg -o apple-touch-icon.png
rsvg-convert -w 192 -h 192 ../../../assets/icon.svg -o icon-192.png
rsvg-convert -w 512 -h 512 ../../../assets/icon.svg -o icon-512.png
oxipng -o max --zopfli --quiet apple-touch-icon.png icon-192.png icon-512.png
```
`oxipng -o max` on its own loses to optipng on the 512; `--zopfli` wins at
every size. Slow, but these are three tiny files built by hand.
## Importing dishes
The **Ruuat** tab takes a bundle of mains and sides: paste the JSON or upload
@@ -141,7 +165,7 @@ The same importer runs from the command line when you just want to repopulate
a scratch database:
```sh
make seed # or: SEED=seeds/other.json make seed
go run ./cmd/foodster -import seeds/testi.json
```
## Icons
@@ -152,12 +176,8 @@ cannot be transparent and must not change with the theme; they are rasterised
from `assets/icon.svg`, which is opaque and keeps the artwork inside the
central 80% so Android can mask it to any shape.
```sh
make icons # rsvg-convert, then optipng -o7
```
The PNGs are committed so the build needs no rasterizer. Re-run `make icons`
after editing `assets/icon.svg`.
The PNGs are committed so the build needs no rasterizer. The commands to
regenerate them are under [Occasional commands](#occasional-commands).
## Migrations
@@ -182,25 +202,24 @@ Everything is environment variables. `.env` is gitignored; start from
| `ADDR` | `:8080` | Listen address. Only useful for a second local instance. |
| `PUID` / `PGID` | `1000` | Host owner of `./data`, for the bind mount. |
| `TZ` | `Europe/Helsinki` | Used for every calendar-day calculation. |
| `REPO` | *required to run* | Image repository, no tag. Used by `compose.yaml`. |
| `TAG` | `latest` | Tag to run under compose. |
| `HOST` | *required to run* | Hostname Traefik routes to. |
Names carry no prefix: the container gives them their own namespace already.
`PUID`/`PGID` are the exception — `UID` is read-only in bash, so a value set
in `.env` would be silently replaced by the invoking shell's own.
| `REPO` | *required to build* | Image repository, no tag. |
| `TAG` | `latest` | Tag to run under compose. |
| `HOST` | *required to run* | Hostname Traefik routes to. |
Set `TZ` in development too. Under UTC the date rolls over three hours late,
which is exactly when dinner gets logged.
## Deployment
Images are built with Podman and run under Docker Compose on a LAN server.
They are OCI images, so either engine works.
Images are built by CI when a pull request merges into `main`, and run under
Docker Compose on a LAN server. They are OCI images, so either engine works.
```sh
make release # build, tag, push
# on the server:
# on the server, once CI reports the build finished:
docker compose pull && docker compose up -d
```
@@ -209,8 +228,15 @@ running version is served at `GET /healthz`, which is the one route outside
authentication.
There is no database container. SQLite lives in `./data`, bind-mounted into
the container, so a backup is `cp -r data` and you can inspect the file with
any sqlite client without going through the engine.
the container, so you can inspect the file with any sqlite client without
going through the engine. Back it up with
```sh
sqlite3 data/foodster.db ".backup data/foodster-$(date +%F).db"
```
rather than copying the directory: the database runs in WAL mode, and a plain
copy of a live database can catch the `.db` and its `-wal` mid-write.
That directory must exist and be owned by the user compose runs as — `make up`
creates it, and `PUID`/`PGID` in `.env` tell the container who