mirror of
https://github.com/melgarafael/DeskcommCRM.git
synced 2026-10-02 01:28:34 +08:00
Fui apagar a tag de ensaio do GHCR e a listagem mostrou o que ninguém tinha ido
olhar: `stable` e `1.3.0` são versions com IDs DIFERENTES nos três pacotes.
deskcommcrm 1.3.0 sha256:fc10b029e326 stable sha256:c4bc70b606c8
worker 1.3.0 sha256:81e5af567cc8 stable sha256:3fe292cad2bd
scheduler 1.3.0 sha256:4396263ba807 stable sha256:a0d5c3ad2296
Eu havia registrado o contrário — "stable nos três pacotes com o mesmo digest".
Era falso, e ninguém o contestou porque era uma boa notícia.
CAUSA, medida nos runs, não deduzida:
19:53:04 evento=push v1.3.0 -> publicou 1.3.0, 1.3 e stable
19:58:05 evento=release v1.3.0 -> reconstruiu, moveu 1.3.0 e 1.3, e NÃO
moveu stable (a condição exige push)
`gh release create` empurra a tag E publica a release. Com `release: published`
ligado ao lado de `push: tags`, o mesmo commit foi construído duas vezes, com 5
minutos de diferença. Os labels confirmam: `revision` idêntico (9bd59e9) e
`version` idêntica nos seis, `created` separado por 5 min. Mesmo código, builds
distintos.
A segunda consequência é a grave, e é maior que a divergência de canal: a tag de
VERSÃO foi movida DEPOIS de publicada. A doutrina inteira se apoia em "instalação
de cliente aponta para número de versão, nunca para tag móvel" — e era o nosso
próprio workflow movendo o número. Editar e republicar uma release antiga
reconstruiria e moveria aquela versão de novo.
CONSERTO: o gatilho `release` sai. `push: tags: ["v*"]` já cobre o fluxo real.
A guarda nova reprova por COMPORTAMENTO — qualquer gatilho que possa republicar
sobre tag existente (`release`, `workflow_run`, `schedule`, `repository_dispatch`),
não só o que causou este caso — e reprova TAMBÉM se o gatilho de tag sumir, senão
ela passaria num workflow que não publica nada. Sabotada nos dois sentidos: cada
sabotagem derruba exatamente 1 dos 12.
E O CHECKLIST DE RELEASE ESTAVA NA ORDEM ERRADA. A conferência de `stable` era o
item 8 e o `gh release create` era o 10 — ou seja, a verificação rodava CINCO
MINUTOS ANTES do ato que a invalidava, e passava, verde e honesta. Verificação que
roda antes do passo que pode quebrá-la é verificação de nada. Foi para depois, nos
dois checklists (doutrina e DEPLOY-CHECKLIST), com o comando que compara digest.
De carona, a mesma classe de defeito em mais três lugares — perguntei "onde mais":
- a doutrina ainda dizia que `imagens-ok` "ainda não está" na branch protection.
Está, desde 13/08. O parágrafo já tinha errado no sentido oposto antes; esta é a
segunda vez que ele mente sobre o mesmo fato. Nota de pendência é dívida com data
de vencimento e sem cobrador;
- CONTRIBUTING.md listava quatro obrigatórios e chamava o `imagens-ok` de
não-bloqueante;
- CLAUDE.md e AGENTS.md mandavam procurar o job em `.github/workflows/imagens.yml`,
arquivo que NÃO EXISTE — ele vive em `publish-image.yml`. Ponteiro morto numa
linha que se lê como prova.
E o runbook de ativação, que já estava todo concluído, seguia se lendo como lista
de pendências: ganhou a tabela de estado com a prova de cada passo.
NÃO CONSERTADO, e é decisão sua: o `stable` publicado continua apontando para o
build de 19:53. Realinhá-lo exige `write:packages` (o token tem delete, não write)
e seria um `imagetools create`, sem rebuild. Impacto prático: nenhum — mesmo
commit, mesma versão. Impacto real: quem comparar digest para verificar o que
instalou encontra divergência legítima. Da próxima release em diante já sai certo.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKm2XcTcfgi1vdMcDYSRWa
179 lines
8.6 KiB
YAML
179 lines
8.6 KiB
YAML
name: Publicar imagem Docker (GHCR)
|
|
|
|
# Builda as imagens GENÉRICAS que o self-hoster instala e publica no GitHub
|
|
# Container Registry. O leigo puxa no VPS (não builda lá). Roda nos runners do
|
|
# GitHub, então o build pesado nunca cai no VPS do usuário.
|
|
#
|
|
# São TRÊS imagens, e a razão de o worker e o scheduler estarem aqui é um defeito
|
|
# real: o serviço `worker` não tinha `image:` no compose de produção, só `build:`.
|
|
# Serviço build-only é pulado por `docker compose pull` e imune a `up -d` sem
|
|
# `--build` — ele era construído na VPS de cada cliente, no install, e NENHUM
|
|
# update.sh jamais o reconstruiu. O runtime do agente de IA congelava no código
|
|
# do dia da instalação. Doutrina: docs/doctrine/packaging.md, invariante 1.
|
|
|
|
on:
|
|
push:
|
|
tags: ["v*"]
|
|
branches: ["main"]
|
|
# NÃO acrescente `release: types: [published]` aqui. Ele já esteve, e o efeito
|
|
# foi medido na v1.3.0: `gh release create` empurra a tag E publica a release,
|
|
# então o MESMO commit foi construído DUAS vezes, com 5 minutos de diferença.
|
|
#
|
|
# 19:53:04 evento=push v1.3.0 -> publicou 1.3.0, 1.3 e stable
|
|
# 19:58:05 evento=release v1.3.0 -> reconstruiu e MOVEU 1.3.0 e 1.3,
|
|
# sem mover stable (a condição exige push)
|
|
#
|
|
# Duas consequências, e a segunda é a grave:
|
|
#
|
|
# 1. `stable` e `1.3.0` ficaram em digests DIFERENTES (mesmo `revision`
|
|
# 9bd59e9 e mesmo `version`, builds distintos) — quem comparasse os dois
|
|
# concluiria, com razão, que são artefatos diferentes.
|
|
# 2. A tag de VERSÃO foi movida depois de publicada. A doutrina inteira se
|
|
# apoia em "instalação de cliente aponta para número de versão, nunca para
|
|
# tag móvel" (docs/doctrine/packaging.md, invariante 3) — e era o nosso
|
|
# próprio workflow movendo o número. Republicar uma release antiga (editar
|
|
# e salvar) reconstruiria e moveria aquela versão de novo.
|
|
#
|
|
# O gatilho de push da tag já cobre o fluxo real: a tag é sempre empurrada,
|
|
# com ou sem release. Vigiado por tests/unit/packaging-artefato-do-cliente.test.ts.
|
|
# Em PR a imagem é CONSTRUÍDA e não publicada. Sem isto, nada no PR consegue
|
|
# revelar que a mudança quebra a imagem — foi assim que um bump de `next`
|
|
# passou por `verify`, `build-and-size`, `invariants` e `e2e` (os quatro
|
|
# obrigatórios) e derrubou o build do Dockerfile na `main`: o `next build`
|
|
# dentro da imagem não enxerga `tests/` (`.dockerignore`), e a partir do
|
|
# next 16.3 ele typecheca os `*.test.ts` colocados. O artefato que o
|
|
# self-hoster instala era o único sem gate.
|
|
pull_request:
|
|
workflow_dispatch: {}
|
|
|
|
env:
|
|
REGISTRY: ghcr.io
|
|
|
|
jobs:
|
|
build-and-push:
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
packages: write
|
|
|
|
strategy:
|
|
# fail-fast desligado: se o worker quebrar, ainda quero saber se o app
|
|
# também quebrou. Com ele ligado, o primeiro erro esconde os outros dois e
|
|
# o conserto vira uma rodada por imagem.
|
|
fail-fast: false
|
|
matrix:
|
|
include:
|
|
- name: deskcommcrm
|
|
dockerfile: Dockerfile
|
|
title: DeskcommCRM
|
|
- name: deskcomm-worker
|
|
dockerfile: Dockerfile.worker
|
|
title: DeskcommCRM worker
|
|
- name: deskcomm-scheduler
|
|
dockerfile: Dockerfile.scheduler
|
|
title: DeskcommCRM scheduler
|
|
|
|
steps:
|
|
- uses: actions/checkout@v7
|
|
|
|
- name: Log in to GHCR
|
|
if: github.event_name != 'pull_request'
|
|
uses: docker/login-action@v4
|
|
with:
|
|
registry: ${{ env.REGISTRY }}
|
|
username: ${{ github.actor }}
|
|
password: ${{ secrets.GITHUB_TOKEN }}
|
|
|
|
# A versão que a imagem vai reportar em /api/v1/health. Numa tag v1.2.3 é
|
|
# "1.2.3"; fora de tag, o SHA curto — que ainda responde "o que está
|
|
# rodando aqui?", que é a pergunta que o campo existe para responder.
|
|
- name: Resolver APP_VERSION
|
|
id: ver
|
|
run: |
|
|
if [ "${GITHUB_REF_TYPE}" = "tag" ]; then
|
|
echo "value=${GITHUB_REF_NAME#v}" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "value=${GITHUB_SHA::7}" >> "$GITHUB_OUTPUT"
|
|
fi
|
|
|
|
- name: Docker metadata (tags/labels)
|
|
id: meta
|
|
uses: docker/metadata-action@v6
|
|
with:
|
|
images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.name }}
|
|
tags: |
|
|
type=ref,event=branch
|
|
type=semver,pattern={{version}}
|
|
type=semver,pattern={{major}}.{{minor}}
|
|
# `enable={{is_default_branch}}` NÃO basta: o metadata-action o
|
|
# considera verdadeiro também num push de TAG, então toda release
|
|
# movia `latest` junto. Isso apagava a distinção que a doutrina
|
|
# inteira depende — `latest` = topo da main, `stable` = última
|
|
# release — e fazia `latest` oscilar entre os dois.
|
|
type=raw,value=latest,enable=${{ github.ref_type == 'branch' && github.ref_name == 'main' }}
|
|
# `stable` = a última RELEASE publicada. Existe porque `latest` aqui
|
|
# significa topo da `main` (código não lançado), e o nome engana:
|
|
# quem quer "a última versão estável" pedia `latest` e recebia edge.
|
|
# Trocar o significado de `latest` faria downgrade silencioso em quem
|
|
# já o consome, então o canal novo é o que ganha o nome certo.
|
|
# Três condições, e cada uma barra um caminho medido. `ref_type ==
|
|
# 'tag'` sozinho deixaria um dispatch numa branch mover o canal;
|
|
# `startsWith(…, 'v')` barra tag de teste (o registry já tem uma
|
|
# `quebrada-teste`, que nenhum push na main consegue produzir);
|
|
# e `event_name == 'push'` barra o pior caso — um dispatch numa
|
|
# release ANTIGA faria `stable` REGREDIR, e todo self-hoster no
|
|
# default do compose sofreria downgrade silencioso no próximo
|
|
# `up -d`, com app velho sobre banco já migrado.
|
|
type=raw,value=stable,enable=${{ github.event_name == 'push' && github.ref_type == 'tag' && startsWith(github.ref_name, 'v') }}
|
|
labels: |
|
|
org.opencontainers.image.title=${{ matrix.title }}
|
|
|
|
- name: Set up Buildx
|
|
uses: docker/setup-buildx-action@v4
|
|
|
|
- name: Build e push (imagem genérica — sem segredos)
|
|
uses: docker/build-push-action@v7
|
|
with:
|
|
context: .
|
|
file: ${{ matrix.dockerfile }}
|
|
# linux/amd64: o VPS HostGator e a imagem do WAHA são amd64.
|
|
platforms: linux/amd64
|
|
# PR de fork não tem permissão de escrita no GHCR, e publicar imagem
|
|
# de código não revisado seria pior que não gatear. Em PR: constrói
|
|
# (que é o gate) e descarta.
|
|
push: ${{ github.event_name != 'pull_request' }}
|
|
tags: ${{ steps.meta.outputs.tags }}
|
|
labels: ${{ steps.meta.outputs.labels }}
|
|
build-args: |
|
|
APP_VERSION=${{ steps.ver.outputs.value }}
|
|
# Cache por imagem: as três têm camadas diferentes e um escopo único
|
|
# faria uma sobrescrever o cache da outra a cada run.
|
|
cache-from: type=gha,scope=${{ matrix.name }}
|
|
cache-to: type=gha,mode=max,scope=${{ matrix.name }}
|
|
# Sem build-args de segredo: a imagem é genérica (NEXT_PUBLIC_* viram
|
|
# placeholder; valores reais entram em runtime no VPS do usuário).
|
|
|
|
# Job de fachada: dá UM nome estável para a branch protection exigir. Sem ele,
|
|
# o required check teria que listar as três instâncias da matriz pelo nome
|
|
# gerado, e acrescentar uma quarta imagem um dia quebraria o gate em silêncio —
|
|
# o merge voltaria a passar sem que ninguém tivesse decidido isso.
|
|
imagens-ok:
|
|
if: always()
|
|
runs-on: ubuntu-latest
|
|
# Nenhuma permissão: este job só lê o resultado de `needs` e sai. Sem o bloco,
|
|
# o GITHUB_TOKEN herdaria o default do REPOSITÓRIO — configuração que vive fora
|
|
# do repo, muda num clique e não passa por PR.
|
|
#
|
|
# A regra e o gate que a impõe vieram do PR #241 (alertas do CodeQL), de um
|
|
# contribuidor. O teste dele pegou este job: eu o criei sem o bloco, e o defeito
|
|
# só apareceu quando a `main` entrou naquela branch. É o próprio caso que a
|
|
# justificativa do teste antecipa — "um job novo nasce sem bloco e herda o
|
|
# default de novo".
|
|
permissions: {}
|
|
needs: [build-and-push]
|
|
steps:
|
|
- name: Falhar se qualquer imagem não construiu
|
|
run: |
|
|
echo "resultado do build-and-push: ${{ needs.build-and-push.result }}"
|
|
[ "${{ needs.build-and-push.result }}" = "success" ]
|