Files
LambdaMail/.github/workflows/release.yml
T
Lucas RayanandClaude Opus 5 13fd2a9cee #21: ship the migrations and the Rspamd config inside images
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>
2026-08-02 14:37:26 -03:00

181 lines
6.4 KiB
YAML

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"