PBI-49: review-fixes — primary_worktree order, idea-claim rollback, sprint mark-ready fallback

Three findings from PBI-47 review:

P1 — primary_worktree_path scheiden van lock-volgorde
  setupProductWorktrees acquired locks in alphabetical order (deadlock prevention)
  but also returned worktrees in that order, so worktrees[0] could point at a
  secondary product when its id sorted before the primary's. Lock-acquire stays
  sorted; output now preserves caller's input order so worktrees[0] is always
  the primary.

P1 — Idea-claim rollback bij worktree setup failure
  setupProductWorktrees runs after tryClaimJob has already flipped the job to
  CLAIMED. A failure in lock-acquire/git-fetch/reset/sync left the job hanging
  until the 30-min stale-reset and the lock-map populated. Wrapped in try/catch
  with releaseLocksOnTerminal + rollbackClaim mirror of the task-pad behaviour.

P2 — SPRINT mark-ready fallback when last task didn't push
  The mark-ready path used updated.pr_url, which is null when the closing task
  was verify-only or had no diff. Now falls back to a Prisma findFirst on the
  SprintRun's earliest job with pr_url IS NOT NULL.

Tests: 31 files, 243 passing (incl. new input-order regression for setupProductWorktrees).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Madhura68 2026-05-07 11:02:23 +02:00
parent f7f5a487ec
commit d2f43fe8e6
4 changed files with 71 additions and 18 deletions

View file

@ -23,15 +23,19 @@ export async function setupProductWorktrees(
// Ensure parent dir exists so lockfile creation succeeds
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 lockPaths = sorted.map(getProductWorktreeLockPath)
const releaseAll = await acquireFileLocksOrdered(lockPaths)
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 }> = []
for (const productId of sorted) {
for (const productId of productIds) {
const repoRoot = await resolveRepoRoot(productId)
if (!repoRoot) continue
const { worktreePath } = await getOrCreateProductWorktree({ repoRoot, productId })