Files
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

4.6 KiB

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.

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 and 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:

    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: 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 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.