# End-to-end PR reviewer

Automate your analytics engineering code reviews with a DinoAI agent that acts like a thorough senior reviewer — reading the PR diff, checking the linked Linear issue, verifying test and documentation coverage, and posting a structured verdict directly to Slack.

{% hint style="info" icon="compass" %}

#### Before You Start

**Paradime**

* Your Paradime API endpoint, API key, and API secret. — You can generate under [Workspce Settings → API](/app-help/developers/generate-api-keys.md). Make sure to have `DinoAI agent API` capabilities. Requires Admin access to generate API keys.

**GitHub**

* Write access to the repository you want to review PRs on
* Ability to add repository secrets and create GitHub Actions workflows

**Integrations**

The following must already be connected in Paradime:

* [**Linear**](/app-help/integrations/linear.md) — the agent calls `get_linear_issue` to fetch linked tickets
* [**Slack**](/app-help/integrations/slack.md) — the agent posts reviews to `#pr-reviews` via `post_slack_message`
  {% endhint %}

{% hint style="info" %}
Linear is recommended but not strictly required. If the PR description has no Linear link, the agent will note it under SCOPE and continue with the rest of the review.
{% endhint %}

## What You'll Build

By the end of this guide you'll have:

* A DinoAI agent configured to review every pull request end-to-end
* A Python driver script that extracts PR context and triggers the agent
* A GitHub Actions workflow that runs automatically on every PR open or update
* A structured Slack review posted to `#pr-reviews` with a clear APPROVE / REQUEST\_CHANGES / REJECT verdict

### What the Review Looks Like

Once triggered, the agent posts a single structured message to `#pr-reviews` in Slack:

```
SCOPE   — does the PR match the Linear ticket? (PASS / PARTIAL / FAIL)
CODE    — correctness, naming, ref/source usage
TESTS   — coverage on new or changed models
DOCS    — descriptions on model + columns
IMPACT  — downstream models / exposures touched
VERDICT — APPROVE / REQUEST_CHANGES / REJECT
```

The agent always cites the file and line number for every issue it raises. It will never return `APPROVE` if either `TESTS` or `DOCS` is `FAIL`.

<figure><img src="/files/R90y0D7bXUSKyjK4MNm2" alt=""><figcaption></figcaption></figure>

## How It Works

When a PR is opened or updated, GitHub Actions runs a Python script that collects the PR title, description, changed files, and any linked Linear issue. That context is handed to the DinoAI agent in a single trigger message. The agent then:

1. Reads every changed `.sql` and `.yml` file
2. Fetches the linked Linear ticket and checks the PR delivers what was asked for
3. Verifies test coverage on new or modified models
4. Checks documentation completeness in schema YAML
5. Flags any breaking-change risk for downstream consumers
6. Posts one structured review to the PR's Slack thread

{% stepper %}
{% step %}

### Create the Agent YAML

Create the following file in your repository at `.dinoai/agents/pr-reviewer-e2e.yml`. This defines the agent's role, goal, tools, and Slack output channel.

{% code title=".dinoai/agents/pr-reviewer-e2e.yml" lineNumbers="true" %}

```yaml
name: pr-reviewer-e2e
version: 1

role: >
  Senior Analytics Engineering Reviewer responsible for end-to-end PR review:
  spec alignment, code quality, tests, docs, and downstream impact.

goal: >
  For the PR referenced in the trigger message:
  (1) read the PR title and description,
  (2) call get_linear_issue to fetch the linked ticket and verify the PR
      delivers what the ticket asked for,
  (3) read every changed .sql and .yml file in the diff,
  (4) check test coverage on new or modified models,
  (5) check documentation completeness in schema YAML,
  (6) flag any breaking-change risk on downstream consumers.
  Post a single structured review to the PR Slack thread under the headings:
  SCOPE / CODE / TESTS / DOCS / IMPACT / VERDICT.

backstory: >
  You are thorough but never noisy. You always cite the file and line for
  any issue you flag. If the PR description has no Linear link, say so in
  SCOPE and continue. You never approve a PR where TESTS or DOCS is FAIL.

tools:
  mode: allowlist
  list:
    - read_file
    - search_files_and_directories
    - ripgrep_search
    - get_linear_issue
    - run_sql_query
    - post_slack_message

slack:
  channel: "#pr-reviews"
```

{% endcode %}

{% hint style="info" %}
The `tools.mode: allowlist` setting means the agent can only use the tools explicitly listed. This keeps the agent focused and prevents unintended actions.
{% endhint %}

{% hint style="info" %}
The Slack channel is set to `#pr-reviews` by default. Update the `slack.channel` value if your team uses a different channel before committing this file.
{% endhint %}
{% endstep %}

{% step %}

### Create the Driver Script

Create the Python script at `scripts/pr_review_e2e.py`. This script runs inside GitHub Actions and is responsible for:

* Reading the PR event payload from GitHub
* Collecting the list of changed files via `git diff`
* Extracting any linked Linear issue ID from the PR description
* Assembling a trigger message and handing it to the DinoAI agent

{% code title="scripts/pr\_review\_e2e.py" lineNumbers="true" %}

