teqo/scripts/agent-promote.mjs
Francisco Solla e04db48742 feat(ops): auto-promote stage→main e merge em stage sem exigir head (#68)
* feat(ops): auto-promote stage→main e merge em stage sem exigir head

Substitui o promote manual por workflow disparado no ci-stage green,
relaxa strict na proteção de stage (script configure:branch-protection)
e documenta o novo paradigma em AGENT-OPS e work-issue.

* style: format AGENT-OPS and configure-branch-protection for ci-pr

* feat(ops): pnpm push com ensure-repo-deps determinístico

Bootstrap install + Husky antes de todo push (pnpm push e pre-push).
Documenta fluxo canônico em work-issue, agent-pr-workflow e AGENT-OPS.

* fix(ops): pnpm push roda gate no script, sem depender do Husky

Cloud environment.json chama prepare explicitamente após install.

* test(int): isolate advisor portfolio search from shared cairu slug

Parallel int specs were stomping municipality.advisors on the fixed
cairu row; allocate per-test municipalities instead.
2026-07-30 21:12:52 -03:00

105 lines
3.2 KiB
JavaScript

/**
* `pnpm agent:promote -- --i-am-human` — manual override: promote `stage` → `main`.
*
* Normal path: `promote-stage-to-main.yml` auto-merges when ci-stage is green.
* Use this only when the workflow failed or you need to promote out of band.
*
* Checks (in order):
* 1. `main` has not diverged from `stage` (hotfix direto em main exige antes
* merge/rebase de main em stage + CI stage green de novo).
* 2. Latest ci-stage run on `stage` is green.
* 3. Creates the PR stage→main if none is open, then merges (ci-stage is the
* pre-promote gate; ci.yml on main is the post-merge safety net).
*/
import { execFileSync } from 'node:child_process'
import { dieAgent, gh, ghJson, parseArgs } from './lib/agent-github.mjs'
const die = dieAgent('promote')
const { flags } = parseArgs(process.argv.slice(2), new Set())
if (flags['i-am-human'] !== true) {
die(
'Refused: promote override is human-only. Re-run as a human with:\n' +
' pnpm agent:promote -- --i-am-human\n' +
'Auto-promote runs via promote-stage-to-main.yml when ci-stage is green.',
)
}
const git = (args) => execFileSync('git', args, { encoding: 'utf8' }).trim()
git(['fetch', 'origin', 'main', 'stage'])
// 1. Divergence: main must be an ancestor of stage.
try {
git(['merge-base', '--is-ancestor', 'origin/main', 'origin/stage'])
} catch {
die(
'`main` divergiu de `stage` (hotfix direto em main?). Antes do promote:\n' +
' git checkout stage && git merge origin/main && git push origin stage\n' +
'…e espere o CI stage ficar verde de novo.',
)
}
// 2. CI stage green on the stage head.
const stageSha = git(['rev-parse', 'origin/stage'])
const stageRuns = ghJson([
'run',
'list',
'--branch',
'stage',
'--workflow',
'ci-stage.yml',
'--limit',
'5',
'--json',
'headSha,status,conclusion,url',
])
const latestStageRun = stageRuns.find((run) => run.headSha === stageSha)
if (
!latestStageRun ||
latestStageRun.status !== 'completed' ||
latestStageRun.conclusion !== 'success'
) {
die(
`CI stage não está verde no head de stage (${stageSha.slice(0, 8)}). ` +
`Último run: ${latestStageRun ? `${latestStageRun.status}/${latestStageRun.conclusion} — ${latestStageRun.url}` : 'nenhum'}`,
)
}
console.log('[agent:promote] CI stage green ✓')
// 3. PR stage → main (create if needed).
let pr = ghJson([
'pr',
'list',
'--state',
'open',
'--base',
'main',
'--head',
'stage',
'--json',
'number,url',
])[0]
if (!pr) {
const url = gh([
'pr',
'create',
'--base',
'main',
'--head',
'stage',
'--title',
`Promote stage → main (${new Date().toISOString().slice(0, 10)})`,
'--body',
'Override manual via `pnpm agent:promote --i-am-human`. CI stage green verificado antes do merge.',
])
pr = { number: url.split('/').pop(), url }
console.log(`[agent:promote] created PR ${pr.url}`)
} else {
console.log(`[agent:promote] reusing open PR ${pr.url}`)
}
// 4. Merge (ci-stage was the pre-promote gate; ci.yml on main is the post-merge safety net).
gh(['pr', 'merge', String(pr.number), '--admin', '--merge'])
console.log(`[agent:promote] merged stage → main via PR #${pr.number}. Deploy Vercel a caminho.`)