diff --git a/.github/workflows/codex-code-review.yml b/.github/workflows/codex-code-review.yml index ee21680..83a0008 100644 --- a/.github/workflows/codex-code-review.yml +++ b/.github/workflows/codex-code-review.yml @@ -1,5 +1,5 @@ # Generated by python3 ocr/render_workflow.py; edit ocr/ci_client.py. -name: OCR Code Review +name: OCR Dispatch on: pull_request_target: @@ -188,10 +188,16 @@ jobs: def summary(identity, reply): - text = (f"## OCR dispatch accepted\n\nRepository: `{identity['repository']}`; PR #{identity['pull_number']}\n\n" + skipped = reply.get('state') == 'skipped' and reply.get('reason') == 'repository_not_enabled' + heading = 'OCR dispatch skipped' if skipped else 'OCR dispatch accepted' + if skipped: + details = 'Automatic review is disabled for this repository. Dispatch skipped by repository policy.\n' + else: + details = ('Task persisted. The backend owns dispatch, recovery and result publication.\n' + 'The separate ocr-review status reports the review verdict; enforcement is configured in Rulesets.\n') + text = (f"## {heading}\n\nRepository: `{identity['repository']}`; PR #{identity['pull_number']}\n\n" f"Head: `{identity['head_sha']}`\n\n" - 'Task persisted. The backend owns dispatch, recovery and result publication.\n' - 'The separate ocr-review status reports the review verdict; enforcement is configured in Rulesets.\n') + + details) if os.environ.get('GITHUB_STEP_SUMMARY'): with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as output: output.write(text) @@ -218,11 +224,14 @@ jobs: result = verdict(reply) state = reply['state'] if state != last_state: - print('OCR state: ' + state, flush=True) + print('OCR dispatch state: ' + state, flush=True) last_state = state if result is not None: summary(identity, reply) - print('OCR dispatch accepted; the separate ocr-review status reports the review verdict.') + if state == 'skipped' and reply.get('reason') == 'repository_not_enabled': + print('OCR dispatch skipped: repository_not_enabled.') + else: + print('OCR dispatch accepted; the separate ocr-review status reports the review verdict.') return result remaining = deadline - time.monotonic() if remaining <= 0: diff --git a/README.md b/README.md index 8b241da..b98b71d 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ The OCR dispatch entry point for selected public repositories in the organization. Workflow source: `.github/workflows/codex-code-review.yml` on the `main` branch. +The workflow is displayed as `OCR Dispatch`. An authenticated request for an unenrolled repository exits successfully with `repository_not_enabled` and a dispatch-skipped summary. The backend records this policy decision without starting review. + ## Administrator setup 1. In the organization settings, go to **Settings → Rules → Rulesets** and create a **Branch ruleset**, for example, `OCR public repositories`. diff --git a/ocr/ci_client.py b/ocr/ci_client.py index 735144e..a6a4e24 100644 --- a/ocr/ci_client.py +++ b/ocr/ci_client.py @@ -164,10 +164,16 @@ def post(path, payload, token, deadline): def summary(identity, reply): - text = (f"## OCR dispatch accepted\n\nRepository: `{identity['repository']}`; PR #{identity['pull_number']}\n\n" + skipped = reply.get('state') == 'skipped' and reply.get('reason') == 'repository_not_enabled' + heading = 'OCR dispatch skipped' if skipped else 'OCR dispatch accepted' + if skipped: + details = 'Automatic review is disabled for this repository. Dispatch skipped by repository policy.\n' + else: + details = ('Task persisted. The backend owns dispatch, recovery and result publication.\n' + 'The separate ocr-review status reports the review verdict; enforcement is configured in Rulesets.\n') + text = (f"## {heading}\n\nRepository: `{identity['repository']}`; PR #{identity['pull_number']}\n\n" f"Head: `{identity['head_sha']}`\n\n" - 'Task persisted. The backend owns dispatch, recovery and result publication.\n' - 'The separate ocr-review status reports the review verdict; enforcement is configured in Rulesets.\n') + + details) if os.environ.get('GITHUB_STEP_SUMMARY'): with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as output: output.write(text) @@ -194,11 +200,14 @@ def main(): result = verdict(reply) state = reply['state'] if state != last_state: - print('OCR state: ' + state, flush=True) + print('OCR dispatch state: ' + state, flush=True) last_state = state if result is not None: summary(identity, reply) - print('OCR dispatch accepted; the separate ocr-review status reports the review verdict.') + if state == 'skipped' and reply.get('reason') == 'repository_not_enabled': + print('OCR dispatch skipped: repository_not_enabled.') + else: + print('OCR dispatch accepted; the separate ocr-review status reports the review verdict.') return result remaining = deadline - time.monotonic() if remaining <= 0: diff --git a/ocr/render_workflow.py b/ocr/render_workflow.py index 897032d..2066c6b 100644 --- a/ocr/render_workflow.py +++ b/ocr/render_workflow.py @@ -4,7 +4,7 @@ ROOT = Path(__file__).resolve().parents[1] HEADER = '''# Generated by python3 ocr/render_workflow.py; edit ocr/ci_client.py. -name: OCR Code Review +name: OCR Dispatch on: pull_request_target: diff --git a/ocr/test_ci_client.py b/ocr/test_ci_client.py index a453173..ea68712 100644 --- a/ocr/test_ci_client.py +++ b/ocr/test_ci_client.py @@ -29,6 +29,7 @@ class WorkflowTests(unittest.TestCase): def test_embedded_workflow_matches_tested_client(self): self.assertEqual((render_workflow.ROOT / '.github/workflows/codex-code-review.yml').read_text(), render_workflow.rendered()) + self.assertIn('\nname: OCR Dispatch\n', render_workflow.rendered()) def test_base_branch_and_drafts_are_not_filtered(self): self.assertEqual(client.coordinates(EVENT, ENV), IDENTITY) @@ -115,6 +116,23 @@ def test_gateway_401_is_not_retried(self): with self.assertRaises(client.ReviewError): client.post('ocr-ci-submit', {}, 'test', client.time.monotonic() + 60) self.assertEqual(opener.return_value.open.call_count, 1) + def test_unenrolled_repository_skip_ends_dispatch_without_polling(self): + with tempfile.TemporaryDirectory() as directory: + event = Path(directory) / 'event.json'; event.write_text(json.dumps(EVENT)) + summary_file = Path(directory) / 'summary.md' + env = {**ENV, 'GITHUB_EVENT_PATH': str(event), 'OCR_CI_TOKEN': 'test-value', + 'GITHUB_STEP_SUMMARY': str(summary_file)} + ack = reply('skipped', durable=True, contract_version=2, acceptance='accepted', + scheduler='backend', task_id='task-1', reason='repository_not_enabled') + with patch.dict(os.environ, env, clear=True), patch.object(client, 'post', return_value=ack) as post: + with patch.object(client.time, 'sleep') as sleep: + self.assertEqual(client.main(), 0) + self.assertEqual(post.call_count, 1) + self.assertEqual(post.call_args.args[0], 'ocr-ci-submit') + sleep.assert_not_called() + self.assertIn('OCR dispatch skipped', summary_file.read_text()) + self.assertIn('disabled for this repository', summary_file.read_text()) + def test_gateway_503_has_bounded_retries(self): error = urllib.error.HTTPError('https://example.invalid', 503, 'Unavailable', {}, io.BytesIO()) with patch.object(client.urllib.request, 'build_opener') as opener, patch.object(client.time, 'sleep'):