Files
claude-hub/src/providers/github/GitHubWebhookProvider.ts
Cheffromspace bf2a517264 feat: Implement Claude orchestration provider for parallel session management (#171)
* feat: Implement Claude orchestration provider for parallel session management

- Add ClaudeWebhookProvider implementing the webhook provider interface
- Create orchestration system for running multiple Claude containers in parallel
- Implement smart task decomposition to break complex projects into workstreams
- Add session management with dependency tracking between sessions
- Support multiple execution strategies (parallel, sequential, wait_for_core)
- Create comprehensive test suite for all components
- Add documentation for Claude orchestration API and usage

This enables super-charged Claude capabilities for the MCP hackathon by allowing
multiple Claude instances to work on different aspects of a project simultaneously,
with intelligent coordination and result aggregation.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* feat: Add session management endpoints for MCP integration

- Add SessionHandler for individual session CRUD operations
- Create endpoints: session.create, session.get, session.list, session.start, session.output
- Fix Claude invocation in Docker containers using proper claude chat command
- Add volume mounts for persistent storage across session lifecycle
- Simplify OrchestrationHandler to create single coordination sessions
- Update documentation with comprehensive MCP integration examples
- Add comprehensive unit and integration tests for new endpoints
- Support dependencies and automatic session queuing/starting

This enables Claude Desktop to orchestrate multiple Claude Code sessions via MCP Server tools.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Update ClaudeWebhookProvider validation for session endpoints

- Make project fields optional for session management operations
- Add validation for session.create requiring session field
- Update tests to match new validation rules
- Fix failing CI tests

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Use Promise.reject for validation errors in parsePayload

- Convert synchronous throws to Promise.reject for async consistency
- Fixes failing unit tests expecting rejected promises

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Mock SessionManager in integration tests to avoid Docker calls in CI

- Add SessionManager mock to prevent Docker operations during tests
- Fix claude-webhook.test.ts to use proper test setup and payload structure
- Ensure all integration tests can run without Docker dependency
- Fix payload structure to include 'data' wrapper

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Mock child_process to prevent Docker calls in CI tests

- Mock execSync and spawn at child_process level to prevent any Docker commands
- This ensures tests work in CI environment without Docker
- Tests now pass both locally and in CI Docker build

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>

* fix: Address PR review comments and fix linter warnings

- Move @types/uuid to devDependencies
- Replace timestamp+Math.random with crypto.randomUUID() for better uniqueness
- Extract magic number into EXTRA_SESSIONS_COUNT constant
- Update determineStrategy return type to use literal union
- Fix unnecessary optional chaining warnings
- Handle undefined labels in GitHub transformers
- Make TaskDecomposer.decompose synchronous
- Add proper eslint-disable comments for intentional sync methods
- Fix all TypeScript and formatting issues

* fix: Mock SessionManager in integration tests to prevent Docker calls in CI

- Add SessionManager mocks to claude-session.test.ts
- Add SessionManager mocks to claude-webhook.test.ts
- Prevents 500 errors when running tests in CI without Docker
- All integration tests now pass without requiring Docker runtime

* fix: Run only unit tests in Docker builds to avoid Docker-in-Docker issues

- Change test stage to run 'npm run test:unit' instead of 'npm test'
- Skips integration tests that require Docker runtime
- Prevents CI failures in Docker container builds
- Integration tests still run in regular CI workflow

* fix: Use Dockerfile CMD for tests in Docker build CI

- Remove explicit 'npm test' command from docker run
- Let Docker use the CMD defined in Dockerfile (npm run test:unit)
- This ensures consistency and runs only unit tests in Docker builds

---------

Co-authored-by: Claude <noreply@anthropic.com>
2025-06-03 12:42:55 -05:00

210 lines
5.6 KiB
TypeScript

import crypto from 'crypto';
import { createLogger } from '../../utils/logger';
import type { WebhookRequest } from '../../types/express';
import type {
WebhookProvider,
BaseWebhookPayload,
RepositoryInfo,
UserInfo,
IssueInfo,
PullRequestInfo
} from '../../types/webhook';
import type {
GitHubRepository,
GitHubUser,
GitHubIssue,
GitHubPullRequest
} from '../../types/github';
const logger = createLogger('GitHubWebhookProvider');
/**
* GitHub-specific webhook payload
*/
export interface GitHubWebhookEvent extends BaseWebhookPayload {
githubEvent: string;
githubDelivery: string;
action?: string;
repository?: GitHubRepository;
sender?: GitHubUser;
installation?: {
id: number;
account: GitHubUser;
};
}
/**
* GitHub webhook provider implementation
*/
export class GitHubWebhookProvider implements WebhookProvider<GitHubWebhookEvent> {
readonly name = 'github';
/**
* Verify GitHub webhook signature
*/
verifySignature(req: WebhookRequest, secret: string): Promise<boolean> {
// eslint-disable-next-line no-sync
return Promise.resolve(this.verifySignatureSync(req, secret));
}
private verifySignatureSync(req: WebhookRequest, secret: string): boolean {
const signature = req.headers['x-hub-signature-256'] as string;
if (!signature) {
logger.warn('No signature found in GitHub webhook request');
return false;
}
try {
const payload = req.rawBody ?? JSON.stringify(req.body);
const hmac = crypto.createHmac('sha256', secret);
const calculatedSignature = 'sha256=' + hmac.update(payload).digest('hex');
// Use timing-safe comparison
if (
signature.length === calculatedSignature.length &&
crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(calculatedSignature))
) {
logger.debug('GitHub webhook signature verified successfully');
return true;
}
logger.warn('GitHub webhook signature verification failed');
return false;
} catch (error) {
logger.error({ err: error }, 'Error verifying GitHub webhook signature');
return false;
}
}
/**
* Parse GitHub webhook payload
*/
parsePayload(req: WebhookRequest): Promise<GitHubWebhookEvent> {
// eslint-disable-next-line no-sync
return Promise.resolve(this.parsePayloadSync(req));
}
private parsePayloadSync(req: WebhookRequest): GitHubWebhookEvent {
const githubEvent = req.headers['x-github-event'] as string;
const githubDelivery = req.headers['x-github-delivery'] as string;
const payload = req.body;
return {
id: githubDelivery || crypto.randomUUID(),
timestamp: new Date().toISOString(),
event: this.normalizeEventType(githubEvent, payload.action),
source: 'github',
githubEvent,
githubDelivery,
action: payload.action,
repository: payload.repository,
sender: payload.sender,
installation: payload.installation,
data: payload
};
}
/**
* Get normalized event type
*/
getEventType(payload: GitHubWebhookEvent): string {
return payload.event;
}
/**
* Get human-readable event description
*/
getEventDescription(payload: GitHubWebhookEvent): string {
const parts = [payload.githubEvent];
if (payload.action) {
parts.push(payload.action);
}
if (payload.repository) {
parts.push(`in ${payload.repository.full_name}`);
}
if (payload.sender) {
parts.push(`by ${payload.sender.login}`);
}
return parts.join(' ');
}
/**
* Normalize GitHub event type to a consistent format
*/
private normalizeEventType(event: string, action?: string): string {
if (!action) {
return event;
}
return `${event}.${action}`;
}
/**
* Transform GitHub repository to generic format
*/
static transformRepository(repo: GitHubRepository): RepositoryInfo {
return {
id: repo.id.toString(),
name: repo.name,
fullName: repo.full_name,
owner: repo.owner.login,
isPrivate: repo.private,
defaultBranch: repo.default_branch
};
}
/**
* Transform GitHub user to generic format
*/
static transformUser(user: GitHubUser): UserInfo {
return {
id: user.id.toString(),
username: user.login,
email: user.email,
displayName: user.name ?? user.login
};
}
/**
* Transform GitHub issue to generic format
*/
static transformIssue(issue: GitHubIssue): IssueInfo {
return {
id: issue.id,
number: issue.number,
title: issue.title,
body: issue.body ?? '',
state: issue.state,
author: GitHubWebhookProvider.transformUser(issue.user),
labels: issue.labels
? issue.labels.map(label => (typeof label === 'string' ? label : label.name))
: [],
createdAt: new Date(issue.created_at),
updatedAt: new Date(issue.updated_at)
};
}
/**
* Transform GitHub pull request to generic format
*/
static transformPullRequest(pr: GitHubPullRequest): PullRequestInfo {
return {
id: pr.id,
number: pr.number,
title: pr.title,
body: pr.body ?? '',
state: pr.state as 'open' | 'closed',
author: GitHubWebhookProvider.transformUser(pr.user),
labels: pr.labels
? pr.labels.map(label => (typeof label === 'string' ? label : label.name))
: [],
createdAt: new Date(pr.created_at),
updatedAt: new Date(pr.updated_at),
sourceBranch: pr.head.ref,
targetBranch: pr.base.ref,
isDraft: pr.draft || false,
isMerged: pr.merged || false,
mergedAt: pr.merged_at ? new Date(pr.merged_at) : undefined
};
}
}