From a3e071bb03e69090657e16efcd887fd976d9724f Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Fri, 30 Oct 2026 12:20:51 -0300 Subject: [PATCH] ci: reuse published images when the image sources are unchanged 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 --- .gitea/workflows/release.yml | 64 +++++++++++++++++++++++++++++++----- CHANGELOG.md | 4 ++- CHANGELOG.pt-BR.md | 4 ++- docs/RELEASING.md | 23 ++++++++++--- 4 files changed, 80 insertions(+), 15 deletions(-) diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index e54dc8e..f06ebd9 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -18,8 +18,46 @@ env: REGISTRY: git.lucasrguerra.dev.br jobs: - # Nothing is published unless the full suite passes first. + # 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". @@ -42,7 +80,8 @@ jobs: # magnitude slower. build: name: Build ${{ matrix.service }} (${{ matrix.platform }}) - needs: [ci, prepare] + needs: [plan, ci, prepare] + if: ${{ needs.plan.outputs.reuse != 'true' }} runs-on: ${{ matrix.runner }} permissions: contents: read @@ -104,7 +143,9 @@ jobs: publish: name: Publica ${{ matrix.service }} - needs: [build, prepare] + 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 @@ -141,13 +182,20 @@ jobs: 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 + # 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 }}' - docker buildx imagetools create \ - $(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}') \ - "$image:build-${{ gitea.sha }}-amd64" "$image:build-${{ gitea.sha }}-arm64" + 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: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 847c8a8..a092802 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -87,7 +87,9 @@ remain are listed in the README under `git.lucasrguerra.dev.br/lucasrguerra/lambdamail/` and the `v*` tag creates a release with notes. See [docs/RELEASING.md](docs/RELEASING.md). - CI caches npm, Go modules, the embedded Postgres, Trivy's database and image - layers. + layers. Release recognises images it already published for the same sources + (`inputs-` tag), so a documentation change or a version tag never + rebuilds them. - One `.env.example` for development and production, and `scripts/init-env.sh` to create `.env` with random secrets (`make env`). It replaces `.env.production.example`. diff --git a/CHANGELOG.pt-BR.md b/CHANGELOG.pt-BR.md index a3448ae..f4284e7 100644 --- a/CHANGELOG.pt-BR.md +++ b/CHANGELOG.pt-BR.md @@ -87,7 +87,9 @@ Gitea. O que ainda falta está no README, em `git.lucasrguerra.dev.br/lucasrguerra/lambdamail/`, e a tag `v*` cria uma release com notas. Veja [docs/RELEASING.md](docs/RELEASING.md). - O CI guarda em cache o npm, os módulos Go, o Postgres embutido, o banco do - Trivy e as camadas das imagens. + Trivy e as camadas das imagens. O Release reconhece imagens que já publicou + para os mesmos fontes (tag `inputs-`), então uma mudança de documentação + ou uma tag de versão nunca as reconstrói. - Um único `.env.example` para desenvolvimento e produção, e o `scripts/init-env.sh` para criar o `.env` com segredos aleatórios (`make env`). Ele substitui o `.env.production.example`. diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 6e93f8a..c12acb3 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -11,10 +11,22 @@ the Oracle server. | Workflow | Runs on | Does | | :--- | :--- | :--- | | `ci.yml` | every push and pull request | language lint, Go and TypeScript suites against a real Postgres, webmail tests, a Trivy scan of the repository and of each image, and the whole stack brought up with compose | -| `release.yml` | a push to `main`, a `v*` tag, or by hand | runs `ci.yml` again, builds the six images for `linux/amd64` and `linux/arm64`, publishes them, and on a tag creates the release | +| `release.yml` | a push to `main`, a `v*` tag, or by hand | builds the six images for `linux/amd64` and `linux/arm64` after `ci.yml` passes, publishes them, and on a tag creates the release | Nothing is published unless the full suite passes first. +### Images are reused, not rebuilt + +The images are built from `services/`, `migrations/` and `config/` and nothing +else. `release.yml` hashes those three trees (the `inputs-` tag) and asks +the registry whether images for that hash already exist. If they do, it skips +the CI run and the build and only points the new tags (`latest`, `sha-`, +the version) at the existing manifests, which takes about a minute. + +So these never recompile anything: a documentation or workflow change merged +to `main`, and a version tag pushed on a commit whose images were just +published. A change under the three trees rebuilds everything, as it should. + Images go to the Gitea registry, one package per service: ``` @@ -39,10 +51,11 @@ logging in. git push gitea v1.2.3 ``` -The tag triggers `release.yml` once more. It republishes the images under -`1.2.3`, `1.2` and `1` (layers are cached, so this is quick) and creates the -entry under **Releases** with the image list and the commit log since the -previous tag. A tag with a hyphen (`v1.3.0-rc.1`) is marked as a pre-release. +The tag triggers `release.yml` once more. Because the commit was already +released from `main`, the images are reused: it adds the tags `1.2.3`, `1.2` and +`1` to them without building, and creates the entry under **Releases** with the +image list and the commit log since the previous tag. Tagging a commit whose +`main` run has not finished (or never ran) builds it from scratch. A tag with a hyphen (`v1.3.0-rc.1`) is marked as a pre-release. Versions follow [Semantic Versioning](https://semver.org/): a change to a migration, a required environment variable, or a published port is at least a