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
4 changes: 4 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ on:
permissions:
pull-requests: write
contents: read
models: read

jobs:
ai-code-review:
Expand All @@ -24,4 +25,7 @@ jobs:
gh_pat: ${{ secrets.GITHUB_TOKEN }}
repo: ${{ github.repository }}
pr_number: ${{ github.event.pull_request.number }}
api_key: ${{ secrets.GITHUB_TOKEN }}
language: Thai
max_tokens: 2048
max_diff_chars: 10000
73 changes: 50 additions & 23 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# AI Code Review Action
# AI Code Review Action

Automated PR code review powered by AI — supports GitHub Copilot, OpenAI, Anthropic, Google Gemini, xAI, OpenRouter, Ollama, Azure, AWS, and more.
Automated PR code review powered by AI — supports GitHub Models, GitHub Copilot, OpenAI, Anthropic, Google Gemini, xAI, OpenRouter, Ollama, Azure, AWS, and more.

## Quick Start

Zero-config setup using **GitHub Models** — works with `GITHUB_TOKEN`, no extra secrets needed:

```yaml
name: AI Code Review
on:
Expand All @@ -13,6 +15,7 @@ on:
permissions:
pull-requests: write
contents: read
models: read

jobs:
review:
Expand All @@ -24,59 +27,80 @@ jobs:
gh_pat: ${{ secrets.GITHUB_TOKEN }}
repo: ${{ github.repository }}
pr_number: ${{ github.event.pull_request.number }}
api_key: ${{ secrets.GITHUB_TOKEN }}
```

## Inputs

