Release hashes services/, migrations/ and config/; when images for that hash already exist it skips CI and the build and only applies the new tags. Tagging a release no longer recompiles the webmail. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
256 lines
10 KiB
YAML
256 lines
10 KiB
YAML
name: Release
|
|
|
|
# Publishes the service images to the Gitea container registry 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. Needs the user
|
|
# secret REGISTRY_TOKEN (a Gitea token with the write:package scope).
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
tags: ['v*']
|
|
workflow_dispatch:
|
|
|
|
concurrency:
|
|
group: release
|
|
cancel-in-progress: false
|
|
|
|
env:
|
|
REGISTRY: git.lucasrguerra.dev.br
|
|
|
|
jobs:
|
|
# The images are built from services/, migrations/ and config/ and nothing
|
|
# else, so those three trees identify them. When an image for the same trees
|
|
# is already in the registry (a docs-only merge, a workflow change, or a tag on
|
|
# a commit that was just released), there is nothing to build: the new tags
|
|
# are pointed at the existing manifest. This is what keeps tagging a release
|
|
# from compiling the webmail a second time.
|
|
plan:
|
|
name: Plano (reaproveitar as imagens?)
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
inputs: ${{ steps.plan.outputs.inputs }}
|
|
reuse: ${{ steps.plan.outputs.reuse }}
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
- id: plan
|
|
env:
|
|
REGISTRY: ${{ env.REGISTRY }}
|
|
REPO: ${{ gitea.repository }}
|
|
run: |
|
|
inputs=$(git rev-parse HEAD:services HEAD:migrations HEAD:config | sha256sum | cut -c1-16)
|
|
ns="${REPO,,}"
|
|
reuse=true
|
|
for s in protocols auth storage webmail migrations rspamd; do
|
|
token=$(curl -fsS "https://$REGISTRY/v2/token?service=container_registry&scope=repository:$ns/$s:pull" | jq -r .token)
|
|
code=$(curl -s -o /dev/null -w '%{http_code}' -I \
|
|
-H "Authorization: Bearer $token" \
|
|
-H 'Accept: application/vnd.oci.image.index.v1+json,application/vnd.docker.distribution.manifest.list.v2+json' \
|
|
"https://$REGISTRY/v2/$ns/$s/manifests/inputs-$inputs")
|
|
echo "$s: inputs-$inputs -> HTTP $code"
|
|
[ "$code" = 200 ] || reuse=false
|
|
done
|
|
echo "inputs=$inputs" >> "$GITHUB_OUTPUT"
|
|
echo "reuse=$reuse" >> "$GITHUB_OUTPUT"
|
|
echo "reuse the published images: $reuse"
|
|
|
|
# Nothing is published unless the full suite passes first. Skipped when the
|
|
# images are reused: the same trees already went through it.
|
|
ci:
|
|
needs: plan
|
|
if: ${{ needs.plan.outputs.reuse != 'true' }}
|
|
uses: ./.gitea/workflows/ci.yml
|
|
|
|
# Registry paths must be lowercase, and this repository is "lucasrguerra/LambdaMail".
|
|
# Lowercasing gitea.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
|
|
# GITHUB_OUTPUT is the runner's own interface: Gitea sets it under that name.
|
|
run: |
|
|
repo='${{ gitea.repository }}'
|
|
echo "value=${repo,,}" >> "$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: [plan, ci, prepare]
|
|
if: ${{ needs.plan.outputs.reuse != 'true' }}
|
|
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: linux-amd64
|
|
arch: amd64
|
|
- platform: linux/arm64
|
|
runner: ubuntu-24.04-arm
|
|
arch: arm64
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
# The only runner is arm64; linux/amd64 is built under QEMU emulation
|
|
# (label linux-amd64 points at the same runner), so it is slower.
|
|
- uses: docker/setup-qemu-action@v3
|
|
if: matrix.arch == 'amd64'
|
|
|
|
- uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Autentica no registro
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ gitea.actor }}
|
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
|
|
- name: Metadados
|
|
id: meta
|
|
uses: docker/metadata-action@v6
|
|
with:
|
|
images: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}
|
|
|
|
# Each architecture is pushed under its own tag (no digest artifacts: they
|
|
# are not portable to Gitea). The final tags are attached later, once both
|
|
# exist and can be joined into a single manifest list.
|
|
- name: Build e push por arquitetura
|
|
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 }}
|
|
push: true
|
|
tags: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}:build-${{ gitea.sha }}-${{ matrix.arch }}
|
|
cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}:buildcache-${{ matrix.arch }}
|
|
cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}:buildcache-${{ matrix.arch }},mode=max
|
|
|
|
publish:
|
|
name: Publica ${{ matrix.service }}
|
|
needs: [plan, build, prepare]
|
|
# build is skipped, not successful, when the images are reused.
|
|
if: ${{ !cancelled() && needs.plan.result == 'success' && needs.prepare.result == 'success' && (needs.build.result == 'success' || needs.plan.outputs.reuse == 'true') }}
|
|
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 registro
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ gitea.actor }}
|
|
password: ${{ secrets.REGISTRY_TOKEN }}
|
|
|
|
# `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. The inputs-<hash> tag is what
|
|
# lets a later run recognise these images and skip the build.
|
|
- name: Aplica as tags
|
|
run: |
|
|
image='${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}/${{ matrix.service }}'
|
|
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}')
|
|
inputs='${{ needs.plan.outputs.inputs }}'
|
|
if [ '${{ needs.plan.outputs.reuse }}' = true ]; then
|
|
# Same manifest under new names: no layer is read or written.
|
|
docker buildx imagetools create $tags "$image:inputs-$inputs"
|
|
else
|
|
docker buildx imagetools create $tags -t "$image:inputs-$inputs" \
|
|
"$image:build-${{ gitea.sha }}-amd64" "$image:build-${{ gitea.sha }}-arm64"
|
|
fi
|
|
|
|
- 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"
|
|
|
|
# Cria a entrada em "Releases" do repositório. Só roda quando uma tag v* é
|
|
# enviada, depois que todas as imagens foram publicadas. Usa o token do próprio
|
|
# Actions, então não precisa de segredo extra.
|
|
gitea-release:
|
|
name: Cria a release no Gitea
|
|
needs: [publish, prepare]
|
|
if: startsWith(gitea.ref, 'refs/tags/v')
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
with:
|
|
fetch-depth: 0
|
|
- name: Release
|
|
env:
|
|
TAG: ${{ gitea.ref_name }}
|
|
TOKEN: ${{ gitea.token }}
|
|
API: ${{ gitea.server_url }}/api/v1/repos/${{ gitea.repository }}
|
|
NAMESPACE: ${{ env.REGISTRY }}/${{ needs.prepare.outputs.namespace }}
|
|
run: |
|
|
prev=$(git describe --tags --abbrev=0 "$TAG^" 2>/dev/null || true)
|
|
range=${prev:+$prev..}$TAG
|
|
{
|
|
echo "## Imagens"
|
|
echo
|
|
echo '```'
|
|
for s in protocols auth storage webmail migrations rspamd; do
|
|
echo "$NAMESPACE/$s:${TAG#v}"
|
|
done
|
|
echo '```'
|
|
echo
|
|
echo "## Mudanças${prev:+ desde $prev}"
|
|
echo
|
|
git log --no-merges --format='- %s (%h)' "$range"
|
|
} > notes.md
|
|
pre=false; case "$TAG" in *-*) pre=true ;; esac
|
|
jq -n --arg tag "$TAG" --arg name "LambdaMail ${TAG#v}" --rawfile body notes.md --argjson pre "$pre" \
|
|
'{tag_name:$tag, name:$name, body:$body, prerelease:$pre}' > release.json
|
|
curl -fsS -X POST "$API/releases" -H "Authorization: token $TOKEN" \
|
|
-H 'Content-Type: application/json' -d @release.json
|