Commit Graph
3 Commits
Author SHA1 Message Date
jcoffey-dev 4b2c97df4e Prune old images, and keep every release
Two artefacts, opposite answers.

Releases stay. They carry no assets -- the image lives in GHCR -- so one costs
a tag, a title and generated notes, and with no CHANGELOG in this repository
those notes are the only changelog there is. Deleting one destroys history
that cannot be reconstructed, and saves nothing.

Images accumulate: a multi-architecture build a week, and the by-digest push
leaves two untagged per-architecture manifests behind each time on top of the
tagged index. Ten tagged versions are kept, which is roughly a quarter of
releases and far more than anything anyone rolls back to.

The obvious tool for this is a trap. delete-package-versions with
`delete-only-untagged-versions` will delete the per-architecture manifests
that a multi-arch tag points at, because they are untagged by design, and
nothing appears to break: the tag still resolves and pulls simply start
failing for one architecture. This action understands manifest lists and
leaves a retained index's children alone, `validate` re-checks every
multi-arch manifest against the registry afterwards, and `latest` is excluded
from consideration entirely.

It is pinned to a commit rather than a major tag. It holds `packages: write`
and its whole purpose is deletion, so a tag repointed upstream is not a risk
worth carrying for the convenience.

Kept in its own file and dispatchable, so a dry run can show exactly what
would go without rebuilding and re-pushing an image to find out.
2026-09-02 23:47:22 -07:00
jcoffey-dev 5cb0b2f3ea Cut a release once a week, and only when there is something in it
Publishing on release is the right trigger only if releases happen. They had
not: main ran 184 commits ahead of the last one, so `:latest` described a
build that neither the demo, nor production, nor anyone building from source
was running. This is the part that makes the trigger true without anyone
having to remember.

Mondays at 09:00 UTC. A run with no commits since the last release does
nothing at all -- an empty release moves `:latest` to an identical build,
spends a version number and mails every watcher about nothing. The decision is
written to the run summary either way, so a quiet week reads as a decision
rather than as a workflow that failed silently.

The awkward part is that a release created with GITHUB_TOKEN raises no
`release` event: GitHub refuses to let a token trigger another workflow, to
stop a workflow looping on its own output. A scheduled job that cut a release
and left publish.yml to notice would tag the commit and never build an image,
which is the kind of failure that looks like success. So publish.yml gains a
`workflow_call` trigger and this calls it directly. The alternative was a
personal access token kept as a secret; this needs no credential.

Two smaller decisions. Drafts are excluded when looking for the last release,
because an unpublished draft is not a release anybody has and counting from it
would hide commits that never shipped. And if the tag a release names has
gone, the count falls back to the whole history -- over-counting cuts a
release that was due anyway, where under-counting skips one that was not.
2026-09-02 23:43:12 -07:00
jcoffey-dev 1734ed0439 Publish the image the docs have been telling people to pull
README has said `docker run ... ghcr.io/coffey-labs/ihasmail:latest` since the
Docker instructions were written, and the docs site repeats it in four places.
Nothing ever pushed that image. `docker pull` answers `denied`, because the
package does not exist: .github/workflows held ci.yml and nothing else, and
there is no reference to ghcr.io, docker/build-push or docker push anywhere in
this repo. The instructions have been wrong the whole time.

Adds the workflow that makes them true. It fires on a published release, and
by hand for a ref -- the same dispatch trigger ci.yml carries, and the only
way to build an image for the tags that predate this file.

Two architectures on native runners rather than one build under QEMU.
Emulated arm64 runs `npm ci` and the Vite build through instruction
translation, which takes tens of minutes and sometimes exhausts memory;
ubuntu-24.04-arm is free for public repositories and does it at native speed.
The cost is pushing by digest and joining the two into one manifest at the
end, which is what the third job does.

`latest` moves only for a real release. A prerelease that moved it would hand
every `:latest` deployment an unfinished build, and a dispatch run has to ask
for it deliberately.

Also documents the images in README: which tags exist, that the dated tag is
the one to pin, and that building it yourself is still fully supported --
`docker compose up --build` is unchanged and the image is a convenience, not a
new requirement.

Worth knowing before the first run: GHCR creates a new package **private**,
even for a public repository, so an anonymous pull will still be refused until
the visibility is changed by hand. That is written at the top of the workflow,
because it is the failure that looks like success.
2026-09-02 23:22:21 -07:00