Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
85e07cf
fix(integrations): enforce current task ownership
tinsever Sep 29, 2026
e04e85e
style(integrations): format scoped gitea close handler
tinsever Sep 29, 2026
8787420
fix(integrations): preserve resources and fetch comments before locki…
tinsever Sep 29, 2026
394cd41
fix(tasks): invalidate moved resources in the initiating window
tinsever Sep 29, 2026
ca29c96
fix: address verified concurrency and migration review findings
tinsever Sep 29, 2026
71e2a3e
fix: preserve live resources across concurrent moves and cleanup
tinsever Sep 29, 2026
f2e2527
fix: address verified delivery and concurrency review findings
tinsever Sep 29, 2026
088a8f1
fix: preserve manual links for destinations without integrations
tinsever Sep 29, 2026
f37b776
merge: preserve current upstream PR matching and reopen behavior
tinsever Sep 29, 2026
6104cd0
Revalidate imported issue links after provider reads
tinsever Sep 30, 2026
2dd66f6
fix(integrations): guard pull request transitions against task moves
tinsever Sep 30, 2026
802c5bd
fix(integrations): revalidate links across webhook and import continu…
tinsever Sep 30, 2026
ca6e4f7
fix(integrations): publish live task updates after committed provider…
tinsever Sep 30, 2026
cf2b030
fix(integrations): refresh imported task details after committed pages
tinsever Sep 30, 2026
b62c486
fix(integrations): publish complete GitHub task creation data
tinsever Sep 30, 2026
53c75ea
fix(integrations): announce committed PR links and reimported resources
tinsever Sep 30, 2026
2fca490
fix(integrations): keep branch link and status changes in one ownersh…
tinsever Sep 30, 2026
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
150 changes: 91 additions & 59 deletions apps/api/src/gitea-integration/controllers/import-gitea-issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ import {
import { formatTaskDescriptionFromIssue } from "../../plugins/github/utils/format";
import { claimTaskNumber } from "../../task/controllers/claim-task-numbers";

import {
type IntegrationDatabase,
linkedTaskScope,
withIntegrationTask,
} from "../../plugins/github/services/integration-task-scope";

type ImportResult = {
imported: number;
updated: number;
Expand Down Expand Up @@ -210,70 +216,94 @@ async function importSingleIssue(
const status = extractIssueStatus(adaptedLabels);

if (existingLink) {
const updateData: Record<string, unknown> = {
title: issue.title,
description: formatTaskDescriptionFromIssue(issue.body),
};

if (priority) updateData.priority = priority;
if (status) updateData.status = status;

await db
.update(taskTable)
.set(updateData)
.where(eq(taskTable.id, existingLink.taskId));

await importLabelsForTask(labels, existingLink.taskId, workspaceId);

await importCommentsForTask(
issue.number,
const result = await withIntegrationTask(
existingLink.taskId,
config,
client,
{ id: integrationId, projectId, project: { workspaceId } },
Comment thread
tinsever marked this conversation as resolved.
async (database) => {
const updateData: Record<string, unknown> = {
title: issue.title,
description: formatTaskDescriptionFromIssue(issue.body),
};

if (priority) updateData.priority = priority;
if (status) updateData.status = status;

await database
.update(taskTable)
.set(updateData)
.where(linkedTaskScope(existingLink.taskId, projectId));

await importLabelsForTask(
labels,
existingLink.taskId,
workspaceId,
database,
);

await importCommentsForTask(
issue.number,
existingLink.taskId,
config,
Comment thread
tinsever marked this conversation as resolved.
Outdated
client,
database,
);
Comment thread
tinsever marked this conversation as resolved.
Outdated

return "updated" as const;
},
);

return "updated";
return result ?? "skipped";
}

const createdTask = await db.transaction(async (tx) => {
const nextNumber = await claimTaskNumber(projectId, tx);
const createdTask = await withIntegrationTask(
null,
{ id: integrationId, projectId, project: { workspaceId } },
async (tx) => {
const nextNumber = await claimTaskNumber(projectId, tx);

const taskValues: typeof taskTable.$inferInsert = {
projectId,
userId: null,
title: issue.title,
description: formatTaskDescriptionFromIssue(issue.body),
status: status || "to-do",
priority: priority ?? "low",
number: nextNumber,
};
const taskValues: typeof taskTable.$inferInsert = {
projectId,
userId: null,
title: issue.title,
description: formatTaskDescriptionFromIssue(issue.body),
status: status || "to-do",
priority: priority ?? "low",
number: nextNumber,
};

const [created] = await tx.insert(taskTable).values(taskValues).returning();
const [created] = await tx
.insert(taskTable)
.values(taskValues)
.returning();

if (!created) {
throw new Error("Failed to create task");
}
if (!created) {
throw new Error("Failed to create task");
}

return created;
});
await createExternalLink(
{
taskId: created.id,
integrationId,
resourceType: "issue",
externalId: issue.number.toString(),
url: issue.html_url,
title: issue.title,
metadata: {
state: issue.state,
createdFrom: "gitea-import",
author: issue.user?.login ?? issue.user?.username,
},
},
tx,
);

await createExternalLink({
taskId: createdTask.id,
integrationId,
resourceType: "issue",
externalId: issue.number.toString(),
url: issue.html_url,
title: issue.title,
metadata: {
state: issue.state,
createdFrom: "gitea-import",
author: issue.user?.login ?? issue.user?.username,
},
});
await importLabelsForTask(labels, created.id, workspaceId, tx);

await importLabelsForTask(labels, createdTask.id, workspaceId);
await importCommentsForTask(issue.number, created.id, config, client, tx);

await importCommentsForTask(issue.number, createdTask.id, config, client);
return created;
},
);
if (!createdTask) return "skipped";

await publishEvent("task.created", {
...createdTask,
Expand All @@ -293,6 +323,7 @@ async function importLabelsForTask(
issueLabels: GiteaIssue["labels"],
taskId: string,
workspaceId: string,
database: IntegrationDatabase = db,
): Promise<void> {
const nonSystemLabels = (issueLabels ?? [])
.map((label) => {
Expand All @@ -316,7 +347,7 @@ async function importLabelsForTask(
const expectedNames = nonSystemLabels.map((label) => label.name);

if (expectedNames.length > 0) {
await db
await database
.delete(labelTable)
.where(
and(
Expand All @@ -325,10 +356,10 @@ async function importLabelsForTask(
),
);
} else {
await db.delete(labelTable).where(eq(labelTable.taskId, taskId));
await database.delete(labelTable).where(eq(labelTable.taskId, taskId));
}

const existingLabelsOnTask = await db.query.labelTable.findMany({
const existingLabelsOnTask = await database.query.labelTable.findMany({
where:
expectedNames.length > 0
? and(
Expand All @@ -347,7 +378,7 @@ async function importLabelsForTask(
continue;
}

const existingWorkspaceLabel = await db.query.labelTable.findFirst({
const existingWorkspaceLabel = await database.query.labelTable.findFirst({
where: and(
eq(labelTable.workspaceId, workspaceId),
eq(labelTable.name, labelData.name),
Expand All @@ -356,7 +387,7 @@ async function importLabelsForTask(

const colorToUse = existingWorkspaceLabel?.color || labelData.color;

await db
await database
.insert(labelTable)
.values({
name: labelData.name,
Expand All @@ -375,6 +406,7 @@ async function importCommentsForTask(
taskId: string,
config: GiteaConfig,
client: ReturnType<typeof createGiteaClient>,
database: IntegrationDatabase = db,
): Promise<void> {
const allComments: Array<{
id: number;
Expand Down Expand Up @@ -407,7 +439,7 @@ async function importCommentsForTask(
continue;
}

await db
await database
.insert(activityTable)
.values({
taskId,
Expand Down
33 changes: 27 additions & 6 deletions apps/api/src/github-integration/controllers/import-issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -173,6 +173,17 @@ export async function importIssues(projectId: string, runId?: string) {
}
const currentRun: typeof githubImportTable.$inferSelect = run;
run = await db.transaction(async (tx) => {
const [currentProject] = await tx
.select()
.from(projectTable)
.where(
and(
eq(projectTable.id, project.id),
eq(projectTable.workspaceId, project.workspaceId),
),
)
.for("key share");
if (!currentProject) throw conflict();
// Serialize against integration changes and webhook issue creation. No
// provider request is made while this transaction holds row locks.
const [currentIntegration] = await tx
Expand Down Expand Up @@ -305,12 +316,16 @@ async function applyPage(
}
const current = state.currentIssue;
if (!current) throw new Error("Import continuation missing");
const task = await tx.query.taskTable.findFirst({
where: and(
eq(taskTable.id, current.taskId),
eq(taskTable.projectId, project.id),
),
});
const [task] = await tx
.select()
.from(taskTable)
.where(
and(
eq(taskTable.id, current.taskId),
eq(taskTable.projectId, project.id),
),
)
Comment thread
tinsever marked this conversation as resolved.
.for("no key update");
if (!task) {
state.skipped++;
finishIssue(state);
Expand Down Expand Up @@ -540,6 +555,12 @@ async function linkPull(
database: tx,
});
if (!task) return;
const [scopedTask] = await tx
.select({ id: taskTable.id })
.from(taskTable)
.where(and(eq(taskTable.id, task.id), eq(taskTable.projectId, project.id)))
.for("share");
if (!scopedTask) return;
await tx.insert(externalLinkTable).values({
taskId: task.id,
integrationId,
Expand Down
Loading
Loading