From 4ad130a3345c1af889d1e74bbc0187816113c03f Mon Sep 17 00:00:00 2001 From: Lucas Rayan Date: Sun, 1 Nov 2026 08:08:52 -0300 Subject: [PATCH] ci: build, test and publish the image once, then let the release reuse it Drop the pull_request trigger (the branch push already validates the commit), drop the second npm run build from the lint job (the image build compiles it), and build the image on arm64 after the cheap checks pass. The CI publishes it as inputs-, so the merge to main only re-tags it. Co-Authored-By: Claude Sonnet 5.5 --- .gitea/workflows/ci.yml | 72 +++++++++++++++++++++++++----------- .gitea/workflows/release.yml | 35 ++++++------------ scripts/plano-imagem.sh | 21 +++++++++++ 3 files changed, 84 insertions(+), 44 deletions(-) create mode 100755 scripts/plano-imagem.sh diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index b355f43..06aad8e 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -3,9 +3,13 @@ name: CI # Documentação e configuração de ferramenta não mudam nada que a suíte teste. # Na main não roda por push: o release.yml é quem a chama, e só quando a imagem # precisa ser reconstruída; rodar nos dois repetiria a mesma suíte no mesmo commit. +# Sem gatilho de pull_request: o push da branch já valida o mesmo commit, e os +# dois juntos rodariam tudo duas vezes (o repositório não recebe PR de fork). +# +# A imagem é construída aqui uma única vez, em arm64 (a arquitetura do servidor), +# testada e publicada como inputs-. Ao fazer o merge, o release encontra +# essa tag e só aplica os nomes novos, sem rodar nem construir nada de novo. on: - pull_request: - paths-ignore: ['**.md', 'LICENSE', '.claude/**', '.superset/**', '.wolf/**', 'scripts/release-notes.sh'] push: branches-ignore: [main] paths-ignore: ['**.md', 'LICENSE', '.claude/**', '.superset/**', '.wolf/**', 'scripts/release-notes.sh'] @@ -20,7 +24,7 @@ concurrency: jobs: verificar: - name: Lint, design e build + name: Lint e auditorias runs-on: ubuntu-latest timeout-minutes: 15 env: @@ -35,24 +39,11 @@ jobs: node-version: 22 cache: npm - # Next.js guarda aqui o resultado da compilação anterior; sem isso, cada - # execução recompila todas as páginas do zero. - - name: Cache de build do Next - uses: actions/cache@v4 - with: - path: .next/cache - key: next-${{ runner.arch }}-${{ hashFiles('package-lock.json') }}-${{ hashFiles('app/**', 'components/**', 'lib/**') }} - restore-keys: | - next-${{ runner.arch }}-${{ hashFiles('package-lock.json') }}- - next-${{ runner.arch }}- - - run: npm ci # eslint + auditoria do design system (tokens, contraste AAA) + auditoria de páginas - run: npm run check - - run: npm run build - # Trivy roda pela imagem oficial: o trivy-action clona o repositório do Trivy # do mesmo servidor do workflow, que aqui é o Gitea, e não o GitHub. # O workspace do job é um volume que o daemon não enxerga, então os arquivos @@ -72,20 +63,51 @@ jobs: docker rm "$cid" >/dev/null exit "$status" + # Só começa depois das verificações baratas: um lint quebrado não gasta um + # build de imagem. Roda no runner arm64, o mesmo tipo do servidor, então o + # que é escaneado e testado é a imagem que vai ao ar. imagem: - name: Imagem Docker - runs-on: ubuntu-latest + name: Imagem Docker (linux/arm64) + needs: [verificar, seguranca] + runs-on: ubuntu-24.04-arm + env: + REGISTRY: git.lucasrguerra.dev.br steps: - uses: actions/checkout@v4 - # `docker build` simples: o cache de camadas fica no volume persistente do - # daemon do runner, e o buildx criaria um BuildKit novo a cada job. + + # Conteúdo já publicado (re-push, tag num commit já validado): nada a fazer. + - id: plano + run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT" + + - if: ${{ steps.plano.outputs.reuse != 'true' }} + uses: docker/setup-buildx-action@v4 + + - if: ${{ steps.plano.outputs.reuse != 'true' }} + uses: docker/login-action@v4 + with: + registry: ${{ env.REGISTRY }} + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + - name: Build - run: docker build -t espdocs:ci . + if: ${{ steps.plano.outputs.reuse != 'true' }} + uses: docker/build-push-action@v7 + with: + context: . + platforms: linux/arm64 + load: true + tags: espdocs:ci + cache-from: type=registry,ref=${{ steps.plano.outputs.image }}:buildcache + cache-to: type=registry,ref=${{ steps.plano.outputs.image }}:buildcache,mode=max + - name: Trivy na imagem + if: ${{ steps.plano.outputs.reuse != 'true' }} run: | docker run --rm --network host -v trivy-cache:/root/.cache/ -e DOCKER_HOST=tcp://127.0.0.1:2375 aquasec/trivy:0.70.0 \ image --severity HIGH,CRITICAL --exit-code 1 espdocs:ci + - name: A imagem sobe e responde + if: ${{ steps.plano.outputs.reuse != 'true' }} run: | docker run -d --name espdocs-ci --network host -e PORT=3999 espdocs:ci for i in $(seq 1 30); do @@ -95,3 +117,11 @@ jobs: docker logs espdocs-ci | tail -5 docker rm -f espdocs-ci >/dev/null test "${ok:-0}" = 1 + + # Passou em tudo: publica a mesma imagem que foi testada, sem reconstruir. + - name: Publica inputs- + if: ${{ steps.plano.outputs.reuse != 'true' }} + run: | + tag="${{ steps.plano.outputs.image }}:inputs-${{ steps.plano.outputs.inputs }}" + docker tag espdocs:ci "$tag" + docker push "$tag" diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml index 44d15a8..0c3ea11 100644 --- a/.gitea/workflows/release.yml +++ b/.gitea/workflows/release.yml @@ -18,10 +18,10 @@ env: jobs: # A imagem só depende dos arquivos que entram no build, não da documentação nem - # da configuração de ferramentas. Quando já existe no registro uma imagem para - # o mesmo conteúdo (um merge só de docs, uma mudança de workflow, uma tag num - # commit que acabou de ser publicado), não há o que construir: as tags novas - # apontam para o manifesto existente. + # da configuração de ferramentas. O push da branch já a construiu, testou e + # publicou como inputs-; quando essa tag existe (o caso normal de um + # merge, uma tag num commit validado, um merge só de docs), não há o que + # construir: as tags novas apontam para o manifesto existente. plano: name: Plano (reaproveitar a imagem?) runs-on: ubuntu-latest @@ -32,23 +32,7 @@ jobs: steps: - uses: actions/checkout@v4 - id: plano - env: - REPO: ${{ gitea.repository }} - run: | - inputs=$(git ls-tree -r HEAD \ - | awk -F'\t' '$2 !~ /^(\.gitea|\.github|\.claude|\.superset|docs)\// && $2 !~ /^(README.*|CLAUDE\.md|DESIGN\.md|LICENSE|\.gitignore|CHANGELOG\.md|scripts\/release-notes\.sh)$/' \ - | sha256sum | cut -c1-16) - ns="${REPO,,}" - token=$(curl -fsS "https://$REGISTRY/v2/token?service=container_registry&scope=repository:$ns: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,application/vnd.oci.image.manifest.v1+json' \ - "https://$REGISTRY/v2/$ns/manifests/inputs-$inputs") - echo "inputs-$inputs -> HTTP $code" - reuse=false; [ "$code" = 200 ] && reuse=true - echo "inputs=$inputs" >> "$GITHUB_OUTPUT" - echo "reuse=$reuse" >> "$GITHUB_OUTPUT" - echo "image=$REGISTRY/$ns" >> "$GITHUB_OUTPUT" + run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT" # Nada é publicado sem a suíte completa passar antes. Pulada quando a imagem é # reaproveitada: o mesmo conteúdo já passou por ela. @@ -68,6 +52,11 @@ jobs: steps: - uses: actions/checkout@v4 + # Se o ci chamado acima acabou de publicar a imagem (push direto na main), + # o plano lá de cima está desatualizado: confere de novo antes de construir. + - id: replano + run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT" + - uses: docker/setup-buildx-action@v4 - name: Autentica no registro @@ -89,7 +78,7 @@ jobs: type=sha,format=long - name: Build e push - if: ${{ needs.plano.outputs.reuse != 'true' }} + if: ${{ steps.replano.outputs.reuse != 'true' }} uses: docker/build-push-action@v7 with: context: . @@ -104,7 +93,7 @@ jobs: # Mesmo manifesto, nomes novos: nenhuma camada é lida nem escrita. - name: Aplica as tags à imagem existente - if: ${{ needs.plano.outputs.reuse == 'true' }} + if: ${{ steps.replano.outputs.reuse == 'true' }} run: | tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}') docker buildx imagetools create $tags "${{ needs.plano.outputs.image }}:inputs-${{ needs.plano.outputs.inputs }}" diff --git a/scripts/plano-imagem.sh b/scripts/plano-imagem.sh new file mode 100755 index 0000000..6cc04f7 --- /dev/null +++ b/scripts/plano-imagem.sh @@ -0,0 +1,21 @@ +#!/usr/bin/env bash +# Uso: scripts/plano-imagem.sh +# Imprime inputs=, image= e reuse= (para $GITHUB_OUTPUT). O hash cobre só os +# arquivos que entram no build da imagem, não a documentação nem a configuração +# de ferramentas. reuse=true quando o registro já tem inputs-. +set -euo pipefail +registry=$1; ns=${2,,} + +inputs=$(git ls-tree -r HEAD \ + | awk -F'\t' '$2 !~ /^(\.gitea|\.github|\.claude|\.superset|docs)\// && $2 !~ /^(README.*|CLAUDE\.md|DESIGN\.md|LICENSE|\.gitignore|CHANGELOG\.md|scripts\/release-notes\.sh)$/' \ + | sha256sum | cut -c1-16) +token=$(curl -fsS "https://$registry/v2/token?service=container_registry&scope=repository:$ns: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,application/vnd.oci.image.manifest.v1+json' \ + "https://$registry/v2/$ns/manifests/inputs-$inputs") +echo "inputs-$inputs -> HTTP $code" >&2 +reuse=false; [ "$code" = 200 ] && reuse=true +echo "inputs=$inputs" +echo "image=$registry/$ns" +echo "reuse=$reuse"