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.
148 lines
4.5 KiB
TypeScript
148 lines
4.5 KiB
TypeScript
import { describe, expect, it } from 'vitest'
|
|
|
|
import { rollupVerdict } from '../../scripts/lib/ci-rollup-check.mjs'
|
|
|
|
const CI_PR = 'CI (PR) / '
|
|
const OWN = 'CI (PR) / checks'
|
|
|
|
const status = (context: string, state: string) => ({ context, state })
|
|
|
|
describe('rollupVerdict (OPS64)', () => {
|
|
it('reports ok when every workflow status is success', () => {
|
|
const verdict = rollupVerdict(
|
|
[
|
|
status('CI (PR) / lint (pull_request)', 'success'),
|
|
status('CI (PR) / e2e (1) (pull_request)', 'success'),
|
|
status('CI (PR) / e2e (2) (pull_request)', 'success'),
|
|
],
|
|
{ prefix: CI_PR, ownPrefix: OWN },
|
|
)
|
|
|
|
expect(verdict.ok).toBe(true)
|
|
})
|
|
|
|
it('fails when a matrix shard is failure even if the parent would report green (run 604/607/610)', () => {
|
|
// Forgejo reports the LAST shard's result as the matrix parent result, so
|
|
// the old `all(needs...)` jq passed with e2e (1) red — the exact hole this
|
|
// verdict closes. The shard's own status is the ground truth.
|
|
const verdict = rollupVerdict(
|
|
[
|
|
status('CI (PR) / lint (pull_request)', 'success'),
|
|
status('CI (PR) / e2e (1) (pull_request)', 'failure'),
|
|
status('CI (PR) / e2e (2) (pull_request)', 'success'),
|
|
],
|
|
{ prefix: CI_PR, ownPrefix: OWN },
|
|
)
|
|
|
|
expect(verdict.ok).toBe(false)
|
|
expect(verdict.failed.map((entry) => entry.context)).toContain(
|
|
'CI (PR) / e2e (1) (pull_request)',
|
|
)
|
|
})
|
|
|
|
it('fails when the failing shard is the last one (run 716)', () => {
|
|
const verdict = rollupVerdict(
|
|
[
|
|
status('CI (PR) / e2e (1) (pull_request)', 'success'),
|
|
status('CI (PR) / e2e (2) (pull_request)', 'failure'),
|
|
],
|
|
{ prefix: CI_PR, ownPrefix: OWN },
|
|
)
|
|
|
|
expect(verdict.ok).toBe(false)
|
|
expect(verdict.failed.map((entry) => entry.context)).toContain(
|
|
'CI (PR) / e2e (2) (pull_request)',
|
|
)
|
|
})
|
|
|
|
it('ignores the rollup own pending status (posted while the job runs)', () => {
|
|
const verdict = rollupVerdict(
|
|
[
|
|
status('CI (PR) / checks (pull_request)', 'pending'),
|
|
status('CI (PR) / e2e (1) (pull_request)', 'success'),
|
|
status('CI (PR) / e2e (2) (pull_request)', 'success'),
|
|
],
|
|
{ prefix: CI_PR, ownPrefix: OWN },
|
|
)
|
|
|
|
expect(verdict.ok).toBe(true)
|
|
})
|
|
|
|
it('ignores contexts of other workflows on the same sha (the safety net)', () => {
|
|
const verdict = rollupVerdict(
|
|
[
|
|
status('PR Ready + auto-merge / ready-automerge (pull_request)', 'pending'),
|
|
status('CI (PR) / e2e (1) (pull_request)', 'success'),
|
|
status('CI (PR) / e2e (2) (pull_request)', 'success'),
|
|
],
|
|
{ prefix: CI_PR, ownPrefix: OWN },
|
|
)
|
|
|
|
expect(verdict.ok).toBe(true)
|
|
})
|
|
|
|
it('ignores downstream pending contexts via ignorePrefixes (ci.yml deploy)', () => {
|
|
const verdict = rollupVerdict(
|
|
[
|
|
status('CI / static (push)', 'success'),
|
|
status('CI / e2e (1) (push)', 'success'),
|
|
status('CI / deploy (push)', 'pending'),
|
|
],
|
|
{ prefix: 'CI / ', ownPrefix: 'CI / checks', ignorePrefixes: ['CI / deploy'] },
|
|
)
|
|
|
|
expect(verdict.ok).toBe(true)
|
|
})
|
|
|
|
it('fails on pending siblings (cascade not settled — fail-closed)', () => {
|
|
const verdict = rollupVerdict(
|
|
[
|
|
status('CI (PR) / e2e (1) (pull_request)', 'pending'),
|
|
status('CI (PR) / e2e (2) (pull_request)', 'success'),
|
|
],
|
|
{ prefix: CI_PR, ownPrefix: OWN },
|
|
)
|
|
|
|
expect(verdict.ok).toBe(false)
|
|
expect(verdict.pending.map((entry) => entry.context)).toContain(
|
|
'CI (PR) / e2e (1) (pull_request)',
|
|
)
|
|
})
|
|
|
|
it('fails on error state', () => {
|
|
const verdict = rollupVerdict([status('CI (PR) / lint (pull_request)', 'error')], {
|
|
prefix: CI_PR,
|
|
ownPrefix: OWN,
|
|
})
|
|
|
|
expect(verdict.ok).toBe(false)
|
|
})
|
|
|
|
it('fails closed on an empty relevant set (no statuses for the sha)', () => {
|
|
const verdict = rollupVerdict([], { prefix: CI_PR, ownPrefix: OWN })
|
|
|
|
expect(verdict.ok).toBe(false)
|
|
})
|
|
|
|
it('allows an empty set only when allowEmpty (ci.yml workflow_dispatch)', () => {
|
|
const verdict = rollupVerdict([], {
|
|
prefix: 'CI / ',
|
|
ownPrefix: 'CI / checks',
|
|
allowEmpty: true,
|
|
})
|
|
|
|
expect(verdict.ok).toBe(true)
|
|
})
|
|
|
|
it('accepts skipped jobs as success (posted with "Has been skipped")', () => {
|
|
const verdict = rollupVerdict(
|
|
[
|
|
status('CI (PR) / build (pull_request)', 'success'),
|
|
status('CI (PR) / int (pull_request)', 'success'),
|
|
],
|
|
{ prefix: CI_PR, ownPrefix: OWN },
|
|
)
|
|
|
|
expect(verdict.ok).toBe(true)
|
|
})
|
|
})
|