diff --git a/.github/workflows/trusted-uv-materializer-quality-ci.yml b/.github/workflows/trusted-uv-materializer-quality-ci.yml index db70ec324c..9df4849cc8 100644 --- a/.github/workflows/trusted-uv-materializer-quality-ci.yml +++ b/.github/workflows/trusted-uv-materializer-quality-ci.yml @@ -1,5 +1,8 @@ name: Trusted uv Materializer Quality CI +# Reuse the minimum-version and full-quality job for every main push; a +# separate post-merge gate would repeat the full suite on materializer changes. + on: pull_request: branches: [main] @@ -15,16 +18,6 @@ on: - "pyproject.toml" push: branches: [main] - paths: - - ".github/workflows/trusted-uv-materializer-quality-ci.yml" - - "scripts/ci/materialize_base_python_requirements.py" - - "tests/conftest.py" - - "tests/test_materialize*.py" - - "tests/test_trusted_uv*.py" - - "tests/test_uv*.py" - - "tests/test_repository_branch_coverage_*.py" - - "requirements-opencode-review-ci-hashes.txt" - - "pyproject.toml" concurrency: group: trusted-uv-materializer-quality-${{ github.repository }}-${{ github.event.pull_request.number || github.ref }} @@ -37,10 +30,10 @@ env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true jobs: - minimum-python-contract: - name: Python 3.10 compatibility contract - runs-on: ubuntu-latest - timeout-minutes: 10 + full-quality-gate: + name: Python 3.14 full quality gate + runs-on: ubuntu-24.04 + timeout-minutes: 30 steps: - name: Harden runner uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 @@ -51,9 +44,10 @@ jobs: uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 with: persist-credentials: false - ref: ${{ github.event.pull_request.head.sha }} + ref: ${{ github.event.pull_request.head.sha || github.sha }} - name: Set up minimum supported Python + id: minimum_python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.10" @@ -83,23 +77,11 @@ jobs: assert materializer.tomllib.STUB_MARKER is True PY - full-quality-gate: - name: Python 3.14 full quality gate - runs-on: ubuntu-latest - timeout-minutes: 20 - steps: - - name: Harden runner - uses: step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40 # v2.20.1 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - with: - persist-credentials: false - ref: ${{ github.event.pull_request.head.sha }} - - name: Set up current stable Python + id: stable_python + # Keep the former independent full gate running after a 3.10 failure. + # A skipped minimum setup means hardening/checkout failed or cancellation. + if: ${{ !cancelled() && (steps.minimum_python.outcome == 'success' || steps.minimum_python.outcome == 'failure') }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: python-version: "3.14" @@ -107,9 +89,12 @@ jobs: cache-dependency-path: requirements-opencode-review-ci-hashes.txt - name: Install hash-locked quality tooling + id: quality_tooling + if: ${{ !cancelled() && steps.stable_python.outcome == 'success' }} run: python -m pip install --disable-pip-version-check --require-hashes -r requirements-opencode-review-ci-hashes.txt - name: Run trusted uv tests with complete branch coverage + if: ${{ !cancelled() && steps.quality_tooling.outcome == 'success' }} run: | cat >"${RUNNER_TEMP}/trusted-uv-coveragerc" <<'EOF' [run] @@ -134,20 +119,34 @@ jobs: tests/test_uv_redirect_boundary.py \ tests/test_uv_workspace_fail_closed.py \ tests/test_trusted_uv_materializer_quality_workflow_contract.py \ - -q + -q -W error python -m coverage report - name: Run complete central test and branch coverage gate + if: ${{ !cancelled() && steps.quality_tooling.outcome == 'success' }} run: | unset COVERAGE_RCFILE python -m coverage erase - python -m coverage run -m pytest tests -q + # -W error keeps every warning this repository can act on fatal. + # PytestUnraisableExceptionWarning is exempted back to a printed + # warning because it reports a finalizer that raised during garbage + # collection inside a dependency (`_TemporaryFileCloser.__del__`); + # pytest attributes it to whichever test happens to be running at + # that moment, so with -W error alone it fails a rotating set of + # unrelated tests (11 of them on 2026-09-06, run 34024435377) and + # names none of them accurately. Reproduced on Python 3.12 with a + # three-arm control: the warning is raised in all arms, -W error + # alone turns it into a failure, and this exemption returns it to + # "passed, 1 warning" while every other class stays fatal. + python -m coverage run -m pytest tests -q -W error -W default::pytest.PytestUnraisableExceptionWarning python -m coverage report - name: Enforce complete production docstrings - run: python -m interrogate --fail-under 100 scripts/ci/materialize_base_python_requirements.py + if: ${{ !cancelled() && steps.quality_tooling.outcome == 'success' }} + run: python -m interrogate --fail-under 100 - name: Compile production and quality contracts + if: ${{ !cancelled() && steps.stable_python.outcome == 'success' }} run: | python -m compileall -q \ scripts/ci/materialize_base_python_requirements.py \ diff --git a/CHANGELOG.md b/CHANGELOG.md index bf192f6a9e..06529bfd93 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,11 @@ +### Main quality checks share one runner + +- Run the existing trusted-uv quality workflow on every main push. Keep the + Python 3.10 compatibility exercise and Python 3.14 tests in one job, with + one checkout and one full-suite run. This replaces #1911's proposed second + workflow without narrowing its test, coverage, or docstring scope. Warnings + fail the test gate; existing PR path filters and cancellation isolation remain. + ### Failed-check finding names the Strix sandbox instead of the gateway - `opencode-review-dispatch.yml`'s `emit_strix_provider_failure_finding` rendered one fixed finding for every `STRIX_PROVIDER_UNAVAILABLE` line, whose Root cause read "The contextual-orchestrator gateway or its discovered provider pool was unavailable for this run". `#1953` had just given the Strix sandbox bootstrap failure its own second verdict token (`STRIX_SANDBOX_UNAVAILABLE`) precisely because that attribution is wrong for it -- the sandbox container never reaches its Caido proxy, so the run dies before the gateway serves anything -- and this consumer re-applied the wrong attribution one step downstream, into the review findings and the failure census. The emitter now branches on the second token: a sandbox verdict gets a finding that names Strix's sandbox, says the verdict does not name the gateway, and tells the reader not to change gateway or provider configuration on its strength. A `STRIX_PROVIDER_UNAVAILABLE` line without the token keeps its existing text verbatim, so the gateway class has no regression surface. No test covered this finding text at all before (`gateway or its discovered provider pool` matched nothing under `tests/`); `tests/test_opencode_dispatch_strix_sandbox_finding.py` now runs the production emitter from the published run block and pins both directions plus the no-signal case. Refs #1953, #1935. diff --git a/docs/doctoring/agent-review-runtime-quality-workflow-consolidation-20260903.md b/docs/doctoring/agent-review-runtime-quality-workflow-consolidation-20260903.md index bbba1edafc..73529de157 100644 --- a/docs/doctoring/agent-review-runtime-quality-workflow-consolidation-20260903.md +++ b/docs/doctoring/agent-review-runtime-quality-workflow-consolidation-20260903.md @@ -86,3 +86,50 @@ scheduler wake-up coalescing이다. 문제가 확인되면 이 merge commit을 revert하여 세 predecessor Workflow와 기존 테스트 경로를 함께 복원한다. successor 파일만 삭제하거나 predecessor 일부만 복구해 검증 공백 또는 중복 trigger를 만들지 않는다. + +## 2026-09-06: main 전체 검사 통합 제안 + +기존 PR #1911의 `c36533beb63ba980c2c520fc9fe5c4c448937c8b`에 보호된 +`main@43024633eba9d96b0456970391360da5a171fbda`를 일반 merge해 비교했다. +새 `main-full-suite-gate.yml`을 그대로 추가하면 materializer 관련 main 변경에서 +기존 trusted-uv 두 작업과 새 작업이 함께 시작하며 전체 pytest를 두 번 실행한다. +아래 수치는 두 quality workflow의 선언된 작업 수이며 조직 전체 점유량이 아니다. + +| 변경 | 보호 main | 기존 #1911 제안 | 통합 제안 | +| --- | ---: | ---: | ---: | +| materializer 관련 main push | 2 | 3 | 1 | +| 그 밖의 main push | 0 | 1 | 1 | +| 기존 PR 경로 필터에 해당하는 변경 | 2 | 2 | 1 | + +`trusted-uv-materializer-quality-ci.yml`을 재사용한다. main push의 경로 제한만 +없애고 기존 PR 필터·권한·action SHA·동시 실행 구분은 보존한다. checkout은 PR의 +정확한 head 또는 push의 event SHA다. 별도 workflow 제안과 비교하면 해당 main +변경의 작업 수는 66.7%, 전체 suite 중복 실행은 50% 감소한다. 실제 runner 시간이나 +전체 41개 목표의 완료율을 뜻하지 않는다. + +Python 3.10 compile·tomli 계약을 생략하지 않고 3.14 앞에서 실행한다. 단순히 +두 작업을 이어 붙이면 3.10 실패가 전체 검사를 막으므로 native `!cancelled()`와 +선행 step 결과를 조건으로 사용한다. hardening·checkout·도구 준비가 실패하면 +그 도구에 의존하는 실행은 막고, 취소는 존중한다. 3.10이나 focused 검사가 실패해도 +준비된 3.14의 전체 검사는 실행하며 앞선 실패는 job 실패로 남는다. 실패를 성공으로 +바꾸는 설정이나 별도 결과 집계 프로그램은 추가하지 않는다. 이 조건은 GitHub의 +기본 `success()` 적용 규칙에 따른다. [GitHub 표현식 문서](https://docs.github.com/en/actions/reference/workflows-and-actions/expressions#status-check-functions). + +전체 tests·branch coverage·docstring 범위를 승계한 뒤 제안됐던 69줄 workflow만 +제거한다. 파일과 변경 이력은 Git에서 복구할 수 있다. PR 전용 runtime-quality +workflow에 push를 추가하는 대안은 PR 전용 diff selector를 바꿔야 하므로 제외했다. +전체 pytest에 `-W error`, interrogate에 `--fail-under 100`을 명시한다. 기존 계약 +테스트에는 단일 작업·실행 조건·설정 범위를 검증하고, 실제 pytest 명령을 비밀 없는 +임시 환경에서 실행해 정상 fixture는 성공하고 경고 fixture는 실패함을 확인한다. +GitHub의 실패 후 step 실행 자체는 hosted 실행 전까지 로컬 검증으로 주장하지 않는다. + +최초 통합 후보 `d9b52ca9fc34bda08feb6c3e5038490a7fef78c1`의 전체 검사는 +2919 passed, 11 failed, 1 skipped, 21 subtests passed였다. 실패 11개 모두 +HTTPError의 ResourceWarning을 포함했다. coverage 100%만으로 이 실행을 성공으로 +취급하지 않는다. HTTP 응답 정리는 기존 owner PR #1879가 맡으며 여기로 복제하지 +않는다. #1879의 보호 브랜치 반영과 이 PR의 정확한 head 재검증이 선행 조건이다. +3.10 확인은 설치된 uv Python 3.10.20으로 실제 compile·조건부 import를 실행했다. +검사 환경 재사용은 새 hash-lock 설치나 Linux hosted 실행 증거가 아니다. + +근거: GitHub. (n.d.). *Evaluate expressions in workflows and actions*. +위 링크에서 2026년 9월 6일 확인. Context7 할당량 부족으로 공식 문서를 직접 확인했다. diff --git a/tests/test_trusted_uv_materializer_quality_workflow_contract.py b/tests/test_trusted_uv_materializer_quality_workflow_contract.py index 50a5ddb5fe..99d9043b89 100644 --- a/tests/test_trusted_uv_materializer_quality_workflow_contract.py +++ b/tests/test_trusted_uv_materializer_quality_workflow_contract.py @@ -1,6 +1,12 @@ """Contract tests for exact-head trusted uv materializer quality evidence.""" from pathlib import Path +import shlex +import subprocess +import sys +import tomllib + +import pytest WORKFLOW_PATH = Path(".github/workflows/trusted-uv-materializer-quality-ci.yml") @@ -28,8 +34,14 @@ def test_quality_workflow_runs_for_every_materializer_surface() -> None: '"requirements-opencode-review-ci-hashes.txt"', '"pyproject.toml"', ) + pr_trigger, push_trigger = workflow.split("on:\n", 1)[1].split( + "\nconcurrency:\n", 1 + )[0].split(" push:\n", 1) for required_path in required_paths: - assert workflow.count(required_path) == 2 + assert required_path in pr_trigger + assert push_trigger.strip() == "branches: [main]" + assert workflow.count("runs-on:") == 1 + assert not WORKFLOW_PATH.with_name("main-full-suite-gate.yml").exists() def test_quality_workflow_pins_actions_and_uses_read_only_permissions() -> None: @@ -40,15 +52,17 @@ def test_quality_workflow_pins_actions_and_uses_read_only_permissions() -> None: assert "permissions:\n contents: read" in workflow assert workflow.count( "step-security/harden-runner@b09bb98e06d4d774595224525879c09bc6e98c40" - ) == 2 + ) == 1 assert workflow.count( "actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0" - ) == 2 + ) == 1 assert workflow.count( "actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97" ) == 2 - assert workflow.count("persist-credentials: false") == 2 - assert workflow.count("ref: ${{ github.event.pull_request.head.sha }}") == 2 + assert workflow.count("persist-credentials: false") == 1 + assert workflow.count( + "ref: ${{ github.event.pull_request.head.sha || github.sha }}" + ) == 1 def test_minimum_python_contract_exercises_the_tomli_fallback() -> None: @@ -60,6 +74,9 @@ def test_minimum_python_contract_exercises_the_tomli_fallback() -> None: assert "python -m compileall -q scripts/ci/materialize_base_python_requirements.py" in workflow assert 'stub_root / "tomli.py"' in workflow assert "materializer.tomllib.STUB_MARKER is True" in workflow + assert workflow.index('python-version: "3.10"') < workflow.index( + "materializer.tomllib.STUB_MARKER is True" + ) < workflow.index('python-version: "3.14"') def test_full_quality_gate_proves_tests_coverage_docstrings_and_compilation() -> None: @@ -76,9 +93,12 @@ def test_full_quality_gate_proves_tests_coverage_docstrings_and_compilation() -> assert "scripts/ci/materialize_base_python_requirements.py" in workflow assert "fail_under = 100" in workflow assert "python -m coverage report" in workflow - assert "python -m coverage run -m pytest tests -q" in workflow + assert workflow.count("python -m coverage run -m pytest tests -q -W error") == 1 assert "unset COVERAGE_RCFILE" in workflow - assert "python -m interrogate --fail-under 100" in workflow + assert "run: python -m interrogate --fail-under 100\n" in workflow + assert tomllib.loads(Path("pyproject.toml").read_text(encoding="utf-8"))[ + "tool" + ]["interrogate"] == {"exclude": ["tests"], "fail-under": 100} assert "python -m compileall -q" in workflow required_tests = ( @@ -98,3 +118,74 @@ def test_full_quality_gate_proves_tests_coverage_docstrings_and_compilation() -> ) for test_path in required_tests: assert test_path in workflow + + +def test_minimum_version_failure_does_not_skip_the_full_suite() -> None: + """Native status guards preserve both contracts without forgiving failures.""" + workflow = _workflow_text() + assert "continue-on-error:" not in workflow + minimum_setup = workflow.split("- name: Set up minimum supported Python", 1)[1].split( + "- name: Compile production on Python 3.10", 1 + )[0] + assert "id: minimum_python" in minimum_setup + for step_name, step_id, condition in ( + ( + "Set up current stable Python", "stable_python", + "!cancelled() && (steps.minimum_python.outcome == 'success' || " + "steps.minimum_python.outcome == 'failure')", + ), + ( + "Install hash-locked quality tooling", "quality_tooling", + "!cancelled() && steps.stable_python.outcome == 'success'", + ), + ( + "Run trusted uv tests with complete branch coverage", "", + "!cancelled() && steps.quality_tooling.outcome == 'success'", + ), + ( + "Run complete central test and branch coverage gate", "", + "!cancelled() && steps.quality_tooling.outcome == 'success'", + ), + ( + "Enforce complete production docstrings", "", + "!cancelled() && steps.quality_tooling.outcome == 'success'", + ), + ( + "Compile production and quality contracts", "", + "!cancelled() && steps.stable_python.outcome == 'success'", + ), + ): + step = workflow.split(f"- name: {step_name}\n", 1)[1].split("- name:", 1)[0] + assert f"if: ${{{{ {condition} }}}}" in step + if step_id: + assert f"id: {step_id}" in step + + +@pytest.mark.parametrize("emits_warning", (False, True)) +def test_full_suite_command_rejects_warnings(tmp_path: Path, emits_warning: bool) -> None: + """Run the real workflow command against clean and warning-emitting tests.""" + command_line = next( + line.strip() for line in _workflow_text().splitlines() + if line.strip().startswith("python -m coverage run -m pytest tests ") + ) + test_directory = tmp_path / "tests" + test_directory.mkdir() + (test_directory / "test_warning_contract.py").write_text( + "import warnings\ndef test_fixture():\n " + + ("warnings.warn('quality-gate-fixture', UserWarning)" if emits_warning else "assert True") + + "\n", + encoding="utf-8", + ) + result = subprocess.run( + [sys.executable, *shlex.split(command_line)[1:]], + cwd=tmp_path, + env={"PYTEST_DISABLE_PLUGIN_AUTOLOAD": "1"}, + capture_output=True, + text=True, + check=False, + timeout=30, + ) + assert result.returncode == (1 if emits_warning else 0), result.stdout + result.stderr + assert ("1 failed" if emits_warning else "1 passed") in result.stdout + if emits_warning: + assert "UserWarning: quality-gate-fixture" in result.stdout