Skip to content

Commit 6437d93

Browse files
authored
Merge pull request #12 from slifty/11-add-github-models-support
Add GitHub Models support
2 parents d4778ee + 787fc51 commit 6437d93

15 files changed

Lines changed: 681 additions & 133 deletions

.github/workflows/qa.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: QA Instructions
2+
3+
on:
4+
pull_request:
5+
types: [opened, synchronize]
6+
7+
permissions:
8+
pull-requests: write
9+
models: read
10+
11+
jobs:
12+
qa-instructions:
13+
runs-on: ubuntu-latest
14+
15+
steps:
16+
- uses: actions/checkout@v6
17+
18+
- name: Setup Node.js
19+
uses: actions/setup-node@v6
20+
with:
21+
node-version-file: ".node-version"
22+
cache: "npm"
23+
24+
- name: Install dependencies
25+
run: npm ci
26+
27+
- name: Build
28+
run: npm run build
29+
30+
- name: Generate QA Instructions
31+
uses: ./
32+
with:
33+
github-token: ${{ secrets.GITHUB_TOKEN }}

README.md

Lines changed: 54 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22

33
[![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)
44

5-
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.
5+
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.
6+
7+
Supports two AI providers:
8+
9+
- **GitHub Models** (default) — uses the GitHub Models inference API with your existing `GITHUB_TOKEN`. No API keys or subscriptions required.
10+
- **Anthropic** — uses the Anthropic API with a Claude model. Requires an API key.
611

712
## Usage
813

14+
### GitHub Models (default)
15+
916
```yaml
1017
name: QA Instructions
1118
on:
@@ -14,6 +21,7 @@ on:
1421

1522
permissions:
1623
pull-requests: write
24+
models: read
1725

1826
jobs:
1927
qa-instructions:
@@ -22,23 +30,59 @@ jobs:
2230
- uses: slifty/qa-instructions-action@v1
2331
with:
2432
github-token: ${{ secrets.GITHUB_TOKEN }}
25-
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
2633
```
2734
2835
**Requirements:**
2936
37+
- `permissions: models: read` is required for GitHub Models API access
3038
- `permissions: pull-requests: write` is required for posting PR comments
31-
- `ANTHROPIC_API_KEY` must be stored as a repository secret
3239
- The `synchronize` event type triggers on each push, updating the existing comment
3340

41+
### Anthropic
42+
43+
```yaml
44+
name: QA Instructions
45+
on:
46+
pull_request:
47+
types: [opened, synchronize]
48+
49+
permissions:
50+
pull-requests: write
51+
52+
jobs:
53+
qa-instructions:
54+
runs-on: ubuntu-latest
55+
steps:
56+
- uses: slifty/qa-instructions-action@v1
57+
with:
58+
github-token: ${{ secrets.GITHUB_TOKEN }}
59+
provider: anthropic
60+
anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }}
61+
```
62+
63+
**Requirements:**
64+
65+
- `ANTHROPIC_API_KEY` must be stored as a repository secret
66+
- `permissions: pull-requests: write` is required for posting PR comments
67+
3468
## Inputs
3569

36-
| Input | Description | Required | Default |
37-
| ------------------- | ------------------------------------------- | -------- | ---------------------------- |
38-
| `github-token` | GitHub token for API access | Yes | `${{ github.token }}` |
39-
| `anthropic-api-key` | Anthropic API key for Claude | Yes | — |
40-
| `prompt` | Optional custom instructions for the prompt | No | `""` |
41-
| `model` | Claude model to use | No | `claude-sonnet-4-5-20250929` |
70+
| Input | Description | Required | Default |
71+
| ------------------- | ------------------------------------------------------------ | -------- | --------------------- |
72+
| `github-token` | GitHub token for API access and GitHub Models authentication | Yes | `${{ github.token }}` |
73+
| `provider` | AI provider: `"github-models"` or `"anthropic"` | No | `"github-models"` |
74+
| `anthropic-api-key` | Anthropic API key (required when provider is `"anthropic"`) | No | `""` |
75+
| `prompt` | Optional custom instructions appended to the prompt | No | `""` |
76+
| `model` | AI model to use (defaults to a provider-appropriate model) | No | `""` |
77+
78+
### Default models
79+
80+
| Provider | Default model |
81+
| --------------- | ---------------------------- |
82+
| `github-models` | `openai/gpt-4o` |
83+
| `anthropic` | `claude-sonnet-4-5-20250929` |
84+
85+
You can override the model with any model supported by the chosen provider.
4286

4387
## Outputs
4488

@@ -50,7 +94,7 @@ jobs:
5094

5195
1. Gathers PR context: metadata, diff, changed file contents, repository file tree, and commit history
5296
2. Builds a structured prompt with tiered truncation to fit within model context limits
53-
3. Sends the prompt to Claude, which generates QA instructions covering:
97+
3. Sends the prompt to the configured AI provider, which generates QA instructions covering:
5498
- Summary of changes
5599
- Test environment setup
56100
- Specific test scenarios with steps and expected results

action.yml

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,26 @@
11
name: "QA Instructions Action"
2-
description: "A GitHub Action that generates QA testing instructions for pull requests using Claude"
2+
description: "A GitHub Action that generates QA testing instructions for pull requests using AI"
33
inputs:
44
github-token:
5-
description: "GitHub token for API access"
5+
description: "GitHub token for API access (also used for GitHub Models authentication)"
66
required: true
77
default: ${{ github.token }}
8+
provider:
9+
description: 'AI provider to use: "github-models" or "anthropic"'
10+
required: false
11+
default: "github-models"
812
anthropic-api-key:
9-
description: "Anthropic API key for Claude"
10-
required: true
13+
description: 'Anthropic API key for Claude (required when provider is "anthropic")'
14+
required: false
15+
default: ""
1116
prompt:
1217
description: "Optional custom instructions appended to the prompt"
1318
required: false
1419
default: ""
1520
model:
16-
description: "Claude model to use"
21+
description: "AI model to use (defaults to provider-appropriate model if not set)"
1722
required: false
18-
default: "claude-sonnet-4-5-20250929"
23+
default: ""
1924
outputs:
2025
instructions:
2126
description: "The generated QA instructions"

src/claude.test.ts

Lines changed: 16 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,31 +14,35 @@ vi.mock("@anthropic-ai/sdk", () => {
1414
};
1515
});
1616

