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"