refactor(repositories): honest reads in the eight files the ruler cou… #600
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy develop → testes | |
| # Sync da branch `develop` pro servidor de testes via SSH + rsync. | |
| # Dispara em cada push em `develop` (após merge de PR ou push direto). | |
| # Pré-requisitos operacionais documentados em CLAUDE.md (seção | |
| # "Develop branch workflow"): SSH habilitado na hospedagem, par de | |
| # chaves gerado, 4 secrets cadastrados no GitHub. | |
| on: | |
| push: | |
| branches: [develop] | |
| workflow_dispatch: | |
| # Manual re-deploy útil quando o servidor de testes foi resetado | |
| # ou o último push falhou e o estado da develop não mudou. | |
| concurrency: | |
| # Cancelar pushes consecutivos: só o último vence. Evita corrida | |
| # de dois rsync paralelos sobre o mesmo destino. | |
| group: deploy-develop-${{ github.ref }} | |
| cancel-in-progress: true | |
| permissions: | |
| contents: read | |
| jobs: | |
| deploy: | |
| name: rsync to testes | |
| runs-on: ubuntu-latest | |
| # Rede de segurança final: com BatchMode + `timeout 300` por tentativa e 3 | |
| # tentativas (2× backoff de 120s), o pior caso é ~19min; 25min cobre isso | |
| # mais o checkout/keyscan. Garante que o job nunca fique pendurado até o | |
| # cancelamento (o caso de ~15min do deploy do 35befc1). | |
| timeout-minutes: 25 | |
| # Sem environment gating — develop é território livre por design. | |
| # Se um dia esse workflow virar `deploy-prod`, aí sim entra | |
| # `environment: production` com required reviewers. | |
| steps: | |
| - uses: actions/checkout@v7 | |
| with: | |
| # Shallow clone OK — rsync envia working tree, não histórico. | |
| fetch-depth: 1 | |
| - name: Configure SSH | |
| env: | |
| SSH_KEY: ${{ secrets.TESTES_SSH_KEY }} | |
| SSH_HOST: ${{ secrets.TESTES_SSH_HOST }} | |
| SSH_PORT: ${{ secrets.TESTES_SSH_PORT }} | |
| run: | | |
| mkdir -p ~/.ssh | |
| echo "$SSH_KEY" > ~/.ssh/deploy_key | |
| chmod 600 ~/.ssh/deploy_key | |
| # `TESTES_SSH_PORT` é opcional — VPS padrão usa 22; hospedagem | |
| # gerenciada (Hostinger, KingHost) costuma usar porta alta. | |
| PORT="${SSH_PORT:-22}" | |
| # Best-effort: keyscan pode falhar (firewall, host atrás de | |
| # CDN, etc.) sem derrubar o deploy. O step de rsync abaixo usa | |
| # `StrictHostKeyChecking=accept-new` como fallback TOFU. | |
| ssh-keyscan -p "$PORT" -H "$SSH_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true | |
| - name: Rsync to testes | |
| env: | |
| SSH_HOST: ${{ secrets.TESTES_SSH_HOST }} | |
| SSH_USER: ${{ secrets.TESTES_SSH_USER }} | |
| SSH_PORT: ${{ secrets.TESTES_SSH_PORT }} | |
| REMOTE_PATH: ${{ secrets.TESTES_REMOTE_PATH }} | |
| run: | | |
| PORT="${SSH_PORT:-22}" | |
| # `--delete` remove arquivos no destino que não existem na | |
| # origem (idempotência total). Exclusões abaixo cobrem: | |
| # - VCS / CI metadata que não pertence ao runtime do plugin. | |
| # - Dependências de dev (composer require-dev only, node_modules). | |
| # - Testes e ferramentas de análise estática. | |
| # - Manifests de build/lint (composer.json, package.json e locks | |
| # não servem runtime do WordPress; analisadores externos não | |
| # precisam deles na pasta do plugin em produção). | |
| # - Docs de repositório (CONTRIBUTING.md, SECURITY.md) que vivem | |
| # no GitHub e não no runtime. `CHANGELOG.md` é mantido por | |
| # preferência — consulta histórica via SSH. | |
| # - Saídas de coverage / build intermediário. | |
| # `StrictHostKeyChecking=accept-new`: TOFU — aceita a chave do | |
| # host na primeira conexão e exige match em conexões futuras. | |
| # Mais seguro que `no` (vulnerável a MITM); funciona quando o | |
| # `ssh-keyscan` acima falhou silenciosamente. | |
| # | |
| # `ConnectTimeout` + retry: a hospedagem de testes ocasionalmente | |
| # recusa/derruba a conexão (servidor reiniciando, blip de rede). | |
| # Sem timeout explícito o connect espera ~2min no SYN e o deploy | |
| # falha num único blip — deixando o testes numa versão antiga. Aqui | |
| # cada tentativa falha rápido (30s) e o rsync (idempotente) é | |
| # repetido até 3× com backoff antes de dar o build como falho. | |
| # Delay fixo de 120s entre tentativas (janela total ~5,5min): o | |
| # backoff original de 20s/40s (~2,5min) era mais curto que um | |
| # restart típico da hospedagem — as 3 tentativas caíam dentro da | |
| # mesma indisponibilidade (run 229 e o deploy de 6.12.0 falharam | |
| # assim). Espaçar mais as mesmas 3 tentativas cobre o restart sem | |
| # alongar o caso de falha real além de ~5,5min. | |
| # | |
| # `BatchMode=yes`: nunca cair num prompt interativo. Sem isso, se a | |
| # autenticação por chave não vinga (chave com passphrase, chave | |
| # errada, ou fallback pra senha), o SSH bloqueia esperando input que | |
| # o runner nunca fornece — o job pendura até ser cancelado (o hang de | |
| # ~15min visto no deploy do 35befc1). Com BatchMode, esse caso vira | |
| # um "Permission denied (publickey)" imediato e legível. | |
| # | |
| # `timeout 300` no rsync: o ConnectTimeout só cobre o SYN inicial — | |
| # uma transferência que trava DEPOIS de conectar (stall de I/O, | |
| # ServerAlive não disparando) ainda penduraria a tentativa. O wrapper | |
| # aborta (exit 124) qualquer tentativa que passe de 5min (um deploy | |
| # real leva ~1-2min), e o `until` trata como falha e faz o retry. | |
| SSH_CMD="ssh -i ~/.ssh/deploy_key -p $PORT -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=4" | |
| attempt=1 | |
| max_attempts=3 | |
| until timeout 300 rsync -avz --delete \ | |
| --exclude='.git/' \ | |
| --exclude='.github/' \ | |
| --exclude='.githooks/' \ | |
| --exclude='.gitignore' \ | |
| --exclude='.gitattributes' \ | |
| --exclude='.distignore' \ | |
| --exclude='vendor/' \ | |
| --exclude='node_modules/' \ | |
| --exclude='tests/' \ | |
| --exclude='coverage-js/' \ | |
| --exclude='coverage/' \ | |
| --exclude='build/' \ | |
| --exclude='dist/' \ | |
| --exclude='.phpunit.result.cache' \ | |
| --exclude='phpunit.xml' \ | |
| --exclude='phpunit.xml.dist' \ | |
| --exclude='phpstan.neon' \ | |
| --exclude='phpstan.neon.dist' \ | |
| --exclude='phpstan-rows.neon.dist' \ | |
| --exclude='phpstan-stubs.php' \ | |
| --exclude='phpcs.xml' \ | |
| --exclude='phpcs.xml.dist' \ | |
| --exclude='patchwork.json' \ | |
| --exclude='.eslintrc*' \ | |
| --exclude='eslint.config.*' \ | |
| --exclude='.stylelintrc*' \ | |
| --exclude='vitest.config.*' \ | |
| --exclude='composer.json' \ | |
| --exclude='composer.lock' \ | |
| --exclude='package.json' \ | |
| --exclude='package-lock.json' \ | |
| --exclude='CLAUDE.md' \ | |
| --exclude='CONTRIBUTING.md' \ | |
| --exclude='SECURITY.md' \ | |
| -e "$SSH_CMD" \ | |
| ./ "${SSH_USER}@${SSH_HOST}:${REMOTE_PATH}/"; do | |
| status=$? | |
| if [ "$attempt" -ge "$max_attempts" ]; then | |
| echo "::error::rsync to testes failed after ${max_attempts} attempts (last exit ${status})." | |
| exit "$status" | |
| fi | |
| backoff=120 | |
| echo "::warning::rsync attempt ${attempt} failed (exit ${status}); retrying in ${backoff}s…" | |
| sleep "$backoff" | |
| attempt=$((attempt + 1)) | |
| done | |
| # Post-deploy alarm, not a gate — this workflow runs on push to `develop`, | |
| # so the merge already happened. Its job is to answer the two questions | |
| # only the real host can: did the deploy actually land, and does the | |
| # provider's own MariaDB hold the schema the code expects. | |
| # | |
| # It now blocks. `continue-on-error` was on while the check proved | |
| # itself against the host's PHP CLI, and #992 set the bar at 10 | |
| # consecutive green smokes; runs 525-537 delivered 13, every one | |
| # `SMOKE PASSED` with zero [FAIL] lines and no skip (the host's CLI is | |
| # 8.5.7, above the plugin's floor). The flag is what made a failing | |
| # smoke show up as a green deploy, so removing it is the whole point of | |
| # having run the evidence period. | |
| - name: Smoke test the deployed site | |
| env: | |
| SSH_HOST: ${{ secrets.TESTES_SSH_HOST }} | |
| SSH_USER: ${{ secrets.TESTES_SSH_USER }} | |
| SSH_PORT: ${{ secrets.TESTES_SSH_PORT }} | |
| REMOTE_PATH: ${{ secrets.TESTES_REMOTE_PATH }} | |
| run: | | |
| PORT="${SSH_PORT:-22}" | |
| SSH_CMD="ssh -i ~/.ssh/deploy_key -p $PORT -o BatchMode=yes -o StrictHostKeyChecking=accept-new -o ConnectTimeout=30" | |
| # The version the workflow believes it just deployed, read from the | |
| # commit rather than the host — that is the whole point of the check. | |
| VERSION=$(grep -oE "define\( 'FFC_VERSION', '[^']+'" ffcertificate.php | grep -oE "'[^']+'$" | tr -d "'") | |
| if [ -z "$VERSION" ]; then | |
| echo "::error::could not read FFC_VERSION from ffcertificate.php" | |
| exit 1 | |
| fi | |
| # The scripts are scp'd to a scratch directory and removed afterwards: | |
| # they are CI tooling, and the plugin directory on a runtime host | |
| # stays clean (the rsync excludes .github/ for the same reason). Both | |
| # files go together and keep their names — the smoke `require`s the | |
| # shared manifest reader from its own directory. | |
| REMOTE_DIR="/tmp/ffc-smoke-${GITHUB_RUN_ID}" | |
| $SSH_CMD "${SSH_USER}@${SSH_HOST}" "mkdir -p ${REMOTE_DIR}" | |
| scp -i ~/.ssh/deploy_key -P "$PORT" -o BatchMode=yes -o StrictHostKeyChecking=accept-new \ | |
| .github/scripts/testes-smoke.php .github/scripts/ffc-uninstall-manifest.php \ | |
| "${SSH_USER}@${SSH_HOST}:${REMOTE_DIR}/" | |
| set +e | |
| timeout 120 $SSH_CMD "${SSH_USER}@${SSH_HOST}" \ | |
| "php ${REMOTE_DIR}/testes-smoke.php '${REMOTE_PATH}' '${VERSION}'" | |
| STATUS=$? | |
| set -e | |
| $SSH_CMD "${SSH_USER}@${SSH_HOST}" "rm -rf ${REMOTE_DIR}" || true | |
| # The timeout stays a warning even now that the step blocks. 124 means | |
| # the host did not answer in 120s — the rsync above already succeeded, | |
| # so this is host latency, not a plugin defect, and reddening the | |
| # deploy for it is how an alarm becomes noise people learn to skip. | |
| # Every other non-zero is the smoke's own verdict and fails the job: | |
| # a stale version, a missing table, a fatal in the plugin. A host | |
| # whose PHP CLI is below the plugin's floor exits 0 with SKIPPED. | |
| if [ "$STATUS" -eq 124 ]; then | |
| echo "::warning::smoke timed out after 120s — the host may be slow or unreachable; the deploy itself succeeded." | |
| exit 0 | |
| fi | |
| if [ "$STATUS" -ne 0 ]; then | |
| echo "::error::post-deploy smoke failed on the testes host (exit ${STATUS}). See the report above." | |
| fi | |
| exit "$STATUS" | |
| - name: Cleanup SSH key | |
| if: always() | |
| run: rm -f ~/.ssh/deploy_key |