/** * Pure verdict for the CI cascade rollup jobs (`checks` in ci-pr.yml and * ci.yml). Decides whether the workflow's cascade is green from the commit * statuses of the head sha — the SAME source `waitForChecks` trusts — instead * of the `needs` context. * * Why not `needs` (OPS64 finding, runs 604/607/610): Forgejo 9.0.3 reports the * matrix parent's `result` as the LAST shard's result, not an AND over shards * (e2e (1) red + e2e (2) green → parent green → the old `all(needs...)` jq * posted `checks` SUCCESS with a red shard). The `needs..results` array * of GitHub does not exist in this runner (gitea/act `Needs` has only * `Outputs` and `Result`), so the aggregation cannot come from `needs` at all. * Each matrix shard posts its OWN status (`CI (PR) / e2e (1)` = failure), * which is what this verdict reads. * * `ownPrefix` excludes the rollup job's own context (posted `pending` while * the job runs). `ignorePrefixes` excludes downstream jobs that legitimately * stay `pending` while the rollup runs (e.g. the `deploy` job of ci.yml, which * `needs` the `checks` job — its status is `pending` at rollup time and must * never gate the verdict; the deploy only starts AFTER the rollup is green). * * Fail-closed: an empty relevant set fails unless `allowEmpty` (used by * ci.yml on workflow_dispatch — that event posts no commit statuses, so the * jq-verified `needs` cascade is the only signal and an empty read must not * red the manual channel). * * @typedef {{ context: string, state: string }} RollupStatus * @typedef {{ prefix: string, ownPrefix: string, ignorePrefixes?: string[], allowEmpty?: boolean }} RollupVerdictOptions * @typedef {{ ok: boolean, failed: RollupStatus[], pending: RollupStatus[], reason?: string }} RollupVerdict */ /** * @param {RollupStatus[]} [statuses] * @param {RollupVerdictOptions} [options] * @returns {RollupVerdict} */ export const rollupVerdict = ( statuses, { prefix, ownPrefix, ignorePrefixes = [], allowEmpty = false } = {}, ) => { const relevant = (statuses ?? []).filter( (entry) => entry.context.startsWith(prefix) && !entry.context.startsWith(ownPrefix) && !ignorePrefixes.some((ignore) => entry.context.startsWith(ignore)), ) if (relevant.length === 0) { return allowEmpty ? { ok: true, failed: [], pending: [] } : { ok: false, failed: [], pending: [], reason: 'nenhum status do workflow no sha' } } const failed = relevant.filter((entry) => entry.state === 'failure' || entry.state === 'error') if (failed.length > 0) { return { ok: false, failed, pending: [], reason: `status vermelhos: ${failed.map((entry) => entry.context).join(', ')}`, } } const pending = relevant.filter((entry) => entry.state === 'pending') if (pending.length > 0) { return { ok: false, failed: [], pending, reason: `status pendentes: ${pending.map((entry) => entry.context).join(', ')}`, } } return { ok: true, failed: [], pending: [] } }