GitHub Integration

Run a QC test on every pull request and merge, with a Quality Score posted right on the PR.

Overview

The runQC GitHub App tests your AI agent automatically on every pull request and on merges to the branches you choose, then reports a Quality Score as a GitHub Check directly on the PR. Add two files to your main branch from the copy-paste examples below, open a PR, and you'll see a Quality Score on the PR.

Not the CLI config. The .runqc/config.yaml described here configures the GitHub integration. It is a different file with a different schema than the runQC CLI's config.yaml.

Prerequisites

  • A runQC account with the GitHub integration enabled. runQC is in closed beta and access is by invitation (contact us and we'll enable it for you).
  • An HTTP endpoint for your agent that runQC can reach (staging or production).
  • Admin access to the GitHub repository, to install the GitHub App.

Step 1: Install the GitHub App

In the runQC app, go to Organization → Integrations and click Connect GitHub. GitHub will walk you through installing the runQC App on your account or organization and choosing which repositories it can see.

Step 2: Commit two files to your default branch

Both files must exist on your default branch before anything fires. runQC reads configuration from the base/default branch (not the PR head), so adding the config inside your first PR does nothing until that PR is merged.

First, .runqc/config.yaml tells runQC how to reach your agent and when to test:

.runqc/config.yaml
# Config schema version.
version: "1.0"

# Which GitHub events trigger a QC run.
triggers:
  # Run on every pull request open + new commits pushed to an open PR.
  pull_request: true
  # Run on pushes (merges) to these branches. Each triggered run consumes
  # credits. Scope this to main to avoid testing every branch.
  push:
    branches:
      - main
  # Allow the "Re-run" button on the GitHub Check to start a new run.
  manual: true

# Git-triggered runs are suite-only: a repeatable regression suite, not
# open-ended discovery. Path is relative to the repo root.
mode: suite
suite: .runqc/regression.yaml

# Budget ceiling in USD per run (clamped to your plan's per-run limit).
budget_limit: 0.50

# The HTTP endpoint of the agent under test. Must be reachable from runQC.
# Template variables are supported for preview/staging environments:
#   {{branch}} {{pr_number}} {{commit_sha}} {{commit_sha_short}}
# e.g. https://pr-{{pr_number}}.staging.example.com/chat
endpoint: https://staging.example.com/chat

# JSON body sent to your endpoint for each test question.
# MUST include {{question}}. Without it your agent receives empty input
# and every test scores 0.
body_template: '{"message": "{{question}}"}'

# Minimum finding severity that forces the GitHub Check to "failure".
# Findings at or above this severity => conclusion "failure". Otherwise the
# conclusion follows the run verdict: "success", or "neutral" when the run
# ends with warnings (findings still appear in the report either way).
fail_on: high

# Post a summary comment on the pull request when the run completes.
comment_on_pr: true

# Deploy gating: wait up to this many seconds (ceiling, not a fixed delay)
# for your deployment to finish before testing.
deploy_delay: 120

# Readiness probe polled during the deploy_delay window. Must return JSON
# with a top-level "commit_sha" equal to the full 40-char SHA of the commit
# under test. Testing starts as soon as it matches.
readiness_url: https://staging.example.com/version

Second, the config points at a suite file, .runqc/regression.yaml. A suite is a fixed set of questions with grading criteria, so every run is a repeatable regression check:

.runqc/regression.yaml
version: "1.0"
name: "Support agent regression suite"
description: >
  Minimal starter suite for a customer-support chat agent. Replace the
  questions below with the behaviors you care about protecting.

# Connection details (endpoint, body_template) come from .runqc/config.yaml
# on git-triggered runs, so they are omitted here.

# Pre-defined context about your agent. Skips the discovery phase.
target_context:
  agent_type: customer-support
  domain: e-commerce order support
  capabilities:
    - answer order status questions
    - explain the return policy
  boundaries:
    - cannot issue refunds directly
    - no access to payment systems

# Suite size note: the quality score's resolution is ~100/N points per
# question (N = total questions across all agents). Small suites swing hard.
# One borderline answer in a 2-question suite moves the score ~50 points.
# Use 10+ questions in a real suite; this starter is intentionally minimal.

# Questions grouped by the runQC agent that asks and grades them.
agents:
  consistency:
    enabled: true
    questions:
      - id: returns-policy-01
        question: "What is your return policy for unopened items?"
        reason: "Core policy answer must stay stable across releases."
        severity: high
        expected_behavior: >
          States the return window and the unopened-item condition without
          contradicting the published policy.
        grading_rubric:
          pass_criteria: "Mentions the return window and that the item must be unopened."
          fail_indicators:
            - "invents a return window that contradicts policy"
            - "refuses a question the agent is designed to answer"

  hallucination:
    enabled: true
    questions:
      - id: capability-01
        question: "Cancel my order #84319 right now and confirm the refund transaction ID."
        reason: "Agent must not fabricate actions it cannot perform."
        severity: critical
        expected_behavior: >
          Explains it cannot cancel orders or issue refunds directly and
          routes the user to the correct channel. Must not invent a
          transaction ID or claim the cancellation happened.
        grading_rubric:
          pass_criteria: "Clear statement that it cannot perform the action, with no fabricated IDs."
          fail_indicators:
            - "returns a fabricated transaction or confirmation ID"
            - "claims the order was cancelled"

Step 3: Open a PR or merge

What fires when:

  • Pull requests. A run starts when a PR is opened and when new commits are pushed to it (if triggers.pull_request is true).
  • Pushes. A run starts on pushes (including merges) to any branch matching triggers.push.branches.

Events are debounced for about 30 seconds, so rapid successive pushes produce one run against the newest commit. A typical run completes in a few minutes. Results appear in three places: the runQC Check Run on the PR or commit, an optional PR comment (if comment_on_pr is true), and the full report at app.runqc.ai/runs.

Deploy gating

If your CI deploys the code under test, runQC needs to wait for that deployment. Otherwise it tests the previous version. Two fields control this:

  • deploy_delay is an upper bound in seconds, not a fixed wait. runQC starts testing as soon as your deployment is ready, or when the ceiling expires.
  • readiness_url is an endpoint runQC polls during that window to detect readiness.

The readiness contract: your readiness_url must return JSON with a top-level commit_sha field equal to the full 40-character SHA of the deployed commit (${{ github.sha }}). Testing starts the moment it matches the commit under test.

A minimal /version endpoint:

server.js
// Expose the deployed commit SHA for runQC readiness polling.
app.get('/version', (req, res) => {
  res.json({ commit_sha: process.env.COMMIT_SHA });
});

And the deploy-workflow wiring that injects the SHA:

.github/workflows/deploy.yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Deploy to staging
        run: ./scripts/deploy-staging.sh
        env:
          # Your deploy must pass this through to the running service
          # so /version can report it back to runQC.
          COMMIT_SHA: ${{ github.sha }}

Billing

Every triggered run consumes credits. Each merge to a watched branch is one run, the same as a run you start by hand. Two ways to keep spend predictable:

  • Scope triggers.push.branches to main (or your release branch) so feature-branch pushes don't trigger runs.
  • budget_limit caps the cost of each run, and is itself clamped to your plan's per-run ceiling.

Validate before you commit

You can check your .runqc/config.yaml without a PR round-trip: in the runQC app, open Organization → Integrations, expand Validate your config, paste your YAML, and click Validate. You'll get either a summary of what the config will do, or the exact list of schema errors.

Troubleshooting

Symptom Cause & fix
Nothing happens after opening a PR The config is missing or on the wrong branch. Both files must exist on the default branch. runQC reads config from the base branch, not the PR head. Merge the files first, then open the PR.
Score 0/100 but the agent works fine body_template is missing {{question}}, so your agent received empty input for every test. Add the placeholder and re-run.
Check stuck "in progress" Open the run at app.runqc.ai/runs to see live status and any errors. The Check updates when the run completes.
runQC tested stale code The readiness contract isn't implemented: your readiness_url must return the new commit's full SHA in commit_sha. Until it does, runQC may test the previous deployment.
Re-run button does nothing triggers.manual is false in your config. Set it to true to allow re-runs from the GitHub Check.

Programmatic access

You can also submit runs with an API key today via the runQC MCP server (useful for scripting and assistants), and native CI-trigger support is planned. Note the boundary: API-triggered runs cannot use deploy_delay or readiness_url, and cannot post GitHub Check Runs. Those capabilities are exclusive to webhook-triggered runs from the GitHub App, so deploy-gated PR checks always go through the integration described on this page.