The deployment failed on the migrate service. Coolify rewrites a relative bind mount to a path under its own application directory, and when that resolves to an empty directory golang-migrate finds no migrations and exits 1 - with nothing useful in the deployment log to say why. A deployment that pulls its images has no business also depending on files being at a particular path on the host. Both mounts are now baked into images built from the repository root: migrations - the schema, so migrate always has it rspamd - actions.conf and ratelimit.conf The Rspamd one mattered for a quieter reason: Rspamd starts happily with an empty local.d, so a mount that resolved to nothing would have left the rate limits and score thresholds unapplied. The defence would have looked configured and done nothing. Both images join the CI and release matrices, building from the root context. Verified by building them, checking their contents, and bringing the whole stack up - Rspamd reports its nine rate directives loaded from the image. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
209 lines
6.9 KiB
YAML
209 lines
6.9 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-${{ github.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: |
|
|
docker run --rm --network host \
|
|
-v "$(pwd)/../../migrations:/migrations" \
|
|
migrate/migrate:v4.17.1 \
|
|
-path=/migrations \
|
|
-database "postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable" \
|
|
up
|
|
- 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: |
|
|
docker run --rm --network host \
|
|
-v "$(pwd)/../../migrations:/migrations" \
|
|
migrate/migrate:v4.17.1 \
|
|
-path=/migrations \
|
|
-database "postgres://lambdamail_test:lambdamail_test@localhost:5432/lambdamail_test?sslmode=disable" \
|
|
up
|
|
- 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.
|
|
- name: Repositório
|
|
uses: aquasecurity/trivy-action@v0.36.0
|
|
with:
|
|
scan-type: 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
|
|
|
|
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
|
|
cache-from: type=gha,scope=${{ matrix.service }}
|
|
cache-to: type=gha,mode=max,scope=${{ matrix.service }}
|
|
|
|
- name: Trivy na imagem
|
|
uses: aquasecurity/trivy-action@v0.36.0
|
|
with:
|
|
image-ref: lambdamail-${{ matrix.service }}:ci
|
|
severity: HIGH,CRITICAL
|
|
exit-code: '1'
|
|
|
|
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
|