ci: publish images to the Gitea registry and move workflows to .gitea
CI / Lint de idioma (R9) (push) Successful in 3s
CI / Go (protocols) (push) Failing after 20s
CI / TypeScript (auth) (push) Canceled after 4s
CI / TypeScript (storage) (push) Canceled after 0s
CI / Build (webmail) (push) Canceled after 0s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Canceled after 0s
CI / Imagem (auth) (push) Canceled after 0s
CI / Imagem (migrations) (push) Canceled after 0s
CI / Imagem (protocols) (push) Canceled after 0s
CI / Imagem (rspamd) (push) Canceled after 0s
CI / Imagem (storage) (push) Canceled after 0s
CI / Imagem (webmail) (push) Canceled after 0s
CI / Stack completa (push) Canceled after 0s
CI / Lint de idioma (R9) (push) Successful in 3s
CI / Go (protocols) (push) Failing after 20s
CI / TypeScript (auth) (push) Canceled after 4s
CI / TypeScript (storage) (push) Canceled after 0s
CI / Build (webmail) (push) Canceled after 0s
CI / Trivy (dependências, segredos e Dockerfiles) (push) Canceled after 0s
CI / Imagem (auth) (push) Canceled after 0s
CI / Imagem (migrations) (push) Canceled after 0s
CI / Imagem (protocols) (push) Canceled after 0s
CI / Imagem (rspamd) (push) Canceled after 0s
CI / Imagem (storage) (push) Canceled after 0s
CI / Imagem (webmail) (push) Canceled after 0s
CI / Stack completa (push) Canceled after 0s
Release now pushes to git.lucasrguerra.dev.br instead of ghcr.io, tags each architecture separately (no artifact upload, no gha cache) and joins them with imagetools. Workflows live in .gitea/workflows and use the gitea context. GITHUB_OUTPUT and GITHUB_STEP_SUMMARY stay: they are the runner's own interface. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
fa2dcb578e
commit
9a722bc5ac
2 files changed
No files matched your search
@@ -0,0 +1,234 @@
|
||||
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
|
||||
|
||||
# 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)
|
||||
uses: aquasecurity/trivy-action@v0.36.0
|
||||
with:
|
||||
scan-type: 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
|
||||
|
||||
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 }}
|
||||
|
||||
# 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'
|
||||
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
|
||||
@@ -0,0 +1,180 @@
|
||||
name: Release
|
||||
|
||||
# Publishes the four service images to GHCR after a merge into main, and again
|
||||
# under semver tags when a v* tag is pushed. The deploy host only pulls, so
|
||||
# nothing has to be built on the server.
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
tags: ['v*']
|
||||
workflow_dispatch:
|
||||
|
||||
concurrency:
|
||||
group: release
|
||||
cancel-in-progress: false
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
jobs:
|
||||
# Nothing is published unless the full suite passes first.
|
||||
ci:
|
||||
uses: ./.github/workflows/ci.yml
|
||||
|
||||
# GHCR rejects an uppercase path, and this repository is "lucasrguerra/LambdaMail".
|
||||
# Lowercasing github.repository here keeps the workflow fork-safe instead of
|
||||
# hardcoding the owner.
|
||||
prepare:
|
||||
name: Namespace das imagens
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
namespace: ${{ steps.namespace.outputs.value }}
|
||||
steps:
|
||||
- id: namespace
|
||||
run: echo "value=${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# Each architecture is built on a runner of its own kind. QEMU emulation
|
||||
# would also work, but it makes the Go and Next builds roughly an order of
|
||||
# magnitude slower.
|
||||
build:
|
||||
name: Build ${{ matrix.service }} (${{ matrix.platform }})
|
||||
needs: [ci, prepare]
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
# Both are axes, so this is a cross product: four services on two
|
||||
# architectures is eight jobs. Declaring platform only under `include`
|
||||
# would not multiply anything - include can add keys to existing
|
||||
# combinations, but it cannot create the combinations themselves.
|
||||
service: [protocols, auth, storage, webmail, migrations, rspamd]
|
||||
platform: [linux/amd64, linux/arm64]
|
||||
include:
|
||||
- platform: linux/amd64
|
||||
runner: ubuntu-latest
|
||||
arch: amd64
|
||||
- platform: linux/arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
arch: arm64
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Autentica no GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Metadados
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}
|
||||
|
||||
# Pushed by digest only: the tags are attached later, once both
|
||||
# architectures exist and can be joined into a single manifest list.
|
||||
- name: Build e push por digest
|
||||
id: 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
|
||||
platforms: ${{ matrix.platform }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
outputs: type=image,name=${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }},push-by-digest=true,name-canonical=true,push=true
|
||||
cache-from: type=gha,scope=${{ matrix.service }}-${{ matrix.arch }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.service }}-${{ matrix.arch }}
|
||||
|
||||
- name: Exporta o digest
|
||||
run: |
|
||||
mkdir -p /tmp/digests
|
||||
# The output already carries the `sha256:` prefix; the file name must
|
||||
# not, since the merge step prepends it again.
|
||||
digest='${{ steps.build.outputs.digest }}'
|
||||
touch "/tmp/digests/${digest#sha256:}"
|
||||
|
||||
- name: Guarda o digest
|
||||
uses: actions/upload-artifact@v7
|
||||
with:
|
||||
# Slashes are not allowed in artifact names, hence the arch alias.
|
||||
name: digest-${{ matrix.service }}-${{ matrix.arch }}
|
||||
path: /tmp/digests/*
|
||||
retention-days: 1
|
||||
if-no-files-found: error
|
||||
|
||||
publish:
|
||||
name: Publica ${{ matrix.service }}
|
||||
needs: [build, prepare]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
service: [protocols, auth, storage, webmail, migrations, rspamd]
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
- uses: docker/setup-buildx-action@v4
|
||||
|
||||
- name: Autentica no GHCR
|
||||
uses: docker/login-action@v4
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Recupera os digests
|
||||
uses: actions/download-artifact@v7
|
||||
with:
|
||||
pattern: digest-${{ matrix.service }}-*
|
||||
merge-multiple: true
|
||||
path: /tmp/digests
|
||||
|
||||
# `latest` is what compose pulls. The semver tags only materialise on a
|
||||
# v* tag push and make it possible to pin or roll back to an exact
|
||||
# release; the sha tag does the same for any main build.
|
||||
- name: Metadados
|
||||
id: meta
|
||||
uses: docker/metadata-action@v6
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}
|
||||
tags: |
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
type=raw,value=latest
|
||||
type=sha,format=long
|
||||
|
||||
# Joins both architectures under one manifest list, so `docker pull`
|
||||
# resolves to the right one automatically.
|
||||
- name: Cria o manifesto multi-arquitetura
|
||||
working-directory: /tmp/digests
|
||||
run: |
|
||||
docker buildx imagetools create \
|
||||
$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}') \
|
||||
$(printf '${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}@sha256:%s ' *)
|
||||
|
||||
- name: Confere o resultado
|
||||
run: |
|
||||
docker buildx imagetools inspect \
|
||||
"${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}:latest" \
|
||||
--format '{{json .Manifest}}' \
|
||||
| jq -r '.manifests[] | select(.platform.os == "linux") | "\(.platform.os)/\(.platform.architecture)"'
|
||||
|
||||
- name: Resumo
|
||||
run: |
|
||||
{
|
||||
echo "### ${{ matrix.service }} publicado (linux/amd64 e linux/arm64)"
|
||||
echo
|
||||
echo '```'
|
||||
echo "${{ steps.meta.outputs.tags }}"
|
||||
echo '```'
|
||||
} >> "$GITHUB_STEP_SUMMARY"
|
||||
Reference in new issue
Block a user