Files
LambdaMail/docs/RELEASING.md
T
Lucas RayanandClaude Sonnet 5.5 a3e071bb03 ci: reuse published images when the image sources are unchanged
Release hashes services/, migrations/ and config/; when images for that hash
already exist it skips CI and the build and only applies the new tags. Tagging
a release no longer recompiles the webmail.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-30 12:20:51 -03:00

102 lines
4.6 KiB
Markdown

# Releasing LambdaMail
How a version gets from a commit to a server, and what runs where. The history
of what changed is in the [CHANGELOG](../CHANGELOG.md).
## The pipeline
Everything runs on **Gitea Actions** (`.gitea/workflows/`), on runners hosted on
the Oracle server.
| Workflow | Runs on | Does |
| :--- | :--- | :--- |
| `ci.yml` | every push and pull request | language lint, Go and TypeScript suites against a real Postgres, webmail tests, a Trivy scan of the repository and of each image, and the whole stack brought up with compose |
| `release.yml` | a push to `main`, a `v*` tag, or by hand | builds the six images for `linux/amd64` and `linux/arm64` after `ci.yml` passes, publishes them, and on a tag creates the release |
Nothing is published unless the full suite passes first.
### Images are reused, not rebuilt
The images are built from `services/`, `migrations/` and `config/` and nothing
else. `release.yml` hashes those three trees (the `inputs-<hash>` tag) and asks
the registry whether images for that hash already exist. If they do, it skips
the CI run and the build and only points the new tags (`latest`, `sha-<commit>`,
the version) at the existing manifests, which takes about a minute.
So these never recompile anything: a documentation or workflow change merged
to `main`, and a version tag pushed on a commit whose images were just
published. A change under the three trees rebuilds everything, as it should.
Images go to the Gitea registry, one package per service:
```
git.lucasrguerra.dev.br/lucasrguerra/lambdamail/{protocols,auth,storage,webmail,migrations,rspamd}
```
Tags on every image: `latest`, `sha-<full commit>`, and on a version tag also
`1.2.3`, `1.2` and `1`. The packages are public, so a server pulls them without
logging in.
## Cutting a release
1. Update [CHANGELOG.md](../CHANGELOG.md) and [CHANGELOG.pt-BR.md](../CHANGELOG.pt-BR.md):
move what is under *Unreleased* to a new `## [X.Y.Z] - date` section.
2. Set `"version"` in `services/{auth,storage,webmail}/package.json` and their
lockfiles to the same number.
3. Merge to `main` and wait for the `Release` run of that merge to go green.
4. Tag the merge commit and push the tag:
```bash
git tag -a v1.2.3 -m "LambdaMail 1.2.3"
git push gitea v1.2.3
```
The tag triggers `release.yml` once more. Because the commit was already
released from `main`, the images are reused: it adds the tags `1.2.3`, `1.2` and
`1` to them without building, and creates the entry under **Releases** with the
image list and the commit log since the previous tag. Tagging a commit whose
`main` run has not finished (or never ran) builds it from scratch. A tag with a hyphen (`v1.3.0-rc.1`) is marked as a pre-release.
Versions follow [Semantic Versioning](https://semver.org/): a change to a
migration, a required environment variable, or a published port is at least a
minor release, and anything that needs manual steps on upgrade is called out in
the changelog.
## Deploying a version
Coolify pulls; it does not build. See the *Upgrading* section of
[DEPLOYMENT.md](DEPLOYMENT.md) for the two-step deploy (set `IMAGE_TAG`, then
deploy). In short:
1. `IMAGE_TAG` = the release (`1.2.3`) or `sha-<commit>`. Never `latest`.
2. Deploy. Migrations run before the services that need them.
3. Check `GET /health` on `protocols` and sign in to the webmail.
To roll back, set `IMAGE_TAG` back to the previous release and deploy again.
Migrations are not reverted: write them so the previous version still runs
against the new schema.
## Runners
Two `act_runner` instances run on the Oracle server (Ampere ARM, 4 cores), each
with its own Docker-in-Docker daemon, so two jobs never share a network or a
port. The jobs run unprivileged.
- `linux/arm64` images build natively; `linux/amd64` images build under QEMU
emulation, which is slower. Layers are cached in the registry.
- The Docker daemon of each runner keeps its layer cache between runs, and the
toolcache volume keeps downloaded toolchains.
- The workspace of a job is not visible to the Docker daemon, so workflows copy
files into containers with `docker cp` instead of mounting them with `-v`.
- Windows and macOS jobs cannot run here.
## Why some actions are not used
- `aquasecurity/trivy-action` clones Trivy from the workflow's own server,
which is Gitea; Trivy runs from its official image instead.
- `actions/upload-artifact` v4+ and the `gha` build cache are GitHub services;
per-architecture images are told apart by tag, and the cache lives in the
registry.
- Workflows read `GITHUB_OUTPUT` and `GITHUB_STEP_SUMMARY` under those names:
the `GITEA_*` equivalents are not set by the runner.