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- 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] # Explicit, like publish: the build job upstream is skipped when the images # are reused, and the implicit success() would then skip this job too. if: ${{ !cancelled() && needs.publish.result == 'success' && 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: | scripts/release-notes.sh "$TAG" "$NAMESPACE" > 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