README has told people to run ghcr.io/coffey-labs/ihasmail:latest since the Docker instructions were written, and docs-site/docs/configure.md on the docs site repeats it in four more places.
Nothing ever pushed that image.
$ docker pull ghcr.io/coffey-labs/ihasmail:latest
Error response from daemon: error from registry: denied
.github/workflows/ held ci.yml and nothing else, and there is no reference to ghcr.io, docker/build-push or docker push anywhere in the repo. Seven documented commands have never worked. Installing escapes it only because it says docker build -t ihasmail:local .
It also blocks distribution: TrueNAS and Unraid both install by pulling an image, neither builds from source, so neither catalogue is possible without this.
The workflow
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 has to run npm ci and the Vite build through instruction translation, which takes tens of minutes and occasionally exhausts memory. ubuntu-24.04-arm is free for public repositories and does the same work at native speed. The cost is pushing by digest and joining the two into one manifest in a final job.
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.
The version is computed once and handed to both builds, so the two architectures cannot disagree about what they are.
README
Documents 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; the image is a convenience, not a new requirement.
Read this before merging
GHCR creates a new package private, even for a public repository. After the first successful run, an anonymous docker pull will still answer denied until someone flips the package to public by hand in its settings. Nothing in a workflow can do it.
That is the failure that looks like success, so it is written at the top of the workflow file. Verify with a logged-out pull, not from a machine holding credentials.
Testing
actionlint clean, including shellcheck. It caught a real bug on the first pass: the digest was saved with its sha256: prefix intact and then re-prefixed when the manifest was assembled, which would have produced image@sha256:sha256:… and failed the merge. Fixed, and the tag/digest arguments now build as arrays rather than by word-splitting a string.
It cannot be exercised further from a branch — workflow_dispatch only offers workflows on the default branch. Once merged I'll dispatch it against an existing tag, flip the package public, and confirm an anonymous multi-arch pull.
Not in this PR
The four configure.md references live in Coffey-Labs/ihasmail.org, so they cannot be in the same PR. They need no wording change — they were only wrong because the image did not exist, and publishing makes them true.
## The bug
README has told people to run `ghcr.io/coffey-labs/ihasmail:latest` since the Docker instructions were written, and `docs-site/docs/configure.md` on the docs site repeats it in four more places.
**Nothing ever pushed that image.**
```
$ docker pull ghcr.io/coffey-labs/ihasmail:latest
Error response from daemon: error from registry: denied
```
`.github/workflows/` held `ci.yml` and nothing else, and there is no reference to `ghcr.io`, `docker/build-push` or `docker push` anywhere in the repo. Seven documented commands have never worked. `Installing` escapes it only because it says `docker build -t ihasmail:local .`
It also blocks distribution: TrueNAS and Unraid both install by **pulling** an image, neither builds from source, so neither catalogue is possible without this.
## The workflow
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 has to run `npm ci` and the Vite build through instruction translation, which takes tens of minutes and occasionally exhausts memory. `ubuntu-24.04-arm` is free for public repositories and does the same work at native speed. The cost is pushing by digest and joining the two into one manifest in a final job.
`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.
The version is computed once and handed to both builds, so the two architectures cannot disagree about what they are.
## README
Documents 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; the image is a convenience, not a new requirement.
## Read this before merging
**GHCR creates a new package private, even for a public repository.** After the first successful run, an anonymous `docker pull` will *still* answer `denied` until someone flips the package to public by hand in its settings. Nothing in a workflow can do it.
That is the failure that looks like success, so it is written at the top of the workflow file. Verify with a logged-out pull, not from a machine holding credentials.
## Testing
`actionlint` clean, including shellcheck. It caught a real bug on the first pass: the digest was saved with its `sha256:` prefix intact and then re-prefixed when the manifest was assembled, which would have produced `image@sha256:sha256:…` and failed the merge. Fixed, and the tag/digest arguments now build as arrays rather than by word-splitting a string.
It cannot be exercised further from a branch — `workflow_dispatch` only offers workflows on the default branch. Once merged I'll dispatch it against an existing tag, flip the package public, and confirm an anonymous multi-arch pull.
## Not in this PR
The four `configure.md` references live in `Coffey-Labs/ihasmail.org`, so they cannot be in the same PR. They need no wording change — they were only wrong because the image did not exist, and publishing makes them true.
**Merged** 2026-09-02 as coffey-labs/ihasmail@e1c85cf82e5e
<sub>Rebuilt from: git history, session transcript.</sub>
This repo is archived. You cannot comment on issues.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The bug
README has told people to run
ghcr.io/coffey-labs/ihasmail:latestsince the Docker instructions were written, anddocs-site/docs/configure.mdon the docs site repeats it in four more places.Nothing ever pushed that image.
.github/workflows/heldci.ymland nothing else, and there is no reference toghcr.io,docker/build-pushordocker pushanywhere in the repo. Seven documented commands have never worked.Installingescapes it only because it saysdocker build -t ihasmail:local .It also blocks distribution: TrueNAS and Unraid both install by pulling an image, neither builds from source, so neither catalogue is possible without this.
The workflow
Fires on a published release, and by hand for a
ref— the same dispatch triggerci.ymlcarries, 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 has to run
npm ciand the Vite build through instruction translation, which takes tens of minutes and occasionally exhausts memory.ubuntu-24.04-armis free for public repositories and does the same work at native speed. The cost is pushing by digest and joining the two into one manifest in a final job.latestmoves only for a real release — a prerelease that moved it would hand every:latestdeployment an unfinished build — and a dispatch run has to ask for it deliberately.The version is computed once and handed to both builds, so the two architectures cannot disagree about what they are.
README
Documents which tags exist, that the dated tag is the one to pin, and that building it yourself is still fully supported.
docker compose up --buildis unchanged; the image is a convenience, not a new requirement.Read this before merging
GHCR creates a new package private, even for a public repository. After the first successful run, an anonymous
docker pullwill still answerdenieduntil someone flips the package to public by hand in its settings. Nothing in a workflow can do it.That is the failure that looks like success, so it is written at the top of the workflow file. Verify with a logged-out pull, not from a machine holding credentials.
Testing
actionlintclean, including shellcheck. It caught a real bug on the first pass: the digest was saved with itssha256:prefix intact and then re-prefixed when the manifest was assembled, which would have producedimage@sha256:sha256:…and failed the merge. Fixed, and the tag/digest arguments now build as arrays rather than by word-splitting a string.It cannot be exercised further from a branch —
workflow_dispatchonly offers workflows on the default branch. Once merged I'll dispatch it against an existing tag, flip the package public, and confirm an anonymous multi-arch pull.Not in this PR
The four
configure.mdreferences live inCoffey-Labs/ihasmail.org, so they cannot be in the same PR. They need no wording change — they were only wrong because the image did not exist, and publishing makes them true.Merged 2026-09-02 as coffey-labs/ihasmail@e1c85cf82e
Rebuilt from: git history, session transcript.