Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,23 @@ jobs:

test-sqlite:
needs: [changes, lint]
if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || needs.changes.outputs.code == 'true' || needs.changes.outputs.deps == 'true' || needs.changes.outputs.workflows == 'true')
# `docs` is in this condition and NOT in the other test jobs' on purpose.
# Part of the suite this job runs takes documentation as its INPUT and
# asserts on its content — tests_py/scripts/test_codex_plugin_contract.py
# reads README.md and checks the canonical published identities (the
# hypermnesia-mcp-viz anchor, the viz/spec migration strings). Excluding
# '*.md' from the `code` filter therefore switched those guards OFF exactly
# when their subject changed: PR #509 was a README-only diff, every test job
# was skipped, it merged green, and the push to main went red on that test
# across five jobs (run 34238410970, 2026-09-08 — 7663 passed, 1 failed;
# fixed by #510).
#
# This job, not the 3.10-3.13 matrix, because it already runs THE FULL SUITE
# (see the "Scope" note on its pytest step) in one ~5-minute job, so it
# covers the doc-contract tests at the smallest cost that still closes the
# gap. Waking the whole matrix on every prose edit would undo the CI work in
# #475-#481.
if: github.event_name != 'schedule' && (github.event_name != 'pull_request' || needs.changes.outputs.code == 'true' || needs.changes.outputs.deps == 'true' || needs.changes.outputs.workflows == 'true' || needs.changes.outputs.docs == 'true')
name: Test (SQLite backend)
runs-on: ubuntu-latest
# source: run 33806380625 (2026-09-03), max 292s; ceil(2 * 292 / 60).
Expand Down
12 changes: 12 additions & 0 deletions scripts/check_ci_gate_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,18 @@ def _required_jobs(flags: dict[str, bool], event_name: str, fork: bool) -> set[s
required.add("docker-smoke")
if flags["docker"] or event_name in ("schedule", "workflow_dispatch"):
required.update(DOCKER_BUILD_JOBS)
# A docs-only PR must still run the one full-suite job. Part of that
# suite takes documentation as its INPUT — tests_py/scripts/
# test_codex_plugin_contract.py asserts on README.md's canonical published
# identities — so excluding '*.md' from `code` switched those guards off
# exactly when their subject changed. PR #509 was a README-only diff, every
# test job skipped, it merged green, and the push to main went red on that
# test across five jobs (run 34238410970, 2026-09-08; fixed by #510).
# Mirrors ci.yml's test-sqlite predicate, which carries the same note; this
# module and that predicate must stay in exact agreement (see the docstring
# on check_policy).
if not full and event_name == "pull_request" and flags["docs"]:
required.add("test-sqlite")
if fork:
required.discard("mcp-host-config")
return required
Expand Down
24 changes: 21 additions & 3 deletions tests_py/scripts/test_check_ci_gate_results.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,25 @@ def _skip(needs: dict, jobs: frozenset[str]) -> dict:


class JustifiedSkips(unittest.TestCase):
def test_docs_only_runs_only_changes_and_lint(self) -> None:
needs = _skip(_needs("docs"), gate.CONDITIONAL_JOBS)
def test_docs_only_still_requires_the_full_suite_job(self) -> None:
"""A docs-only PR may skip every conditional job except test-sqlite.

Regression for the #509 -> #510 incident: README.md is the INPUT of
tests_py/scripts/test_codex_plugin_contract.py, so skipping every test
job on a docs-only diff switched that guard off exactly when its subject
changed. #509 merged green and main went red on the push (run
34238410970). test-sqlite is the one job that runs the full suite.
"""
allowed = gate.CONDITIONAL_JOBS - {"test-sqlite"}
needs = _skip(_needs("docs"), allowed)
self.assertEqual(gate.check(needs, "pull_request", _event()), [])

needs["test-sqlite"]["result"] = "skipped"
self.assertTrue(
gate.check(needs, "pull_request", _event()),
"skipping test-sqlite on a docs-only PR must not be justified",
)

def test_docker_only_requires_docker_jobs(self) -> None:
needs = _skip(_needs("docker"), gate.CODE_JOBS)
self.assertEqual(gate.check(needs, "pull_request", _event()), [])
Expand Down Expand Up @@ -193,7 +208,10 @@ def _run(self, needs: str) -> subprocess.CompletedProcess:
)

def test_docs_only_cli_succeeds(self) -> None:
result = self._run(json.dumps(_skip(_needs("docs"), gate.CONDITIONAL_JOBS)))
# test-sqlite runs on a docs-only PR (it carries the doc-contract
# guards); every other conditional job may skip.
allowed = gate.CONDITIONAL_JOBS - {"test-sqlite"}
result = self._run(json.dumps(_skip(_needs("docs"), allowed)))
self.assertEqual(result.returncode, 0, result.stderr)

def test_malformed_json_cli_fails_with_diagnostic(self) -> None:
Expand Down