CI / Lint de idioma (R9) (push) Successful in 9s
CI / Go (protocols) (push) Failing after 2m22s
CI / TypeScript (storage) (push) Successful in 12s
CI / TypeScript (auth) (push) Failing after 3m42s
CI / Build (webmail) (push) Successful in 1m53s
CI / Stack completa (push) Skipped
CI / Trivy (dependências, segredos e Dockerfiles) (push) Successful in 1m3s
CI / Imagem (migrations) (push) Successful in 1m45s
CI / Imagem (auth) (push) Successful in 2m53s
CI / Imagem (rspamd) (push) Successful in 27s
CI / Imagem (protocols) (push) Successful in 2m10s
CI / Imagem (storage) (push) Canceled after 2m9s
CI / Imagem (webmail) (push) Canceled after 1m6s
The job workspace is a volume the DinD daemon cannot see, so migrations and the Trivy filesystem scan now docker cp their inputs. Trivy's image gate flagged next 15.5.27 (unauthenticated RCE in image optimisation, fixed in 15.5.28) and go-jose 4.1.4 (DoS, fixed in 4.1.6); both are patch bumps. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
251 lines
9.6 KiB
YAML
251 lines
9.6 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 ./...
|
|
- run: go test ./... -v
|
|
env:
|
|
TEST_DATABASE_URL: postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable
|
|
|
|
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" }
|
|
# 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" }
|
|
- run: npm install
|
|
- run: npm run lint
|
|
- run: npm test
|
|
|
|
webmail-build:
|
|
name: Build (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" }
|
|
- run: npm install
|
|
- run: npm run lint
|
|
- run: npm test
|
|
- run: npm run build
|
|
|
|
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 -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 -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
|
|
|
|
- uses: docker/setup-buildx-action@v4
|
|
|
|
# Built but not pushed: this proves each Dockerfile still works on every
|
|
# branch, while publishing stays in release.yml.
|
|
- name: Build
|
|
uses: docker/build-push-action@v7
|
|
with:
|
|
context: ${{ (matrix.service == 'migrations' || matrix.service == 'rspamd') && '.' || format('services/{0}', matrix.service) }}
|
|
file: services/${{ matrix.service }}/Dockerfile
|
|
push: false
|
|
load: true
|
|
tags: lambdamail-${{ matrix.service }}:ci
|
|
|
|
# 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 -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
|