diff --git a/.github/workflows/accounting-information-platform-hourly-review-repair.yml b/.github/workflows/accounting-information-platform-hourly-review-repair.yml deleted file mode 100644 index 83e1190f04..0000000000 --- a/.github/workflows/accounting-information-platform-hourly-review-repair.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Accounting Information Platform Hourly Review Repair - -on: - schedule: - # Minute 27 avoids existing organization product callers and minute-zero pressure. - - cron: "27 * * * *" - -concurrency: - group: accounting-information-platform-hourly-review-repair - # Central OpenCode, Noema, and exact-head accounting checks can exceed one hour. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/accounting-information-platform - base_branch: develop - max_prs: "50" - max_dispatches: "1" - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/afipc-hourly-review-repair.yml b/.github/workflows/afipc-hourly-review-repair.yml deleted file mode 100644 index 3191e5ea03..0000000000 --- a/.github/workflows/afipc-hourly-review-repair.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: aFIPC Hourly Review Repair - -on: - schedule: - # Minute 2 avoids pg-llm-batch (1), kaefa (3), LineageWeave (4), - # codec-carver (5), life-os (6), Wardnet (7), mightyETL (8), - # psychometrics-commons (9), OriginWeave (10), naruon (11), - # DiagramWeave (12), pg-erd-cloud (13), mhtml-etl-gateway (14), - # html4tree (15), nonnest2 (16), orchestrator (17), newsdom-api (18), - # noema (19), github (21), Clearfolio (23), accounting-information-platform (27), - # Keyverse (29), Scopeweave (31), contextual-orchestrator (34), DiskSage (37), Appguardrail (41), - # governance-risk-compliance (43), fast-mlsirm (49), BandScope (53), - # Inkspan (56), orgmetra (58), and semantic-data-portal (59). - - cron: "2 * * * *" - -concurrency: - group: afipc-hourly-review-repair - # A later heartbeat must not cancel an in-flight FIPC or calibration RCA. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/aFIPC - base_branch: master - max_prs: "50" - max_dispatches: "1" - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/bandscope-hourly-review-repair.yml b/.github/workflows/bandscope-hourly-review-repair.yml deleted file mode 100644 index 78e5276ec2..0000000000 --- a/.github/workflows/bandscope-hourly-review-repair.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: BandScope Hourly Review Repair - -on: - schedule: - # Minute 53 avoids established product-specific heartbeat minutes. - - cron: "53 * * * *" - -concurrency: - group: bandscope-hourly-review-repair - # Preserve a legitimate long-running root-cause analysis across heartbeats. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/bandscope - base_branch: develop - max_prs: "50" - max_dispatches: "1" - # Music, browser, Rust, and NVIDIA-backed review work can exceed one hour. - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/clearfolio-hourly-review-repair.yml b/.github/workflows/clearfolio-hourly-review-repair.yml deleted file mode 100644 index e8d2991fac..0000000000 --- a/.github/workflows/clearfolio-hourly-review-repair.yml +++ /dev/null @@ -1,26 +0,0 @@ -name: Clearfolio Hourly Review Repair - -on: - schedule: - # Offset the heartbeat from minute zero to reduce shared-runner congestion. - - cron: "23 * * * *" - -concurrency: - group: clearfolio-hourly-review-repair - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/clearfolio - base_branch: main - max_prs: "50" - max_dispatches: "1" - retry_hours: "1" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/contextual-orchestrator-hourly-review-repair.yml b/.github/workflows/contextual-orchestrator-hourly-review-repair.yml deleted file mode 100644 index a7aba287b3..0000000000 --- a/.github/workflows/contextual-orchestrator-hourly-review-repair.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: Contextual Orchestrator Hourly Review Repair - -on: - schedule: - # Minute 34 avoids the minute-zero runner surge and every existing sibling - # heartbeat (2, 7, 10, 14, 16, 17 central scheduler, 21, 23, 27, 31, - # 37, 41, 43, 49, 53, 58, 59). - - cron: "34 * * * *" - -concurrency: - group: contextual-orchestrator-hourly-review-repair - # The queue scan is bounded and the worker has its own exact-head lease. Do not - # discard an in-flight RCA merely because the next hourly heartbeat arrives. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - uses: ./.github/workflows/pr-review-fix-scheduler.yml - permissions: - contents: read - id-token: write - with: - target_repository: ContextualWisdomLab/contextual-orchestrator - base_branch: main - max_prs: "50" - max_dispatches: "1" - # Central OpenCode/NVIDIA NIM work can legitimately approach two hours. - # A two-hour same-head floor avoids duplicate writers without freezing the - # next eligible PR or confusing provider latency with a source-code defect. - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/disksage-hourly-review-repair.yml b/.github/workflows/disksage-hourly-review-repair.yml deleted file mode 100644 index 00106b2e0b..0000000000 --- a/.github/workflows/disksage-hourly-review-repair.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: DiskSage Hourly Review Repair - -on: - schedule: - # Minute 37 avoids the minute-zero runner surge and the Clearfolio heartbeat. - - cron: "37 * * * *" - -concurrency: - group: disksage-hourly-review-repair - # The queue scan is bounded and the worker has its own exact-head lease. Do not - # discard an in-flight RCA merely because the next hourly heartbeat arrives. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/disksage - base_branch: main - max_prs: "50" - max_dispatches: "1" - # Central OpenCode/NVIDIA NIM work can legitimately approach two hours. - # A two-hour same-head floor avoids duplicate writers without freezing the - # next eligible PR or confusing provider latency with a source-code defect. - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/fast-mlsirm-hourly-review-repair.yml b/.github/workflows/fast-mlsirm-hourly-review-repair.yml deleted file mode 100644 index a3651cce45..0000000000 --- a/.github/workflows/fast-mlsirm-hourly-review-repair.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: fast-mlsirm Hourly Review Repair - -on: - schedule: - # Minute 49 avoids minute-zero pressure and the existing product callers. - - cron: "49 * * * *" - -concurrency: - group: fast-mlsirm-hourly-review-repair - # Preserve bounded RCA when a later hourly heartbeat arrives. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/fast-mlsirm - base_branch: main - max_prs: "50" - max_dispatches: "1" - # Central OpenCode/NVIDIA NIM review and psychometric CI can approach two hours. - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/github-hourly-review-repair.yml b/.github/workflows/github-hourly-review-repair.yml deleted file mode 100644 index 7c8557ba6f..0000000000 --- a/.github/workflows/github-hourly-review-repair.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: Central GitHub Hourly Review Repair - -on: - schedule: - # Keep the control-plane queue moving without colliding with minute-zero jobs. - - cron: "21 * * * *" - -concurrency: - group: github-hourly-review-repair - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/.github - base_branch: main - max_prs: "50" - max_dispatches: "1" - resolve_unreviewed_conflicts: true - retry_hours: "1" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/governance-risk-compliance-hourly-review-repair.yml b/.github/workflows/governance-risk-compliance-hourly-review-repair.yml deleted file mode 100644 index 813fe360e2..0000000000 --- a/.github/workflows/governance-risk-compliance-hourly-review-repair.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Governance Risk Compliance Hourly Review Repair - -on: - schedule: - # Minute 43 avoids minute-zero pressure and the existing product callers. - - cron: "43 * * * *" - -concurrency: - group: governance-risk-compliance-hourly-review-repair - # Preserve an in-flight exact-head RCA when the next heartbeat arrives. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/governance-risk-compliance - base_branch: develop - max_prs: "50" - max_dispatches: "1" - # Central OpenCode, Noema, Strix, and security evidence can exceed one hour. - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/hourly-nvidia-nim-review-repair.yml b/.github/workflows/hourly-nvidia-nim-review-repair.yml index 5cd0b096f6..f141cbc971 100644 --- a/.github/workflows/hourly-nvidia-nim-review-repair.yml +++ b/.github/workflows/hourly-nvidia-nim-review-repair.yml @@ -15,21 +15,9 @@ on: - .github/workflows/pr-review-fix-scheduler.yml - scripts/ci/pr_review_fix_scheduler.py - .github/workflows/pr-review-autofix.yml - - .github/workflows/bandscope-hourly-review-repair.yml - - .github/workflows/contextual-orchestrator-hourly-review-repair.yml - - .github/workflows/clearfolio-hourly-review-repair.yml - - .github/workflows/disksage-hourly-review-repair.yml - - .github/workflows/inkspan-hourly-review-repair.yml - - .github/workflows/lineageweave-hourly-review-repair.yml - - .github/workflows/fast-mlsirm-hourly-review-repair.yml - - .github/workflows/github-hourly-review-repair.yml - - .github/workflows/governance-risk-compliance-hourly-review-repair.yml + - .github/workflows/hourly-review-repair.yml + - .github/workflows/hourly-review-repair-registry-retirement.yml - .github/workflows/hourly-nvidia-nim-review-repair.yml - - .github/workflows/nonnest2-hourly-review-repair.yml - - .github/workflows/orgmetra-hourly-review-repair.yml - - .github/workflows/originweave-hourly-review-repair.yml - - .github/workflows/quarantine-sandbox-hourly-review-repair.yml - - .github/workflows/afipc-hourly-review-repair.yml - scripts/ci/pr_review_conflict_scope.py - scripts/ci/pr_review_autofix_context.py - scripts/ci/zdr_policy.py @@ -42,22 +30,13 @@ on: - docs/doctoring/contextual-orchestrator-vendored-sidecar.md - docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md - docs/doctoring/review-repair-quality-workflow-identity.md + - docs/doctoring/hourly-review-repair-registry-retirement.md - docs/product-technical-gap-baseline.md - CHANGELOG.md - - tests/test_bandscope_hourly_review_caller.py - - tests/test_disksage_hourly_review_caller.py - - tests/test_inkspan_hourly_review_caller.py - - tests/test_lineageweave_hourly_review_caller.py - - tests/test_fast_mlsirm_hourly_review_caller.py + - tests/test_hourly_review_repair_callers.py + - tests/test_hourly_review_repair_registry_retirement.py - tests/test_github_hourly_conflict_repair.py - - tests/test_governance_risk_compliance_hourly_review_caller.py - tests/test_hourly_scheduler_runtime_budget.py - - tests/test_nonnest2_hourly_review_caller.py - - tests/test_orgmetra_hourly_review_caller.py - - tests/test_originweave_hourly_review_caller.py - - tests/test_quarantine_sandbox_hourly_review_caller.py - - tests/test_contextual_orchestrator_hourly_review_caller.py - - tests/test_afipc_hourly_review_caller.py - tests/test_hourly_autofix_context_quality_gate.py - tests/test_pr_review_conflict_scope.py - tests/test_pr_review_conflict_scope_control_files.py @@ -92,21 +71,9 @@ on: - .github/workflows/pr-review-fix-scheduler.yml - scripts/ci/pr_review_fix_scheduler.py - .github/workflows/pr-review-autofix.yml - - .github/workflows/bandscope-hourly-review-repair.yml - - .github/workflows/contextual-orchestrator-hourly-review-repair.yml - - .github/workflows/clearfolio-hourly-review-repair.yml - - .github/workflows/disksage-hourly-review-repair.yml - - .github/workflows/inkspan-hourly-review-repair.yml - - .github/workflows/lineageweave-hourly-review-repair.yml - - .github/workflows/fast-mlsirm-hourly-review-repair.yml - - .github/workflows/github-hourly-review-repair.yml - - .github/workflows/governance-risk-compliance-hourly-review-repair.yml + - .github/workflows/hourly-review-repair.yml + - .github/workflows/hourly-review-repair-registry-retirement.yml - .github/workflows/hourly-nvidia-nim-review-repair.yml - - .github/workflows/nonnest2-hourly-review-repair.yml - - .github/workflows/orgmetra-hourly-review-repair.yml - - .github/workflows/originweave-hourly-review-repair.yml - - .github/workflows/quarantine-sandbox-hourly-review-repair.yml - - .github/workflows/afipc-hourly-review-repair.yml - scripts/ci/pr_review_conflict_scope.py - scripts/ci/pr_review_autofix_context.py - scripts/ci/zdr_policy.py @@ -119,22 +86,13 @@ on: - docs/doctoring/contextual-orchestrator-vendored-sidecar.md - docs/adr/0003-contextual-orchestrator-vendored-free-zdr.md - docs/doctoring/review-repair-quality-workflow-identity.md + - docs/doctoring/hourly-review-repair-registry-retirement.md - docs/product-technical-gap-baseline.md - CHANGELOG.md - - tests/test_bandscope_hourly_review_caller.py - - tests/test_disksage_hourly_review_caller.py - - tests/test_inkspan_hourly_review_caller.py - - tests/test_lineageweave_hourly_review_caller.py - - tests/test_fast_mlsirm_hourly_review_caller.py + - tests/test_hourly_review_repair_callers.py + - tests/test_hourly_review_repair_registry_retirement.py - tests/test_github_hourly_conflict_repair.py - - tests/test_governance_risk_compliance_hourly_review_caller.py - tests/test_hourly_scheduler_runtime_budget.py - - tests/test_nonnest2_hourly_review_caller.py - - tests/test_orgmetra_hourly_review_caller.py - - tests/test_originweave_hourly_review_caller.py - - tests/test_quarantine_sandbox_hourly_review_caller.py - - tests/test_contextual_orchestrator_hourly_review_caller.py - - tests/test_afipc_hourly_review_caller.py - tests/test_hourly_autofix_context_quality_gate.py - tests/test_pr_review_conflict_scope.py - tests/test_pr_review_conflict_scope_control_files.py @@ -222,20 +180,10 @@ jobs: tests/test_zdr_policy.py \ tests/test_contextual_orchestrator_review_policy.py \ tests/test_contextual_orchestrator_review_sidecar_contract.py \ - tests/test_bandscope_hourly_review_caller.py \ - tests/test_disksage_hourly_review_caller.py \ - tests/test_inkspan_hourly_review_caller.py \ - tests/test_lineageweave_hourly_review_caller.py \ - tests/test_fast_mlsirm_hourly_review_caller.py \ + tests/test_hourly_review_repair_callers.py \ + tests/test_hourly_review_repair_registry_retirement.py \ tests/test_github_hourly_conflict_repair.py \ - tests/test_governance_risk_compliance_hourly_review_caller.py \ tests/test_hourly_scheduler_runtime_budget.py \ - tests/test_nonnest2_hourly_review_caller.py \ - tests/test_orgmetra_hourly_review_caller.py \ - tests/test_originweave_hourly_review_caller.py \ - tests/test_quarantine_sandbox_hourly_review_caller.py \ - tests/test_contextual_orchestrator_hourly_review_caller.py \ - tests/test_afipc_hourly_review_caller.py \ tests/test_pr_review_conflict_scope_control_files.py \ tests/test_hourly_autofix_context_quality_gate.py \ tests/test_pr_review_conflict_scope_git_executable.py \ @@ -247,4 +195,4 @@ jobs: tests/test_pr_review_autofix_context_head_binding.py \ tests/test_pr_review_autofix_nvidia_nim_contract.py \ tests/test_pr_review_autofix_writer_security_contract.py - git diff --check \ No newline at end of file + git diff --check diff --git a/.github/workflows/hourly-review-repair-registry-retirement.yml b/.github/workflows/hourly-review-repair-registry-retirement.yml new file mode 100644 index 0000000000..13871bd8f8 --- /dev/null +++ b/.github/workflows/hourly-review-repair-registry-retirement.yml @@ -0,0 +1,121 @@ +name: Hourly Review Repair Registry Retirement + +# One-shot control-plane migration for the single-file hourly review-repair +# consolidation. GitHub keeps workflow registry identities after YAML paths are +# removed, so deleting the 18 legacy caller files without retiring their IDs +# would leave enabled orphan identities. This workflow runs only from reviewed +# source on protected main after the replacement is present, disables every +# legacy registry ID, verifies the disabled state, then disables its own +# registry identity last. After hosted success, remove this source file in a +# follow-up cleanup; its own registry identity will already be disabled. +on: + push: + branches: + - main + paths: + - .github/workflows/hourly-review-repair.yml + - .github/workflows/hourly-review-repair-registry-retirement.yml + +permissions: + actions: write + contents: read + +jobs: + retire-legacy-identities: + if: >- + github.repository == 'ContextualWisdomLab/.github' && + github.event_name == 'push' && + github.ref == 'refs/heads/main' + runs-on: ubuntu-24.04 + timeout-minutes: 10 + env: + GH_TOKEN: ${{ github.token }} + REPOSITORY: ${{ github.repository }} + REPLACEMENT_PATH: .github/workflows/hourly-review-repair.yml + SELF_PATH: .github/workflows/hourly-review-repair-registry-retirement.yml + steps: + - name: Verify replacement and retire legacy registry identities + shell: bash + run: | + set -euo pipefail + + legacy_paths=( + ".github/workflows/accounting-information-platform-hourly-review-repair.yml" + ".github/workflows/afipc-hourly-review-repair.yml" + ".github/workflows/bandscope-hourly-review-repair.yml" + ".github/workflows/clearfolio-hourly-review-repair.yml" + ".github/workflows/contextual-orchestrator-hourly-review-repair.yml" + ".github/workflows/disksage-hourly-review-repair.yml" + ".github/workflows/fast-mlsirm-hourly-review-repair.yml" + ".github/workflows/github-hourly-review-repair.yml" + ".github/workflows/governance-risk-compliance-hourly-review-repair.yml" + ".github/workflows/inkspan-hourly-review-repair.yml" + ".github/workflows/lineageweave-hourly-review-repair.yml" + ".github/workflows/metering-billing-platform-hourly-review-repair.yml" + ".github/workflows/nonnest2-hourly-review-repair.yml" + ".github/workflows/orgmetra-hourly-review-repair.yml" + ".github/workflows/originweave-hourly-review-repair.yml" + ".github/workflows/psychometrics-commons-hourly-review-repair.yml" + ".github/workflows/quarantine-sandbox-hourly-review-repair.yml" + ".github/workflows/semantic-data-portal-hourly-review-repair.yml" + ) + + workflow_inventory="$({ + gh api --paginate "/repos/${REPOSITORY}/actions/workflows?per_page=100" + } | jq -s '[.[].workflows[]]')" + + workflow_id_for_path() { + local path="$1" + local count + count="$(jq --arg path "$path" '[.[] | select(.path == $path)] | length' <<<"$workflow_inventory")" + if [[ "$count" != "1" ]]; then + echo "::error::Expected exactly one workflow registry identity for ${path}; found ${count}." >&2 + return 1 + fi + jq -r --arg path "$path" '.[] | select(.path == $path) | .id' <<<"$workflow_inventory" + } + + replacement_id="$(workflow_id_for_path "$REPLACEMENT_PATH")" + replacement_state="$(gh api "/repos/${REPOSITORY}/actions/workflows/${replacement_id}" --jq '.state')" + if [[ "$replacement_state" != "active" ]]; then + echo "::error::Replacement workflow ${REPLACEMENT_PATH} is not active; refusing retirement." >&2 + exit 1 + fi + + disable_and_verify() { + local path="$1" + local workflow_id + local state + workflow_id="$(workflow_id_for_path "$path")" + state="$(gh api "/repos/${REPOSITORY}/actions/workflows/${workflow_id}" --jq '.state')" + case "$state" in + active) + gh api --method PUT "/repos/${REPOSITORY}/actions/workflows/${workflow_id}/disable" >/dev/null + ;; + disabled_manually) + ;; + *) + echo "::error::Workflow ${path} has unexpected state ${state}; refusing partial retirement." >&2 + return 1 + ;; + esac + state="$(gh api "/repos/${REPOSITORY}/actions/workflows/${workflow_id}" --jq '.state')" + if [[ "$state" != "disabled_manually" ]]; then + echo "::error::Workflow ${path} did not reach disabled_manually; observed ${state}." >&2 + return 1 + fi + printf 'retired %s (%s)\n' "$path" "$workflow_id" + } + + for path in "${legacy_paths[@]}"; do + disable_and_verify "$path" + done + + # The one-shot migration identity is disabled only after every legacy + # caller has been verified disabled and the replacement remains active. + replacement_state="$(gh api "/repos/${REPOSITORY}/actions/workflows/${replacement_id}" --jq '.state')" + if [[ "$replacement_state" != "active" ]]; then + echo "::error::Replacement workflow changed state during retirement; preserving the migration identity." >&2 + exit 1 + fi + disable_and_verify "$SELF_PATH" diff --git a/.github/workflows/hourly-review-repair.yml b/.github/workflows/hourly-review-repair.yml new file mode 100644 index 0000000000..6ff60e4685 --- /dev/null +++ b/.github/workflows/hourly-review-repair.yml @@ -0,0 +1,249 @@ +name: Hourly Review Repair + +# Consolidates the 18 former thin per-repository callers +# (`-hourly-review-repair.yml`) into one file. GitHub Actions' own +# `on.schedule` list plus a `github.event.schedule` lookup replaces 18 +# near-identical copy-pasted files that differed only in `name:`, one +# `cron:` minute, the `concurrency.group` name and its rationale comment, +# and the `target_repository` / `base_branch` / `retry_hours` values passed +# to the shared reusable workflow. Consolidated per the org owner's request +# (2026-09-02, citing a "Governance Risk Compliance Hourly Review Repair" +# run): "이런 Workflow는 단일 파일로 통합하라" (consolidate workflows like +# this into a single file). See +# docs/doctoring/hourly-review-repair-single-file-consolidation.md and +# docs/adr/0021-hourly-review-repair-single-file-consolidation.md. +# +# `pr-review-fix-scheduler.yml`, the reusable engine this dispatches to, is +# unchanged and stays product-neutral (see AGENTS.md / CLAUDE.md: "Product +# hourly callers stay thin. Do not hard-code ... into +# pr-review-fix-scheduler.yml"). Only the trigger/dispatch layer above it is +# consolidated here. +# +# Each `on.schedule` entry below keeps its original file's distinct +# minute-of-hour offset and staggering-rationale comment verbatim, so +# cadence is byte-for-byte unchanged. `resolve-target` reads +# `github.event.schedule` -- the exact cron expression GitHub sets on the +# triggering event -- to look up which repository(ies) that minute serves. +# `dispatch-review-repair` then fans out over that lookup with a matrix, so +# concurrency stays isolated per repository exactly as it was when each +# repository had its own file and its own `concurrency.group`. +on: + schedule: + # Minute 2 avoids pg-llm-batch (1), kaefa (3), LineageWeave (4), + # codec-carver (5), life-os (6), Wardnet (7), mightyETL (8), + # psychometrics-commons (9), OriginWeave (10), naruon (11), + # DiagramWeave (12), pg-erd-cloud (13), mhtml-etl-gateway (14), + # html4tree (15), nonnest2 (16), orchestrator (17), newsdom-api (18), + # noema (19), github (21), Clearfolio (23), accounting-information-platform (27), + # Keyverse (29), Scopeweave (31), contextual-orchestrator (34), DiskSage (37), Appguardrail (41), + # governance-risk-compliance (43), fast-mlsirm (49), BandScope (53), + # Inkspan (56), orgmetra (58), and semantic-data-portal (59). + # -- aFIPC (formerly afipc-hourly-review-repair.yml) + - cron: "2 * * * *" + # -- LineageWeave (formerly lineageweave-hourly-review-repair.yml; the + # original file stated no staggering rationale for this minute) + - cron: "4 * * * *" + # Minute 9 avoids minute-zero pressure and the existing product callers. + # -- psychometrics-commons (formerly psychometrics-commons-hourly-review-repair.yml) + - cron: "9 * * * *" + # Minute 10 avoids pg-llm-batch (1), aFIPC (2), kaefa (3), LineageWeave (4), + # codec-carver (5), life-os (6), Wardnet (7), mightyETL (8), + # psychometrics-commons (9), naruon (11), pg-erd-cloud (13), + # orchestrator (17), noema (19), Clearfolio (23), Keyverse (29), + # Scopeweave (31), contextual-orchestrator (34), DiskSage (37), Appguardrail (41), newsdom-api (43), + # fast-mlsirm (49), BandScope (53), Inkspan (56), and + # semantic-data-portal (59). + # -- OriginWeave (formerly originweave-hourly-review-repair.yml) + - cron: "10 * * * *" + # Minute 14 avoids existing product callers while keeping one bounded + # review-repair heartbeat per hour for the sandbox runtime. + # -- quarantine-sandbox (formerly quarantine-sandbox-hourly-review-repair.yml) + - cron: "14 * * * *" + # Minute 16 avoids pg-llm-batch (1), aFIPC (2), kaefa (3), LineageWeave (4), + # codec-carver (5), life-os (6), Wardnet (7), mightyETL (8), + # psychometrics-commons (9), OriginWeave (10), naruon (11), + # DiagramWeave (12), pg-erd-cloud (13), mhtml-etl-gateway (14), + # html4tree (15), orchestrator (17), noema (19), Clearfolio (23), + # Keyverse (29), Scopeweave (31), contextual-orchestrator (34), DiskSage (37), Appguardrail (41), + # newsdom-api (43), fast-mlsirm (49), BandScope (53), Inkspan (56), + # and semantic-data-portal (59). + # -- nonnest2 (formerly nonnest2-hourly-review-repair.yml) + - cron: "16 * * * *" + # Keep the control-plane queue moving without colliding with minute-zero jobs. + # -- ContextualWisdomLab/.github self-caller (formerly github-hourly-review-repair.yml) + - cron: "21 * * * *" + # Offset the heartbeat from minute zero to reduce shared-runner congestion. + # -- Clearfolio (formerly clearfolio-hourly-review-repair.yml) + - cron: "23 * * * *" + # Minute 27 avoids existing organization product callers and minute-zero pressure. + # -- accounting-information-platform (formerly accounting-information-platform-hourly-review-repair.yml) + - cron: "27 * * * *" + # Minute 34 avoids the minute-zero runner surge and every existing sibling + # heartbeat (2, 7, 10, 14, 16, 17 central scheduler, 21, 23, 27, 31, + # 37, 41, 43, 49, 53, 58, 59). + # -- contextual-orchestrator (formerly contextual-orchestrator-hourly-review-repair.yml) + - cron: "34 * * * *" + # Minute 37 avoids the minute-zero runner surge and the Clearfolio heartbeat. + # -- DiskSage (formerly disksage-hourly-review-repair.yml) + - cron: "37 * * * *" + # Minute 43 avoids minute-zero pressure and the existing product callers. + # -- governance-risk-compliance (formerly governance-risk-compliance-hourly-review-repair.yml) + - cron: "43 * * * *" + # Minute 49 avoids minute-zero pressure and the existing product callers. + # Serves TWO repositories, fast-mlsirm and metering-billing-platform: their + # original standalone files had both independently chosen minute 49, an + # unnoticed collision (see + # docs/doctoring/hourly-review-repair-single-file-consolidation.md). + # Consolidating them onto one shared trigger, fanned out by the matrix + # below, makes that sharing explicit instead of relying on two files + # coincidentally firing side by side; each repository still gets exactly + # one dispatch attempt at :49 of every hour, matching original behavior. + # -- fast-mlsirm + metering-billing-platform (formerly + # fast-mlsirm-hourly-review-repair.yml and + # metering-billing-platform-hourly-review-repair.yml) + - cron: "49 * * * *" + # Minute 53 avoids established product-specific heartbeat minutes. + # -- BandScope (formerly bandscope-hourly-review-repair.yml) + - cron: "53 * * * *" + # Minute 56 avoids every existing hourly heartbeat minute and the + # half-hourly merge scheduler ticks. + # -- Inkspan (formerly inkspan-hourly-review-repair.yml) + - cron: "56 * * * *" + # Minute 58 avoids the existing product callers and leaves room for the + # central merge scheduler to consume the queue. + # -- Orgmetra (formerly orgmetra-hourly-review-repair.yml) + - cron: "58 * * * *" + # Minute 59 is reserved for semantic-data-portal in the organization + # caller ledger and is unique among product heartbeats. GitHub may delay + # scheduled runs, so this is a heartbeat rather than a minute-zero surge + # avoidance guarantee. + # -- semantic-data-portal (formerly semantic-data-portal-hourly-review-repair.yml) + - cron: "59 * * * *" + +permissions: + contents: read + +jobs: + resolve-target: + name: Resolve target(s) for ${{ github.event.schedule }} + runs-on: ubuntu-latest + outputs: + targets: ${{ steps.lookup.outputs.targets }} + steps: + - name: Look up repository parameters for this schedule + id: lookup + env: + SCHEDULE: ${{ github.event.schedule }} + run: | + set -euo pipefail + case "$SCHEDULE" in + "2 * * * *") + # A later heartbeat must not cancel an in-flight FIPC or calibration RCA. + TARGETS='[{"name":"afipc","target_repository":"ContextualWisdomLab/aFIPC","base_branch":"master","retry_hours":"2","concurrency_group":"afipc-hourly-review-repair"}]' + ;; + "4 * * * *") + TARGETS='[{"name":"lineageweave","target_repository":"ContextualWisdomLab/LineageWeave","base_branch":"*","retry_hours":"2","concurrency_group":"lineageweave-hourly-review-repair"}]' + ;; + "9 * * * *") + # Preserve bounded RCA when a later hourly heartbeat arrives. + TARGETS='[{"name":"psychometrics-commons","target_repository":"ContextualWisdomLab/psychometrics-commons","base_branch":"main","retry_hours":"2","concurrency_group":"psychometrics-commons-hourly-review-repair"}]' + ;; + "10 * * * *") + # A later heartbeat must not cancel an in-flight agent-browser RCA. + TARGETS='[{"name":"originweave","target_repository":"ContextualWisdomLab/OriginWeave","base_branch":"main","retry_hours":"2","concurrency_group":"originweave-hourly-review-repair"}]' + ;; + "14 * * * *") + # A later heartbeat must not cancel an in-flight security RCA. + TARGETS='[{"name":"quarantine-sandbox","target_repository":"ContextualWisdomLab/quarantine-sandbox-runtime","base_branch":"develop","retry_hours":"2","concurrency_group":"quarantine-sandbox-hourly-review-repair"}]' + ;; + "16 * * * *") + # A later heartbeat must not cancel an in-flight Vuong or fit RCA. + TARGETS='[{"name":"nonnest2","target_repository":"ContextualWisdomLab/nonnest2","base_branch":"master","retry_hours":"2","concurrency_group":"nonnest2-hourly-review-repair"}]' + ;; + "21 * * * *") + TARGETS='[{"name":"github","target_repository":"ContextualWisdomLab/.github","base_branch":"main","retry_hours":"1","concurrency_group":"github-hourly-review-repair"}]' + ;; + "23 * * * *") + TARGETS='[{"name":"clearfolio","target_repository":"ContextualWisdomLab/clearfolio","base_branch":"main","retry_hours":"1","concurrency_group":"clearfolio-hourly-review-repair"}]' + ;; + "27 * * * *") + # Central OpenCode, Noema, and exact-head accounting checks can exceed one hour. + TARGETS='[{"name":"accounting-information-platform","target_repository":"ContextualWisdomLab/accounting-information-platform","base_branch":"develop","retry_hours":"2","concurrency_group":"accounting-information-platform-hourly-review-repair"}]' + ;; + "34 * * * *") + # The queue scan is bounded and the worker has its own exact-head lease. Do not + # discard an in-flight RCA merely because the next hourly heartbeat arrives. + TARGETS='[{"name":"contextual-orchestrator","target_repository":"ContextualWisdomLab/contextual-orchestrator","base_branch":"main","retry_hours":"2","concurrency_group":"contextual-orchestrator-hourly-review-repair"}]' + ;; + "37 * * * *") + # The queue scan is bounded and the worker has its own exact-head lease. Do not + # discard an in-flight RCA merely because the next hourly heartbeat arrives. + TARGETS='[{"name":"disksage","target_repository":"ContextualWisdomLab/disksage","base_branch":"main","retry_hours":"2","concurrency_group":"disksage-hourly-review-repair"}]' + ;; + "43 * * * *") + # Preserve an in-flight exact-head RCA when the next heartbeat arrives. + TARGETS='[{"name":"governance-risk-compliance","target_repository":"ContextualWisdomLab/governance-risk-compliance","base_branch":"develop","retry_hours":"2","concurrency_group":"governance-risk-compliance-hourly-review-repair"}]' + ;; + "49 * * * *") + # fast-mlsirm: preserve bounded RCA when a later hourly heartbeat arrives. + # metering-billing-platform: preserve bounded RCA when a later hourly heartbeat arrives. + TARGETS='[{"name":"fast-mlsirm","target_repository":"ContextualWisdomLab/fast-mlsirm","base_branch":"main","retry_hours":"2","concurrency_group":"fast-mlsirm-hourly-review-repair"},{"name":"metering-billing-platform","target_repository":"ContextualWisdomLab/metering-billing-platform","base_branch":"develop","retry_hours":"1","concurrency_group":"metering-billing-platform-hourly-review-repair"}]' + ;; + "53 * * * *") + # Preserve a legitimate long-running root-cause analysis across heartbeats. + TARGETS='[{"name":"bandscope","target_repository":"ContextualWisdomLab/bandscope","base_branch":"develop","retry_hours":"2","concurrency_group":"bandscope-hourly-review-repair"}]' + ;; + "56 * * * *") + # The queue scan is bounded and the worker has its own exact-head lease. Do not + # discard an in-flight RCA merely because the next hourly heartbeat arrives. + TARGETS='[{"name":"inkspan","target_repository":"ContextualWisdomLab/inkspan","base_branch":"main","retry_hours":"2","concurrency_group":"inkspan-hourly-review-repair"}]' + ;; + "58 * * * *") + # Preserve an in-flight exact-head RCA when the next heartbeat arrives. + TARGETS='[{"name":"orgmetra","target_repository":"ContextualWisdomLab/Orgmetra","base_branch":"develop","retry_hours":"2","concurrency_group":"orgmetra-hourly-review-repair"}]' + ;; + "59 * * * *") + # The queue scan is bounded and the worker has its own exact-head lease. Do not + # discard an in-flight RCA merely because the next hourly heartbeat arrives. + TARGETS='[{"name":"semantic-data-portal","target_repository":"ContextualWisdomLab/semantic-data-portal","base_branch":"main","retry_hours":"2","concurrency_group":"semantic-data-portal-hourly-review-repair"}]' + ;; + *) + echo "::error::Unrecognized schedule '$SCHEDULE'; no target repository is configured for it." >&2 + exit 1 + ;; + esac + echo "targets=$TARGETS" >> "$GITHUB_OUTPUT" + + dispatch-review-repair: + name: dispatch-review-repair (${{ matrix.name }}) + needs: resolve-target + strategy: + fail-fast: false + matrix: + include: ${{ fromJson(needs.resolve-target.outputs.targets) }} + permissions: + contents: read + id-token: write + # Each repository keeps the independent, non-cancelling concurrency group + # its own former dedicated file used (e.g. `afipc-hourly-review-repair`), + # so all 18 (17 distinct-minute) schedules still run independently of + # each other and a later heartbeat never cancels this repository's + # in-flight RCA. `matrix.*` is available to a job-level `concurrency:` + # expression because the matrix is resolved before the job starts. + concurrency: + group: ${{ matrix.concurrency_group }} + cancel-in-progress: false + uses: ./.github/workflows/pr-review-fix-scheduler.yml + with: + target_repository: ${{ matrix.target_repository }} + base_branch: ${{ matrix.base_branch }} + max_prs: "50" + max_dispatches: "1" + retry_hours: ${{ matrix.retry_hours }} + # Explicit for every target: the reusable workflow's own default is + # already `true`, so this is behaviorally identical to the 17 original + # files that omitted the key and the 1 (github) that set it explicitly. + resolve_unreviewed_conflicts: true + secrets: + PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} + OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/inkspan-hourly-review-repair.yml b/.github/workflows/inkspan-hourly-review-repair.yml deleted file mode 100644 index 835369fed2..0000000000 --- a/.github/workflows/inkspan-hourly-review-repair.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Inkspan Hourly Review Repair - -on: - schedule: - # Minute 56 avoids every existing hourly heartbeat minute and the - # half-hourly merge scheduler ticks. - - cron: "56 * * * *" - -concurrency: - group: inkspan-hourly-review-repair - # The queue scan is bounded and the worker has its own exact-head lease. Do not - # discard an in-flight RCA merely because the next hourly heartbeat arrives. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - uses: ./.github/workflows/pr-review-fix-scheduler.yml - permissions: - contents: read - id-token: write - with: - target_repository: ContextualWisdomLab/inkspan - base_branch: main - max_prs: "50" - max_dispatches: "1" - # Central OpenCode/NVIDIA NIM work can legitimately approach two hours. - # A two-hour same-head floor avoids duplicate writers without freezing the - # next eligible PR or confusing provider latency with a source-code defect. - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/lineageweave-hourly-review-repair.yml b/.github/workflows/lineageweave-hourly-review-repair.yml deleted file mode 100644 index 633957ac81..0000000000 --- a/.github/workflows/lineageweave-hourly-review-repair.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: LineageWeave Hourly Review Repair - -on: - schedule: - - cron: "4 * * * *" - -concurrency: - group: lineageweave-hourly-review-repair - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - uses: ./.github/workflows/pr-review-fix-scheduler.yml - permissions: - contents: read - id-token: write - with: - target_repository: ContextualWisdomLab/LineageWeave - base_branch: "*" - max_prs: "50" - max_dispatches: "1" - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/metering-billing-platform-hourly-review-repair.yml b/.github/workflows/metering-billing-platform-hourly-review-repair.yml deleted file mode 100644 index 1521246940..0000000000 --- a/.github/workflows/metering-billing-platform-hourly-review-repair.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: metering-billing-platform Hourly Review Repair - -on: - schedule: - # Minute 49 avoids minute-zero pressure and the existing product callers. - - cron: "49 * * * *" - -concurrency: - group: metering-billing-platform-hourly-review-repair - # Preserve bounded RCA when a later hourly heartbeat arrives. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/metering-billing-platform - base_branch: develop - max_prs: "50" - max_dispatches: "1" - # Central OpenCode/NVIDIA NIM review and Foundation CI (PostgreSQL 18 - # integration suite) can approach one hour on this repository. - retry_hours: "1" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/nonnest2-hourly-review-repair.yml b/.github/workflows/nonnest2-hourly-review-repair.yml deleted file mode 100644 index 2ed0d5fdf0..0000000000 --- a/.github/workflows/nonnest2-hourly-review-repair.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: nonnest2 Hourly Review Repair - -on: - schedule: - # Minute 16 avoids pg-llm-batch (1), aFIPC (2), kaefa (3), LineageWeave (4), - # codec-carver (5), life-os (6), Wardnet (7), mightyETL (8), - # psychometrics-commons (9), OriginWeave (10), naruon (11), - # DiagramWeave (12), pg-erd-cloud (13), mhtml-etl-gateway (14), - # html4tree (15), orchestrator (17), noema (19), Clearfolio (23), - # Keyverse (29), Scopeweave (31), contextual-orchestrator (34), DiskSage (37), Appguardrail (41), - # newsdom-api (43), fast-mlsirm (49), BandScope (53), Inkspan (56), - # and semantic-data-portal (59). - - cron: "16 * * * *" - -concurrency: - group: nonnest2-hourly-review-repair - # A later heartbeat must not cancel an in-flight Vuong or fit RCA. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/nonnest2 - base_branch: master - max_prs: "50" - max_dispatches: "1" - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/orgmetra-hourly-review-repair.yml b/.github/workflows/orgmetra-hourly-review-repair.yml deleted file mode 100644 index 0801a8e372..0000000000 --- a/.github/workflows/orgmetra-hourly-review-repair.yml +++ /dev/null @@ -1,33 +0,0 @@ -name: Orgmetra Hourly Review Repair - -on: - schedule: - # Minute 58 avoids the existing product callers and leaves room for the - # central merge scheduler to consume the queue. - - cron: "58 * * * *" - -concurrency: - group: orgmetra-hourly-review-repair - # Preserve an in-flight exact-head RCA when the next heartbeat arrives. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/Orgmetra - base_branch: develop - max_prs: "50" - max_dispatches: "1" - # Hosted review, security, PostgreSQL, Rust, and browser checks can - # legitimately outlive one heartbeat. - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/originweave-hourly-review-repair.yml b/.github/workflows/originweave-hourly-review-repair.yml deleted file mode 100644 index 7473afdb15..0000000000 --- a/.github/workflows/originweave-hourly-review-repair.yml +++ /dev/null @@ -1,36 +0,0 @@ -name: OriginWeave Hourly Review Repair - -on: - schedule: - # Minute 10 avoids pg-llm-batch (1), aFIPC (2), kaefa (3), LineageWeave (4), - # codec-carver (5), life-os (6), Wardnet (7), mightyETL (8), - # psychometrics-commons (9), naruon (11), pg-erd-cloud (13), - # orchestrator (17), noema (19), Clearfolio (23), Keyverse (29), - # Scopeweave (31), contextual-orchestrator (34), DiskSage (37), Appguardrail (41), newsdom-api (43), - # fast-mlsirm (49), BandScope (53), Inkspan (56), and - # semantic-data-portal (59). - - cron: "10 * * * *" - -concurrency: - group: originweave-hourly-review-repair - # A later heartbeat must not cancel an in-flight agent-browser RCA. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/OriginWeave - base_branch: main - max_prs: "50" - max_dispatches: "1" - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/psychometrics-commons-hourly-review-repair.yml b/.github/workflows/psychometrics-commons-hourly-review-repair.yml deleted file mode 100644 index 3f253f1d1e..0000000000 --- a/.github/workflows/psychometrics-commons-hourly-review-repair.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: psychometrics-commons Hourly Review Repair - -on: - schedule: - # Minute 9 avoids minute-zero pressure and the existing product callers. - - cron: "9 * * * *" - -concurrency: - group: psychometrics-commons-hourly-review-repair - # Preserve bounded RCA when a later hourly heartbeat arrives. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/psychometrics-commons - base_branch: main - max_prs: "50" - max_dispatches: "1" - # Central OpenCode/NVIDIA NIM review and psychometric CI can approach two hours. - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/quarantine-sandbox-hourly-review-repair.yml b/.github/workflows/quarantine-sandbox-hourly-review-repair.yml deleted file mode 100644 index 2649ee3e6d..0000000000 --- a/.github/workflows/quarantine-sandbox-hourly-review-repair.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: Quarantine Sandbox Hourly Review Repair - -on: - schedule: - # Minute 14 avoids existing product callers while keeping one bounded - # review-repair heartbeat per hour for the sandbox runtime. - - cron: "14 * * * *" - -concurrency: - group: quarantine-sandbox-hourly-review-repair - # A later heartbeat must not cancel an in-flight security RCA. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/quarantine-sandbox-runtime - base_branch: develop - max_prs: "50" - max_dispatches: "1" - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/.github/workflows/semantic-data-portal-hourly-review-repair.yml b/.github/workflows/semantic-data-portal-hourly-review-repair.yml deleted file mode 100644 index c779793827..0000000000 --- a/.github/workflows/semantic-data-portal-hourly-review-repair.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: Semantic Data Portal Hourly Review Repair - -on: - schedule: - # Minute 59 is reserved for semantic-data-portal in the organization - # caller ledger and is unique among product heartbeats. GitHub may delay - # scheduled runs, so this is a heartbeat rather than a minute-zero surge - # avoidance guarantee. - - cron: "59 * * * *" - -concurrency: - group: semantic-data-portal-hourly-review-repair - # The queue scan is bounded and the worker has its own exact-head lease. Do not - # discard an in-flight RCA merely because the next hourly heartbeat arrives. - cancel-in-progress: false - -permissions: - contents: read - -jobs: - dispatch-review-repair: - permissions: - contents: read - id-token: write - uses: ./.github/workflows/pr-review-fix-scheduler.yml - with: - target_repository: ContextualWisdomLab/semantic-data-portal - base_branch: main - max_prs: "50" - max_dispatches: "1" - # Central OpenCode/NVIDIA NIM work can legitimately approach two hours. - # A two-hour same-head floor avoids duplicate writers without freezing the - # next eligible PR or confusing provider latency with a source-code defect. - retry_hours: "2" - secrets: - PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }} - OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md index f53342aadb..cf8df236be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -9,8 +9,7 @@ commit and exposed without running build hooks; a lone `--require-hashes` directive is not trust evidence. See [`docs/doctoring/opencode-exact-vcs-dependency-evidence.md`](docs/doctoring/opencode-exact-vcs-dependency-evidence.md). Conflict-scope roots fail closed when the immediate parent directory is a symbolic link. -OriginWeave hourly NVIDIA NIM repair is a thin caller at minute 10. See [`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md). -nonnest2 hourly NVIDIA NIM repair is a thin caller at minute 16. See [`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md). +All 18 product hourly review-repair callers (OriginWeave at minute 10, nonnest2 at minute 16, and 16 others) are one file, [`.github/workflows/hourly-review-repair.yml`](.github/workflows/hourly-review-repair.yml), a `github.event.schedule` lookup table rather than 18 near-copy-pasted files. See [`docs/doctoring/hourly-review-repair-single-file-consolidation.md`](docs/doctoring/hourly-review-repair-single-file-consolidation.md); the per-repository doctoring records (e.g. [`docs/doctoring/originweave-hourly-review-caller.md`](docs/doctoring/originweave-hourly-review-caller.md), [`docs/doctoring/nonnest2-hourly-review-caller.md`](docs/doctoring/nonnest2-hourly-review-caller.md)) remain as historical background per repository. Organization edge runtimes use Cloudflare Pingora. Do not add or preserve active Nginx containers, packages, commands, service/config files, or Kubernetes Nginx ingress annotations/classes. Read [`docs/policies/PINGORA_EDGE_POLICY.md`](docs/policies/PINGORA_EDGE_POLICY.md) and ADR-0019 before changing HTTP edge, static-serving, ingress, TLS, or proxy deployment behavior. Semgrep hosted scans bind one job-level `SEMGREP_IMAGE` digest for log evidence, manifest inspection, and `docker run`. See [`docs/doctoring/semgrep-image-digest-single-source.md`](docs/doctoring/semgrep-image-digest-single-source.md). diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 0c248e43af..e12f33542d 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -92,26 +92,22 @@ non-cancellable so a newer heartbeat cannot abandon partially updated fleet state. See ADR-0020 and the operational baseline for the authority and live-verification contract. -## OriginWeave hourly caller - -`originweave-hourly-review-repair.yml` is a thin, read-only caller at minute -10. It names `ContextualWisdomLab/OriginWeave` and protected `main`, maps -only established scheduler credentials, and grants job-scoped -`id-token: write`. The reusable engine stays product-neutral. - -## nonnest2 hourly caller - -`nonnest2-hourly-review-repair.yml` is a thin, read-only caller at minute -16. It names `ContextualWisdomLab/nonnest2` and protected `master`, maps -only established scheduler credentials, and grants job-scoped -`id-token: write`. The reusable engine stays product-neutral. - -## aFIPC hourly caller - -`afipc-hourly-review-repair.yml` is a thin, read-only caller at minute -2. It names `ContextualWisdomLab/aFIPC` and protected `master`, maps -only established scheduler credentials, and grants job-scoped -`id-token: write`. The reusable engine stays product-neutral. +## Hourly product callers + +`hourly-review-repair.yml` is one thin, read-only caller for all 18 product +repositories (formerly 18 near-identical files, one per repository; see +ADR-0021 and +`docs/doctoring/hourly-review-repair-single-file-consolidation.md`). Its +`on.schedule` list carries all 17 distinct cron minutes; a `resolve-target` +job reads `github.event.schedule` to look up which repository (or, for the +one shared minute, repositories) fired, and a matrix `dispatch-review-repair` +job calls the reusable scheduler once per resolved target with job-scoped +`id-token: write` and each repository's own independent, +non-cancelling `concurrency.group`. OriginWeave (minute 10, protected +`main`), nonnest2 (minute 16, protected `master`), and aFIPC (minute 2, +protected `master`) are three of the 18 resolved targets; every target maps +only established scheduler credentials. The reusable engine stays +product-neutral. ## Hourly contextual-orchestrator repair gate diff --git a/CHANGELOG.md b/CHANGELOG.md index 552966c233..66145dc939 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,31 @@ this file. The format follows Keep a Changelog, and versioned releases follow Semantic Versioning where the repository publishes a release. ## [Unreleased] +- **Consolidate the 18 per-repository hourly review-repair caller workflows into one file.** + At the repository owner's request ("이런 Workflow는 단일 파일로 통합하라"), replaced + `accounting-information-platform-`, `afipc-`, `bandscope-`, `clearfolio-`, + `contextual-orchestrator-`, `disksage-`, `fast-mlsirm-`, `github-`, + `governance-risk-compliance-`, `inkspan-`, `lineageweave-`, + `metering-billing-platform-`, `nonnest2-`, `orgmetra-`, `originweave-`, + `psychometrics-commons-`, `quarantine-sandbox-`, and + `semantic-data-portal-hourly-review-repair.yml` with one file, + `.github/workflows/hourly-review-repair.yml`: a single `on.schedule` list (all 17 + distinct minutes, staggering comments preserved) plus a `github.event.schedule` + lookup table that resolves each minute's repository, base branch, and retry floor, + fanned out through a `strategy.matrix` job that keeps every repository's own + independent, non-cancelling `concurrency.group`. `pr-review-fix-scheduler.yml`, + the reusable engine every caller dispatches to, is unchanged. Auditing the 18 + originals for this consolidation found `fast-mlsirm` and `metering-billing-platform` + had independently collided on the same minute (49) and that + `clearfolio-hourly-review-repair.yml` was the only one of the 18 missing its + job-level `id-token: write` grant; both are called out and the latter closed + uniformly across the consolidated matrix. 13 dedicated per-repository test files + are replaced by `tests/test_hourly_review_repair_callers.py`, which extracts and + executes the lookup script for every schedule against the exact parameters the + deleted files used; four other test files that used a since-deleted caller as a + representative example were updated in place. See + `docs/doctoring/hourly-review-repair-single-file-consolidation.md` and + ADR-0021. - **Fix stale test assertions and dead-code gaps left by `#1654`, `#1656`, and `#1658`.** Reproduced all failures on a fresh unmodified `main` clone before attributing blame. `#1654` (introducing `scripts/ci/current_head_run_coalescer.py` and hardening several diff --git a/docs/adr/0021-hourly-review-repair-single-file-consolidation.md b/docs/adr/0021-hourly-review-repair-single-file-consolidation.md new file mode 100644 index 0000000000..bf1df41942 --- /dev/null +++ b/docs/adr/0021-hourly-review-repair-single-file-consolidation.md @@ -0,0 +1,148 @@ +# ADR-0021: Consolidate the 18 hourly review-repair callers into one file + +- **Status:** Accepted +- **Date:** 2026-09-02 +- **Scope:** ContextualWisdomLab/.github `.github/workflows/` hourly review-repair trigger/dispatch layer + +## Context + +18 near-identical files (`-hourly-review-repair.yml`) each existed +solely to give one product repository its own hourly `schedule` trigger and +call the shared, product-neutral `pr-review-fix-scheduler.yml` with that +repository's `target_repository` / `base_branch` / `retry_hours`. Every file +differed from every other one only in `name:`, one `cron:` minute (and a +staggering-rationale comment), the `concurrency.group` name (and a +cancellation-rationale comment), and those three `with:` values; +`max_prs`/`max_dispatches` were uniform. Adding, auditing, or re-staggering +a caller required editing (or copy-pasting) one of 18 files. + +The repository owner requested consolidating this pattern into a single +file, citing hosted run +`ContextualWisdomLab/.github/actions/runs/33524178483/job/99910668839` (a +"Governance Risk Compliance Hourly Review Repair" run) as an example of the +duplication, and specifically identifying that GitHub Actions' own native +syntax already supports this without a new abstraction layer. +`docs/doctoring/hourly-review-repair-single-file-consolidation.md` records +the full before/after mapping, verification, and every non-uniform field +found while auditing. + +## Decision + +1. One file, `.github/workflows/hourly-review-repair.yml`, replaces all 18. + Its `on.schedule` list carries all 17 distinct cron minutes the 18 files + used, each keeping its original file's staggering-rationale comment. +2. A `resolve-target` job reads `github.event.schedule` in a `run:` step and + looks it up in a `case`/`esac` table -- a small, readable lookup table, + not a new configuration format -- producing a JSON array of + `{name, target_repository, base_branch, retry_hours, concurrency_group}` + via `GITHUB_OUTPUT`. Every deleted file's concurrency-cancellation + rationale comment survives as a comment on its `case` branch. +3. A `dispatch-review-repair` job (`needs: resolve-target`) fans out over + that array with `strategy.matrix.include` and calls + `pr-review-fix-scheduler.yml` once per resolved target, forwarding the + two secrets exactly as the 18 originals did. +4. `concurrency.group` is `${{ matrix.concurrency_group }}` -- each + repository's own former group name, reused verbatim -- so the 18 (17 + distinct-minute) schedules keep the same independent, non-cancelling + isolation the 18 separate files gave them. A job-level `concurrency:` + expression may reference `matrix.*` because the matrix is resolved + before the job starts. +5. `fast-mlsirm` and `metering-billing-platform` had each independently + chosen `cron: "49 * * * *"` in their original files -- an unnoticed + collision, not a deliberate shared heartbeat. Rather than rely on + GitHub's undocumented behavior for two textually-identical `on.schedule` + entries in one file, the consolidated file has exactly one `"49 * * * *"` + entry whose lookup resolves to a two-element array; the matrix dispatches + both. Each repository still gets exactly one dispatch attempt at minute + 49 of every hour. +6. `resolve_unreviewed_conflicts: true` is passed explicitly and uniformly + to every target. The reusable workflow's own input already defaults to + `true`, so this is behaviorally identical to the prior state (17 files + omitted it, one set it explicitly) and avoids needing to conditionally + omit a `with:` key per matrix element, which reusable-workflow calls do + not support. +7. Job-level `permissions:` (`contents: read`, `id-token: write`) is granted + uniformly to every target. `clearfolio-hourly-review-repair.yml` was the + sole one of the 18 originals that omitted this override, so it alone + never actually granted the reusable scheduler `id-token: write` -- a + latent gap closed by this uniform grant. `pr-review-fix-scheduler.yml`'s + own `permissions:` block is unchanged; this widens only one caller's own + job permissions to match its 17 siblings. +8. `pr-review-fix-scheduler.yml` is not modified. It remains product-neutral + per this repository's existing convention (AGENTS.md / CLAUDE.md: + "Product hourly callers stay thin. Do not hard-code ... into + pr-review-fix-scheduler.yml"); only the trigger/dispatch layer above it + is consolidated. +9. `.github/workflows/hourly-nvidia-nim-review-repair.yml`'s path-filter + lists (a separate, pre-existing focused quality-gate workflow) are + updated to track the one consolidated file and its one consolidated test + file instead of the 14 individual entries they previously tracked. +10. 13 dedicated per-repository test files + (`tests/test__hourly_review_caller.py`), each pinning only that + one repository's now-deleted caller file, are replaced by one file, + `tests/test_hourly_review_repair_callers.py`, which asserts the full + 18-repository mapping by extracting and executing the `resolve-target` + lookup script for every schedule. Test files with additional, + non-caller-shape logic (`tests/test_github_hourly_conflict_repair.py`, + `tests/test_hourly_scheduler_runtime_budget.py`, + `tests/test_pr_review_fix_hourly_contract.py`, + `tests/test_pr_review_autofix_nvidia_nim_contract.py`) are kept and + updated in place rather than deleted. +11. The 14 per-repository doctoring records for the individual callers are + kept as historical decision records rather than merged, since their + prose (unlike the deleted YAML) was never byte-for-byte duplicated + across repositories; only the one doc that named its own deleted + filename (`docs/doctoring/clearfolio-hourly-review-caller.md`) is + corrected to point at the consolidated file. + +## Consequences + +- Adding, removing, or re-staggering a product's hourly heartbeat is a + one-file, one-`case`-branch edit instead of a new copy-pasted file. +- The full minute-to-repository mapping, and every staggering/cancellation + rationale, is visible in one place rather than requiring 18 separate file + reads to audit for a collision -- which is how the pre-existing minute-49 + collision between fast-mlsirm and metering-billing-platform surfaced + during this consolidation's audit. +- Concurrency isolation depends on `matrix.*` being available to job-level + `concurrency:` expressions, a documented but less commonly exercised + GitHub Actions capability; `tests/test_hourly_review_repair_callers.py` + and `actionlint` both verify the consolidated file directly rather than + assuming this. +- The consolidated file is longer (comments included) than any single one + of the 18 originals, trading per-repository file separation for one file + whose structure (schedule list, then lookup table, then matrix dispatch) + is uniform and mechanically auditable. +- Clearfolio's job-level OIDC permission gap is closed as a side effect of + uniform matrix permissions; this is a narrow, intentional, and + behaviorally inert widening (Clearfolio's forwarded PAT secrets already + kept its mutation-credential check passing), not an unreviewed permission + escalation. + +## Rejected alternatives + +- **Duplicate `cron: "49 * * * *"` twice in `on.schedule` and let each + physical trigger resolve to its one repository.** Rejected because + GitHub's behavior for two textually-identical schedule entries in one + workflow (one physical run, or two) is not documented; relying on it + would make dispatch correctness depend on unspecified platform behavior + instead of one entry with a two-element lookup result. +- **Silently re-stagger `metering-billing-platform` off minute 49 during + this consolidation.** Rejected as out of scope for a pure consolidation: + changing effective dispatch timing is a separate decision from replacing + 18 files with one, and is called out explicitly instead, for the owner or + a follow-up change to decide. +- **One shared `concurrency.group` for the whole consolidated workflow.** + Rejected because the 18 originals were deliberately independent (a + Governance Risk Compliance heartbeat must not queue behind, or cancel, an + unrelated Clearfolio run); a dynamic per-target group was required to + preserve that. +- **Merge the 14 per-repository doctoring records into one document.** + Rejected because their content is repository-specific decision history, + not duplicated boilerplate; merging would blur which repository a given + security or activation rationale applies to. +- **Delete the 13 dedicated per-repository test files outright without a + replacement.** Rejected: their assertions (exact cron, target repository, + base branch, retry floor, permissions, secrets) are real correctness + properties for production scheduling infrastructure and are preserved, + consolidated into one parametrized module instead of dropped. diff --git a/docs/automation/hourly-review-repair.md b/docs/automation/hourly-review-repair.md index 8994a0fc10..c7ad21bd1d 100644 --- a/docs/automation/hourly-review-repair.md +++ b/docs/automation/hourly-review-repair.md @@ -3,10 +3,14 @@ The central automation separates **product cadence** from the **reusable repair engine**. -- `clearfolio-hourly-review-repair.yml` owns Clearfolio's heartbeat at minute 23 - of every hour. -- `orgmetra-hourly-review-repair.yml` owns Orgmetra's heartbeat at minute 58 - of every hour against protected `develop`. +- `hourly-review-repair.yml` owns every product's heartbeat, including + Clearfolio's at minute 23 and Orgmetra's at minute 58 (against protected + `develop`), as one file: an `on.schedule` list plus a lookup table keyed on + `github.event.schedule` that resolves the repository, base branch, and + retry floor for whichever minute fired. It replaced 18 near-identical + per-repository caller files (`clearfolio-hourly-review-repair.yml`, + `orgmetra-hourly-review-repair.yml`, and 16 others); see + [`docs/doctoring/hourly-review-repair-single-file-consolidation.md`](../doctoring/hourly-review-repair-single-file-consolidation.md). - `pr-review-fix-scheduler.yml` is the reusable, product-neutral scheduler module. It has no product-specific timer and can be called by naruon, contextual-orchestrator, Inkspan, or another CWL service with an explicit diff --git a/docs/doctoring/clearfolio-hourly-review-caller.md b/docs/doctoring/clearfolio-hourly-review-caller.md index 239fdbd3ee..7fdcf405f2 100644 --- a/docs/doctoring/clearfolio-hourly-review-caller.md +++ b/docs/doctoring/clearfolio-hourly-review-caller.md @@ -3,10 +3,14 @@ ## Decision Clearfolio's one-hour review → repair → revalidation support heartbeat is owned -by a dedicated central caller workflow, -`.github/workflows/clearfolio-hourly-review-repair.yml`. The product-neutral -engine remains `.github/workflows/pr-review-fix-scheduler.yml` and contains no -scheduled trigger or Clearfolio repository literal. +by the central caller workflow `.github/workflows/hourly-review-repair.yml` +(minute 23 of every hour; formerly its own dedicated file, +`clearfolio-hourly-review-repair.yml`, before the 18-file single-file +consolidation recorded in +[`docs/doctoring/hourly-review-repair-single-file-consolidation.md`](hourly-review-repair-single-file-consolidation.md)). +The product-neutral engine remains +`.github/workflows/pr-review-fix-scheduler.yml` and contains no scheduled +trigger or Clearfolio repository literal. This split is an architecture decision rather than a naming preference. A scheduled workflow executes in the repository that contains it. Letting a @@ -18,7 +22,7 @@ contextual-orchestrator, and other CWL services. ## Product caller -The Clearfolio caller runs at minute 23 of every hour and invokes the local +The Clearfolio matrix row runs at minute 23 of every hour and invokes the local reusable workflow with explicit, reviewable values: ```yaml @@ -29,18 +33,20 @@ max_dispatches: "1" retry_hours: "1" ``` -The caller and reusable engine both use `cancel-in-progress: true`. This keeps -queue inspection single-flight at the product and engine boundaries. At most one +The consolidated caller preserves Clearfolio's independent concurrency group +but deliberately uses `cancel-in-progress: false`. A later hourly heartbeat +therefore does not kill an in-flight root-cause/review-repair pass; the group +still prevents unrelated repositories from sharing the same lease. At most one autofix dispatch is issued during an invocation, and the same exact PR head is not retried more than once per hour. ## Modular MSA contract The shared workflow accepts explicit `target_repository` and `base_branch` -inputs. A sibling product may add a small schedule caller with its own exact -repository and base branch, or invoke the engine through an approved dispatch. -It does not copy the scheduler implementation, OpenCode configuration, repair -worker, or credential logic. +inputs. A sibling product may add a matrix row in the single central scheduler +or invoke the engine through an approved dispatch. It does not copy the +scheduler implementation, OpenCode configuration, repair worker, or credential +logic. The shared target-selection precedence remains: @@ -49,29 +55,33 @@ The shared target-selection precedence remains: 3. `PR_REVIEW_FIX_TARGET_REPOSITORY` repository variable; 4. the workflow execution repository. -The product-specific caller resolves the target before this fallback chain is -needed. Clearfolio therefore has a functioning default heartbeat without +The product-specific matrix row resolves the target before this fallback chain +is needed. Clearfolio therefore has a functioning default heartbeat without changing the engine's standalone or modular semantics. ## Credential and privilege boundary -The caller passes exactly two established optional scheduler credentials: +The consolidated dispatch job passes exactly two established optional scheduler +credentials: - `PR_REVIEW_MERGE_TOKEN`; - `OPENCODE_APPROVE_TOKEN`. -It does not use `secrets: inherit`. It does not receive -`NVIDIA_NIM_API_KEY`, because queue inspection and dispatch are not model -execution. The NVIDIA credential is bound only inside the separately reviewed -`PR Review Autofix` workflow's two OpenCode execution steps. - -Both the caller and reusable scheduler keep the workflow-generated -`GITHUB_TOKEN` read-only with only `contents: read`; neither declares job-level -write elevation. Cross-repository PR inspection, acknowledgement, workflow -dispatch, and branch updates are authorized only through the explicitly mapped +It does not use `secrets: inherit`. It does not receive `NVIDIA_NIM_API_KEY`, +because queue inspection and dispatch are not model execution. The NVIDIA +credential is bound only inside the separately reviewed `PR Review Autofix` +workflow's OpenCode execution steps. + +The consolidated caller keeps `contents: read` and adds job-level +`id-token: write`, matching the OIDC-capable caller boundary used by the other +review-repair targets after consolidation. It still has no repository-content +write permission. The reusable scheduler keeps its own bounded permissions and +cross-repository PR inspection, acknowledgement, workflow dispatch, and branch +updates are authorized only through the explicitly mapped `PR_REVIEW_MERGE_TOKEN` or `OPENCODE_APPROVE_TOKEN`, exposed to the scheduler as -`GH_TOKEN`. The scheduler has no `github.token` fallback. Missing credentials -therefore fail closed instead of silently broadening the workflow token. +`GH_TOKEN`. The scheduler has no `github.token` mutation fallback. Missing +credentials therefore fail closed instead of silently broadening the workflow +token. The repair worker still cannot approve a PR, merge a PR, publish a release, lower branch protection, or convert incomplete checks into success. @@ -93,18 +103,20 @@ evidence only. Permanent tests require all of the following: -1. the Clearfolio caller contains the exact hourly cron; -2. the caller invokes the local reusable scheduler; +1. the Clearfolio matrix row contains the exact hourly cron mapping; +2. the consolidated caller invokes the local reusable scheduler; 3. the target repository and protected base branch are explicit; 4. dispatch and retry bounds remain one; -5. caller and engine use single-flight concurrency; +5. the caller preserves Clearfolio's independent concurrency group and uses + non-cancelling concurrency; 6. the reusable engine contains no Clearfolio literal or scheduled trigger; 7. only the two established scheduler secrets cross the caller boundary; 8. `secrets: inherit`, `COPILOT_GITHUB_TOKEN`, and direct NVIDIA credential binding are absent from the caller; -9. the focused exact-head contract workflow reruns whenever the caller changes; -10. the caller and reusable scheduler retain read-only workflow-token - permissions, declare no job-level write elevation, and contain no +9. the focused exact-head contract workflow reruns whenever the consolidated + caller or its relevant contracts change; +10. the caller retains `contents: read` plus the explicit `id-token: write` + OIDC capability, has no repository-content write elevation, and contains no `github.token` mutation fallback. Repository acceptance still requires current-head workflow, security, @@ -113,12 +125,13 @@ branch-protection evidence. ## Rollback -Rollback removes the dedicated caller and its documentation while leaving the -reusable scheduler and reviewer credentials unchanged. A rollback must not -restore an ambiguous schedule that defaults to the central repository, add a -product literal to the shared engine, expose NVIDIA credentials to queue -inspection, replace explicit secret mapping with `secrets: inherit`, add a -`github.token` mutation fallback, or elevate the workflow-generated token. +Rollback removes Clearfolio's row from the consolidated caller and updates this +document while leaving the reusable scheduler and reviewer credentials +unchanged. A rollback must not restore an ambiguous schedule that defaults to +the central repository, add a product literal to the shared engine, expose +NVIDIA credentials to queue inspection, replace explicit secret mapping with +`secrets: inherit`, add a `github.token` mutation fallback, or broaden +repository-content permissions. ## References (APA 7th edition) diff --git a/docs/doctoring/hourly-review-repair-registry-retirement.md b/docs/doctoring/hourly-review-repair-registry-retirement.md new file mode 100644 index 0000000000..978a04bd46 --- /dev/null +++ b/docs/doctoring/hourly-review-repair-registry-retirement.md @@ -0,0 +1,41 @@ +# Hourly review-repair workflow registry retirement + +## Status + +Prepared 2026-09-02 for the single-file hourly review-repair consolidation in `ContextualWisdomLab/.github` PR #1673. This record addresses the control-plane lifecycle gap found during current-head review: deleting a workflow YAML path does not prove that GitHub has retired the corresponding workflow registry identity. + +## Problem and authority boundary + +The consolidation intentionally replaces 18 scheduled caller files with `.github/workflows/hourly-review-repair.yml`. GitHub Actions, however, keeps workflow registry identities independently of the current Git tree. A source deletion can therefore leave an enabled identity that no longer has an obvious owner path. This repository already treats that as a governance defect in `docs/doctoring/review-repair-quality-workflow-identity.md` and in the read-only orphan-inventory work tracked by `ContextualWisdomLab/.github#1026`. + +The replacement scheduler must therefore be active before legacy identities are retired. Source-file absence is not retirement evidence. Conversely, registry retirement is control-plane lifecycle work only: it does not grant review, merge, repository-content, model-provider, or accounting authority. + +## Migration contract + +PR #1673 adds the one-shot compatibility workflow `.github/workflows/hourly-review-repair-registry-retirement.yml`. It has **no `workflow_dispatch` entrypoint**: its `actions: write` shell is executable only from reviewed source after a push to protected `main`. The job also checks `github.event_name == 'push'` and `github.ref == 'refs/heads/main'` before receiving destructive registry authority. On protected-`main` activation it: + +1. enumerates the complete GitHub Actions workflow registry with pagination; +2. resolves exactly one registry identity for the consolidated replacement and requires its state to be `active` before any destructive mutation; +3. resolves exactly one registry identity for each of the 18 removed per-repository callers; +4. accepts only `active` or already-`disabled_manually` legacy states, disabling `active` identities through the GitHub Actions disable endpoint; +5. reads every mutated workflow identity back and requires `disabled_manually` rather than treating a successful HTTP mutation as sufficient evidence; +6. rechecks that the replacement remains active after all legacy identities are retired; and +7. disables the one-shot migration workflow's own registry identity last. + +The migration has repository `actions: write` plus `contents: read`, no checkout, no model/reviewer secrets, no OIDC grant, no repository-content mutation, no schedule, and no arbitrary-branch manual dispatch. It fails closed on missing, duplicate, unresolved, or unexpected registry states. A transient hosted-run failure is retried through GitHub's run/job retry controls against the same reviewed protected-main source rather than by dispatching a feature branch. The permanent consolidated scheduler retains its narrower read/OIDC dispatch permissions and does not inherit registry-mutation authority. + +## Cleanup and evidence + +The migration source must remain in protected `main` until a hosted run proves all 18 legacy identities and the migration identity itself are `disabled_manually` while `.github/workflows/hourly-review-repair.yml` remains active. After that evidence exists, remove the migration YAML in a normal protected-branch PR. Deleting it only after self-disable leaves its historical registry identity disabled rather than creating another enabled orphan. Do not claim the migration complete from PR checks alone; PR checks validate source contracts, while the registry mutation can occur only after the replacement is active on protected `main`. + +## Regression contract + +`tests/test_hourly_review_repair_registry_retirement.py` requires the one-shot workflow to have neither a schedule nor `workflow_dispatch`, to bind execution to protected-main push context, to name all 18 legacy paths exactly once, to prove the replacement active before the first disable request, to re-read and verify every disabled state, to disable itself last, and to avoid reviewer/model/provider credentials. The focused `Contextual Orchestrator Review Repair Quality CI` watches the migration workflow, this doctoring record, and the retirement contract test so a future change cannot bypass that regression. This complements `tests/test_hourly_review_repair_callers.py`, which continues to verify the 18-repository schedule/target/concurrency mapping in the single active scheduler file. + +## References + +GitHub, Inc. (n.d.). *REST API endpoints for workflows*. GitHub Docs. Retrieved September 2, 2026, from https://docs.github.com/en/rest/actions/workflows + +ContextualWisdomLab. (2026). *Review-repair quality workflow identity RCA*. `docs/doctoring/review-repair-quality-workflow-identity.md`. + +ContextualWisdomLab. (2026). *Inventory orphaned workflow identities* (`ContextualWisdomLab/.github#1026`). GitHub governance work. diff --git a/docs/doctoring/hourly-review-repair-single-file-consolidation.md b/docs/doctoring/hourly-review-repair-single-file-consolidation.md new file mode 100644 index 0000000000..10b42377cc --- /dev/null +++ b/docs/doctoring/hourly-review-repair-single-file-consolidation.md @@ -0,0 +1,184 @@ +# Hourly review-repair single-file consolidation + +## Decision + +The 18 near-identical per-repository hourly review-repair caller files +(`accounting-information-platform-hourly-review-repair.yml`, +`afipc-hourly-review-repair.yml`, `bandscope-hourly-review-repair.yml`, +`clearfolio-hourly-review-repair.yml`, +`contextual-orchestrator-hourly-review-repair.yml`, +`disksage-hourly-review-repair.yml`, `fast-mlsirm-hourly-review-repair.yml`, +`github-hourly-review-repair.yml`, +`governance-risk-compliance-hourly-review-repair.yml`, +`inkspan-hourly-review-repair.yml`, `lineageweave-hourly-review-repair.yml`, +`metering-billing-platform-hourly-review-repair.yml`, +`nonnest2-hourly-review-repair.yml`, `orgmetra-hourly-review-repair.yml`, +`originweave-hourly-review-repair.yml`, +`psychometrics-commons-hourly-review-repair.yml`, +`quarantine-sandbox-hourly-review-repair.yml`, and +`semantic-data-portal-hourly-review-repair.yml`) are replaced by one file, +`.github/workflows/hourly-review-repair.yml`, at the request of the +repository owner (2026-09-02, citing hosted run +`ContextualWisdomLab/.github/actions/runs/33524178483/job/99910668839` of the +"Governance Risk Compliance Hourly Review Repair" workflow): "이런 Workflow는 +단일 파일로 통합하라" (consolidate workflows like this into a single file). +See also [ADR-0021](../adr/0021-hourly-review-repair-single-file-consolidation.md). + +Each deleted file differed from every other one only in `name:`, one +`cron:` minute (and its staggering-rationale comment), the +`concurrency.group` name (and its one-line cancellation-rationale comment), +and the `target_repository` / `base_branch` / `retry_hours` values passed to +`pr-review-fix-scheduler.yml`. `max_prs` ("50") and `max_dispatches` ("1") +were uniform across all 18. That reusable engine already followed this +repository's own stated convention (AGENTS.md / CLAUDE.md: "Product hourly +callers stay thin. Do not hard-code ... into pr-review-fix-scheduler.yml"), +so it is unchanged; only the trigger/dispatch layer above it is +consolidated. + +## Mechanism + +`.github/workflows/hourly-review-repair.yml` uses GitHub Actions' own native +syntax controls, as requested, rather than a new abstraction: + +1. A single `on.schedule` list carries all 17 distinct cron minutes the 18 + files used (one minute, `49 * * * *`, was shared by two files -- see + "The minute-49 collision" below). +2. A `resolve-target` job reads `github.event.schedule` -- the exact cron + expression GitHub sets on the triggering event (GitHub, n.d.-b) -- in a + `run:` step, and looks it up in a `case`/`esac` table that sets a JSON + `targets` array via `GITHUB_OUTPUT`. Every deleted file's staggering and + concurrency-cancellation rationale comments survive as comments on the + corresponding `on.schedule` entry and `case` branch. +3. A `dispatch-review-repair` job (`needs: resolve-target`) fans out over + that JSON array with `strategy.matrix.include`, then calls + `pr-review-fix-scheduler.yml` once per resolved target with + `target_repository` / `base_branch` / `retry_hours` from `matrix.*` and + the two static uniform values (`max_prs: "50"`, `max_dispatches: "1"`). + +### Per-repository concurrency stays isolated + +All 18 original files used SEPARATE, independent `concurrency.group` values +(never one shared group) with `cancel-in-progress: false`, so a later +heartbeat never cancels one repository's in-flight RCA. The consolidated +job's `concurrency:` is `group: ${{ matrix.concurrency_group }}`, reusing +each repository's exact former group name (e.g. +`afipc-hourly-review-repair`). A job-level `concurrency:` expression may +reference `${{ matrix.* }}` because the matrix is resolved before the job +starts (GitHub, n.d.-a), so this reproduces the 18 independent leases inside +one job definition instead of one group shared across every schedule -- +verified directly with `actionlint` and with the extracted lookup script +executed for every one of the 18 original repositories (see Verification). + +### The minute-49 collision + +Auditing the 18 originals for this consolidation found that +`fast-mlsirm-hourly-review-repair.yml` and +`metering-billing-platform-hourly-review-repair.yml` had each +independently chosen `cron: "49 * * * *"` -- an unnoticed collision, not a +deliberate shared heartbeat (their staggering comments both read "Minute 49 +avoids minute-zero pressure and the existing product callers" with no +mention of each other). Under the original 18-file design this was +harmless: each file is its own workflow, so GitHub triggered two +independent workflow runs at `:49`, one per file, each dispatching its own +repository once. + +A consolidated single file cannot rely on two textually-identical +`on.schedule` entries to reproduce that: GitHub Actions' behavior for +duplicate identical cron strings within one workflow's schedule list is not +documented, so this consolidation does not depend on it. Instead there is +exactly **one** `"49 * * * *"` entry in `on.schedule`, and the +`resolve-target` lookup for that one schedule returns a two-element JSON +array (fast-mlsirm, then metering-billing-platform); `dispatch-review-repair`'s +matrix fans out over both. Each of the two repositories still gets exactly +one dispatch attempt at minute 49 of every hour -- the same net cadence as +before -- through a mechanism whose correctness does not depend on +unspecified GitHub scheduling behavior. + +## Other non-uniform fields found while auditing + +- `retry_hours` was **not** uniform: `clearfolio`, `github`, and + `metering-billing-platform` used `"1"`; the other 15 used `"2"`. Preserved + exactly per repository in the lookup table. +- `base_branch` was **not** uniform: `develop` (6), `main` (9), `master` + (2), and `LineageWeave`'s literal `"*"` (1). Preserved exactly. +- `resolve_unreviewed_conflicts: true` appeared explicitly only in + `github-hourly-review-repair.yml`; the other 17 omitted it. The reusable + workflow's own input already defaults to `true` + (`pr-review-fix-scheduler.yml`), so the consolidated file sets it + explicitly and uniformly for all 18 targets -- behaviorally identical to + the prior mixed omitted/explicit state, and simpler than conditionally + omitting a `with:` key per matrix element (which reusable-workflow + `with:` blocks do not support). +- Job-level `permissions:` (`contents: read`, `id-token: write`) was present + in 17 of the 18 files. `clearfolio-hourly-review-repair.yml` was the sole + exception: it had no job-level `permissions:` override, so its job + inherited only the workflow-level `contents: read` and never actually + granted the reusable scheduler `id-token: write` for Clearfolio's calls -- + a latent, silent gap (the scheduler's OIDC token-exchange step could not + mint a token for that one caller; its established + `PR_REVIEW_MERGE_TOKEN` / `OPENCODE_APPROVE_TOKEN` secrets kept the + mutation-credential check passing regardless, so this was not + externally visible). The consolidated file grants + `contents: read` / `id-token: write` uniformly to every matrix target, + matching the other 17 and closing that gap. This is a deliberate, + narrow widening of one caller's own job permissions -- not of + `pr-review-fix-scheduler.yml`, whose own `permissions:` block is + unchanged -- and does not observably change dispatch behavior under the + secrets already provisioned for Clearfolio. +- `max_prs` (`"50"`) and `max_dispatches` (`"1"`) were uniform across all 18 + files; the consolidated file keeps them as static `with:` values rather + than carrying them through the per-target lookup table, since there is + nothing to look up. + +## Verification + +`tests/test_hourly_review_repair_callers.py` extracts the `resolve-target` +job's `run:` script (the same extraction pattern already used in +`tests/test_pr_review_fix_hourly_contract.py`) and executes it as a real +subprocess for each of the 17 schedules, asserting the exact JSON target(s) +against every field the 18 deleted files passed to +`pr-review-fix-scheduler.yml`; an 18th case (the minute-49 pair) is asserted +within the `"49 * * * *"` schedule. It also asserts: the 18 former files no +longer exist; the dynamic `concurrency.group` expression and non-cancelling +posture; the matrix/`needs` wiring; the narrow job permissions; explicit +secrets with no `secrets: inherit`; and that no consolidated target +repository is hard-coded into `pr-review-fix-scheduler.yml`. `actionlint` +passes on the consolidated file. `tests/test_pr_review_fix_hourly_contract.py`, +`tests/test_hourly_scheduler_runtime_budget.py`, +`tests/test_github_hourly_conflict_repair.py`, and +`tests/test_pr_review_autofix_nvidia_nim_contract.py` -- which previously +used Clearfolio, DiskSage, or the central `.github` self-caller as a +representative example caller -- were updated to read the consolidated file +instead of a deleted one, with per-repository flat-string assertions +(`target_repository: ...`, `base_branch: ...`, `retry_hours: ...`) replaced +by the equivalent JSON-literal check against that repository's row in the +lookup table. + +## Non-goals + +The 14 per-repository doctoring records this consolidation's caller files +previously had (e.g. `docs/doctoring/originweave-hourly-review-caller.md`, +`docs/doctoring/nonnest2-hourly-review-caller.md`) are historical decision +records with their own repository-specific security and activation-boundary +narrative; they are kept as-is rather than merged into this document, since +merging would blur which repository a given rationale applies to without +reducing any real duplication (their prose, unlike the deleted YAML, was +never byte-for-byte identical across repositories). Only the one doc that +named its own now-deleted filename +(`docs/doctoring/clearfolio-hourly-review-caller.md`) was corrected to point +at `hourly-review-repair.yml`. + +`docs/product-technical-gap-baseline.md` is a live per-PR gap-tracking +ledger, not a description of current architecture; this internal-only +consolidation does not add a new tracked product gap, so no row was added +there. + +## References (APA 7th edition) + +GitHub, Inc. (n.d.-a). *Using concurrency*. GitHub Docs. Retrieved +2026-09-02, from +https://docs.github.com/en/actions/using-jobs/using-concurrency + +GitHub, Inc. (n.d.-b). *Events that trigger workflows: schedule*. GitHub +Docs. Retrieved 2026-09-02, from +https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule diff --git a/tests/test_afipc_hourly_review_caller.py b/tests/test_afipc_hourly_review_caller.py deleted file mode 100644 index bfcaa5d8e3..0000000000 --- a/tests/test_afipc_hourly_review_caller.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Contract tests for aFIPC's bounded hourly review-repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/afipc-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/afipc-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") -SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - return path.read_text(encoding="utf-8") - - -def _yaml_path_entries(block: str) -> set[str]: - """Return dashed YAML path entries from one trigger or compileall block.""" - entries: set[str] = set() - for raw_line in block.splitlines(): - stripped = raw_line.strip() - if stripped.startswith("- "): - entries.add(stripped[2:].strip()) - elif stripped.startswith("tests/") or stripped.startswith("scripts/"): - entries.add(stripped.rstrip(" \\")) - return entries - - -def _trigger_path_block(quality: str, trigger: str) -> str: - """Return the dashed path list under one named workflow trigger.""" - marker = f" {trigger}:\n paths:\n" - start = quality.index(marker) + len(marker) - lines: list[str] = [] - for line in quality[start:].splitlines(): - if line.startswith(" - "): - lines.append(line) - continue - if line.strip() == "": - continue - break - return "\n".join(lines) - - -def _compileall_block(quality: str) -> str: - """Return the compileall argument list from the focused quality job.""" - marker = "python -m compileall -q \\" - start = quality.index(marker) - remainder = quality[start:] - end = remainder.find("\n git ") - return remainder if end < 0 else remainder[:end] - - -def test_afipc_caller_is_hourly_bounded_and_non_cancelling() -> None: - """aFIPC receives one realistic calibration repair without cancellation.""" - caller = _read(CALLER) - - assert 'cron: "2 * * * *"' in caller - assert "group: afipc-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/aFIPC" in caller - assert "base_branch: master" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_afipc_caller_preserves_oidc_and_explicit_secret_scope() -> None: - """The queue scanner maps established credentials without model secrets.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert ( - "\n permissions:\n contents: read\n id-token: write\n" - in jobs_scope - ) - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_afipc_target_is_not_hard_coded_in_shared_scheduler() -> None: - """Product identity remains in the thin caller rather than the engine.""" - assert "ContextualWisdomLab/aFIPC" not in _read(SCHEDULER) - - -def test_afipc_doctoring_records_fipc_activation_and_credentials() -> None: - """Operators retain target-allowlist, FIPC, and approval prerequisites.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "ContextualWisdomLab/aFIPC", - "OPENCODE_REPOSITORY_DISPATCH_TARGETS", - "independent non-author approval", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "id-token: write", - "two-hour same-head retry floor", - "root-cause analysis", - "remediation feasibility", - "protected-master operational acceptance", - "APA 7th references", - "ContextualWisdomLab/aFIPC#259", - "ContextualWisdomLab/aFIPC#263", - "ContextualWisdomLab/aFIPC#261", - "ContextualWisdomLab/aFIPC#262", - ): - assert phrase in doctoring - - -def test_path_block_helpers_keep_trigger_and_compileall_sets_disjoint() -> None: - """A path listed only under push or compileall must not satisfy pull_request.""" - quality = ( - "on:\n" - " pull_request:\n" - " paths:\n" - " - .github/workflows/afipc-hourly-review-repair.yml\n" - " push:\n" - " paths:\n" - " - docs/doctoring/afipc-hourly-review-caller.md\n" - " python -m compileall -q \\\n" - " tests/test_afipc_hourly_review_caller.py\n" - " git diff --check\n" - ) - - pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) - push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) - compileall_paths = _yaml_path_entries(_compileall_block(quality)) - - assert pull_request_paths == {".github/workflows/afipc-hourly-review-repair.yml"} - assert push_paths == {"docs/doctoring/afipc-hourly-review-caller.md"} - assert compileall_paths == {"tests/test_afipc_hourly_review_caller.py"} - assert "docs/doctoring/afipc-hourly-review-caller.md" not in pull_request_paths - assert ".github/workflows/afipc-hourly-review-repair.yml" not in compileall_paths - - -def test_focused_quality_workflow_tracks_afipc_contracts() -> None: - """Caller, test, and doctoring edits always rerun the focused gate.""" - quality = _read(QUALITY_WORKFLOW) - pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) - push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) - compileall_paths = _yaml_path_entries(_compileall_block(quality)) - caller = ".github/workflows/afipc-hourly-review-repair.yml" - doctoring = "docs/doctoring/afipc-hourly-review-caller.md" - contract = "tests/test_afipc_hourly_review_caller.py" - - assert caller in pull_request_paths - assert doctoring in pull_request_paths - assert contract in pull_request_paths - assert caller in push_paths - assert doctoring in push_paths - assert contract in push_paths - assert contract in compileall_paths - assert caller not in compileall_paths - assert doctoring not in compileall_paths diff --git a/tests/test_bandscope_hourly_review_caller.py b/tests/test_bandscope_hourly_review_caller.py deleted file mode 100644 index 3c8d96cbfb..0000000000 --- a/tests/test_bandscope_hourly_review_caller.py +++ /dev/null @@ -1,87 +0,0 @@ -"""Contract tests for BandScope's bounded hourly review-repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/bandscope-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/bandscope-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") - - -def _read(path: Path) -> str: - """Return one required repository contract file as UTF-8 text.""" - assert path.is_file(), f"missing required contract file: {path}" - return path.read_text(encoding="utf-8") - - -def test_bandscope_caller_is_hourly_bounded_and_non_cancelling() -> None: - """BandScope receives one bounded repair opportunity per hourly heartbeat.""" - caller = _read(CALLER) - - assert 'cron: "53 * * * *"' in caller - assert "group: bandscope-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/bandscope" in caller - assert "base_branch: develop" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_bandscope_caller_preserves_oidc_and_credential_scope() -> None: - """The caller grants only read and OIDC while mapping scheduler credentials.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - pr_review_secret = "$" + "{{ secrets.PR_REVIEW_MERGE_TOKEN }}" - opencode_secret = "$" + "{{ secrets.OPENCODE_APPROVE_TOKEN }}" - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert ( - "\n permissions:\n" - " contents: read\n" - " id-token: write\n" - ) in jobs_scope - assert f"PR_REVIEW_MERGE_TOKEN: {pr_review_secret}" in caller - assert f"OPENCODE_APPROVE_TOKEN: {opencode_secret}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_bandscope_doctoring_records_music_and_governance_bounds() -> None: - """Operators retain RCA, music-evidence, credential, and approval contracts.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "root-cause analysis", - "remediation feasibility", - "two-hour same-head retry floor", - "real-audio acceptance", - "Rust-owned production arithmetic", - "independent non-author approval", - "id-token: write", - "OPENCODE_REPOSITORY_DISPATCH_TARGETS", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "ContextualWisdomLab/bandscope", - "APA 7th references", - ): - assert phrase in doctoring - - -def test_focused_quality_workflow_tracks_bandscope_contracts() -> None: - """Caller and doctoring edits always rerun exact-head verification.""" - quality = _read(QUALITY_WORKFLOW) - - assert quality.count(".github/workflows/bandscope-hourly-review-repair.yml") == 2 - assert quality.count("docs/doctoring/bandscope-hourly-review-caller.md") == 2 - assert quality.count("tests/test_bandscope_hourly_review_caller.py") == 3 diff --git a/tests/test_contextual_orchestrator_hourly_review_caller.py b/tests/test_contextual_orchestrator_hourly_review_caller.py deleted file mode 100644 index 204ed52887..0000000000 --- a/tests/test_contextual_orchestrator_hourly_review_caller.py +++ /dev/null @@ -1,88 +0,0 @@ -"""Contract tests for Contextual Orchestrator's bounded hourly review-repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/contextual-orchestrator-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/contextual-orchestrator-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") -SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - return path.read_text(encoding="utf-8") - - -def test_contextual_orchestrator_caller_is_hourly_bounded_and_non_cancelling() -> None: - """The gateway repo receives one realistic repair opportunity without cancellation.""" - caller = _read(CALLER) - - assert 'cron: "34 * * * *"' in caller - assert "group: contextual-orchestrator-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/contextual-orchestrator" in caller - assert "base_branch: main" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_contextual_orchestrator_caller_preserves_credentials_and_read_only_scope() -> None: - """The queue scanner maps established credentials without exposing model secrets.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_contextual_orchestrator_target_is_not_hard_coded_in_shared_scheduler() -> None: - """Product identity remains in the thin caller rather than the engine.""" - assert "ContextualWisdomLab/contextual-orchestrator" not in _read(SCHEDULER) - - -def test_contextual_orchestrator_doctoring_records_rca_feasibility_and_latency() -> None: - """Operators retain the exact rationale for the bounded two-hour retry policy.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "root-cause analysis", - "remediation feasibility", - "two-hour same-head retry floor", - "independent non-author approval", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "ContextualWisdomLab/contextual-orchestrator", - "APA 7th references", - ): - assert phrase in doctoring - - -def test_focused_quality_workflow_tracks_contextual_orchestrator_contracts() -> None: - """Every caller or doctoring edit reruns exact-head scheduler verification.""" - quality = _read(QUALITY_WORKFLOW) - - assert quality.count( - ".github/workflows/contextual-orchestrator-hourly-review-repair.yml" - ) == 2 - assert quality.count( - "docs/doctoring/contextual-orchestrator-hourly-review-caller.md" - ) == 2 - assert quality.count( - "tests/test_contextual_orchestrator_hourly_review_caller.py" - ) == 3 diff --git a/tests/test_disksage_hourly_review_caller.py b/tests/test_disksage_hourly_review_caller.py deleted file mode 100644 index 5ad14b2488..0000000000 --- a/tests/test_disksage_hourly_review_caller.py +++ /dev/null @@ -1,76 +0,0 @@ -"""Contract tests for DiskSage's bounded hourly review-repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/disksage-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/disksage-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - return path.read_text(encoding="utf-8") - - -def test_disksage_caller_is_hourly_bounded_and_non_cancelling() -> None: - """DiskSage receives one realistic repair opportunity without overlap cancellation.""" - caller = _read(CALLER) - - assert 'cron: "37 * * * *"' in caller - assert "group: disksage-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/disksage" in caller - assert "base_branch: main" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_disksage_caller_preserves_credentials_and_read_only_token_scope() -> None: - """The queue scanner maps established credentials without exposing model secrets.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_disksage_caller_doctoring_records_rca_feasibility_and_latency() -> None: - """Operators retain the exact rationale for the bounded two-hour retry policy.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "root-cause analysis", - "remediation feasibility", - "two-hour same-head retry floor", - "independent non-author approval", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "ContextualWisdomLab/disksage", - "APA 7th references", - ): - assert phrase in doctoring - - -def test_focused_quality_workflow_tracks_disksage_caller_contracts() -> None: - """Every caller or doctoring edit reruns exact-head scheduler verification.""" - quality = _read(QUALITY_WORKFLOW) - - assert quality.count(".github/workflows/disksage-hourly-review-repair.yml") == 2 - assert quality.count("docs/doctoring/disksage-hourly-review-caller.md") == 2 - assert quality.count("tests/test_disksage_hourly_review_caller.py") == 3 diff --git a/tests/test_fast_mlsirm_hourly_review_caller.py b/tests/test_fast_mlsirm_hourly_review_caller.py deleted file mode 100644 index 1fd0965860..0000000000 --- a/tests/test_fast_mlsirm_hourly_review_caller.py +++ /dev/null @@ -1,80 +0,0 @@ -"""Contract tests for fast-mlsirm's bounded hourly review-repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/fast-mlsirm-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/fast-mlsirm-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - return path.read_text(encoding="utf-8") - - -def test_fast_mlsirm_caller_is_hourly_bounded_and_non_cancelling() -> None: - """fast-mlsirm receives one realistic repair opportunity per heartbeat.""" - caller = _read(CALLER) - - assert 'cron: "49 * * * *"' in caller - assert "group: fast-mlsirm-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/fast-mlsirm" in caller - assert "base_branch: main" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_fast_mlsirm_caller_preserves_credentials_and_read_only_scope() -> None: - """The caller maps scheduler credentials without model-secret exposure.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - pr_review_secret = "$" + "{{ secrets.PR_REVIEW_MERGE_TOKEN }}" - opencode_secret = "$" + "{{ secrets.OPENCODE_APPROVE_TOKEN }}" - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope - assert f"PR_REVIEW_MERGE_TOKEN: {pr_review_secret}" in caller - assert f"OPENCODE_APPROVE_TOKEN: {opencode_secret}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_fast_mlsirm_doctoring_records_scientific_and_governance_bounds() -> None: - """Operators retain RCA, scientific, credential, and approval contracts.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "root-cause analysis", - "remediation feasibility", - "two-hour same-head retry floor", - "true-parameter recovery", - "Rust ownership of production arithmetic", - "independent non-author approval", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "ContextualWisdomLab/fast-mlsirm", - "APA 7th references", - ): - assert phrase in doctoring - - -def test_focused_quality_workflow_tracks_fast_mlsirm_contracts() -> None: - """Caller and doctoring edits always rerun exact-head verification.""" - quality = _read(QUALITY_WORKFLOW) - - assert quality.count(".github/workflows/fast-mlsirm-hourly-review-repair.yml") == 2 - assert quality.count("docs/doctoring/fast-mlsirm-hourly-review-caller.md") == 2 - assert quality.count("tests/test_fast_mlsirm_hourly_review_caller.py") == 3 diff --git a/tests/test_github_hourly_conflict_repair.py b/tests/test_github_hourly_conflict_repair.py index b4b8e5d6af..5e9b483651 100644 --- a/tests/test_github_hourly_conflict_repair.py +++ b/tests/test_github_hourly_conflict_repair.py @@ -8,7 +8,7 @@ from scripts.ci import pr_review_fix_scheduler as scheduler -_CALLER = Path(".github/workflows/github-hourly-review-repair.yml") +_CALLER = Path(".github/workflows/hourly-review-repair.yml") _REUSABLE_SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") @@ -121,11 +121,15 @@ def test_central_repository_has_hourly_self_caller() -> None: assert 'cron: "21 * * * *"' in workflow assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in workflow - assert "target_repository: ContextualWisdomLab/.github" in workflow - assert "base_branch: main" in workflow + # The consolidated file resolves per-repository parameters through a + # github.event.schedule lookup table rather than flat `key: value` + # lines; the github/.github entry's JSON literal carries the same + # values the former dedicated caller passed literally. + assert '"target_repository":"ContextualWisdomLab/.github"' in workflow + assert '"base_branch":"main"' in workflow + assert '"retry_hours":"1"' in workflow assert "resolve_unreviewed_conflicts: true" in workflow assert 'max_dispatches: "1"' in workflow - assert 'retry_hours: "1"' in workflow assert "\n permissions:\n contents: read\n id-token: write\n" in workflow assert "COPILOT_GITHUB_TOKEN" not in workflow diff --git a/tests/test_governance_risk_compliance_hourly_review_caller.py b/tests/test_governance_risk_compliance_hourly_review_caller.py deleted file mode 100644 index 4b0fb4f93e..0000000000 --- a/tests/test_governance_risk_compliance_hourly_review_caller.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Contract tests for the GRC product's bounded hourly review-repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/governance-risk-compliance-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/governance-risk-compliance-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - return path.read_text(encoding="utf-8") - - -def test_grc_caller_is_hourly_bounded_and_non_cancelling() -> None: - """GRC receives one realistic exact-head repair opportunity per heartbeat.""" - caller = _read(CALLER) - - assert 'cron: "43 * * * *"' in caller - assert "group: governance-risk-compliance-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/governance-risk-compliance" in caller - assert "base_branch: develop" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_grc_caller_preserves_credentials_and_read_only_scope() -> None: - """The caller maps scheduler credentials without exposing provider secrets.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - pr_review_secret = "$" + "{{ secrets.PR_REVIEW_MERGE_TOKEN }}" - opencode_secret = "$" + "{{ secrets.OPENCODE_APPROVE_TOKEN }}" - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope - assert f"PR_REVIEW_MERGE_TOKEN: {pr_review_secret}" in caller - assert f"OPENCODE_APPROVE_TOKEN: {opencode_secret}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_grc_doctoring_records_product_and_governance_bounds() -> None: - """Operators retain RCA, ownership, credential, and approval contracts.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "root-cause analysis", - "remediation feasibility", - "two-hour same-head retry floor", - "policy, control, risk, evidence, and compliance-audit truth", - "Keyverse", - "independent non-author approval", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "ContextualWisdomLab/governance-risk-compliance", - "APA 7th references", - ): - assert phrase in doctoring - - -def test_focused_quality_workflow_tracks_grc_contracts() -> None: - """Caller, doctoring, and contract edits always rerun exact-head verification.""" - quality = _read(QUALITY_WORKFLOW) - - assert quality.count( - ".github/workflows/governance-risk-compliance-hourly-review-repair.yml" - ) == 2 - assert quality.count( - "docs/doctoring/governance-risk-compliance-hourly-review-caller.md" - ) == 2 - assert quality.count("tests/test_governance_risk_compliance_hourly_review_caller.py") == 3 diff --git a/tests/test_hourly_review_repair_callers.py b/tests/test_hourly_review_repair_callers.py new file mode 100644 index 0000000000..eccf7630f2 --- /dev/null +++ b/tests/test_hourly_review_repair_callers.py @@ -0,0 +1,433 @@ +"""Contracts for the consolidated hourly review-repair caller. + +Replaces the 18 near-identical per-repository ``-hourly-review-repair.yml`` +caller files (and their 13 dedicated test modules) with one file, +``.github/workflows/hourly-review-repair.yml``, and one test module. See +``docs/doctoring/hourly-review-repair-single-file-consolidation.md`` and +``docs/adr/0021-hourly-review-repair-single-file-consolidation.md`` for why. +""" + +from __future__ import annotations + +import json +import re +import subprocess +from pathlib import Path + +import pytest + +_CALLER = Path(".github/workflows/hourly-review-repair.yml") +_REUSABLE_SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") + +_FORMER_CALLERS = ( + "accounting-information-platform-hourly-review-repair.yml", + "afipc-hourly-review-repair.yml", + "bandscope-hourly-review-repair.yml", + "clearfolio-hourly-review-repair.yml", + "contextual-orchestrator-hourly-review-repair.yml", + "disksage-hourly-review-repair.yml", + "fast-mlsirm-hourly-review-repair.yml", + "github-hourly-review-repair.yml", + "governance-risk-compliance-hourly-review-repair.yml", + "inkspan-hourly-review-repair.yml", + "lineageweave-hourly-review-repair.yml", + "metering-billing-platform-hourly-review-repair.yml", + "nonnest2-hourly-review-repair.yml", + "orgmetra-hourly-review-repair.yml", + "originweave-hourly-review-repair.yml", + "psychometrics-commons-hourly-review-repair.yml", + "quarantine-sandbox-hourly-review-repair.yml", + "semantic-data-portal-hourly-review-repair.yml", +) + +# schedule -> exact list of {name, target_repository, base_branch, +# retry_hours, concurrency_group} the resolve-target lookup must produce, +# reproducing every field the 18 deleted files passed to +# pr-review-fix-scheduler.yml. max_prs ("50") and max_dispatches ("1") were +# uniform across all 18 originals and are asserted separately as static +# `with:` values rather than carried per-target. +_EXPECTED_TARGETS: dict[str, list[dict[str, str]]] = { + "2 * * * *": [ + { + "name": "afipc", + "target_repository": "ContextualWisdomLab/aFIPC", + "base_branch": "master", + "retry_hours": "2", + "concurrency_group": "afipc-hourly-review-repair", + }, + ], + "4 * * * *": [ + { + "name": "lineageweave", + "target_repository": "ContextualWisdomLab/LineageWeave", + "base_branch": "*", + "retry_hours": "2", + "concurrency_group": "lineageweave-hourly-review-repair", + }, + ], + "9 * * * *": [ + { + "name": "psychometrics-commons", + "target_repository": "ContextualWisdomLab/psychometrics-commons", + "base_branch": "main", + "retry_hours": "2", + "concurrency_group": "psychometrics-commons-hourly-review-repair", + }, + ], + "10 * * * *": [ + { + "name": "originweave", + "target_repository": "ContextualWisdomLab/OriginWeave", + "base_branch": "main", + "retry_hours": "2", + "concurrency_group": "originweave-hourly-review-repair", + }, + ], + "14 * * * *": [ + { + "name": "quarantine-sandbox", + "target_repository": "ContextualWisdomLab/quarantine-sandbox-runtime", + "base_branch": "develop", + "retry_hours": "2", + "concurrency_group": "quarantine-sandbox-hourly-review-repair", + }, + ], + "16 * * * *": [ + { + "name": "nonnest2", + "target_repository": "ContextualWisdomLab/nonnest2", + "base_branch": "master", + "retry_hours": "2", + "concurrency_group": "nonnest2-hourly-review-repair", + }, + ], + "21 * * * *": [ + { + "name": "github", + "target_repository": "ContextualWisdomLab/.github", + "base_branch": "main", + "retry_hours": "1", + "concurrency_group": "github-hourly-review-repair", + }, + ], + "23 * * * *": [ + { + "name": "clearfolio", + "target_repository": "ContextualWisdomLab/clearfolio", + "base_branch": "main", + "retry_hours": "1", + "concurrency_group": "clearfolio-hourly-review-repair", + }, + ], + "27 * * * *": [ + { + "name": "accounting-information-platform", + "target_repository": "ContextualWisdomLab/accounting-information-platform", + "base_branch": "develop", + "retry_hours": "2", + "concurrency_group": "accounting-information-platform-hourly-review-repair", + }, + ], + "34 * * * *": [ + { + "name": "contextual-orchestrator", + "target_repository": "ContextualWisdomLab/contextual-orchestrator", + "base_branch": "main", + "retry_hours": "2", + "concurrency_group": "contextual-orchestrator-hourly-review-repair", + }, + ], + "37 * * * *": [ + { + "name": "disksage", + "target_repository": "ContextualWisdomLab/disksage", + "base_branch": "main", + "retry_hours": "2", + "concurrency_group": "disksage-hourly-review-repair", + }, + ], + "43 * * * *": [ + { + "name": "governance-risk-compliance", + "target_repository": "ContextualWisdomLab/governance-risk-compliance", + "base_branch": "develop", + "retry_hours": "2", + "concurrency_group": "governance-risk-compliance-hourly-review-repair", + }, + ], + # Minute 49 is the one collision the original 18 files carried: two + # independent files (fast-mlsirm, metering-billing-platform) had each + # chosen minute 49 without knowing about the other. The consolidated + # lookup makes that sharing explicit and still dispatches each + # repository exactly once per hour, via the matrix in + # dispatch-review-repair. + "49 * * * *": [ + { + "name": "fast-mlsirm", + "target_repository": "ContextualWisdomLab/fast-mlsirm", + "base_branch": "main", + "retry_hours": "2", + "concurrency_group": "fast-mlsirm-hourly-review-repair", + }, + { + "name": "metering-billing-platform", + "target_repository": "ContextualWisdomLab/metering-billing-platform", + "base_branch": "develop", + "retry_hours": "1", + "concurrency_group": "metering-billing-platform-hourly-review-repair", + }, + ], + "53 * * * *": [ + { + "name": "bandscope", + "target_repository": "ContextualWisdomLab/bandscope", + "base_branch": "develop", + "retry_hours": "2", + "concurrency_group": "bandscope-hourly-review-repair", + }, + ], + "56 * * * *": [ + { + "name": "inkspan", + "target_repository": "ContextualWisdomLab/inkspan", + "base_branch": "main", + "retry_hours": "2", + "concurrency_group": "inkspan-hourly-review-repair", + }, + ], + "58 * * * *": [ + { + "name": "orgmetra", + "target_repository": "ContextualWisdomLab/Orgmetra", + "base_branch": "develop", + "retry_hours": "2", + "concurrency_group": "orgmetra-hourly-review-repair", + }, + ], + "59 * * * *": [ + { + "name": "semantic-data-portal", + "target_repository": "ContextualWisdomLab/semantic-data-portal", + "base_branch": "main", + "retry_hours": "2", + "concurrency_group": "semantic-data-portal-hourly-review-repair", + }, + ], +} + + +def _read(path: Path) -> str: + """Return one workflow as UTF-8 text.""" + return path.read_text(encoding="utf-8") + + +def _resolve_step_script(workflow_text: str) -> str: + """Extract the resolve-target job's inline lookup script. + + Mirrors the extraction pattern already used in + ``test_scheduler_validates_dispatch_authority_before_credentials`` + (``tests/test_pr_review_fix_hourly_contract.py``) for exercising an + embedded ``run:`` block as a real subprocess instead of only pattern + matching the YAML text. + """ + marker = " run: |\n" + start = workflow_text.index(marker) + len(marker) + lines = workflow_text[start:].splitlines() + script_lines: list[str] = [] + for line in lines: + if line.strip() == "": + script_lines.append(line) + continue + indent = len(line) - len(line.lstrip(" ")) + if indent < 10: + break + script_lines.append(line[10:]) + return "\n".join(script_lines) + + +def _run_lookup(script: str, schedule: str, tmp_path: Path) -> list[dict[str, str]]: + """Execute the extracted lookup script for one schedule and parse its output.""" + output_file = tmp_path / f"gh_output_{abs(hash(schedule))}.txt" + output_file.write_text("") + result = subprocess.run( + ["bash", "-c", script], + env={"SCHEDULE": schedule, "GITHUB_OUTPUT": str(output_file), "PATH": "/usr/bin:/bin"}, + capture_output=True, + text=True, + check=False, + ) + assert result.returncode == 0, ( + f"lookup script failed for schedule={schedule!r}: {result.stderr}" + ) + match = re.match(r"targets=(.*)\n?\Z", output_file.read_text(), re.S) + assert match, f"no targets= output for schedule={schedule!r}" + return json.loads(match.group(1)) + + +def test_all_eighteen_former_callers_are_deleted() -> None: + """The 18 former per-repository files are fully replaced, not duplicated.""" + for filename in _FORMER_CALLERS: + assert not Path(f".github/workflows/{filename}").exists(), ( + f"{filename} should have been deleted by the single-file consolidation" + ) + assert _CALLER.is_file() + + +def test_schedule_list_has_every_distinct_minute_exactly_once() -> None: + """The 17 distinct minutes (49 is intentionally shared) each appear once.""" + text = _read(_CALLER) + cron_lines = re.findall(r'- cron: "([^"]+)"', text) + + assert len(cron_lines) == len(set(cron_lines)) == 17 + assert set(cron_lines) == set(_EXPECTED_TARGETS) + + +@pytest.mark.parametrize("schedule", sorted(_EXPECTED_TARGETS)) +def test_resolve_target_lookup_matches_original_per_repo_parameters( + schedule: str, tmp_path: Path +) -> None: + """Every schedule resolves to the exact target(s) its deleted file(s) used.""" + script = _resolve_step_script(_read(_CALLER)) + + assert _run_lookup(script, schedule, tmp_path) == _EXPECTED_TARGETS[schedule] + + +def test_resolve_target_lookup_fails_closed_on_an_unknown_schedule( + tmp_path: Path, +) -> None: + """An unrecognized schedule value must not dispatch to any repository.""" + script = _resolve_step_script(_read(_CALLER)) + output_file = tmp_path / "gh_output_unknown.txt" + output_file.write_text("") + + result = subprocess.run( + ["bash", "-c", script], + env={ + "SCHEDULE": "0 0 * * *", + "GITHUB_OUTPUT": str(output_file), + "PATH": "/usr/bin:/bin", + }, + capture_output=True, + text=True, + check=False, + ) + + assert result.returncode != 0 + assert output_file.read_text() == "" + + +def test_max_prs_and_max_dispatches_stay_uniform_static_values() -> None: + """The two fields that never varied across the 18 originals stay static.""" + text = _read(_CALLER) + + assert 'max_prs: "50"' in text + assert 'max_dispatches: "1"' in text + # They are static `with:` values, not carried through the per-target + # lookup table (they never varied, so there is nothing to look up). + assert '"max_prs"' not in text + assert '"max_dispatches"' not in text + + +def test_dispatch_job_uses_a_per_repository_dynamic_concurrency_group() -> None: + """Each repository keeps its own independent, non-cancelling lease. + + All 18 original files used SEPARATE `concurrency.group` values (one per + repository), never a shared group. A `concurrency:` expression at job + level may reference `matrix.*` because the matrix is resolved before the + job starts (GitHub, n.d.-a), so keying the group on + `matrix.concurrency_group` reproduces that per-repository isolation + inside one job definition instead of one group shared by every + schedule. + """ + text = _read(_CALLER) + + assert "group: ${{ matrix.concurrency_group }}" in text + assert "cancel-in-progress: false" in text + assert "cancel-in-progress: true" not in text + # No single hard-coded group name: isolation is per resolved target. + for filename in _FORMER_CALLERS: + repo_slug = filename.removesuffix("-hourly-review-repair.yml") + assert f"group: {repo_slug}-hourly-review-repair" not in text + + +def test_dispatch_job_fans_out_over_the_resolved_targets_matrix() -> None: + """The matrix consumes resolve-target's output for every schedule.""" + text = _read(_CALLER) + + assert "needs: resolve-target" in text + assert ( + "include: ${{ fromJson(needs.resolve-target.outputs.targets) }}" in text + ) + assert "fail-fast: false" in text + assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in text + assert "target_repository: ${{ matrix.target_repository }}" in text + assert "base_branch: ${{ matrix.base_branch }}" in text + assert 'retry_hours: ${{ matrix.retry_hours }}' in text + + +def test_dispatch_job_grants_only_read_and_oidc_permissions() -> None: + """Every resolved target gets the same narrow, explicit permission set.""" + text = _read(_CALLER) + workflow_scope, jobs_scope = text.split("\njobs:\n", maxsplit=1) + + assert "\npermissions:\n contents: read\n" in workflow_scope + assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope + for permission in ( + "actions: write", + "issues: write", + "contents: write", + "pull-requests: write", + "statuses: write", + ): + assert permission not in text + + +def test_dispatch_job_forwards_only_the_two_established_secrets() -> None: + """No `secrets: inherit`, no gateway provider credential leakage.""" + text = _read(_CALLER) + + assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in text + assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in text + assert "secrets: inherit" not in text + assert "COPILOT_GITHUB_TOKEN" not in text + assert "NVIDIA_NIM_API_KEY" not in text + + +def test_no_target_repository_is_hard_coded_in_the_shared_scheduler() -> None: + """The reusable engine stays product-neutral for every consolidated target. + + ``ContextualWisdomLab/.github`` legitimately appears in the reusable + workflow as the *default* ``autofix_repository`` (the central repository + that owns ``pr-review-autofix.yml``, not a scanned product target), so + the central self-caller is excluded from this check the same way the + original per-repository tests only checked the product repositories + (OriginWeave, aFIPC, nonnest2, quarantine-sandbox, + contextual-orchestrator) and not the central repository's own name. + """ + reusable_text = _read(_REUSABLE_SCHEDULER) + + for targets in _EXPECTED_TARGETS.values(): + for target in targets: + if target["name"] == "github": + continue + assert target["target_repository"] not in reusable_text + + +def test_resolve_unreviewed_conflicts_is_explicit_and_matches_the_default() -> None: + """Making the input explicit for every target changes nothing behaviorally. + + The reusable workflow's own `resolve_unreviewed_conflicts` input already + defaults to `true`; 17 of the 18 original files omitted the key (relying + on that default) and only the central `.github` self-caller set it + explicitly. The consolidated file sets it explicitly and uniformly, + which is behaviorally identical to the prior mixed omitted/explicit + state for every one of the 18 targets. + """ + caller_text = _read(_CALLER) + reusable_text = _read(_REUSABLE_SCHEDULER) + + assert "resolve_unreviewed_conflicts: true" in caller_text + policy_block = reusable_text.split("resolve_unreviewed_conflicts:", maxsplit=1)[ + 1 + ].split("retry_hours:", maxsplit=1)[0] + assert "default: true" in policy_block diff --git a/tests/test_hourly_review_repair_registry_retirement.py b/tests/test_hourly_review_repair_registry_retirement.py new file mode 100644 index 0000000000..fe253fde51 --- /dev/null +++ b/tests/test_hourly_review_repair_registry_retirement.py @@ -0,0 +1,105 @@ +"""Contracts for retiring legacy hourly review-repair workflow identities.""" + +from __future__ import annotations + +from pathlib import Path + + +_WORKFLOW = Path(".github/workflows/hourly-review-repair-registry-retirement.yml") +_REPLACEMENT = ".github/workflows/hourly-review-repair.yml" +_LEGACY_PATHS = ( + ".github/workflows/accounting-information-platform-hourly-review-repair.yml", + ".github/workflows/afipc-hourly-review-repair.yml", + ".github/workflows/bandscope-hourly-review-repair.yml", + ".github/workflows/clearfolio-hourly-review-repair.yml", + ".github/workflows/contextual-orchestrator-hourly-review-repair.yml", + ".github/workflows/disksage-hourly-review-repair.yml", + ".github/workflows/fast-mlsirm-hourly-review-repair.yml", + ".github/workflows/github-hourly-review-repair.yml", + ".github/workflows/governance-risk-compliance-hourly-review-repair.yml", + ".github/workflows/inkspan-hourly-review-repair.yml", + ".github/workflows/lineageweave-hourly-review-repair.yml", + ".github/workflows/metering-billing-platform-hourly-review-repair.yml", + ".github/workflows/nonnest2-hourly-review-repair.yml", + ".github/workflows/orgmetra-hourly-review-repair.yml", + ".github/workflows/originweave-hourly-review-repair.yml", + ".github/workflows/psychometrics-commons-hourly-review-repair.yml", + ".github/workflows/quarantine-sandbox-hourly-review-repair.yml", + ".github/workflows/semantic-data-portal-hourly-review-repair.yml", +) + + +def _text() -> str: + """Return the one-shot registry-retirement workflow source.""" + return _WORKFLOW.read_text(encoding="utf-8") + + +def test_retirement_is_protected_main_push_only_and_not_scheduled() -> None: + """Privileged registry mutation cannot run from an arbitrary branch or cadence.""" + text = _text() + + assert " schedule:" not in text + assert " push:" in text + assert " - main" in text + assert "workflow_dispatch:" not in text + assert "github.event_name == 'push'" in text + assert "github.ref == 'refs/heads/main'" in text + assert "actions: write" in text + assert "contents: read" in text + assert "contents: write" not in text + assert "id-token: write" not in text + + +def test_retirement_names_every_legacy_identity_exactly_once() -> None: + """No deleted hourly caller can remain an untracked active registry ID.""" + text = _text() + + assert len(_LEGACY_PATHS) == 18 + for path in _LEGACY_PATHS: + assert text.count(f'"{path}"') == 1 + assert text.count(f"REPLACEMENT_PATH: {_REPLACEMENT}") == 1 + + +def test_replacement_is_proven_active_before_any_disable_call() -> None: + """The migration fails closed unless the consolidated scheduler is active.""" + text = _text() + replacement_guard = 'if [[ "$replacement_state" != "active" ]]' + disable_endpoint = '/actions/workflows/${workflow_id}/disable' + + assert replacement_guard in text + assert disable_endpoint in text + assert text.index(replacement_guard) < text.index(disable_endpoint) + assert "Expected exactly one workflow registry identity" in text + + +def test_every_disabled_identity_is_read_back_and_verified() -> None: + """A successful mutation is not evidence until the registry state is re-read.""" + text = _text() + + assert ( + "gh api \"/repos/${REPOSITORY}/actions/workflows/${workflow_id}\" --jq '.state'" + in text + ) + assert 'if [[ "$state" != "disabled_manually" ]]' in text + assert 'disable_and_verify "$SELF_PATH"' in text + assert text.rindex('disable_and_verify "$SELF_PATH"') > text.rindex( + 'for path in "${legacy_paths[@]}"' + ) + + +def test_retirement_does_not_expose_reviewer_or_provider_credentials() -> None: + """Registry mutation uses only the scoped GitHub token and no model secrets.""" + text = _text() + + assert "GH_TOKEN: ${{ github.token }}" in text + for forbidden in ( + "PR_REVIEW_MERGE_TOKEN", + "OPENCODE_APPROVE_TOKEN", + "COPILOT_GITHUB_TOKEN", + "OPENAI_API_KEY", + "OPENROUTER_API_KEY", + "NVIDIA_NIM_API_KEY", + "BYTEZ_API_KEY", + "actions/checkout", + ): + assert forbidden not in text diff --git a/tests/test_hourly_scheduler_runtime_budget.py b/tests/test_hourly_scheduler_runtime_budget.py index bf24b15183..0bf25e7a0b 100644 --- a/tests/test_hourly_scheduler_runtime_budget.py +++ b/tests/test_hourly_scheduler_runtime_budget.py @@ -4,8 +4,9 @@ REUSABLE = Path(".github/workflows/pr-review-fix-scheduler.yml") -CLEARFOLIO = Path(".github/workflows/clearfolio-hourly-review-repair.yml") -DISKSAGE = Path(".github/workflows/disksage-hourly-review-repair.yml") +# Clearfolio and DiskSage (like all 18 former per-repository callers) are now +# both resolved from the one consolidated caller file. +CONSOLIDATED_CALLER = Path(".github/workflows/hourly-review-repair.yml") QUALITY = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") REPLACEMENT_QUALITY = Path( ".github/workflows/contextual-orchestrator-review-repair-quality.yml" @@ -28,16 +29,16 @@ def test_queue_scanner_has_a_bounded_superseding_runtime() -> None: def test_product_callers_do_not_cancel_an_in_flight_rca() -> None: - """Clearfolio and DiskSage preserve the non-cancelling product lease.""" - for caller_path in (CLEARFOLIO, DISKSAGE): - caller = _read(caller_path) - assert "cancel-in-progress: false" in caller - assert "cancel-in-progress: true" not in caller + """Every consolidated product caller preserves the non-cancelling lease.""" + caller = _read(CONSOLIDATED_CALLER) + + assert "cancel-in-progress: false" in caller + assert "cancel-in-progress: true" not in caller def test_disksage_caller_grants_oidc_permission_to_reusable_scheduler() -> None: """The called scheduler must be able to exchange its OpenCode OIDC token.""" - caller = _read(DISKSAGE) + caller = _read(CONSOLIDATED_CALLER) job = caller.split(" dispatch-review-repair:\n", maxsplit=1)[1] assert " permissions:\n contents: read\n id-token: write\n" in job diff --git a/tests/test_inkspan_hourly_review_caller.py b/tests/test_inkspan_hourly_review_caller.py deleted file mode 100644 index fb8f903694..0000000000 --- a/tests/test_inkspan_hourly_review_caller.py +++ /dev/null @@ -1,77 +0,0 @@ -"""Contract tests for Inkspan's bounded hourly review-repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/inkspan-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/inkspan-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - return path.read_text(encoding="utf-8") - - -def test_inkspan_caller_is_hourly_bounded_and_non_cancelling() -> None: - """Inkspan receives one bounded repair opportunity without overlap cancellation.""" - caller = _read(CALLER) - - assert 'cron: "56 * * * *"' in caller - assert "group: inkspan-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/inkspan" in caller - assert "base_branch: main" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_inkspan_caller_preserves_credentials_and_oidc_scope() -> None: - """The caller grants only reusable-worker read/OIDC permissions.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_inkspan_doctoring_records_governance_and_research_bounds() -> None: - """Operators retain RCA, credential, approval, and citation contracts.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "root-cause analysis", - "remediation feasibility", - "two-hour same-head retry floor", - "independent non-author approval", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "ContextualWisdomLab/inkspan#299", - "ContextualWisdomLab/inkspan#362", - "APA 7th references", - ): - assert phrase in doctoring - - -def test_focused_quality_workflow_tracks_inkspan_caller_contracts() -> None: - """Caller and doctoring edits rerun exact-head scheduler verification.""" - quality = _read(QUALITY_WORKFLOW) - - assert quality.count(".github/workflows/inkspan-hourly-review-repair.yml") == 2 - assert quality.count("docs/doctoring/inkspan-hourly-review-caller.md") == 2 - assert quality.count("tests/test_inkspan_hourly_review_caller.py") == 3 diff --git a/tests/test_lineageweave_hourly_review_caller.py b/tests/test_lineageweave_hourly_review_caller.py deleted file mode 100644 index fb7b8ba8cc..0000000000 --- a/tests/test_lineageweave_hourly_review_caller.py +++ /dev/null @@ -1,72 +0,0 @@ -"""Contract tests for LineageWeave's bounded hourly review-repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/lineageweave-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/lineageweave-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") -SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - return path.read_text(encoding="utf-8") - - -def test_lineageweave_caller_is_hourly_bounded_and_stack_aware() -> None: - """The stacked repository receives one non-cancelling repair opportunity.""" - caller = _read(CALLER) - - for contract in ( - 'cron: "4 * * * *"', - "group: lineageweave-hourly-review-repair", - "cancel-in-progress: false", - "uses: ./.github/workflows/pr-review-fix-scheduler.yml", - "target_repository: ContextualWisdomLab/LineageWeave", - 'base_branch: "*"', - 'max_prs: "50"', - 'max_dispatches: "1"', - 'retry_hours: "2"', - ): - assert contract in caller - - -def test_lineageweave_caller_preserves_the_existing_credential_boundary() -> None: - """The caller maps only scheduler credentials and exposes no model secret.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller - assert "secrets: inherit" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "LLM_GATEWAY_API_URL" not in caller - assert "ContextualWisdomLab/LineageWeave" not in _read(SCHEDULER) - - -def test_lineageweave_caller_is_covered_by_the_focused_quality_gate() -> None: - """Caller, evidence, and regression test all trigger the focused gate.""" - quality = _read(QUALITY_WORKFLOW) - - assert quality.count(str(CALLER)) == 2 - assert quality.count(str(DOCTORING)) == 2 - assert quality.count("tests/test_lineageweave_hourly_review_caller.py") == 3 - - -def test_lineageweave_doctoring_keeps_product_and_review_claims_separate() -> None: - """The evidence record states what this caller can and cannot prove.""" - doctoring = _read(DOCTORING).lower() - - for contract in ( - "stacked pull requests", - "independent current-head approval", - "does not create product work", - "contextual-orchestrator", - "copilot_github_token", - "apa 7th references", - ): - assert contract in doctoring diff --git a/tests/test_nonnest2_hourly_review_caller.py b/tests/test_nonnest2_hourly_review_caller.py deleted file mode 100644 index 0830c08704..0000000000 --- a/tests/test_nonnest2_hourly_review_caller.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Contract tests for nonnest2's bounded hourly review-repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/nonnest2-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/nonnest2-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") -SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - return path.read_text(encoding="utf-8") - - -def _yaml_path_entries(block: str) -> set[str]: - """Return dashed YAML path entries from one trigger or compileall block.""" - entries: set[str] = set() - for raw_line in block.splitlines(): - stripped = raw_line.strip() - if stripped.startswith("- "): - entries.add(stripped[2:].strip()) - elif stripped.startswith("tests/") or stripped.startswith("scripts/"): - entries.add(stripped.rstrip(" \\")) - return entries - - -def _trigger_path_block(quality: str, trigger: str) -> str: - """Return the dashed path list under one named workflow trigger.""" - marker = f" {trigger}:\n paths:\n" - start = quality.index(marker) + len(marker) - lines: list[str] = [] - for line in quality[start:].splitlines(): - if line.startswith(" - "): - lines.append(line) - continue - if line.strip() == "": - continue - break - return "\n".join(lines) - - -def _compileall_block(quality: str) -> str: - """Return the compileall argument list from the focused quality job.""" - marker = "python -m compileall -q \\" - start = quality.index(marker) - remainder = quality[start:] - end = remainder.find("\n git ") - return remainder if end < 0 else remainder[:end] - - -def test_nonnest2_caller_is_hourly_bounded_and_non_cancelling() -> None: - """nonnest2 receives one realistic Vuong-test repair without cancellation.""" - caller = _read(CALLER) - - assert 'cron: "16 * * * *"' in caller - assert "group: nonnest2-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/nonnest2" in caller - assert "base_branch: master" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_nonnest2_caller_preserves_oidc_and_explicit_secret_scope() -> None: - """The queue scanner maps established credentials without model secrets.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert ( - "\n permissions:\n contents: read\n id-token: write\n" - in jobs_scope - ) - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_nonnest2_target_is_not_hard_coded_in_shared_scheduler() -> None: - """Product identity remains in the thin caller rather than the engine.""" - assert "ContextualWisdomLab/nonnest2" not in _read(SCHEDULER) - - -def test_nonnest2_doctoring_records_vuong_activation_and_credentials() -> None: - """Operators retain target-allowlist, Vuong tests, and approval prerequisites.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "ContextualWisdomLab/nonnest2", - "OPENCODE_REPOSITORY_DISPATCH_TARGETS", - "independent non-author approval", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "id-token: write", - "two-hour same-head retry floor", - "root-cause analysis", - "remediation feasibility", - "protected-master operational acceptance", - "APA 7th references", - "ContextualWisdomLab/nonnest2#89", - "ContextualWisdomLab/nonnest2#86", - "ContextualWisdomLab/nonnest2#84", - "ContextualWisdomLab/nonnest2#90", - ): - assert phrase in doctoring - - -def test_path_block_helpers_keep_trigger_and_compileall_sets_disjoint() -> None: - """A path listed only under push or compileall must not satisfy pull_request.""" - quality = ( - "on:\n" - " pull_request:\n" - " paths:\n" - " - .github/workflows/nonnest2-hourly-review-repair.yml\n" - " push:\n" - " paths:\n" - " - docs/doctoring/nonnest2-hourly-review-caller.md\n" - " python -m compileall -q \\\n" - " tests/test_nonnest2_hourly_review_caller.py\n" - " git diff --check\n" - ) - - pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) - push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) - compileall_paths = _yaml_path_entries(_compileall_block(quality)) - - assert pull_request_paths == {".github/workflows/nonnest2-hourly-review-repair.yml"} - assert push_paths == {"docs/doctoring/nonnest2-hourly-review-caller.md"} - assert compileall_paths == {"tests/test_nonnest2_hourly_review_caller.py"} - assert "docs/doctoring/nonnest2-hourly-review-caller.md" not in pull_request_paths - assert ".github/workflows/nonnest2-hourly-review-repair.yml" not in compileall_paths - - -def test_focused_quality_workflow_tracks_nonnest2_contracts() -> None: - """Caller, test, and doctoring edits always rerun the focused gate.""" - quality = _read(QUALITY_WORKFLOW) - pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) - push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) - compileall_paths = _yaml_path_entries(_compileall_block(quality)) - caller = ".github/workflows/nonnest2-hourly-review-repair.yml" - doctoring = "docs/doctoring/nonnest2-hourly-review-caller.md" - contract = "tests/test_nonnest2_hourly_review_caller.py" - - assert caller in pull_request_paths - assert doctoring in pull_request_paths - assert contract in pull_request_paths - assert caller in push_paths - assert doctoring in push_paths - assert contract in push_paths - assert contract in compileall_paths - assert caller not in compileall_paths - assert doctoring not in compileall_paths diff --git a/tests/test_orgmetra_hourly_review_caller.py b/tests/test_orgmetra_hourly_review_caller.py deleted file mode 100644 index 9b5b85f485..0000000000 --- a/tests/test_orgmetra_hourly_review_caller.py +++ /dev/null @@ -1,105 +0,0 @@ -"""Contract tests for Orgmetra's bounded hourly review-repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/orgmetra-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/orgmetra-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - return path.read_text(encoding="utf-8") - - -def _path_block(quality: str, trigger: str) -> set[str]: - """Return the path entries under one focused workflow trigger.""" - marker = f" {trigger}:\n paths:\n" - start = quality.index(marker) + len(marker) - entries: set[str] = set() - for line in quality[start:].splitlines(): - stripped = line.strip() - if not stripped: - continue - if not stripped.startswith("-"): - break - entries.add(stripped[1:].strip()) - return entries - - -def test_orgmetra_caller_is_hourly_bounded_and_non_cancelling() -> None: - """Orgmetra receives one protected-develop repair opportunity per heartbeat.""" - caller = _read(CALLER) - - assert 'cron: "58 * * * *"' in caller - assert "group: orgmetra-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/Orgmetra" in caller - assert "base_branch: develop" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_orgmetra_caller_keeps_scheduler_credentials_explicit() -> None: - """The queue scanner receives only its established scheduler credentials.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_orgmetra_doctoring_records_runtime_and_governance_bounds() -> None: - """Operators retain the product, HCM, provider, and approval boundaries.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "ContextualWisdomLab/Orgmetra", - "protected develop", - "root-cause analysis", - "remediation feasibility", - "two-hour same-head retry floor", - "contextual-orchestrator", - "automatic model discovery", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "independent non-author approval", - "APA 7th references", - ): - assert phrase in doctoring - assert "protected\nprotected" not in doctoring - - -def test_focused_quality_workflow_tracks_orgmetra_contracts() -> None: - """Caller, test, and doctoring edits stay inside the focused quality gate.""" - quality = _read(QUALITY_WORKFLOW) - caller = ".github/workflows/orgmetra-hourly-review-repair.yml" - doctoring = "docs/doctoring/orgmetra-hourly-review-caller.md" - contract = "tests/test_orgmetra_hourly_review_caller.py" - - for trigger in ("pull_request", "push"): - paths = _path_block(quality, trigger) - assert caller in paths - assert doctoring in paths - assert contract in paths - - compileall_start = quality.index("python -m compileall -q \\") - compileall_end = quality.index("git diff --check", compileall_start) - compileall = quality[compileall_start:compileall_end] - assert contract in compileall diff --git a/tests/test_originweave_hourly_review_caller.py b/tests/test_originweave_hourly_review_caller.py deleted file mode 100644 index 11b3353786..0000000000 --- a/tests/test_originweave_hourly_review_caller.py +++ /dev/null @@ -1,166 +0,0 @@ -"""Contract tests for OriginWeave's bounded hourly review-repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/originweave-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/originweave-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") -SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - return path.read_text(encoding="utf-8") - - -def _yaml_path_entries(block: str) -> set[str]: - """Return dashed YAML path entries from one trigger or compileall block.""" - entries: set[str] = set() - for raw_line in block.splitlines(): - stripped = raw_line.strip() - if stripped.startswith("- "): - entries.add(stripped[2:].strip()) - elif stripped.startswith("tests/") or stripped.startswith("scripts/"): - entries.add(stripped.rstrip(" \\")) - return entries - - -def _trigger_path_block(quality: str, trigger: str) -> str: - """Return the dashed path list under one named workflow trigger.""" - marker = f" {trigger}:\n paths:\n" - start = quality.index(marker) + len(marker) - lines: list[str] = [] - for line in quality[start:].splitlines(): - if line.startswith(" - "): - lines.append(line) - continue - if line.strip() == "": - continue - break - return "\n".join(lines) - - -def _compileall_block(quality: str) -> str: - """Return the compileall argument list from the focused quality job.""" - marker = "python -m compileall -q \\" - start = quality.index(marker) - remainder = quality[start:] - end = remainder.find("\n git ") - return remainder if end < 0 else remainder[:end] - - -def test_originweave_caller_is_hourly_bounded_and_non_cancelling() -> None: - """OriginWeave receives one realistic agent-browser repair without cancellation.""" - caller = _read(CALLER) - - assert 'cron: "10 * * * *"' in caller - assert "group: originweave-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/OriginWeave" in caller - assert "base_branch: main" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_originweave_caller_preserves_oidc_and_explicit_secret_scope() -> None: - """The queue scanner maps established credentials without model secrets.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert ( - "\n permissions:\n contents: read\n id-token: write\n" - in jobs_scope - ) - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_originweave_target_is_not_hard_coded_in_shared_scheduler() -> None: - """Product identity remains in the thin caller rather than the engine.""" - assert "ContextualWisdomLab/OriginWeave" not in _read(SCHEDULER) - - -def test_originweave_doctoring_records_browser_activation_and_credentials() -> None: - """Operators retain target-allowlist, browser runtime, and approval prerequisites.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "ContextualWisdomLab/OriginWeave", - "OPENCODE_REPOSITORY_DISPATCH_TARGETS", - "independent non-author approval", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "id-token: write", - "two-hour same-head retry floor", - "root-cause analysis", - "remediation feasibility", - "protected-main operational acceptance", - "APA 7th references", - "ContextualWisdomLab/OriginWeave#175", - "ContextualWisdomLab/OriginWeave#173", - "ContextualWisdomLab/OriginWeave#168", - "ContextualWisdomLab/OriginWeave#166", - ): - assert phrase in doctoring - - -def test_path_block_helpers_keep_trigger_and_compileall_sets_disjoint() -> None: - """A path listed only under push or compileall must not satisfy pull_request.""" - quality = ( - "on:\n" - " pull_request:\n" - " paths:\n" - " - .github/workflows/originweave-hourly-review-repair.yml\n" - " push:\n" - " paths:\n" - " - docs/doctoring/originweave-hourly-review-caller.md\n" - " python -m compileall -q \\\n" - " tests/test_originweave_hourly_review_caller.py\n" - " git diff --check\n" - ) - - pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) - push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) - compileall_paths = _yaml_path_entries(_compileall_block(quality)) - - assert pull_request_paths == {".github/workflows/originweave-hourly-review-repair.yml"} - assert push_paths == {"docs/doctoring/originweave-hourly-review-caller.md"} - assert compileall_paths == {"tests/test_originweave_hourly_review_caller.py"} - assert "docs/doctoring/originweave-hourly-review-caller.md" not in pull_request_paths - assert ".github/workflows/originweave-hourly-review-repair.yml" not in compileall_paths - - -def test_focused_quality_workflow_tracks_originweave_contracts() -> None: - """Caller, test, and doctoring edits always rerun the focused gate.""" - quality = _read(QUALITY_WORKFLOW) - pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) - push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) - compileall_paths = _yaml_path_entries(_compileall_block(quality)) - caller = ".github/workflows/originweave-hourly-review-repair.yml" - doctoring = "docs/doctoring/originweave-hourly-review-caller.md" - contract = "tests/test_originweave_hourly_review_caller.py" - - assert caller in pull_request_paths - assert doctoring in pull_request_paths - assert contract in pull_request_paths - assert caller in push_paths - assert doctoring in push_paths - assert contract in push_paths - assert contract in compileall_paths - assert caller not in compileall_paths - assert doctoring not in compileall_paths diff --git a/tests/test_pr_review_autofix_nvidia_nim_contract.py b/tests/test_pr_review_autofix_nvidia_nim_contract.py index 750894fe44..4b749d93c4 100644 --- a/tests/test_pr_review_autofix_nvidia_nim_contract.py +++ b/tests/test_pr_review_autofix_nvidia_nim_contract.py @@ -12,9 +12,7 @@ AUTOFIX_WORKFLOW = Path(".github/workflows/pr-review-autofix.yml") FIX_SCHEDULER_WORKFLOW = Path(".github/workflows/pr-review-fix-scheduler.yml") -HOURLY_CALLER_WORKFLOW = Path( - ".github/workflows/clearfolio-hourly-review-repair.yml" -) +HOURLY_CALLER_WORKFLOW = Path(".github/workflows/hourly-review-repair.yml") AUTOMATION_GUIDE = Path("docs/automation/hourly-review-repair.md") DOCTORING_RECORD = Path("docs/doctoring/hourly-nvidia-nim-autofix.md") CHANGELOG = Path("CHANGELOG.md") diff --git a/tests/test_pr_review_fix_hourly_contract.py b/tests/test_pr_review_fix_hourly_contract.py index a31562550c..63e2c1e7c2 100644 --- a/tests/test_pr_review_fix_hourly_contract.py +++ b/tests/test_pr_review_fix_hourly_contract.py @@ -13,7 +13,7 @@ _REUSABLE_WORKFLOW = Path(".github/workflows/pr-review-fix-scheduler.yml") _AUTOFIX_WORKFLOW = Path(".github/workflows/pr-review-autofix.yml") -_CLEARFOLIO_CALLER = Path(".github/workflows/clearfolio-hourly-review-repair.yml") +_CONSOLIDATED_CALLER = Path(".github/workflows/hourly-review-repair.yml") _CONTRACT_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") _AUTOMATION_GUIDE = Path("docs/automation/hourly-review-repair.md") @@ -50,22 +50,40 @@ def _current_head_change_request(body: str) -> dict[str, object]: def test_clearfolio_caller_runs_once_each_hour() -> None: - """Clearfolio receives the requested hourly bounded repair heartbeat.""" - text = _read(_CLEARFOLIO_CALLER) + """Clearfolio receives the requested hourly bounded repair heartbeat. + + The consolidated caller resolves per-repository parameters through a + ``github.event.schedule`` lookup table (see + ``docs/doctoring/hourly-review-repair-single-file-consolidation.md``) + rather than flat ``key: value`` lines, so Clearfolio's values are read + from its JSON literal in that table instead of a bare substring. + """ + text = _read(_CONSOLIDATED_CALLER) assert 'cron: "23 * * * *"' in text assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in text - assert "target_repository: ContextualWisdomLab/clearfolio" in text - assert "base_branch: main" in text + assert '"target_repository":"ContextualWisdomLab/clearfolio"' in text + assert '"base_branch":"main"' in text assert 'max_dispatches: "1"' in text - assert 'retry_hours: "1"' in text + assert '"retry_hours":"1"' in text assert "COPILOT_GITHUB_TOKEN" not in text assert "NVIDIA_NIM_API_KEY" not in text def test_clearfolio_caller_keeps_github_token_read_only() -> None: - """The hourly caller delegates with explicit secrets and no token elevation.""" - text = _read(_CLEARFOLIO_CALLER) + """The hourly caller delegates with explicit secrets and no token elevation. + + The former dedicated Clearfolio file was the sole one of the 18 original + callers that omitted a job-level ``permissions:`` override (it fell back + to the workflow-level ``contents: read`` only, silently withholding + ``id-token: write`` from the reusable scheduler for Clearfolio alone -- + see the consolidation doctoring record). The consolidated file grants + the same ``contents: read`` / ``id-token: write`` job permissions to + every matrix target uniformly, matching the other 17 repositories and + closing that latent gap; this test now checks that the grant stays + narrow (no broader token permission is added) rather than absent. + """ + text = _read(_CONSOLIDATED_CALLER) workflow_scope, jobs_scope = text.split("\njobs:\n", maxsplit=1) assert "\npermissions:\n contents: read\n" in workflow_scope @@ -77,7 +95,7 @@ def test_clearfolio_caller_keeps_github_token_read_only() -> None: "statuses: write", ): assert permission not in text - assert "\n permissions:\n" not in jobs_scope + assert "\n permissions:\n contents: read\n id-token: write\n" in jobs_scope def test_reusable_scheduler_has_no_product_specific_timer() -> None: @@ -98,7 +116,7 @@ def test_reusable_scheduler_has_no_product_specific_timer() -> None: def test_reusable_scheduler_declares_only_required_caller_secrets() -> None: """The caller forwards only established secrets; OIDC supplies the app fallback.""" reusable = _read(_REUSABLE_WORKFLOW) - caller = _read(_CLEARFOLIO_CALLER) + caller = _read(_CONSOLIDATED_CALLER) assert "PR_REVIEW_MERGE_TOKEN:" in reusable assert "OPENCODE_APPROVE_TOKEN:" in reusable @@ -228,7 +246,7 @@ def test_review_fix_scheduler_retries_same_head_after_one_hour() -> None: def test_review_fix_scheduler_remains_bounded_and_single_flight() -> None: """Higher cadence keeps one mutation and supersedes only a stale queue scan.""" reusable = _read(_REUSABLE_WORKFLOW) - caller = _read(_CLEARFOLIO_CALLER) + caller = _read(_CONSOLIDATED_CALLER) dispatch_block = reusable.split("max_dispatches:", maxsplit=1)[1].split( "target_repository:", maxsplit=1 @@ -241,10 +259,10 @@ def test_review_fix_scheduler_remains_bounded_and_single_flight() -> None: def test_contract_workflow_tracks_the_product_caller() -> None: - """Changes to the active Clearfolio caller always rerun the focused gate.""" + """Changes to the consolidated product caller always rerun the focused gate.""" text = _read(_CONTRACT_WORKFLOW) - assert text.count(".github/workflows/clearfolio-hourly-review-repair.yml") == 2 + assert text.count(".github/workflows/hourly-review-repair.yml") == 2 def test_contract_workflow_tracks_scheduler_implementation() -> None: diff --git a/tests/test_quarantine_sandbox_hourly_review_caller.py b/tests/test_quarantine_sandbox_hourly_review_caller.py deleted file mode 100644 index 1755bb5e77..0000000000 --- a/tests/test_quarantine_sandbox_hourly_review_caller.py +++ /dev/null @@ -1,179 +0,0 @@ -"""Contract tests for Quarantine Sandbox Runtime's hourly repair caller.""" - -from pathlib import Path - - -CALLER = Path(".github/workflows/quarantine-sandbox-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/quarantine-sandbox-hourly-review-caller.md") -QUALITY_WORKFLOW = Path(".github/workflows/hourly-nvidia-nim-review-repair.yml") -SCHEDULER = Path(".github/workflows/pr-review-fix-scheduler.yml") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - - return path.read_text(encoding="utf-8") - - -def _yaml_path_entries(block: str) -> set[str]: - """Return dashed YAML path entries from one trigger or compileall block.""" - - entries: set[str] = set() - for raw_line in block.splitlines(): - stripped = raw_line.strip() - if stripped.startswith("- "): - entries.add(stripped[2:].strip()) - elif stripped.startswith("tests/") or stripped.startswith("scripts/"): - entries.add(stripped.rstrip(" \\")) - return entries - - -def _trigger_path_block(quality: str, trigger: str) -> str: - """Return the dashed path list under one named workflow trigger.""" - - marker = f" {trigger}:\n paths:\n" - start = quality.index(marker) + len(marker) - lines: list[str] = [] - for line in quality[start:].splitlines(): - if line.startswith(" - "): - lines.append(line) - continue - if line.strip() == "": - continue - break - return "\n".join(lines) - - -def _compileall_block(quality: str) -> str: - """Return the compileall argument list from the focused quality job.""" - - marker = "python -m compileall -q \\" - start = quality.index(marker) - remainder = quality[start:] - end = remainder.find("\n git ") - return remainder if end < 0 else remainder[:end] - - -def test_caller_is_hourly_bounded_and_non_cancelling() -> None: - """The sandbox receives one bounded security repair without cancellation.""" - - caller = _read(CALLER) - - assert 'cron: "14 * * * *"' in caller - assert "group: quarantine-sandbox-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/quarantine-sandbox-runtime" in caller - assert "base_branch: develop" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_caller_preserves_oidc_and_explicit_secret_scope() -> None: - """The queue scanner maps scheduler credentials without model secrets.""" - - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - - assert "\npermissions:\n contents: read\n" in workflow_scope - assert ( - "\n permissions:\n contents: read\n id-token: write\n" - in jobs_scope - ) - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_target_is_not_hard_coded_in_shared_scheduler() -> None: - """Product identity remains in the thin caller rather than the engine.""" - - assert "ContextualWisdomLab/quarantine-sandbox-runtime" not in _read(SCHEDULER) - - -def test_doctoring_records_security_boundary_and_activation_contract() -> None: - """Operators retain exact target, authority, and activation prerequisites.""" - - doctoring = _read(DOCTORING) - - for phrase in ( - "ContextualWisdomLab/quarantine-sandbox-runtime", - "OPENCODE_REPOSITORY_DISPATCH_TARGETS", - "independent non-author approval", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "id-token: write", - "two-hour same-head retry floor", - "root-cause analysis", - "remediation feasibility", - "protected-main operational acceptance", - "artifact-analysis evidence", - "Wardnet owns WAF/IDS", - "Naruon owns email admission", - "APA 7th references", - ): - assert phrase in doctoring - - -def test_path_helpers_keep_trigger_and_compileall_sets_disjoint() -> None: - """A path listed only under push or compileall must not satisfy PR coverage.""" - - quality = ( - "on:\n" - " pull_request:\n" - " paths:\n" - " - .github/workflows/quarantine-sandbox-hourly-review-repair.yml\n" - " push:\n" - " paths:\n" - " - docs/doctoring/quarantine-sandbox-hourly-review-caller.md\n" - " python -m compileall -q \\\n" - " tests/test_quarantine_sandbox_hourly_review_caller.py\n" - " git diff --check\n" - ) - - pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) - push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) - compileall_paths = _yaml_path_entries(_compileall_block(quality)) - - assert pull_request_paths == { - ".github/workflows/quarantine-sandbox-hourly-review-repair.yml" - } - assert push_paths == { - "docs/doctoring/quarantine-sandbox-hourly-review-caller.md" - } - assert compileall_paths == { - "tests/test_quarantine_sandbox_hourly_review_caller.py" - } - - -def test_focused_quality_workflow_tracks_sandbox_contracts() -> None: - """Caller, test, and doctoring edits always rerun the focused gate.""" - - quality = _read(QUALITY_WORKFLOW) - pull_request_paths = _yaml_path_entries(_trigger_path_block(quality, "pull_request")) - push_paths = _yaml_path_entries(_trigger_path_block(quality, "push")) - compileall_paths = _yaml_path_entries(_compileall_block(quality)) - caller = ".github/workflows/quarantine-sandbox-hourly-review-repair.yml" - doctoring = "docs/doctoring/quarantine-sandbox-hourly-review-caller.md" - contract = "tests/test_quarantine_sandbox_hourly_review_caller.py" - - assert caller in pull_request_paths - assert doctoring in pull_request_paths - assert contract in pull_request_paths - assert caller in push_paths - assert doctoring in push_paths - assert contract in push_paths - assert contract in compileall_paths - assert caller not in compileall_paths - assert doctoring not in compileall_paths diff --git a/tests/test_semantic_data_portal_hourly_review_caller.py b/tests/test_semantic_data_portal_hourly_review_caller.py deleted file mode 100644 index 18cef1cdde..0000000000 --- a/tests/test_semantic_data_portal_hourly_review_caller.py +++ /dev/null @@ -1,114 +0,0 @@ -"""Contract tests for the semantic-data-portal bounded hourly review-repair caller.""" - -import re -from pathlib import Path - - -CALLER = Path(".github/workflows/semantic-data-portal-hourly-review-repair.yml") -DOCTORING = Path("docs/doctoring/semantic-data-portal-hourly-review-caller.md") - - -def _read(path: Path) -> str: - """Return one repository contract file as UTF-8 text.""" - return path.read_text(encoding="utf-8") - - -def _permission_map(caller: str, header: str) -> dict[str, str]: - """Parse one exact YAML permission block without widening test dependencies.""" - lines = caller.splitlines() - header_index = lines.index(header) - entry_indent = len(header) - len(header.lstrip()) + 2 - permissions: dict[str, str] = {} - for line in lines[header_index + 1 :]: - if not line.strip(): - continue - indent = len(line) - len(line.lstrip()) - if indent < entry_indent: - break - if indent != entry_indent: - continue - key, separator, value = line.strip().partition(":") - assert separator, f"malformed permission entry: {line!r}" - permissions[key] = value.strip() - return permissions - - -def test_semantic_data_portal_caller_is_hourly_bounded_and_non_cancelling() -> None: - """The portal receives one realistic repair opportunity without overlap cancellation.""" - caller = _read(CALLER) - - assert 'cron: "59 * * * *"' in caller - assert "group: semantic-data-portal-hourly-review-repair" in caller - assert "cancel-in-progress: false" in caller - assert "uses: ./.github/workflows/pr-review-fix-scheduler.yml" in caller - assert "target_repository: ContextualWisdomLab/semantic-data-portal" in caller - assert "base_branch: main" in caller - assert 'max_prs: "50"' in caller - assert 'max_dispatches: "1"' in caller - assert 'retry_hours: "2"' in caller - - -def test_semantic_data_portal_caller_preserves_credentials_and_read_only_token_scope() -> None: - """The queue scanner maps established credentials without exposing model secrets.""" - caller = _read(CALLER) - workflow_scope, jobs_scope = caller.split("\njobs:\n", maxsplit=1) - - assert _permission_map(workflow_scope, "permissions:") == {"contents": "read"} - assert _permission_map(jobs_scope, " permissions:") == { - "contents": "read", - "id-token": "write", - } - assert "PR_REVIEW_MERGE_TOKEN: ${{ secrets.PR_REVIEW_MERGE_TOKEN }}" in caller - assert "OPENCODE_APPROVE_TOKEN: ${{ secrets.OPENCODE_APPROVE_TOKEN }}" in caller - assert "secrets: inherit" not in caller - assert "NVIDIA_NIM_API_KEY" not in caller - assert "COPILOT_GITHUB_TOKEN" not in caller - for forbidden in ( - "actions: write", - "contents: write", - "issues: write", - "pull-requests: write", - "statuses: write", - ): - assert forbidden not in caller - - -def test_semantic_data_portal_caller_cron_avoids_other_callers() -> None: - """Minute 59 does not collide with any other product caller heartbeat.""" - caller = _read(CALLER) - assert '- cron: "59 * * * *"' in caller - other_minutes = { - minute - for path in Path(".github/workflows").glob("*hourly-review-repair.yml") - if path != CALLER - for minute in re.findall(r'cron:\s*["\'](\d+) \* \* \* \*["\']', _read(path)) - } - assert "59" not in other_minutes - - -def test_semantic_data_portal_caller_doctoring_records_rca_feasibility_and_latency() -> None: - """Operators retain the exact rationale for the bounded two-hour retry policy.""" - doctoring = _read(DOCTORING) - - for phrase in ( - "root-cause analysis", - "remediation feasibility", - "two-hour same-head retry floor", - "exact-head", - "cancel-in-progress: false", - "NVIDIA_NIM_API_KEY", - "COPILOT_GITHUB_TOKEN", - "PR_REVIEW_MERGE_TOKEN", - "OPENCODE_APPROVE_TOKEN", - "ContextualWisdomLab/semantic-data-portal", - "minute 59", - ): - assert phrase in doctoring, phrase - - for reference in ( - "https://docs.github.com/en/actions/how-tos/write-workflows/choose-when-workflows-run/control-workflow-concurrency", - "https://docs.github.com/en/actions/reference/workflows-and-actions/events-that-trigger-workflows#schedule", - "https://docs.github.com/en/actions/how-tos/sharing-automations/reusing-workflows", - "https://doi.org/10.6028/NIST.SP.800-218", - ): - assert reference in doctoring, reference