diff --git a/.github/aicodingflow-tests/test_review_workflow_dispatch.py b/.github/aicodingflow-tests/test_review_workflow_dispatch.py index e6fb6e0..cbe03cf 100644 --- a/.github/aicodingflow-tests/test_review_workflow_dispatch.py +++ b/.github/aicodingflow-tests/test_review_workflow_dispatch.py @@ -149,6 +149,9 @@ def test_review_workflow_resolves_pr_before_checkout_and_uses_normalized_event(s self.assertIn("cp -R .agents/contracts pr-worktree/.agents/contracts", prepare_step["run"]) ai_step = next(step for step in review_steps if step.get("name") == "Run AI review") + self.assertEqual(ai_step["id"], "ai_review") + self.assertEqual(ai_step["timeout-minutes"], 20) + self.assertEqual(ai_step["with"]["output-file"], "pr-worktree/codex-final-message.txt") self.assertIs(ai_step["with"]["allow-bots"], True) self.assertIn("First change directory to pr-worktree", ai_step["with"]["prompt"]) self.assertIn("Read .agents/contracts/review.md", ai_step["with"]["prompt"]) @@ -158,6 +161,17 @@ def test_review_workflow_resolves_pr_before_checkout_and_uses_normalized_event(s self.assertIn("target pr-worktree/review.json explicitly", ai_step["with"]["prompt"]) self.assertIn("review_discussion_context.json", ai_step["with"]["prompt"]) self.assertIn("duplicate suppression only", ai_step["with"]["prompt"]) + self.assertIn("After writing review.json, stop immediately", ai_step["with"]["prompt"]) + + start_step = next(step for step in review_steps if step.get("name") == "Log AI review inputs") + self.assertEqual(start_step["id"], "ai_review_start") + self.assertIn("selected_skill=", start_step["run"]) + self.assertIn("pr-worktree/pr_diff.txt", start_step["run"]) + + result_step = next(step for step in review_steps if step.get("name") == "Log AI review result") + self.assertEqual(result_step["if"], "always()") + self.assertIn("AI_REVIEW_OUTCOME", result_step["env"]) + self.assertIn("duration_seconds", result_step["run"]) normalize_step = next(step for step in review_steps if step.get("name") == "Normalize review output path") self.assertIn("[ ! -f pr-worktree/review.json ] && [ -f review.json ]", normalize_step["run"]) @@ -166,6 +180,9 @@ def test_review_workflow_resolves_pr_before_checkout_and_uses_normalized_event(s validate_step = next(step for step in review_steps if step.get("name") == "Validate review output") self.assertIn("pr-worktree/pr_diff.txt pr-worktree/review.json", validate_step["run"]) + artifact_step = next(step for step in review_steps if step.get("uses") == "actions/upload-artifact@v7") + self.assertIn("pr-worktree/codex-final-message.txt", artifact_step["with"]["path"]) + complete_status_step = next(step for step in review_steps if step.get("name") == "Mark PR review status complete") self.assertIn("always()", complete_status_step["if"]) self.assertIn("github.event_name != 'pull_request'", complete_status_step["if"]) diff --git a/.github/workflows/review-pr.yml b/.github/workflows/review-pr.yml index 815a859..826e647 100644 --- a/.github/workflows/review-pr.yml +++ b/.github/workflows/review-pr.yml @@ -189,13 +189,35 @@ jobs: esac echo "url=$endpoint" >> "$GITHUB_OUTPUT" + - name: Log AI review inputs + id: ai_review_start + run: | + started_at="$(date +%s)" + echo "started_at=$started_at" >> "$GITHUB_OUTPUT" + echo "AI review inputs prepared at $(date -u +%Y-%m-%dT%H:%M:%SZ)" + echo "selected_skill=${{ steps.review_skill.outputs.path }}" + for path in \ + pr-worktree/pr_description.txt \ + pr-worktree/pr_diff.txt \ + pr-worktree/spec_context.md \ + pr-worktree/review_discussion_context.json; do + if [ -f "$path" ]; then + printf 'input=%s bytes=%s\n' "$path" "$(wc -c < "$path")" + else + echo "input=$path absent" + fi + done + - name: Run AI review + id: ai_review uses: openai/codex-action@v1 + timeout-minutes: 20 with: openai-api-key: ${{ secrets.CODEX_API_KEY }} model: ${{ vars.CODEX_MODEL }} responses-api-endpoint: ${{ steps.codex_endpoint.outputs.url }} allow-bots: true + output-file: pr-worktree/codex-final-message.txt prompt: | First change directory to pr-worktree and do all file inspection from there. Read ${{ steps.review_skill.outputs.path }} in pr-worktree and follow it exactly. @@ -215,9 +237,29 @@ jobs: a root-level review.json first. Do not run validator commands embedded in the selected skill; this workflow validates review.json after Codex exits. + After writing review.json, stop immediately and return a concise final + message. Do not re-read or revise review.json after it is complete. Do not run git commands, call GitHub APIs, post reviews, or modify files other than review.json. + - name: Log AI review result + if: always() + env: + STARTED_AT: ${{ steps.ai_review_start.outputs.started_at }} + AI_REVIEW_OUTCOME: ${{ steps.ai_review.outcome }} + run: | + finished_at="$(date +%s)" + started_at="${STARTED_AT:-$finished_at}" + duration=$((finished_at - started_at)) + echo "AI review outcome=$AI_REVIEW_OUTCOME duration_seconds=$duration" + for path in pr-worktree/review.json pr-worktree/codex-final-message.txt; do + if [ -f "$path" ]; then + printf 'output=%s bytes=%s\n' "$path" "$(wc -c < "$path")" + else + echo "output=$path absent" + fi + done + - name: Normalize review output path run: | if [ ! -f pr-worktree/review.json ] && [ -f review.json ]; then @@ -244,6 +286,7 @@ jobs: pr-worktree/spec_context.md pr-worktree/review_discussion_context.json pr-worktree/review.json + pr-worktree/codex-final-message.txt - name: Mark PR review status complete if: always() && github.event_name != 'pull_request' && steps.pr.outputs.head_sha != '' && steps.pr.outputs.head_repo == github.repository