Skip to content

Commit 1c669a5

Browse files
committed
Copilot feedback
1 parent 1714e46 commit 1c669a5

5 files changed

Lines changed: 141 additions & 34 deletions

File tree

packages/bot-runner/lib/command-runner.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,11 @@ export class CommandRunner {
8888
return;
8989
}
9090

91-
if (!eventContent?.input || typeof eventContent.input !== 'object') {
91+
if (
92+
!eventContent?.input ||
93+
typeof eventContent.input !== 'object' ||
94+
Array.isArray(eventContent.input)
95+
) {
9296
return;
9397
}
9498

packages/bot-runner/lib/pr-listing/pr-listing-workflow-handler.ts

Lines changed: 59 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,7 @@ import {
1313
CreateListingPRHandler,
1414
type BotTriggerEventContent,
1515
} from './create-listing-pr-handler';
16-
import type {
17-
BotCommandHandler,
18-
EnqueueRunCommandFn,
19-
} from '../command-runner';
16+
import type { BotCommandHandler, EnqueueRunCommandFn } from '../command-runner';
2017
import type { GitHubClient } from '../github';
2118

2219
export type LintSubmissionFilesFn = (
@@ -114,7 +111,13 @@ export class PrListingWorkflowHandler implements BotCommandHandler {
114111
return this.runWorkflow(ctx);
115112
}
116113
if (eventContent.type === PR_LISTING_RETRY) {
117-
let ctx = await this.buildRetryContext(runAs, eventContent);
114+
let ctx;
115+
try {
116+
ctx = await this.buildRetryContext(runAs, eventContent);
117+
} catch (err) {
118+
await this.recordRetryContextFailure(runAs, eventContent, err);
119+
throw err;
120+
}
118121
return this.runWorkflow(ctx);
119122
}
120123
throw new Error(
@@ -244,9 +247,7 @@ export class PrListingWorkflowHandler implements BotCommandHandler {
244247
: await this.runFreshPrCardFlow(ctx);
245248

246249
await runStep('github-pr', () => this.pushToGitHub(ctx, prCardData));
247-
await runStep('github-pr', () =>
248-
this.linkPrCardOnWorkflow(ctx, prCardData.prCardUrl),
249-
);
250+
await runStep('github-pr', () => this.clearWorkflowError(ctx));
250251

251252
return prCardData.prCardResult;
252253
} catch (err) {
@@ -264,6 +265,9 @@ export class PrListingWorkflowHandler implements BotCommandHandler {
264265
let { prCardResult, prCardUrl } = await runStep('create-pr-card', () =>
265266
this.createPrCard(ctx, textFiles, totalCount),
266267
);
268+
await runStep('create-pr-card', () =>
269+
this.linkPrCardOnWorkflow(ctx, prCardUrl),
270+
);
267271
return { prCardResult, prCardUrl, binaryFiles };
268272
}
269273

@@ -425,13 +429,19 @@ export class PrListingWorkflowHandler implements BotCommandHandler {
425429
): Promise<void> {
426430
if (!ctx.workflowCardUrl || !prCardUrl) return;
427431
await this.patchWorkflowCard(ctx, {
428-
attributes: { prCreationError: null, failedStep: null },
429432
relationships: {
430433
prCard: { links: { self: prCardUrl } },
431434
},
432435
});
433436
}
434437

438+
private async clearWorkflowError(ctx: WorkflowContext): Promise<void> {
439+
if (!ctx.workflowCardUrl) return;
440+
await this.patchWorkflowCard(ctx, {
441+
attributes: { prCreationError: null, failedStep: null },
442+
});
443+
}
444+
435445
private async recordWorkflowFailure(
436446
ctx: WorkflowContext,
437447
err: unknown,
@@ -457,6 +467,46 @@ export class PrListingWorkflowHandler implements BotCommandHandler {
457467
}
458468
}
459469

470+
private async recordRetryContextFailure(
471+
runAs: string,
472+
eventContent: BotTriggerEventContent,
473+
err: unknown,
474+
): Promise<void> {
475+
let input = eventContent.input as Record<string, unknown> | null;
476+
let workflowCardUrl =
477+
typeof input?.workflowCardUrl === 'string' && input.workflowCardUrl.trim()
478+
? input.workflowCardUrl.trim()
479+
: null;
480+
if (!workflowCardUrl) return;
481+
let workflowCardRealm =
482+
(typeof input?.workflowCardRealm === 'string' &&
483+
input.workflowCardRealm.trim()
484+
? input.workflowCardRealm.trim()
485+
: null) ?? (eventContent.realm as string);
486+
let message = err instanceof Error ? err.message : String(err);
487+
try {
488+
let result = await this.enqueueRunCommand({
489+
runAs,
490+
realmURL: workflowCardRealm,
491+
command: PATCH_CARD_INSTANCE_COMMAND,
492+
commandInput: {
493+
cardId: workflowCardUrl,
494+
patch: {
495+
attributes: {
496+
prCreationError: `PR retry failed: ${message}`,
497+
},
498+
},
499+
},
500+
});
501+
requireReady(result, 'patch-card-instance (retry pre-context failure)');
502+
} catch (patchError: any) {
503+
log.error(
504+
'pr-listing-retry: failed to patch workflow card after retry context build failed',
505+
{ patchError: patchError?.message ?? patchError },
506+
);
507+
}
508+
}
509+
460510
// ── Realm helpers ──
461511

462512
private async patchWorkflowCard(

packages/bot-runner/tests/command-runner-test.ts

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -263,8 +263,8 @@ module('command runner', () => {
263263

264264
assert.strictEqual(
265265
publishedJobs.length,
266-
4,
267-
'enqueues collect-files, lintStatus=passed patch, create-pr-card, and prCard-link patch (lint step skipped)',
266+
5,
267+
'enqueues collect-files, lintStatus=passed patch, create-pr-card, prCard-link patch, and clear-error patch (lint step skipped)',
268268
);
269269
assert.strictEqual(createdBranches.length, 1, 'creates branch');
270270
assert.strictEqual(branchWrites.length, 1, 'writes files to branch');
@@ -296,12 +296,19 @@ module('command runner', () => {
296296
`command:${SUBMISSION_REALM_URL}`,
297297
'Job 3 (create-pr-card) uses submissions realm concurrency group',
298298
);
299-
// Job 4: prCard link patch — user realm
299+
// Job 4: prCard link patch — user realm. Persisted immediately after
300+
// create-pr-card so retry on a later GitHub failure can find it.
300301
assert.strictEqual(
301302
(publishedJobs[3] as { concurrencyGroup: string }).concurrencyGroup,
302303
'command:http://localhost:4201/test/',
303304
'Job 4 (prCard link patch) uses default realm concurrency group',
304305
);
306+
// Job 5: clear-error patch — user realm
307+
assert.strictEqual(
308+
(publishedJobs[4] as { concurrencyGroup: string }).concurrencyGroup,
309+
'command:http://localhost:4201/test/',
310+
'Job 5 (clear-error patch) uses default realm concurrency group',
311+
);
305312

306313
assert.deepEqual(
307314
(publishedJobs[2] as { args: Record<string, unknown> }).args,
@@ -335,10 +342,6 @@ module('command runner', () => {
335342
commandInput: {
336343
cardId: submissionCardUrl,
337344
patch: {
338-
attributes: {
339-
prCreationError: null,
340-
failedStep: null,
341-
},
342345
relationships: {
343346
prCard: {
344347
links: {
@@ -349,7 +352,26 @@ module('command runner', () => {
349352
},
350353
},
351354
},
352-
'enqueues workflow card patch in the user realm (clears any prior error state and links PrCard)',
355+
'persists prCard link on the workflow card immediately after create-pr-card succeeds (so retry on later failure can reuse the existing PrCard)',
356+
);
357+
assert.deepEqual(
358+
(publishedJobs[4] as { args: Record<string, unknown> }).args,
359+
{
360+
realmURL: 'http://localhost:4201/test/',
361+
realmUsername: '@alice:localhost',
362+
runAs: '@alice:localhost',
363+
command: '@cardstack/boxel-host/commands/patch-card-instance/default',
364+
commandInput: {
365+
cardId: submissionCardUrl,
366+
patch: {
367+
attributes: {
368+
prCreationError: null,
369+
failedStep: null,
370+
},
371+
},
372+
},
373+
},
374+
'clears prior error attributes on the workflow card after the GitHub PR succeeds',
353375
);
354376
let prBody =
355377
(
@@ -765,7 +787,8 @@ module('command runner', () => {
765787
// title is the display-formatted "Submit <X>";
766788
// retry must NOT derive branchName from it.
767789
title: 'Submit My Listing',
768-
branchName: 'room-IWFiYzEyMzpsb2NhbGhvc3Q/my-listing',
790+
branchName:
791+
'room-IWFiYzEyMzpsb2NhbGhvc3Q/my-listing',
769792
},
770793
relationships: {
771794
listing: {

packages/host/app/commands/create-submission-workflow.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,7 @@ export default class CreateSubmissionWorkflowCommand extends HostBaseCommand<
155155
realm,
156156
listingId,
157157
workflowCardUrl: workflowCardId,
158+
workflowCardRealm: workflowRealm,
158159
branchName,
159160
...(listingName ? { listingName } : {}),
160161
...(listingSummary ? { listingSummary } : {}),

packages/host/app/commands/retry-submission-workflow.ts

Lines changed: 44 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,15 @@ import SendBotTriggerEventCommand from './bot-requests/send-bot-trigger-event';
1010

1111
import type RealmService from '../services/realm';
1212
import type StoreService from '../services/store';
13-
import type { SubmissionWorkflowCard } from '@cardstack/catalog/submission-workflow-card/submission-workflow-card';
13+
14+
// Local view: host's tsconfig maps @cardstack/catalog/* to the legacy
15+
// in-monorepo catalog-realm, which lacks `failedStep`. Drop this once
16+
// that path points at boxel-catalog (the canonical source).
17+
interface WorkflowCardView {
18+
roomId?: string;
19+
listing?: { id?: string };
20+
failedStep?: string | null;
21+
}
1422

1523
// Re-emits the `pr-listing-retry` bot trigger event for an existing
1624
// SubmissionWorkflowCard that ended in a failed state. Reads roomId + listing
@@ -38,13 +46,13 @@ export default class RetrySubmissionWorkflowCommand extends HostBaseCommand<
3846
): Promise<undefined> {
3947
let { workflowCardId } = input;
4048

41-
let workflowCard =
42-
await this.store.get<SubmissionWorkflowCard>(workflowCardId);
43-
if (!workflowCard || !isCardInstance(workflowCard)) {
49+
let result = await this.store.get(workflowCardId);
50+
if (!result || !isCardInstance(result)) {
4451
throw new Error(
4552
`Cannot retry: workflow card ${workflowCardId} not found`,
4653
);
4754
}
55+
let workflowCard = result as unknown as WorkflowCardView;
4856

4957
let roomId = workflowCard.roomId;
5058
let listingId = workflowCard.listing?.id;
@@ -64,9 +72,12 @@ export default class RetrySubmissionWorkflowCommand extends HostBaseCommand<
6472
);
6573
}
6674

67-
// Clear the prior failure state up front for instant UI feedback. The
68-
// bot-runner will repopulate these fields on success or on a fresh
69-
// failure; we don't have to wait for it to do so.
75+
// Snapshot the prior failedStep so we can restore it if the send fails
76+
// — without it, the optimistic clear below would hide the Retry button
77+
// (canRetry requires prCreationError || failedStep) and strand the user.
78+
let priorFailedStep = workflowCard.failedStep ?? null;
79+
80+
// Optimistic clear for instant UI feedback. Re-set on send failure.
7081
await this.store.patch(
7182
workflowCardId,
7283
{
@@ -78,13 +89,31 @@ export default class RetrySubmissionWorkflowCommand extends HostBaseCommand<
7889
{ doNotWaitForPersist: true },
7990
);
8091

81-
await new SendBotTriggerEventCommand(this.commandContext).execute({
82-
roomId,
83-
realm: listingRealm,
84-
type: 'pr-listing-retry',
85-
input: {
86-
workflowCardUrl: workflowCardId,
87-
},
88-
});
92+
try {
93+
await new SendBotTriggerEventCommand(this.commandContext).execute({
94+
roomId,
95+
realm: listingRealm,
96+
type: 'pr-listing-retry',
97+
input: {
98+
workflowCardUrl: workflowCardId,
99+
workflowCardRealm:
100+
this.realm.realmOf(rri(workflowCardId)) ?? undefined,
101+
},
102+
});
103+
} catch (sendError: any) {
104+
let message =
105+
sendError instanceof Error ? sendError.message : String(sendError);
106+
await this.store.patch(
107+
workflowCardId,
108+
{
109+
attributes: {
110+
prCreationError: `Failed to send retry: ${message}`,
111+
failedStep: priorFailedStep,
112+
},
113+
},
114+
{ doNotWaitForPersist: true },
115+
);
116+
throw sendError;
117+
}
89118
}
90119
}

0 commit comments

Comments
 (0)