mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 15:13:07 +00:00
- issue-done/plan-issue-ready: env FORGEJO_API_TOKEN (GITHUB_TOKEN 403 em Issues) + fork guard (checkout de fork roda script do fork com o PAT — RCE) - forgejo-issue-transition: falha de flip vira exit 1 (job vermelho, nunca skip silencioso) - waitForChecks: exige o rollup CI (PR) / checks presente+verde (raça PR #52: snapshot com jobs não-agendados parecia verde); mergeable=false só conta como conflito após a cascata assentar; timeouts 30→45min (CLI) e 40→55min (workflow) - configure-branch-protection: contexto real CI (PR) / checks* (Forgejo casa por glob contra 'CI (PR) / checks (pull_request)'; literal 'checks' nunca casaria), idempotente GET→POST/PATCH, verificação pós-aplicação; decisão pura em scripts/lib/branch-protection.mjs - docs honestas (AGENT-OPS, agent-pr-workflow.mdc, CUTOVER-MAIN-ONLY, README) - testes: raça do rollup/rollup failure/mergeable pré-settle + branchProtection create/update/noop + glob real
60 lines
2 KiB
JavaScript
60 lines
2 KiB
JavaScript
/**
|
|
* Pure decision for the main branch protection rule (OPS61).
|
|
*
|
|
* The desired rule is the repo contract: main requires the CI (PR) cascade
|
|
* rollup as the only status check (`strict=false`, 0 reviews). The context is
|
|
* a glob on purpose — Forgejo matches `status_check_contexts` with gobwas/glob
|
|
* against the real commit-status contexts (`CI (PR) / checks (pull_request)` —
|
|
* the ` (event)` suffix is part of the context), so the literal `checks` would
|
|
* match nothing and block every merge.
|
|
*/
|
|
|
|
export const DESIRED_RULE = Object.freeze({
|
|
rule_name: 'main',
|
|
enable_status_check: true,
|
|
status_check_contexts: ['CI (PR) / checks*'],
|
|
enable_push: false,
|
|
required_approvals: 0,
|
|
dismiss_stale_approvals: false,
|
|
})
|
|
|
|
const COMPARED_FIELDS = [
|
|
'rule_name',
|
|
'enable_status_check',
|
|
'status_check_contexts',
|
|
'enable_push',
|
|
'required_approvals',
|
|
'dismiss_stale_approvals',
|
|
]
|
|
|
|
const sameStrings = (have, want) =>
|
|
have.length === want.length &&
|
|
want.every((value) => have.includes(value)) &&
|
|
have.every((value) => want.includes(value))
|
|
|
|
/**
|
|
* True when the existing rule (from the API, already normalized) matches the
|
|
* desired rule on every compared field — array fields as sets.
|
|
*/
|
|
export const ruleMatches = (existing, desired = DESIRED_RULE) => {
|
|
if (!existing) return false
|
|
return COMPARED_FIELDS.every((field) => {
|
|
const want = desired[field]
|
|
const have = existing[field]
|
|
if (Array.isArray(want)) return Array.isArray(have) && sameStrings(have, want)
|
|
return have === want
|
|
})
|
|
}
|
|
|
|
/**
|
|
* What to do with the rule: `create` (no rule), `update` (rule exists with
|
|
* drift), `noop` (already compliant). Idempotent re-runs land on `noop`.
|
|
*
|
|
* @param {object|null} existing - normalized rule from the API, or null
|
|
* @param {object} [desired]
|
|
* @returns {{ action: 'create' | 'update' | 'noop' }}
|
|
*/
|
|
export const planBranchProtectionRule = (existing, desired = DESIRED_RULE) => {
|
|
if (ruleMatches(existing, desired)) return { action: 'noop' }
|
|
return { action: existing ? 'update' : 'create' }
|
|
}
|