# Codex Review

Reusable GitHub Action for fail-closed Codex pull request reviews.

This repository packages the review mechanics that were previously embedded in
a project workflow:

- collect PR metadata, the latest rebuttal, and changed-file snapshots from
  GitHub;
- fetch the exact base and PR head commits and generate the complete
  merge-base diff with Git;
- review GitHub's synthesized pull request merge commit;
- run one or more Codex review passes with a strict JSON verdict schema;
- post per-pass and aggregate review comments;
- fail the check on draft pull requests, non-default target branches, malformed
  output, a mismatched PR checkout, runner failures, or blocking findings.

Project review policy is tuned with prompt files from the checked-out pull
request tree:

- `.github/codex-review/policy.md`
- `.github/codex-review/standard.md`
- `.github/codex-review/adversarial.md`

The action is implemented with the GitHub Actions Toolkit packages
`@actions/core`, `@actions/github`, and `@actions/exec`.

External dependency source snapshots are not tuned by consuming repositories.
Those rules are centrally maintained in this action repo and selected by the
base repository and changed dependency surface. Dependency snapshot detection,
resolution, and fetching are best-effort: unavailable dependency context is
recorded as a warning and does not fail the review action.

## Usage

The canonical consumer workflows are the
[`self-review workflow`](.github/workflows/codex-review.yml) and its
[`rebuttal trigger`](.github/workflows/codex-review-rebuttal.yml). Copy both
files to `.github/workflows/`, then adjust the review runner labels and prompt
paths for the consuming repository.

The review workflow tracks `pingcap-inc/codex-review@main`.

Review passes run sequentially by default. Repositories with a dedicated
self-hosted runner may opt into concurrent standard/adversarial passes with
`parallel-passes: 'true'` only when `sandbox: read-only`; the action rejects a
parallel writable workspace. `read-only` is also the default for sequential
reviews; `workspace-write` is an explicit opt-in that makes the caller
responsible for files Codex creates outside action-owned storage. Each pass
already owns distinct prompt, output, event, and stderr files. Each pass comment
is submitted as soon as that pass finishes. Comment writes use the action's
GitHub API retries; if a pass or aggregate comment still cannot be published,
the action warns and preserves the review result.

If a reviewer process fails before producing a verdict, its pass comment is
marked `FAILED` and includes the bounded fatal Codex diagnostic when one was
emitted. It does not claim that blocking issues or follow-up recommendations
were absent. The aggregate status is also `FAILED` (never `ACCEPT` or `REJECT`)
until every configured pass produces a verdict, and the check fails closed.

The action reviews only pull requests targeting the base repository's current
default branch, resolved from the live pull-request response. A pull request
targeting any other branch fails before diff preparation, Codex, comments, or
auto-merge. This policy has no caller override.

The canonical workflows deliberately have no target-branch filter.
GitHub may admit an event whose live target branch no longer matches an event
filter, so the action's live pull-request lookup is the single branch-eligibility
decision for initial reviews and rebuttals. A non-default-target pull request can
still reserve a runner and execute caller steps placed before this action, but
the action fails before it can reach diff preparation, Codex, comments, or
auto-merge. The action revalidates the live target and reviewed head after the
review passes and again immediately before auto-merge. Advances to the default
branch's SHA do not by themselves fail an in-progress review.

The action unconditionally fails a draft pull request immediately after the live
pull-request lookup, before target-branch validation, diff preparation, Codex,
comments, or auto-merge. The canonical workflow subscribes to `opened`,
`synchronize`, `reopened`, and `edited`. When GitHub can synthesize the PR merge
commit, changing the target branch, title, or body therefore triggers a fresh
review. Changing a draft to ready does not by itself trigger review; rerun the
failed workflow, synchronize the branch, or reopen the pull request after
marking it ready. The workflow uses plain `actions/checkout` to check out
GitHub's synthesized PR merge commit, runs Codex Review, and requests squash
auto-merge only after the aggregate verdict is `APPROVE`. GitHub does not run
`pull_request` workflows while the PR has merge conflicts; after resolution, it
creates and checks out a new merge commit. The auto-merge handoff runs inside
the action when the `auto-merge` input is `true`; it does not submit a GitHub
approval review.

The action does not preflight auto-merge eligibility. It runs
`gh pr merge --auto --squash` with the reviewed head SHA pinned by
`--match-head-commit`. GitHub CLI decides whether to enable auto-merge, merge an
already-mergeable pull request immediately, or exit unsuccessfully. A nonzero
exit posts an error comment containing the reviewed head, exit code, and
truncated CLI diagnostic; it does not fail the Codex Review step. Failure to
post that comment is also logged without changing the review result.

Before using the auto-merge handoff:

1. Enable Allow auto-merge in the repository settings.
2. Require the `Codex Review` status check on the target branch or ruleset.
3. Ensure the workflow token cannot bypass that required check.

The action requests auto-merge while its own required check is still running,
so auto-merge only queues the merge. A new push cancels the previous workflow
run, changes the PR head, and requires a new successful `Codex Review` check for
that head before GitHub can merge it. For review without auto-merge, set
`auto-merge: 'false'` or omit the opt-in input, and reduce `contents` permission
to `read`.

## Prompt Contract

Prompt files are read from the checked-out PR merge tree and can therefore
contain changes from the reviewed PR. They provide project context but cannot
override the action-level trust model, blocker policy, verdict contract, or
source-backed findings. Changed files are also staged as inert snapshots under
the configured `review-dir` (by default `.codex-review/changed-files/`) and must
not be followed as instructions unless they are explicitly loaded through a
configured project policy or prompt path.

