ci: build, test and publish the image once, then let the release reuse it
CI / Lint e auditorias (push) Canceled after 46s
CI / Trivy (dependências e segredos) (push) Canceled after 0s
CI / Imagem Docker (linux/arm64) (push) Canceled after 0s

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-<hash>, so the merge to main only re-tags it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-11-01 08:08:52 -03:00
1 parent 7d0edf13c4
commit 4ad130a334
3 files changed
+84 -44

No files matched your search

+51 -21
View File
@@ -3,9 +3,13 @@ name: CI
# Documentação e configuração de ferramenta não mudam nada que a suíte teste. # 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 # 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. # 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-<hash>. Ao fazer o merge, o release encontra
# essa tag e só aplica os nomes novos, sem rodar nem construir nada de novo.
on: on:
pull_request:
paths-ignore: ['**.md', 'LICENSE', '.claude/**', '.superset/**', '.wolf/**', 'scripts/release-notes.sh']
push: push:
branches-ignore: [main] branches-ignore: [main]
paths-ignore: ['**.md', 'LICENSE', '.claude/**', '.superset/**', '.wolf/**', 'scripts/release-notes.sh'] paths-ignore: ['**.md', 'LICENSE', '.claude/**', '.superset/**', '.wolf/**', 'scripts/release-notes.sh']
@@ -20,7 +24,7 @@ concurrency:
jobs: jobs:
verificar: verificar:
name: Lint, design e build name: Lint e auditorias
runs-on: ubuntu-latest runs-on: ubuntu-latest
timeout-minutes: 15 timeout-minutes: 15
env: env:
@@ -35,24 +39,11 @@ jobs:
node-version: 22 node-version: 22
cache: npm 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 - run: npm ci
# eslint + auditoria do design system (tokens, contraste AAA) + auditoria de páginas # eslint + auditoria do design system (tokens, contraste AAA) + auditoria de páginas
- run: npm run check - run: npm run check
- run: npm run build
# Trivy roda pela imagem oficial: o trivy-action clona o repositório do Trivy # 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. # 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 # 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 docker rm "$cid" >/dev/null
exit "$status" 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: imagem:
name: Imagem Docker name: Imagem Docker (linux/arm64)
runs-on: ubuntu-latest needs: [verificar, seguranca]
runs-on: ubuntu-24.04-arm
env:
REGISTRY: git.lucasrguerra.dev.br
steps: steps:
- uses: actions/checkout@v4 - 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 - 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 - name: Trivy na imagem
if: ${{ steps.plano.outputs.reuse != 'true' }}
run: | 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 \ 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 image --severity HIGH,CRITICAL --exit-code 1 espdocs:ci
- name: A imagem sobe e responde - name: A imagem sobe e responde
if: ${{ steps.plano.outputs.reuse != 'true' }}
run: | run: |
docker run -d --name espdocs-ci --network host -e PORT=3999 espdocs:ci docker run -d --name espdocs-ci --network host -e PORT=3999 espdocs:ci
for i in $(seq 1 30); do for i in $(seq 1 30); do
@@ -95,3 +117,11 @@ jobs:
docker logs espdocs-ci | tail -5 docker logs espdocs-ci | tail -5
docker rm -f espdocs-ci >/dev/null docker rm -f espdocs-ci >/dev/null
test "${ok:-0}" = 1 test "${ok:-0}" = 1
# Passou em tudo: publica a mesma imagem que foi testada, sem reconstruir.
- name: Publica inputs-<hash>
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"
+12 -23
View File
@@ -18,10 +18,10 @@ env:
jobs: jobs:
# A imagem só depende dos arquivos que entram no build, não da documentação nem # 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 # da configuração de ferramentas. O push da branch já a construiu, testou e
# o mesmo conteúdo (um merge só de docs, uma mudança de workflow, uma tag num # publicou como inputs-<hash>; quando essa tag existe (o caso normal de um
# commit que acabou de ser publicado), não há o que construir: as tags novas # merge, uma tag num commit validado, um merge só de docs), não há o que
# apontam para o manifesto existente. # construir: as tags novas apontam para o manifesto existente.
plano: plano:
name: Plano (reaproveitar a imagem?) name: Plano (reaproveitar a imagem?)
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -32,23 +32,7 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- id: plano - id: plano
env: run: scripts/plano-imagem.sh "$REGISTRY" "${{ gitea.repository }}" >> "$GITHUB_OUTPUT"
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"
# Nada é publicado sem a suíte completa passar antes. Pulada quando a imagem é # Nada é publicado sem a suíte completa passar antes. Pulada quando a imagem é
# reaproveitada: o mesmo conteúdo já passou por ela. # reaproveitada: o mesmo conteúdo já passou por ela.
@@ -68,6 +52,11 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - 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 - uses: docker/setup-buildx-action@v4
- name: Autentica no registro - name: Autentica no registro
@@ -89,7 +78,7 @@ jobs:
type=sha,format=long type=sha,format=long
- name: Build e push - name: Build e push
if: ${{ needs.plano.outputs.reuse != 'true' }} if: ${{ steps.replano.outputs.reuse != 'true' }}
uses: docker/build-push-action@v7 uses: docker/build-push-action@v7
with: with:
context: . context: .
@@ -104,7 +93,7 @@ jobs:
# Mesmo manifesto, nomes novos: nenhuma camada é lida nem escrita. # Mesmo manifesto, nomes novos: nenhuma camada é lida nem escrita.
- name: Aplica as tags à imagem existente - name: Aplica as tags à imagem existente
if: ${{ needs.plano.outputs.reuse == 'true' }} if: ${{ steps.replano.outputs.reuse == 'true' }}
run: | run: |
tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< '${{ steps.meta.outputs.json }}') 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 }}" docker buildx imagetools create $tags "${{ needs.plano.outputs.image }}:inputs-${{ needs.plano.outputs.inputs }}"
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Uso: scripts/plano-imagem.sh <registro> <repositório>
# 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-<hash>.
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"