Skip to content
Open
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
38 changes: 38 additions & 0 deletions .github/workflows/opencode-fact-gate-quality-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: OpenCode Fact Gate Quality CI

on:
pull_request:
paths:
- ".github/workflows/opencode-fact-gate-quality-ci.yml"
- ".github/workflows/opencode-review-dispatch.yml"
- "scripts/ci/test_opencode_fact_gate_contract.sh"

permissions:
contents: read

concurrency:
group: opencode-fact-gate-quality-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
fact-gate-contract:
name: fact-gate-contract
runs-on: ubuntu-24.04
timeout-minutes: 10
steps:
- name: Harden runner
uses: step-security/harden-runner@bf7454d06d71f1098171f2acdf0cd4708d7b5920 # v2.20.0
with:
egress-policy: audit

- name: Checkout exact pull request head
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ github.event.pull_request.head.sha }}
persist-credentials: false

- name: Verify OpenCode fact-gate evidence contract
shell: bash --noprofile --norc -e -o pipefail {0}
run: |
test "$(git rev-parse HEAD)" = "${{ github.event.pull_request.head.sha }}"
bash scripts/ci/test_opencode_fact_gate_contract.sh
10 changes: 10 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,16 @@ this file. The format follows Keep a Changelog, and versioned releases follow
Semantic Versioning where the repository publishes a release.

## [Unreleased]
- Wire `scripts/ci/test_opencode_fact_gate_contract.sh` (the 15-assertion
regression contract for the fact-gate evidence strings in
`opencode-review-dispatch.yml`) into CI: a new
`.github/workflows/opencode-fact-gate-quality-ci.yml` runs it on every pull
request touching that workflow or the contract script itself, mirroring how
`test_strix_quick_gate.sh` is invoked by
`strix-changed-path-quality-ci.yml`. Previously the script existed and
passed but was never invoked by any workflow, script, or test, so its
assertions enforced nothing; `tests/test_opencode_fact_gate_quality_ci_contract.py`
now also pins the wiring and runs the contract script directly.
- **Pin `opencode-review-dispatch.yml` off the starved floating `ubuntu-latest` image.**
The 2026-09-01 floating-image fix (see that entry below) pinned `strix.yml`,
`opencode-review.yml`, and `noema-review.yml` -- the three required-check
Expand Down
93 changes: 93 additions & 0 deletions tests/test_opencode_fact_gate_quality_ci_contract.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
"""Permanent contract wiring the OpenCode fact-gate regression test into CI.

``scripts/ci/test_opencode_fact_gate_contract.sh`` asserts that
``.github/workflows/opencode-review-dispatch.yml`` still carries the
fact-gate evidence strings that stop OpenCode review from claiming a repo
path, evidence excerpt, or reviewer thread is unavailable without proof. A
real 15-assertion contract that no workflow ever invokes enforces nothing:
this module pins that ``opencode-fact-gate-quality-ci.yml`` actually runs it
on every pull request that could change either file, and executes the
regression contract directly so a change to either file that breaks it fails
this repository's own test suite too, not only the dedicated workflow.
"""

from __future__ import annotations

import subprocess
from pathlib import Path

_REPOSITORY_ROOT = Path(__file__).resolve().parents[1]
_CONTRACT_SCRIPT_PATH = (
_REPOSITORY_ROOT / "scripts/ci/test_opencode_fact_gate_contract.sh"
)
_DISPATCH_WORKFLOW_PATH = (
_REPOSITORY_ROOT / ".github/workflows/opencode-review-dispatch.yml"
)
_QUALITY_WORKFLOW_PATH = (
_REPOSITORY_ROOT / ".github/workflows/opencode-fact-gate-quality-ci.yml"
)


def _quality_workflow_text() -> str:
"""Return the workflow that wires the fact-gate contract into CI."""

return _QUALITY_WORKFLOW_PATH.read_text(encoding="utf-8")


def test_quality_workflow_watches_the_contract_and_the_dispatch_workflow() -> None:
"""A change to either watched file must retrigger this quality gate."""

quality_workflow = _quality_workflow_text()
assert (
' - ".github/workflows/opencode-review-dispatch.yml"\n'
in quality_workflow
)
assert (
' - "scripts/ci/test_opencode_fact_gate_contract.sh"\n'
in quality_workflow
)


def test_quality_workflow_actually_invokes_the_contract_script() -> None:
"""The workflow must execute the contract, not merely reference it."""

quality_workflow = _quality_workflow_text()
assert (
"bash scripts/ci/test_opencode_fact_gate_contract.sh\n" in quality_workflow
)
assert 'ref: ${{ github.event.pull_request.head.sha }}' in quality_workflow


def test_quality_workflow_watched_paths_resolve_to_repository_files() -> None:
"""Every watched path in the quality workflow must exist in the repo."""

quality_workflow = _quality_workflow_text()
watched_section = quality_workflow.split(" paths:\n", 1)[1].split(
"\n\npermissions:\n", 1
)[0]
watched_paths = [
line.strip()[2:].strip('"')
for line in watched_section.splitlines()
if line.strip().startswith("- ")
]

assert watched_paths
assert str(_CONTRACT_SCRIPT_PATH.relative_to(_REPOSITORY_ROOT)) in watched_paths
assert str(_DISPATCH_WORKFLOW_PATH.relative_to(_REPOSITORY_ROOT)) in watched_paths
for relative_path in watched_paths:
assert (_REPOSITORY_ROOT / relative_path).is_file(), relative_path


def test_fact_gate_contract_script_currently_passes() -> None:
"""The regression contract the workflow runs must pass right now too."""

result = subprocess.run(
["bash", str(_CONTRACT_SCRIPT_PATH)],
check=False,
capture_output=True,
text=True,
cwd=_REPOSITORY_ROOT,
)

assert result.returncode == 0, result.stderr
assert "OpenCode fact-gate contract OK" in result.stdout
Loading