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
Original file line number Diff line number Diff line change
Expand Up @@ -58,13 +58,28 @@ def fetch_live_contexts(repo: str, branch: str) -> list[str]:


def reconcile(
manifest_path: Path, live_contexts: list[str]
manifest_path: Path, live_contexts: list[str], branch: str | None = None
) -> tuple[set[str], set[str]]:
"""Compare manifest REQUIRED rows against a branch's live required contexts.

OMN-15117: the manifest is otherwise single-branch (dev-scoped, see the
file header), but a REQUIRED row may carry an optional `branch:` field to
mark it as applying to one specific branch only (e.g. `verify / verify`,
live-required on `main` and absent from `dev`'s required set). A row
without `branch:` is treated as applicable regardless of which branch is
being reconciled (legacy/default behavior, unchanged). A row WITH
`branch:` is only counted toward `manifest_required` when it matches the
`branch` argument — this prevents a main-only addition from surfacing as
a false `stale_in_manifest` when reconciling against `dev` (and vice
versa). When `branch` is None (no branch context given), every REQUIRED
row counts, matching pre-OMN-15117 behavior.
"""
manifest = yaml.safe_load(manifest_path.read_text(encoding="utf-8")) or {}
manifest_required = {
gate["name"]
for gate in manifest.get("gates", [])
if gate.get("mode") == "REQUIRED"
and (branch is None or gate.get("branch", branch) == branch)
}
live_set = set(live_contexts)

Expand All @@ -81,7 +96,9 @@ def main() -> int:
args = parser.parse_args()

live_contexts = fetch_live_contexts(args.repo, args.branch)
missing_from_manifest, stale_in_manifest = reconcile(args.manifest, live_contexts)
missing_from_manifest, stale_in_manifest = reconcile(
args.manifest, live_contexts, branch=args.branch
)

if missing_from_manifest or stale_in_manifest:
if missing_from_manifest:
Expand Down
17 changes: 17 additions & 0 deletions .github/required-checks.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -460,6 +460,23 @@ gates:
skip_semantics: never
rationale: "Central OCC eligibility gate (cross-repo reusable hosted in omnibase_core)."

- name: "verify / verify"
mode: REQUIRED
branch: main
producer_kind: cross_repo
caller_workflow: call-receipt-gate.yml
caller_job: verify
cross_repo_ref: "OmniNode-ai/omnibase_core/.github/workflows/receipt-gate.yml@main"
skip_semantics: never
rationale: "Receipt Gate — live required status check on omniclaude `main` branch protection
(dev→main promotion boundary), absent from `dev`'s required set. This manifest is otherwise
dev-scoped (see file header); this row is a deliberate main-only addition (OMN-15117) closing
a guard blind spot: `verify / verify` was live-required on main since OMN-10411 but had zero
row here, so validate_no_required_check_skip_vectors.py never had a context to check it
against and OMN-15057's remediation (#1931) never touched this file. `verify`'s `needs:
occ-preflight` had no `if:` at all (vector-5, OMN-15057 shape) until this ticket added
`if: always()`."

- name: "call-reject-skip-token / scan / reject-skip-gate-token"
mode: REQUIRED
producer_kind: local
Expand Down
21 changes: 21 additions & 0 deletions .github/workflows/call-receipt-gate.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,29 @@ jobs:
contracts-dir: contracts
receipts-dir: drift/dod_receipts