17-
import { generateQAInstructions } from "./claude.js";
18-
import { DEFAULT_MODEL } from "./constants.js";
17+
import { createAnthropicProvider } from "./claude.js";
18+
import { DEFAULT_ANTHROPIC_MODEL } from "./constants.js";
1919

20-
describe("generateQAInstructions", () => {
20+
describe("createAnthropicProvider", () => {
2121
beforeEach(() => {
2222
vi.clearAllMocks();
2323
});
2424

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

30-
const result = await generateQAInstructions(
30+
const provider = createAnthropicProvider(
3131
"test-api-key",
32-
DEFAULT_MODEL,
33-
"PR context here",
32+
DEFAULT_ANTHROPIC_MODEL,
33+
);
34+
const result = await provider.generateQAInstructions(
35+
"system prompt",
36+
"user prompt",
3437
);
3538

3639
expect(result).toBe("## QA Instructions\n\nTest this.");
3740
expect(mockCreate).toHaveBeenCalledWith(
3841
expect.objectContaining({
39-
model: DEFAULT_MODEL,
42+
model: DEFAULT_ANTHROPIC_MODEL,
4043
max_tokens: 4096,
41-
messages: [{ role: "user", content: "PR context here" }],
44+
system: "system prompt",
45+
messages: [{ role: "user", content: "user prompt" }],
4246
}),
4347
);
4448
});
@@ -48,8 +52,10 @@ describe("generateQAInstructions", () => {
4852
content: [],
4953
});
5054

55+
const provider = createAnthropicProvider("key", "model");
56+
5157
await expect(
52-
generateQAInstructions("key", "model", "context"),
58+
provider.generateQAInstructions("system", "user"),
5359
).rejects.toThrow("No text content in Claude response");
5460
});
5561
});

src/claude.ts

Lines changed: 23 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,30 @@
11
import Anthropic from "@anthropic-ai/sdk";
2+
import type { AiProvider } from "./types.js";
23

