Copilot SDKでAI駆動のGitHub issueトリアージを構築
GitHubはCopilot SDKを活用したIssueCrushアプリの開発事例を公開し、サーバーサイド統合によるセキュアで堅牢なAI issue分類の実装パターンを示した。
キーポイント
IssueCrushのUI/UXと機能概要
GitHub issueをスワイプ可能なカード形式で表示し、Copilotによる即時要約とアクション提案により、メンテナの分類作業を大幅に高速化する。
サーバーサイド統合の技術的必然性
React Native環境ではNode.jsランタイム制約があるため、Copilot SDKはサーバー側で実行し、CLIプロセスとJSON-RPC経由で通信する設計を採用。
堅牢なアーキテクチャ設計パターン
シングルSDKインスタンスの共有、シークレット管理、AI停止時のフォールバック機能、およびログ記録を実装し、実務レベルでのセキュリティと信頼性を確保。
バックエンド連携とエラーハンドリング
認証トークンを取得してAPIに通信し、Copilotサブスクリプションがない場合の403エラーを適切に処理する仕組みを実装している。
ReactのUI状態管理
非同期処理で要約を生成し、読み込み状態を表示しながら問題リストを更新する。エラー発生時もコンソールに記録してアプリケーションのクラッシュを防ぐ。
クライアント側キャッシュとUX
生成された要約を問題オブジェクトに直接保存し、画面遷移後も再API呼び出しなしで即座にキャッシュされたテキストを表示する。
エラーハンドリングとフォールバック設計
AIサービス失敗時にサブスクリプションエラーは明確な403を返し、その他のエラーではissueのタイトルやラベルなどメタデータから自動生成された要約に安全にフォールバックする。
影響分析・編集コメントを表示
影響分析
本記事は、Copilot SDKの商用・カスタムアプリへの適用可能性を具体例で示しており、開発者ツールのAI統合における標準的なアーキテクチャパターンを確立する。サーバーサイドでの実行とフォールバック設計は、実務レベルでのAI導入におけるセキュリティと信頼性のバランスを示す参考となる。これにより、GitHubエコシステム内の開発ワークフロー自動化がさらに加速すると期待される。
編集コメント
Copilot SDKのサーバーサイドラップパターンは、実務でのAI導入におけるセキュリティと堅牢性のベストプラクティスを示しており、開発者向けツールのAI統合における標準設計として注目される。
優雅な機能低下 (Graceful Degradation)
AIサービスは失敗することがあります。ネットワークの問題、レート制限、サービス停止は発生します。サーバーは2つの障害モードを処理します:サブスクリプションエラーは403を返すため、クライアントは明確なメッセージを表示できます。それ以外のすべてのエラーでは、issueのメタデータから構築された要約にフォールバックします。
} catch (error) {
// エラー時にクリーンアップ
try {
if (session) await session.disconnect().catch(() => {});
if (client) await client.stop().catch(() => {});
} catch (cleanupError) {
// クリーンアップエラーは無視
}
const errorMessage = error.message.toLowerCase();
// Copilotサブスクリプションエラーは明確な403を返す
if (errorMessage.includes('unauthorized') ||
errorMessage.includes('forbidden') ||
errorMessage.includes('copilot') ||
errorMessage.includes('subscription')) {
return res.status(403).json({
error: 'Copilotアクセスが必要です',
message: 'AI要約にはGitHub Copilotサブスクリプションが必要です。',
requiresCopilot: true
});
}
// それ以外はメタデータベースの要約にフォールバック
const fallbackSummary = generateFallbackSummary(issue);
res.json({ summary: fallbackSummary, fallback: true });
}フォールバックは、既に持っている情報から有用な要約を構築します:
function generateFallbackSummary(issue) {
const parts = [issue.title];
if (issue.labels?.length) {
parts.push(`\nラベル: ${issue.labels.map(l => l.name).join(', ')}`);
}
if (issue.body) {
const firstSentence = issue.body.split(/[.!?]\s/)[0];
if (firstSentence && firstSentence.length < 200) {
parts.push(`\n\n${firstSentence}.`);
}
}
parts.push('\n\n次のステップを決定するには、issueの詳細を確認してください。');
return parts.join('');
}注目すべきその他のパターン
サーバーはAIの可用性を示す/healthエンドポイントを公開します。クライアントは起動時にこれをチェックし、バックエンドがサポートできない場合は要約ボタンを完全に非表示にします。壊れたボタンはありません。
要約は事前生成ではなく、オンデマンドで生成されます。これによりAPIコストが抑えられ、ユーザーがissueを読まずにスワイプした場合の無駄な呼び出しを回避できます。
SDKはトップレベルのrequireではなく、await import('@github/copilot-sdk')で読み込まれます。これにより、SDKに問題があってもサーバーを起動できるため、デプロイとデバッグがスムーズになります。
依存関係
{
"dependencies": {
"@github/copilot-sdk": "^0.1.14",
"express": "^5.2.1"
}
}SDKはJSON-RPCを介してCopilot CLIプロセスと通信します。Copilot CLIがインストールされ、PATHで利用可能である必要があります。最小Node.jsバージョンについては、SDKのパッケージ要件を確認してください。
これを構築して学んだこと
サーバーサイドが正しい選択です。SDKにはCopilot CLIバイナリが必要ですが、それを電話にインストールすることはありません。サーバーで実行することで、AIロジックを1か所に保ち、モバイルクライアントを簡素化し、認証情報がバックエンドから流出しないようにします。
プロンプトの長さよりも構造が重要です。タイトル、ラベル、作成者などの整理されたメタデータをモデルに提供すると、issue本文全体を生テキストとしてダンプするよりもはるかに優れた要約が生成されます。モデルに作業材料を与えれば、有用な結果を返してくれます。
常にフォールバックを用意してください。AIサービスはダウンします。レート制限は発生します。初日から優雅な機能低下を設計してください。AI部分がオフラインでも、ユーザーはissueをトリアージできるはずです。
セッションをクリーンアップしてください。SDKには明示的なクリーンアップが必要です:disconnect()、次にstop()。私は一度disconnect()呼び出しを省略し、メモリリークを2時間追いかけました。毎回try/finallyを使用してください。
結果をキャッシュしてください。要約を取得したら、issueオブジェクトに保存します。ユーザーがスワイプして戻ってきた場合、キャッシュされたバージョンが即座にレンダリングされます。2回目のAPI呼び出し、無駄な費用、追加の遅延はありません。
AIはメンテナンスを持続可能にできます。トリアージは、人々を消耗させる目に見えないタスクの1つです。誰もそれに感謝せず、すぐに山積みになります。50件のissueを処理する時間を半分にできれば、それはコードレビュー、メンタリング、または通知バッジを恐れないための時間を取り戻せます。Copilot SDKは1つのツールですが、より大きなアイデアの方が重要です:メンテナンスの中であなたを消耗させる部分を見て、AIが最初のパスを処理できるかどうか考えてみてください。
自分で試してみてください
@github/copilot-sdkは、インテリジェントな開発者ツールを構築するための真の可能性を開きます。React Nativeのクロスプラットフォームリーチと組み合わせることで、ネイティブで高速に感じられる方法でAI搭載ワークフローをモバイルに持ち込めます。
同様のものを構築している場合は、ここで概説したサーバーサイドパターンから始めてください。それは動作する統合への最も簡単な道であり、アプリとともにスケールします。ソースコードはGitHub:AndreaGriffiths11/IssueCrushで利用可能です。
Copilot SDKを使って、他に何が構築できるかを確認してください。Getting Startedガイドでは、約5行のコードで最初の統合を案内します。フィードバックやアイデアがありますか?SDKディスカッションで会話に参加してください。
この投稿「Copilot SDKを使用したAI搭載GitHub issueトリアージの構築」は、The GitHub Blogで最初に公開されました。
原文を表示
The Copilot SDK lets you add the same AI that powers Copilot Chat to your own applications. I wanted to see what that looks like in practice, so I built an issue triage app called IssueCrush. Here’s what I learned and how you can get started.
If you’ve ever maintained an open source project, or worked on a team with active repositories, you know the feeling. You open GitHub and see that notification badge: 47 issues. Some are bugs, some are feature requests, some are questions that should be discussions, and some are duplicates of issues from three years ago.
The mental overhead of triaging issues is real. Each one requires context-switching: read the title, scan the description, check the labels, think about priority, decide what to do. Multiply that by dozens of issues across multiple repositories, and suddenly your brain is mush.
I wanted to make this faster. And with the GitHub Copilot SDK, I found a way.
Enter IssueCrush: Swipe right to ship
IssueCrush shows your GitHub issues as swipeable cards. Left to close, right to keep. When you tap “Get AI Summary,” Copilot reads the issue and tells you what it’s about and what to do with it. Instead of reading through every lengthy description, maintainers can get instant, actionable context to make faster triage decisions. Here’s how I integrated the GitHub Copilot SDK to make it happen.

