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
26 changes: 22 additions & 4 deletions src/cli/dashboard/projects/review-trigger-set.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ export default class ProjectsReviewTriggerSet extends DashboardCommand {
allowNo: true,
default: undefined,
}),
'pr-opened': Flags.boolean({
description:
'Enable respond-to-review on newly opened PRs (filtered by own-prs-only / external-prs).',
allowNo: true,
default: undefined,
}),
};

async run(): Promise<void> {
Expand All @@ -49,10 +55,16 @@ export default class ProjectsReviewTriggerSet extends DashboardCommand {
const ownPrsOnly = flags['own-prs-only'];
const externalPrs = flags['external-prs'];
const onReviewRequested = flags['on-review-requested'];
const prOpened = flags['pr-opened'];

if (ownPrsOnly === undefined && externalPrs === undefined && onReviewRequested === undefined) {
if (
ownPrsOnly === undefined &&
externalPrs === undefined &&
onReviewRequested === undefined &&
prOpened === undefined
) {
this.error(
'At least one flag must be provided: --own-prs-only, --external-prs, --on-review-requested (use --no-<flag> to disable).',
'At least one flag must be provided: --own-prs-only, --external-prs, --on-review-requested, --pr-opened (use --no-<flag> to disable).',
);
}

Expand All @@ -62,22 +74,28 @@ export default class ProjectsReviewTriggerSet extends DashboardCommand {
if (externalPrs !== undefined) reviewTrigger.externalPrs = externalPrs;
if (onReviewRequested !== undefined) reviewTrigger.onReviewRequested = onReviewRequested;

// Build the top-level triggers payload
const triggers: Record<string, boolean | Record<string, boolean>> = {};
if (Object.keys(reviewTrigger).length > 0) triggers.reviewTrigger = reviewTrigger;
if (prOpened !== undefined) triggers.prOpened = prOpened;

try {
await this.client.projects.integrations.updateTriggers.mutate({
projectId: args.id,
category: 'scm',
triggers: { reviewTrigger },
triggers,
});

if (flags.json) {
this.outputJson({ ok: true, reviewTrigger });
this.outputJson({ ok: true, ...triggers });
return;
}

const lines: string[] = [`Review trigger modes updated for project: ${args.id}`];
if (ownPrsOnly !== undefined) lines.push(` ownPrsOnly: ${ownPrsOnly}`);
if (externalPrs !== undefined) lines.push(` externalPrs: ${externalPrs}`);
if (onReviewRequested !== undefined) lines.push(` onReviewRequested: ${onReviewRequested}`);
if (prOpened !== undefined) lines.push(` prOpened: ${prOpened}`);
this.log(lines.join('\n'));
} catch (err) {
this.handleError(err);
Expand Down
33 changes: 28 additions & 5 deletions src/triggers/github/pr-opened.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import { resolveGitHubTriggerEnabled } from '../../config/triggerConfig.js';
import { isCascadeBot } from '../../github/personas.js';
import {
resolveGitHubTriggerEnabled,
resolveReviewTriggerConfig,
} from '../../config/triggerConfig.js';
import type { TriggerContext, TriggerHandler, TriggerResult } from '../../types/index.js';
import { logger } from '../../utils/logging.js';
import { isGitHubPullRequestPayload } from './types.js';
Expand All @@ -22,6 +24,12 @@ export class PROpenedTrigger implements TriggerHandler {
return false;
}

// Respect reviewTrigger config — at least one author mode must be active
const reviewConfig = resolveReviewTriggerConfig(ctx.project.github?.triggers);
if (!reviewConfig.ownPrsOnly && !reviewConfig.externalPrs) {
return false;
}

// Only trigger on newly opened PRs
if (ctx.payload.action !== 'opened') return false;

Expand All @@ -47,9 +55,24 @@ export class PROpenedTrigger implements TriggerHandler {
const prNumber = payload.pull_request.number;
const prAuthor = payload.pull_request.user.login;

// Skip PRs authored by CASCADE bots — nothing to "respond to" on our own PRs
if (ctx.personaIdentities && isCascadeBot(prAuthor, ctx.personaIdentities)) {
logger.info('Skipping PR opened by CASCADE bot', { prNumber, prAuthor });
// Gate on PR author based on configured review trigger modes
if (!ctx.personaIdentities) return null;
const implLogin = ctx.personaIdentities.implementer;
const isImplementerPR = prAuthor === implLogin || prAuthor === `${implLogin}[bot]`;

const reviewConfig = resolveReviewTriggerConfig(ctx.project.github?.triggers);
const shouldTrigger =
(reviewConfig.ownPrsOnly && isImplementerPR) ||
(reviewConfig.externalPrs && !isImplementerPR);

if (!shouldTrigger) {
logger.info('PR author does not match any enabled review trigger mode, skipping', {
prNumber,
prAuthor,
isImplementerPR,
ownPrsOnly: reviewConfig.ownPrsOnly,
externalPrs: reviewConfig.externalPrs,
});
return null;
}

Expand Down
Loading