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>
283 lines
11 KiB
YAML
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
|