# 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-` 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-`, 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-`, 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-`. 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.