The buildEventContext() function transforms GitHub webhook events into a normalized EventContext structure that Warden uses for skill execution.
Function Signature
async function buildEventContext(
eventName: string,
eventPayload: unknown,
repoPath: string,
octokit: Octokit
): Promise<EventContext>
GitHub event type (e.g., 'pull_request', 'schedule').In GitHub Actions, read from github.event_name:const eventName = process.env.GITHUB_EVENT_NAME || 'pull_request';
GitHub webhook payload. In Actions, read from GITHUB_EVENT_PATH:import { readFileSync } from 'fs';
const eventPath = process.env.GITHUB_EVENT_PATH!;
const eventPayload = JSON.parse(readFileSync(eventPath, 'utf-8'));
Absolute path to the cloned repository on disk.In Actions:const repoPath = process.env.GITHUB_WORKSPACE!;
Authenticated Octokit instance for fetching PR file metadata:import { Octokit } from '@octokit/rest';
const octokit = new Octokit({
auth: process.env.GITHUB_TOKEN
});
Returns
Normalized event context:interface EventContext {
eventType: GitHubEventType;
action: string;
repository: RepositoryContext;
pullRequest?: PullRequestContext;
repoPath: string;
}
EventContext Fields
GitHub event type:
'pull_request'
'schedule'
'issues'
'issue_comment'
'pull_request_review'
'pull_request_review_comment'
Event action (e.g., 'opened', 'synchronize', 'reopened').
Repository metadata:interface RepositoryContext {
owner: string; // e.g., "getsentry"
name: string; // e.g., "warden"
fullName: string; // e.g., "getsentry/warden"
defaultBranch: string; // e.g., "main"
}
pullRequest
PullRequestContext | undefined
Pull request context (only present for eventType === 'pull_request'):interface PullRequestContext {
number: number;
title: string;
body: string | null;
author: string;
baseBranch: string;
headBranch: string;
headSha: string;
baseSha: string;
files: FileChange[];
}
Absolute path to the repository on disk.
FileChange Fields
Per-file change metadata:interface FileChange {
filename: string;
status: 'added' | 'removed' | 'modified' | 'renamed' | 'copied' | 'changed' | 'unchanged';
additions: number;
deletions: number;
patch?: string; // Unified diff patch
}
Example: GitHub Actions
Minimal GitHub Action integration:
import { buildEventContext } from '@sentry/warden';
import { Octokit } from '@octokit/rest';
import { readFileSync } from 'fs';
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const repoPath = process.env.GITHUB_WORKSPACE!;
const eventName = process.env.GITHUB_EVENT_NAME!;
const eventPayload = JSON.parse(
readFileSync(process.env.GITHUB_EVENT_PATH!, 'utf-8')
);
const context = await buildEventContext(
eventName,
eventPayload,
repoPath,
octokit
);
console.log(`PR #${context.pullRequest?.number}: ${context.pullRequest?.title}`);
console.log(`Files changed: ${context.pullRequest?.files.length}`);
Example: Webhook Server
Handle webhook events in a Node.js server:
import express from 'express';
import { buildEventContext } from '@sentry/warden';
import { Octokit } from '@octokit/rest';
import { execSync } from 'child_process';
import { mkdtempSync } from 'fs';
import { tmpdir } from 'os';
import { join } from 'path';
const app = express();
app.use(express.json());
app.post('/webhooks/github', async (req, res) => {
const eventName = req.headers['x-github-event'] as string;
const eventPayload = req.body;
// Clone repo to temp dir
const repoPath = mkdtempSync(join(tmpdir(), 'warden-'));
const cloneUrl = eventPayload.repository.clone_url;
execSync(`git clone ${cloneUrl} ${repoPath}`);
// Build context
const octokit = new Octokit({ auth: process.env.GITHUB_TOKEN });
const context = await buildEventContext(
eventName,
eventPayload,
repoPath,
octokit
);
// Process event
// ...
res.status(200).send('OK');
});
app.listen(3000);
Example: Filter Context by Paths
Combine with filterContextByPaths() to apply path filters:
import {
buildEventContext,
filterContextByPaths
} from '@sentry/warden';
const context = await buildEventContext(
eventName,
eventPayload,
repoPath,
octokit
);
// Filter to only TypeScript files
const tsContext = filterContextByPaths(context, {
paths: ['**/*.ts', '**/*.tsx'],
ignorePaths: ['**/test/**'],
});
console.log(`Filtered: ${tsContext.pullRequest?.files.length} TS files`);
Error Handling
Throws EventContextError on validation failures:
import {
buildEventContext,
EventContextError
} from '@sentry/warden';
try {
const context = await buildEventContext(
eventName,
eventPayload,
repoPath,
octokit
);
} catch (error) {
if (error instanceof EventContextError) {
console.error('Invalid event payload:', error.message);
// Possible causes:
// - Missing required fields (repository, pull_request, etc.)
// - Invalid field types
// - Malformed webhook payload
}
}
Validation
The function validates:
- Event payload structure - Required fields present and correctly typed
- Repository metadata - Owner, name, default branch
- Pull request data - Number, title, base/head refs (if present)
- Final context - Complete structure matches
EventContextSchema
API Calls
For pull_request events, the function makes paginated API calls to fetch file metadata:
// Fetches all changed files with pagination
const files = await octokit.paginate(octokit.pulls.listFiles, {
owner,
repo,
pull_number,
per_page: 100,
});
Each file includes:
- Filename and status
- Additions/deletions counts
- Unified diff patch (if available)
Non-PR Events
For non-PR events (e.g., schedule), pullRequest is undefined:
const context = await buildEventContext(
'schedule',
schedulePayload,
repoPath,
octokit
);
if (context.pullRequest) {
// PR-specific logic
} else {
// Schedule or other event types
}
Context Usage
The returned context is passed to:
import {
buildEventContext,
loadWardenConfig,
resolveSkillConfigs,
matchTrigger,
resolveSkillAsync,
runSkill,
} from '@sentry/warden';
// Build context
const context = await buildEventContext(
eventName,
eventPayload,
repoPath,
octokit
);
// Load config and match triggers
const config = loadWardenConfig(repoPath);
const triggers = resolveSkillConfigs(config);
const matchedTriggers = triggers.filter(t =>
matchTrigger(t, context, 'github')
);
// Run matched skills
for (const trigger of matchedTriggers) {
const skill = await resolveSkillAsync(trigger.name, repoPath);
const report = await runSkill(skill, context, {
model: trigger.model,
failOn: trigger.failOn,
});
}