Files
LambdaMail/.gitea/workflows/ci.yml
T
Lucas RayanandClaude Sonnet 5.5 5d4e7147ce ci: cache what is rebuilt on every run
npm and Go module caches, the embedded Postgres binaries, Trivy's database, and
the daemon's own layer cache for image builds. The webmail Next build is no
longer run a second time outside the Dockerfile.

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

283 lines
11 KiB
YAML

name: CI
# Runs on every branch and pull request. Publishing lives in release.yml and
# only runs once this workflow has passed.
#
# main is excluded from the push trigger on purpose: release.yml already calls
# this workflow there. Running both would put two jobs in the same concurrency
# group, and one would cancel the other - which is why every merge used to
# leave a cancelled CI run behind.
on:
push:
branches-ignore: [main]
pull_request:
branches: [main]
workflow_call:
concurrency:
group: ci-${{ gitea.ref }}
cancel-in-progress: true
jobs:
lang-lint:
name: Lint de idioma (R9)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- run: node scripts/lang-lint.mjs
go-protocols:
name: Go (protocols)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: lambdamail_test
POSTGRES_USER: lambdamail_test
POSTGRES_PASSWORD: lambdamail_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
defaults:
run:
working-directory: services/protocols
steps:
- uses: actions/checkout@v7
- uses: actions/setup-go@v7
with:
go-version-file: services/protocols/go.mod
# defaults.run.working-directory applies to run steps, not to
# actions, so setup-go looks for go.sum at the repository root and
# silently skips its cache without this.
cache-dependency-path: services/protocols/go.sum
- name: Run migrations
run: |
# The job's workspace is a volume of its own, invisible to the Docker
# daemon the job talks to, so `-v "$PWD:..."` would mount an empty
# directory. Files are copied into the container instead.
cid=$(docker create --network host migrate/migrate:v4.17.1 \
-path=/migrations \
-database "postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable" \
up)
docker cp "$(pwd)/../../migrations/." "$cid:/migrations"
docker start -a "$cid"
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
docker rm "$cid" >/dev/null
exit "$status"
- run: go vet ./...
# Module and build caches live under the unprivileged user's home, which
# setup-go's cache does not cover, and the embedded Postgres binaries are
# otherwise downloaded again by every test that starts one.
- uses: actions/cache@v4
with:
path: |
/home/tester/go/pkg/mod
/home/tester/.cache/go-build
/home/tester/.embedded-postgres-go
key: go-tester-${{ runner.arch }}-${{ hashFiles('services/protocols/go.sum') }}
restore-keys: go-tester-${{ runner.arch }}-
# The end-to-end suites start an embedded Postgres, and initdb refuses to
# run as root, which is what a job container is. Run the tests as a
# throwaway unprivileged user (GitHub's hosted runners already are one).
- name: Testes
env:
TEST_DATABASE_URL: postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable
run: |
id tester >/dev/null 2>&1 || useradd -m -u 1001 tester
chown -R tester:tester "$GITHUB_WORKSPACE" /home/tester
runuser -u tester -- env "HOME=/home/tester" "PATH=$PATH" \
"TEST_DATABASE_URL=$TEST_DATABASE_URL" go test ./... -v
ts-auth:
name: TypeScript (auth)
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_DB: lambdamail_test
POSTGRES_USER: lambdamail_test
POSTGRES_PASSWORD: lambdamail_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 5s
--health-timeout 5s
--health-retries 10
defaults:
run:
working-directory: services/auth
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "20"
cache: npm
cache-dependency-path: services/auth/package-lock.json
# The auth suite asserts against real rows: password verification,
# lockout, TOTP replay and recovery-code consumption are all decided in
# the database, so a mock would prove nothing.
- name: Run migrations
run: |
# The job's workspace is a volume of its own, invisible to the Docker
# daemon the job talks to, so `-v "$PWD:..."` would mount an empty
# directory. Files are copied into the container instead.
cid=$(docker create --network host migrate/migrate:v4.17.1 \
-path=/migrations \
-database "postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable" \
up)
docker cp "$(pwd)/../../migrations/." "$cid:/migrations"
docker start -a "$cid"
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
docker rm "$cid" >/dev/null
exit "$status"
- run: npm install
- run: npm run lint
- run: npm test
env:
TEST_DATABASE_URL: postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable
LAMBDAMAIL_MASTER_KEY: ci-master-key-at-least-16-chars
ts-storage:
name: TypeScript (storage)
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/storage
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "20"
cache: npm
cache-dependency-path: services/storage/package-lock.json
- run: npm install
- run: npm run lint
- run: npm test
webmail-build:
name: Testes (webmail)
runs-on: ubuntu-latest
defaults:
run:
working-directory: services/webmail
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version: "20"
cache: npm
cache-dependency-path: services/webmail/package-lock.json
- run: npm install
- run: npm run lint
- run: npm test
# No `npm run build` here: the webmail image job runs the same Next build
# inside its Dockerfile, so building it twice only doubles the wait.
security:
name: Trivy (dependências, segredos e Dockerfiles)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
# A mail server holds credentials for every mailbox it serves, so the
# secret scanner matters as much as the vulnerability one here: a DKIM
# key or a Cloudflare token committed by accident is a full compromise.
#
# Trivy runs from its official image rather than aquasecurity/trivy-action:
# that action clones the Trivy repository from the workflow's own server,
# which on Gitea is not GitHub.
- name: Repositório
run: |
cid=$(docker create --network host -v trivy-cache:/root/.cache/ -w /src aquasec/trivy:0.70.0 \
fs --scanners vuln,secret,misconfig --severity HIGH,CRITICAL --exit-code 1 \
--skip-dirs services/auth/node_modules,services/storage/node_modules,services/webmail/node_modules,services/webmail/.next .)
docker cp . "$cid:/src"
docker start -a "$cid"
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
docker rm "$cid" >/dev/null
exit "$status"
# MEDIUM is reported but does not fail the build.
#
# The gate above stops at HIGH, which is why a MEDIUM XSS advisory in
# DOMPurify - the library that sanitises every message body the webmail
# renders - sat unnoticed through green builds. Severity is the
# advisory's opinion, not this project's: a MEDIUM in the HTML sanitiser
# matters more here than a HIGH in a build-time tool.
#
# Reporting rather than gating, because failing on every MEDIUM makes the
# scanner something people learn to skip.
- name: Repositório (MEDIUM, informativo)
run: |
cid=$(docker create --network host -v trivy-cache:/root/.cache/ -w /src aquasec/trivy:0.70.0 \
fs --scanners vuln,secret --severity MEDIUM --exit-code 0 \
--skip-dirs services/auth/node_modules,services/storage/node_modules,services/webmail/node_modules,services/webmail/.next .)
docker cp . "$cid:/src"
docker start -a "$cid"
status=$(docker inspect -f '{{.State.ExitCode}}' "$cid")
docker rm "$cid" >/dev/null
exit "$status"
image:
name: Imagem (${{ matrix.service }})
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
service: [protocols, auth, storage, webmail, migrations, rspamd]
steps:
- uses: actions/checkout@v7
# Built but not pushed: this proves each Dockerfile still works on every
# branch, while publishing stays in release.yml.
#
# Plain `docker build` on purpose. setup-buildx would start a fresh
# BuildKit container per job and throw its layer cache away; the daemon's
# own builder keeps the cache in the runner's persistent volume, and the
# "Stack completa" job reuses it for the same Dockerfiles.
- name: Build
run: |
case '${{ matrix.service }}' in
migrations|rspamd) context=. ;;
*) context=services/${{ matrix.service }} ;;
esac
docker build -f services/${{ matrix.service }}/Dockerfile \
-t lambdamail-${{ matrix.service }}:ci "$context"
# Scanned only for the images whose contents we author. migrations and
# rspamd add .sql and .conf files to an upstream image and nothing else,
# so every finding in them belongs to that upstream project and cannot be
# fixed here - gating on it would gate this pipeline on somebody else's
# release cadence. They are still built above, and OPERATIONS.md records
# that their base images have to be watched.
- name: Trivy na imagem
if: matrix.service != 'migrations' && matrix.service != 'rspamd'
run: |
docker run --rm --network host -v trivy-cache:/root/.cache/ -e DOCKER_HOST=tcp://127.0.0.1:2375 aquasec/trivy:0.70.0 \
image --severity HIGH,CRITICAL --exit-code 1 lambdamail-${{ matrix.service }}:ci
compose-up:
name: Stack completa
runs-on: ubuntu-latest
needs: [go-protocols, ts-auth, ts-storage, webmail-build]
steps:
- uses: actions/checkout@v7
- run: cp .env.example .env
# The proxy network is declared external because Coolify owns it on the
# deploy host. Nothing creates it here, and without it compose refuses to
# start the stack at all.
- name: Cria a rede do proxy
run: docker network create "${COOLIFY_PROXY_NETWORK:-coolify}"
# The build overlay is explicit here: CI has to exercise the code in the
# branch, not the images already published from main.
- run: docker compose -f docker-compose.yaml -f docker-compose.build.yaml --env-file .env up --build --wait
- run: docker compose -f docker-compose.yaml -f docker-compose.build.yaml ps
- run: docker compose -f docker-compose.yaml -f docker-compose.build.yaml down -v