3-
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.
4-
5-
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.
6-
7-
Analyze the provided PR context and produce testing instructions using whichever of these sections are relevant:
8-
9-
- **Summary** — What the PR changes and why (1-3 sentences).
10-
- **Test Environment Setup** — Prerequisites or setup steps, if any beyond the standard dev environment. Omit if none.
11-
- **Test Scenarios** — Numbered test cases with steps and expected results. Focus on the most important paths; don't enumerate the obvious.
12-
- **Regression Risks** — Areas that might break as a side effect. Omit if the changes are well-isolated.
13-
- **Things to Watch For** — Edge cases or concerns spotted in the code. Omit if nothing stands out.
14-
15-
Be specific and practical. Reference actual file names, function names, and UI elements from the PR when possible.`;
16-
17-
export async function generateQAInstructions(
4+
export function createAnthropicProvider(
185
apiKey: string,
196
model: string,
20-
promptContext: string,
21-
): Promise<string> {
7+
): AiProvider {
228
const client = new Anthropic({ apiKey });
239

24-
const response = await client.messages.create({
25-
model,
26-
max_tokens: 4096,
27-
system: SYSTEM_PROMPT,
28-
messages: [
29-
{
30-
role: "user",
31-
content: promptContext,
32-
},
33-
],
34-
});
35-
36-
const textBlock = response.content.find((block) => block.type === "text");
37-
if (!textBlock || textBlock.type !== "text") {
38-
throw new Error("No text content in Claude response");
39-
}
40-
41-
return textBlock.text;
10+
return {
11+
async generateQAInstructions(
12+
systemPrompt: string,
13+
userPrompt: string,
14+
): Promise<string> {
15+
const response = await client.messages.create({
16+
model,
17+
max_tokens: 4096,
18+
system: systemPrompt,
19+
messages: [{ role: "user", content: userPrompt }],
20+
});
21+
22+
const textBlock = response.content.find((block) => block.type === "text");
23+
if (!textBlock || textBlock.type !== "text") {
24+
throw new Error("No text content in Claude response");
25+
}
26+
27+
return textBlock.text;
28+
},
29+
};
4230
}

src/constants.ts

Lines changed: 49 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,52 @@
11
export const COMMENT_MARKER = "<!-- qa-instructions-action -->";
22

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

5-
export const MAX_DIFF_CHARS = 80_000;
6-
export const MAX_CHANGED_FILES_CHARS = 60_000;
7-
export const MAX_FILE_CHARS = 10_000;
8-
export const MAX_FILE_TREE_CHARS = 20_000;
9-
export const MAX_TOTAL_CHARS = 180_000;
6+
export const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-4-5-20250929";
7+
export const DEFAULT_GITHUB_MODELS_MODEL = "openai/gpt-4o";
8+
9+
export const GITHUB_MODELS_BASE_URL =
10+
"https://models.github.ai/inference/chat/completions";
11+
12+
export interface ContextLimits {
13+
maxDiffChars: number;
14+
maxChangedFilesChars: number;
15+
maxFileChars: number;
16+
maxFileTreeChars: number;
17+
maxTotalChars: number;
18+
}
19+
20+
export const ANTHROPIC_CONTEXT_LIMITS: ContextLimits = {
21+
maxDiffChars: 80_000,
22+
maxChangedFilesChars: 60_000,
23+
maxFileChars: 10_000,
24+
maxFileTreeChars: 20_000,
25+
maxTotalChars: 180_000,
26+
};
27+
28+
// GitHub Models free tier: 8k input tokens for gpt-4o (~32k chars).
29+
// JSON encoding inflates newlines (\n → \\n), so budget ~20k chars
30+
// for the user prompt after reserving room for the system prompt and
31+
// JSON/HTTP overhead.
32+
export const GITHUB_MODELS_CONTEXT_LIMITS: ContextLimits = {
33+
maxDiffChars: 8_000,
34+
maxChangedFilesChars: 6_000,
35+
maxFileChars: 3_000,
36+
maxFileTreeChars: 3_000,
37+
maxTotalChars: 20_000,
38+
};
39+
40+
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.
41+
42+
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.
43+
44+
Analyze the provided PR context and produce testing instructions using whichever of these sections are relevant:
45+
46+
- **Summary** — What the PR changes and why (1-3 sentences).
47+
- **Test Environment Setup** — Prerequisites or setup steps, if any beyond the standard dev environment. Omit if none.
48+
- **Test Scenarios** — Numbered test cases with steps and expected results. Focus on the most important paths; don't enumerate the obvious.
49+
- **Regression Risks** — Areas that might break as a side effect. Omit if the changes are well-isolated.
50+
- **Things to Watch For** — Edge cases or concerns spotted in the code. Omit if nothing stands out.
51+
52+
Be specific and practical. Reference actual file names, function names, and UI elements from the PR when possible.`;

0 commit comments

Comments
 (0)