ihasmail called itself 2.0 on About and 2.0.0 from /api/health, both hardcoded — four places in total, drifted from each other and from anything meaningful.
ihasmail v2.16.57
│ │ └─ the pull request the commit came from
│ └──── the Stalwart generation this build targets — 0.16
└─────── ihasmail's own major
The first two live in the root package.json, so there is one place to bump them, and 16 becomes 17 when ihasmail moves to Stalwart 0.17. Dropping 0.15 is what makes that middle number honest — while two generations were supported it could not have been either.
Why nothing is committed
The PR number does not exist until the pull request has merged. A version written into the tree would always be describing a merge that hadn't happened yet, and every open branch would collide on the same line. So it is read from git at build time and never written back.
A commit that didn't arrive through a pull request carries the last number plus its own short SHA — 2.16.57+g1fa6578 — saying plainly that it is past that PR rather than quietly claiming to be it.
node scripts/version.mjs prints the version for a checkout.
Getting it into Docker
.dockerignore excludes .git deliberately, so an image build can't work any of this out. It takes --build-arg IHASMAIL_VERSION instead: the build stage bakes it into the bundle via a Vite define, and the runtime stage keeps it as an env var for the server.
Left out, it falls back to the base version from package.json rather than failing — so a version with no PR number on it means whoever built the image didn't pass one. That seemed better than a build that dies over a cosmetic string.
scripts/ is copied into the runtime image because the server resolves its version through it; there's no git in there to ask, which is the fallback's whole purpose.
Testing
226 web + 75 server tests, typecheck and build clean. Beyond that, the paths that matter here aren't unit-testable, so I exercised them for real:
check
result
dev checkout, HEAD is a PR merge
2.16.57
dev checkout, HEAD is not (verified in a detached worktree at d5636e4)
2.16.58+gd5636e4
bundle after npm run build
contains 2.16.57
/api/health on a dev checkout
2.16.57
real docker build --build-arg, queried inside the container
2.16.57, in both health and the bundle
same image with the arg left off
2.16.0 — falls back, doesn't crash
One thing this PR cannot fix
ihasmail-deploy.sh lives on the host, outside this repo. It currently runs a bare docker build, so production will report 2.16.0 until that line becomes:
I've left that alone rather than reaching into the production host unasked. Happy to apply it at deploy time — it's also worth considering moving that script into the repo so it stops being the one piece that can silently fall out of step.
ihasmail called itself `2.0` on About and `2.0.0` from `/api/health`, both hardcoded — four places in total, drifted from each other and from anything meaningful.
```
ihasmail v2.16.57
│ │ └─ the pull request the commit came from
│ └──── the Stalwart generation this build targets — 0.16
└─────── ihasmail's own major
```
The first two live in the root `package.json`, so there is one place to bump them, and `16` becomes `17` when ihasmail moves to Stalwart 0.17. **Dropping 0.15 is what makes that middle number honest** — while two generations were supported it could not have been either.
## Why nothing is committed
The PR number does not exist until the pull request has merged. A version written into the tree would always be describing a merge that hadn't happened yet, and every open branch would collide on the same line. So it is read from git at build time and never written back.
A commit that didn't arrive through a pull request carries the last number plus its own short SHA — `2.16.57+g1fa6578` — saying plainly that it is *past* that PR rather than quietly claiming to be it.
`node scripts/version.mjs` prints the version for a checkout.
## Getting it into Docker
`.dockerignore` excludes `.git` deliberately, so an image build can't work any of this out. It takes `--build-arg IHASMAIL_VERSION` instead: the build stage bakes it into the bundle via a Vite `define`, and the runtime stage keeps it as an env var for the server.
Left out, it falls back to the base version from `package.json` rather than failing — so **a version with no PR number on it means whoever built the image didn't pass one.** That seemed better than a build that dies over a cosmetic string.
`scripts/` is copied into the runtime image because the server resolves its version through it; there's no git in there to ask, which is the fallback's whole purpose.
## Testing
226 web + 75 server tests, typecheck and build clean. Beyond that, the paths that matter here aren't unit-testable, so I exercised them for real:
| check | result |
|---|---|
| dev checkout, HEAD *is* a PR merge | `2.16.57` |
| dev checkout, HEAD is *not* (verified in a detached worktree at `d5636e4`) | `2.16.58+gd5636e4` |
| bundle after `npm run build` | contains `2.16.57` |
| `/api/health` on a dev checkout | `2.16.57` |
| real `docker build --build-arg`, queried inside the container | `2.16.57`, in both health and the bundle |
| same image with the arg left off | `2.16.0` — falls back, doesn't crash |
## One thing this PR cannot fix
`ihasmail-deploy.sh` lives on the host, outside this repo. It currently runs a bare `docker build`, so **production will report `2.16.0` until that line becomes**:
```bash
docker build --build-arg IHASMAIL_VERSION="$(node scripts/version.mjs)" -t ihasmail:2.0 .
```
I've left that alone rather than reaching into the production host unasked. Happy to apply it at deploy time — it's also worth considering moving that script into the repo so it stops being the one piece that can silently fall out of step.
**Merged** 2026-08-26 as coffey-labs/ihasmail@0613b5bf292d
<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.
ihasmail called itself
2.0on About and2.0.0from/api/health, both hardcoded — four places in total, drifted from each other and from anything meaningful.The first two live in the root
package.json, so there is one place to bump them, and16becomes17when ihasmail moves to Stalwart 0.17. Dropping 0.15 is what makes that middle number honest — while two generations were supported it could not have been either.Why nothing is committed
The PR number does not exist until the pull request has merged. A version written into the tree would always be describing a merge that hadn't happened yet, and every open branch would collide on the same line. So it is read from git at build time and never written back.
A commit that didn't arrive through a pull request carries the last number plus its own short SHA —
2.16.57+g1fa6578— saying plainly that it is past that PR rather than quietly claiming to be it.node scripts/version.mjsprints the version for a checkout.Getting it into Docker
.dockerignoreexcludes.gitdeliberately, so an image build can't work any of this out. It takes--build-arg IHASMAIL_VERSIONinstead: the build stage bakes it into the bundle via a Vitedefine, and the runtime stage keeps it as an env var for the server.Left out, it falls back to the base version from
package.jsonrather than failing — so a version with no PR number on it means whoever built the image didn't pass one. That seemed better than a build that dies over a cosmetic string.scripts/is copied into the runtime image because the server resolves its version through it; there's no git in there to ask, which is the fallback's whole purpose.Testing
226 web + 75 server tests, typecheck and build clean. Beyond that, the paths that matter here aren't unit-testable, so I exercised them for real:
2.16.57d5636e4)2.16.58+gd5636e4npm run build2.16.57/api/healthon a dev checkout2.16.57docker build --build-arg, queried inside the container2.16.57, in both health and the bundle2.16.0— falls back, doesn't crashOne thing this PR cannot fix
ihasmail-deploy.shlives on the host, outside this repo. It currently runs a baredocker build, so production will report2.16.0until that line becomes:I've left that alone rather than reaching into the production host unasked. Happy to apply it at deploy time — it's also worth considering moving that script into the repo so it stops being the one piece that can silently fall out of step.
Merged 2026-08-26 as coffey-labs/ihasmail@0613b5bf29
Rebuilt from: git history, session transcript.