@anselmdk/feature-spec-md 0.5.0 → 0.6.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/README.md +59 -78
- package/SPEC_FORMAT.md +38 -78
- package/dist/cli.js +194 -86
- package/dist/cli.js.map +1 -1
- package/dist/featureSpecs.d.ts +1 -1
- package/dist/featureSpecs.d.ts.map +1 -1
- package/dist/featureSpecs.js +57 -10
- package/dist/featureSpecs.js.map +1 -1
- package/dist/githubActionDiffReport.d.ts +61 -0
- package/dist/githubActionDiffReport.d.ts.map +1 -1
- package/dist/githubActionDiffReport.js +343 -119
- package/dist/githubActionDiffReport.js.map +1 -1
- package/dist/index.d.ts +8 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -1
- package/dist/index.js.map +1 -1
- package/dist/mockReports.d.ts +36 -0
- package/dist/mockReports.d.ts.map +1 -0
- package/dist/mockReports.js +250 -0
- package/dist/mockReports.js.map +1 -0
- package/dist/mocks/current/app/supportDesk.d.ts +12 -0
- package/dist/mocks/current/app/supportDesk.d.ts.map +1 -0
- package/dist/mocks/current/app/supportDesk.js +19 -0
- package/dist/mocks/current/app/supportDesk.js.map +1 -0
- package/dist/mocks/current/tests/supportDesk.spec.d.ts +3 -0
- package/dist/mocks/current/tests/supportDesk.spec.d.ts.map +1 -0
- package/dist/mocks/current/tests/supportDesk.spec.js +15 -0
- package/dist/mocks/current/tests/supportDesk.spec.js.map +1 -0
- package/dist/mocks/previous/app/supportDesk.d.ts +10 -0
- package/dist/mocks/previous/app/supportDesk.d.ts.map +1 -0
- package/dist/mocks/previous/app/supportDesk.js +9 -0
- package/dist/mocks/previous/app/supportDesk.js.map +1 -0
- package/dist/mocks/previous/tests/supportDesk.spec.d.ts +3 -0
- package/dist/mocks/previous/tests/supportDesk.spec.d.ts.map +1 -0
- package/dist/mocks/previous/tests/supportDesk.spec.js +15 -0
- package/dist/mocks/previous/tests/supportDesk.spec.js.map +1 -0
- package/dist/reportMetadata.d.ts +9 -0
- package/dist/reportMetadata.d.ts.map +1 -0
- package/dist/reportMetadata.js +28 -0
- package/dist/reportMetadata.js.map +1 -0
- package/dist/screenshots.d.ts +3 -1
- package/dist/screenshots.d.ts.map +1 -1
- package/dist/screenshots.js +23 -0
- package/dist/screenshots.js.map +1 -1
- package/dist/types.d.ts +11 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/evidence-policy.md +63 -0
- package/docs/report-development.md +102 -0
- package/docs/spec-driven-flow.md +67 -8
- package/package.json +3 -1
- package/src/mocks/current/app/supportDesk.ts +27 -0
- package/src/mocks/current/screenshots/SUPPORT-INBOX-S001-line-27-current.svg +10 -0
- package/src/mocks/current/screenshots/SUPPORT-REPLY-S001-line-27-current.svg +10 -0
- package/src/mocks/current/screenshots/screenshots.json +18 -0
- package/src/mocks/current/specs/support-desk.model.md +29 -0
- package/src/mocks/current/specs/ticket-inbox.feature.md +27 -0
- package/src/mocks/current/specs/ticket-reply.feature.md +33 -0
- package/src/mocks/current/tests/supportDesk.spec.ts +17 -0
- package/src/mocks/previous/app/supportDesk.ts +17 -0
- package/src/mocks/previous/screenshots/SUPPORT-INBOX-S001-line-27-previous.svg +10 -0
- package/src/mocks/previous/screenshots/screenshots.json +11 -0
- package/src/mocks/previous/specs/support-desk.model.md +29 -0
- package/src/mocks/previous/specs/ticket-inbox.feature.md +26 -0
- package/src/mocks/previous/specs/ticket-reply.feature.md +26 -0
- package/src/mocks/previous/tests/supportDesk.spec.ts +17 -0
|
@@ -1,4 +1,65 @@
|
|
|
1
1
|
import { type GithubActionOptions } from "./githubActionFtp.js";
|
|
2
2
|
export type GithubActionDiffReportOptions = GithubActionOptions;
|
|
3
|
+
export type LocalDiffReportOptions = {
|
|
4
|
+
previousDir: string;
|
|
5
|
+
currentDir: string;
|
|
6
|
+
prNumber?: string;
|
|
7
|
+
baseBuild?: string;
|
|
8
|
+
currentBuild?: string;
|
|
9
|
+
baseBuildUrl?: string;
|
|
10
|
+
currentBuildUrl?: string;
|
|
11
|
+
baseLabel?: DiffReport["baseLabel"];
|
|
12
|
+
previousAssetUrlPrefix?: string;
|
|
13
|
+
currentAssetUrlPrefix?: string;
|
|
14
|
+
};
|
|
15
|
+
type ComparedFile = {
|
|
16
|
+
path: string;
|
|
17
|
+
kind: "report" | "screenshot" | "asset";
|
|
18
|
+
status: "added" | "removed" | "changed" | "unchanged";
|
|
19
|
+
previousHash?: string;
|
|
20
|
+
currentHash?: string;
|
|
21
|
+
previousSize?: number;
|
|
22
|
+
currentSize?: number;
|
|
23
|
+
};
|
|
24
|
+
type DiffLine = {
|
|
25
|
+
kind: "context" | "added" | "removed";
|
|
26
|
+
previousLine?: number;
|
|
27
|
+
currentLine?: number;
|
|
28
|
+
text: string;
|
|
29
|
+
};
|
|
30
|
+
type SpecDiff = {
|
|
31
|
+
key: string;
|
|
32
|
+
title: string;
|
|
33
|
+
filePath?: string;
|
|
34
|
+
status: "added" | "removed" | "changed";
|
|
35
|
+
lines: DiffLine[];
|
|
36
|
+
};
|
|
37
|
+
type ScreenshotDiffItem = {
|
|
38
|
+
path: string;
|
|
39
|
+
title: string;
|
|
40
|
+
status: ComparedFile["status"];
|
|
41
|
+
previousUrl?: string;
|
|
42
|
+
currentUrl?: string;
|
|
43
|
+
previousSize?: number;
|
|
44
|
+
currentSize?: number;
|
|
45
|
+
};
|
|
46
|
+
type ScreenshotDiffGroup = {
|
|
47
|
+
specLabel: string;
|
|
48
|
+
specPath?: string;
|
|
49
|
+
items: ScreenshotDiffItem[];
|
|
50
|
+
};
|
|
51
|
+
type DiffReport = {
|
|
52
|
+
prNumber: string;
|
|
53
|
+
baseBuild?: string;
|
|
54
|
+
currentBuild: string;
|
|
55
|
+
baseBuildUrl?: string;
|
|
56
|
+
currentBuildUrl: string;
|
|
57
|
+
baseLabel: "main" | "previous" | "none";
|
|
58
|
+
files: ComparedFile[];
|
|
59
|
+
specDiffs: SpecDiff[];
|
|
60
|
+
screenshotDiffs: ScreenshotDiffGroup[];
|
|
61
|
+
};
|
|
3
62
|
export declare function publishGithubActionDiffReport(options: GithubActionDiffReportOptions): Promise<void>;
|
|
63
|
+
export declare function renderLocalDiffReport(options: LocalDiffReportOptions): Promise<string>;
|
|
64
|
+
export {};
|
|
4
65
|
//# sourceMappingURL=githubActionDiffReport.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"githubActionDiffReport.d.ts","sourceRoot":"","sources":["../src/githubActionDiffReport.ts"],"names":[],"mappings":"AAKA,OAAO,EAQL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAG9B,MAAM,MAAM,6BAA6B,GAAG,mBAAmB,CAAC;
|
|
1
|
+
{"version":3,"file":"githubActionDiffReport.d.ts","sourceRoot":"","sources":["../src/githubActionDiffReport.ts"],"names":[],"mappings":"AAKA,OAAO,EAQL,KAAK,mBAAmB,EACzB,MAAM,sBAAsB,CAAC;AAG9B,MAAM,MAAM,6BAA6B,GAAG,mBAAmB,CAAC;AAEhE,MAAM,MAAM,sBAAsB,GAAG;IACnC,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,SAAS,CAAC,EAAE,UAAU,CAAC,WAAW,CAAC,CAAC;IACpC,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,qBAAqB,CAAC,EAAE,MAAM,CAAC;CAChC,CAAC;AAEF,KAAK,YAAY,GAAG;IAClB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,QAAQ,GAAG,YAAY,GAAG,OAAO,CAAC;IACxC,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,GAAG,WAAW,CAAC;IACtD,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,QAAQ,GAAG;IACd,IAAI,EAAE,SAAS,GAAG,OAAO,GAAG,SAAS,CAAC;IACtC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;CACd,CAAC;AAUF,KAAK,QAAQ,GAAG;IACd,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,GAAG,SAAS,GAAG,SAAS,CAAC;IACxC,KAAK,EAAE,QAAQ,EAAE,CAAC;CACnB,CAAC;AAEF,KAAK,kBAAkB,GAAG;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;IAC/B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAEF,KAAK,mBAAmB,GAAG;IACzB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B,CAAC;AAEF,KAAK,UAAU,GAAG;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,YAAY,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,EAAE,MAAM,CAAC;IACxB,SAAS,EAAE,MAAM,GAAG,UAAU,GAAG,MAAM,CAAC;IACxC,KAAK,EAAE,YAAY,EAAE,CAAC;IACtB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtB,eAAe,EAAE,mBAAmB,EAAE,CAAC;CACxC,CAAC;AAEF,wBAAsB,6BAA6B,CACjD,OAAO,EAAE,6BAA6B,iBAyCvC;AAED,wBAAsB,qBAAqB,CAAC,OAAO,EAAE,sBAAsB,mBAE1E"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
2
|
+
import { mkdir, readFile, readdir, writeFile } from "node:fs/promises";
|
|
3
3
|
import { basename, join, relative } from "node:path";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { html } from "./html.js";
|
|
@@ -8,29 +8,18 @@ import { writeGithubOutput, writeGithubSummary } from "./githubActionOutput.js";
|
|
|
8
8
|
export async function publishGithubActionDiffReport(options) {
|
|
9
9
|
const config = ftpConfig(options);
|
|
10
10
|
const prNumber = config.prNumber;
|
|
11
|
-
if (!prNumber)
|
|
11
|
+
if (!prNumber)
|
|
12
12
|
throw new Error("Missing required option --pr-number or FEATURE_SPEC_PR_NUMBER.");
|
|
13
|
-
}
|
|
14
13
|
const buildsRoot = pathJoin(config.remoteDir, "build");
|
|
15
14
|
const builds = await listBuildNumbers(buildsRoot, config);
|
|
16
15
|
const currentBuild = config.buildNumber;
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
: {
|
|
25
|
-
prNumber,
|
|
26
|
-
currentBuild,
|
|
27
|
-
currentBuildUrl,
|
|
28
|
-
files: [],
|
|
29
|
-
};
|
|
30
|
-
if (previousBuild) {
|
|
31
|
-
report.previousBuild = previousBuild;
|
|
32
|
-
report.previousBuildUrl = previousBuildUrl;
|
|
33
|
-
}
|
|
16
|
+
const requestedBaseBuild = optionValue(options, "base-build-number", "FEATURE_SPEC_BASE_BUILD_NUMBER");
|
|
17
|
+
const hasRequestedBaseBuild = Boolean(requestedBaseBuild && builds.includes(requestedBaseBuild));
|
|
18
|
+
const baseBuild = hasRequestedBaseBuild ? requestedBaseBuild : previousBuildNumber(builds, currentBuild);
|
|
19
|
+
const baseLabel = hasRequestedBaseBuild ? "main" : baseBuild ? "previous" : "none";
|
|
20
|
+
const report = baseBuild
|
|
21
|
+
? await compareBuilds(config, baseBuild, currentBuild, prNumber, baseLabel)
|
|
22
|
+
: emptyReport(config, prNumber, currentBuild);
|
|
34
23
|
const localReportDir = join(tmpdir(), `feature-spec-md-pr-diff-${process.pid}-${prNumber}-${currentBuild}`);
|
|
35
24
|
await mkdir(localReportDir, { recursive: true });
|
|
36
25
|
await writeFile(join(localReportDir, "index.html"), renderDiffReport(report), "utf8");
|
|
@@ -41,145 +30,368 @@ export async function publishGithubActionDiffReport(options) {
|
|
|
41
30
|
"## Feature Spec PR Diff",
|
|
42
31
|
"",
|
|
43
32
|
`<p><strong>Diff report:</strong> <a href=\"${html(reportUrl)}\">PR #${html(prNumber)} build ${html(currentBuild)}</a></p>`,
|
|
44
|
-
report
|
|
45
|
-
? `<p>Compared build <strong>${html(report.previousBuild)}</strong> with build <strong>${html(currentBuild)}</strong>.</p>`
|
|
46
|
-
: `<p>No earlier build was found, so this report establishes the first comparison baseline.</p>`,
|
|
33
|
+
summaryComparisonSentence(report),
|
|
47
34
|
"",
|
|
48
35
|
].join("\n"));
|
|
49
36
|
await writeGithubOutput({
|
|
50
37
|
"diff-report-url": reportUrl,
|
|
51
38
|
"diff-comment-body": commentBody(report, reportUrl),
|
|
52
|
-
"
|
|
39
|
+
"base-build": report.baseLabel === "main" ? report.baseBuild ?? "" : "",
|
|
40
|
+
"previous-build": report.baseLabel === "previous" ? report.baseBuild ?? "" : "",
|
|
53
41
|
"current-build": currentBuild,
|
|
54
42
|
});
|
|
55
43
|
console.log(`Feature spec PR diff report uploaded to ${reportUrl}`);
|
|
56
44
|
}
|
|
57
|
-
async function
|
|
58
|
-
|
|
45
|
+
export async function renderLocalDiffReport(options) {
|
|
46
|
+
return renderDiffReport(await compareLocalBuilds(options));
|
|
47
|
+
}
|
|
48
|
+
async function compareLocalBuilds(options) {
|
|
49
|
+
const baseBuild = options.baseBuild ?? "127";
|
|
50
|
+
const currentBuild = options.currentBuild ?? "128";
|
|
51
|
+
const files = await compareLocalFiles(options.previousDir, options.currentDir);
|
|
52
|
+
const previousIndex = await readFileMaybe(join(options.previousDir, "index.html"));
|
|
53
|
+
const currentIndex = await readFileMaybe(join(options.currentDir, "index.html"));
|
|
54
|
+
const previousSpecs = extractSpecSections(previousIndex ?? "");
|
|
55
|
+
const currentSpecs = extractSpecSections(currentIndex ?? "");
|
|
56
|
+
const specDiffs = compareSpecSections(previousSpecs, currentSpecs);
|
|
57
|
+
const scenarioToSpec = scenarioSpecMap([...previousSpecs, ...currentSpecs]);
|
|
58
|
+
const screenshotDiffs = groupScreenshotDiffs(files, scenarioToSpec, {
|
|
59
|
+
previousUrl: (filePath) => relativeAssetUrl(options.previousAssetUrlPrefix ?? "previous", filePath),
|
|
60
|
+
currentUrl: (filePath) => relativeAssetUrl(options.currentAssetUrlPrefix ?? "current", filePath),
|
|
61
|
+
});
|
|
62
|
+
return {
|
|
63
|
+
prNumber: options.prNumber ?? "42",
|
|
64
|
+
baseBuild,
|
|
65
|
+
currentBuild,
|
|
66
|
+
baseBuildUrl: options.baseBuildUrl ?? "previous/",
|
|
67
|
+
currentBuildUrl: options.currentBuildUrl ?? "current/",
|
|
68
|
+
baseLabel: options.baseLabel ?? "previous",
|
|
69
|
+
files,
|
|
70
|
+
specDiffs,
|
|
71
|
+
screenshotDiffs,
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
function emptyReport(config, prNumber, currentBuild) {
|
|
75
|
+
return {
|
|
76
|
+
prNumber,
|
|
77
|
+
currentBuild,
|
|
78
|
+
currentBuildUrl: publicUrl(config.baseUrl, "build", currentBuild, ""),
|
|
79
|
+
baseLabel: "none",
|
|
80
|
+
files: [],
|
|
81
|
+
specDiffs: [],
|
|
82
|
+
screenshotDiffs: [],
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
async function compareBuilds(config, baseBuild, currentBuild, prNumber, baseLabel) {
|
|
86
|
+
const baseRoot = pathJoin(config.remoteDir, "build", baseBuild);
|
|
59
87
|
const currentRoot = pathJoin(config.remoteDir, "build", currentBuild);
|
|
60
|
-
const
|
|
88
|
+
const baseFiles = await listRemoteFilesRecursive(baseRoot, config);
|
|
61
89
|
const currentFiles = await listRemoteFilesRecursive(currentRoot, config);
|
|
62
|
-
const
|
|
90
|
+
const baseRelative = new Set(baseFiles.map((file) => relativeRemotePath(baseRoot, file)));
|
|
63
91
|
const currentRelative = new Set(currentFiles.map((file) => relativeRemotePath(currentRoot, file)));
|
|
64
|
-
const allPaths = Array.from(new Set([...
|
|
65
|
-
const localRoot = join(tmpdir(), `feature-spec-md-build-compare-${process.pid}-${
|
|
92
|
+
const allPaths = Array.from(new Set([...baseRelative, ...currentRelative])).sort();
|
|
93
|
+
const localRoot = join(tmpdir(), `feature-spec-md-build-compare-${process.pid}-${baseBuild}-${currentBuild}`);
|
|
66
94
|
const files = [];
|
|
67
95
|
for (const filePath of allPaths) {
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
const currentRemote = currentRelative.has(filePath)
|
|
72
|
-
? pathJoin(currentRoot, filePath)
|
|
73
|
-
: undefined;
|
|
74
|
-
const previousLocal = previousRemote
|
|
75
|
-
? join(localRoot, "previous", filePath)
|
|
76
|
-
: undefined;
|
|
96
|
+
const baseRemote = baseRelative.has(filePath) ? pathJoin(baseRoot, filePath) : undefined;
|
|
97
|
+
const currentRemote = currentRelative.has(filePath) ? pathJoin(currentRoot, filePath) : undefined;
|
|
98
|
+
const baseLocal = baseRemote ? join(localRoot, "base", filePath) : undefined;
|
|
77
99
|
const currentLocal = currentRemote ? join(localRoot, "current", filePath) : undefined;
|
|
78
|
-
if (
|
|
79
|
-
await downloadRemoteFile(
|
|
80
|
-
|
|
81
|
-
if (currentRemote && currentLocal) {
|
|
100
|
+
if (baseRemote && baseLocal)
|
|
101
|
+
await downloadRemoteFile(baseRemote, baseLocal, config);
|
|
102
|
+
if (currentRemote && currentLocal)
|
|
82
103
|
await downloadRemoteFile(currentRemote, currentLocal, config);
|
|
83
|
-
|
|
84
|
-
const previousInfo = previousLocal ? await fileInfo(previousLocal) : undefined;
|
|
104
|
+
const baseInfo = baseLocal ? await fileInfo(baseLocal) : undefined;
|
|
85
105
|
const currentInfo = currentLocal ? await fileInfo(currentLocal) : undefined;
|
|
86
|
-
const status = !
|
|
87
|
-
? "added"
|
|
88
|
-
: !currentInfo
|
|
89
|
-
? "removed"
|
|
90
|
-
: previousInfo.hash === currentInfo.hash
|
|
91
|
-
? "unchanged"
|
|
92
|
-
: "changed";
|
|
106
|
+
const status = !baseInfo ? "added" : !currentInfo ? "removed" : baseInfo.hash === currentInfo.hash ? "unchanged" : "changed";
|
|
93
107
|
files.push({
|
|
94
108
|
path: filePath,
|
|
95
109
|
kind: fileKind(filePath),
|
|
96
110
|
status,
|
|
97
|
-
previousHash:
|
|
111
|
+
previousHash: baseInfo?.hash,
|
|
98
112
|
currentHash: currentInfo?.hash,
|
|
99
|
-
previousSize:
|
|
113
|
+
previousSize: baseInfo?.size,
|
|
100
114
|
currentSize: currentInfo?.size,
|
|
101
115
|
});
|
|
102
116
|
}
|
|
117
|
+
const previousIndex = await readFileMaybe(join(localRoot, "base", "index.html"));
|
|
118
|
+
const currentIndex = await readFileMaybe(join(localRoot, "current", "index.html"));
|
|
119
|
+
const previousSpecs = extractSpecSections(previousIndex ?? "");
|
|
120
|
+
const currentSpecs = extractSpecSections(currentIndex ?? "");
|
|
121
|
+
const specDiffs = compareSpecSections(previousSpecs, currentSpecs);
|
|
122
|
+
const scenarioToSpec = scenarioSpecMap([...previousSpecs, ...currentSpecs]);
|
|
123
|
+
const screenshotDiffs = groupScreenshotDiffs(files, scenarioToSpec, {
|
|
124
|
+
previousUrl: (filePath) => publicUrl(config.baseUrl, "build", baseBuild, filePath),
|
|
125
|
+
currentUrl: (filePath) => publicUrl(config.baseUrl, "build", currentBuild, filePath),
|
|
126
|
+
});
|
|
103
127
|
return {
|
|
104
128
|
prNumber,
|
|
105
|
-
|
|
129
|
+
baseBuild,
|
|
106
130
|
currentBuild,
|
|
107
|
-
|
|
131
|
+
baseBuildUrl: publicUrl(config.baseUrl, "build", baseBuild, ""),
|
|
108
132
|
currentBuildUrl: publicUrl(config.baseUrl, "build", currentBuild, ""),
|
|
133
|
+
baseLabel,
|
|
109
134
|
files,
|
|
135
|
+
specDiffs,
|
|
136
|
+
screenshotDiffs,
|
|
110
137
|
};
|
|
111
138
|
}
|
|
139
|
+
async function compareLocalFiles(previousDir, currentDir) {
|
|
140
|
+
const previousFiles = await listLocalFilesRecursive(previousDir);
|
|
141
|
+
const currentFiles = await listLocalFilesRecursive(currentDir);
|
|
142
|
+
const previousSet = new Set(previousFiles);
|
|
143
|
+
const currentSet = new Set(currentFiles);
|
|
144
|
+
const allPaths = Array.from(new Set([...previousSet, ...currentSet])).sort();
|
|
145
|
+
const files = [];
|
|
146
|
+
for (const filePath of allPaths) {
|
|
147
|
+
const previousInfo = previousSet.has(filePath) ? await fileInfo(join(previousDir, filePath)) : undefined;
|
|
148
|
+
const currentInfo = currentSet.has(filePath) ? await fileInfo(join(currentDir, filePath)) : undefined;
|
|
149
|
+
const status = !previousInfo ? "added" : !currentInfo ? "removed" : previousInfo.hash === currentInfo.hash ? "unchanged" : "changed";
|
|
150
|
+
files.push({
|
|
151
|
+
path: filePath,
|
|
152
|
+
kind: fileKind(filePath),
|
|
153
|
+
status,
|
|
154
|
+
previousHash: previousInfo?.hash,
|
|
155
|
+
currentHash: currentInfo?.hash,
|
|
156
|
+
previousSize: previousInfo?.size,
|
|
157
|
+
currentSize: currentInfo?.size,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return files;
|
|
161
|
+
}
|
|
162
|
+
async function listLocalFilesRecursive(root) {
|
|
163
|
+
const files = [];
|
|
164
|
+
await visit("");
|
|
165
|
+
return files.sort();
|
|
166
|
+
async function visit(relativeDir) {
|
|
167
|
+
const dir = relativeDir ? join(root, relativeDir) : root;
|
|
168
|
+
for (const entry of await readdir(dir, { withFileTypes: true })) {
|
|
169
|
+
const relativePath = relativeDir ? `${relativeDir}/${entry.name}` : entry.name;
|
|
170
|
+
if (entry.isDirectory()) {
|
|
171
|
+
await visit(relativePath);
|
|
172
|
+
}
|
|
173
|
+
else if (entry.isFile()) {
|
|
174
|
+
files.push(relativePath);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
112
179
|
function previousBuildNumber(builds, currentBuild) {
|
|
113
180
|
const current = Number(currentBuild);
|
|
114
|
-
return builds
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
.at(0)
|
|
119
|
-
?.toString();
|
|
181
|
+
return builds.map(Number).filter((build) => Number.isFinite(build) && build < current).sort((a, b) => b - a).at(0)?.toString();
|
|
182
|
+
}
|
|
183
|
+
function optionValue(options, key, envKey) {
|
|
184
|
+
return options[key] ?? process.env[envKey];
|
|
120
185
|
}
|
|
121
186
|
async function fileInfo(filePath) {
|
|
122
187
|
const buffer = await readFile(filePath);
|
|
123
|
-
return {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
188
|
+
return { hash: createHash("sha256").update(buffer).digest("hex"), size: buffer.byteLength };
|
|
189
|
+
}
|
|
190
|
+
async function readFileMaybe(filePath) {
|
|
191
|
+
try {
|
|
192
|
+
return await readFile(filePath, "utf8");
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return undefined;
|
|
196
|
+
}
|
|
127
197
|
}
|
|
128
198
|
function relativeRemotePath(root, filePath) {
|
|
129
199
|
return relative(root, filePath).split("\\").join("/");
|
|
130
200
|
}
|
|
131
201
|
function fileKind(filePath) {
|
|
132
202
|
const name = basename(filePath).toLowerCase();
|
|
133
|
-
if (name === "index.html" || name.endsWith(".html") || name.endsWith(".json"))
|
|
203
|
+
if (name === "index.html" || name.endsWith(".html") || name.endsWith(".json"))
|
|
134
204
|
return "report";
|
|
135
|
-
|
|
136
|
-
if (/\.(png|jpe?g|webp|gif)$/i.test(name))
|
|
205
|
+
if (/\.(png|jpe?g|webp|gif|svg)$/i.test(name))
|
|
137
206
|
return "screenshot";
|
|
138
207
|
return "asset";
|
|
139
208
|
}
|
|
209
|
+
function extractSpecSections(source) {
|
|
210
|
+
if (!source)
|
|
211
|
+
return [];
|
|
212
|
+
const sections = [];
|
|
213
|
+
const matches = source.matchAll(/<section class="panel">([\s\S]*?)(?=<section class="panel">|<script>|<\/body>)/g);
|
|
214
|
+
for (const match of matches) {
|
|
215
|
+
const fragment = match[1] ?? "";
|
|
216
|
+
const heading = fragment.match(/<h2[^>]*>([\s\S]*?)<\/h2>/)?.[1];
|
|
217
|
+
if (!heading)
|
|
218
|
+
continue;
|
|
219
|
+
const title = textContent(heading).trim();
|
|
220
|
+
if (!title || title === "Validation" || title === "Models")
|
|
221
|
+
continue;
|
|
222
|
+
const filePaths = Array.from(fragment.matchAll(/title="([^":]+\.md):\d+"/g), (item) => item[1]);
|
|
223
|
+
const scenarioIds = Array.from(fragment.matchAll(/<summary><code>([^<]+)<\/code>/g), (item) => textContent(item[1] ?? ""));
|
|
224
|
+
const text = specText(fragment);
|
|
225
|
+
const filePath = filePaths[0];
|
|
226
|
+
sections.push({
|
|
227
|
+
key: filePath ?? title,
|
|
228
|
+
title,
|
|
229
|
+
filePath,
|
|
230
|
+
scenarioIds,
|
|
231
|
+
text,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
return sections;
|
|
235
|
+
}
|
|
236
|
+
function specText(fragment) {
|
|
237
|
+
return textContent(fragment)
|
|
238
|
+
.split(/\n+/)
|
|
239
|
+
.map((line) => line.trim())
|
|
240
|
+
.filter(Boolean)
|
|
241
|
+
.join("\n");
|
|
242
|
+
}
|
|
243
|
+
function textContent(fragment) {
|
|
244
|
+
return decodeEntities(fragment
|
|
245
|
+
.replace(/<br\s*\/?>/gi, "\n")
|
|
246
|
+
.replace(/<\/p>|<\/li>|<\/summary>|<\/h[1-6]>|<\/div>/gi, "\n")
|
|
247
|
+
.replace(/<[^>]+>/g, ""));
|
|
248
|
+
}
|
|
249
|
+
function decodeEntities(value) {
|
|
250
|
+
return value
|
|
251
|
+
.replace(/</g, "<")
|
|
252
|
+
.replace(/>/g, ">")
|
|
253
|
+
.replace(/"/g, '"')
|
|
254
|
+
.replace(/'/g, "'")
|
|
255
|
+
.replace(/&/g, "&");
|
|
256
|
+
}
|
|
257
|
+
function compareSpecSections(previous, current) {
|
|
258
|
+
const previousByKey = new Map(previous.map((spec) => [spec.key, spec]));
|
|
259
|
+
const currentByKey = new Map(current.map((spec) => [spec.key, spec]));
|
|
260
|
+
const keys = Array.from(new Set([...previousByKey.keys(), ...currentByKey.keys()])).sort();
|
|
261
|
+
const diffs = [];
|
|
262
|
+
for (const key of keys) {
|
|
263
|
+
const before = previousByKey.get(key);
|
|
264
|
+
const after = currentByKey.get(key);
|
|
265
|
+
const status = !before ? "added" : !after ? "removed" : before.text === after.text ? undefined : "changed";
|
|
266
|
+
if (!status)
|
|
267
|
+
continue;
|
|
268
|
+
diffs.push({
|
|
269
|
+
key,
|
|
270
|
+
title: after?.title ?? before?.title ?? key,
|
|
271
|
+
filePath: after?.filePath ?? before?.filePath,
|
|
272
|
+
status,
|
|
273
|
+
lines: diffLines(before?.text ?? "", after?.text ?? ""),
|
|
274
|
+
});
|
|
275
|
+
}
|
|
276
|
+
return diffs;
|
|
277
|
+
}
|
|
278
|
+
function diffLines(previous, current) {
|
|
279
|
+
const a = previous.split("\n");
|
|
280
|
+
const b = current.split("\n");
|
|
281
|
+
const matrix = Array.from({ length: a.length + 1 }, () => Array(b.length + 1).fill(0));
|
|
282
|
+
for (let i = a.length - 1; i >= 0; i -= 1) {
|
|
283
|
+
for (let j = b.length - 1; j >= 0; j -= 1) {
|
|
284
|
+
matrix[i][j] = a[i] === b[j] ? matrix[i + 1][j + 1] + 1 : Math.max(matrix[i + 1][j], matrix[i][j + 1]);
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
const lines = [];
|
|
288
|
+
let i = 0;
|
|
289
|
+
let j = 0;
|
|
290
|
+
while (i < a.length || j < b.length) {
|
|
291
|
+
if (i < a.length && j < b.length && a[i] === b[j]) {
|
|
292
|
+
lines.push({ kind: "context", previousLine: i + 1, currentLine: j + 1, text: a[i] });
|
|
293
|
+
i += 1;
|
|
294
|
+
j += 1;
|
|
295
|
+
}
|
|
296
|
+
else if (j < b.length && (i === a.length || matrix[i][j + 1] >= matrix[i + 1][j])) {
|
|
297
|
+
lines.push({ kind: "added", currentLine: j + 1, text: b[j] });
|
|
298
|
+
j += 1;
|
|
299
|
+
}
|
|
300
|
+
else if (i < a.length) {
|
|
301
|
+
lines.push({ kind: "removed", previousLine: i + 1, text: a[i] });
|
|
302
|
+
i += 1;
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
return compactContext(lines);
|
|
306
|
+
}
|
|
307
|
+
function compactContext(lines) {
|
|
308
|
+
const keep = new Set();
|
|
309
|
+
lines.forEach((line, index) => {
|
|
310
|
+
if (line.kind === "context")
|
|
311
|
+
return;
|
|
312
|
+
for (let offset = -3; offset <= 3; offset += 1) {
|
|
313
|
+
const candidate = index + offset;
|
|
314
|
+
if (candidate >= 0 && candidate < lines.length)
|
|
315
|
+
keep.add(candidate);
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
return lines.filter((line, index) => line.kind !== "context" || keep.has(index));
|
|
319
|
+
}
|
|
320
|
+
function scenarioSpecMap(specs) {
|
|
321
|
+
const map = new Map();
|
|
322
|
+
for (const spec of specs) {
|
|
323
|
+
for (const scenarioId of spec.scenarioIds) {
|
|
324
|
+
map.set(scenarioId, spec);
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
return map;
|
|
328
|
+
}
|
|
329
|
+
function groupScreenshotDiffs(files, scenarioMap, urls) {
|
|
330
|
+
const groups = new Map();
|
|
331
|
+
for (const file of files.filter((item) => item.kind === "screenshot" && item.status !== "unchanged")) {
|
|
332
|
+
const scenarioId = scenarioIdFromScreenshotPath(file.path);
|
|
333
|
+
const spec = scenarioId ? scenarioMap.get(scenarioId) : undefined;
|
|
334
|
+
const specLabel = spec?.filePath ?? spec?.title ?? scenarioId ?? "Unmapped screenshots";
|
|
335
|
+
const group = groups.get(specLabel) ?? { specLabel, specPath: spec?.filePath, items: [] };
|
|
336
|
+
group.items.push({
|
|
337
|
+
path: file.path,
|
|
338
|
+
title: screenshotTitle(file.path, scenarioId),
|
|
339
|
+
status: file.status,
|
|
340
|
+
previousUrl: file.status !== "added" ? urls.previousUrl(file.path) : undefined,
|
|
341
|
+
currentUrl: file.status !== "removed" ? urls.currentUrl(file.path) : undefined,
|
|
342
|
+
previousSize: file.previousSize,
|
|
343
|
+
currentSize: file.currentSize,
|
|
344
|
+
});
|
|
345
|
+
groups.set(specLabel, group);
|
|
346
|
+
}
|
|
347
|
+
return Array.from(groups.values()).sort((a, b) => a.specLabel.localeCompare(b.specLabel));
|
|
348
|
+
}
|
|
349
|
+
function scenarioIdFromScreenshotPath(filePath) {
|
|
350
|
+
return basename(filePath).match(/^(.+?)-line-\d+-/)?.[1];
|
|
351
|
+
}
|
|
352
|
+
function screenshotTitle(filePath, scenarioId) {
|
|
353
|
+
const name = basename(filePath).replace(/\.(png|jpe?g|webp|gif|svg)$/i, "");
|
|
354
|
+
return scenarioId ? name.replace(`${scenarioId}-`, `${scenarioId} `) : name;
|
|
355
|
+
}
|
|
356
|
+
function relativeAssetUrl(prefix, filePath) {
|
|
357
|
+
return [prefix.replace(/\/+$/, ""), ...filePath.split("/").filter(Boolean)]
|
|
358
|
+
.filter(Boolean)
|
|
359
|
+
.join("/");
|
|
360
|
+
}
|
|
140
361
|
function renderDiffReport(report) {
|
|
141
|
-
const changed = report.files.filter((file) => file.status !== "unchanged");
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
return
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
<
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
<p>Current: <a href="${html(report.currentBuildUrl)}">build ${html(report.currentBuild)}</a></p>
|
|
170
|
-
<p><span class="badge">${changed.length} changed file${changed.length === 1 ? "" : "s"}</span> <span class="muted">${report.files.length} file${report.files.length === 1 ? "" : "s"} checked</span></p>
|
|
171
|
-
</section>
|
|
172
|
-
${sections}
|
|
173
|
-
</body>
|
|
174
|
-
</html>`;
|
|
362
|
+
const changed = report.files.filter((file) => file.status !== "unchanged" && file.kind !== "report");
|
|
363
|
+
const assetChanges = changed.filter((file) => file.kind === "asset");
|
|
364
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Feature spec PR diff</title><style>body{font-family:system-ui,sans-serif;max-width:1180px;margin:0 auto;padding:40px 24px;color:#1f2328}.panel{border:1px solid #d0d7de;border-radius:8px;padding:20px;margin:18px 0}.badge{border:1px solid #d0d7de;border-radius:999px;padding:2px 8px;font-size:12px}.added{color:#1a7f37}.removed{color:#cf222e}.changed{color:#9a6700}.muted{color:#57606a}table{border-collapse:collapse;width:100%;font-size:14px}th,td{border:1px solid #d0d7de;padding:6px 8px;text-align:left;vertical-align:top}th{background:#f6f8fa}a{color:#0969da}.diff{width:100%;font-family:ui-monospace,SFMono-Regular,Consolas,monospace;font-size:12px}.diff td{padding:2px 8px}.line-no{width:1%;color:#57606a;background:#f6f8fa;text-align:right;user-select:none}.diff-added td{background:#dafbe1}.diff-removed td{background:#ffebe9}.diff-context td{background:#fff}.image-pair{display:grid;grid-template-columns:repeat(auto-fit,minmax(280px,1fr));gap:12px}.image-card{border:1px solid #d0d7de;border-radius:8px;background:#f6f8fa;overflow:hidden}.image-card h4{margin:0;padding:8px 10px;background:#fff;border-bottom:1px solid #d0d7de}.image-card img{display:block;width:100%;height:auto}</style></head><body><h1>Feature spec PR diff for PR #${html(report.prNumber)}</h1><p>Generated ${html(new Date().toISOString())}.</p><section class="panel"><h2>Compared builds</h2><p>${report.baseBuild ? `${baseLabel(report)}: <a href="${html(report.baseBuildUrl ?? "")}">build ${html(report.baseBuild)}</a>` : "No base build found."}</p><p>PR: <a href="${html(report.currentBuildUrl)}">build ${html(report.currentBuild)}</a></p><p><span class="badge">${report.specDiffs.length} spec change${report.specDiffs.length === 1 ? "" : "s"}</span> <span class="badge">${screenshotChangeCount(report)} screenshot change${screenshotChangeCount(report) === 1 ? "" : "s"}</span></p></section>${renderSpecDiffs(report.specDiffs)}${renderScreenshotDiffs(report.screenshotDiffs)}${renderFileSection("Other assets", assetChanges)}</body></html>`;
|
|
365
|
+
}
|
|
366
|
+
function renderSpecDiffs(specDiffs) {
|
|
367
|
+
if (!specDiffs.length)
|
|
368
|
+
return `<section class="panel"><h2>Spec changes</h2><p class="muted">No spec text changes detected in the published report.</p></section>`;
|
|
369
|
+
return `<section class="panel"><h2>Spec changes</h2>${specDiffs.map(renderSpecDiff).join("\n")}</section>`;
|
|
370
|
+
}
|
|
371
|
+
function renderSpecDiff(diff) {
|
|
372
|
+
return `<details open><summary><strong>${html(diff.filePath ?? diff.title)}</strong> <span class="badge ${diff.status}">${html(diff.status)}</span></summary><table class="diff"><tbody>${diff.lines.map(renderDiffLine).join("")}</tbody></table></details>`;
|
|
373
|
+
}
|
|
374
|
+
function renderDiffLine(line) {
|
|
375
|
+
const marker = line.kind === "added" ? "+" : line.kind === "removed" ? "-" : "";
|
|
376
|
+
return `<tr class="diff-${line.kind}"><td class="line-no">${line.previousLine ?? ""}</td><td class="line-no">${line.currentLine ?? ""}</td><td>${html(marker)} ${html(line.text)}</td></tr>`;
|
|
377
|
+
}
|
|
378
|
+
function renderScreenshotDiffs(groups) {
|
|
379
|
+
if (!groups.length)
|
|
380
|
+
return `<section class="panel"><h2>Screenshots</h2><p class="muted">No screenshot changes.</p></section>`;
|
|
381
|
+
return `<section class="panel"><h2>Screenshots</h2>${groups.map(renderScreenshotGroup).join("\n")}</section>`;
|
|
382
|
+
}
|
|
383
|
+
function renderScreenshotGroup(group) {
|
|
384
|
+
return `<section><h3>${html(group.specPath ?? group.specLabel)}</h3>${group.items.map(renderScreenshotItem).join("\n")}</section>`;
|
|
385
|
+
}
|
|
386
|
+
function renderScreenshotItem(item) {
|
|
387
|
+
const before = item.previousUrl ? `<div class="image-card"><h4>Before</h4><img src="${html(item.previousUrl)}" alt="Before ${html(item.title)}"></div>` : "";
|
|
388
|
+
const after = item.currentUrl ? `<div class="image-card"><h4>After</h4><img src="${html(item.currentUrl)}" alt="After ${html(item.title)}"></div>` : "";
|
|
389
|
+
return `<details open><summary><code>${html(item.path)}</code> <span class="badge ${item.status}">${html(item.status)}</span> <span class="muted">${html(sizeChange({ previousSize: item.previousSize, currentSize: item.currentSize }))}</span></summary><div class="image-pair">${before}${after}</div></details>`;
|
|
175
390
|
}
|
|
176
391
|
function renderFileSection(title, files) {
|
|
177
|
-
if (!files.length)
|
|
392
|
+
if (!files.length)
|
|
178
393
|
return `<section class="panel"><h2>${html(title)}</h2><p class="muted">No changes.</p></section>`;
|
|
179
|
-
}
|
|
180
|
-
return `<section class="panel"><h2>${html(title)}</h2><table><thead><tr><th>Status</th><th>File</th><th>Size change</th></tr></thead><tbody>${files
|
|
181
|
-
.map((file) => `<tr><td class="${file.status}">${html(file.status)}</td><td><code>${html(file.path)}</code></td><td>${html(sizeChange(file))}</td></tr>`)
|
|
182
|
-
.join("")}</tbody></table></section>`;
|
|
394
|
+
return `<section class="panel"><h2>${html(title)}</h2><table><thead><tr><th>Status</th><th>File</th><th>Size change</th></tr></thead><tbody>${files.map((file) => `<tr><td class="${file.status}">${html(file.status)}</td><td><code>${html(file.path)}</code></td><td>${html(sizeChange(file))}</td></tr>`).join("")}</tbody></table></section>`;
|
|
183
395
|
}
|
|
184
396
|
function sizeChange(file) {
|
|
185
397
|
if (file.previousSize === undefined)
|
|
@@ -187,23 +399,35 @@ function sizeChange(file) {
|
|
|
187
399
|
if (file.currentSize === undefined)
|
|
188
400
|
return `-${file.previousSize} bytes`;
|
|
189
401
|
const diff = file.currentSize - file.previousSize;
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
402
|
+
return diff === 0 ? "same size" : `${diff > 0 ? "+" : ""}${diff} bytes`;
|
|
403
|
+
}
|
|
404
|
+
function screenshotChangeCount(report) {
|
|
405
|
+
return report.screenshotDiffs.reduce((sum, group) => sum + group.items.length, 0);
|
|
193
406
|
}
|
|
194
407
|
function commentBody(report, reportUrl) {
|
|
195
|
-
const changed = report.files.filter((file) => file.status !== "unchanged");
|
|
196
|
-
const reportChanges = changed.filter((file) => file.kind === "report").length;
|
|
197
|
-
const screenshotChanges = changed.filter((file) => file.kind === "screenshot").length;
|
|
198
|
-
const previous = report.previousBuild
|
|
199
|
-
? `build ${report.previousBuild} → build ${report.currentBuild}`
|
|
200
|
-
: `build ${report.currentBuild}`;
|
|
201
408
|
return [
|
|
202
409
|
"## Feature spec PR diff",
|
|
203
410
|
"",
|
|
204
|
-
`[Open the diff report](${reportUrl}) for ${
|
|
411
|
+
`[Open the diff report](${reportUrl}) for ${comparisonText(report)}.`,
|
|
205
412
|
"",
|
|
206
|
-
`Changed
|
|
413
|
+
`Changed: **${report.specDiffs.length}** spec file(s), **${screenshotChangeCount(report)}** screenshot(s).`,
|
|
207
414
|
].join("\n");
|
|
208
415
|
}
|
|
416
|
+
function comparisonText(report) {
|
|
417
|
+
if (!report.baseBuild)
|
|
418
|
+
return `PR build ${report.currentBuild}`;
|
|
419
|
+
return `${baseLabel(report).toLowerCase()} build ${report.baseBuild} → PR build ${report.currentBuild}`;
|
|
420
|
+
}
|
|
421
|
+
function summaryComparisonSentence(report) {
|
|
422
|
+
if (!report.baseBuild)
|
|
423
|
+
return `<p>No base build was found, so this report establishes the first comparison baseline.</p>`;
|
|
424
|
+
return `<p>Compared ${html(baseLabel(report).toLowerCase())} build <strong>${html(report.baseBuild)}</strong> with PR build <strong>${html(report.currentBuild)}</strong>.</p>`;
|
|
425
|
+
}
|
|
426
|
+
function baseLabel(report) {
|
|
427
|
+
if (report.baseLabel === "main")
|
|
428
|
+
return "Main";
|
|
429
|
+
if (report.baseLabel === "previous")
|
|
430
|
+
return "Previous";
|
|
431
|
+
return "Base";
|
|
432
|
+
}
|
|
209
433
|
//# sourceMappingURL=githubActionDiffReport.js.map
|