Skip to content
Open
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
11 changes: 11 additions & 0 deletions sdk/typescript/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,17 @@ scan must already exist in the local scan history. Running publication again
creates another set of issues for the same scan; existing issues are not
matched, updated, or reused.

Connected-app publication checks recorded issue-creation arguments against the
prepared payload. If a completed mutation cannot be verified, publication keeps
any verified successes in local history, retains its private recovery handoff
and available event log, and exits with the recovery path. Inspect the existing
Linear issues before retrying. A failure to save the auxiliary event log does
not discard verified issue records; event logs are written only when recovery
is needed, after verified history persistence has been attempted. Indeterminate
receipts include an explicit status and the private recovery path, even when
every planned issue was created. This checks the submitted arguments; it is not
a fresh readback of the remote issues.

Issue descriptions contain source code and vulnerability details. Select a
Linear destination authorized to receive that information. Publication receipts
are stored separately from the sealed scan artifacts.
Expand Down
65 changes: 58 additions & 7 deletions sdk/typescript/src/publication-events.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import type {
PreparedPublicationIssue,
PreparedScanPublication,
import {
linearPublicationArguments,
type PreparedPublicationIssue,
type PreparedScanPublication,
} from "./publication.js";

export interface CollectedPublicationEvents {
Expand All @@ -11,6 +12,9 @@ export interface CollectedPublicationEvents {
url?: string;
}>;
failed: Array<{ findingId: string; error: string }>;
indeterminate?: boolean;
completedEvents?: string[];
unresolvedCompletions?: string[];
}

export function collectPublicationEvents(
Expand All @@ -24,6 +28,12 @@ export function collectPublicationEvents(
>();
const failed = new Map<string, string>();
const unexpected: string[] = [];
const completedEvents: Array<{
findingId: string | undefined;
line: string;
}> = [];
const indeterminateFindings = new Set<string>();
const unresolvedCompletions = new Set<string>();

for (const line of output.split(/\r?\n/)) {
if (line.trim().length === 0) continue;
Expand All @@ -49,11 +59,23 @@ export function collectPublicationEvents(
const issue = isRecord(args)
? matchPublicationIssue(publication, args)
: undefined;
if (item["status"] === "completed") {
completedEvents.push({ findingId: issue?.findingId, line });
}
if (issue === undefined) {
unexpected.push("Codex attempted to create an unexpected Linear issue.");
continue;
}
if (!hasExpectedPublicationArguments(publication, issue, args)) {
indeterminateFindings.add(issue.findingId);
failed.set(
issue.findingId,
"Codex attempted to create a Linear issue with unexpected arguments or destination.",
);
continue;
}
if (failed.has(issue.findingId) || created.has(issue.findingId)) {
indeterminateFindings.add(issue.findingId);
failed.set(
issue.findingId,
"Codex attempted to create more than one Linear issue for this finding.",
Expand All @@ -73,6 +95,7 @@ export function collectPublicationEvents(

const saved = savedIssue(item["result"]);
if (saved === undefined) {
unresolvedCompletions.add(issue.findingId);
failed.set(
issue.findingId,
"The connected Linear app did not return a created issue identifier.",
Expand All @@ -88,13 +111,25 @@ export function collectPublicationEvents(
}

if (unexpected.length > 0 && publication.issues.length > 0) {
const target =
publication.issues.find((issue) => !created.has(issue.findingId)) ??
publication.issues[0]!;
failed.set(target.findingId, unexpected.join(" "));
const target = publication.issues.find(
(issue) => !created.has(issue.findingId),
);
if (target !== undefined)
failed.set(target.findingId, unexpected.join(" "));
}
const indeterminate = completedEvents.some(
({ findingId }) =>
findingId === undefined || indeterminateFindings.has(findingId),
);

return {
...(indeterminate ? { indeterminate: true } : {}),
...(completedEvents.length > 0
? { completedEvents: completedEvents.map(({ line }) => line) }
: {}),
...(unresolvedCompletions.size > 0
? { unresolvedCompletions: [...unresolvedCompletions] }
: {}),
created: publication.issues.flatMap((issue) => {
if (failed.has(issue.findingId)) return [];
const result = created.get(issue.findingId);
Expand All @@ -110,6 +145,22 @@ export function collectPublicationEvents(
};
}

export function hasExpectedPublicationArguments(
publication: PreparedScanPublication,
issue: PreparedPublicationIssue,
actual: unknown,
): boolean {
if (!isRecord(actual)) return false;
const expected = linearPublicationArguments(publication.destination, issue);
return (
Object.keys(actual).length === Object.keys(expected).length &&
Object.entries(expected).every(
([key, value]) =>
Object.hasOwn(actual, key) && Object.is(actual[key], value),
)
);
}

export function matchPublicationIssue(
publication: PreparedScanPublication,
arguments_: Record<string, unknown>,
Expand Down
18 changes: 18 additions & 0 deletions sdk/typescript/src/publication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ export interface PreparedScanPublication {
issues: PreparedPublicationIssue[];
}

export function linearPublicationArguments(
destination: LinearPublicationDestination,
issue: PreparedPublicationIssue,
): Pick<PreparedPublicationIssue, "title" | "description" | "priority"> & {
team: string;
project?: string;
} {
return {
team: destination.teamId,
...(destination.projectId === undefined
? {}
: { project: destination.projectId }),
title: issue.title,
description: issue.description,
...(issue.priority === undefined ? {} : { priority: issue.priority }),
};
}

const LINEAR_PRIORITIES = {
critical: 1,
high: 2,
Expand Down
Loading
Loading