diff --git a/.github/workflows/publicar.yml b/.github/workflows/publicar.yml index 4d175ae..5424316 100644 --- a/.github/workflows/publicar.yml +++ b/.github/workflows/publicar.yml @@ -86,3 +86,8 @@ jobs: docker logs fumaca-worker 2>&1 | grep -c '"backend":"cgo"' | grep -q '^5$' \ || { echo "::error::a lib nativa não carregou nos 5 serviços"; docker logs fumaca-worker; exit 1; } echo "fumaça OK" + + # A fumaça prova que a lib carrega, não qual lib é. A v1.3.0 subiu e passou + # na fumaça com a lib de NFS-e sem os patches que o próprio release trazia. + - name: as libs da imagem são as do release + run: scripts/conferir-libs-imagem.sh "${IMAGEM}:${GITHUB_SHA}" acbr-libs diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6ecdddf..9dc02e8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,6 +39,15 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + # O publicar.yml empurra : antes de conferir, e daqui só se sabe que + # a imagem existe, não que aquele workflow passou. Conferir de novo aqui é + # o que impede promover uma imagem com lib diferente da do release. + - name: baixa as libs do release + run: make acbr-libs-baixar + + - name: as libs da imagem são as do release + run: scripts/conferir-libs-imagem.sh "${IMAGEM}:${GITHUB_SHA}" acbr-libs + - name: promove : → :vX.Y.Z (+ :stable) run: | set -eu @@ -68,6 +77,18 @@ jobs: docker buildx imagetools inspect "${IMAGEM}:${VERSAO}" echo "tags publicadas: $(echo "$TAGS" | tr ' ' '\n' | grep -v '^-t$' | tr '\n' ' ')" + # Conferir, não presumir: cada tag nova tem de estar no digest de origem. + digest() { docker buildx imagetools inspect "$1" --format '{{json .Manifest}}' | jq -r .digest; } + DIGEST="$(digest "$ORIGEM")" + for t in $(echo "$TAGS" | tr ' ' '\n' | grep -v '^-t$'); do + d="$(digest "$t")" + if [ "$d" != "$DIGEST" ]; then + echo "::error::$t está em $d, a origem $ORIGEM está em $DIGEST" + exit 1 + fi + done + echo "DIGEST=$DIGEST" >> "$GITHUB_ENV" + # Promover a imagem sem criar a página deixa a aba de Releases atrás das # tags, e quem abre o repositório vê como "Latest" o que estiver lá, que # já foi um pacote de binários de biblioteca. @@ -109,6 +130,8 @@ jobs: echo "anexos da ACBrLib conferidos em $LIBS_REL" fi + NFSE_SHA="$(awk '$2 ~ /libacbrnfse64\.so$/ {print $1}' acbr-libs/SHA256SUMS)" + { printf 'Imagem: `%s:%s`' "$IMAGEM" "$VERSAO" case "$VERSAO" in @@ -116,6 +139,10 @@ jobs: *) printf ', que é o mesmo digest de `:%s` e `:stable`.\n' "${VERSAO%%.*}" ;; esac printf '\n' + # O digest não muda quando alguém move uma tag. É ele que se fixa no + # deploy: image: ...@sha256:... + printf 'Digest: `%s@%s`\n\n' "$IMAGEM" "$DIGEST" + printf 'Lib de NFS-e na imagem: `libacbrnfse64.so` sha256 `%s`, conferida contra o `SHA256SUMS` do release das libs.\n\n' "$NFSE_SHA" if [ -n "$LIBS_REL" ]; then if [ "$LIBS_REL" = "$VERSAO" ]; then printf 'Binários da ACBrLib (revisão r%s): anexos deste release.\n\n' "$ACBR_R" diff --git a/Makefile b/Makefile index 459daaf..1e5db86 100644 --- a/Makefile +++ b/Makefile @@ -189,6 +189,12 @@ acbr-libs-conferir: if [ "$$falta" = 1 ]; then echo; echo "Rode 'make acbr-libs-baixar'."; exit 1; fi @echo "acbr-libs OK" +## imagem-libs-conferir: confere que as .so de IMAGEM são as do SHA256SUMS em acbr-libs/ +# Rode depois do docker-build, ou contra uma imagem publicada: +# make imagem-libs-conferir IMAGEM=ghcr.io/4devsmart/wrapper-api/api:v1.3.1 +imagem-libs-conferir: + @scripts/conferir-libs-imagem.sh "$(or $(IMAGEM),$(APP_IMAGE))" $(ACBRLIBS) + ## docker-build: constrói a imagem (exige acbr-libs/) docker-build: acbr-libs-conferir docker build \ @@ -212,4 +218,4 @@ help: .PHONY: build build-cgo run test test-cgo openapi openapi-check openapi-valida yaml-valida enums-conferir vet fmt fmt-check tidy limpar help \ acbr-fonte acbr-compilar acbr-extrair \ - acbr-libs-baixar acbr-libs-publicar acbr-libs-conferir docker-build up down + acbr-libs-baixar acbr-libs-publicar acbr-libs-conferir imagem-libs-conferir docker-build up down diff --git a/docs/ACBRLIB.md b/docs/ACBRLIB.md index c60a661..d646809 100644 --- a/docs/ACBRLIB.md +++ b/docs/ACBRLIB.md @@ -183,6 +183,18 @@ arquivos entrarem em `acbr-libs/`: download truncado que chega ao diretório vir `.so` corrompido embutido na imagem, e SIGSEGV em runtime longe da causa. Como o repositório é público, o download não usa token. +O cache local vale só se bater com o `SHA256SUMS` **do release**. Comparar com o +`SHA256SUMS` local não basta: o `acbr-extrair` regera o local a partir do que +extraiu, e um cache com a lib oficial por cima das patchadas conferia consigo +mesmo e passava. Sem acesso ao release, o download avisa e segue com o cache. + +A imagem também é conferida. A fumaça prova que a lib carrega, não qual lib é, +e a v1.3.0 saiu com a lib de NFS-e sem patch passando por ela. Agora o +`publicar.yml` e o `release.yml` comparam as `.so` dentro da imagem com o +`SHA256SUMS` do release (`scripts/conferir-libs-imagem.sh`), e o release não +promove a imagem se divergirem. As notas do release trazem o digest da imagem e +o sha da lib de NFS-e. Localmente: `make imagem-libs-conferir IMAGEM=`. + ### Num repositório novo, ou num fork Os jobs `cgo` e `publicar` do CI baixam os `.so` do release apontado por diff --git a/scripts/baixar-acbr-libs.sh b/scripts/baixar-acbr-libs.sh index 8770c2d..5346905 100755 --- a/scripts/baixar-acbr-libs.sh +++ b/scripts/baixar-acbr-libs.sh @@ -25,21 +25,6 @@ ARQUIVOS=( ) mkdir -p "$DESTINO" - -# Cache quente: se tudo já bate com o SHA256SUMS local, não baixa nada. -if [[ -f "$DESTINO/SHA256SUMS" ]]; then - if quebrados="$(cd "$DESTINO" && sha256sum -c --quiet SHA256SUMS 2>/dev/null)"; then - if [[ "$(tr -d '[:space:]' < "$DESTINO/revisao.txt" 2>/dev/null)" == "$REV" ]]; then - echo "acbr-libs: cache já íntegro para r${REV}: nada a baixar." - exit 0 - fi - echo "acbr-libs: cache íntegro mas de outra revisão, rebaixando" - fi - echo "acbr-libs: cache inválido, rebaixando:" - sed 's/^/ · /' <<<"$quebrados" -fi - -echo "acbr-libs: baixando r${REV} do release $TAG de $REPO" tmp="$(mktemp -d)" trap 'rm -rf "$tmp"' EXIT @@ -53,6 +38,31 @@ baixar() { fi } +# Cache quente: o cache vale se bate com o SHA256SUMS DO RELEASE, não com o +# local. O local é regerado por `make acbr-extrair` a partir do que acabou de +# ser extraído, então sempre confere consigo mesmo: depois de extrair a lib +# oficial por cima das patchadas, este script dizia "nada a baixar" e o +# docker-build seguia com a lib sem patch. +if [[ -f "$DESTINO/SHA256SUMS" ]]; then + if curl -fsSL --retry 3 --retry-delay 2 -o "$tmp/SHA256SUMS" "$BASE/SHA256SUMS" && + curl -fsSL --retry 3 --retry-delay 2 -o "$tmp/revisao.txt" "$BASE/revisao.txt"; then + if [[ "$(tr -d '[:space:]' < "$tmp/revisao.txt")" == "$REV" ]] && + (cd "$DESTINO" && sha256sum -c --quiet "$tmp/SHA256SUMS" >/dev/null 2>&1); then + cp "$tmp/SHA256SUMS" "$tmp/revisao.txt" "$DESTINO"/ + echo "acbr-libs: cache confere com o release $TAG (r${REV}): nada a baixar." + exit 0 + fi + echo "acbr-libs: cache difere do release $TAG, rebaixando" + elif (cd "$DESTINO" && sha256sum -c --quiet SHA256SUMS >/dev/null 2>&1) && + [[ "$(tr -d '[:space:]' < "$DESTINO/revisao.txt" 2>/dev/null)" == "$REV" ]]; then + # Sem rede não há com o que comparar. Seguir com o cache é o que permite + # trabalhar offline, mas sem fingir que ele foi conferido. + echo "AVISO: sem acesso ao release $TAG; usando o cache local SEM conferir contra ele." >&2 + exit 0 + fi +fi + +echo "acbr-libs: baixando r${REV} do release $TAG de $REPO" for f in "${ARQUIVOS[@]}"; do echo " · $f" baixar "$f" diff --git a/scripts/conferir-libs-imagem.sh b/scripts/conferir-libs-imagem.sh new file mode 100755 index 0000000..05093e1 --- /dev/null +++ b/scripts/conferir-libs-imagem.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Confere que as .so DENTRO de uma imagem são as do SHA256SUMS de um cache de +# libs (acbr-libs/, populado por `make acbr-libs-baixar` a partir do release em +# ACBR_LIBS_RELEASE). +# +# Existe porque a imagem v1.3.0 saiu com a lib de NFS-e oficial, sem os patches +# de docker/acbr-patches, enquanto o próprio release carregava as patchadas. +# Nada falhou: o Dockerfile copia o que estiver em acbr-libs/, e ninguém +# comparava o resultado. A fumaça prova que a lib carrega, não qual lib é. +# +# Uso: scripts/conferir-libs-imagem.sh [diretório-do-cache] +set -euo pipefail + +IMAGEM="${1:?uso: $0 [diretório-do-cache]}" +CACHE="${2:-acbr-libs}" +SOS=(libacbrnfse64.so libacbrcte64.so libacbrmdfe64.so libacbrnfe64.so libacbrboleto64.so) + +if [[ ! -f "$CACHE/SHA256SUMS" ]]; then + echo "ERRO: $CACHE/SHA256SUMS não existe. Rode 'make acbr-libs-baixar'." >&2 + exit 1 +fi + +# --entrypoint direto no sha256sum: o entrypoint da imagem sobe a API ou o +# worker, e aqui só interessa ler os arquivos. +caminhos=("${SOS[@]/#//usr/lib/}") +na_imagem="$(docker run --rm --entrypoint sha256sum "$IMAGEM" "${caminhos[@]}")" + +divergiu=0 +for so in "${SOS[@]}"; do + esperado="$(awk -v f="$so" '$2 == f || $2 == "*"f {print $1}' "$CACHE/SHA256SUMS")" + obtido="$(awk -v f="/usr/lib/$so" '$2 == f {print $1}' <<<"$na_imagem")" + if [[ -z "$esperado" ]]; then + echo "ERRO: $so não está em $CACHE/SHA256SUMS" >&2 + divergiu=1 + elif [[ "$esperado" != "$obtido" ]]; then + echo "ERRO: $so na imagem é ${obtido:0:16}, o release declara ${esperado:0:16}" >&2 + divergiu=1 + else + echo " $so ${obtido:0:16}" + fi +done + +if [[ "$divergiu" == 1 ]]; then + echo "ERRO: $IMAGEM não carrega as libs de $CACHE/SHA256SUMS." >&2 + exit 1 +fi +echo "libs da imagem conferem com $CACHE/SHA256SUMS"