mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:35:21 +00:00
Workflows nativos em .forgejo/workflows/ (ci, ci-pr, agent-pool, automerge, issue-transition, plan-issue, vercel-promote, archive), removendo .github/workflows e .github/actions. Runner Forgejo v13 com services Postgres por porta + DB_HOST via gateway do bridge. Libs de agente sobre o novo cliente forgejo-api.mjs (zero dependências, GITHUB_SERVER_URL/FORGEJO_API_URL), helpers de workflow plain-Node. pnpm issue corrigido (await em função sync) e validado ao vivo. Testes: forgejoApi.unit.spec.ts novo, pin do matrix do ci.yml atualizado. Closes #1 (parte 1 — docs/issues/pool seguem na parte 2)
76 lines
2.7 KiB
JavaScript
76 lines
2.7 KiB
JavaScript
/**
|
|
* Plan-issue lifecycle helpers (OPS17 + OPS18): register with `--plan` starts
|
|
* non-claimable; promote to `ready` only after the intention plan is on `main`
|
|
* (agent `pnpm agent:ready` and/or merge Action reading `Related #N`).
|
|
*
|
|
* Pure — no IO. Used by `agent-register` / `agent-ready` / the OPS18 Action
|
|
* script and pinned by unit tests.
|
|
*/
|
|
|
|
import { labelNames } from './agent-forgejo.mjs'
|
|
import { HUMAN_GATE_LABELS, issueHasPlanLink } from './agent-pool-eligibility.mjs'
|
|
|
|
/**
|
|
* Initial state label for a newly registered issue.
|
|
*
|
|
* @param {{ hasPlan: boolean, explicitBlocked?: boolean }} options
|
|
* @returns {'ready' | 'blocked'}
|
|
*/
|
|
export const resolveRegisterStateLabel = ({ hasPlan, explicitBlocked = false }) => {
|
|
if (explicitBlocked || hasPlan) return 'blocked'
|
|
return 'ready'
|
|
}
|
|
|
|
const TERMINAL_OR_ACTIVE = ['in-progress', 'done', 'in-prod']
|
|
|
|
/**
|
|
* Whole-word `Related #N` only (case-insensitive). Does not match
|
|
* closing keywords (`Closes`/`Fixes`/`Resolves`) and rejects hyphenated
|
|
* compounds like `non-related #5`.
|
|
*/
|
|
const RELATED_ISSUE_RE = /(?<![\w-])related\s+#(\d+)\b/gi
|
|
|
|
/**
|
|
* Issue numbers cited as `Related #N` in a PR body (plans-only contract).
|
|
* Deduped, stable first-seen order. Only matches the Related keyword —
|
|
* closing keywords are owned by issue-done and never appear here.
|
|
*
|
|
* @param {string | null | undefined} body
|
|
* @returns {number[]}
|
|
*/
|
|
export const parseRelatedIssueNumbers = (body) => {
|
|
if (!body) return []
|
|
const seen = new Set()
|
|
const numbers = []
|
|
for (const match of body.matchAll(RELATED_ISSUE_RE)) {
|
|
const number = Number(match[1])
|
|
if (seen.has(number)) continue
|
|
seen.add(number)
|
|
numbers.push(number)
|
|
}
|
|
return numbers
|
|
}
|
|
|
|
/**
|
|
* Whether an issue is waiting for its intention plan on `main` and may be
|
|
* flipped `blocked` → `ready` by `agent:ready` (or the OPS18 Action).
|
|
*
|
|
* @param {{ state?: string, body?: string, labels?: Array<{ name: string }> }} issue
|
|
* @returns {{ ok: true } | { ok: false, reason: string }}
|
|
*/
|
|
export const canPromotePlanIssue = (issue) => {
|
|
if ((issue?.state ?? 'OPEN') !== 'OPEN') return { ok: false, reason: 'not-open' }
|
|
const labels = labelNames(issue ?? { labels: [] })
|
|
if (labels.includes('ready') && !labels.includes('blocked')) {
|
|
return { ok: false, reason: 'already-ready' }
|
|
}
|
|
if (!labels.includes('blocked')) return { ok: false, reason: 'not-blocked' }
|
|
if (TERMINAL_OR_ACTIVE.some((label) => labels.includes(label))) {
|
|
return { ok: false, reason: 'state-label' }
|
|
}
|
|
if (HUMAN_GATE_LABELS.some((label) => labels.includes(label))) {
|
|
return { ok: false, reason: 'needs-human' }
|
|
}
|
|
if (!issueHasPlanLink(issue)) return { ok: false, reason: 'no-plan-link' }
|
|
return { ok: true }
|
|
}
|