diff --git a/.github/workflows/conditional-tests.yml b/.github/workflows/conditional-tests.yml index ad78aef..8cd1856 100644 --- a/.github/workflows/conditional-tests.yml +++ b/.github/workflows/conditional-tests.yml @@ -43,6 +43,10 @@ on: description: OBLITERATUS issue URL approving older software conditional evidence type: string default: "" + stale_evidence_expires: + description: ISO date when the stale-evidence waiver expires (maximum 30 days) + type: string + default: "" schedule: - cron: "17 6 * * 0" release: @@ -57,6 +61,7 @@ concurrency: env: CONDITIONAL_CANDIDATE_SHA: ${{ github.event.inputs.candidate_sha || github.sha }} + CONDITIONAL_STALE_EVIDENCE_EXPIRES: ${{ github.event.inputs.stale_evidence_expires || '' }} CONDITIONAL_STALE_EVIDENCE_ISSUE: ${{ github.event.inputs.stale_evidence_issue || '' }} CONDITIONAL_STALE_EVIDENCE_REASON: ${{ github.event.inputs.stale_evidence_reason || '' }} PIP_DISABLE_PIP_VERSION_CHECK: "1" @@ -119,6 +124,7 @@ jobs: --require-gate external-evaluation --stale-evidence-reason "$CONDITIONAL_STALE_EVIDENCE_REASON" --stale-evidence-issue "$CONDITIONAL_STALE_EVIDENCE_ISSUE" + --stale-evidence-expires "$CONDITIONAL_STALE_EVIDENCE_EXPIRES" - name: Upload model-runtime evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -160,6 +166,7 @@ jobs: --require-gate network-services --stale-evidence-reason "$CONDITIONAL_STALE_EVIDENCE_REASON" --stale-evidence-issue "$CONDITIONAL_STALE_EVIDENCE_ISSUE" + --stale-evidence-expires "$CONDITIONAL_STALE_EVIDENCE_EXPIRES" - name: Upload network evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 @@ -201,6 +208,7 @@ jobs: --require-gate operator-ui --stale-evidence-reason "$CONDITIONAL_STALE_EVIDENCE_REASON" --stale-evidence-issue "$CONDITIONAL_STALE_EVIDENCE_ISSUE" + --stale-evidence-expires "$CONDITIONAL_STALE_EVIDENCE_EXPIRES" - name: Upload UI evidence if: always() uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 diff --git a/scripts/check_conditional_policy.py b/scripts/check_conditional_policy.py index 891addb..ea367bf 100644 --- a/scripts/check_conditional_policy.py +++ b/scripts/check_conditional_policy.py @@ -6,6 +6,7 @@ from __future__ import annotations import argparse import json import re +from datetime import date, timedelta from pathlib import Path @@ -19,7 +20,10 @@ SOFTWARE_ONLY_GATES = ( "operator-ui", ) SHA = re.compile(r"^[0-9a-f]{40}$") -ISSUE_URL = "https://github.com/elder-plinius/OBLITERATUS/issues/" +ISSUE_URL = re.compile( + r"^https://github\.com/elder-plinius/OBLITERATUS/issues/[1-9][0-9]*$", +) +MAX_STALE_EXCEPTION_DAYS = 30 def validate(policy_path: Path, quality_path: Path, workflow_path: Path) -> list[str]: @@ -88,13 +92,24 @@ def _load_json_object(path: Path, label: str, errors: list[str]) -> dict: return value -def _valid_stale_exception(reason: str | None, issue: str | None) -> bool: - return ( - isinstance(reason, str) - and bool(reason.strip()) - and isinstance(issue, str) - and issue.startswith(ISSUE_URL) - ) +def _valid_stale_exception( + reason: str | None, + issue: str | None, + expires: str | None, + *, + today: date, +) -> bool: + if not isinstance(reason, str) or not reason.strip(): + return False + if not isinstance(issue, str) or ISSUE_URL.fullmatch(issue) is None: + return False + if not isinstance(expires, str): + return False + try: + expiry = date.fromisoformat(expires) + except ValueError: + return False + return today <= expiry <= today + timedelta(days=MAX_STALE_EXCEPTION_DAYS) def validate_evidence( @@ -105,6 +120,8 @@ def validate_evidence( required_gates: list[str] | None = None, stale_exception_reason: str | None = None, stale_exception_issue: str | None = None, + stale_exception_expires: str | None = None, + today: date | None = None, ) -> list[str]: """Validate selected software-only conditional evidence against a candidate SHA.""" @@ -129,10 +146,21 @@ def validate_evidence( if gate_id not in policy_gate_ids: errors.append(f"conditional policy does not define gate {gate_id}") - exception = _valid_stale_exception(stale_exception_reason, stale_exception_issue) - if (stale_exception_reason or stale_exception_issue) and not exception: + today = today or date.today() + exception = _valid_stale_exception( + stale_exception_reason, + stale_exception_issue, + stale_exception_expires, + today=today, + ) + if ( + stale_exception_reason + or stale_exception_issue + or stale_exception_expires + ) and not exception: errors.append( - "stale evidence exception requires a non-empty reason and an OBLITERATUS issue URL", + "stale evidence exception requires a reason, a canonical OBLITERATUS issue URL, " + "and an ISO expiry no more than 30 days away", ) for gate_id in requested: @@ -188,6 +216,11 @@ def main() -> int: default="", help="OBLITERATUS issue URL approving older software conditional evidence", ) + parser.add_argument( + "--stale-evidence-expires", + default="", + help="ISO expiry date for a stale-evidence waiver (maximum 30 days)", + ) args = parser.parse_args() errors = validate(args.policy, args.quality, args.workflow) if args.evidence_dir is not None: @@ -202,6 +235,7 @@ def main() -> int: required_gates=args.require_gate or None, stale_exception_reason=args.stale_evidence_reason or None, stale_exception_issue=args.stale_evidence_issue or None, + stale_exception_expires=args.stale_evidence_expires or None, ), ) if errors: diff --git a/tests/test_conditional_evidence_freshness.py b/tests/test_conditional_evidence_freshness.py index 81973a3..d352b25 100644 --- a/tests/test_conditional_evidence_freshness.py +++ b/tests/test_conditional_evidence_freshness.py @@ -3,6 +3,7 @@ from __future__ import annotations import json +from datetime import date from pathlib import Path from scripts import check_conditional_policy @@ -11,6 +12,7 @@ from scripts import check_conditional_policy ROOT = Path(__file__).parents[1] SHA = "0123456789abcdef0123456789abcdef01234567" OLD_SHA = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +TODAY = date(2026, 8, 15) def _write_evidence(path: Path, gate: str, *, git_sha: str = SHA, status: str = "passed") -> None: @@ -65,6 +67,8 @@ def test_maintainer_exception_only_allows_sha_mismatch(tmp_path): required_gates=["external-evaluation"], stale_exception_reason="Gate reviewed against equivalent conditional surface.", stale_exception_issue="https://github.com/elder-plinius/OBLITERATUS/issues/123", + stale_exception_expires="2026-08-30", + today=TODAY, ) == [] @@ -80,7 +84,8 @@ def test_exception_requires_reason_and_issue_url(tmp_path): ) assert ( - "stale evidence exception requires a non-empty reason and an OBLITERATUS issue URL" + "stale evidence exception requires a reason, a canonical OBLITERATUS issue URL, " + "and an ISO expiry no more than 30 days away" ) in errors assert ( "conditional evidence external-evaluation git_sha " @@ -97,3 +102,21 @@ def test_evidence_freshness_does_not_make_hardware_lanes_mandatory(tmp_path): candidate_sha=SHA, required_gates=["cuda-runtime"], ) == ["hardware or credential gate is not software-only: cuda-runtime"] + + +def test_exception_rejects_noncanonical_issue_and_unbounded_expiry(tmp_path): + _write_evidence(tmp_path / "network-services.json", "network-services", git_sha=OLD_SHA) + + errors = check_conditional_policy.validate_evidence( + ROOT / "ci" / "conditional-test-policy.json", + tmp_path, + candidate_sha=SHA, + required_gates=["network-services"], + stale_exception_reason="reviewed", + stale_exception_issue="https://github.com/elder-plinius/OBLITERATUS/issues/not-a-number", + stale_exception_expires="2027-01-01", + today=TODAY, + ) + + assert errors[0].startswith("stale evidence exception requires a reason") + assert "does not match candidate" in errors[1]