Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions .github/workflows/qa.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: QA Instructions

on:
pull_request:
types: [opened, synchronize]

permissions:
pull-requests: write
models: read

jobs:
qa-instructions:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v6

- name: Setup Node.js
uses: actions/setup-node@v6
with:
node-version-file: ".node-version"
cache: "npm"

- name: Install dependencies
run: npm ci

- name: Build
run: npm run build

- name: Generate QA Instructions
uses: ./
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
64 changes: 54 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@

[![CI](https://github.com/slifty/qa-instructions-action/actions/workflows/ci.yml/badge.svg)](https://github.com/slifty/qa-instructions-action/actions/workflows/ci.yml)

A GitHub Action that automatically generates QA testing instructions for pull requests using Claude. On each PR push, it gathers context about the changes and posts (or updates) a comment with structured testing instructions.
A GitHub Action that automatically generates QA testing instructions for pull requests using AI. On each PR push, it gathers context about the changes and posts (or updates) a comment with structured testing instructions.

Supports two AI providers:

- **GitHub Models** (default) — uses the GitHub Models inference API with your existing `GITHUB_TOKEN`. No API keys or subscriptions required.
- **Anthropic** — uses the Anthropic API with a Claude model. Requires an API key.

## Usage

### GitHub Models (default)

```yaml
name: QA Instructions
on:
Expand All @@ -14,6 +21,7 @@ on:

permissions:
pull-requests: write
models: read

jobs:
qa-instructions:
Expand All @@ -22,23 +30,59 @@ jobs:
- uses: slifty/qa-instructions-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
```

**Requirements:**

- `permissions: models: read` is required for GitHub Models API access
- `permissions: pull-requests: write` is required for posting PR comments
- `ANTHROPIC_API_KEY` must be stored as a repository secret
- The `synchronize` event type triggers on each push, updating the existing comment

### Anthropic

```yaml
name: QA Instructions
on:
pull_request:
types: [opened, synchronize]

permissions:
pull-requests: write

jobs:
qa-instructions:
runs-on: ubuntu-latest
steps:
- uses: slifty/qa-instructions-action@v1
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
provider: anthropic
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
```

**Requirements:**

- `ANTHROPIC_API_KEY` must be stored as a repository secret
- `permissions: pull-requests: write` is required for posting PR comments

## Inputs

| Input | Description | Required | Default |
| ------------------- | ------------------------------------------- | -------- | ---------------------------- |
| `github-token` | GitHub token for API access | Yes | `${{ github.token }}` |
| `anthropic-api-key` | Anthropic API key for Claude | Yes | — |
| `prompt` | Optional custom instructions for the prompt | No | `""` |
| `model` | Claude model to use | No | `claude-sonnet-4-5-20250929` |
| Input | Description | Required | Default |
| ------------------- | ------------------------------------------------------------ | -------- | --------------------- |
| `github-token` | GitHub token for API access and GitHub Models authentication | Yes | `${{ github.token }}` |
| `provider` | AI provider: `"github-models"` or `"anthropic"` | No | `"github-models"` |
| `anthropic-api-key` | Anthropic API key (required when provider is `"anthropic"`) | No | `""` |
| `prompt` | Optional custom instructions appended to the prompt | No | `""` |
| `model` | AI model to use (defaults to a provider-appropriate model) | No | `""` |

### Default models

| Provider | Default model |
| --------------- | ---------------------------- |
| `github-models` | `openai/gpt-4o` |
| `anthropic` | `claude-sonnet-4-5-20250929` |

You can override the model with any model supported by the chosen provider.

## Outputs

Expand All @@ -50,7 +94,7 @@ jobs:

1. Gathers PR context: metadata, diff, changed file contents, repository file tree, and commit history
2. Builds a structured prompt with tiered truncation to fit within model context limits
3. Sends the prompt to Claude, which generates QA instructions covering:
3. Sends the prompt to the configured AI provider, which generates QA instructions covering:
- Summary of changes
- Test environment setup
- Specific test scenarios with steps and expected results
Expand Down
17 changes: 11 additions & 6 deletions action.yml
Original file line number Diff line number Diff line change
@@ -1,21 +1,26 @@
name: "QA Instructions Action"
description: "A GitHub Action that generates QA testing instructions for pull requests using Claude"
description: "A GitHub Action that generates QA testing instructions for pull requests using AI"
inputs:
github-token:
description: "GitHub token for API access"
description: "GitHub token for API access (also used for GitHub Models authentication)"
required: true
default: ${{ github.token }}
provider:
description: 'AI provider to use: "github-models" or "anthropic"'
required: false
default: "github-models"
anthropic-api-key:
description: "Anthropic API key for Claude"
required: true
description: 'Anthropic API key for Claude (required when provider is "anthropic")'
required: false
default: ""
prompt:
description: "Optional custom instructions appended to the prompt"
required: false
default: ""
model:
description: "Claude model to use"
description: "AI model to use (defaults to provider-appropriate model if not set)"
required: false
default: "claude-sonnet-4-5-20250929"
default: ""
outputs:
instructions:
description: "The generated QA instructions"
Expand Down
26 changes: 16 additions & 10 deletions src/claude.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,31 +14,35 @@ vi.mock("@anthropic-ai/sdk", () => {
};
});

import { generateQAInstructions } from "./claude.js";
import { DEFAULT_MODEL } from "./constants.js";
import { createAnthropicProvider } from "./claude.js";
import { DEFAULT_ANTHROPIC_MODEL } from "./constants.js";

describe("generateQAInstructions", () => {
describe("createAnthropicProvider", () => {
beforeEach(() => {
vi.clearAllMocks();
});

it("sends context to Claude and returns text response", async () => {
it("sends prompts to Claude and returns text response", async () => {
mockCreate.mockResolvedValue({
content: [{ type: "text", text: "## QA Instructions\n\nTest this." }],
});

const result = await generateQAInstructions(
const provider = createAnthropicProvider(
"test-api-key",
DEFAULT_MODEL,
"PR context here",
DEFAULT_ANTHROPIC_MODEL,
);
const result = await provider.generateQAInstructions(
"system prompt",
"user prompt",
);

expect(result).toBe("## QA Instructions\n\nTest this.");
expect(mockCreate).toHaveBeenCalledWith(
expect.objectContaining({
model: DEFAULT_MODEL,
model: DEFAULT_ANTHROPIC_MODEL,
max_tokens: 4096,
messages: [{ role: "user", content: "PR context here" }],
system: "system prompt",
messages: [{ role: "user", content: "user prompt" }],
}),
);
});
Expand All @@ -48,8 +52,10 @@ describe("generateQAInstructions", () => {
content: [],
});

const provider = createAnthropicProvider("key", "model");

await expect(
generateQAInstructions("key", "model", "context"),
provider.generateQAInstructions("system", "user"),
).rejects.toThrow("No text content in Claude response");
});
});
58 changes: 23 additions & 35 deletions src/claude.ts
Original file line number Diff line number Diff line change
@@ -1,42 +1,30 @@
import Anthropic from "@anthropic-ai/sdk";
import type { AiProvider } from "./types.js";

const SYSTEM_PROMPT = `You are an expert QA engineer reviewing a pull request. Your job is to generate clear, actionable QA testing instructions that a human tester can follow.

Scale your response to the complexity of the changes. A small documentation fix needs just a sentence or two. A large feature needs thorough coverage. Be concise — omit sections that add no value for the specific PR.

Analyze the provided PR context and produce testing instructions using whichever of these sections are relevant:

- **Summary** — What the PR changes and why (1-3 sentences).
- **Test Environment Setup** — Prerequisites or setup steps, if any beyond the standard dev environment. Omit if none.
- **Test Scenarios** — Numbered test cases with steps and expected results. Focus on the most important paths; don't enumerate the obvious.
- **Regression Risks** — Areas that might break as a side effect. Omit if the changes are well-isolated.
- **Things to Watch For** — Edge cases or concerns spotted in the code. Omit if nothing stands out.

Be specific and practical. Reference actual file names, function names, and UI elements from the PR when possible.`;

export async function generateQAInstructions(
export function createAnthropicProvider(
apiKey: string,
model: string,
promptContext: string,
): Promise<string> {
): AiProvider {
const client = new Anthropic({ apiKey });

const response = await client.messages.create({
model,
max_tokens: 4096,
system: SYSTEM_PROMPT,
messages: [
{
role: "user",
content: promptContext,
},
],
});

const textBlock = response.content.find((block) => block.type === "text");
if (!textBlock || textBlock.type !== "text") {
throw new Error("No text content in Claude response");
}

return textBlock.text;
return {
async generateQAInstructions(
systemPrompt: string,
userPrompt: string,
): Promise<string> {
const response = await client.messages.create({
model,
max_tokens: 4096,
system: systemPrompt,
messages: [{ role: "user", content: userPrompt }],
});

const textBlock = response.content.find((block) => block.type === "text");
if (!textBlock || textBlock.type !== "text") {
throw new Error("No text content in Claude response");
}

return textBlock.text;
},
};
}
55 changes: 49 additions & 6 deletions src/constants.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,52 @@
export const COMMENT_MARKER = "<!-- qa-instructions-action -->";

export const DEFAULT_MODEL = "claude-sonnet-4-5-20250929";
export const VALID_PROVIDERS = ["github-models", "anthropic"] as const;
export type Provider = (typeof VALID_PROVIDERS)[number];

export const MAX_DIFF_CHARS = 80_000;
export const MAX_CHANGED_FILES_CHARS = 60_000;
export const MAX_FILE_CHARS = 10_000;
export const MAX_FILE_TREE_CHARS = 20_000;
export const MAX_TOTAL_CHARS = 180_000;
export const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-5-20250929";
export const DEFAULT_GITHUB_MODELS_MODEL = "openai/gpt-4o";

export const GITHUB_MODELS_BASE_URL =
"https://models.github.ai/inference/chat/completions";

export interface ContextLimits {
maxDiffChars: number;
maxChangedFilesChars: number;
maxFileChars: number;
maxFileTreeChars: number;
maxTotalChars: number;
}

export const ANTHROPIC_CONTEXT_LIMITS: ContextLimits = {
maxDiffChars: 80_000,
maxChangedFilesChars: 60_000,
maxFileChars: 10_000,
maxFileTreeChars: 20_000,
maxTotalChars: 180_000,
};

// GitHub Models free tier: 8k input tokens for gpt-4o (~32k chars).
// JSON encoding inflates newlines (\n → \\n), so budget ~20k chars
// for the user prompt after reserving room for the system prompt and
// JSON/HTTP overhead.
export const GITHUB_MODELS_CONTEXT_LIMITS: ContextLimits = {
maxDiffChars: 8_000,
maxChangedFilesChars: 6_000,
maxFileChars: 3_000,
maxFileTreeChars: 3_000,
maxTotalChars: 20_000,
};

export const SYSTEM_PROMPT = `You are an expert QA engineer reviewing a pull request. Your job is to generate clear, actionable QA testing instructions that a human tester can follow.

Scale your response to the complexity of the changes. A small documentation fix needs just a sentence or two. A large feature needs thorough coverage. Be concise — omit sections that add no value for the specific PR.

Analyze the provided PR context and produce testing instructions using whichever of these sections are relevant:

- **Summary** — What the PR changes and why (1-3 sentences).
- **Test Environment Setup** — Prerequisites or setup steps, if any beyond the standard dev environment. Omit if none.
- **Test Scenarios** — Numbered test cases with steps and expected results. Focus on the most important paths; don't enumerate the obvious.
- **Regression Risks** — Areas that might break as a side effect. Omit if the changes are well-isolated.
- **Things to Watch For** — Edge cases or concerns spotted in the code. Omit if nothing stands out.

Be specific and practical. Reference actual file names, function names, and UI elements from the PR when possible.`;
Loading