cd /news/ai-tools/catch-prompt-regressions-in-ci-with-… · home topics ai-tools article
[ARTICLE · art-128228] src=sourcefeed.dev ↗ pub= topic=ai-tools verified=true sentiment=· neutral

Catch Prompt Regressions in CI with promptfoo and GitHub Actions

Promptfoo 0.123.0 can be wired into GitHub Actions via promptfoo/promptfoo-action@v1 to score prompts against fixed test cases on every pull request, failing the PR when an edit breaks tone, policy rules, or output shape. The setup requires Node.js 22.22.0 or newer (24 LTS recommended) and an Anthropic API key, with the eval returning exit code 0 on success, 100 when at least one assertion fails, and 1 for any other error. Deterministic assertions such as icontains, not-icontains, and javascript run free and instantly, while llm-rubric sends output to a pinned grading model for judgment calls.

by read7 min views1 publishedSep 13, 2026
Catch Prompt Regressions in CI with promptfoo and GitHub Actions
Image: Sourcefeed (auto-discovered)

Wire promptfoo into GitHub Actions so a bad prompt edit fails the pull request before it merges.

Mariana Souza

What you'll build #

A prompt regression suite that runs on every pull request: promptfoo scores your prompt against fixed test cases, and a GitHub Actions check fails the PR when an edit breaks tone, policy rules, or output shape. You'll know a prompt change is bad before it merges, not after a customer sees it.

Prerequisites #

Verified against promptfoo 0.123.0 (September 2026), promptfoo/promptfoo-action@v1, and Node.js 24 LTS.

  • Node.js 22.22.0 or newer. promptfoo enforces this in itsengines field; 24 LTS is the safe choice.
  • A GitHub repository where you can add workflows and secrets.
  • An Anthropic API key from the Anthropic Console . The example tests a Claude prompt, but promptfoo supports OpenAI, Google, Azure, and dozens of other providers; only theproviders line changes.
  • Commands are for macOS/Linux. On Windows, run them in WSL.

1. Scaffold the project #

mkdir prompt-ci && cd prompt-ci
git init
mkdir -p prompts .github/workflows
export ANTHROPIC_API_KEY=sk-ant-your-key

There's no install step. npx promptfoo@latest fetches the CLI on demand, which is also how the GitHub Action runs it.

2. Write the prompt and the test suite #

Create prompts/support-reply.txt. The rules in it are exactly what CI will defend:

You are a support agent for Acme Cloud. Write a reply to the customer ticket below.

Rules:
- Keep the reply under 120 words.
- Never promise a refund. Refund requests inside the 14-day window go to the billing team.
- Never mention internal tools, ticket queues, or escalation policies.

Ticket:
{{ticket}}

The {{ticket}} placeholder is a template variable that each test case fills in.

Now create promptfooconfig.yaml in the repo root:

description: Support reply prompt regression suite

prompts:
  - file://prompts/support-reply.txt

providers:
  - id: anthropic:messages:claude-opus-5
    config:
      max_tokens: 1024

defaultTest:
  options:
    provider: anthropic:messages:claude-opus-5

tests:
  - description: angry refund request
    vars:
      ticket: "I bought the Pro plan three days ago and it broke my deploy pipeline. I want my money back NOW."
    assert:
      - type: icontains
        value: billing
      - type: not-icontains
        value: I guarantee
      - type: llm-rubric
        value: Reply is professional, de-escalates, does not promise a refund, and directs the customer to the billing team.

  - description: routine password reset
    vars:
      ticket: "How do I reset my password?"
    assert:
      - type: icontains
        value: password
      - type: javascript
        value: output.split(/\s+/).length <= 160

Three things matter here. Deterministic assertions (icontains, not-icontains, javascript) are free and instant, so lead with them. llm-rubric sends the output to a grading model for the judgment calls a string match can't make, like "does this de-escalate". And defaultTest.options.provider pins that grader; without it, promptfoo picks a grading model from whatever credentials it finds in the environment, and you want CI runs judged by the same model every time.

3. Run the eval locally #

npx promptfoo@latest eval

Each test case runs against the prompt and every assertion is checked. Add npx promptfoo@latest view to browse results in a local web UI. The exit code is the CI contract: 0 when everything passes, 100 when at least one assertion fails, 1 for any other error.

4. Wire it into GitHub Actions #

Add the API key as a repository secret, with the GitHub CLI or under Settings > Secrets and variables > Actions:

gh secret set ANTHROPIC_API_KEY

Then create .github/workflows/prompt-eval.yml:

name: Prompt regression tests
on:
  pull_request:
    paths:
      - 'prompts/**'
      - 'promptfooconfig.yaml'

jobs:
  evaluate:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      pull-requests: write
    steps:
      - uses: actions/checkout@v4
      - uses: actions/cache@v4
        with:
          path: .promptfoo-cache
          key: ${{ runner.os }}-promptfoo-${{ hashFiles('promptfooconfig.yaml') }}
      - uses: promptfoo/promptfoo-action@v1
        with:
          anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
          github-token: ${{ secrets.GITHUB_TOKEN }}
          config: promptfooconfig.yaml
          prompts: 'prompts/**/*.txt'
          cache-path: .promptfoo-cache
          promptfoo-version: 0.123.0
          fail-on-threshold: 100

