> ## Documentation Index
> Fetch the complete documentation index at: https://docs.paradime.io/llms.txt
> Use this file to discover all available pages before exploring further.

# End-to-end PR reviewer (Jira)

> DinoAI agent that checks PR scope against Jira, validates dbt™ code, tests, and docs, then posts a verdict via a GitHub Action that gates merges.

The end-to-end PR reviewer is a DinoAI agent that acts like a thorough senior reviewer: it checks out the PR branch and reads the diff with the [Terminal Tool](/products/dino-ai/tools-and-features/terminal-tool), checks the linked Jira issue, verifies test and documentation coverage, traces downstream impact through `get_lineage` and `get_exposures`, and posts a single structured verdict (APPROVE / REQUEST\_CHANGES / REJECT) to Slack. A GitHub Actions workflow triggers the agent on every PR and fails the check when the verdict is `REJECT`, so the review gates the merge.

<Info>
  This is the Jira variant of the [end-to-end PR reviewer](/guides/programmable-agents/end-to-end-pr-reviewer). If your team tracks work in Linear, follow that guide instead: the two differ only in the ticket tool (`get_jira_issue` vs `get_linear_issue`) and the ticket wording in the agent and trigger script.
</Info>

<Note>
  **Prerequisites**

  * [Jira connected](/integrations/jira) (the agent calls `get_jira_issue` to fetch linked tickets). Recommended but not strictly required: if the PR has no Jira link, the agent notes it under SCOPE and continues.
  * [Slack connected](/integrations/slack) (the agent posts reviews to `#pr-reviews` via `post_slack_message`).
  * An [account API key](/developers/api-keys) with the **DinoAI agent API** capability, and admin access to your dbt™ repo on GitHub to add Actions secrets.
  * Familiarity with [programmable agents](/products/dino-ai/programmable-agents/index).

  Estimated time: 20 minutes.
</Note>

## Steps

