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
9 changes: 8 additions & 1 deletion packages/factory-sdk/src/cli/fleet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
closeProbePr,
createFactory,
createFleet,
defaultGhRunner,
isInFactoryScope,
parseLinearIssue,
reapFactoryOrphansOnce,
Expand All @@ -18,6 +19,7 @@ import {
type FactoryConfig,
type FleetBackend,
type FleetClient,
type GhRunner,
type MountClient,
type ProbeCloser,
type RelayfileCloudMountClientConfig,
Expand All @@ -34,6 +36,7 @@ interface FleetCliDeps {
stdout?: Pick<NodeJS.WriteStream, 'write'>
stderr?: Pick<NodeJS.WriteStream, 'write'>
probeCloser?: ProbeCloser
probePrGhRunner?: GhRunner
now?: () => number
stopSignalProcessLike?: Pick<NodeJS.Process, 'once' | 'off'>
daemonExit?: (code: number) => void
Expand Down Expand Up @@ -128,7 +131,11 @@ export async function runFleetCli(argv: string[], deps: FleetCliDeps = {}): Prom
return 0
}
const mount = await buildMount(loaded, deps)
const factory = (deps.createFactory ?? createFactory)(loaded.config, { mount, fleet })
const factory = (deps.createFactory ?? createFactory)(loaded.config, {
mount,
fleet,
probePrGhRunner: deps.probePrGhRunner ?? defaultGhRunner,
})
return await runFactoryCommand(command, factory, mount, fleet, loaded.config, globals, out, deps)
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/factory-sdk/src/github/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export {
GhCliGithubMergeGate,
GithubMergeGate,
defaultGhRunner,
evaluateGithubMergeGate,
} from './merge-gate'
export {
Expand Down
2 changes: 1 addition & 1 deletion packages/factory-sdk/src/github/merge-gate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,7 +172,7 @@ export function evaluateGithubMergeGate(
}
}

const defaultGhRunner: GhRunner = async (args) => {
export const defaultGhRunner: GhRunner = async (args) => {
const { stdout, stderr } = await execFileAsync('gh', args, { maxBuffer: 1024 * 1024 })
return { stdout, stderr }
}
Expand Down
26 changes: 26 additions & 0 deletions packages/factory-sdk/src/github/probe-closer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,32 @@ describe('closeProbePr', () => {
])
})

it('treats an already-closed probe PR as idempotent success', async () => {
const calls: string[][] = []
const runner: GhRunner = async (args) => {
calls.push(args)
return {
stdout: JSON.stringify({
state: 'CLOSED',
headRefName: 'ar-229-is-positive',
title: 'Add isPositive util',
body: '',
}),
}
}

await expect(closeProbePr({
repo: 'AgentWorkforce/pear',
prNumber: 279,
expectedIssueKey: 'AR-229',
requireTitleMarker: false,
runner,
})).resolves.toEqual({ repo: 'AgentWorkforce/pear', prNumber: 279, state: 'CLOSED' })
expect(calls.map((args) => args.slice(0, 3))).toEqual([
['pr', 'view', '279'],
])
})

it('refuses a probe that is not tied to the expected issue key before closing', async () => {
const calls: string[][] = []
const runner: GhRunner = async (args) => {
Expand Down
11 changes: 8 additions & 3 deletions packages/factory-sdk/src/github/probe-closer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,10 @@ export interface CloseProbePrResult {
export async function closeProbePr(input: CloseProbePrInput): Promise<CloseProbePrResult> {
const run = input.runner ?? defaultGhRunner
const before = await viewPr(run, input)
assertOpenProbe(before, input)
const beforeState = assertClosableProbe(before, input)
if (beforeState === 'CLOSED') {
return { repo: input.repo, prNumber: input.prNumber, state: 'CLOSED' }
}

await run(['pr', 'close', String(input.prNumber), '--repo', input.repo])

Expand Down Expand Up @@ -53,9 +56,10 @@ const viewPr = async (run: GhRunner, input: CloseProbePrInput): Promise<Record<s
return parseGhJson(result.stdout)
}

const assertOpenProbe = (live: Record<string, unknown>, input: CloseProbePrInput): void => {
const assertClosableProbe = (live: Record<string, unknown>, input: CloseProbePrInput): 'OPEN' | 'CLOSED' => {
const state = stringValue(live.state)
if (normalizeState(state) !== 'OPEN') {
const normalized = normalizeState(state)
if (normalized !== 'OPEN' && normalized !== 'CLOSED') {
throw new Error(`Refusing to close probe PR #${input.prNumber}: live state is ${state ?? 'unknown'}`)
}

Expand All @@ -69,6 +73,7 @@ const assertOpenProbe = (live: Record<string, unknown>, input: CloseProbePrInput
if ((input.requireTitleMarker ?? true) && !hasFactoryE2eMarker(title)) {
throw new Error(`Refusing to close probe PR #${input.prNumber}: missing ${FACTORY_E2E_MARKER} probe marker`)
}
return normalized
}

const hasFactoryE2eMarker = (title: string): boolean =>
Expand Down
1 change: 1 addition & 0 deletions packages/factory-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ export {
GhCliGithubMergeGate,
GithubMergeGate,
closeProbePr,
defaultGhRunner,
evaluateGithubMergeGate,
} from './github'
export type {
Expand Down
Loading
Loading