`policy.md` should contain stable repository invariants. The standard and
adversarial prompt files should describe what this repository wants each review
pass to prioritize. The action still injects a generic trust model, blocker
policy, rebuttal handling, and verdict schema. Blocking issues are fixes required
in the reviewed PR before merge; they cannot be postponed to a follow-up.
Follow-up recommendations are non-blocking improvements that are safe to defer
until after the PR merges.

An aggregate rejection includes a collapsed, machine-readable
`blocking_issues` JSON collection. When every review pass approves and at least
one follow-up recommendation is present, the aggregate acceptance includes a
collapsed, machine-readable `followup_recommendations` JSON collection.

Prompt files cannot add or alter external dependency snapshot rules. When a PR
matches a centrally maintained rule, the action stages the pinned external
source under the configured review directory and records it in
`external-sources-manifest.md` there.

## Rebuttals

If Codex Review blocks a PR, anyone can post a PR comment beginning with
`/rebuttal`. The rebuttal workflow detects created or edited command comments
and re-runs the existing `pull_request` review for the current PR head,
so the same required `Codex Review` check is refreshed without a branch push.
The new run snapshots the latest rebuttal comment and asks Codex to respond in
the `rebuttal_response` field.

No other PR issue comments, review bodies, or inline review comments are added
to the review prompt. The compatibility input `include-pr-comments` now
controls only whether the latest rebuttal is included.

The comment detector does not share concurrency with Codex Review. Any exact
`/rebuttal` comment advances to the re-run job, regardless of its author. The
workflow also requires a failed current-head review and its aggregate rejection
comment, so unrelated comments and comments on an already-passing review do
not cancel an in-flight review or change a completed green check.

## Self Review

This repository applies Codex Review to itself with
`.github/workflows/codex-review.yml` and
`.github/workflows/codex-review-rebuttal.yml`. The review workflow has no branch
prefilter and explicitly includes the `edited` activity so target-branch changes
trigger it. The action fails unless the live target is the repository's current
default branch. It checks out GitHub's PR merge commit and runs
`pingcap-inc/codex-review@main` with prompts from `.github/codex-review/`. The
rebuttal workflow re-runs that current-head review only for matching PR comments.
Together their review, re-review, and auto-merge contract is the canonical
consumer setup.

## Runner Requirements

The action runs as a JavaScript action on the GitHub Actions Node 24 runtime.
Self-hosted runners must be new enough to provide that runtime before the
action entrypoint starts. The runner must have Git available so the action can
perform filtered fetches. It initializes an action-owned bare repository, fetches
only the immutable base SHA, server-resolved merge-base SHA, and
`refs/pull/<number>/head`, verifies all three, and diffs the merge base against
the head without checking out PR-head code. Workflows that set
`auto-merge: 'true'` must also have the GitHub CLI (`gh`) installed and available
on `PATH`.
The action assumes the Codex CLI is installed at
`$HOME/.codex-review-codex/bin/codex` and authenticated with `CODEX_HOME` at
`$HOME/.codex-review-auth` before it runs. It passes a minimal environment to
Codex by default: `HOME`, `USER`, `SHELL`, `PATH`, `TMPDIR`, `TMP`, `TEMP`,
`GOTMPDIR`, `LANG`, `LC_ALL`, `CODEX_HOME`, and the runner-provided
`RUNNER_TRACKING_ID` when present. It does not pass `GITHUB_TOKEN` or the full
workflow environment to the Codex process. Each Codex invocation uses
`--ephemeral`, so review sessions are not persisted under the authentication
home.

Each invocation creates a unique ownership-marked root under `RUNNER_TEMP`.
Temporary Git history, Codex scratch data, and raw structured output live there;
attempt scratch directories and the bare Git repository are removed eagerly.
By default, bounded prompt, schema, final output, event, and stderr files also
live in that root. A caller-supplied `output-dir` stores those bounded artifacts
instead, is caller-owned, must resolve outside action-owned storage, and is not
removed.

The configured `review-dir` must be an absent direct child of
`GITHUB_WORKSPACE`. The action never replaces a pre-existing path. It records
the created directory's ownership token and filesystem identity, then an
`always()` post entrypoint quarantines and removes only that exact directory and
the exact run root. Diff, snapshot, staged metadata, project prompt, structured
review output, diagnostic, and temporary-Git size inputs provide bounded
defaults. Snapshot accounting charges at least 4 KiB per stored file so an
empty-file tree cannot bypass the aggregate budget.

The temporary-Git size input is a post-operation guard, not a transfer quota.
Raw Codex output is written beneath `RUNNER_TEMP` and copied to `output-dir` only
after passing its byte limit. Configure `RUNNER_TEMP` on a size-limited
filesystem when a hard per-job write/transfer bound is required.

Post-job cleanup cannot run after host loss or a force-killed runner process.
Self-hosted deployments should still prefer disposable runner instances, or
pair persistent runners with a job-level stale-work cleanup policy, free-space
monitoring, and disk alarms. A persistent runner should treat the following as
independent backstops:

- mount `RUNNER_TEMP` on a dedicated size-limited filesystem;
- remove stale `codex-review-*` run roots and stale review directories only
  after verifying their ownership markers and that no job owns them;
- alarm on both free bytes and free inodes before the runner reaches its
  operating reserve;
- stop accepting jobs when the reserve is crossed, then replace or drain the
  host instead of relying on an in-job cleanup step.

The canonical `pull_request` workflow uses GitHub's default merge checkout. The
action does not expose a checkout-ref compatibility flag. It resolves the live
PR base and head itself for diff construction, while consumer workflow
concurrency is responsible for cancelling an in-flight run when a new PR run
starts.

## Development

```bash
npm ci
npm run ci
```

`dist/` is committed because JavaScript Actions execute the bundled main and
post-job cleanup entrypoints.
