mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-11-11 21:59:38 +01:00
fix(server): clear stale workspace finalize blockers
Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
1 parent
ae7ca9b456
commit
0f5a674ea0
3 files changed
+132
-18
No files matched your search
@@ -1016,14 +1016,20 @@ async function listPendingFinalizeBlockerIssueIds(
|
||||
and(
|
||||
eq(workspaceOperations.companyId, companyId),
|
||||
inArray(workspaceOperations.executionWorkspaceId, executionWorkspaceIds),
|
||||
or(inArray(workspaceOperations.issueId, blockerIssueIds), isNull(workspaceOperations.issueId)),
|
||||
),
|
||||
);
|
||||
|
||||
const latestAttributedByBlockerWorkspace = new Map<string, { phase: string; status: string; startedAt: Date }>();
|
||||
const latestUnattributedByWorkspace = new Map<string, { phase: string; status: string; startedAt: Date }>();
|
||||
const latestSuccessfulFinalizeByWorkspace = new Map<string, Date>();
|
||||
for (const row of rows) {
|
||||
if (!row.executionWorkspaceId) continue;
|
||||
if (row.phase === "workspace_finalize" && row.status === "succeeded") {
|
||||
const current = latestSuccessfulFinalizeByWorkspace.get(row.executionWorkspaceId);
|
||||
if (!current || row.startedAt > current) {
|
||||
latestSuccessfulFinalizeByWorkspace.set(row.executionWorkspaceId, row.startedAt);
|
||||
}
|
||||
}
|
||||
if (row.issueId) {
|
||||
const key = `${row.issueId}:${row.executionWorkspaceId}`;
|
||||
if (!blockerWorkspaceKeys.has(key)) continue;
|
||||
@@ -1053,6 +1059,8 @@ async function listPendingFinalizeBlockerIssueIds(
|
||||
?? latestUnattributedByWorkspace.get(pair.executionWorkspaceId);
|
||||
if (!latest) continue; // no ops recorded -> nothing to finalize for this blocker
|
||||
if (latest.phase === "workspace_finalize" && latest.status === "succeeded") continue;
|
||||
const laterSuccessfulFinalize = latestSuccessfulFinalizeByWorkspace.get(pair.executionWorkspaceId);
|
||||
if (laterSuccessfulFinalize && laterSuccessfulFinalize > latest.startedAt) continue;
|
||||
pending.add(pair.blockerIssueId);
|
||||
}
|
||||
|
||||
@@ -1217,6 +1225,34 @@ async function listIssueDependencyReadinessMap(
|
||||
return readinessMap;
|
||||
}
|
||||
|
||||
async function listUnresolvedBlockerDetails(
|
||||
dbOrTx: Pick<Db, "select">,
|
||||
companyId: string,
|
||||
unresolvedBlockerIssueIds: string[],
|
||||
pendingFinalizeBlockerIssueIds: string[] = [],
|
||||
) {
|
||||
if (unresolvedBlockerIssueIds.length === 0) return [];
|
||||
const pendingFinalizeIds = new Set(pendingFinalizeBlockerIssueIds);
|
||||
const rows = await dbOrTx
|
||||
.select({
|
||||
issueId: issues.id,
|
||||
identifier: issues.identifier,
|
||||
title: issues.title,
|
||||
})
|
||||
.from(issues)
|
||||
.where(and(eq(issues.companyId, companyId), inArray(issues.id, unresolvedBlockerIssueIds)));
|
||||
const rowsById = new Map(rows.map((row) => [row.issueId, row]));
|
||||
return unresolvedBlockerIssueIds.map((issueId) => {
|
||||
const row = rowsById.get(issueId);
|
||||
return {
|
||||
issueId,
|
||||
identifier: row?.identifier ?? null,
|
||||
title: row?.title ?? null,
|
||||
reason: pendingFinalizeIds.has(issueId) ? "pending_finalize" as const : "not_done" as const,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
async function listUnresolvedBlockerIssueIds(
|
||||
dbOrTx: Pick<Db, "select">,
|
||||
companyId: string,
|
||||
@@ -1924,6 +1960,7 @@ function createIssueBlockerAttention(input: Partial<IssueBlockerAttention> = {})
|
||||
coveredBlockerCount: input.coveredBlockerCount ?? 0,
|
||||
stalledBlockerCount: input.stalledBlockerCount ?? 0,
|
||||
attentionBlockerCount: input.attentionBlockerCount ?? 0,
|
||||
pendingFinalizeBlockerIssueIds: input.pendingFinalizeBlockerIssueIds ?? [],
|
||||
sampleBlockerIdentifier: input.sampleBlockerIdentifier ?? null,
|
||||
sampleStalledBlockerIdentifier: input.sampleStalledBlockerIdentifier ?? null,
|
||||
};
|
||||
@@ -2179,10 +2216,17 @@ async function listIssueBlockerAttentionMap(
|
||||
|
||||
let frontier = roots.map((root) => root.id);
|
||||
let truncated = false;
|
||||
const pendingFinalizeBlockerIssueIds = new Set<string>();
|
||||
for (let depth = 0; frontier.length > 0 && depth < BLOCKER_ATTENTION_MAX_DEPTH; depth += 1) {
|
||||
const nextFrontier = new Set<string>();
|
||||
|
||||
for (const chunk of chunkList([...new Set(frontier)], ISSUE_LIST_RELATED_QUERY_CHUNK_SIZE)) {
|
||||
const readinessByIssueId = await listIssueDependencyReadinessMap(dbOrTx, companyId, chunk);
|
||||
for (const readiness of readinessByIssueId.values()) {
|
||||
for (const blockerIssueId of readiness.pendingFinalizeBlockerIssueIds) {
|
||||
pendingFinalizeBlockerIssueIds.add(blockerIssueId);
|
||||
}
|
||||
}
|
||||
const explicitBlockerRowsPromise: Promise<IssueBlockerAttentionQueryRow[]> = dbOrTx
|
||||
.select({
|
||||
issueId: issueRelations.relatedIssueId,
|
||||
@@ -2205,7 +2249,6 @@ async function listIssueBlockerAttentionMap(
|
||||
eq(issueRelations.type, "blocks"),
|
||||
inArray(issueRelations.relatedIssueId, chunk),
|
||||
eq(issues.companyId, companyId),
|
||||
ne(issues.status, "done"),
|
||||
),
|
||||
);
|
||||
const childRowsPromise: Promise<IssueBlockerAttentionQueryRow[]> = dbOrTx
|
||||
@@ -2235,8 +2278,11 @@ async function listIssueBlockerAttentionMap(
|
||||
childRowsPromise,
|
||||
]);
|
||||
|
||||
const unresolvedExplicitBlockerRows = explicitBlockerRows.filter(
|
||||
(row) => row.status !== "done" || pendingFinalizeBlockerIssueIds.has(row.blockerIssueId),
|
||||
);
|
||||
appendBlockerAttentionEdges(edgesByIssueId, [
|
||||
...explicitBlockerRows
|
||||
...unresolvedExplicitBlockerRows
|
||||
.filter((row): row is IssueBlockerAttentionQueryRow & { issueId: string } => row.issueId !== null)
|
||||
.map((row) => ({ issueId: row.issueId, blockerIssueId: row.blockerIssueId })),
|
||||
...childRows
|
||||
@@ -2244,7 +2290,7 @@ async function listIssueBlockerAttentionMap(
|
||||
.map((row) => ({ issueId: row.issueId, blockerIssueId: row.blockerIssueId })),
|
||||
]);
|
||||
|
||||
for (const row of [...explicitBlockerRows, ...childRows]) {
|
||||
for (const row of [...unresolvedExplicitBlockerRows, ...childRows]) {
|
||||
if (!row.issueId || nodesById.has(row.blockerIssueId)) continue;
|
||||
nodesById.set(row.blockerIssueId, {
|
||||
id: row.blockerIssueId,
|
||||
@@ -2416,7 +2462,7 @@ async function listIssueBlockerAttentionMap(
|
||||
return { covered: false, stalled: false, sampleBlockerIdentifier: nodeId, sampleStalledBlockerIdentifier: null };
|
||||
}
|
||||
const nodeSample = blockerSampleIdentifier(node);
|
||||
if (node.status === "done") {
|
||||
if (node.status === "done" && !pendingFinalizeBlockerIssueIds.has(node.id)) {
|
||||
return { covered: true, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
|
||||
}
|
||||
if (explicitWaitingIssueIds.has(node.id)) {
|
||||
@@ -2442,7 +2488,10 @@ async function listIssueBlockerAttentionMap(
|
||||
return { covered: false, stalled: false, sampleBlockerIdentifier: nodeSample, sampleStalledBlockerIdentifier: null };
|
||||
}
|
||||
|
||||
const downstream = (edgesByIssueId.get(node.id) ?? []).filter((edge) => nodesById.get(edge.blockerIssueId)?.status !== "done");
|
||||
const downstream = (edgesByIssueId.get(node.id) ?? []).filter((edge) => {
|
||||
const blocker = nodesById.get(edge.blockerIssueId);
|
||||
return blocker?.status !== "done" || pendingFinalizeBlockerIssueIds.has(edge.blockerIssueId);
|
||||
});
|
||||
if (downstream.length > 0) {
|
||||
const nextSeen = new Set(seen);
|
||||
nextSeen.add(nodeId);
|
||||
@@ -2486,7 +2535,10 @@ async function listIssueBlockerAttentionMap(
|
||||
};
|
||||
|
||||
for (const root of roots) {
|
||||
const topLevelEdges = (edgesByIssueId.get(root.id) ?? []).filter((edge) => nodesById.get(edge.blockerIssueId)?.status !== "done");
|
||||
const topLevelEdges = (edgesByIssueId.get(root.id) ?? []).filter((edge) => {
|
||||
const blocker = nodesById.get(edge.blockerIssueId);
|
||||
return blocker?.status !== "done" || pendingFinalizeBlockerIssueIds.has(edge.blockerIssueId);
|
||||
});
|
||||
if (topLevelEdges.length === 0) {
|
||||
attentionMap.set(root.id, createIssueBlockerAttention({
|
||||
state: "needs_attention",
|
||||
@@ -2532,6 +2584,9 @@ async function listIssueBlockerAttentionMap(
|
||||
coveredBlockerCount,
|
||||
stalledBlockerCount,
|
||||
attentionBlockerCount,
|
||||
pendingFinalizeBlockerIssueIds: topLevelEdges
|
||||
.map((edge) => edge.blockerIssueId)
|
||||
.filter((blockerIssueId) => pendingFinalizeBlockerIssueIds.has(blockerIssueId)),
|
||||
sampleBlockerIdentifier: sampleEntry?.result.sampleBlockerIdentifier ?? blockerSampleIdentifier(sampleNode),
|
||||
sampleStalledBlockerIdentifier:
|
||||
stalledEntry?.result.sampleStalledBlockerIdentifier ?? sampleStalledFromChain ?? null,
|
||||
@@ -6624,13 +6679,23 @@ export function issueService(db: Db) {
|
||||
throw unprocessable("in_progress issues require an assignee");
|
||||
}
|
||||
if (patch.status === "in_progress") {
|
||||
const dependencyReadiness = blockedByIssueIds === undefined
|
||||
? (await listIssueDependencyReadinessMap(dbOrTx, existing.companyId, [id])).get(id)
|
||||
: null;
|
||||
const unresolvedBlockerIssueIds = blockedByIssueIds !== undefined
|
||||
? await listUnresolvedBlockerIssueIds(dbOrTx, existing.companyId, blockedByIssueIds)
|
||||
: (
|
||||
await listIssueDependencyReadinessMap(dbOrTx, existing.companyId, [id])
|
||||
).get(id)?.unresolvedBlockerIssueIds ?? [];
|
||||
: dependencyReadiness?.unresolvedBlockerIssueIds ?? [];
|
||||
if (unresolvedBlockerIssueIds.length > 0) {
|
||||
throw unprocessable("Issue is blocked by unresolved blockers", { unresolvedBlockerIssueIds });
|
||||
const unresolvedBlockers = await listUnresolvedBlockerDetails(
|
||||
dbOrTx,
|
||||
existing.companyId,
|
||||
unresolvedBlockerIssueIds,
|
||||
dependencyReadiness?.pendingFinalizeBlockerIssueIds,
|
||||
);
|
||||
throw unprocessable("Issue is blocked by unresolved blockers", {
|
||||
unresolvedBlockerIssueIds,
|
||||
unresolvedBlockers,
|
||||
});
|
||||
}
|
||||
}
|
||||
const shouldValidateNextAssignee =
|
||||
@@ -6926,9 +6991,19 @@ export function issueService(db: Db) {
|
||||
await clearCheckoutRunIfTerminal(id);
|
||||
|
||||
const dependencyReadiness = await listIssueDependencyReadinessMap(db, issueCompany.companyId, [id]);
|
||||
const unresolvedBlockerIssueIds = dependencyReadiness.get(id)?.unresolvedBlockerIssueIds ?? [];
|
||||
const readiness = dependencyReadiness.get(id);
|
||||
const unresolvedBlockerIssueIds = readiness?.unresolvedBlockerIssueIds ?? [];
|
||||
if (unresolvedBlockerIssueIds.length > 0) {
|
||||
throw unprocessable("Issue is blocked by unresolved blockers", { unresolvedBlockerIssueIds });
|
||||
const unresolvedBlockers = await listUnresolvedBlockerDetails(
|
||||
db,
|
||||
issueCompany.companyId,
|
||||
unresolvedBlockerIssueIds,
|
||||
readiness?.pendingFinalizeBlockerIssueIds,
|
||||
);
|
||||
throw unprocessable("Issue is blocked by unresolved blockers", {
|
||||
unresolvedBlockerIssueIds,
|
||||
unresolvedBlockers,
|
||||
});
|
||||
}
|
||||
|
||||
const sameRunAssigneeCondition = checkoutRunId
|
||||
|
||||
Reference in new issue
Block a user