diff --git a/services/gateway/CHANGELOG.md b/services/gateway/CHANGELOG.md index d51587fe..6c058d43 100644 --- a/services/gateway/CHANGELOG.md +++ b/services/gateway/CHANGELOG.md @@ -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. diff --git a/services/gateway/src/run-completion/pr-publication.test.ts b/services/gateway/src/run-completion/pr-publication.test.ts index 42e89d21..258ccf4a 100644 --- a/services/gateway/src/run-completion/pr-publication.test.ts +++ b/services/gateway/src/run-completion/pr-publication.test.ts @@ -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'; diff --git a/services/gateway/src/run-completion/pr-publication.ts b/services/gateway/src/run-completion/pr-publication.ts index 8fbc7632..f7058725 100644 --- a/services/gateway/src/run-completion/pr-publication.ts +++ b/services/gateway/src/run-completion/pr-publication.ts @@ -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 { +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 { + 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'; }