# OMN-15117 (OMN-15057 vector-5 shape, missed by #1931 because `verify / verify`
# had no required-checks.yaml row for the guard to check): `needs: occ-preflight`
# with no `if:` override means GitHub's implicit job-level `if:` — `success()`
# evaluated over `needs:` — SKIPS this job on any occ-preflight failure/
# cancellation, and a skipped job satisfies branch protection (skipped counts
# as passing). `verify / verify` is live-required on `main` (the dev->main
# promotion boundary), so that was a silent-pass bypass on the load-bearing gate.
#
# `if: always()` makes `verify` run unconditionally so it can never silently
# skip-as-pass. This is a PARTIAL fix in isolation — a job whose body is
# `uses:` (calls a reusable workflow) cannot also declare `steps:`, so there
# is no way to insert an inline `needs.occ-preflight.result != 'success'` ->
# `exit 1` guard step ahead of the call (unlike Shape-A jobs with their own
# steps, e.g. main-target-guard.yml, url-authority-gate.yml). Real enforcement
# does not depend on this job's own result: `occ-preflight / eligibility`
# (produced unconditionally, no `needs`/`if`, by call-occ-preflight.yml) is
# independently required on this repo's dev+main branch protection, so an
# occ-preflight failure still blocks merge via that separate required context
# even though this job decouples. Same precedent/rationale as omnibase_core#1499
# applied to its own `deploy-gate`/`verify` jobs (identical `uses:`-only shape).
verify:
needs: occ-preflight
if: always()
# self-gating-ok: intentional cross-repo caller — omniclaude does not host
# receipt-gate.yml itself; the reusable workflow lives in omnibase_core.
uses: OmniNode-ai/omnibase_core/.github/workflows/receipt-gate.yml@main
24 changes: 15 additions & 9 deletions .github/workflows/required-check-manifest-reconcile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,15 +9,21 @@
# branch protection needs a token scope (administration:read) that must not be
# granted to a PR-triggered workflow (read/exfil risk from a malicious PR).
#
# SCOPE NOTE (OMN-14854): `main`'s required_status_checks set is verifiably
# NOT identical to `dev`'s (confirmed live 2026-07-20 — e.g. main additionally
# requires "Security Gate" and "verify / verify"; dev additionally requires
# "Cross-Repo Boundary Parity" and "no-noncanonical-lifecycle-classes"). The
# manifest schema in this PR is single-branch (dev). A `main`-scoped manifest
# section is an explicit open follow-up, not resolved here — do not add a
# `main` reconcile step until the manifest schema grows a per-branch gates
# list; a naive main-vs-this-manifest comparison would fail on every run by
# construction, which is worse than no signal.
# SCOPE NOTE (OMN-14854, updated OMN-15117): `main`'s required_status_checks
# set is verifiably NOT identical to `dev`'s (confirmed live 2026-07-20 — e.g.
# main additionally requires "Security Gate" and "verify / verify"; dev
# additionally requires "Cross-Repo Boundary Parity" and
# "no-noncanonical-lifecycle-classes"). The manifest is still single-branch
# (dev) in the sense that this job only ever reconciles against `dev` — no
# `main` reconcile step exists here, and adding one against the current
# manifest would still fail on every run by construction (most rows have no
# `branch:` field, i.e. dev-only in practice). OMN-15117 added an optional
# per-row `branch:` field (`reconcile()` in reconcile_manifest_vs_live.py) so
# a specific main-only-required context (`verify / verify`) can be represented
# without corrupting this dev-only reconcile — reconcile() excludes any row
# whose `branch:` doesn't match the branch being checked. This is NOT a full
# `main`-scoped manifest section (the remaining main-only gap, "Security
# Gate", still has no row) — that remains an open follow-up.
#
# This job is NOT itself a required status check (push/schedule cannot be
# gated by required_status_checks anyway). Its failure surfaces as a
Expand Down
2 changes: 1 addition & 1 deletion .secrets.baseline

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions tests/ci/test_reconcile_manifest_vs_live.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,3 +74,65 @@ def test_advisory_rows_are_excluded_from_manifest_required_set(tmp_path: Path) -
missing, stale = reconcile(manifest_path, ["A"])
assert missing == set()
assert stale == set()


def test_branch_scoped_row_excluded_when_reconciling_a_different_branch(
tmp_path: Path,
) -> None:
"""OMN-15117: a REQUIRED row carrying `branch: main` must not surface as
`stale_in_manifest` when reconciling `dev` — it simply isn't in scope for
that branch's comparison."""
manifest_path = _manifest(
tmp_path,
[
{"name": "A", "mode": "REQUIRED"},
{"name": "verify / verify", "mode": "REQUIRED", "branch": "main"},
],
)
missing, stale = reconcile(manifest_path, ["A"], branch="dev")
assert missing == set()
assert stale == set()


def test_branch_scoped_row_included_when_reconciling_its_own_branch(
tmp_path: Path,
) -> None:
"""The same row IS in scope, and must resolve cleanly, when reconciling
against the branch it declares."""
manifest_path = _manifest(
tmp_path,
[
{"name": "A", "mode": "REQUIRED"},
{"name": "verify / verify", "mode": "REQUIRED", "branch": "main"},
],
)
missing, stale = reconcile(manifest_path, ["A", "verify / verify"], branch="main")
assert missing == set()
assert stale == set()


def test_branch_scoped_row_missing_from_live_is_still_flagged_on_its_own_branch(
tmp_path: Path,
) -> None:
"""A branch-scoped row is not exempt from detection on its own branch —
only exempt from cross-branch noise."""
manifest_path = _manifest(
tmp_path,
[{"name": "verify / verify", "mode": "REQUIRED", "branch": "main"}],
)
missing, stale = reconcile(manifest_path, [], branch="main")
assert missing == set()
assert stale == {"verify / verify"}


def test_no_branch_argument_preserves_legacy_behavior(tmp_path: Path) -> None:
"""Calling reconcile() without a `branch` kwarg (pre-OMN-15117 call sites)
must count every REQUIRED row regardless of any `branch:` field — this is
the pre-existing, un-scoped comparison."""
manifest_path = _manifest(
tmp_path,
[{"name": "verify / verify", "mode": "REQUIRED", "branch": "main"}],
)
missing, stale = reconcile(manifest_path, ["verify / verify"])
assert missing == set()
assert stale == set()
Loading