Merge pull request #34 from madhura68/feat/sprint-worker
PBI-49: review-fixes (primary_worktree order, idea rollback, sprint mark-ready fallback)
This commit is contained in:
commit
7dbc9fe249
4 changed files with 71 additions and 18 deletions
|
|
@ -118,4 +118,19 @@ describe('job-locks: setupProductWorktrees', () => {
|
||||||
// Lock was still acquired and registered — release cleans up
|
// Lock was still acquired and registered — release cleans up
|
||||||
await releaseLocksOnTerminal('j3')
|
await releaseLocksOnTerminal('j3')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('output preserves input order regardless of alphabetical lock-acquire order', async () => {
|
||||||
|
// 'z-primary' sorts AFTER 'a-secondary' alphabetically, but caller passes
|
||||||
|
// primary first → output[0] must be 'z-primary' so wait_for_job's
|
||||||
|
// primary_worktree_path = worktrees[0]?.worktreePath points at the right repo.
|
||||||
|
const result = await setupProductWorktrees(
|
||||||
|
'j4',
|
||||||
|
['z-primary', 'a-secondary'],
|
||||||
|
async () => originRepo,
|
||||||
|
)
|
||||||
|
expect(result).toHaveLength(2)
|
||||||
|
expect(result[0].productId).toBe('z-primary')
|
||||||
|
expect(result[1].productId).toBe('a-secondary')
|
||||||
|
await releaseLocksOnTerminal('j4')
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
|
||||||
|
|
@ -23,15 +23,19 @@ export async function setupProductWorktrees(
|
||||||
// Ensure parent dir exists so lockfile creation succeeds
|
// Ensure parent dir exists so lockfile creation succeeds
|
||||||
await fs.mkdir(path.join(getWorktreeRoot(), '_products'), { recursive: true })
|
await fs.mkdir(path.join(getWorktreeRoot(), '_products'), { recursive: true })
|
||||||
|
|
||||||
// Lock-first, alphabetically sorted (deadlock prevention for multi-product idea-jobs)
|
// Lock-first, alphabetically sorted (deadlock prevention for multi-product idea-jobs).
|
||||||
|
// Locks acquired in sorted order; output preserves caller's input order so that
|
||||||
|
// worktrees[0] is the primary product (Idea.product_id), regardless of how its
|
||||||
|
// id sorts alphabetically against secondary products.
|
||||||
const sorted = [...productIds].sort()
|
const sorted = [...productIds].sort()
|
||||||
const lockPaths = sorted.map(getProductWorktreeLockPath)
|
const lockPaths = sorted.map(getProductWorktreeLockPath)
|
||||||
const releaseAll = await acquireFileLocksOrdered(lockPaths)
|
const releaseAll = await acquireFileLocksOrdered(lockPaths)
|
||||||
registerJobLockReleases(jobId, [releaseAll])
|
registerJobLockReleases(jobId, [releaseAll])
|
||||||
|
|
||||||
// After lock-acquire, create/reuse worktrees and sync
|
// After lock-acquire, create/reuse worktrees and sync — iterate input order
|
||||||
|
// so callers get back [primary, ...secondaries] in their original sequence.
|
||||||
const out: Array<{ productId: string; worktreePath: string }> = []
|
const out: Array<{ productId: string; worktreePath: string }> = []
|
||||||
for (const productId of sorted) {
|
for (const productId of productIds) {
|
||||||
const repoRoot = await resolveRepoRoot(productId)
|
const repoRoot = await resolveRepoRoot(productId)
|
||||||
if (!repoRoot) continue
|
if (!repoRoot) continue
|
||||||
const { worktreePath } = await getOrCreateProductWorktree({ repoRoot, productId })
|
const { worktreePath } = await getOrCreateProductWorktree({ repoRoot, productId })
|
||||||
|
|
|
||||||
|
|
@ -590,21 +590,36 @@ export function registerUpdateJobStatusTool(server: McpServer) {
|
||||||
|
|
||||||
// SPRINT-mode: bij sprint-DONE de draft-PR ready-for-review zetten.
|
// SPRINT-mode: bij sprint-DONE de draft-PR ready-for-review zetten.
|
||||||
// Mens reviewt + mergt zelf — geen auto-merge in deze modus.
|
// Mens reviewt + mergt zelf — geen auto-merge in deze modus.
|
||||||
if (sprintRunBecameDone && updated.pr_url) {
|
// PBI-49 P2: gebruik niet alleen updated.pr_url — als de laatste task
|
||||||
const sprintRun = await prisma.claudeJob
|
// verify-only is of geen wijzigingen pusht, krijgt die geen pr_url.
|
||||||
|
// Zoek de eerst aangemaakte PR op binnen de SprintRun als fallback.
|
||||||
|
if (sprintRunBecameDone) {
|
||||||
|
const ctx = await prisma.claudeJob
|
||||||
.findUnique({
|
.findUnique({
|
||||||
where: { id: job_id },
|
where: { id: job_id },
|
||||||
select: {
|
select: {
|
||||||
|
sprint_run_id: true,
|
||||||
sprint_run: { select: { pr_strategy: true, status: true } },
|
sprint_run: { select: { pr_strategy: true, status: true } },
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then((j) => j?.sprint_run)
|
if (
|
||||||
if (sprintRun?.pr_strategy === 'SPRINT' && sprintRun.status === 'DONE') {
|
ctx?.sprint_run?.pr_strategy === 'SPRINT'
|
||||||
|
&& ctx.sprint_run.status === 'DONE'
|
||||||
|
&& ctx.sprint_run_id
|
||||||
|
) {
|
||||||
|
const sprintPrUrl = updated.pr_url
|
||||||
|
?? (await prisma.claudeJob.findFirst({
|
||||||
|
where: { sprint_run_id: ctx.sprint_run_id, pr_url: { not: null } },
|
||||||
|
orderBy: { created_at: 'asc' },
|
||||||
|
select: { pr_url: true },
|
||||||
|
}))?.pr_url
|
||||||
|
?? null
|
||||||
|
if (sprintPrUrl) {
|
||||||
try {
|
try {
|
||||||
const ready = await markPullRequestReady({ prUrl: updated.pr_url })
|
const ready = await markPullRequestReady({ prUrl: sprintPrUrl })
|
||||||
if ('error' in ready) {
|
if ('error' in ready) {
|
||||||
console.warn(
|
console.warn(
|
||||||
`[update_job_status] markPullRequestReady failed for ${updated.pr_url}: ${ready.error}`,
|
`[update_job_status] markPullRequestReady failed for ${sprintPrUrl}: ${ready.error}`,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|
@ -612,6 +627,7 @@ export function registerUpdateJobStatusTool(server: McpServer) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// M12: bij failed voor IDEA_*-jobs: zet idea.status op
|
// M12: bij failed voor IDEA_*-jobs: zet idea.status op
|
||||||
// GRILL_FAILED / PLAN_FAILED + log JOB_EVENT. Bij done laten we de
|
// GRILL_FAILED / PLAN_FAILED + log JOB_EVENT. Bij done laten we de
|
||||||
|
|
|
||||||
|
|
@ -428,9 +428,27 @@ async function getFullJobContext(jobId: string) {
|
||||||
involvedProductIds.push(ip.product_id)
|
involvedProductIds.push(ip.product_id)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
const worktrees = involvedProductIds.length > 0
|
// PBI-49 P1: rollback the claim if worktree setup fails so the job
|
||||||
? await setupProductWorktrees(job.id, involvedProductIds, (pid) => resolveRepoRoot(pid))
|
// doesn't hang in CLAIMED until the 30-min stale-reset, and any partial
|
||||||
: []
|
// locks are released. Mirrors attachWorktreeToJob's task-pad behaviour.
|
||||||
|
let worktrees: Array<{ productId: string; worktreePath: string }> = []
|
||||||
|
if (involvedProductIds.length > 0) {
|
||||||
|
try {
|
||||||
|
worktrees = await setupProductWorktrees(
|
||||||
|
job.id,
|
||||||
|
involvedProductIds,
|
||||||
|
(pid) => resolveRepoRoot(pid),
|
||||||
|
)
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(
|
||||||
|
`[wait-for-job] product-worktree setup failed for idea-job ${job.id}; rolling back claim:`,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
await releaseLocksOnTerminal(job.id)
|
||||||
|
await rollbackClaim(job.id)
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
job_id: job.id,
|
job_id: job.id,
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue