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-<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:
1 parent
7d0edf13c4
commit
4ad130a334
3 files changed
+84
-44
No files matched your search
+51
-21
@@ -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"
|
||||||
@@ -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 }}"
|
||||||
|
|||||||
Executable
+21
@@ -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"
|
||||||
Reference in new issue
Block a user