The architecture challenge
The first technical decision was figuring out where to run the Copilot SDK. React Native apps can’t directly use Node.js packages, and the Copilot SDK requires a Node.js runtime. Internally, the SDK manages a local Copilot CLI process and communicates with it over JSON-RPC. Because of this dependency on the CLI binary and a Node environment, the integration must run server-side rather than directly in a React Native app. This means the server must have the Copilot CLI installed and available on the system PATH.
I settled on a server-side integration pattern:

Here’s why this setup works:
Single SDK instance shared across all clients, so you’re not spinning up a new connection per mobile client. The server manages one instance for every request. Less overhead, fewer auth handshakes, simpler cleanup.
Server-side secrets for Copilot authentication, to keep credentials secure. Your API tokens never touch the client. They live on the server where they belongnot inside a React Native bundle someone can decompile.
Graceful degradation when AI is unavailable, so you can still triage issues even if the Copilot service goes down or times out. The app falls back to a basic summary. AI makes triage faster, but it shouldn’t be a single point of failure.
Logging of requests for debugging and monitoring, because every prompt and response passes through your server. You can track latency, catch failures, and debug prompt issues without bolting instrumentation onto the mobile client.
Before you build something like this, you need:
The Copilot CLI installed on your server.
A GitHub Copilot subscription, or a BYOK configuration with your own API keys.
The Copilot CLI authenticated. Run copilot auth on your server, or set a COPILOT_GITHUB_TOKEN environment variable.
How to implement the Copilot SDK integration
The Copilot SDK uses a session-based model. You start a client (which spawns the CLI process), create a session, send messages, then clean up.
const { CopilotClient, approveAll } = await import('@github/copilot-sdk');
let client = null;
let session = null;
try {
// 1. Initialize the client (spawns Copilot CLI in server mode)
client = new CopilotClient();
await client.start();
// 2. Create a session with your preferred model
session = await client.createSession({
model: 'gpt-4.1',
onPermissionRequest: approveAll,
});
// 3. Send your prompt and wait for response
const response = await session.sendAndWait({ prompt });
// 4. Extract the content
if (response && response.data && response.data.content) {
const summary = response.data.content;
// Use the summary...
}
} finally {
// 5. Always clean up
if (session) await session.disconnect().catch(() => {});
if (client) await client.stop().catch(() => {});
}
Key SDK patterns
- Lifecycle management
The SDK follows a strict lifecycle: start() → createSession() → sendAndWait() → disconnect() → stop().
Here’s something I learned the hard way: failing to clean up sessions leaks resources. I spent two hours debugging memory issues before realizing I’d forgotten a disconnect() call. Wrap every session interaction in try/finally. The .catch(() => {}) on cleanup calls prevents cleanup errors from masking the original error.
- Prompt engineering for triage
Prompt structure gives the model enough context to do its job. I provide structured information about the issue rather than dumping raw text:
const prompt = `You are analyzing a GitHub issue to help a developer quickly understand it and decide how to handle it.
Issue Details:
- Title: ${issue.title}
- Number: #${issue.number}
- Repository: ${issue.repository?.full_name || 'Unknown'}
- State: ${issue.state}
- Labels: ${issue.labels?.length ? issue.labels.map(l => l.name).join(', ') : 'None'}
- Created: ${issue.created_at}
- Author: ${issue.user?.login || 'Unknown'}
Issue Body:
${issue.body || 'No description provided.'}
Provide a concise 2-3 sentence summary that:
- Explains what the issue is about
- Identifies the key problem or request
- Suggests a recommended action (e.g., "needs investigation", "ready to implement", "assign to backend team", "close as duplicate")
Keep it clear, actionable, and helpful for quick triage. No markdown formatting.`;
The labels and author context matter more than you’d think. An issue from a first-time contributor needs different handling than one from a core maintainer, and the AI uses this information to adjust its summary.
- Response handling
The sendAndWait() method returns the assistant’s response once the session goes idle. Always validate that the response chain exists before accessing nested properties:
const response = await session.sendAndWait({ prompt }, 30000); // 30 second timeout
let summary;
if (response && response.data && response.data.content) {
summary = response.data.content;
} else {
throw new Error('No content received from Copilot');
}
The second argument to sendAndWait() is a timeout in milliseconds. Set it high enough for complex issues but low enough that users aren’t staring at a spinner. I’ve seen enough “undefined is not an object” errors to know you should never skip the null checks on the response chain.
Client-side service layer
On the React Native side, I wrap the API calls in a service class that handles initialization and error states:
// src/lib/copilotService.ts
import type { GitHubIssue } from '../api/github';
import { getToken } from './tokenStorage';
export interface SummaryResult {
summary: string;
fallback?: boolean;
requiresCopilot?: boolean;
}
export class CopilotService {
private backendUrl = process.env.EXPO_PUBLIC_API_URL || 'http://localhost:3000';
async initialize(): Promise<{ copilotMode: string }> {
try {
const response = await fetch(${this.backendUrl}/health);
const data = await response.json();
console.log('Backend health check:', data);
return { copilotMode: data.copilotMode || 'unknown' };
} catch (error) {
console.error('Failed to connect to backend:', error);
throw new Error('Backend server not available');
}
}
async summarizeIssue(issue: GitHubIssue): Promise<SummaryResult> {
try {
const token = await getToken();
if (!token) {
throw new Error('No GitHub token available');
}
const response = await fetch(${this.backendUrl}/api/ai-summary, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({ issue, token }),
});
const data = await response.json();
if (!response.ok) {
if (response.status === 403 && data.requiresCopilot) {
return {
summary: data.message || 'AI summaries require a GitHub Copilot subscription.',
requiresCopilot: true,
};
}
throw new Error(data.error || 'Failed to generate summary');
}
return {
summary: data.summary || 'Unable to generate summary',
fallback: data.fallback || false,
};
} catch (error) {
console.error('Copilot summarization error:', error);
throw error;
}
}
}
export const copilotService = new CopilotService();
React Native integration
The UI is straightforward React state management. Tap the button, call the service, cache the result:
const [loadingAiSummary, setLoadingAiSummary] = useState(false);
const handleGetAiSummary = async () => {
const issue = issues[currentIndex];
if (!issue || issue.aiSummary) return;
setLoadingAiSummary(true);
try {
const result = await copilotService.summarizeIssue(issue);
setIssues(prevIssues =>
prevIssues.map((item, index) =>
index === currentIndex ? { ...item, aiSummary: result.summary } : item
)
);
} catch (error) {
console.error('AI Summary error:', error);
} finally {
setLoadingAiSummary(false);
}
};
Once a summary exists on the issue object, the card swaps the button for the summary text. If the user swipes away and comes back, the cached version renders instantly. No second API call needed.
Graceful degradation
AI services can fail. Network issues, rate limits, and service outages happen. The server handles two failure modes: subscription errors return a 403 so the client can show a clear message, and everything else falls back to a summary built from issue metadata.
} catch (error) {
// Clean up on error
try {
if (session) await session.disconnect().catch(() => {});
if (client) await client.stop().catch(() => {});
} catch (cleanupError) {
// Ignore cleanup errors
}
const errorMessage = error.message.toLowerCase();
// Copilot subscription errors get a clear 403
if (errorMessage.includes('unauthorized') ||
errorMessage.includes('forbidden') ||
errorMessage.includes('copilot') ||
errorMessage.includes('subscription')) {
return res.status(403).json({
error: 'Copilot access required',
message: 'AI summaries require a GitHub Copilot subscription.',
requiresCopilot: true
});
}
// Everything else falls back to a metadata-based summary
const fallbackSummary = generateFallbackSummary(issue);
res.json({ summary: fallbackSummary, fallback: true });
}
The fallback builds a useful summary from what we already have:
function generateFallbackSummary(issue) {
const parts = [issue.title];
if (issue.labels?.length) {
parts.push(\nLabels: ${issue.labels.map(l => l.name).join(', ')});
}
if (issue.body) {
const firstSentence = issue.body.split(/[.!?]\s/)[0];
if (firstSentence && firstSentence.length < 200) {
parts.push(\n\n${firstSentence}.);
}
}
parts.push('\n\nReview the full issue details to determine next steps.');
return parts.join('');
}
A few other patterns worth noting
The server exposes a /health endpoint that signals AI availability. Clients check it on startup and hide the summary button entirely if the backend can’t support it. No broken buttons.
Summaries are generated on -demand, not preemptively. This keeps API costs down and avoids wasted calls when users swipe past an issue without reading it.
The SDK is loaded with await import('@github/copilot-sdk') instead of a top-level require. This lets the server start even if the SDK has issues, which makes deployment and debugging smoother.
Dependencies
{
"dependencies": {
"@github/copilot-sdk": "^0.1.14",
"express": "^5.2.1"
}
}
The SDK communicates with the Copilot CLI process via JSON-RPC. You need the Copilot CLI installed and available in your PATH. Check the SDK’s package requirements for the minimum Node.js version.
What I learned building this
Server-side is the right call. The SDK needs the Copilot CLI binary, and you’re not installing that on a phone. Running it on a server keeps AI logic in one place, simplifies the mobile client, and means credentials never leave the backend.
Prompt structure matters more than prompt length. Feeding the model organized metadata like title, labels, and author produces much better summaries than dumping the entire issue body as raw text. Give the model something to work with, and it’ll give you something useful back.
Always have a fallback. AI services go down. Rate limits happen. Design for graceful degradation from day one. Your users should still be able to triage issues even if the AI piece is offline.
Clean up your sessions. The SDK requires explicit cleanup: disconnect() then stop(). I skipped a disconnect() call once and spent two hours chasing a memory leak. Use try/finally every time.
Cache the results. Once you have a summary, store it on the issue object. If the user swipes away and comes back, the cached version renders instantly. No second API call, no wasted money, no extra latency.
AI can make maintainership sustainable. Triage is one of those invisible tasks that burns people out. Nobody thanks you for it, and it piles up fast. If you can cut the time it takes to process 50 issues in half, that’s time back for code review, mentoring, or just not dreading your notification badge. The Copilot SDK is one tool, but the bigger idea matters more: look at the parts of maintaining that drain you and ask if AI can take a first pass.
Try it yourself
The @github/copilot-sdk opens real possibilities for building intelligent developer tools. Combined with React Native’s cross-platform reach, you can bring AI-powered workflows to mobile in a way that feels native and fast.
If you’re building something similar, start with the server-side pattern I’ve outlined here. It’s the simplest path to a working integration, and it scales with your app. The source code is available on GitHub: AndreaGriffiths11/IssueCrush.
Get started with the Copilot SDK to see what else you can build. The Getting Started guide walks you through your first integration in about five lines of code. Have feedback or ideas? Join the conversation in the SDK discussions.
The post Building AI-powered GitHub issue triage with the Copilot SDK appeared first on The GitHub Blog.
関連記事
今日のまとめ
AI日報で今日の重要ニュースをまとめ読み