```python
import os
import re
import json
import subprocess
import urllib.request

from paradime import Paradime

paradime = Paradime(
    api_endpoint=os.environ["PARADIME_API_ENDPOINT"],
    api_key=os.environ["PARADIME_API_KEY"],
    api_secret=os.environ["PARADIME_API_SECRET"],
)

# 1. Read the PR event payload that GitHub Actions writes to disk
with open(os.environ["GITHUB_EVENT_PATH"]) as f:
    event = json.load(f)

pr = event["pull_request"]
pr_number = pr["number"]
pr_title = pr["title"]
pr_body = pr["body"] or ""
pr_url = pr["html_url"]
base_sha = pr["base"]["sha"]
head_sha = pr["head"]["sha"]

# 2. Get the list of changed files
changed = subprocess.check_output(
    ["git", "diff", "--name-only", f"{base_sha}...{head_sha}"]
).decode().splitlines()

# 3. Extract Linear issue id from the PR body (e.g. "Closes DATA-417")
linear_match = re.search(r"\b([A-Z]{2,10}-\d+)\b", pr_body)
linear_id = linear_match.group(1) if linear_match else None

# 4. Build the trigger message
files_block = "\n".join(f"  - {f}" for f in changed) or "  (no files)"
linear_block = (
    f"Linked Linear issue: {linear_id}. Call get_linear_issue to read it."
    if linear_id
    else "No Linear issue link found in the PR description."
)

message = f"""Review PR #{pr_number}: {pr_title}
URL: {pr_url}

PR description:
\"\"\"
{pr_body}
\"\"\"

{linear_block}

Changed files:
{files_block}

Read each changed file with read_file, verify the PR delivers what the ticket
asked for, and post your review to the PR Slack thread under SCOPE / CODE /
TESTS / DOCS / IMPACT / VERDICT.
"""

result = paradime.dinoai_agents.trigger_run(
    agent="pr-reviewer-e2e",
    message=message,
)
print(f"Started review session: {result.agent_session_id}")

# 5. Post the session id back to the PR as a comment
gh_token = os.environ.get("GITHUB_TOKEN")
if gh_token:
    repo = os.environ["GITHUB_REPOSITORY"]
    body = f"DinoAI review started — session `{result.agent_session_id}` (see #pr-reviews)."
    req = urllib.request.Request(
        f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
        data=json.dumps({"body": body}).encode(),
        headers={
            "Authorization": f"Bearer {gh_token}",
            "Accept": "application/vnd.github+json",
        },
        method="POST",
    )
    urllib.request.urlopen(req).read()
```

{% endcode %}

{% hint style="info" %}
The script posts the DinoAI session ID back to the PR as a comment so your team can track the review. Remove step 5 if you prefer Slack-only updates.
{% endhint %}
{% endstep %}

{% step %}

### Add Your Paradime Credentials to GitHub Secrets

The driver script authenticates with Paradime using three values. Add the following as GitHub Actions secrets in your repository under **Settings → Secrets and variables → Actions**:

* `PARADIME_API_KEY`
* `PARADIME_API_SECRET`
* `PARADIME_API_ENDPOINT`

<div data-with-frame="true"><figure><img src="/files/sjX3gJXiHEMSElrVl9oS" alt=""><figcaption></figcaption></figure></div>
{% endstep %}

{% step %}

### Create the GitHub Actions Workflow

Create the workflow file at `.github/workflows/pr-reviewer-e2e.yml`. This triggers the driver script automatically whenever a PR is opened, updated, or marked as ready for review.

{% code title=".github/workflows/pr-reviewer-e2e.yml" lineNumbers="true" %}

```yaml
name: DinoAI end-to-end PR review

on:
  pull_request:
    types: [opened, synchronize, ready_for_review]

permissions:
  pull-requests: write
  contents: read

jobs:
  review:
    runs-on: ubuntu-latest
    timeout-minutes: 35
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0   # need history for git diff between base and head

      - uses: actions/setup-python@v5
        with:
          python-version: "3.11"

      - name: Install Paradime SDK
        run: pip install paradime-io

      - name: Run end-to-end PR reviewer
        env:
          PARADIME_API_ENDPOINT:  ${{ secrets.PARADIME_API_ENDPOINT }}
          PARADIME_API_KEY: ${{ secrets.PARADIME_API_KEY }}
          PARADIME_API_SECRET: ${{ secrets.PARADIME_API_SECRET }}
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: python scripts/pr_review_e2e.py
```

{% endcode %}
{% endstep %}
{% endstepper %}

### Optional: Block the Workflow on a REJECT Verdict

By default the workflow fires and forgets. If you want the GitHub Actions job to fail when the agent rejects the PR, replace `trigger_run` with `trigger_run_and_wait` in the driver script:

```python
run = paradime.dinoai_agents.trigger_run_and_wait(
    agent="pr-reviewer-e2e",
    message=message,
    timeout=1800,
)

verdict_line = next(
    (line for line in run.messages[-1].content.splitlines() if line.startswith("VERDICT:")),
    "VERDICT: UNKNOWN",
)
print(verdict_line)

if "REJECT" in verdict_line:
    raise SystemExit("PR rejected by DinoAI reviewer")
```

{% hint style="info" %}
`timeout=1800` allows the agent up to 30 minutes to complete its review. The GitHub Actions workflow has a matching `timeout-minutes: 35` to account for startup overhead.
{% endhint %}

## Related Docs

* [**Programmable Agents — Quick Start** — getting started with DinoAI agents](/app-help/products/dino-ai/programmable-agents/quick-start.md)
* [**Programmable Agents — YAML Configuration** — full reference for agent config options](/app-help/products/dino-ai/programmable-agents/yaml-configuration.md)
* [**Programmable Agents — Tools Reference** — all available tools including `read_file`, `get_linear_issue`, and `post_slack_message`](/app-help/products/dino-ai/programmable-agents/tools-reference.md)
* [**Linear Integration** — connecting Linear to Paradime](/app-help/integrations/linear.md)
* [**Slack Integration** — connecting Slack to Paradime](/app-help/integrations/slack.md)
* [**Paradime API & Credentials** — where to find your API endpoint, key, and secret](/app-help/developers/generate-api-keys.md)


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.paradime.io/app-help/guides-new/porgrammable-agents/end-to-end-pr-reviewer.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
