Repository navigation
CI: one workflow and a merge gate that verifies queue failures in the same run (shadow) - #35465
mohityadav766 wants to merge 11 commits into
Conversation
… same run ci.yml runs once per PR head and per queue entry: static checks gate every suite, the Java reactor is built once for the IT lanes and their verification, and UI checkstyle and the service unit tests run as the workflows they already are. merge-gate.yml turns each finished run into the merge-gate status. A failing test is re-run on the entry and on its parent commit in the same run, and the entry is ejected only when the failure is its own. Network blips and other infrastructure failures are re-run instead (known signatures first, then a bounded triage call that can only add a re-run), and every forgiven failure becomes a tracked flaky-test or broken-main issue. Ships in shadow: merge-gate (shadow) is required by nothing until the cutover in docs/ci.md. Design records: docs/decisions/2026-10-13-*.
- Merge gate: the sweep looks back six hours and decides runs that finished 15 to 60 minutes ago whose head has no gate status or only a pending one (a re-run leaves it pending). A run still going is never decided: its unfinished jobs would read as passed. - merge-gate.yml: a dispatched dry run records nothing; the record job writes the ledger with the gate App token from the ci-ai environment. - Same-run ADR v3: a parent control recorded for the same test, failure and parent commit stands in for running it again. - Conflict and fix workflows run no PR code: their verify jobs ran it in main's cache scope (CodeQL cache poisoning), and /ci-resolve is gone (TOCTOU). Tooling and .git stay out of the agent's reach, no step runs inside a PR checkout, and publish re-merges with git merge-tree and refuses edits outside the conflict. The PR's own CI tests what they push; taken generated files and lockfiles wait for a person. - CodeQL: a linear test-id regex, escaped invisible-character classes. - CI Scripts Tests run on workflow changes and leave out the two whole-repository Java impact-map audits (ownership is checked before the PR, #34984). Four stale tests follow today's CI: PRs run unit tests, the merge queue runs integration and Playwright, now pinned by a guard test. An unreadable first Playwright attempt counts as unknown, not as a missing report.
…dicts The sweep read one listing of every CI run, newest first, and cut it at 50 runs before filtering. Runs that already had a final status, or would never get one (outside the sample, superseded, out of the queue), could push the oldest queue runs, the ones nearest their check timeout, past the cut; and a listing past its page budget failed the whole sweep. Queue runs now have their own listing (6 hours) and PR runs theirs (3); each is sorted oldest first, queue runs before PR runs, and the limit of 50 counts only runs that reach a verdict. A listing that cannot be read is reported and the other is still swept. The merge-gate ADR (v3) records the window, the order and the limit, that a pending status counts as undecided, and that a held entry is decided again on each sweep in the window.
❌ PR checklist incompleteThis PR cannot be merged until the following are addressed on its linked issue:
The fields live on the linked issue in the Shipping project (open the issue → right sidebar → Projects). After you set them, re-run this check (or push a commit) — issue/project changes do not re-trigger it automatically. Maintainers can bypass this check by adding the |
…wn job The merge gate passed runs in which no job ran, lost the evidence of jobs a re-run left alone, and could forgive a regression on a parent control that failed for another reason. - merge_gate.py decides only ci.yml runs whose jobs ran: a run that could not start fails; one awaiting approval or cancelled before any job began gets no verdict. Several jobs that need a re-run go through rerun-failed-jobs, since GitHub refuses a targeted re-run once another has started a new attempt. - merge_gate_evidence.py reads each failed job's evidence from the execution that wrote it (GitHub copies the jobs a re-run leaves alone into the new attempt with their original start time) and answers a failure only with the verification of its own job, run no earlier than the attempt that saw it. - verify_failures.py verifies each engine and lane separately, reads each lane's newest evidence, and counts a parent failure only when it is the entry's failure (the same fingerprint). - merge_gate_registry.py keys ledger records and parent-control lookups by job and records nothing from strict entries or runs the gate did not decide. - The ambiguous-failure tie-break applies the impact-map link the record promised, with plan_local_java_tests.py's static rules.
A re-run leaves the lanes it did not re-run with an earlier attempt's evidence. Downloading only the current attempt's left a re-run verify job with nothing to verify for them, so the entry was ejected as unverified. verify_failures.py select keeps each lane's newest outcome.
- py-tests-shared.yml: the spaCy job's concurrency group takes the caller's suffix, and ci.yml passes one. GitHub keeps one pending job per group and cancels the one before it, so ci.yml's shadow run, a third caller beside py-tests and py-tests-postgres, could cancel one of their spaCy jobs for the same PR or queue entry and fail the required py-tests-status. A guard test now requires every concurrency group in a workflow ci.yml calls to take the caller's suffix. - Off by default: both shadow sampling shares default to 0, the merge gate starts no job while MERGE_GATE_MODE is unset (a dispatch still decides one run), and CI_CONFLICT_MODE defaults to off. Local scenarios cover each of them left unset. - The Redis IT workflow keeps its merge_group trigger until the cutover. - Merge-gate ADR v4; docs/ci.md says what merging changes and how to turn the shadow on.
py-tests-shared.yml's changes job runs dorny/paths-filter on pull_request, which lists the PR's files through the API and needs pull-requests: read. ci.yml's python job granted only the workflow's contents: read, so on a sampled PR run the filter would fail and every Python job behind it would be skipped. The job now grants pull-requests: read, and a guard test requires it of every ci.yml job that reaches a paths filter.
Code Review ✅ Approved 6 closed / 6 findings🔴 High risk · When enabled, the new merge gate controls queue eligibility; an incorrect verdict could block valid PRs or allow failing changes to merge. Adds a single CI workflow with a merge gate that verifies queue failures in the same run, shipped off by default. Open findings: none. Resolved this session: the sweep now lists queue runs first so long-running runs are not dropped, dry-run dispatches no longer write the ledger or flake issues, parent-control reuse matches the same-run ADR, and the ✅ 6 closed✅ Bug: Sweep's 30-minute
|
| Compact |
|
Was this helpful? React with 👍 / 👎 | Powered by Gitar — free for open source
|



