When a pull request opens, waiting for a human review can stall momentum. Imagine an AI instantly proposing a first draft of the changes. By connecting GitHub Copilot's fresh suggestion endpoint to EventBridge Pipes, you can automate that magic. This post shows you how to turn a webhook into a live coding assistant.
Why EventBridge Pipes are Perfect for AI‑Driven PR Workflows
The problem
A pull‑request (PR) opened event lives inside GitHub. Traditional bots poll the API or run on a schedule, which adds latency and complexity.
The solution
EventBridge Pipes act like a conveyor belt that moves an event from a source (GitHub webhook) straight to a target (a Lambda function) without you writing glue code.
In plain English: Think of a pipe as a kitchen counter where a raw ingredient (the PR event) slides automatically to the next chef (your Lambda) who prepares it.
Why this matters for AI:
- The moment GitHub tells us a PR opened, the pipe triggers our Lambda instantly.
- No extra polling loops, so the AI suggestion arrives while the author is still typing.
Key pieces
| Piece | What it does | SDK used |
|---|---|---|
| EventBridge Pipe | Connects a source to a target, handling retries and DLQs | @aws-sdk/client-eventbridge |
| Lambda | Small serverless function that runs your code | Node 22 runtime |
| GitHub Copilot Suggestion API | Returns a code snippet based on the PR diff | native fetch
|
| Octokit | Official GitHub client library for posting comments | octokit |
Setting Up the GitHub Webhook → EventBridge Integration
Why we need a webhook
GitHub can push a webhook (an HTTP POST) whenever a PR is opened. That POST becomes the source event for our pipe.
How to create the pipe
- Create an EventBridge Pipe in the AWS console or via the SDK.
- Source – choose API Destination that points to the GitHub webhook URL you set up.
- Target – choose AWS Lambda (the function we’ll write next).
Tip: When you register the API Destination, enable IAM role authentication and give the destination permission to invoke the pipe.
Minimal TypeScript to create the pipe (run once with Node)
import { EventBridgeClient, CreatePipeCommand } from "@aws-sdk/client-eventbridge";
// 1️⃣ Set up a client that talks to the EventBridge service
const eb = new EventBridgeClient({ region: "us-east-1" });
// 2️⃣ Define the pipe – source is the GitHub webhook (API Destination), target is a Lambda ARN
const pipeParams = {
Name: "GitHubPRToCopilotPipe",
// The API Destination you created that points to GitHub's webhook URL
Source: "arn:aws:events:us-east-1:123456789012:api-destination/GitHubWebhookDestination",
// The Lambda that will process the event
Target: "arn:aws:lambda:us-east-1:123456789012:function:CopilotSuggestionHandler",
// Optional: dead‑letter queue ARN for failed events
// DeadLetterConfig: { Arn: "arn:aws:sqs:us-east-1:123456789012:MyDLQ" },
};
async function createPipe() {
try {
const cmd = new CreatePipeCommand(pipeParams);
const response = await eb.send(cmd);
console.log("Pipe created:", response.Arn);
} catch (err) {
console.error("Failed to create pipe:", err);
}
}
createPipe();
Explanation of each line
-
EventBridgeClient– object that lets us call EventBridge APIs. -
CreatePipeCommand– the specific request to make a new pipe. -
Source– the ARN of the API Destination you configured with GitHub. -
Target– the ARN of the Lambda we’ll write next.
Key takeaway: The pipe removes the need for a separate listener service; GitHub’s POST goes straight to your Lambda.
Lambda Code: Calling Copilot's Suggestion API with Native Fetch
Why use native fetch
Node 22 ships with the standard fetch API, so we don’t need an extra HTTP client library. It keeps the Lambda bundle small and fast.
The tricky part: required header
Copilot’s suggestion endpoint refuses calls without a special header called x-copilot-client-id. If you forget it, GitHub returns a 401 (unauthorized) error that looks like a generic failure.
Full Lambda handler
import { APIGatewayProxyEvent, APIGatewayProxyResult } from "aws-lambda";
import { Octokit } from "octokit";
// 1️⃣ Copilot endpoint (hypothetical URL – replace with the real one)
const COPILOT_SUGGEST_URL = "https://api.githubcopilot.com/v1/suggest";
// 2️⃣ Your Copilot client identifier – you obtain this from the Copilot dashboard
const COPILOT_CLIENT_ID = process.env.COPILOT_CLIENT_ID!;
// 3️⃣ GitHub token for posting comments (must have repo scope)
const GITHUB_TOKEN = process.env.GITHUB_TOKEN!;
// 4️⃣ Octokit instance – a thin wrapper around GitHub's REST API
const octokit = new Octokit({ auth: GITHUB_TOKEN });
export const handler = async (
event: APIGatewayProxyEvent
): Promise<APIGatewayProxyResult> => {
// --------------------------------------------------------------
// Step 1 – Extract PR information from the EventBridge payload
// --------------------------------------------------------------
const body = JSON.parse(event.body ?? "{}");
const pullRequest = body.pull_request;
if (!pullRequest) {
// Not a PR opened event – ignore
return { statusCode: 200, body: "Not a PR event" };
}
const { number: prNumber, html_url: prUrl, head: { sha } } = pullRequest;
const repoOwner = body.repository.owner.login;
const repoName = body.repository.name;
// --------------------------------------------------------------
// Step 2 – Ask Copilot for a suggestion
// --------------------------------------------------------------
// Build the request payload Copilot expects – simplified for this demo
const payload = {
// Provide the diff or file list; here we send the latest commit SHA
// In a real setup you might fetch the diff first.
sha,
repository: `${repoOwner}/${repoName}`
};
let suggestion: string | null = null;
try {
const response = await fetch(COPILOT_SUGGEST_URL, {
method: "POST",
// Important: Copilot needs the client‑id header or it returns 401
headers: {
"Content-Type": "application/json",
"x-copilot-client-id": COPILOT_CLIENT_ID,
// Copilot limits request time; we give it a generous 30 s timeout
// fetch's AbortController handles the timeout
},
body: JSON.stringify(payload),
// Abort after 30 seconds to respect Copilot's limit
signal: AbortSignal.timeout(30_000),
});
if (!response.ok) {
// 401 is the most common mistake if the header is missing
console.error("Copilot request failed:", response.status, await response.text());
throw new Error(`Copilot error ${response.status}`);
}
const data = await response.json();
// Assume Copilot returns { suggestion: "code snippet" }
suggestion = data.suggestion;
} catch (err) {
console.error("Error calling Copilot:", err);
// Let the pipe's retry policy handle transient failures
throw err;
}
// --------------------------------------------------------------
// Step 3 – Post the suggestion back to the PR as a comment
// --------------------------------------------------------------
if (suggestion) {
await octokit.rest.issues.createComment({
owner: repoOwner,
repo: repoName,
issue_number: prNumber,
body: `🤖 **Copilot suggestion**\n\n\`\`\`typescript\n${suggestion}\n\`\`\``,
});
}
// --------------------------------------------------------------
// Step 4 – Return success to EventBridge
// --------------------------------------------------------------
return {
statusCode: 200,
body: JSON.stringify({ message: "Suggestion posted", pr: prUrl }),
};
};
Line‑by‑line explanation
- Lines 1‑4 – import types for the Lambda handler and the Octokit client.
- Lines 7‑10 – set constants for the Copilot endpoint, client ID, and GitHub token. Both IDs come from environment variables you configure in the Lambda console.
- Lines 13‑15 – create an Octokit instance that will talk to GitHub later.
-
Lines 17‑24 – parse the incoming EventBridge payload; GitHub’s webhook format puts the PR under
pull_request. - Lines 28‑34 – pull out the PR number, URL, latest commit SHA, and repository identifiers.
- Lines 38‑48 – build a minimal request body for Copilot. Real implementations may need to fetch the file diff first.
-
Lines 50‑70 –
fetchthe suggestion. Thex-copilot-client-idheader satisfies Copilot’s authentication requirement. AnAbortSignalenforces the 30‑second limit. If the response isn’tok, we log the status (401 is the silent killer) and throw to let EventBridge retry. - Lines 72‑82 – on success, use Octokit to create a comment on the PR. The comment wraps the suggestion in a fenced code block so it renders nicely.
- Lines 84‑89 – return a 200 status so EventBridge knows the pipe succeeded.
Helpful tip: Put the Copilot client ID and GitHub token in the Lambda’s encrypted environment variables. That way you never expose secrets in code.
Posting the AI‑Generated Suggestion Back to GitHub
Why comment instead of push
Pushing code directly could break the branch before a human reviews it. Adding a comment lets the author decide whether to apply the suggestion.
How Octokit works
Octokit is a thin wrapper around GitHub’s REST API. It handles pagination, authentication, and request building for you.
Minimal Octokit usage example (stand‑alone)
import { Octokit } from "octokit";
// The personal access token must have the `repo` scope
const token = process.env.GITHUB_TOKEN!;
const octokit = new Octokit({ auth: token });
async function commentOnPR() {
await octokit.rest.issues.createComment({
owner: "alice", // repository owner
repo: "awesome‑project", // repository name
issue_number: 42, // PR number (issues and PRs share the same ID space)
body: "Hello from the bot! 👋", // markdown comment body
});
console.log("Comment posted");
}
commentOnPR().catch(console.error);
Explanation
-
octokit.rest.issues.createComment– the endpoint used for both issues and PRs. -
owner,repo,issue_number– identify the PR we want to comment on. -
body– any markdown text; GitHub renders fenced code blocks automatically.
In plain English: Think of Octokit as a friendly receptionist that knows the exact hallway (API endpoint) to deliver your note (comment) to the right office (PR).
Wiring the comment into the Lambda
The previous Lambda code already includes the comment step (lines 73‑82). The key points to remember are:
- Use the same
owner,repo, andissue_numberyou extracted from the webhook. - Include the suggestion inside triple backticks and specify the language (
typescript) for syntax highlighting.
Testing, Observability, and Common Pitfalls
Why testing matters
Event‑driven pipelines can fail silently if you don’t surface errors. A missing header, a timeout, or a permission issue will cause the pipe to dead‑letter the event.
Quick local test
You can invoke the Lambda locally with the AWS SAM CLI:
sam local invoke CopilotSuggestionHandler \
-e examples/pull_request_opened_event.json \
--env-vars examples/env.json
-
examples/pull_request_opened_event.json– a copy of a real GitHub webhook payload (redacted). -
examples/env.json– containsCOPILOT_CLIENT_IDandGITHUB_TOKEN.
The command prints the Lambda’s JSON response and any console logs, letting you verify the fetch and Octokit calls.
Observability tips
-
CloudWatch Logs – every
console.errororconsole.logends up here. Add a correlation ID (event.id) to trace a single PR through the pipeline. -
Pipe metrics – EventBridge Pipes expose
Invocations,FailedInvocations, andThrottledInvocations. Set a CloudWatch alarm onFailedInvocations > 0to catch 401 errors early.
Common pitfalls
| Symptom | Likely cause | Fix |
|---|---|---|
| 401 response from Copilot | Missing x-copilot-client-id header or wrong value |
Double‑check the environment variable and header spelling |
| Lambda times out after 30 s | Copilot request exceeded its limit or network slowdown | Ensure you use AbortSignal.timeout(30_000) and increase the Lambda timeout to 60 s for safety |
| No comment appears on PR | Octokit token lacks repo scope or the owner/repo values are wrong |
Regenerate the token with proper scopes; log the extracted identifiers |
| Pipe moves event to DLQ | EventBridge cannot invoke Lambda (missing permission) | Add lambda:InvokeFunction permission for the EventBridge service principal on the Lambda’s resource policy |
Tip: When you first see events land in the dead‑letter queue, inspect the message body; it usually contains the exact error message from the Lambda.
The Takeaway
- EventBridge Pipes let you stitch a GitHub webhook directly to a Lambda, removing the need for custom polling code.
- Copilot’s suggestion endpoint requires the
x-copilot-client-idheader and must finish within 30 seconds; forgetting the header shows up as a silent 401. - A tiny TypeScript Lambda can fetch a suggestion with native
fetch, extract the snippet, and post it back to the PR using Octokit. - Keep secrets in encrypted Lambda environment variables and grant only the permissions you need (GitHub
reposcope, EventBridge invoke rights). - Test locally with SAM, watch CloudWatch logs, and set alarms on Pipe failure metrics to catch issues early.
Now you have a live, AI‑powered assistant that whispers a first draft the moment a PR opens, helping your team keep the code flow moving. Happy coding!
Transparency notice
This article was written with the help of an AI system — Groq (GPT OSS 120B).
Published: 2026-11-11 · Primary focus: eventbridge
All code blocks are intended to be correct and runnable, but please verify them
against the official docs for the tools mentioned before using in production.Find an error? Drop a comment — corrections are always welcome.
Top comments (0)