<Steps>
  <Step title="Create the agent">
    In the **Agent** app, open **Agents**, select **New agent**, and start from the `dbt-model-reviewer` template (or **Start from scratch**). Fill in the builder fields with the content below. See [Build an agent in the UI](/guides/programmable-agents/build-in-the-agent-ui) for a tour of the builder.

    **Name**: `pr-reviewer-e2e`

    **Role**:

    ```text theme={"system"}
    Senior Analytics Engineering Reviewer responsible for end-to-end PR
    review: spec alignment, code quality, tests, docs, and downstream
    impact.
    ```

    **Goal**:

    ```text theme={"system"}
    For the PR described in the trigger message (you are checked out on
    the PR branch):
    (1) read the full diff with run_terminal_command:
        git fetch origin <base-branch> and
        git diff origin/<base-branch>...HEAD,
    (2) call get_jira_issue to fetch the linked ticket and verify the
        PR delivers what the ticket asked for,
    (3) read every changed .sql and .yml file,
    (4) check test coverage on new or modified models,
    (5) check documentation completeness in schema YAML,
    (6) assess breaking-change risk using get_lineage for downstream
        models and get_exposures for dashboards and apps consuming them.
    Post a single structured review to the PR Slack thread under the
    headings: SCOPE / CODE / TESTS / DOCS / IMPACT / VERDICT.
    ```

    **Backstory**:

    ```text theme={"system"}
    You are thorough but never noisy. You always cite the file and line
    for any issue you flag. If the PR description has no Jira link, say
    so in SCOPE and continue. You never approve a PR where TESTS or DOCS
    is FAIL. You are a reviewer, not an author: use the terminal only to
    read (git fetch, git diff, git log), never to commit, push, or
    modify files.
    ```

    **Model**: leave **Auto** selected.

    **Allowed tools** (everything else is refused, see the [Tools Reference](/products/dino-ai/programmable-agents/tools-reference)):

    * `run_terminal_command`
    * `read_file`, `search_files_and_directories`, `ripgrep_search`
    * `get_jira_issue`
    * `get_lineage`, `get_exposures`
    * `run_sql_query`
    * `post_slack_message`

    **Output**: set the Slack channel to `#pr-reviews` (or your team's channel).

    <Info>
      The Copilot-only GitHub PR tools (`read_pull_request`, `list_pull_requests`) are not available to programmable agents, so the agent reads the diff with git via `run_terminal_command` instead: the trigger script passes `base_branch` so the agent pod checks out the PR branch, and the trigger message carries the PR description, base branch, changed-file list, and Jira issue key. `get_lineage` and `get_exposures` ([dbt Discovery tools](/products/dino-ai/tools-and-features/dbt-discovery-tools)) ground the IMPACT section in the real dependency graph, including dashboards, instead of a text search.
    </Info>
  </Step>

  <Step title="Deploy the agent">
    Select **Deploy** and choose where the agent lives: **Just here** saves it to the workspace and makes it live immediately, **Open a pull request** commits it to your repo as `.dinoai/agents/pr-reviewer-e2e.yml` so it is governed as code. Either way, the agent is triggerable by name from the API. See [Creating Agents in the App](/products/dino-ai/programmable-agents/creating-agents-in-the-app).
  </Step>

  <Step title="Store Paradime credentials as GitHub secrets">
    Generate an [account API key](/developers/api-keys) with the **DinoAI agent API** capability. Copy the **API Endpoint** shown when the key is generated, the key itself (it starts with `prdm_cmp_`), and the `workspace_token` of the workspace the agent lives in (see [Company & workspace token](/developers/company-and-workspace-token)).

    In your dbt™ repo on GitHub, go to **Settings** → **Secrets and variables** → **Actions** and add three repository secrets:

    | Secret                   | Value                                                                                                            |
    | ------------------------ | ---------------------------------------------------------------------------------------------------------------- |
    | `PARADIME_API_ENDPOINT`  | The API endpoint shown when you generated the key, e.g. `https://api.paradime.io/api/v1/<company_token>/graphql` |
    | `PARADIME_API_SECRET`    | Your account API key (`prdm_cmp_...`)                                                                            |
    | `PARADIME_WORKSPACE_UID` | The `workspace_token` of the target workspace                                                                    |
  </Step>

  <Step title="Add the GitHub Actions workflow">
    Create `.github/workflows/dinoai-pr-review.yml` in your dbt™ repo. The workflow runs on every PR when it is opened, updated, or marked ready for review:

    ```yaml .github/workflows/dinoai-pr-review.yml theme={"system"}
    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>=6.0.0"

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

    Then add the trigger script as `scripts/pr_review_e2e.py`. It reads the PR event payload, computes the changed-file list and any linked Jira issue, triggers the agent, waits for the run to finish, posts the full review as a PR comment, and exits non-zero on a `REJECT` verdict so the check fails:

    ```python scripts/pr_review_e2e.py [expandable] theme={"system"}
    import json
    import os
    import re
    import subprocess
    import time
    import urllib.request

    from paradime import Paradime
    from paradime.apis.dinoai_agents.exception import DinoaiAgentRunFailedException
    from paradime.apis.dinoai_agents.types import DinoaiAgentRunStatus

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

    # 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"]
    base_ref = pr["base"]["ref"]
    head_ref = pr["head"]["ref"]

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

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

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

    message = f"""Review PR #{pr_number}: {pr_title}
    URL: {pr_url}
    Base branch: {base_ref}
    You are checked out on the PR branch. Read the diff with
    run_terminal_command: git fetch origin {base_ref} and
    git diff origin/{base_ref}...HEAD.

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

    {jira_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 under these exact sections:
    SCOPE / CODE / TESTS / DOCS / IMPACT / VERDICT

    The VERDICT line must be one of:
      VERDICT: APPROVE
      VERDICT: REQUEST_CHANGES
      VERDICT: REJECT
    """

    # 5. Trigger the agent and capture the session ID before blocking
    print(f"Starting DinoAI review for PR #{pr_number}: {pr_title}")

    trigger = paradime.dinoai_agents.trigger_run(
        agent="pr-reviewer-e2e",
        message=message,
        base_branch=head_ref,  # check out the PR branch, not the default branch
    )
    session_id = trigger.agent_session_id
    print(f"Review session started: {session_id}")

    # 6. Block until the agent finishes by polling get_run (up to 30 minutes)
    timeout = 1800
    poll_interval = 10
    start = time.time()

    while True:
        run = paradime.dinoai_agents.get_run(agent_session_id=session_id)

        if run.status == DinoaiAgentRunStatus.COMPLETED:
            print(f"Review session completed: {session_id}")
            break

        if run.status == DinoaiAgentRunStatus.FAILED:
            last = run.messages[-1].content if run.messages else "no messages"
            raise DinoaiAgentRunFailedException(f"Agent run failed. Last message: {last}")

        if time.time() - start > timeout:
            raise TimeoutError(f"Timed out waiting for session {session_id} to complete.")

        print(f"  status: {run.status.value}, waiting {poll_interval}s")
        time.sleep(poll_interval)

    # 7. Extract the full review report from the last agent message
    final_report = run.messages[-1].content if run.messages else "(no report returned)"

    # 8. Parse the VERDICT line
    verdict_line = next(
        (line.strip() for line in final_report.splitlines() if line.strip().startswith("VERDICT:")),
        "VERDICT: UNKNOWN",
    )
    print(verdict_line)

    # 9. Post the full review report as a PR comment
    gh_token = os.environ.get("GITHUB_TOKEN")
    if gh_token:
        repo = os.environ["GITHUB_REPOSITORY"]

        comment_body = (
            f"## 🦕 DinoAI PR Review (session `{session_id}`)\n\n"
            f"{final_report}"
        )

        req = urllib.request.Request(
            f"https://api.github.com/repos/{repo}/issues/{pr_number}/comments",
            data=json.dumps({"body": comment_body}).encode(),
            headers={
                "Authorization": f"Bearer {gh_token}",
                "Accept": "application/vnd.github+json",
                "X-GitHub-Api-Version": "2022-11-28",
            },
            method="POST",
        )
        urllib.request.urlopen(req).read()
        print(f"Review report posted to PR #{pr_number}")

    # 10. Fail the workflow if the agent rejected the PR
    if "REJECT" in verdict_line:
        raise SystemExit("PR rejected by DinoAI reviewer, see the PR comment for details.")
    ```

    <Info>
      Authentication uses an account API key (`api_secret` starting with `prdm_cmp_` plus `workspace_uid`), which requires `paradime-io` 6.0.0 or later. See [Install and authenticate the Paradime Python SDK](/developers/python-sdk/getting-started).
    </Info>

    <Info>
      The issue-key pattern `[A-Z]{2,10}-\d+` matches standard Jira keys like `DATA-417`. If your project keys contain digits (e.g. `TEAM1-42`), widen the pattern to `[A-Z][A-Z0-9]{1,9}-\d+`.
    </Info>
  </Step>

  <Step title="Read the verdict">
    Open a PR. The workflow triggers the agent, and when the run completes the full review lands in two places: as a comment on the PR itself and as a message in `#pr-reviews`. The agent always cites the file and line number for every issue it raises, and it never returns `APPROVE` if either `TESTS` or `DOCS` is `FAIL`. On a `REJECT` verdict the workflow exits non-zero, so the check fails and blocks the merge.

    ```text theme={"system"}
    SCOPE   — does the PR match the Jira 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 (via lineage)
    VERDICT — APPROVE / REQUEST_CHANGES / REJECT
    ```
  </Step>
</Steps>

<Check>
  On a PR with full coverage that matches its Jira ticket, the **DinoAI end-to-end PR review** check passes and the review appears as a PR comment and in `#pr-reviews` with an `APPROVE` verdict. On a PR missing tests or docs, `TESTS` or `DOCS` reads `FAIL` and the verdict is `REQUEST_CHANGES` or `REJECT`, with each issue cited by file and line; a `REJECT` fails the check. If the run never starts or times out, the Actions log shows the session ID to look up in the Agent app. If the PR has no Jira link, `SCOPE` notes it and the rest of the review still runs.
</Check>

## How it works

The workflow fires on every PR open, push, or ready-for-review event. The trigger script reads the PR event payload, diffs base against head for the changed-file list, extracts the Jira issue key from the PR description, and passes all of it in the trigger message, along with `base_branch` so the agent pod checks out the PR branch. The agent then reads the full diff with git via `run_terminal_command`, pulls the ticket's requirements and acceptance criteria with `get_jira_issue` ([Jira Tool](/products/dino-ai/tools-and-features/jira-tool)), and works through the six review dimensions. For IMPACT it walks the dependency graph with `get_lineage` from each changed model and checks `get_exposures` for dashboards and apps that consume them, so "this change breaks the revenue dashboard" is a graph fact, not a guess. When the run completes, the script posts the review as a PR comment and fails the job on a `REJECT` verdict, gating the merge.

### Delegate test-writing (optional)

Instead of just flagging missing tests in the verdict, let the reviewer fix them: add `test-maintainer` to this agent's **Squad** in the builder and `invoke_agent` to its allowed tools. The reviewer then delegates to the [dbt™ test maintainer](/guides/programmable-agents/test-maintainer), which writes and validates the tests and reports back via `notify_parent_session` before the final verdict is composed.

### Run it from Bolt instead (optional)

If you do not want the review to gate the merge, you can skip the GitHub Action and run the same agent natively from Bolt: add a **Run Paradime DinoAI Agent** command to a Bolt schedule with an on-merge trigger, with no API keys to configure. The review then runs after each PR merges and posts to Slack only. See [Run an agent with Bolt](/guides/programmable-agents/run-an-agent-with-bolt).

## Next steps

<CardGroup cols={2}>
  <Card title="End-to-end PR reviewer (Linear)" href="/guides/programmable-agents/end-to-end-pr-reviewer" icon="git-branch">
    The same agent for teams that track work in Linear.
  </Card>

  <Card title="dbt™ test maintainer" href="/guides/programmable-agents/test-maintainer" icon="table">
    Delegate test-writing to this sub-agent from the reviewer.
  </Card>

  <Card title="Jira Tool" href="/products/dino-ai/tools-and-features/jira-tool" icon="ticket">
    Everything the agent can read and write in Jira.
  </Card>

  <Card title="Programmable Agents reference" href="/products/dino-ai/programmable-agents/index" icon="code">
    The agent schema, tools, and API.
  </Card>
</CardGroup>


## Related topics

- [End-to-end PR reviewer](/guides/programmable-agents/end-to-end-pr-reviewer.md)
- [dbt™ test maintainer](/guides/programmable-agents/test-maintainer.md)
- [dbt™ impact assessor](/guides/programmable-agents/impact-assessor.md)
- [dbt™ documentation backfiller](/guides/programmable-agents/doc-backfiller.md)
- [Jira change request agent](/guides/programmable-agents/jira-change-request-agent.md)
