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
20 changes: 20 additions & 0 deletions src/pm/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
*/

import { AsyncLocalStorage } from 'node:async_hooks';
import type { PMIntegration } from './integration.js';
import type { PMProvider } from './types.js';

const pmProviderStore = new AsyncLocalStorage<PMProvider>();
Expand All @@ -28,3 +29,22 @@ export function getPMProvider(): PMProvider {
export function getPMProviderOrNull(): PMProvider | null {
return pmProviderStore.getStore() ?? null;
}

/**
* Establish PM credential scope for a project.
*
* Uses the integration's withCredentials() for the correct PM type.
* Falls through to running fn() directly if no PM type is configured
* or the integration is unknown.
*/
export async function withPMCredentials<T>(
projectId: string,
pmType: string | undefined,
getIntegration: (type: string) => PMIntegration | null,
fn: () => Promise<T>,
): Promise<T> {
if (!pmType) return fn();
const integration = getIntegration(pmType);
if (!integration) return fn();
return integration.withCredentials(projectId, fn);
}
56 changes: 47 additions & 9 deletions src/pm/webhook-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,23 +88,49 @@ async function cleanupOrphanAck(
}
}

async function handleMatchedTrigger(
async function resolveTriggerResult(
integration: PMIntegration,
registry: TriggerRegistry,
payload: unknown,
project: ProjectConfig,
config: CascadeConfig,
ackCommentId?: string,
): Promise<void> {
ackCommentId: string | undefined,
preResolvedResult: TriggerResult | undefined,
): Promise<TriggerResult | null> {
if (preResolvedResult) {
logger.info(`Using pre-resolved trigger result for ${integration.type} webhook`, {
agentType: preResolvedResult.agentType,
});
return preResolvedResult;
}
const ctx: TriggerContext = { project, source: integration.type as TriggerSource, payload };
const result = await registry.dispatch(ctx);
if (!result) {
logger.info(`No trigger matched for ${integration.type} webhook`);
if (ackCommentId) {
await cleanupOrphanAck(integration, project.id, payload, ackCommentId);
}
return;
}
return result;
}

async function handleMatchedTrigger(
integration: PMIntegration,
registry: TriggerRegistry,
payload: unknown,
project: ProjectConfig,
config: CascadeConfig,
ackCommentId?: string,
preResolvedResult?: TriggerResult,
): Promise<void> {
const result = await resolveTriggerResult(
integration,
registry,
payload,
project,
ackCommentId,
preResolvedResult,
);
if (!result) return;

// Pass ack comment ID into agent input for ProgressMonitor pre-seeding
if (ackCommentId) {
Expand Down Expand Up @@ -152,7 +178,8 @@ async function handleMatchedTrigger(
*
* Validates the payload via the integration's `parseWebhookPayload()`,
* looks up the project, establishes credential + PM provider scope,
* dispatches to the trigger registry, and runs the matched agent.
* dispatches to the trigger registry (or uses pre-resolved result),
* and runs the matched agent.
*
* Used by both Trello and JIRA webhook handlers.
*/
Expand All @@ -161,8 +188,11 @@ export async function processPMWebhook(
payload: unknown,
registry: TriggerRegistry,
ackCommentId?: string,
triggerResult?: TriggerResult,
): Promise<void> {
logger.info(`Processing ${integration.type} webhook`);
logger.info(`Processing ${integration.type} webhook`, {
hasTriggerResult: !!triggerResult,
});

const event = integration.parseWebhookPayload(payload);
if (!event) {
Expand Down Expand Up @@ -201,11 +231,19 @@ export async function processPMWebhook(
}
const { project, config } = projectConfig;

// Establish credential + PM provider scope for trigger dispatch
// Establish credential + PM provider scope for agent execution
const pmProvider = pmRegistry.createProvider(project);
await integration.withCredentials(project.id, () =>
withPMProvider(pmProvider, () =>
handleMatchedTrigger(integration, registry, payload, project, config, ackCommentId),
handleMatchedTrigger(
integration,
registry,
payload,
project,
config,
ackCommentId,
triggerResult,
),
),
);
}
Loading