| Input | Required | Default | Description |
|---|---|---|---|
| `gh_pat` | ✅ | — | GitHub PAT (for Copilot provider, also used as API key) |
| `gh_pat` | ✅ | — | GitHub PAT for GitHub API (fetching diff, posting comments) |
| `repo` | ✅ | — | Repository in `owner/repo` format |
| `pr_number` | ✅ | — | Pull request number to review |
| `provider` | | `copilot` | AI provider shorthand (see [Providers](#providers)) |
| `model` | | `gpt-5-mini` | Model name |
| `api_key` | | — | API key for the provider (not needed when using `copilot`) |
| `api_key` | ✅ | — | API key for the provider. For `github-models`: use `GITHUB_TOKEN`. For `copilot`: use a personal PAT with Copilot subscription |
| `provider` | | `github-models` | AI provider shorthand (see [Providers](#providers)) |
| `model` | | `gpt-4o` | Model name |
| `title` | | `AI Code Review` | Title shown in the PR comment header |
| `api_url` | | — | Full endpoint URL — overrides provider + api_type defaults |
| `api_type` | | `chat` | API format: `chat`, `responses`, or `messages` |
| `max_tokens` | | `4096` | Maximum tokens in AI response |
| `max_diff_chars` | | `30000` | Maximum characters of diff sent to AI |
| `max_tokens` | | `2048` | Maximum tokens in AI response |
| `max_diff_chars` | | `10000` | Maximum characters of diff sent to AI |
| `language` | | `English` | Language for the review response |

## Providers

| `provider` | `api_type` default | Base URL |
|---|---|---|
| `copilot` | `chat` | `https://api.githubcopilot.com` |
| `openai` | `chat` | `https://api.openai.com/v1` |
| `anthropic` | `messages` | `https://api.anthropic.com/v1` |
| `openrouter` | `chat` | `https://openrouter.ai/api/v1` |
| `ollama` | `chat` | `http://localhost:11434/v1` |
| `xai` | `chat` | `https://api.x.ai/v1` (Grok) |
| `zai` | `chat` | `https://api.z.ai/api/coding/paas/v4` (GLM) |
| `google` | `chat` | `https://generativelanguage.googleapis.com/v1beta/openai` |
| `azure` | `chat` | ⚠️ requires `api_url` |
| `aws` | `chat` | ⚠️ requires `api_url` |
| `custom` | `chat` | ⚠️ requires `api_url` |
| `provider` | `api_type` default | Base URL | Auth |
|---|---|---|---|
| `github-models` | `chat` | `https://models.inference.ai.azure.com` | `GITHUB_TOKEN` ✅ |
| `copilot` | `chat` | `https://api.githubcopilot.com` | Personal PAT with Copilot subscription |
| `openai` | `chat` | `https://api.openai.com/v1` | API key |
| `anthropic` | `messages` | `https://api.anthropic.com/v1` | API key |
| `openrouter` | `chat` | `https://openrouter.ai/api/v1` | API key |
| `ollama` | `chat` | `http://localhost:11434/v1` | — |
| `xai` | `chat` | `https://api.x.ai/v1` | API key (Grok) |
| `zai` | `chat` | `https://api.z.ai/api/coding/paas/v4` | API key (GLM) |
| `google` | `chat` | `https://generativelanguage.googleapis.com/v1beta/openai` | API key |
| `azure` | `chat` | ⚠️ requires `api_url` | `api-key` header |
| `aws` | `chat` | ⚠️ requires `api_url` | API key |
| `custom` | `chat` | ⚠️ requires `api_url` | API key |

> **Note:** `copilot` provider exchanges a personal PAT for a short-lived session token automatically. `GITHUB_TOKEN` from GitHub Actions will not work for Copilot — use `github-models` instead for zero-config setups.

## API Types

| `api_type` | Path appended | Compatible with |
|---|---|---|
| `chat` | `/chat/completions` | Copilot, OpenAI, OpenRouter, Ollama, xAI, Google, Azure |
| `chat` | `/chat/completions` | GitHub Models, Copilot, OpenAI, OpenRouter, Ollama, xAI, Google, Azure |
| `responses` | `/responses` | OpenAI Responses API |
| `messages` | `/messages` | Anthropic Claude |
| `text` | `/completions` | Legacy completions endpoint |

## Examples

### GitHub Copilot (default)
### GitHub Models (default — zero config)

```yaml
- uses: n-devs/ai-code-review@v1
with:
gh_pat: ${{ secrets.GITHUB_TOKEN }}
repo: ${{ github.repository }}
pr_number: ${{ github.event.pull_request.number }}
api_key: ${{ secrets.GITHUB_TOKEN }}
```

### GitHub Copilot

> Requires a personal PAT with Copilot subscription stored as `COPILOT_PAT` secret.

```yaml
- uses: n-devs/ai-code-review@v1
with:
gh_pat: ${{ secrets.GITHUB_TOKEN }}
repo: ${{ github.repository }}
pr_number: ${{ github.event.pull_request.number }}
provider: copilot
api_key: ${{ secrets.COPILOT_PAT }}
model: gpt-4o
```

### OpenAI GPT-4o
Expand Down Expand Up @@ -154,6 +178,7 @@ jobs:
repo: ${{ github.repository }}
pr_number: ${{ github.event.pull_request.number }}
provider: ollama
api_key: ""
api_url: http://my-server:11434/v1/chat/completions
model: llama3
```
Expand Down Expand Up @@ -208,6 +233,7 @@ jobs:
gh_pat: ${{ secrets.GITHUB_TOKEN }}
repo: ${{ github.repository }}
pr_number: ${{ github.event.pull_request.number }}
api_key: ${{ secrets.GITHUB_TOKEN }}
language: Thai
```

Expand All @@ -216,4 +242,5 @@ jobs:
```bash
npm install
npm run build # outputs dist/index.js via @vercel/ncc
npm test # runs unit tests via vitest
```
20 changes: 12 additions & 8 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,17 +12,21 @@ inputs:
pr_number:
description: "Pull request number to review"
required: true
api_key:
description: "API key for the selected provider. For copilot: provide a personal GitHub PAT with Copilot subscription (GITHUB_TOKEN will not work — it lacks Copilot access)"
required: true
title:
description: "Title for the code review comment"
required: false
default: "AI Code Review"
model:
description: "AI model to use (e.g. gpt-4o, claude-3-5-sonnet-20241022, llama3)"
required: false
default: "gpt-5-mini"
default: "gpt-4o"
provider:
description: "AI provider shorthand: copilot, openai, anthropic, openrouter, ollama, xai, zai, google, azure, aws, custom — sets api_url and api_type defaults automatically (azure/aws require api_url)"
required: false
default: "copilot"
api_key:
description: "API key for the selected provider (not needed for copilot — uses gh_pat)"
description: "AI provider shorthand: copilot, github-models, openai, anthropic, openrouter, ollama, xai, zai, google, azure, aws, custom — sets api_url and api_type defaults automatically (azure/aws require api_url). Use github-models to authenticate with GITHUB_TOKEN without needing a personal PAT."
required: false
default: "github-models"
api_url:
description: "Full API endpoint URL (e.g. https://api.anthropic.com/v1/messages). Overrides provider + api_type defaults when set."
required: false
Expand All @@ -37,12 +41,12 @@ inputs:
max_diff_chars:
description: "Maximum number of characters of diff to send to Copilot"
required: false
default: "30000"
default: "10000"
language:
description: "Language for the code review response (e.g. English, Thai, Japanese)"
required: false
default: "English"

runs:
using: "node20"
using: "node24"
main: "dist/index.js"
130 changes: 99 additions & 31 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,61 @@
/******/ "use strict";
/******/ var __webpack_modules__ = ({

/***/ 973:
/***/ ((__unused_webpack_module, exports) => {


Object.defineProperty(exports, "__esModule", ({ value: true }));
exports.REQUIRES_API_URL = exports.PROVIDER_DEFAULT_TYPE = exports.PROVIDER_BASES = exports.API_TYPE_PATHS = void 0;
exports.resolveApiType = resolveApiType;
exports.resolveApiUrl = resolveApiUrl;
exports.isCopilotUrl = isCopilotUrl;
exports.isAzureProvider = isAzureProvider;
exports.requiresApiUrl = requiresApiUrl;
exports.API_TYPE_PATHS = {
chat: "/chat/completions",
responses: "/responses",
messages: "/messages",
text: "/completions",
};
exports.PROVIDER_BASES = {
"github-models": "https://models.inference.ai.azure.com",
copilot: "https://api.githubcopilot.com",
openai: "https://api.openai.com/v1",
anthropic: "https://api.anthropic.com/v1",
openrouter: "https://openrouter.ai/api/v1",
ollama: "http://localhost:11434/v1",
xai: "https://api.x.ai/v1",
zai: "https://api.z.ai/api/coding/paas/v4",
google: "https://generativelanguage.googleapis.com/v1beta/openai",
// azure/aws/custom: must provide api_url
};
exports.PROVIDER_DEFAULT_TYPE = {
anthropic: "messages",
};
exports.REQUIRES_API_URL = ["custom", "azure", "aws"];
function resolveApiType(provider, envApiType) {
return (envApiType ?? (provider && exports.PROVIDER_DEFAULT_TYPE[provider]) ?? "chat");
}
function resolveApiUrl(provider, apiType, override) {
if (override)
return override;
const base = ((provider && exports.PROVIDER_BASES[provider]) ?? "https://api.githubcopilot.com").replace(/\/$/, "");
return base + exports.API_TYPE_PATHS[apiType];
}
function isCopilotUrl(url) {
return url.includes("githubcopilot.com");
}
function isAzureProvider(provider, url) {
return provider === "azure" || url.includes(".openai.azure.com");
}
function requiresApiUrl(provider) {
return exports.REQUIRES_API_URL.includes(provider);
}


/***/ }),

/***/ 407:
/***/ (function(__unused_webpack_module, exports, __nccwpck_require__) {

Expand Down Expand Up @@ -41,45 +96,28 @@ var __importStar = (this && this.__importStar) || (function () {
})();
Object.defineProperty(exports, "__esModule", ({ value: true }));
const https = __importStar(__nccwpck_require__(692));
const config_1 = __nccwpck_require__(973);
// ดึงค่าจาก Environment Variables (รองรับทั้ง GitHub Actions inputs และ env vars ตรง)
const GH_PAT = process.env.INPUT_GH_PAT ?? process.env.GH_PAT;
const PR_NUMBER = process.env.INPUT_PR_NUMBER ?? process.env.PR_NUMBER;
const REPO = process.env.INPUT_REPO ?? process.env.REPO; // format: owner/repo
const MODEL = process.env.INPUT_MODEL ?? process.env.INPUT_COPILOT_MODEL ?? process.env.MODEL ?? process.env.COPILOT_MODEL ?? "gpt-5-mini";
const MODEL = process.env.INPUT_MODEL ?? process.env.INPUT_COPILOT_MODEL ?? process.env.MODEL ?? process.env.COPILOT_MODEL ?? "gpt-4o";
const MAX_DIFF_CHARS = parseInt(process.env.INPUT_MAX_DIFF_CHARS ?? process.env.MAX_DIFF_CHARS ?? "30000", 10);
const MAX_TOKENS = parseInt(process.env.INPUT_MAX_TOKENS ?? process.env.MAX_TOKENS ?? "4096", 10);
const LANGUAGE = process.env.INPUT_LANGUAGE ?? process.env.LANGUAGE ?? "English";
const API_TYPE_PATHS = {
chat: "/chat/completions",
responses: "/responses",
messages: "/messages",
text: "/completions",
};
const PROVIDER_BASES = {
copilot: "https://api.githubcopilot.com",
openai: "https://api.openai.com/v1",
anthropic: "https://api.anthropic.com/v1",
openrouter: "https://openrouter.ai/api/v1",
ollama: "http://localhost:11434/v1",
xai: "https://api.x.ai/v1",
zai: "https://api.z.ai/api/coding/paas/v4",
google: "https://generativelanguage.googleapis.com/v1beta/openai",
// azure: URL is resource-specific — use api_url
// aws: URL is region-specific — use api_url
// custom: must provide api_url
};
const PROVIDER_DEFAULT_TYPE = {
anthropic: "messages",
};
const TITLE = process.env.INPUT_TITLE ?? process.env.TITLE ?? "AI Code Review";
const PROVIDER = (process.env.INPUT_PROVIDER ?? process.env.PROVIDER);
const API_TYPE = (process.env.INPUT_API_TYPE ?? process.env.INPUT_COMPLETION_TYPE ?? process.env.API_TYPE ?? process.env.COMPLETION_TYPE ?? (PROVIDER && PROVIDER_DEFAULT_TYPE[PROVIDER]) ?? "chat");
const API_BASE = ((PROVIDER && PROVIDER_BASES[PROVIDER]) ?? "https://api.githubcopilot.com").replace(/\/$/, "");
const API_URL = process.env.INPUT_API_URL ?? process.env.API_URL ?? (API_BASE + API_TYPE_PATHS[API_TYPE]);
if (["custom", "azure", "aws"].includes(PROVIDER ?? "") && !process.env.INPUT_API_URL && !process.env.API_URL) {
const API_TYPE = (0, config_1.resolveApiType)(PROVIDER, process.env.INPUT_API_TYPE ?? process.env.INPUT_COMPLETION_TYPE ?? process.env.API_TYPE ?? process.env.COMPLETION_TYPE);
const API_URL = (0, config_1.resolveApiUrl)(PROVIDER, API_TYPE, process.env.INPUT_API_URL ?? process.env.API_URL);
if ((0, config_1.requiresApiUrl)(PROVIDER) && !process.env.INPUT_API_URL && !process.env.API_URL) {
console.error(`api_url is required when provider is '${PROVIDER}'.`);
process.exit(1);
}
const API_KEY = process.env.INPUT_API_KEY ?? process.env.API_KEY ?? GH_PAT ?? "";
// EXPLICIT_API_KEY is the raw api_key input (personal PAT or provider key)
// For Copilot, api_key should be a personal GitHub PAT with Copilot subscription
// Use || (not ??) so empty strings (from unset GitHub Actions secrets) fall through
const EXPLICIT_API_KEY = process.env.INPUT_API_KEY || process.env.API_KEY || undefined;
let API_KEY = EXPLICIT_API_KEY ?? GH_PAT ?? "";
function fetchJson(url, options, body) {
return new Promise((resolve, reject) => {
const req = https.request(url, options, (res) => {
Expand Down Expand Up @@ -171,11 +209,11 @@ async function askOpenAICompat(systemPrompt, userPrompt) {
"User-Agent": "ai-code-review",
};
// Azure uses api-key header instead of Bearer
if (PROVIDER === "azure" || API_URL.includes(".openai.azure.com")) {
if ((0, config_1.isAzureProvider)(PROVIDER, API_URL)) {
delete headers["Authorization"];
headers["api-key"] = API_KEY;
}
if (API_URL.includes("githubcopilot.com")) {
if ((0, config_1.isCopilotUrl)(API_URL)) {
headers["editor-version"] = "vscode/1.95.0";
headers["editor-plugin-version"] = "copilot-chat/0.22.0";
headers["openai-organization"] = "github-copilot";
Expand Down Expand Up @@ -258,8 +296,9 @@ async function askResponses(systemPrompt, userPrompt) {
// 3. โพสต์คำตอบกลับลงใน GitHub PR
async function postCommentToPr(message) {
const url = `https://api.github.com/repos/${REPO}/issues/${PR_NUMBER}/comments`;
const provider = PROVIDER ?? "copilot";
const payload = JSON.stringify({
body: `## 🤖 GitHub Copilot Code Review Bot\n\n${message}`,
body: `## ${TITLE} — \`${MODEL}\` via \`${provider}\`\n\n${message}`,
});
const options = {
method: "POST",
Expand All @@ -280,12 +319,41 @@ async function postCommentToPr(message) {
}
}
// --- Main Execution ---
async function getCopilotToken(ghPat) {
const url = "https://api.github.com/copilot_internal/v2/token";
const options = {
method: "GET",
headers: {
Authorization: `token ${ghPat}`,
Accept: "application/json",
"User-Agent": "ai-code-review",
},
};
const { status, data, text } = await fetchJson(url, options);
if (status !== 200 || !data.token) {
throw new Error(`Failed to get Copilot token: ${status} ${text.slice(0, 200)}`);
}
console.log("Copilot token obtained.");
return data.token;
}
async function main() {
if (!GH_PAT || !PR_NUMBER || !REPO) {
console.error("Missing required environment variables. Set GH_PAT, PR_NUMBER, REPO.");
process.exit(1);
}
console.log("Fetching PR diff...");
// Exchange PAT for Copilot session token when using Copilot API.
// Uses api_key (personal PAT with Copilot subscription) if provided,
// otherwise falls back to gh_pat. Note: GITHUB_TOKEN will fail here — use a personal PAT.
if ((0, config_1.isCopilotUrl)(API_URL)) {
const patForExchange = EXPLICIT_API_KEY ?? GH_PAT ?? "";
if (!patForExchange) {
console.error("Copilot requires a GitHub personal PAT with Copilot subscription. Set api_key (preferred) or gh_pat.");
process.exit(1);
}
console.log("Exchanging GitHub PAT for Copilot session token...");
API_KEY = await getCopilotToken(patForExchange);
}
const diff = await getPrDiff();
if (!diff) {
console.log("No diff found or diff is too large.");
Expand Down
Loading
Loading