@sdods/integrations 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/store.js ADDED
@@ -0,0 +1,75 @@
1
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { dirname } from 'node:path';
3
+ import { newId } from '@sdods/contracts';
4
+ /**
5
+ * JSON-file store so the CLI works without a database (`.sdods/issue-links.json`).
6
+ * Pass `null` as the file for an in-memory store (tests). Not safe for concurrent writers across machines.
7
+ */
8
+ export class FileIssueLinkStore {
9
+ file;
10
+ links = null;
11
+ constructor(file) {
12
+ this.file = file;
13
+ }
14
+ load() {
15
+ if (this.links)
16
+ return this.links;
17
+ if (!this.file || !existsSync(this.file)) {
18
+ this.links = [];
19
+ return this.links;
20
+ }
21
+ try {
22
+ const parsed = JSON.parse(readFileSync(this.file, 'utf8'));
23
+ this.links = Array.isArray(parsed.links) ? parsed.links : [];
24
+ }
25
+ catch {
26
+ this.links = [];
27
+ }
28
+ return this.links;
29
+ }
30
+ persist() {
31
+ if (!this.file)
32
+ return;
33
+ mkdirSync(dirname(this.file), { recursive: true });
34
+ writeFileSync(this.file, JSON.stringify({ version: 1, links: this.load() }, null, 2));
35
+ }
36
+ async findOpen(projectSlug, provider, fingerprint) {
37
+ return this.load().find((l) => l.projectSlug === projectSlug &&
38
+ l.provider === provider &&
39
+ l.fingerprint === fingerprint &&
40
+ l.status !== 'closed');
41
+ }
42
+ async findByKey(projectSlug, provider, externalKey) {
43
+ return this.load().find((l) => l.projectSlug === projectSlug && l.provider === provider && l.externalKey === externalKey);
44
+ }
45
+ async save(input) {
46
+ const links = this.load();
47
+ const now = new Date().toISOString();
48
+ const existing = links.find((l) => (input.id !== undefined && l.id === input.id) ||
49
+ (l.projectSlug === input.projectSlug &&
50
+ l.provider === input.provider &&
51
+ l.fingerprint === input.fingerprint &&
52
+ l.externalKey === input.externalKey));
53
+ if (existing) {
54
+ Object.assign(existing, input, { updatedAt: now });
55
+ this.persist();
56
+ return existing;
57
+ }
58
+ const link = {
59
+ ...input,
60
+ id: input.id ?? newId(),
61
+ createdAt: input.createdAt ?? now,
62
+ updatedAt: now,
63
+ };
64
+ links.push(link);
65
+ this.persist();
66
+ return link;
67
+ }
68
+ async list(projectSlug, provider) {
69
+ return this.load().filter((l) => l.projectSlug === projectSlug && (provider ? l.provider === provider : true));
70
+ }
71
+ }
72
+ export function createMemoryStore() {
73
+ return new FileIssueLinkStore(null);
74
+ }
75
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1,15 @@
1
+ import type { RunSummaryInput, ScenarioSummary } from './types.js';
2
+ /**
3
+ * Build a RunSummaryInput from the files in a run directory (`.sdods/runs/<runId>`), so
4
+ * `sdods integrations notify --from-files` works without a database.
5
+ * Reads run.json (manifest), summary.json (optional totals) and messages*.ndjson (cucumber messages).
6
+ */
7
+ export declare function buildRunSummaryFromFiles(runDir: string, opts?: {
8
+ projectSlug?: string;
9
+ reportUrl?: string;
10
+ }): RunSummaryInput;
11
+ export declare function parseMessages(text: string, opts: {
12
+ runDir?: string;
13
+ projectSlug: string;
14
+ }): ScenarioSummary[];
15
+ //# sourceMappingURL=summary.d.ts.map
@@ -0,0 +1,349 @@
1
+ import { existsSync, readFileSync } from 'node:fs';
2
+ import { basename, join } from 'node:path';
3
+ import { fingerprint as makeFingerprint, parseAttachmentName, parseRunnerProjectName, runFiles, scenarioFiles, } from '@sdods/contracts';
4
+ /**
5
+ * Build a RunSummaryInput from the files in a run directory (`.sdods/runs/<runId>`), so
6
+ * `sdods integrations notify --from-files` works without a database.
7
+ * Reads run.json (manifest), summary.json (optional totals) and messages*.ndjson (cucumber messages).
8
+ */
9
+ export function buildRunSummaryFromFiles(runDir, opts = {}) {
10
+ const manifest = readJson(join(runDir, runFiles.manifest));
11
+ const runId = manifest?.runId ?? basename(runDir);
12
+ const projectSlug = opts.projectSlug ?? manifest?.projectSlug ?? 'unknown';
13
+ const ndjsonFiles = listNdjson(runDir);
14
+ const scenarios = ndjsonFiles.flatMap((f) => parseMessages(readFileSync(f, 'utf8'), { runDir, projectSlug }));
15
+ const totals = computeTotals(scenarios, manifest);
16
+ const run = {
17
+ id: runId,
18
+ projectSlug,
19
+ env: manifest?.env ?? 'unknown',
20
+ trigger: manifest?.trigger ?? 'cli',
21
+ status: totals.failed > 0 ? 'failed' : manifest?.exitCode === 130 ? 'cancelled' : 'passed',
22
+ suiteTag: manifest?.suiteTag,
23
+ tagsExpr: manifest?.tagsExpr,
24
+ layers: manifest?.layers ?? [],
25
+ browsers: manifest?.browsers ?? [],
26
+ gitSha: manifest?.git?.sha,
27
+ gitBranch: manifest?.git?.branch,
28
+ ciProvider: manifest?.ci?.provider,
29
+ ciRunId: manifest?.ci?.runId,
30
+ ciUrl: manifest?.ci?.url,
31
+ startedAt: manifest?.startedAt,
32
+ finishedAt: manifest?.finishedAt,
33
+ durationMs: totals.durationMs,
34
+ totals,
35
+ artifactsDir: runDir,
36
+ exitCode: manifest?.exitCode,
37
+ };
38
+ const failed = scenarios.filter((s) => s.status === 'failed' || s.status === 'timedOut');
39
+ const flaky = scenarios.filter((s) => s.flaky);
40
+ const passed = scenarios.filter((s) => s.status === 'passed');
41
+ return { run, totals, scenarios, failed, flaky, passed, runDir, reportUrl: opts.reportUrl };
42
+ }
43
+ function listNdjson(runDir) {
44
+ const out = [];
45
+ const main = join(runDir, runFiles.messages);
46
+ if (existsSync(main))
47
+ out.push(main);
48
+ for (let i = 1; i <= 64; i++) {
49
+ const f = join(runDir, runFiles.messagesShard(i));
50
+ if (existsSync(f))
51
+ out.push(f);
52
+ }
53
+ return out;
54
+ }
55
+ function readJson(file) {
56
+ if (!existsSync(file))
57
+ return undefined;
58
+ try {
59
+ return JSON.parse(readFileSync(file, 'utf8'));
60
+ }
61
+ catch {
62
+ return undefined;
63
+ }
64
+ }
65
+ function computeTotals(scenarios, manifest) {
66
+ const totals = {
67
+ total: scenarios.length,
68
+ passed: 0,
69
+ failed: 0,
70
+ skipped: 0,
71
+ timedOut: 0,
72
+ flaky: 0,
73
+ healed: 0,
74
+ durationMs: 0,
75
+ };
76
+ for (const s of scenarios) {
77
+ if (s.status === 'passed')
78
+ totals.passed++;
79
+ else if (s.status === 'failed')
80
+ totals.failed++;
81
+ else if (s.status === 'timedOut')
82
+ totals.timedOut++;
83
+ else if (s.status === 'skipped')
84
+ totals.skipped++;
85
+ if (s.flaky)
86
+ totals.flaky++;
87
+ }
88
+ if (manifest?.startedAt && manifest.finishedAt) {
89
+ totals.durationMs = Math.max(0, Date.parse(manifest.finishedAt) - Date.parse(manifest.startedAt));
90
+ }
91
+ else {
92
+ totals.durationMs = scenarios.reduce((acc, s) => acc + (s.durationMs ?? 0), 0);
93
+ }
94
+ return totals;
95
+ }
96
+ export function parseMessages(text, opts) {
97
+ const docs = new Map();
98
+ const pickles = new Map();
99
+ const testCases = new Map();
100
+ const attempts = new Map();
101
+ const scenarios = new Map();
102
+ const stepStarts = new Map();
103
+ for (const line of text.split('\n')) {
104
+ const trimmed = line.trim();
105
+ if (!trimmed)
106
+ continue;
107
+ let env;
108
+ try {
109
+ env = JSON.parse(trimmed);
110
+ }
111
+ catch {
112
+ continue;
113
+ }
114
+ if (env.gherkinDocument?.uri) {
115
+ const lines = new Map();
116
+ const examplesIndex = new Map();
117
+ const walk = (children) => {
118
+ for (const c of children ?? []) {
119
+ const node = c.scenario ?? c.background;
120
+ if (node?.id)
121
+ lines.set(node.id, node.location?.line ?? 0);
122
+ if (c.scenario?.examples) {
123
+ let idx = 0;
124
+ for (const ex of c.scenario.examples)
125
+ for (const row of ex.tableBody ?? [])
126
+ examplesIndex.set(row.id, idx++);
127
+ }
128
+ if (c.rule)
129
+ walk(c.rule.children);
130
+ }
131
+ };
132
+ walk(env.gherkinDocument.feature?.children);
133
+ docs.set(splitUri(env.gherkinDocument.uri).uri, {
134
+ name: env.gherkinDocument.feature?.name ?? '',
135
+ lines,
136
+ examplesIndex,
137
+ });
138
+ }
139
+ else if (env.pickle) {
140
+ pickles.set(env.pickle.id, env.pickle);
141
+ }
142
+ else if (env.testCase) {
143
+ testCases.set(env.testCase.id, env.testCase);
144
+ }
145
+ else if (env.testCaseStarted) {
146
+ const tc = testCases.get(env.testCaseStarted.testCaseId);
147
+ const pickle = tc && pickles.get(tc.pickleId);
148
+ if (!tc || !pickle)
149
+ continue;
150
+ const { runnerProject, uri } = splitUri(pickle.uri);
151
+ const key = `${runnerProject}:${pickle.id}`;
152
+ const state = {
153
+ scenarioKey: key,
154
+ attempt: env.testCaseStarted.attempt ?? 0,
155
+ steps: [],
156
+ status: 'passed',
157
+ startedAt: tsMs(env.testCaseStarted.timestamp),
158
+ screenshots: [],
159
+ };
160
+ attempts.set(env.testCaseStarted.id, state);
161
+ const entry = scenarios.get(key) ?? {
162
+ pickle: { ...pickle, uri },
163
+ runnerProject,
164
+ attempts: [],
165
+ };
166
+ entry.attempts.push(state);
167
+ scenarios.set(key, entry);
168
+ }
169
+ else if (env.testStepStarted) {
170
+ stepStarts.set(`${env.testStepStarted.testCaseStartedId}:${env.testStepStarted.testStepId}`, tsMs(env.testStepStarted.timestamp) ?? 0);
171
+ }
172
+ else if (env.testStepFinished) {
173
+ const state = attempts.get(env.testStepFinished.testCaseStartedId);
174
+ if (!state)
175
+ continue;
176
+ const entry = scenarios.get(state.scenarioKey);
177
+ const tc = entry && [...testCases.values()].find((t) => t.pickleId === entry.pickle.id);
178
+ const ts = tc?.testSteps?.find((s) => s.id === env.testStepFinished.testStepId);
179
+ const result = env.testStepFinished.testStepResult ?? {};
180
+ const status = mapStatus(result.status);
181
+ if (ts?.pickleStepId) {
182
+ const idx = entry.pickle.steps?.findIndex((s) => s.id === ts.pickleStepId) ?? -1;
183
+ const pstep = entry.pickle.steps?.[idx];
184
+ state.steps.push({
185
+ index: idx,
186
+ keyword: keywordFromType(pstep?.type),
187
+ text: pstep?.text ?? '',
188
+ status,
189
+ errorMessage: result.exception?.message ?? result.message,
190
+ durationMs: durMs(result.duration),
191
+ });
192
+ }
193
+ if ((status === 'failed' || status === 'timedOut') && state.status === 'passed') {
194
+ state.status = status;
195
+ state.error = result.exception?.message ?? result.message;
196
+ state.stack = result.exception?.stackTrace;
197
+ }
198
+ else if (status === 'skipped' && state.status === 'passed' && ts?.pickleStepId) {
199
+ state.status = 'skipped';
200
+ }
201
+ }
202
+ else if (env.attachment?.testCaseStartedId) {
203
+ const state = attempts.get(env.attachment.testCaseStartedId);
204
+ if (!state || !env.attachment.fileName)
205
+ continue;
206
+ const parsed = parseAttachmentName(env.attachment.fileName);
207
+ const entry = scenarios.get(state.scenarioKey);
208
+ const rel = (p) => relPathFor(opts.projectSlug, entry, state, p);
209
+ if (parsed.kind === 'shot-scenario')
210
+ state.screenshots.push({
211
+ relPath: rel(scenarioFiles.scenarioShot(parsed.phase)),
212
+ phase: parsed.phase === 'start'
213
+ ? 'scenario-start'
214
+ : parsed.phase === 'end'
215
+ ? 'scenario-end'
216
+ : 'failure',
217
+ });
218
+ else if (parsed.kind === 'shot-step')
219
+ state.screenshots.push({
220
+ relPath: rel(scenarioFiles.stepShot(parsed.stepIndex, parsed.phase)),
221
+ phase: parsed.phase,
222
+ stepIndex: parsed.stepIndex,
223
+ });
224
+ else if (parsed.kind === 'visual-baseline')
225
+ state.screenshots.push({
226
+ relPath: env.attachment.url ?? `${parsed.name}-${parsed.phase}.png`,
227
+ phase: parsed.phase,
228
+ name: parsed.name,
229
+ });
230
+ else if (parsed.kind === 'runner-builtin' && parsed.name === 'screenshot')
231
+ state.screenshots.push({
232
+ relPath: env.attachment.url ?? 'screenshot.png',
233
+ phase: 'failure',
234
+ });
235
+ else if (parsed.kind === 'runner-builtin' && parsed.name === 'trace')
236
+ state.tracePath = env.attachment.url ?? 'trace.zip';
237
+ }
238
+ else if (env.testCaseFinished) {
239
+ const state = attempts.get(env.testCaseFinished.testCaseStartedId);
240
+ if (!state)
241
+ continue;
242
+ state.finishedAt = tsMs(env.testCaseFinished.timestamp);
243
+ state.willBeRetried = env.testCaseFinished.willBeRetried;
244
+ }
245
+ }
246
+ const out = [];
247
+ for (const { pickle, runnerProject, attempts: atts } of scenarios.values()) {
248
+ const sorted = atts.sort((a, b) => a.attempt - b.attempt);
249
+ const final = sorted[sorted.length - 1];
250
+ const doc = docs.get(pickle.uri);
251
+ const parts = parseRunnerProjectName(runnerProject);
252
+ const layer = parts?.layer ?? 'ui';
253
+ const scenarioAst = pickle.astNodeIds?.[0];
254
+ const exampleRow = pickle.astNodeIds?.[1];
255
+ const exampleIndex = exampleRow && doc ? (doc.examplesIndex.get(exampleRow) ?? null) : null;
256
+ const tags = (pickle.tags ?? []).map((t) => t.name);
257
+ const fp = makeFingerprint({
258
+ project: opts.projectSlug,
259
+ featureUri: pickle.uri,
260
+ scenarioName: pickle.name,
261
+ exampleIndex,
262
+ layer,
263
+ });
264
+ out.push({
265
+ fingerprint: fp,
266
+ featureUri: pickle.uri,
267
+ featureName: doc?.name ?? pickle.uri,
268
+ scenarioName: pickle.name,
269
+ line: scenarioAst && doc ? doc.lines.get(scenarioAst) : undefined,
270
+ exampleIndex,
271
+ runnerProject,
272
+ layer,
273
+ browser: parts?.browser,
274
+ suiteTag: tags.find((t) => ['@smoke', '@regression', '@sanity'].includes(t)),
275
+ tags,
276
+ status: final.status,
277
+ flaky: sorted.length > 1 && final.status === 'passed',
278
+ attemptsCount: sorted.length,
279
+ durationMs: final.startedAt !== undefined && final.finishedAt !== undefined
280
+ ? final.finishedAt - final.startedAt
281
+ : undefined,
282
+ errorMessage: final.error,
283
+ errorStack: final.stack,
284
+ steps: final.steps,
285
+ screenshots: final.screenshots,
286
+ jiraKeys: tags
287
+ .filter((t) => /^@jira:[A-Z][A-Z0-9]+-\d+$/i.test(t))
288
+ .map((t) => t.slice('@jira:'.length).toUpperCase()),
289
+ githubIssues: tags
290
+ .filter((t) => /^@github:\d+$/.test(t))
291
+ .map((t) => t.slice('@github:'.length)),
292
+ tracePath: final.tracePath,
293
+ });
294
+ }
295
+ return out;
296
+ }
297
+ function relPathFor(projectSlug, entry, state, file) {
298
+ const parts = parseRunnerProjectName(entry.runnerProject);
299
+ const fp = makeFingerprint({
300
+ project: projectSlug,
301
+ featureUri: entry.pickle.uri,
302
+ scenarioName: entry.pickle.name,
303
+ exampleIndex: null,
304
+ layer: parts?.layer ?? 'ui',
305
+ });
306
+ return `${projectSlug}/${fp}/r${state.attempt}/${file}`;
307
+ }
308
+ function splitUri(uri) {
309
+ const m = /^\[([^\]]+)\]:(.*)$/.exec(uri);
310
+ return m ? { runnerProject: m[1], uri: m[2] } : { runnerProject: 'unknown', uri };
311
+ }
312
+ function mapStatus(s) {
313
+ switch ((s ?? '').toUpperCase()) {
314
+ case 'PASSED':
315
+ return 'passed';
316
+ case 'FAILED':
317
+ return 'failed';
318
+ case 'SKIPPED':
319
+ case 'PENDING':
320
+ case 'UNDEFINED':
321
+ case 'AMBIGUOUS':
322
+ return 'skipped';
323
+ default:
324
+ return 'unknown';
325
+ }
326
+ }
327
+ function keywordFromType(type) {
328
+ switch (type) {
329
+ case 'Context':
330
+ return 'Given';
331
+ case 'Action':
332
+ return 'When';
333
+ case 'Outcome':
334
+ return 'Then';
335
+ default:
336
+ return undefined;
337
+ }
338
+ }
339
+ function tsMs(ts) {
340
+ if (!ts)
341
+ return undefined;
342
+ return (ts.seconds ?? 0) * 1000 + Math.floor((ts.nanos ?? 0) / 1e6);
343
+ }
344
+ function durMs(d) {
345
+ if (!d)
346
+ return undefined;
347
+ return (d.seconds ?? 0) * 1000 + Math.floor((d.nanos ?? 0) / 1e6);
348
+ }
349
+ //# sourceMappingURL=summary.js.map
@@ -0,0 +1,154 @@
1
+ import type { GitHubIntegrationConfig, JiraIntegrationConfig, RunRecord, RunTotals, SuiteStatus } from '@sdods/contracts';
2
+ export type ProviderName = 'github' | 'jira' | (string & {});
3
+ export interface IssueRef {
4
+ provider: ProviderName;
5
+ key: string;
6
+ url: string;
7
+ status: 'open' | 'closed' | 'unknown';
8
+ }
9
+ export interface IssueLink {
10
+ id: string;
11
+ projectSlug: string;
12
+ provider: ProviderName;
13
+ fingerprint: string;
14
+ scenarioName: string;
15
+ externalKey: string;
16
+ externalUrl: string;
17
+ status: 'open' | 'closed' | 'unknown';
18
+ source: 'auto' | 'tag' | 'manual';
19
+ lastRunId?: string;
20
+ lastSyncedAt?: string;
21
+ closedAt?: string;
22
+ createdAt: string;
23
+ updatedAt: string;
24
+ }
25
+ /** Narrow persistence contract; the CLI uses a JSON file, the server can plug the DB. */
26
+ export interface IssueLinkStore {
27
+ findOpen(projectSlug: string, provider: ProviderName, fingerprint: string): Promise<IssueLink | undefined>;
28
+ findByKey(projectSlug: string, provider: ProviderName, externalKey: string): Promise<IssueLink | undefined>;
29
+ save(link: Omit<IssueLink, 'id' | 'createdAt' | 'updatedAt'> & Partial<Pick<IssueLink, 'id' | 'createdAt'>>): Promise<IssueLink>;
30
+ list(projectSlug: string, provider?: ProviderName): Promise<IssueLink[]>;
31
+ }
32
+ export interface StepSummary {
33
+ index: number;
34
+ keyword?: string;
35
+ text: string;
36
+ status: SuiteStatus;
37
+ errorMessage?: string;
38
+ durationMs?: number;
39
+ }
40
+ export interface ScreenshotRef {
41
+ /** path relative to the run directory */
42
+ relPath: string;
43
+ absPath?: string;
44
+ phase?: string;
45
+ stepIndex?: number | null;
46
+ name?: string;
47
+ }
48
+ export interface ScenarioSummary {
49
+ fingerprint: string;
50
+ featureUri: string;
51
+ featureName: string;
52
+ scenarioName: string;
53
+ line?: number;
54
+ exampleIndex?: number | null;
55
+ runnerProject: string;
56
+ layer: string;
57
+ browser?: string;
58
+ suiteTag?: string;
59
+ tags: string[];
60
+ status: SuiteStatus;
61
+ flaky: boolean;
62
+ attemptsCount: number;
63
+ durationMs?: number;
64
+ errorMessage?: string;
65
+ errorStack?: string;
66
+ steps: StepSummary[];
67
+ screenshots: ScreenshotRef[];
68
+ jiraKeys: string[];
69
+ githubIssues: string[];
70
+ tracePath?: string;
71
+ }
72
+ export interface RunSummaryInput {
73
+ run: RunRecord;
74
+ totals: RunTotals;
75
+ scenarios: ScenarioSummary[];
76
+ failed: ScenarioSummary[];
77
+ flaky: ScenarioSummary[];
78
+ passed: ScenarioSummary[];
79
+ runDir?: string;
80
+ reportUrl?: string;
81
+ }
82
+ export interface CiInfo {
83
+ provider?: 'github' | 'gitlab' | 'other';
84
+ isPullRequest: boolean;
85
+ prNumber?: number;
86
+ sha?: string;
87
+ branch?: string;
88
+ runUrl?: string;
89
+ artifactUrl?: string;
90
+ eventName?: string;
91
+ repository?: string;
92
+ }
93
+ export interface IntegrationLogger {
94
+ info(message: string, data?: unknown): void;
95
+ warn(message: string, data?: unknown): void;
96
+ error(message: string, data?: unknown): void;
97
+ debug(message: string, data?: unknown): void;
98
+ }
99
+ export interface IntegrationContext {
100
+ store: IssueLinkStore;
101
+ logger: IntegrationLogger;
102
+ publicUrl?: string;
103
+ ci: CiInfo;
104
+ dryRun?: boolean;
105
+ /** Resolve a screenshot to a URL viewers can open (public server first, then CI artifact page). */
106
+ artifactUrl(artifact: ScreenshotRef, run: RunRecord): string | null;
107
+ /** Resolve a screenshot to a local file for upload. */
108
+ artifactPath(artifact: ScreenshotRef, run: RunRecord): string | null;
109
+ }
110
+ export interface CreateIssueInput {
111
+ projectSlug: string;
112
+ run: RunRecord;
113
+ scenario: ScenarioSummary;
114
+ screenshots: ScreenshotRef[];
115
+ diffUrl?: string;
116
+ reportUrl?: string;
117
+ }
118
+ export interface NotifyAction {
119
+ provider: ProviderName;
120
+ kind: 'check-run' | 'pr-comment' | 'issue-created' | 'issue-commented' | 'issue-closed' | 'transition' | 'skipped';
121
+ target?: string;
122
+ url?: string;
123
+ detail?: string;
124
+ fingerprint?: string;
125
+ }
126
+ export interface NotifyResult {
127
+ provider: ProviderName;
128
+ actions: NotifyAction[];
129
+ }
130
+ export interface ProviderTestResult {
131
+ ok: boolean;
132
+ detail: string;
133
+ }
134
+ export interface IntegrationSecrets {
135
+ token?: string;
136
+ email?: string;
137
+ }
138
+ export interface IntegrationProvider<C = unknown> {
139
+ readonly name: ProviderName;
140
+ init(config: C, secrets: IntegrationSecrets): Promise<void>;
141
+ test(): Promise<ProviderTestResult>;
142
+ onRunFinished(summary: RunSummaryInput, ctx: IntegrationContext): Promise<NotifyResult>;
143
+ createIssue(input: CreateIssueInput, ctx: IntegrationContext): Promise<IssueRef>;
144
+ linkIssue(fingerprint: string, key: string): Promise<IssueRef>;
145
+ syncStatuses(links: IssueLink[]): Promise<IssueRef[]>;
146
+ onScenarioPassed?(link: IssueLink, run: RunRecord, ctx: IntegrationContext): Promise<NotifyAction | null>;
147
+ }
148
+ export type GitHubConfig = GitHubIntegrationConfig;
149
+ export type JiraConfig = JiraIntegrationConfig;
150
+ export interface CustomProviderModule {
151
+ default?: new (options?: Record<string, unknown>) => IntegrationProvider;
152
+ createProvider?: (options?: Record<string, unknown>) => IntegrationProvider;
153
+ }
154
+ //# sourceMappingURL=types.d.ts.map
package/dist/types.js ADDED
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=types.js.map
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@sdods/integrations",
3
+ "version": "0.2.0",
4
+ "description": "SDODS integrations: GitHub checks and issues, Jira issues and links.",
5
+ "license": "Apache-2.0",
6
+ "author": "SDODS <admin@sdods.com>",
7
+ "type": "module",
8
+ "exports": {
9
+ ".": "./dist/index.js"
10
+ },
11
+ "publishConfig": {
12
+ "access": "public"
13
+ },
14
+ "files": [
15
+ "dist"
16
+ ],
17
+ "engines": {
18
+ "node": ">=22"
19
+ },
20
+ "dependencies": {
21
+ "@sdods/contracts": "0.2.0",
22
+ "@octokit/rest": "^22.0.1",
23
+ "zod": "^4.5.4"
24
+ },
25
+ "homepage": "https://sdods.com",
26
+ "bugs": {
27
+ "url": "https://github.com/siri1410/SDODS/issues"
28
+ },
29
+ "repository": {
30
+ "type": "git",
31
+ "url": "git+https://github.com/siri1410/SDODS.git",
32
+ "directory": "packages/integrations"
33
+ },
34
+ "keywords": [
35
+ "sdods",
36
+ "automation",
37
+ "orchestration",
38
+ "testing",
39
+ "bdd",
40
+ "ai-agents",
41
+ "mcp"
42
+ ]
43
+ }