@sdods/integrations 0.7.2 → 0.8.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/.tsbuildinfo +1 -1
- package/dist/evidence.d.ts +150 -0
- package/dist/evidence.js +569 -0
- package/dist/gif.d.ts +36 -0
- package/dist/gif.js +85 -0
- package/dist/github.d.ts +28 -0
- package/dist/github.js +175 -13
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/registry.js +6 -2
- package/dist/secrets.d.ts +2 -0
- package/dist/secrets.js +3 -0
- package/dist/types.d.ts +34 -0
- package/package.json +2 -2
package/dist/github.d.ts
CHANGED
|
@@ -1,10 +1,20 @@
|
|
|
1
1
|
import { Octokit } from '@octokit/rest';
|
|
2
2
|
import type { RunRecord } from '@sdods/contracts';
|
|
3
|
+
import { type PruneResult } from './evidence.js';
|
|
4
|
+
import { type CommandRunner } from './gif.js';
|
|
3
5
|
import type { CreateIssueInput, GitHubConfig, IntegrationContext, IntegrationProvider, IntegrationSecrets, IssueLink, IssueRef, NotifyAction, NotifyResult, ProviderTestOptions, ProviderTestResult, RunSummaryInput } from './types.js';
|
|
4
6
|
export declare const PR_COMMENT_MARKER = "<!-- sdods:run-summary -->";
|
|
5
7
|
export interface GitHubProviderOptions {
|
|
6
8
|
baseUrl?: string;
|
|
7
9
|
octokit?: Octokit;
|
|
10
|
+
/** Octokit for the evidence repository (default: one built from `evidence.tokenEnv`, else `octokit`) */
|
|
11
|
+
evidenceOctokit?: Octokit;
|
|
12
|
+
/** web origin for evidence links (default `GITHUB_SERVER_URL`, then https://github.com) */
|
|
13
|
+
serverUrl?: string;
|
|
14
|
+
/** runs ffmpeg for GIF previews; injected by tests */
|
|
15
|
+
runCommand?: CommandRunner;
|
|
16
|
+
/** backoff between evidence ref-update attempts; injected by tests */
|
|
17
|
+
sleep?: (ms: number) => Promise<void>;
|
|
8
18
|
}
|
|
9
19
|
export declare class GitHubProvider implements IntegrationProvider<GitHubConfig> {
|
|
10
20
|
private readonly options;
|
|
@@ -13,15 +23,33 @@ export declare class GitHubProvider implements IntegrationProvider<GitHubConfig>
|
|
|
13
23
|
private octokit;
|
|
14
24
|
private owner;
|
|
15
25
|
private repo;
|
|
26
|
+
private evidence?;
|
|
27
|
+
private evidenceError?;
|
|
28
|
+
private gif?;
|
|
16
29
|
constructor(options?: GitHubProviderOptions);
|
|
17
30
|
init(config: GitHubConfig, secrets: IntegrationSecrets): Promise<void>;
|
|
31
|
+
/** `integrations.github.evidence.host: branch` — off unless configured. */
|
|
32
|
+
private initEvidence;
|
|
18
33
|
test(opts?: ProviderTestOptions): Promise<ProviderTestResult>;
|
|
34
|
+
/**
|
|
35
|
+
* Rewrite the evidence branch without the runs uploaded more than `olderThan` ago
|
|
36
|
+
* (default `evidence.retainDays`). Used by `sdods integrations evidence prune`.
|
|
37
|
+
*/
|
|
38
|
+
pruneEvidence(opts?: {
|
|
39
|
+
olderThan?: string;
|
|
40
|
+
dryRun?: boolean;
|
|
41
|
+
}): Promise<PruneResult>;
|
|
19
42
|
/**
|
|
20
43
|
* Issues are created with `labels`; a label the repository lacks is a configuration error that
|
|
21
44
|
* nothing reported. Missing labels fail the test, and `createMissingLabels` creates them.
|
|
22
45
|
*/
|
|
23
46
|
private checkLabels;
|
|
24
47
|
onRunFinished(summary: RunSummaryInput, ctx: IntegrationContext): Promise<NotifyResult>;
|
|
48
|
+
/**
|
|
49
|
+
* Commit the screenshots, video and GIF preview of the scenarios about to get an issue to the
|
|
50
|
+
* evidence branch. Any failure is a warning: the issues then use the links they had before.
|
|
51
|
+
*/
|
|
52
|
+
private publishEvidence;
|
|
25
53
|
/**
|
|
26
54
|
* Search the repository for an open issue whose body carries this scenario's fingerprint marker
|
|
27
55
|
* and record it as the scenario's link. A failed search falls back to the local links.
|
package/dist/github.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
-
import {
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, statSync } from 'node:fs';
|
|
2
|
+
import { tmpdir } from 'node:os';
|
|
3
|
+
import { basename, extname, join } from 'node:path';
|
|
3
4
|
import { Octokit } from '@octokit/rest';
|
|
4
5
|
import { TRACE_CREDENTIALS_WARNING, artifactDisplayPath, gherkinBlock, selectIssueScreenshots, truncate, } from './context.js';
|
|
5
6
|
import { IssueDedupe, fingerprintMarker, issueTitle } from './dedupe.js';
|
|
7
|
+
import { GitHubBranchEvidence, evidenceKey, formatBytes, parseAge, scenarioSlug, } from './evidence.js';
|
|
8
|
+
import { GifPreviewer } from './gif.js';
|
|
6
9
|
export const PR_COMMENT_MARKER = '<!-- sdods:run-summary -->';
|
|
7
10
|
const ANNOTATION_BATCH = 50;
|
|
8
11
|
/** colour for labels created by `sdods integrations test --create-labels` */
|
|
9
12
|
const LABEL_COLOR = 'd93f0b';
|
|
13
|
+
/** evidence key of a scenario's GIF preview */
|
|
14
|
+
const PREVIEW_REL = '#preview.gif';
|
|
10
15
|
export class GitHubProvider {
|
|
11
16
|
options;
|
|
12
17
|
name = 'github';
|
|
@@ -14,6 +19,9 @@ export class GitHubProvider {
|
|
|
14
19
|
octokit;
|
|
15
20
|
owner = '';
|
|
16
21
|
repo = '';
|
|
22
|
+
evidence;
|
|
23
|
+
evidenceError;
|
|
24
|
+
gif;
|
|
17
25
|
constructor(options = {}) {
|
|
18
26
|
this.options = options;
|
|
19
27
|
}
|
|
@@ -34,17 +42,76 @@ export class GitHubProvider {
|
|
|
34
42
|
baseUrl: this.options.baseUrl ?? process.env.GITHUB_API_URL,
|
|
35
43
|
userAgent: 'sdods',
|
|
36
44
|
});
|
|
45
|
+
this.initEvidence(config, secrets);
|
|
46
|
+
}
|
|
47
|
+
/** `integrations.github.evidence.host: branch` — off unless configured. */
|
|
48
|
+
initEvidence(config, secrets) {
|
|
49
|
+
const ev = config.evidence;
|
|
50
|
+
if (ev?.host !== 'branch')
|
|
51
|
+
return;
|
|
52
|
+
const [owner, repo] = ev.repo ? ev.repo.split('/') : [this.owner, this.repo];
|
|
53
|
+
let octokit = this.options.evidenceOctokit;
|
|
54
|
+
if (!octokit && ev.tokenEnv && !this.options.octokit) {
|
|
55
|
+
if (!secrets.evidenceToken) {
|
|
56
|
+
// issues are still created, with the links used without an evidence host
|
|
57
|
+
this.evidenceError = `evidence host needs a token in $${ev.tokenEnv}`;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
octokit = new Octokit({
|
|
61
|
+
auth: secrets.evidenceToken,
|
|
62
|
+
baseUrl: this.options.baseUrl ?? process.env.GITHUB_API_URL,
|
|
63
|
+
userAgent: 'sdods',
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
this.evidence = new GitHubBranchEvidence({
|
|
67
|
+
octokit: octokit ?? this.octokit,
|
|
68
|
+
owner: owner,
|
|
69
|
+
repo: repo,
|
|
70
|
+
branch: ev.branch,
|
|
71
|
+
serverUrl: this.options.serverUrl ?? process.env.GITHUB_SERVER_URL,
|
|
72
|
+
retainDays: ev.retainDays,
|
|
73
|
+
sleep: this.options.sleep,
|
|
74
|
+
});
|
|
75
|
+
if (ev.gifPreview)
|
|
76
|
+
this.gif = new GifPreviewer(this.options.runCommand);
|
|
37
77
|
}
|
|
38
78
|
async test(opts = {}) {
|
|
39
79
|
let detail;
|
|
80
|
+
let isPrivate;
|
|
40
81
|
try {
|
|
41
82
|
const { data } = await this.octokit.rest.repos.get({ owner: this.owner, repo: this.repo });
|
|
42
83
|
detail = `repo ${data.full_name} reachable (${data.private ? 'private' : 'public'})`;
|
|
84
|
+
isPrivate = data.private;
|
|
43
85
|
}
|
|
44
86
|
catch (e) {
|
|
45
87
|
return { ok: false, detail: e.message };
|
|
46
88
|
}
|
|
47
|
-
|
|
89
|
+
const result = await this.checkLabels(detail, opts);
|
|
90
|
+
if (this.config.evidence?.host !== 'branch')
|
|
91
|
+
return result;
|
|
92
|
+
const evidence = this.evidence
|
|
93
|
+
? await this.evidence.check(isPrivate)
|
|
94
|
+
: { ok: false, detail: this.evidenceError ?? 'evidence host not initialised' };
|
|
95
|
+
return {
|
|
96
|
+
...result,
|
|
97
|
+
ok: result.ok && evidence.ok,
|
|
98
|
+
detail: `${result.detail}; ${evidence.detail}`,
|
|
99
|
+
evidence,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Rewrite the evidence branch without the runs uploaded more than `olderThan` ago
|
|
104
|
+
* (default `evidence.retainDays`). Used by `sdods integrations evidence prune`.
|
|
105
|
+
*/
|
|
106
|
+
async pruneEvidence(opts = {}) {
|
|
107
|
+
if (this.config.evidence?.host !== 'branch')
|
|
108
|
+
throw new Error('integrations.github.evidence.host is not "branch" for this project.');
|
|
109
|
+
if (!this.evidence)
|
|
110
|
+
throw new Error(`GitHub ${this.evidenceError ?? 'evidence host not initialised'}.`);
|
|
111
|
+
const olderThanMs = opts.olderThan
|
|
112
|
+
? parseAge(opts.olderThan)
|
|
113
|
+
: this.config.evidence.retainDays * 86_400_000;
|
|
114
|
+
return this.evidence.prune({ olderThanMs, dryRun: opts.dryRun });
|
|
48
115
|
}
|
|
49
116
|
/**
|
|
50
117
|
* Issues are created with `labels`; a label the repository lacks is a configuration error that
|
|
@@ -115,8 +182,8 @@ export class GitHubProvider {
|
|
|
115
182
|
if (this.config.prComment && ctx.ci.isPullRequest && ctx.ci.prNumber) {
|
|
116
183
|
actions.push(await this.upsertPrComment(summary, ctx));
|
|
117
184
|
}
|
|
118
|
-
const
|
|
119
|
-
for (const scenario of
|
|
185
|
+
const decided = [];
|
|
186
|
+
for (const scenario of this.failuresToReport(summary)) {
|
|
120
187
|
// The local link file does not survive a fresh CI runner, so an open issue carrying the
|
|
121
188
|
// fingerprint marker is looked up in the repository before creating another one.
|
|
122
189
|
const dedupe = new IssueDedupe(ctx.store, {
|
|
@@ -124,7 +191,15 @@ export class GitHubProvider {
|
|
|
124
191
|
? undefined
|
|
125
192
|
: () => this.findOpenIssueByFingerprint(scenario, run, ctx),
|
|
126
193
|
});
|
|
127
|
-
|
|
194
|
+
decided.push({
|
|
195
|
+
scenario,
|
|
196
|
+
decision: await dedupe.decide(run.projectSlug, this.name, scenario.fingerprint),
|
|
197
|
+
});
|
|
198
|
+
}
|
|
199
|
+
// every new issue's evidence goes up in one commit, before the issues are created
|
|
200
|
+
const toCreate = decided.filter((d) => d.decision.action === 'create').map((d) => d.scenario);
|
|
201
|
+
const evidence = ctx.dryRun || !toCreate.length ? undefined : await this.publishEvidence(toCreate, run, ctx);
|
|
202
|
+
for (const { scenario, decision } of decided) {
|
|
128
203
|
if (decision.action === 'create') {
|
|
129
204
|
if (ctx.dryRun) {
|
|
130
205
|
actions.push({
|
|
@@ -141,6 +216,7 @@ export class GitHubProvider {
|
|
|
141
216
|
scenario,
|
|
142
217
|
screenshots: selectIssueScreenshots(scenario),
|
|
143
218
|
reportUrl: summary.reportUrl,
|
|
219
|
+
evidence,
|
|
144
220
|
}, ctx);
|
|
145
221
|
actions.push({
|
|
146
222
|
provider: this.name,
|
|
@@ -209,6 +285,74 @@ export class GitHubProvider {
|
|
|
209
285
|
}
|
|
210
286
|
return { provider: this.name, actions };
|
|
211
287
|
}
|
|
288
|
+
/**
|
|
289
|
+
* Commit the screenshots, video and GIF preview of the scenarios about to get an issue to the
|
|
290
|
+
* evidence branch. Any failure is a warning: the issues then use the links they had before.
|
|
291
|
+
*/
|
|
292
|
+
async publishEvidence(scenarios, run, ctx) {
|
|
293
|
+
if (this.evidenceError) {
|
|
294
|
+
ctx.logger.warn(`github: ${this.evidenceError}; issues link evidence without it`);
|
|
295
|
+
return undefined;
|
|
296
|
+
}
|
|
297
|
+
const host = this.evidence;
|
|
298
|
+
const ev = this.config.evidence;
|
|
299
|
+
if (!host || !ev)
|
|
300
|
+
return undefined;
|
|
301
|
+
host.logger = ctx.logger;
|
|
302
|
+
const files = [];
|
|
303
|
+
let tmp;
|
|
304
|
+
const add = (scenario, key, localPath, kind, taken, wanted = basename(localPath)) => {
|
|
305
|
+
let name = wanted;
|
|
306
|
+
for (let i = 2; taken.has(name); i++)
|
|
307
|
+
name = `${i}-${wanted}`;
|
|
308
|
+
taken.add(name);
|
|
309
|
+
files.push({
|
|
310
|
+
key: evidenceKey(scenario.fingerprint, key),
|
|
311
|
+
fingerprint: scenario.fingerprint,
|
|
312
|
+
scenarioDir: scenarioSlug(scenario.scenarioName, scenario.fingerprint),
|
|
313
|
+
name,
|
|
314
|
+
localPath,
|
|
315
|
+
kind,
|
|
316
|
+
bytes: statSync(localPath).size,
|
|
317
|
+
});
|
|
318
|
+
};
|
|
319
|
+
try {
|
|
320
|
+
for (const scenario of scenarios) {
|
|
321
|
+
const taken = new Set();
|
|
322
|
+
for (const shot of selectIssueScreenshots(scenario)) {
|
|
323
|
+
const local = ctx.artifactPath(shot, run);
|
|
324
|
+
if (local)
|
|
325
|
+
add(scenario, shot.relPath, local, 'screenshot', taken);
|
|
326
|
+
}
|
|
327
|
+
// the trace is never offered: it carries credentials (#101)
|
|
328
|
+
const video = scenario.videoPath
|
|
329
|
+
? ctx.artifactPath({ relPath: scenario.videoPath }, run)
|
|
330
|
+
: null;
|
|
331
|
+
if (!video)
|
|
332
|
+
continue;
|
|
333
|
+
if (this.gif) {
|
|
334
|
+
tmp ??= mkdtempSync(join(tmpdir(), 'sdods-gif-'));
|
|
335
|
+
const gif = await this.gif.make(video, join(tmp, `${files.length}.gif`), ctx.logger);
|
|
336
|
+
if (gif)
|
|
337
|
+
add(scenario, PREVIEW_REL, gif, 'preview', taken, 'preview.gif');
|
|
338
|
+
}
|
|
339
|
+
add(scenario, scenario.videoPath, video, 'video', taken);
|
|
340
|
+
}
|
|
341
|
+
if (!files.length)
|
|
342
|
+
return undefined;
|
|
343
|
+
const published = await host.publishRun({ id: run.id, projectSlug: run.projectSlug }, files, ev);
|
|
344
|
+
ctx.logger.info(`github evidence: ${published.urls.size} file(s) on ${host.fullName}@${host.branch}${published.skipped.length ? `, ${published.skipped.length} skipped by the size caps` : ''}`);
|
|
345
|
+
return published;
|
|
346
|
+
}
|
|
347
|
+
catch (e) {
|
|
348
|
+
ctx.logger.warn(`github: evidence upload to ${host.fullName}@${host.branch} failed, issues link evidence without it: ${e.message}`);
|
|
349
|
+
return undefined;
|
|
350
|
+
}
|
|
351
|
+
finally {
|
|
352
|
+
if (tmp)
|
|
353
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
354
|
+
}
|
|
355
|
+
}
|
|
212
356
|
/**
|
|
213
357
|
* Search the repository for an open issue whose body carries this scenario's fingerprint marker
|
|
214
358
|
* and record it as the scenario's link. A failed search falls back to the local links.
|
|
@@ -420,15 +564,17 @@ export class GitHubProvider {
|
|
|
420
564
|
const { scenario, run } = input;
|
|
421
565
|
const title = issueTitle(scenario.featureName, scenario.scenarioName, scenario.browser);
|
|
422
566
|
const shotLines = [];
|
|
567
|
+
const hosted = (relPath) => input.evidence?.urls.get(evidenceKey(scenario.fingerprint, relPath));
|
|
423
568
|
for (const shot of input.screenshots) {
|
|
424
|
-
|
|
425
|
-
|
|
569
|
+
// precedence: evidence branch, then release asset, then SDODS_PUBLIC_URL, then CI artifacts
|
|
570
|
+
let url = hosted(shot.relPath) ?? ctx.artifactUrl(shot, run);
|
|
571
|
+
if (this.config.uploadToRelease && !hosted(shot.relPath)) {
|
|
426
572
|
const uploaded = await this.uploadReleaseAsset(shot, run, ctx).catch(() => null);
|
|
427
573
|
if (uploaded)
|
|
428
574
|
url = uploaded;
|
|
429
575
|
}
|
|
430
576
|
if (url)
|
|
431
|
-
shotLines.push(`- ${shot.phase ?? 'screenshot'}${shot.stepIndex != null ? ` (step ${shot.stepIndex})` : ''}: ${
|
|
577
|
+
shotLines.push(`- ${shot.phase ?? 'screenshot'}${shot.stepIndex != null ? ` (step ${shot.stepIndex})` : ''}: ${/\.png(\?raw=true)?$/.test(url) ? `` : url}`);
|
|
432
578
|
}
|
|
433
579
|
const body = [
|
|
434
580
|
`**Project** \`${run.projectSlug}\` · **Environment** \`${run.env}\` · **Browser** \`${scenario.browser ?? scenario.layer}\` · **Run** \`${run.id}\``,
|
|
@@ -447,7 +593,8 @@ export class GitHubProvider {
|
|
|
447
593
|
'```',
|
|
448
594
|
'',
|
|
449
595
|
...(shotLines.length ? ['### Screenshots', ...shotLines, ''] : []),
|
|
450
|
-
...(await this.mediaLines(scenario, run, ctx)),
|
|
596
|
+
...(await this.mediaLines(scenario, run, ctx, input.evidence)),
|
|
597
|
+
...skippedEvidenceLines(scenario, input.evidence),
|
|
451
598
|
...(input.reportUrl || ctx.ci.runUrl
|
|
452
599
|
? [
|
|
453
600
|
'### Links',
|
|
@@ -502,13 +649,17 @@ export class GitHubProvider {
|
|
|
502
649
|
* render masked, but anything the application prints on screen (an OTP, a token in a URL bar
|
|
503
650
|
* shown in a page) is visible in it, so projects with such screens should set `evidence.video: off`.
|
|
504
651
|
*/
|
|
505
|
-
async mediaLines(scenario, run, ctx) {
|
|
652
|
+
async mediaLines(scenario, run, ctx, evidence) {
|
|
506
653
|
const lines = [];
|
|
654
|
+
const preview = evidence?.urls.get(evidenceKey(scenario.fingerprint, PREVIEW_REL));
|
|
655
|
+
if (preview)
|
|
656
|
+
lines.push(`- Preview (last ${this.gif?.seconds ?? 5} s): `);
|
|
507
657
|
if (scenario.videoPath) {
|
|
508
658
|
const relPath = scenario.videoPath;
|
|
509
659
|
const local = artifactDisplayPath(relPath, run);
|
|
510
|
-
|
|
511
|
-
|
|
660
|
+
const hostedVideo = evidence?.urls.get(evidenceKey(scenario.fingerprint, relPath));
|
|
661
|
+
let url = hostedVideo ?? ctx.artifactUrl({ relPath }, run);
|
|
662
|
+
if (this.config.uploadToRelease && !hostedVideo) {
|
|
512
663
|
const uploaded = await this.uploadReleaseAsset({ relPath }, run, ctx).catch(() => null);
|
|
513
664
|
if (uploaded)
|
|
514
665
|
url = uploaded;
|
|
@@ -627,6 +778,17 @@ export class GitHubProvider {
|
|
|
627
778
|
};
|
|
628
779
|
}
|
|
629
780
|
}
|
|
781
|
+
/** Files the evidence host did not take, so a reader knows why they are only linked. */
|
|
782
|
+
function skippedEvidenceLines(scenario, evidence) {
|
|
783
|
+
const mine = evidence?.skipped.filter((s) => s.fingerprint === scenario.fingerprint) ?? [];
|
|
784
|
+
if (!mine.length)
|
|
785
|
+
return [];
|
|
786
|
+
return [
|
|
787
|
+
'### Evidence not uploaded',
|
|
788
|
+
...mine.map((s) => `- \`${s.name}\` (${formatBytes(s.bytes)}): ${s.reason}`),
|
|
789
|
+
'',
|
|
790
|
+
];
|
|
791
|
+
}
|
|
630
792
|
export function issueNumber(key) {
|
|
631
793
|
const m = /#?(\d+)$/.exec(key.trim());
|
|
632
794
|
if (!m)
|
package/dist/index.d.ts
CHANGED
|
@@ -4,6 +4,8 @@ export * from './dedupe.js';
|
|
|
4
4
|
export * from './secrets.js';
|
|
5
5
|
export * from './context.js';
|
|
6
6
|
export * from './github.js';
|
|
7
|
+
export * from './evidence.js';
|
|
8
|
+
export * from './gif.js';
|
|
7
9
|
export * from './jira.js';
|
|
8
10
|
export * from './registry.js';
|
|
9
11
|
export * from './summary.js';
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,8 @@ export * from './dedupe.js';
|
|
|
4
4
|
export * from './secrets.js';
|
|
5
5
|
export * from './context.js';
|
|
6
6
|
export * from './github.js';
|
|
7
|
+
export * from './evidence.js';
|
|
8
|
+
export * from './gif.js';
|
|
7
9
|
export * from './jira.js';
|
|
8
10
|
export * from './registry.js';
|
|
9
11
|
export * from './summary.js';
|
package/dist/registry.js
CHANGED
|
@@ -11,15 +11,19 @@ export async function getProviders(project, opts = {}) {
|
|
|
11
11
|
const gh = project.integrations.github;
|
|
12
12
|
if (gh && want('github')) {
|
|
13
13
|
const provider = new GitHubProvider();
|
|
14
|
+
const names = {
|
|
15
|
+
tokenEnv: gh.tokenEnv,
|
|
16
|
+
evidenceTokenEnv: gh.evidence?.host === 'branch' ? gh.evidence.tokenEnv : undefined,
|
|
17
|
+
};
|
|
14
18
|
const entry = {
|
|
15
19
|
name: 'github',
|
|
16
20
|
provider,
|
|
17
21
|
enabled: gh.enabled,
|
|
18
|
-
secrets: secretPresence(
|
|
22
|
+
secrets: secretPresence(names, env),
|
|
19
23
|
};
|
|
20
24
|
if (gh.enabled && opts.init !== false) {
|
|
21
25
|
try {
|
|
22
|
-
await provider.init(gh, readSecrets(
|
|
26
|
+
await provider.init(gh, readSecrets(names, env));
|
|
23
27
|
}
|
|
24
28
|
catch (e) {
|
|
25
29
|
entry.initError = e.message;
|
package/dist/secrets.d.ts
CHANGED
|
@@ -3,10 +3,12 @@ import type { CiInfo, IntegrationSecrets } from './types.js';
|
|
|
3
3
|
export declare function readSecrets(names: {
|
|
4
4
|
tokenEnv?: string;
|
|
5
5
|
emailEnv?: string;
|
|
6
|
+
evidenceTokenEnv?: string;
|
|
6
7
|
}, env?: NodeJS.ProcessEnv): IntegrationSecrets;
|
|
7
8
|
export declare function secretPresence(names: {
|
|
8
9
|
tokenEnv?: string;
|
|
9
10
|
emailEnv?: string;
|
|
11
|
+
evidenceTokenEnv?: string;
|
|
10
12
|
}, env?: NodeJS.ProcessEnv): Record<string, boolean>;
|
|
11
13
|
/** Detect CI context (GitHub Actions and GitLab CI; everything else is "other"). */
|
|
12
14
|
export declare function detectCi(env?: NodeJS.ProcessEnv): CiInfo;
|
package/dist/secrets.js
CHANGED
|
@@ -3,6 +3,7 @@ export function readSecrets(names, env = process.env) {
|
|
|
3
3
|
return {
|
|
4
4
|
token: names.tokenEnv ? env[names.tokenEnv] : undefined,
|
|
5
5
|
email: names.emailEnv ? env[names.emailEnv] : undefined,
|
|
6
|
+
...(names.evidenceTokenEnv ? { evidenceToken: env[names.evidenceTokenEnv] } : {}),
|
|
6
7
|
};
|
|
7
8
|
}
|
|
8
9
|
export function secretPresence(names, env = process.env) {
|
|
@@ -11,6 +12,8 @@ export function secretPresence(names, env = process.env) {
|
|
|
11
12
|
out[names.tokenEnv] = Boolean(env[names.tokenEnv]);
|
|
12
13
|
if (names.emailEnv)
|
|
13
14
|
out[names.emailEnv] = Boolean(env[names.emailEnv]);
|
|
15
|
+
if (names.evidenceTokenEnv)
|
|
16
|
+
out[names.evidenceTokenEnv] = Boolean(env[names.evidenceTokenEnv]);
|
|
14
17
|
return out;
|
|
15
18
|
}
|
|
16
19
|
/** Detect CI context (GitHub Actions and GitLab CI; everything else is "other"). */
|
package/dist/types.d.ts
CHANGED
|
@@ -117,6 +117,33 @@ export interface CreateIssueInput {
|
|
|
117
117
|
screenshots: ScreenshotRef[];
|
|
118
118
|
diffUrl?: string;
|
|
119
119
|
reportUrl?: string;
|
|
120
|
+
/** files already committed to the evidence branch for this run (`integrations.github.evidence`) */
|
|
121
|
+
evidence?: PublishedEvidence;
|
|
122
|
+
}
|
|
123
|
+
/** One local file offered to an evidence host. */
|
|
124
|
+
export interface EvidenceFile {
|
|
125
|
+
/** lookup key for the issue body: `evidenceKey(fingerprint, relPath)` */
|
|
126
|
+
key: string;
|
|
127
|
+
fingerprint: string;
|
|
128
|
+
/** directory under `runs/<runId>/` */
|
|
129
|
+
scenarioDir: string;
|
|
130
|
+
name: string;
|
|
131
|
+
localPath: string;
|
|
132
|
+
kind: 'screenshot' | 'preview' | 'video';
|
|
133
|
+
bytes: number;
|
|
134
|
+
}
|
|
135
|
+
export interface SkippedEvidence {
|
|
136
|
+
key: string;
|
|
137
|
+
fingerprint: string;
|
|
138
|
+
name: string;
|
|
139
|
+
bytes: number;
|
|
140
|
+
reason: string;
|
|
141
|
+
}
|
|
142
|
+
export interface PublishedEvidence {
|
|
143
|
+
/** evidence key → link that renders inline for readers of the evidence repository */
|
|
144
|
+
urls: Map<string, string>;
|
|
145
|
+
skipped: SkippedEvidence[];
|
|
146
|
+
commitSha?: string;
|
|
120
147
|
}
|
|
121
148
|
export interface NotifyAction {
|
|
122
149
|
provider: ProviderName;
|
|
@@ -138,6 +165,11 @@ export interface ProviderTestResult {
|
|
|
138
165
|
missing: string[];
|
|
139
166
|
created: string[];
|
|
140
167
|
};
|
|
168
|
+
/** the evidence host, when `integrations.github.evidence.host` is `branch` */
|
|
169
|
+
evidence?: {
|
|
170
|
+
ok: boolean;
|
|
171
|
+
detail: string;
|
|
172
|
+
};
|
|
141
173
|
}
|
|
142
174
|
export interface ProviderTestOptions {
|
|
143
175
|
/** create configured labels the target is missing (GitHub) */
|
|
@@ -146,6 +178,8 @@ export interface ProviderTestOptions {
|
|
|
146
178
|
export interface IntegrationSecrets {
|
|
147
179
|
token?: string;
|
|
148
180
|
email?: string;
|
|
181
|
+
/** token for `integrations.github.evidence.tokenEnv`, when set */
|
|
182
|
+
evidenceToken?: string;
|
|
149
183
|
}
|
|
150
184
|
export interface IntegrationProvider<C = unknown> {
|
|
151
185
|
readonly name: ProviderName;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sdods/integrations",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "SDODS integrations: GitHub checks and issues, Jira issues and links.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"author": "SDODS <admin@sdods.com>",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"node": ">=22"
|
|
19
19
|
},
|
|
20
20
|
"dependencies": {
|
|
21
|
-
"@sdods/contracts": "0.
|
|
21
|
+
"@sdods/contracts": "0.8.0",
|
|
22
22
|
"@octokit/rest": "^22.0.1",
|
|
23
23
|
"zod": "^4.5.4"
|
|
24
24
|
},
|