Describe your changes:
Fixes #ISSUE
One CI workflow per event that builds once and stops at the first static failure, and a
merge gate that decides queue entries from the run's own evidence: a failing test is
re-run on the entry and on its parent commit in the same run, and the entry is ejected
only when the failure is its own. Network blips and other infrastructure failures are
re-run instead of ejecting the PR, with a bounded AI triage for failures no signature
recognises. Everything ships off: merging it changes nothing until an admin turns the
shadow on, in which
ci.ymlruns beside today's workflows and the gate postsmerge-gate (shadow), which no ruleset requires. Nothing changes for the live queue untilthe cutover in
docs/ci.md.Why: 32.4% of 1,405 queue entries (2026-09-29 → 10-13) had a failed required check and
merged PRs needed 2.86 entries on average; recurring flakes (TestCaseResourceIT bulk
tests: 28 PRs) and broken-main failures ejected PRs that did not cause them, and nothing
acted on them.
Type of change:
High-level design:
Read
docs/ci.mdfirst; the decisions are indocs/decisions/2026-10-13-*(five records).Pipeline (
ci.yml):plan→ static checks ∥ builds → suites →verify / *→ci-summary.needs:); a PR run cancels itself on a static failure, a queue run never does.static / uiandunit / javacallui-checkstyle.ymlandopenmetadata-service-unit-tests.yml, so the gate and the old required checks cannot drift; Playwright and Python call their reusables.CI_SHADOW_PR_PERCENT/CI_SHADOW_MQ_PERCENT); a run outside the sample gets no gate status.Merge gate (
merge-gate.yml,merge_gate*.py,.github/merge-gate/policy.json)merge_gate_policy.py: deterministic and static failures fail; failing tests go to verification (3× entry, 3× parent, classified flaky / pre-existing / introduced / ambiguous); infrastructure failures re-run only the failed jobs; budgets cap re-runs.ci_ai/triage.py) that must quote the log line; it can only add a re-run, never a pass (property-tested).merge_gate_registry.py): every verified test is recorded; forgiven failures becomeflaky-test/broken-mainissues; a parent control recorded for the same test, failure and parent is reused by later entries on that parent (same-run ADR v3).AI roles (advisory, capped, off by default): ejection explanations,
/ci-fix, conflict resolution (mechanical resolvers first), blip triage. Kill switches:CI_AI_MODE,CI_AI_TRIAGE_MODE. The conflict and fix workflows run in main's context, so they never run the code they edit: the agent only edits files and cannot reach the tooling or.gitlater steps use, publish re-checks the result with main's code (a conflict resolution must equal a freshgit merge-treeoutside the conflicted files), and the CI of the PR it lands in tests it.Merging changes nothing until it is turned on. With the repository variables unset, every
ci.ymlrun stops afterplan(both sampling shares default to 0), the merge gate starts no job (MERGE_GATE_MODEunset; a dispatch still decides one run), and the conflict resolver starts none (CI_CONFLICT_MODEdefaults tooff). An admin starts the shadow withMERGE_GATE_MODE=shadowandCI_SHADOW_PR_PERCENT/CI_SHADOW_MQ_PERCENT(start around 10, ramp while watching runner queue time).Rollout: shadow ≥ 2 weeks, compare with
ci_metrics.py compare, then one cutover PR to main that deletes or strips the replaced workflows (nevergh workflow disable: release-branch rulesets still require those checks).Workflow changes to existing files (defaults keep today's behaviour):
ui-checkstyle.ymlandopenmetadata-service-unit-tests.ymlgainworkflow_call;playwright-e2e-reusable.ymlandpy-tests-shared.ymlgain inputs, including a concurrency-group suffix so a call fromci.ymlnever shares a group with the workflow's own run (sharing the spaCy job's group could have cancelledpy-tests' job and failedpy-tests-status);backport-automerge.yamlgets a job-levelifthat only skips branches its own gate rejects.classify_test_outcome.pyadditionally lists tests that passed only on a re-run (flakyTests).Overlap: #34944 (#34945–#34948) changes some of the same workflows (Playwright pipeline, IT lanes, quarantine/flaky reporting). Needs coordinating before either lands.
Tests:
Use cases covered
needs:.Unit tests
.github/scripts/tests/and.github/scripts/test_*.py(gate policy, controller against a fake GitHub, evidence, verification, registry, triage, conflicts, publishing, metrics, local runner): 875 passing. CI leaves out the two whole-repository Java impact-map audits: map ownership is checked before the PR (Fixes #34815: Make agents own new code and ITs in the Java impact map before the PR #34984), not by CI.merge-gate; called workflows get the permissions their jobs declare and never share a concurrency group with their own runs; every paths filter can list a PR's files; no artifact path contains..; the conflict and fix workflows never run the code they check out; only the triage adapter reaches a model; a blip read can only add a re-run.Backend integration tests
Ingestion integration tests
Playwright (UI) tests
Manual testing performed
Locally with
act(.github/scripts/ci_local.py, documented indocs/ci.md):ci_local.py scenarios: the real job graph ofci.ymlandmerge-gate.ymlin 20 scenarios (PR, draft, static failure, IT lane failure, build failure, manual run, gate triggers, and a PR, a queue entry, the gate and its sweep with the variables unset, where nothing runs): 20/20. Four deliberately broken copies ofci.ymleach fail a scenario.plan,static / json-yaml,static / ci-scripts,static / pythonpass.plan, infrastructure".ci_local.py job verify-java: reactor build, theretry-queuelane on Postgres + OpenSearch via Testcontainers, verification, the gate's decision):Found and fixed by these runs:
verify / java-itwrote undergithub.workspace/.., whichupload-artifactrejects: verification would never have reached the gate (guard test added).rerunFailingTestsCount.Review rounds
ab5ca3b:
ci.yml'spythonjob grantspull-requests: read, which the paths filter inpy-tests-shared.ymlneeds onpull_request(without it every Python job of a sampled PR run would have been skipped); a guard requires it of every job that reaches a paths filter.c380d5c: merging changes nothing until it is turned on (sampling shares 0, gate off while
MERGE_GATE_MODEis unset, conflict resolveroff); the spaCy concurrency collision above is fixed and guarded; the Redis IT workflow keeps its queue trigger until the cutover.8b93c6b, 25d6280: the gate decides only
ci.ymlruns whose jobs ran (one that could not start fails; one awaiting approval or cancelled before any job began gets no verdict) and re-runs several jobs together; each failure is answered by its own job's verification across re-run attempts; a parent failure counts only when it is the entry's own failure; the ledger keys records by job and keeps nothing from strict or undecided entries; the impact-map link applies to the ambiguous tie-break.Round 2 (f4bff18): the sweep lists queue runs separately and first, oldest first, and its limit of 50 counts only runs that reach a verdict (runs with a final status, outside the sample, superseded or out of the queue no longer crowd out an entry near its timeout); a listing past its page budget no longer fails the whole sweep. The merge-gate record (v3) now states the sweep's window, order and limit, and that a pending status, including a held entry's, is decided again.
Round 1 (5a393c6):
verifyjobs ran PR code (fork code included) in main's cache scope, and/ci-resolvewas a comment-triggered checkout (TOCTOU). Both are gone; the agent can no longer reach the trusted tooling or.git; nothing runs inside a PR checkout (a PR'ssecrets.pywould have been imported by a heredoc step); a symlinked locale file can no longer redirect a write. Also a linear test-id regex and escaped invisible-character classes.ci.yml'sworkflow_dispatchinto the reusables'github.event.pull_request.head.shacheckout, which is empty on a dispatch, so the dispatched branch is checked out, never a PR head. The other ten alerts onui-checkstyle.yml/py-tests-shared.ymlare open on main already.Admin actions before enforce mode:
docs/ci.md. Draft PRs skip most CI.UI screen recording:
Not applicable — CI workflows, scripts and docs only; no product behaviour changes.
Checklist:
Fixes <issue-number>: <short explanation>(pending the issue;Fixes #ISSUEabove is a placeholder)docs/decisions/2026-10-13-*(five records; four revised 2026-10-14).🤖 Generated with Claude Code