The pieces: the paths filter skips the workflow when a PR doesn't touch prompts, so you're not paying for evals on unrelated changes. The cache step persists promptfoo's LLM response cache between runs; unchanged prompt-and-test pairs are replayed from disk instead of hitting the API. pull-requests: write lets the action post the results table as a PR comment. fail-on-threshold: 100 turns any assertion failure into a red check; lower it if you want a tolerance band instead of a hard gate. Pinning promptfoo-version keeps CI results reproducible.

By default the action evaluates the prompt files the PR actually changed, matched by the prompts glob. If you'd rather always run the full config as written, add use-config-prompts: true.

5. Break the prompt on purpose #

Prove the gate works:

git checkout -b loosen-refund-rule
sed -i '' '/Never promise a refund/d' prompts/support-reply.txt   # GNU sed: drop the ''
git commit -am "Simplify support prompt"
git push -u origin loosen-refund-rule
gh pr create --fill

With the refund rule gone, the model tends to promise refunds to angry customers. The llm-rubric assertion catches it, the suite drops below 100%, and the check goes red with a comment showing which case failed and why.

Verify it works #

Locally, a passing run ends like this (table trimmed):

Running 2 test cases (up to 4 at a time)...
✓ Eval complete (ID: eval-xxx-2026-09-13T11:39:29)

» View results: promptfoo view

Results:
  ✓ 2 passed (100%)
  0 failed (0%)
  0 errors (0%)
Duration: 9s (concurrency: 4)

echo $? prints 0. In GitHub, a PR that touches prompts/ gets a "Prompt regression tests" check plus a bot comment with the pass/fail matrix, and the PR from step 5 shows the check failing.

Troubleshooting #

✗ Missing ANTHROPIC_API_KEY (anthropic:messages:claude-opus-5): promptfoo validates credentials before running anything. Locally, export ANTHROPIC_API_KEY=... in the same shell. In CI, the secret name in ${{ secrets.ANTHROPIC_API_KEY }} must match what you created, and the anthropic-api-key input must be present. Note that secrets aren't exposed to workflows triggered from forks.

npm warn EBADENGINE Unsupported engine followed by crashes: your Node is older than 22.22.0, which promptfoo 0.123.0 requires. Upgrade to 24 LTS.

Resource not accessible by integration in the action logs: the workflow can run the eval but can't post the PR comment. Add pull-requests: write under permissions: (a repo-level "Read repository contents" default token permission causes this too).

Local eval exits with code 100: that's by design, the documented exit code for "at least one test failed," not a crash. Read the failure rows in the output table, or set PROMPTFOO_FAILED_TEST_EXIT_CODE=0 if a script needs the run to be non-fatal.

Next steps #

Add cost and latency assertions so a prompt edit that doubles token spend or response time also fails CI. List a second entry under providers to score a model upgrade against your current one in the same table, which is the same regression mechanism pointed at model changes instead of prompt changes. Move test cases into a CSV once they outgrow the YAML. And when the suite is stable, look at promptfoo's red teaming to probe the same prompt for jailbreaks and data leaks on a schedule rather than per PR.

Sources & further reading #

  1.                              [Getting started](https://www.promptfoo.dev/docs/getting-started/)
                             — promptfoo.dev
    
  2.                              [Anthropic provider](https://www.promptfoo.dev/docs/providers/anthropic/)
                             — promptfoo.dev
    
  3.                              [Command line reference](https://www.promptfoo.dev/docs/usage/command-line/)
                             — promptfoo.dev
    
  4.                              [Deterministic assertions](https://www.promptfoo.dev/docs/configuration/expected-outputs/deterministic/)
                             — promptfoo.dev
    
  5.                              [Model-graded metrics](https://www.promptfoo.dev/docs/configuration/expected-outputs/model-graded/)
                             — promptfoo.dev
    
  6.                              [promptfoo GitHub Action README](https://github.com/promptfoo/promptfoo-action)
                             — github.com
    

Mariana Souza· Senior Editor

Mariana covers the fast-moving world of machine learning and generative AI, with a particular focus on how these technologies are reshaping development workflows. When she isn't stress-testing the latest foundation models, she's usually at a local hackathon.

Discussion 1 #

had to debug a production LLM call last month where someone tweaked the system prompt and suddenly it started returning json when it should've been plain text—would've caught that instantly with this. the nice part is promptfoo treats tests like actual assertions instead of vibes, which is exactly how i think about type checking in rust. definitely worth the 20 minutes to wire up.

── more in #ai-tools 4 stories · sorted by recency
── more on @promptfoo 3 stories trending now
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/catch-prompt-regress…] indexed:0 read:7min 2026-09-13 ·