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
1 change: 1 addition & 0 deletions services/gateway/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to `@farmslot/gateway` are tracked here.

## Unreleased

- fix(run-completion): reconcile merged and already-ready PRs without replaying `gh pr ready`, and report closed-unmerged PRs explicitly.
- fix(run-engine): project publication review counts against the current prepared package, so an older passing review is shown as stale instead of contradicting the publish cockpit.
- fix(runners): require a task-scoped signal or exact runner hook before cold dispatch or a warm-session handoff starts monitoring, recreate the persisted primary worker target before restart delivery, validate the exact Claude settings artifact even when an older node installer exits successfully, never reuse a pre-replay task signal, and never repair failed dispatch by matching runner prose.
- feat(run-engine): ready-gate and package-refresh soft-chip probe for behind-main count + non-destructive merge-tree conflicts (`behindMain`, `mergeConflicts`) so operators see staleness before more review loops — never auto-rebases mid-loop.
Expand Down
17 changes: 17 additions & 0 deletions services/gateway/src/run-completion/pr-publication.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,26 @@ import {
paginatedPrCommentOutputContainsRun,
prCommentBelongsToRun,
prCommentIdentityMarker,
resolvePrReadyAction,
shouldPostWorkerReportComment,
} from './pr-publication.js';

test('PR ready action is idempotent across lifecycle states', () => {
assert.equal(
resolvePrReadyAction({ state: 'MERGED', isDraft: false, mergedAt: '2026-08-04T09:37:17Z' }),
'merged',
);
assert.equal(
resolvePrReadyAction({ state: 'OPEN', isDraft: false, mergedAt: null }),
'already-ready',
);
assert.equal(
resolvePrReadyAction({ state: 'OPEN', isDraft: true, mergedAt: null }),
'mark-ready',
);
assert.equal(resolvePrReadyAction({ state: 'CLOSED', isDraft: false, mergedAt: null }), 'closed');
});

test('PR comment identity is stable per run and supports legacy comments', () => {
const runId = 'b26f39be-fb72-407b-ac77-47fdea539fb9';

Expand Down
38 changes: 37 additions & 1 deletion services/gateway/src/run-completion/pr-publication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -298,7 +298,43 @@ export async function updatePRTitle(run: Run, ciRepo: string, prNumber: number):

// ─── Mark PR ready for review ───

export async function markPRReady(ciRepo: string, prNumber: number): Promise<void> {
export type PrReadyResult = 'marked-ready' | 'already-ready' | 'merged';

interface PrLifecycle {
state: 'OPEN' | 'CLOSED' | 'MERGED';
isDraft: boolean;
mergedAt: string | null;
}

export function resolvePrReadyAction(pr: PrLifecycle): PrReadyResult | 'closed' | 'mark-ready' {
if (pr.state === 'MERGED' || pr.mergedAt) return 'merged';
if (pr.state === 'CLOSED') return 'closed';
return pr.isDraft ? 'mark-ready' : 'already-ready';
}

export async function markPRReady(ciRepo: string, prNumber: number): Promise<PrReadyResult> {
const result = await ghRequest([
'pr',
'view',
String(prNumber),
'--repo',
ciRepo,
'--json',
'state,isDraft,mergedAt',
]);
const action = resolvePrReadyAction(JSON.parse(result.stdout || '{}') as PrLifecycle);
if (action === 'merged') {
console.log(`[run-completion] PR #${prNumber} is already merged`);
return action;
}
if (action === 'already-ready') {
console.log(`[run-completion] PR #${prNumber} is already ready for review`);
return action;
}
if (action === 'closed') {
throw new Error(`PR #${prNumber} is closed without merging; it cannot be published`);
}
await ghRequest(['pr', 'ready', String(prNumber), '--repo', ciRepo], { force: true });
console.log(`[run-completion] marked PR #${prNumber} as ready for review`);
return 'marked-ready';
}
Loading