Some checks failed
CI (PR) / scope (pull_request) Successful in 33s
CI (PR) / lint (pull_request) Successful in 44s
CI (PR) / docs-guards (pull_request) Successful in 7s
CI (PR) / plans-only-closes (pull_request) Successful in 7s
CI (PR) / format (pull_request) Successful in 55s
CI (PR) / typecheck (pull_request) Successful in 31s
CI (PR) / knip (pull_request) Successful in 36s
CI (PR) / cycles (pull_request) Successful in 39s
CI (PR) / unit (pull_request) Successful in 59s
CI (PR) / int (pull_request) Successful in 2m23s
CI (PR) / build (pull_request) Has been skipped
CI (PR) / e2e (1) (pull_request) Has been skipped
CI (PR) / e2e (2) (pull_request) Has been skipped
CI (PR) / checks (pull_request) Failing after 1s
PR Ready + auto-merge / ready-automerge (pull_request) Failing after 50m5s
O sintoma primário da Issue (deadlock do waitForChecks no próprio pending) já tinha sido entregue pelo OPS61; o OPS64 fecha o secundário, que estava vivo e medido: o Forgejo 9.0.3 reporta o resultado do ÚLTIMO shard como o resultado do parent de matrix no contexto `needs` (runs 604/607/610: e2e (1) vermelho + e2e (2) verde → checks verde), e o `needs.<job>.results` do GitHub não existe neste runner. Com isso o gate do servidor (branch protection) e o deploy do ci.yml podiam passar com um shard vermelho. O rollup `checks` (ci-pr.yml e ci.yml) agora lê os statuses do commit via API (scripts/ci-rollup-check.mjs + lib pura, zero-dep, plain Node) — a mesma fonte que o waitForChecks confia — filtrando pelo prefixo do workflow, excluindo o próprio contexto e os downstreams legítimos (deploy, pending enquanto o rollup roda), falhando fechado em failure/error/pending/vazio. Fork guard + FORGEJO_API_TOKEN no job checks do ci-pr.yml (sem o guard, fork rodaria o script do head com o PAT — RCE classe OPS61); PRs de fork ficam não-mergeáveis no servidor (hardening). ci.yml usa --allow-empty em workflow_dispatch (evento não posta commit status). Testes: 12 specs do veredito (run 607/716 pinados, próprio pending ignorado, safety net ignorado, deploy ignorado, vazio fail-closed) + waitForChecks lança com rollup verde + shard vermelho. Unit 2360 verdes; gates locais ok.
75 lines
3 KiB
JavaScript
75 lines
3 KiB
JavaScript
/**
|
|
* 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.<job>.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: [] }
|
|
}
|