@@ -3,7 +3,7 @@ name: Template drift audit
33# Monthly audit — runs `precisa doctor` to detect drift between this
44# repo's templated files (workflows, dotfiles, configs) and the current
55# `@precisa-saude/cli` templates. When drift shows up, open a PR running
6- # `pnpm exec precisa sync` to absorb the changes.
6+ # `precisa sync` to absorb the changes.
77#
88# Runs on the 1st of each month at 06:00 America/Sao_Paulo (09:00 UTC).
99# `workflow_dispatch` is wired so you can trigger it ad-hoc.
@@ -13,6 +13,12 @@ name: Template drift audit
1313# in each repo. See fhir-brasil#26 (PAT_TOKEN → GitHub App) for the
1414# concrete failure mode — drift went undetected until a conditional
1515# job finally fired in production.
16+ #
17+ # Roda via `npx @precisa-saude/cli`, não via `pnpm exec`. O CLI não é
18+ # devDependency de nenhum repo consumidor, então `pnpm exec precisa` saía
19+ # "command not found" — não-zero, indistinguível de drift real. Todo repo
20+ # com este workflow abriu uma issue de "drift" por mês sem nunca ter
21+ # inspecionado um template. Ver a separação de exit codes abaixo.
1622
1723on :
1824 schedule :
@@ -30,42 +36,130 @@ jobs:
3036 timeout-minutes : 10
3137 steps :
3238 - uses : actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
33- - uses : pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
3439 - uses : actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
3540 with :
3641 node-version : 22
37- cache : ' pnpm'
38- - run : pnpm install --frozen-lockfile
42+
43+ # `npx --yes` dispensa o CLI como devDependency (nenhum repo o tem) e
44+ # dispensa `pnpm install` — o doctor só lê `.precisa.json` e os templates
45+ # que vêm dentro do próprio pacote.
46+ #
47+ # O exit code é capturado em vez de virar falha do step, para separar os
48+ # dois casos que antes colidiam:
49+ # 0 — sem drift
50+ # 1 — drift encontrado (o doctor rodou e comparou)
51+ # 2 — `.precisa.json` ausente ou inválido (nada comparado)
52+ # qualquer outro — o audit não rodou (rede, versão quebrada, etc.)
53+ # `PIPESTATUS[0]` pega o código do npx, não do `tee`.
3954 - name : Audit templates
4055 id : doctor
41- run : pnpm exec precisa doctor
42- continue-on-error : true
43- - name : Open issue on drift
44- if : steps.doctor.outcome == 'failure'
56+ # `shell: bash` explícito: `PIPESTATUS` é bashismo. É o default em
57+ # runners Linux, mas depender do default deixa o passo frágil se o
58+ # workflow for reaproveitado em outro runner.
59+ shell : bash
60+ env :
61+ NO_COLOR : ' 1'
62+ run : |
63+ set +e
64+ npx --yes @precisa-saude/cli@latest doctor 2>&1 | tee "$RUNNER_TEMP/precisa-doctor.log"
65+ echo "exit_code=${PIPESTATUS[0]}" >> "$GITHUB_OUTPUT"
66+
67+ # Falha barulhenta e visível quando a própria auditoria quebra. Antes
68+ # isso virava uma issue mensal de "drift" — o modo de falha que fez o
69+ # sinal perder credibilidade.
70+ - name : Fail when the manifest is invalid
71+ if : steps.doctor.outputs.exit_code == '2'
72+ run : |
73+ echo "::error title=.precisa.json inválido::O manifesto deste repo não valida contra o schema atual do CLI, então nenhum template foi comparado. Isto NÃO é drift — rodar 'sync' não resolve. Corrija os campos listados no log acima."
74+ exit 1
75+
76+ - name : Fail when the audit could not run
77+ if : steps.doctor.outputs.exit_code != '0' && steps.doctor.outputs.exit_code != '1' && steps.doctor.outputs.exit_code != '2'
78+ run : |
79+ echo "::error title=precisa doctor não rodou::exit ${{ steps.doctor.outputs.exit_code }} — isto NÃO é drift; a auditoria em si falhou."
80+ exit 1
81+
82+ # Uma issue por repo, atualizada a cada rodada. A versão anterior criava
83+ # uma issue nova todo mês, acumulando duplicatas até ninguém mais ler.
84+ - name : Report drift
85+ if : steps.doctor.outputs.exit_code == '1'
4586 uses : actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
4687 with :
4788 script : |
89+ const fs = require('node:fs');
4890 const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`;
91+ const MARKER = '<!-- precisa-doctor-drift -->';
92+
93+ let log = '';
94+ try {
95+ // `RUNNER_TEMP` em vez de `/tmp`: é limpo por job e não colide
96+ // com outro job no mesmo runner (relevante em self-hosted).
97+ log = fs.readFileSync(`${process.env.RUNNER_TEMP}/precisa-doctor.log`, 'utf8').trim();
98+ } catch {
99+ log = '(log indisponível)';
100+ }
101+ // Cabe folgado no limite de 65536 chars do corpo de uma issue.
102+ // Corta em fronteira de linha: cada linha do doctor é um arquivo,
103+ // e cortar no meio produziria um nome de arquivo pela metade.
104+ const LIMITE = 20000;
105+ if (log.length > LIMITE) {
106+ const corte = log.lastIndexOf('\n', LIMITE);
107+ log = `${log.slice(0, corte > 0 ? corte : LIMITE)}\n… (truncado — ver o log completo do run)`;
108+ }
109+
49110 const body = [
50- '`precisa doctor` detected drift between this repo and the canonical templates.',
111+ MARKER,
112+ '`precisa doctor` encontrou divergência entre este repo e os templates canônicos.',
51113 '',
52- `Full log: ${runUrl}` ,
114+ '### Saída do doctor' ,
53115 '',
54- 'To fix:',
116+ '```',
117+ log,
118+ '```',
119+ '',
120+ `Log completo do run: ${runUrl}`,
121+ '',
122+ '### Como resolver',
55123 '',
56124 '```bash',
57- 'pnpm exec precisa sync --dry-run # review the diff',
58- 'pnpm exec precisa sync # apply ',
125+ 'npx @ precisa-saude/cli@latest sync --dry-run # revisar o diff',
126+ 'npx @ precisa-saude/cli@latest sync # aplicar ',
59127 '```',
60128 '',
61- 'Drift typically shows up when `@precisa-saude/cli` publishes',
62- 'new templates (workflow changes, dotfile updates) and this',
63- 'repo has not run `precisa sync` since.',
129+ '> **Revise o `--dry-run` antes de aplicar.** Templates marcados',
130+ '> `overwrite` são reescritos sem backup nem confirmação — se este',
131+ '> repo tem customização deliberada num arquivo templatizado, o',
132+ '> sync a descarta. Nesse caso, leve a mudança para o `tooling`',
133+ '> em vez de rodar o sync.',
134+ '',
135+ `Atualizado em ${new Date().toISOString()} pelo run ${context.runId}.`,
64136 ].join('\n');
65- await github.rest.issues.create({
137+
138+ const title = 'precisa doctor: template drift detected';
139+ const existing = await github.paginate(github.rest.issues.listForRepo, {
66140 owner: context.repo.owner,
67141 repo: context.repo.repo,
68- title : 'precisa doctor: template drift detected ',
69- body ,
70- labels: ['dependencies', 'tooling-drift'] ,
142+ state : 'open ',
143+ labels: 'tooling-drift' ,
144+ per_page: 100 ,
71145 });
146+ const open = existing.find((i) => !i.pull_request && (i.body ?? '').includes(MARKER));
147+
148+ if (open) {
149+ await github.rest.issues.update({
150+ owner: context.repo.owner,
151+ repo: context.repo.repo,
152+ issue_number: open.number,
153+ body,
154+ });
155+ core.notice(`Drift persiste — issue #${open.number} atualizada.`);
156+ } else {
157+ const { data } = await github.rest.issues.create({
158+ owner: context.repo.owner,
159+ repo: context.repo.repo,
160+ title,
161+ body,
162+ labels: ['dependencies', 'tooling-drift'],
163+ });
164+ core.notice(`Drift detectado — issue #${data.number} aberta.`);
165+ }
0 commit comments