executable-stories-formatters 1.9.2 → 1.11.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/cli.js +2458 -585
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +1559 -126
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +563 -1
- package/dist/index.d.ts +563 -1
- package/dist/index.js +1534 -123
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
|
-
import { parseArgs as
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
4
|
+
import { parseArgs as parseArgs3 } from "util";
|
|
5
|
+
import * as fs17 from "fs";
|
|
6
|
+
import * as path19 from "path";
|
|
7
7
|
|
|
8
8
|
// src/validation/schema-validator.ts
|
|
9
9
|
import Ajv from "ajv/dist/2020.js";
|
|
@@ -941,28 +941,28 @@ function validateRawRun(data) {
|
|
|
941
941
|
return { valid: true, errors: [] };
|
|
942
942
|
}
|
|
943
943
|
const errors = (validate.errors ?? []).map((err) => {
|
|
944
|
-
const
|
|
944
|
+
const path20 = err.instancePath || "/";
|
|
945
945
|
const message = err.message ?? "unknown error";
|
|
946
946
|
if (err.keyword === "additionalProperties") {
|
|
947
947
|
const extra = err.params.additionalProperty;
|
|
948
|
-
return `${
|
|
948
|
+
return `${path20}: ${message} \u2014 '${extra}'`;
|
|
949
949
|
}
|
|
950
950
|
if (err.keyword === "enum") {
|
|
951
951
|
const allowed = err.params.allowedValues;
|
|
952
|
-
return `${
|
|
952
|
+
return `${path20}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
953
953
|
}
|
|
954
|
-
return `${
|
|
954
|
+
return `${path20}: ${message}`;
|
|
955
955
|
});
|
|
956
956
|
return { valid: false, errors };
|
|
957
957
|
}
|
|
958
958
|
|
|
959
959
|
// src/cli.ts
|
|
960
|
-
import { synthesizeStories as
|
|
961
|
-
import { canonicalizeRun as
|
|
960
|
+
import { synthesizeStories as synthesizeStories4 } from "executable-stories-core/converters/synthesize";
|
|
961
|
+
import { canonicalizeRun as canonicalizeRun6 } from "executable-stories-core/converters/acl/index";
|
|
962
962
|
import { assertValidRun as assertValidRun2 } from "executable-stories-core/converters/acl/validate";
|
|
963
963
|
|
|
964
964
|
// src/index.ts
|
|
965
|
-
import * as
|
|
965
|
+
import * as path8 from "path";
|
|
966
966
|
import * as fsPromises from "fs/promises";
|
|
967
967
|
import { toStoryReportWithIndex } from "executable-stories-core/converters/story-report";
|
|
968
968
|
|
|
@@ -1610,50 +1610,50 @@ function scenarioLines(scenario) {
|
|
|
1610
1610
|
if (scenario.errorMessage) lines.push(indent(`error: ${scenario.errorMessage}`, " "));
|
|
1611
1611
|
return lines;
|
|
1612
1612
|
}
|
|
1613
|
-
function docLines(entry,
|
|
1614
|
-
const lines = ownDocLines(entry,
|
|
1615
|
-
for (const child of entry.children ?? []) lines.push(...docLines(child,
|
|
1613
|
+
function docLines(entry, pad2) {
|
|
1614
|
+
const lines = ownDocLines(entry, pad2);
|
|
1615
|
+
for (const child of entry.children ?? []) lines.push(...docLines(child, pad2 + " "));
|
|
1616
1616
|
return lines;
|
|
1617
1617
|
}
|
|
1618
|
-
function ownDocLines(entry,
|
|
1618
|
+
function ownDocLines(entry, pad2) {
|
|
1619
1619
|
switch (entry.kind) {
|
|
1620
1620
|
case "note":
|
|
1621
|
-
return [indent(entry.text,
|
|
1621
|
+
return [indent(entry.text, pad2)];
|
|
1622
1622
|
case "tag":
|
|
1623
1623
|
return [];
|
|
1624
1624
|
// already on the scenario's tag line
|
|
1625
1625
|
case "kv":
|
|
1626
|
-
return [`${
|
|
1626
|
+
return [`${pad2}${entry.label}: ${compact(entry.value)}`];
|
|
1627
1627
|
case "code":
|
|
1628
|
-
return [`${
|
|
1628
|
+
return [`${pad2}code ${entry.label}${entry.lang ? ` (${entry.lang})` : ""}:`, indent(entry.content, pad2 + " ")];
|
|
1629
1629
|
case "table":
|
|
1630
1630
|
return [
|
|
1631
|
-
`${
|
|
1632
|
-
...entry.rows.map((row) => `${
|
|
1631
|
+
`${pad2}table ${entry.label}: ${entry.columns.join(" | ")}`,
|
|
1632
|
+
...entry.rows.map((row) => `${pad2} ${row.join(" | ")}`)
|
|
1633
1633
|
];
|
|
1634
1634
|
case "link":
|
|
1635
|
-
return [`${
|
|
1635
|
+
return [`${pad2}link ${entry.label}: ${entry.url}`];
|
|
1636
1636
|
case "section":
|
|
1637
|
-
return [`${
|
|
1637
|
+
return [`${pad2}section ${entry.title}:`, indent(entry.markdown, pad2 + " ")];
|
|
1638
1638
|
case "mermaid":
|
|
1639
|
-
return [`${
|
|
1639
|
+
return [`${pad2}mermaid${entry.title ? ` ${entry.title}` : ""}:`, indent(entry.code, pad2 + " ")];
|
|
1640
1640
|
case "screenshot":
|
|
1641
|
-
return [`${
|
|
1641
|
+
return [`${pad2}screenshot ${entry.path}${entry.alt ? ` \u2014 ${entry.alt}` : ""}`];
|
|
1642
1642
|
case "video":
|
|
1643
|
-
return [`${
|
|
1643
|
+
return [`${pad2}video ${entry.path}${entry.caption ? ` \u2014 ${entry.caption}` : ""}`];
|
|
1644
1644
|
case "html":
|
|
1645
|
-
return [`${
|
|
1645
|
+
return [`${pad2}html ${entry.title ?? entry.path ?? entry.url ?? "(inline)"}`];
|
|
1646
1646
|
case "state":
|
|
1647
|
-
return [`${
|
|
1647
|
+
return [`${pad2}state${entry.label ? ` ${entry.label}` : ""}: ${compact(entry.value)}`];
|
|
1648
1648
|
case "custom":
|
|
1649
|
-
return [`${
|
|
1649
|
+
return [`${pad2}custom ${entry.type}: ${compact(entry.data)}`];
|
|
1650
1650
|
}
|
|
1651
1651
|
}
|
|
1652
1652
|
function compact(value) {
|
|
1653
1653
|
return typeof value === "string" ? value : JSON.stringify(value);
|
|
1654
1654
|
}
|
|
1655
|
-
function indent(text2,
|
|
1656
|
-
return text2.split("\n").map((line) =>
|
|
1655
|
+
function indent(text2, pad2) {
|
|
1656
|
+
return text2.split("\n").map((line) => pad2 + line).join("\n");
|
|
1657
1657
|
}
|
|
1658
1658
|
|
|
1659
1659
|
// src/formatters/junit-xml.ts
|
|
@@ -2556,9 +2556,9 @@ var MAX_FUZZ = 2;
|
|
|
2556
2556
|
var normalize = (line) => line.trim();
|
|
2557
2557
|
var HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@ ?(.*)$/;
|
|
2558
2558
|
function stripPathPrefix(raw) {
|
|
2559
|
-
const
|
|
2560
|
-
if (
|
|
2561
|
-
return
|
|
2559
|
+
const path20 = raw.split(" ")[0].trim();
|
|
2560
|
+
if (path20 === "/dev/null") return void 0;
|
|
2561
|
+
return path20.replace(/^[ab]\//, "");
|
|
2562
2562
|
}
|
|
2563
2563
|
function parseUnifiedDiff(patch) {
|
|
2564
2564
|
const files = [];
|
|
@@ -2638,7 +2638,7 @@ function createAnchor(args) {
|
|
|
2638
2638
|
};
|
|
2639
2639
|
}
|
|
2640
2640
|
function changedRunCandidates(anchor, file, fileIndex) {
|
|
2641
|
-
const
|
|
2641
|
+
const path20 = file.newPath ?? file.oldPath ?? "";
|
|
2642
2642
|
const out = [];
|
|
2643
2643
|
file.hunks.forEach((hunk, hunkIndex) => {
|
|
2644
2644
|
outer: for (let i = 0; i + anchor.changed.length <= hunk.lines.length; i++) {
|
|
@@ -2649,7 +2649,7 @@ function changedRunCandidates(anchor, file, fileIndex) {
|
|
|
2649
2649
|
continue outer;
|
|
2650
2650
|
}
|
|
2651
2651
|
}
|
|
2652
|
-
out.push({ fileIndex, file:
|
|
2652
|
+
out.push({ fileIndex, file: path20, hunkIndex, lineIndex: i, lines: hunk.lines });
|
|
2653
2653
|
}
|
|
2654
2654
|
});
|
|
2655
2655
|
return out;
|
|
@@ -2746,18 +2746,18 @@ function deriveChangeType(tags) {
|
|
|
2746
2746
|
}
|
|
2747
2747
|
return "unknown";
|
|
2748
2748
|
}
|
|
2749
|
-
function extensionOf(
|
|
2750
|
-
const base =
|
|
2749
|
+
function extensionOf(path20) {
|
|
2750
|
+
const base = path20.split("/").pop() ?? path20;
|
|
2751
2751
|
const dot = base.lastIndexOf(".");
|
|
2752
2752
|
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
2753
2753
|
}
|
|
2754
|
-
function isTestFile(
|
|
2755
|
-
return TEST_INFIX.test(
|
|
2754
|
+
function isTestFile(path20) {
|
|
2755
|
+
return TEST_INFIX.test(path20);
|
|
2756
2756
|
}
|
|
2757
|
-
function isReviewableSource(
|
|
2758
|
-
if (isTestFile(
|
|
2759
|
-
if (
|
|
2760
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
2757
|
+
function isReviewableSource(path20) {
|
|
2758
|
+
if (isTestFile(path20)) return false;
|
|
2759
|
+
if (path20.endsWith(".d.ts")) return false;
|
|
2760
|
+
return CODE_EXTENSIONS.has(extensionOf(path20));
|
|
2761
2761
|
}
|
|
2762
2762
|
function testBaseKey(testFile) {
|
|
2763
2763
|
return testFile.replace(TEST_INFIX, "");
|
|
@@ -2861,7 +2861,7 @@ function toClaim(testCase, changedSourcePaths) {
|
|
|
2861
2861
|
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
2862
2862
|
const key = testBaseKey(testCase.sourceFile);
|
|
2863
2863
|
const coversFiles = changedSourcePaths.filter(
|
|
2864
|
-
(
|
|
2864
|
+
(path20) => sourceBaseKey(path20) === key
|
|
2865
2865
|
);
|
|
2866
2866
|
return {
|
|
2867
2867
|
id: testCase.id,
|
|
@@ -3033,14 +3033,14 @@ var TraceabilityMatrixFormatter = class {
|
|
|
3033
3033
|
lines.push("");
|
|
3034
3034
|
lines.push(`Status: ${renderRequirementStatus(req.status)}`);
|
|
3035
3035
|
if (req.covers.length > 0) {
|
|
3036
|
-
lines.push(`Covers: ${req.covers.map((
|
|
3036
|
+
lines.push(`Covers: ${req.covers.map((path20) => `\`${path20}\``).join(", ")}`);
|
|
3037
3037
|
}
|
|
3038
3038
|
lines.push("");
|
|
3039
3039
|
lines.push("| Status | Scenario | Source | Covers |");
|
|
3040
3040
|
lines.push("| --- | --- | --- | --- |");
|
|
3041
3041
|
for (const scenario of req.scenarios) {
|
|
3042
3042
|
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
3043
|
-
const covers = scenario.covers.length > 0 ? scenario.covers.map((
|
|
3043
|
+
const covers = scenario.covers.length > 0 ? scenario.covers.map((path20) => `\`${path20}\``).join(", ") : "";
|
|
3044
3044
|
lines.push(`| ${scenario.status} | ${escapePipe2(scenario.title)} | \`${source}\` | ${covers} |`);
|
|
3045
3045
|
}
|
|
3046
3046
|
lines.push("");
|
|
@@ -3483,9 +3483,9 @@ function buildDataTable(table2, line) {
|
|
|
3483
3483
|
const rowLine = line + 1 + r;
|
|
3484
3484
|
rows.push({
|
|
3485
3485
|
location: { line: rowLine },
|
|
3486
|
-
cells: table2.rows[r].map((
|
|
3486
|
+
cells: table2.rows[r].map((cell2) => ({
|
|
3487
3487
|
location: { line: rowLine },
|
|
3488
|
-
value:
|
|
3488
|
+
value: cell2
|
|
3489
3489
|
})),
|
|
3490
3490
|
id: ""
|
|
3491
3491
|
});
|
|
@@ -3586,7 +3586,7 @@ function buildPickleTable(table2) {
|
|
|
3586
3586
|
});
|
|
3587
3587
|
for (const row of table2.rows) {
|
|
3588
3588
|
rows.push({
|
|
3589
|
-
cells: row.map((
|
|
3589
|
+
cells: row.map((cell2) => ({ value: cell2 }))
|
|
3590
3590
|
});
|
|
3591
3591
|
}
|
|
3592
3592
|
return { rows };
|
|
@@ -3805,8 +3805,8 @@ function extractDocAttachments(step) {
|
|
|
3805
3805
|
}
|
|
3806
3806
|
return attachments;
|
|
3807
3807
|
}
|
|
3808
|
-
function guessMediaType(
|
|
3809
|
-
const lower =
|
|
3808
|
+
function guessMediaType(path20) {
|
|
3809
|
+
const lower = path20.toLowerCase();
|
|
3810
3810
|
if (lower.endsWith(".png")) return "image/png";
|
|
3811
3811
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
3812
3812
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -3947,11 +3947,11 @@ var CucumberHtmlFormatter = class {
|
|
|
3947
3947
|
for (const envelope of envelopes) {
|
|
3948
3948
|
const accepted = htmlStream.write(envelope);
|
|
3949
3949
|
if (!accepted) {
|
|
3950
|
-
await new Promise((
|
|
3950
|
+
await new Promise((resolve12) => htmlStream.once("drain", resolve12));
|
|
3951
3951
|
}
|
|
3952
3952
|
}
|
|
3953
|
-
await new Promise((
|
|
3954
|
-
collector.on("finish",
|
|
3953
|
+
await new Promise((resolve12, reject) => {
|
|
3954
|
+
collector.on("finish", resolve12);
|
|
3955
3955
|
collector.on("error", reject);
|
|
3956
3956
|
htmlStream.end();
|
|
3957
3957
|
});
|
|
@@ -4685,7 +4685,7 @@ function formatDocEntry(doc) {
|
|
|
4685
4685
|
return `${escapeHtml2(doc.label)}${doc.lang ? ` (${escapeHtml2(doc.lang)})` : ""}: <code>${escapeHtml2(doc.content)}</code>`;
|
|
4686
4686
|
case "table": {
|
|
4687
4687
|
const header = `<tr>${doc.columns.map((c) => `<th>${escapeHtml2(c)}</th>`).join("")}</tr>`;
|
|
4688
|
-
const rows = doc.rows.map((row) => `<tr>${row.map((
|
|
4688
|
+
const rows = doc.rows.map((row) => `<tr>${row.map((cell2) => `<td>${escapeHtml2(cell2)}</td>`).join("")}</tr>`).join("");
|
|
4689
4689
|
return `${escapeHtml2(doc.label)}<table>${header}${rows}</table>`;
|
|
4690
4690
|
}
|
|
4691
4691
|
case "link":
|
|
@@ -5722,7 +5722,7 @@ ${tc.errorStack}` : "");
|
|
|
5722
5722
|
table([
|
|
5723
5723
|
tableRow(entry.columns.map((c) => tableHeader(c))),
|
|
5724
5724
|
...entry.rows.map(
|
|
5725
|
-
(row) => tableRow(row.map((
|
|
5725
|
+
(row) => tableRow(row.map((cell2) => tableCell(cell2)))
|
|
5726
5726
|
)
|
|
5727
5727
|
])
|
|
5728
5728
|
);
|
|
@@ -6157,230 +6157,1611 @@ function replaceAssetRefInData(html, original, replacement) {
|
|
|
6157
6157
|
|
|
6158
6158
|
// src/index.ts
|
|
6159
6159
|
import { STORY_META_KEY } from "executable-stories-core/types/story";
|
|
6160
|
-
import {
|
|
6161
|
-
STORY_REPORT_SCHEMA_VERSION,
|
|
6162
|
-
STORY_REPORT_SCHEMA_MAJOR
|
|
6163
|
-
} from "executable-stories-core/types/story-report";
|
|
6164
|
-
import { ES_THEME_TOKENS_CSS as ES_THEME_TOKENS_CSS2, ES_THEME_TOKEN_VALUES } from "executable-stories-core/theme/tokens";
|
|
6165
|
-
import { canonicalizeRun as canonicalizeRun3 } from "executable-stories-core/converters/acl/index";
|
|
6166
|
-
import { normalizeStatus } from "executable-stories-core/converters/acl/index";
|
|
6167
|
-
import { generateTestCaseId } from "executable-stories-core/converters/acl/index";
|
|
6168
|
-
import { generateRunId } from "executable-stories-core/converters/acl/index";
|
|
6169
|
-
import { slugify as slugify2 } from "executable-stories-core/converters/acl/index";
|
|
6170
|
-
import { deriveStepResults } from "executable-stories-core/converters/acl/index";
|
|
6171
|
-
import { mergeStepResults } from "executable-stories-core/converters/acl/index";
|
|
6172
|
-
import { resolveAttachment } from "executable-stories-core/converters/acl/index";
|
|
6173
|
-
import { resolveAttachments } from "executable-stories-core/converters/acl/index";
|
|
6174
|
-
import {
|
|
6175
|
-
validateCanonicalRun,
|
|
6176
|
-
assertValidRun
|
|
6177
|
-
} from "executable-stories-core/converters/acl/validate";
|
|
6178
6160
|
|
|
6179
|
-
// src/
|
|
6161
|
+
// src/sync/engine.ts
|
|
6162
|
+
import { behaviourFingerprint as behaviourFingerprint2, behaviourSimilarity as behaviourSimilarity2 } from "executable-stories-core/converters/acl/ids";
|
|
6163
|
+
|
|
6164
|
+
// src/sync/lockfile.ts
|
|
6180
6165
|
import * as fs4 from "fs";
|
|
6181
6166
|
import * as path5 from "path";
|
|
6182
|
-
import {
|
|
6183
|
-
|
|
6184
|
-
|
|
6185
|
-
|
|
6186
|
-
|
|
6187
|
-
|
|
6188
|
-
|
|
6189
|
-
|
|
6190
|
-
|
|
6191
|
-
|
|
6192
|
-
|
|
6193
|
-
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
6194
|
-
const generator = new ReportGenerator({
|
|
6195
|
-
formats: options.formats,
|
|
6196
|
-
outputDir: options.outputDir,
|
|
6197
|
-
outputName: options.outputName
|
|
6167
|
+
import { createHash as createHash5 } from "crypto";
|
|
6168
|
+
var DEFAULT_LOCKFILE_PATH = ".executable-stories/sync.lock.json";
|
|
6169
|
+
var LOCKFILE_VERSION = 1;
|
|
6170
|
+
function emptyLockfile() {
|
|
6171
|
+
return { version: LOCKFILE_VERSION, providers: {} };
|
|
6172
|
+
}
|
|
6173
|
+
function hashCaseBody(body) {
|
|
6174
|
+
const canonical = JSON.stringify({
|
|
6175
|
+
title: body.title.trim(),
|
|
6176
|
+
steps: body.steps.map((s) => `${s.keyword.toLowerCase()}:${s.text.trim()}`),
|
|
6177
|
+
description: body.description.trim()
|
|
6198
6178
|
});
|
|
6199
|
-
|
|
6200
|
-
return { files: [...result.values()].flat(), run };
|
|
6201
|
-
}
|
|
6202
|
-
async function regenerateArtifacts(options, deps = {}) {
|
|
6203
|
-
return (await regenerateRun(options, deps)).files;
|
|
6204
|
-
}
|
|
6205
|
-
function startWatch(options, deps = {}) {
|
|
6206
|
-
const log = deps.log ?? ((message) => console.log(message));
|
|
6207
|
-
const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
|
|
6208
|
-
const watchFn = deps.watch ?? ((filePath, listener) => fs4.watch(filePath, listener));
|
|
6209
|
-
const debounceMs = options.debounceMs ?? 150;
|
|
6210
|
-
let timer;
|
|
6211
|
-
let running = false;
|
|
6212
|
-
let pending = false;
|
|
6213
|
-
const run = async () => {
|
|
6214
|
-
if (running) {
|
|
6215
|
-
pending = true;
|
|
6216
|
-
return;
|
|
6217
|
-
}
|
|
6218
|
-
running = true;
|
|
6219
|
-
try {
|
|
6220
|
-
const files = await regenerate(options.input);
|
|
6221
|
-
log(`Regenerated ${files.length} artifact file(s) from ${options.input}`);
|
|
6222
|
-
} catch (error) {
|
|
6223
|
-
log(`Watch regeneration failed: ${error.message}`);
|
|
6224
|
-
} finally {
|
|
6225
|
-
running = false;
|
|
6226
|
-
if (pending) {
|
|
6227
|
-
pending = false;
|
|
6228
|
-
trigger();
|
|
6229
|
-
}
|
|
6230
|
-
}
|
|
6231
|
-
};
|
|
6232
|
-
const trigger = () => {
|
|
6233
|
-
if (timer) clearTimeout(timer);
|
|
6234
|
-
timer = setTimeout(() => void run(), debounceMs);
|
|
6235
|
-
};
|
|
6236
|
-
trigger();
|
|
6237
|
-
const watcher = watchFn(path5.resolve(options.input), trigger);
|
|
6238
|
-
return {
|
|
6239
|
-
close: () => {
|
|
6240
|
-
if (timer) clearTimeout(timer);
|
|
6241
|
-
watcher.close();
|
|
6242
|
-
}
|
|
6243
|
-
};
|
|
6179
|
+
return createHash5("sha1").update(canonical).digest("hex").slice(0, 16);
|
|
6244
6180
|
}
|
|
6245
|
-
|
|
6246
|
-
// src/index.ts
|
|
6247
|
-
import { advanceState, initialRunState } from "executable-stories-core";
|
|
6248
|
-
import { toStoryReport as toStoryReport6 } from "executable-stories-core/converters/story-report";
|
|
6249
|
-
|
|
6250
|
-
// src/publishers/confluence.ts
|
|
6251
|
-
function parseAdf(adf) {
|
|
6181
|
+
function parseLockfile(contents, label) {
|
|
6252
6182
|
let parsed;
|
|
6253
6183
|
try {
|
|
6254
|
-
parsed = JSON.parse(
|
|
6184
|
+
parsed = JSON.parse(contents);
|
|
6255
6185
|
} catch (err) {
|
|
6256
6186
|
throw new Error(
|
|
6257
|
-
`
|
|
6187
|
+
`Sync lockfile at ${label} is not valid JSON: ${err.message}
|
|
6188
|
+
Fix or delete it \u2014 deleting orphans every existing case binding, so prefer fixing.`
|
|
6258
6189
|
);
|
|
6259
6190
|
}
|
|
6260
|
-
if (!parsed || typeof parsed !== "object" ||
|
|
6191
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6192
|
+
throw new Error(`Sync lockfile at ${label} must contain an object.`);
|
|
6193
|
+
}
|
|
6194
|
+
const lock = parsed;
|
|
6195
|
+
if (lock.version !== LOCKFILE_VERSION) {
|
|
6261
6196
|
throw new Error(
|
|
6262
|
-
`
|
|
6197
|
+
`Sync lockfile at ${label} has version ${String(lock.version)}, expected ${LOCKFILE_VERSION}.`
|
|
6263
6198
|
);
|
|
6264
6199
|
}
|
|
6265
|
-
return
|
|
6200
|
+
return { version: LOCKFILE_VERSION, providers: lock.providers ?? {} };
|
|
6266
6201
|
}
|
|
6267
|
-
function
|
|
6268
|
-
const
|
|
6269
|
-
const
|
|
6270
|
-
|
|
6202
|
+
function serializeLockfile(lock) {
|
|
6203
|
+
const providers = {};
|
|
6204
|
+
for (const provider of Object.keys(lock.providers).sort()) {
|
|
6205
|
+
const entries = lock.providers[provider] ?? {};
|
|
6206
|
+
const sorted = {};
|
|
6207
|
+
for (const key of Object.keys(entries).sort()) sorted[key] = entries[key];
|
|
6208
|
+
providers[provider] = sorted;
|
|
6209
|
+
}
|
|
6210
|
+
return `${JSON.stringify({ version: LOCKFILE_VERSION, providers }, null, 2)}
|
|
6211
|
+
`;
|
|
6271
6212
|
}
|
|
6272
|
-
|
|
6273
|
-
|
|
6274
|
-
|
|
6275
|
-
|
|
6276
|
-
|
|
6277
|
-
|
|
6213
|
+
function entriesFor(lock, provider) {
|
|
6214
|
+
return lock.providers[provider] ?? {};
|
|
6215
|
+
}
|
|
6216
|
+
function setEntry(lock, provider, fingerprint, entry) {
|
|
6217
|
+
lock.providers[provider] ??= {};
|
|
6218
|
+
lock.providers[provider][fingerprint] = entry;
|
|
6219
|
+
}
|
|
6220
|
+
|
|
6221
|
+
// src/sync/engine.ts
|
|
6222
|
+
var DEFAULT_DUPLICATE_THRESHOLD = 0.7;
|
|
6223
|
+
var PARTIAL_RUN_ORPHAN_RATIO = 0.25;
|
|
6224
|
+
function normalizeTitle(text2) {
|
|
6225
|
+
return text2.toLowerCase().replace(/[^\p{L}\p{N}\s]/gu, " ").replace(/\s+/g, " ").trim();
|
|
6226
|
+
}
|
|
6227
|
+
function renderDocs(docs, depth = 0) {
|
|
6228
|
+
if (!docs || docs.length === 0) return "";
|
|
6229
|
+
const lines = [];
|
|
6230
|
+
for (const doc of docs) {
|
|
6231
|
+
switch (doc.kind) {
|
|
6232
|
+
case "note":
|
|
6233
|
+
lines.push(doc.text);
|
|
6234
|
+
break;
|
|
6235
|
+
case "kv":
|
|
6236
|
+
lines.push(`**${doc.label}:** ${formatValue(doc.value)}`);
|
|
6237
|
+
break;
|
|
6238
|
+
case "state":
|
|
6239
|
+
lines.push(`**${doc.label ?? "State"}:** ${formatValue(doc.value)}`);
|
|
6240
|
+
break;
|
|
6241
|
+
case "code":
|
|
6242
|
+
lines.push(`**${doc.label}**`, "", "```" + (doc.lang ?? ""), doc.content, "```");
|
|
6243
|
+
break;
|
|
6244
|
+
case "table":
|
|
6245
|
+
lines.push(
|
|
6246
|
+
`**${doc.label}**`,
|
|
6247
|
+
"",
|
|
6248
|
+
`| ${doc.columns.join(" | ")} |`,
|
|
6249
|
+
`| ${doc.columns.map(() => "---").join(" | ")} |`,
|
|
6250
|
+
...doc.rows.map((row) => `| ${row.join(" | ")} |`)
|
|
6251
|
+
);
|
|
6252
|
+
break;
|
|
6253
|
+
case "link":
|
|
6254
|
+
lines.push(`[${doc.label}](${doc.url})`);
|
|
6255
|
+
break;
|
|
6256
|
+
case "section":
|
|
6257
|
+
lines.push(`${"#".repeat(Math.min(6, depth + 3))} ${doc.title}`, "", doc.markdown);
|
|
6258
|
+
break;
|
|
6259
|
+
case "mermaid":
|
|
6260
|
+
lines.push(...doc.title ? [`**${doc.title}**`, ""] : [], "```mermaid", doc.code, "```");
|
|
6261
|
+
break;
|
|
6262
|
+
case "screenshot":
|
|
6263
|
+
lines.push(`_Screenshot: ${doc.alt ?? doc.path}_`);
|
|
6264
|
+
break;
|
|
6265
|
+
case "video":
|
|
6266
|
+
lines.push(`_Video: ${doc.caption ?? doc.path}_`);
|
|
6267
|
+
break;
|
|
6268
|
+
case "html":
|
|
6269
|
+
lines.push(`_Embedded: ${doc.title ?? doc.url ?? doc.path ?? "html"}_`);
|
|
6270
|
+
break;
|
|
6271
|
+
case "custom":
|
|
6272
|
+
lines.push(`_${doc.type}_: ${formatValue(doc.data)}`);
|
|
6273
|
+
break;
|
|
6274
|
+
case "tag":
|
|
6275
|
+
break;
|
|
6276
|
+
}
|
|
6277
|
+
const children = renderDocs(doc.children, depth + 1);
|
|
6278
|
+
if (children) lines.push(children);
|
|
6278
6279
|
}
|
|
6280
|
+
return lines.join("\n");
|
|
6279
6281
|
}
|
|
6280
|
-
|
|
6281
|
-
|
|
6282
|
-
|
|
6283
|
-
|
|
6284
|
-
|
|
6282
|
+
function formatValue(value) {
|
|
6283
|
+
if (typeof value === "string") return value;
|
|
6284
|
+
return JSON.stringify(value);
|
|
6285
|
+
}
|
|
6286
|
+
function scenarioUrl(config, tc) {
|
|
6287
|
+
if (!config.reportUrl) return void 0;
|
|
6288
|
+
const base = config.reportUrl.replace(/\/$/, "");
|
|
6289
|
+
const anchor = config.scenarioAnchor?.(tc);
|
|
6290
|
+
return anchor ? `${base}#${anchor}` : base;
|
|
6291
|
+
}
|
|
6292
|
+
function toCaseBody(tc, config) {
|
|
6293
|
+
const sections = [];
|
|
6294
|
+
const docs = renderDocs(tc.story.docs);
|
|
6295
|
+
if (docs) sections.push(docs);
|
|
6296
|
+
const tickets = tc.story.tickets ?? [];
|
|
6297
|
+
if (tickets.length > 0) {
|
|
6298
|
+
sections.push(
|
|
6299
|
+
`**Requirements:** ${tickets.map((t) => t.url ? `[${t.id}](${t.url})` : t.id).join(", ")}`
|
|
6285
6300
|
);
|
|
6286
6301
|
}
|
|
6287
|
-
if (
|
|
6288
|
-
|
|
6302
|
+
if (tc.story.covers?.length) {
|
|
6303
|
+
sections.push(`**Covers:** ${tc.story.covers.join(", ")}`);
|
|
6289
6304
|
}
|
|
6290
|
-
|
|
6291
|
-
const
|
|
6292
|
-
|
|
6293
|
-
|
|
6305
|
+
sections.push(`_Generated from ${tc.sourceFile}:${tc.sourceLine} by executable-stories. Edit the test, not this case._`);
|
|
6306
|
+
const links = [];
|
|
6307
|
+
const report = scenarioUrl(config, tc);
|
|
6308
|
+
if (report) links.push({ label: "Living documentation", url: report });
|
|
6309
|
+
for (const ticket of tickets) {
|
|
6310
|
+
if (ticket.url) links.push({ label: ticket.id, url: ticket.url });
|
|
6294
6311
|
}
|
|
6295
|
-
|
|
6296
|
-
|
|
6297
|
-
|
|
6298
|
-
|
|
6312
|
+
return {
|
|
6313
|
+
title: tc.story.scenario,
|
|
6314
|
+
steps: tc.story.steps.map((s) => ({ keyword: s.keyword, text: s.text })),
|
|
6315
|
+
description: sections.join("\n\n"),
|
|
6316
|
+
links
|
|
6299
6317
|
};
|
|
6300
|
-
if (args.pageId) {
|
|
6301
|
-
return updatePage(args, base, headers, fetchFn);
|
|
6302
|
-
}
|
|
6303
|
-
return createPage(args, base, headers, fetchFn);
|
|
6304
6318
|
}
|
|
6305
|
-
|
|
6306
|
-
const
|
|
6307
|
-
|
|
6308
|
-
|
|
6309
|
-
|
|
6310
|
-
|
|
6311
|
-
|
|
6312
|
-
);
|
|
6313
|
-
}
|
|
6314
|
-
const current = await getResp.json();
|
|
6315
|
-
const nextVersion = current.version.number + 1;
|
|
6316
|
-
const title = args.title ?? current.title;
|
|
6317
|
-
const putUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
|
|
6318
|
-
const putResp = await fetchFn(putUrl, {
|
|
6319
|
-
method: "PUT",
|
|
6320
|
-
headers,
|
|
6321
|
-
body: JSON.stringify({
|
|
6322
|
-
id: args.pageId,
|
|
6323
|
-
status: "current",
|
|
6324
|
-
title,
|
|
6325
|
-
body: {
|
|
6326
|
-
representation: "atlas_doc_format",
|
|
6327
|
-
value: args.adf
|
|
6328
|
-
},
|
|
6329
|
-
version: { number: nextVersion }
|
|
6319
|
+
function projectBehaviours(run, config) {
|
|
6320
|
+
const fingerprints = run.testCases.map(
|
|
6321
|
+
(tc) => behaviourFingerprint2({
|
|
6322
|
+
scenario: tc.story.scenario,
|
|
6323
|
+
sourceFile: tc.sourceFile,
|
|
6324
|
+
steps: tc.story.steps.map((s) => ({ keyword: s.keyword, text: s.text })),
|
|
6325
|
+
covers: tc.story.covers
|
|
6330
6326
|
})
|
|
6331
|
-
|
|
6332
|
-
|
|
6333
|
-
|
|
6334
|
-
|
|
6335
|
-
`PUT ${putUrl} failed with ${putResp.status} ${putResp.statusText}${body ? `: ${body}` : ""}`
|
|
6336
|
-
);
|
|
6327
|
+
);
|
|
6328
|
+
const counts = /* @__PURE__ */ new Map();
|
|
6329
|
+
for (const fp of fingerprints) {
|
|
6330
|
+
if (fp) counts.set(fp, (counts.get(fp) ?? 0) + 1);
|
|
6337
6331
|
}
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6332
|
+
return run.testCases.map((tc, index) => {
|
|
6333
|
+
const fp = fingerprints[index];
|
|
6334
|
+
const unique = fp !== "" && counts.get(fp) === 1;
|
|
6335
|
+
return {
|
|
6336
|
+
fingerprint: unique ? fp : tc.id,
|
|
6337
|
+
testCase: tc,
|
|
6338
|
+
body: toCaseBody(tc, config)
|
|
6339
|
+
};
|
|
6340
|
+
});
|
|
6346
6341
|
}
|
|
6347
|
-
|
|
6348
|
-
const
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
|
|
6354
|
-
|
|
6342
|
+
function roleFor(attachment) {
|
|
6343
|
+
const type = attachment.mediaType.toLowerCase();
|
|
6344
|
+
if (type.startsWith("image/")) return "screenshot";
|
|
6345
|
+
if (type.startsWith("video/")) return "video";
|
|
6346
|
+
if (attachment.name.toLowerCase().includes("trace")) return "trace";
|
|
6347
|
+
return "log";
|
|
6348
|
+
}
|
|
6349
|
+
function decode(attachment) {
|
|
6350
|
+
return attachment.contentEncoding === "BASE64" ? Uint8Array.from(Buffer.from(attachment.body, "base64")) : new TextEncoder().encode(attachment.body);
|
|
6351
|
+
}
|
|
6352
|
+
function collectAttachments(args) {
|
|
6353
|
+
const { testCase, policy, maxBytes } = args;
|
|
6354
|
+
if (policy === "none") return { attachments: [], oversized: [] };
|
|
6355
|
+
if (policy === "failed" && testCase.status !== "failed") return { attachments: [], oversized: [] };
|
|
6356
|
+
const attachments = [];
|
|
6357
|
+
const oversized = [];
|
|
6358
|
+
for (const raw of testCase.attachments) {
|
|
6359
|
+
const body = decode(raw);
|
|
6360
|
+
if (maxBytes !== void 0 && body.byteLength > maxBytes) {
|
|
6361
|
+
oversized.push({ filename: raw.name, bytes: body.byteLength, limit: maxBytes });
|
|
6362
|
+
continue;
|
|
6355
6363
|
}
|
|
6356
|
-
|
|
6357
|
-
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
body: JSON.stringify(body)
|
|
6363
|
-
});
|
|
6364
|
-
if (!resp.ok) {
|
|
6365
|
-
const errBody = await parseErrorBody(resp);
|
|
6366
|
-
throw new Error(
|
|
6367
|
-
`POST ${postUrl} failed with ${resp.status} ${resp.statusText}${errBody ? `: ${errBody}` : ""}`
|
|
6368
|
-
);
|
|
6364
|
+
attachments.push({
|
|
6365
|
+
filename: raw.name,
|
|
6366
|
+
mediaType: raw.mediaType,
|
|
6367
|
+
body,
|
|
6368
|
+
role: roleFor(raw)
|
|
6369
|
+
});
|
|
6369
6370
|
}
|
|
6370
|
-
|
|
6371
|
+
return { attachments, oversized };
|
|
6372
|
+
}
|
|
6373
|
+
function toCaseResult(args) {
|
|
6374
|
+
const { behaviour, caseId, provider, config } = args;
|
|
6375
|
+
const tc = behaviour.testCase;
|
|
6376
|
+
if (tc.status === "pending") return void 0;
|
|
6377
|
+
const { attachments, oversized } = collectAttachments({
|
|
6378
|
+
testCase: tc,
|
|
6379
|
+
policy: config.attach ?? "failed",
|
|
6380
|
+
maxBytes: provider.maxAttachmentBytes
|
|
6381
|
+
});
|
|
6371
6382
|
return {
|
|
6372
|
-
|
|
6373
|
-
|
|
6374
|
-
|
|
6375
|
-
|
|
6376
|
-
|
|
6383
|
+
result: {
|
|
6384
|
+
caseId,
|
|
6385
|
+
status: tc.status,
|
|
6386
|
+
durationMs: tc.durationMs,
|
|
6387
|
+
message: tc.errorMessage,
|
|
6388
|
+
url: scenarioUrl(config, tc),
|
|
6389
|
+
attachments: attachments.length > 0 ? attachments : void 0
|
|
6390
|
+
},
|
|
6391
|
+
oversized
|
|
6377
6392
|
};
|
|
6378
6393
|
}
|
|
6379
|
-
function
|
|
6380
|
-
|
|
6381
|
-
|
|
6382
|
-
|
|
6383
|
-
return
|
|
6394
|
+
function ticketBinding(tc, config) {
|
|
6395
|
+
const prefix = config.ticketPrefix;
|
|
6396
|
+
if (!prefix) return void 0;
|
|
6397
|
+
const match = (tc.story.tickets ?? []).find((t) => t.id.startsWith(prefix));
|
|
6398
|
+
if (!match) return void 0;
|
|
6399
|
+
return config.ticketPrefixStrip === false ? match.id : match.id.slice(prefix.length);
|
|
6400
|
+
}
|
|
6401
|
+
async function analyzeSync(args) {
|
|
6402
|
+
const { run, provider, lockfile, config } = args;
|
|
6403
|
+
const local = projectBehaviours(run, config);
|
|
6404
|
+
const remoteCases = await provider.listCases();
|
|
6405
|
+
const remoteById = new Map(remoteCases.map((c) => [c.id, c]));
|
|
6406
|
+
const locked = entriesFor(lockfile, provider.name);
|
|
6407
|
+
const create = [];
|
|
6408
|
+
const update = [];
|
|
6409
|
+
const unchanged = [];
|
|
6410
|
+
const adopted = [];
|
|
6411
|
+
const skipped = [];
|
|
6412
|
+
const results = [];
|
|
6413
|
+
const oversized = [];
|
|
6414
|
+
const boundCaseIds = /* @__PURE__ */ new Set();
|
|
6415
|
+
let driftUncheckable = 0;
|
|
6416
|
+
for (const behaviour of local) {
|
|
6417
|
+
const entry = locked[behaviour.fingerprint];
|
|
6418
|
+
const caseId = entry?.caseId ?? ticketBinding(behaviour.testCase, config);
|
|
6419
|
+
const remote2 = caseId ? remoteById.get(caseId) : void 0;
|
|
6420
|
+
if (!caseId) {
|
|
6421
|
+
create.push({
|
|
6422
|
+
fingerprint: behaviour.fingerprint,
|
|
6423
|
+
scenario: behaviour.body.title,
|
|
6424
|
+
body: behaviour.body
|
|
6425
|
+
});
|
|
6426
|
+
continue;
|
|
6427
|
+
}
|
|
6428
|
+
if (!remote2) {
|
|
6429
|
+
skipped.push({
|
|
6430
|
+
fingerprint: behaviour.fingerprint,
|
|
6431
|
+
caseId,
|
|
6432
|
+
url: entry?.url ?? "",
|
|
6433
|
+
title: entry?.title ?? behaviour.body.title,
|
|
6434
|
+
reason: "case-missing"
|
|
6435
|
+
});
|
|
6436
|
+
continue;
|
|
6437
|
+
}
|
|
6438
|
+
boundCaseIds.add(caseId);
|
|
6439
|
+
const pending = toCaseResult({ behaviour, caseId, provider, config });
|
|
6440
|
+
if (pending) {
|
|
6441
|
+
results.push(pending.result);
|
|
6442
|
+
oversized.push(...pending.oversized);
|
|
6443
|
+
}
|
|
6444
|
+
const planned = {
|
|
6445
|
+
fingerprint: behaviour.fingerprint,
|
|
6446
|
+
caseId,
|
|
6447
|
+
url: remote2.url,
|
|
6448
|
+
scenario: behaviour.body.title,
|
|
6449
|
+
body: behaviour.body
|
|
6450
|
+
};
|
|
6451
|
+
if (!entry?.owned) {
|
|
6452
|
+
adopted.push(planned);
|
|
6453
|
+
continue;
|
|
6454
|
+
}
|
|
6455
|
+
const remoteHash = remote2.body ? hashCaseBody(remote2.body) : void 0;
|
|
6456
|
+
if (remoteHash !== void 0 && remoteHash !== entry.hash) {
|
|
6457
|
+
skipped.push({
|
|
6458
|
+
fingerprint: behaviour.fingerprint,
|
|
6459
|
+
caseId,
|
|
6460
|
+
url: remote2.url,
|
|
6461
|
+
title: remote2.title,
|
|
6462
|
+
reason: "remote-edited"
|
|
6463
|
+
});
|
|
6464
|
+
continue;
|
|
6465
|
+
}
|
|
6466
|
+
const baseline = remoteHash ?? entry.hash;
|
|
6467
|
+
if (baseline !== "" && hashCaseBody(behaviour.body) === baseline) {
|
|
6468
|
+
unchanged.push(planned);
|
|
6469
|
+
} else {
|
|
6470
|
+
update.push(planned);
|
|
6471
|
+
}
|
|
6472
|
+
if (remoteHash === void 0) driftUncheckable += 1;
|
|
6473
|
+
}
|
|
6474
|
+
const byNormalizedTitle = /* @__PURE__ */ new Map();
|
|
6475
|
+
for (const behaviour of local) byNormalizedTitle.set(normalizeTitle(behaviour.body.title), behaviour);
|
|
6476
|
+
const threshold = config.duplicateThreshold ?? DEFAULT_DUPLICATE_THRESHOLD;
|
|
6477
|
+
const remote = remoteCases.map((remoteCase) => {
|
|
6478
|
+
if (boundCaseIds.has(remoteCase.id)) {
|
|
6479
|
+
return { case: remoteCase, classification: "automated" };
|
|
6480
|
+
}
|
|
6481
|
+
const titleMatch = byNormalizedTitle.get(normalizeTitle(remoteCase.title));
|
|
6482
|
+
if (titleMatch) {
|
|
6483
|
+
return {
|
|
6484
|
+
case: remoteCase,
|
|
6485
|
+
classification: "duplicated",
|
|
6486
|
+
resembles: titleMatch.body.title
|
|
6487
|
+
};
|
|
6488
|
+
}
|
|
6489
|
+
if (remoteCase.body && remoteCase.body.steps.length > 0) {
|
|
6490
|
+
let best;
|
|
6491
|
+
for (const behaviour of local) {
|
|
6492
|
+
const score = behaviourSimilarity2(
|
|
6493
|
+
{
|
|
6494
|
+
scenario: remoteCase.title,
|
|
6495
|
+
sourceFile: "",
|
|
6496
|
+
steps: remoteCase.body.steps
|
|
6497
|
+
},
|
|
6498
|
+
{
|
|
6499
|
+
scenario: behaviour.body.title,
|
|
6500
|
+
sourceFile: behaviour.testCase.sourceFile,
|
|
6501
|
+
steps: behaviour.body.steps
|
|
6502
|
+
}
|
|
6503
|
+
);
|
|
6504
|
+
if (!best || score > best.score) best = { behaviour, score };
|
|
6505
|
+
}
|
|
6506
|
+
if (best && best.score >= threshold) {
|
|
6507
|
+
return {
|
|
6508
|
+
case: remoteCase,
|
|
6509
|
+
classification: "possible-duplicate",
|
|
6510
|
+
resembles: best.behaviour.body.title,
|
|
6511
|
+
similarity: Number(best.score.toFixed(2))
|
|
6512
|
+
};
|
|
6513
|
+
}
|
|
6514
|
+
}
|
|
6515
|
+
return { case: remoteCase, classification: "manual-only" };
|
|
6516
|
+
});
|
|
6517
|
+
const localFingerprints = new Set(local.map((b) => b.fingerprint));
|
|
6518
|
+
const orphaned = Object.entries(locked).filter(([fingerprint]) => !localFingerprints.has(fingerprint)).map(([fingerprint, entry]) => ({
|
|
6519
|
+
fingerprint,
|
|
6520
|
+
caseId: entry.caseId,
|
|
6521
|
+
url: entry.url,
|
|
6522
|
+
title: entry.title
|
|
6523
|
+
}));
|
|
6524
|
+
const lockedCount = Object.keys(locked).length;
|
|
6525
|
+
const partialRunWarning = lockedCount > 0 && orphaned.length / lockedCount > PARTIAL_RUN_ORPHAN_RATIO ? `${orphaned.length} of ${lockedCount} bindings have no matching story. If this run was filtered (a -t/--grep flag, a single file), those are not deletions. Nothing is removed either way.` : void 0;
|
|
6526
|
+
const unsupported = [];
|
|
6527
|
+
if (create.length > 0 && !provider.createCase) unsupported.push("createCase");
|
|
6528
|
+
if (update.length > 0 && !provider.updateCase) unsupported.push("updateCase");
|
|
6529
|
+
if (results.length > 0 && !provider.recordResults) unsupported.push("recordResults");
|
|
6530
|
+
const byRole = {};
|
|
6531
|
+
let files = 0;
|
|
6532
|
+
let bytes = 0;
|
|
6533
|
+
for (const result of results) {
|
|
6534
|
+
for (const attachment of result.attachments ?? []) {
|
|
6535
|
+
files += 1;
|
|
6536
|
+
bytes += attachment.body.byteLength;
|
|
6537
|
+
const role = attachment.role ?? "log";
|
|
6538
|
+
byRole[role] = (byRole[role] ?? 0) + 1;
|
|
6539
|
+
}
|
|
6540
|
+
}
|
|
6541
|
+
return {
|
|
6542
|
+
provider: provider.name,
|
|
6543
|
+
target: provider.describeTarget?.(),
|
|
6544
|
+
local,
|
|
6545
|
+
remote,
|
|
6546
|
+
create,
|
|
6547
|
+
update,
|
|
6548
|
+
unchanged,
|
|
6549
|
+
adopted,
|
|
6550
|
+
skipped,
|
|
6551
|
+
orphaned,
|
|
6552
|
+
results,
|
|
6553
|
+
attachments: { files, bytes, oversized, byRole },
|
|
6554
|
+
unsupported,
|
|
6555
|
+
driftUncheckable,
|
|
6556
|
+
partialRunWarning
|
|
6557
|
+
};
|
|
6558
|
+
}
|
|
6559
|
+
async function applySync(args, deps) {
|
|
6560
|
+
const { analysis, provider, lockfile, config } = args;
|
|
6561
|
+
const result = {
|
|
6562
|
+
created: [],
|
|
6563
|
+
updated: [],
|
|
6564
|
+
resultsRecorded: 0,
|
|
6565
|
+
resultsSkipped: [],
|
|
6566
|
+
attachmentsUploaded: 0,
|
|
6567
|
+
errors: []
|
|
6568
|
+
};
|
|
6569
|
+
const byFingerprint = new Map(analysis.local.map((b) => [b.fingerprint, b]));
|
|
6570
|
+
const results = [...analysis.results];
|
|
6571
|
+
if (analysis.create.length > 0 && provider.createCase) {
|
|
6572
|
+
for (const planned of analysis.create) {
|
|
6573
|
+
try {
|
|
6574
|
+
const created = await provider.createCase(planned.body);
|
|
6575
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
6576
|
+
caseId: created.id,
|
|
6577
|
+
url: created.url,
|
|
6578
|
+
hash: hashCaseBody(created.body ?? planned.body),
|
|
6579
|
+
title: created.title,
|
|
6580
|
+
owned: true
|
|
6581
|
+
});
|
|
6582
|
+
result.created.push({ scenario: planned.scenario, caseId: created.id, url: created.url });
|
|
6583
|
+
const behaviour = byFingerprint.get(planned.fingerprint);
|
|
6584
|
+
if (behaviour) {
|
|
6585
|
+
const pending = toCaseResult({ behaviour, caseId: created.id, provider, config });
|
|
6586
|
+
if (pending) results.push(pending.result);
|
|
6587
|
+
}
|
|
6588
|
+
} catch (err) {
|
|
6589
|
+
result.errors.push(`create "${planned.scenario}": ${err.message}`);
|
|
6590
|
+
}
|
|
6591
|
+
}
|
|
6592
|
+
}
|
|
6593
|
+
if (analysis.update.length > 0 && provider.updateCase) {
|
|
6594
|
+
for (const planned of analysis.update) {
|
|
6595
|
+
try {
|
|
6596
|
+
const updated = await provider.updateCase(planned.caseId, planned.body);
|
|
6597
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
6598
|
+
caseId: updated.id,
|
|
6599
|
+
url: updated.url,
|
|
6600
|
+
hash: hashCaseBody(updated.body ?? planned.body),
|
|
6601
|
+
title: updated.title,
|
|
6602
|
+
owned: true
|
|
6603
|
+
});
|
|
6604
|
+
result.updated.push({ scenario: planned.scenario, caseId: updated.id, url: updated.url });
|
|
6605
|
+
} catch (err) {
|
|
6606
|
+
result.errors.push(`update "${planned.scenario}" (${planned.caseId}): ${err.message}`);
|
|
6607
|
+
}
|
|
6608
|
+
}
|
|
6609
|
+
}
|
|
6610
|
+
for (const planned of analysis.unchanged) {
|
|
6611
|
+
const existing = entriesFor(lockfile, provider.name)[planned.fingerprint];
|
|
6612
|
+
if (existing) {
|
|
6613
|
+
setEntry(lockfile, provider.name, planned.fingerprint, { ...existing, url: planned.url });
|
|
6614
|
+
}
|
|
6615
|
+
}
|
|
6616
|
+
for (const planned of analysis.adopted) {
|
|
6617
|
+
setEntry(lockfile, provider.name, planned.fingerprint, {
|
|
6618
|
+
caseId: planned.caseId,
|
|
6619
|
+
url: planned.url,
|
|
6620
|
+
hash: "",
|
|
6621
|
+
title: planned.scenario,
|
|
6622
|
+
owned: false
|
|
6623
|
+
});
|
|
6624
|
+
}
|
|
6625
|
+
if (results.length > 0 && provider.recordResults) {
|
|
6626
|
+
try {
|
|
6627
|
+
const summary = await provider.recordResults(results);
|
|
6628
|
+
result.resultsRecorded = summary.recorded;
|
|
6629
|
+
result.resultsSkipped = summary.skipped;
|
|
6630
|
+
result.attachmentsUploaded = summary.attachmentsUploaded;
|
|
6631
|
+
result.runUrl = summary.runUrl;
|
|
6632
|
+
} catch (err) {
|
|
6633
|
+
result.errors.push(`record results: ${err.message}`);
|
|
6634
|
+
}
|
|
6635
|
+
} else if (results.length > 0) {
|
|
6636
|
+
deps.logger.warn(
|
|
6637
|
+
`${provider.name} does not support recording results \u2014 ${results.length} execution(s) not pushed.`
|
|
6638
|
+
);
|
|
6639
|
+
}
|
|
6640
|
+
return result;
|
|
6641
|
+
}
|
|
6642
|
+
|
|
6643
|
+
// src/sync/report.ts
|
|
6644
|
+
var MANUAL_ONLY_LIMIT = 50;
|
|
6645
|
+
function summarize2(analysis) {
|
|
6646
|
+
const count2 = (kind) => analysis.remote.filter((c) => c.classification === kind).length;
|
|
6647
|
+
return {
|
|
6648
|
+
provider: analysis.provider,
|
|
6649
|
+
target: analysis.target,
|
|
6650
|
+
totalCases: analysis.remote.length,
|
|
6651
|
+
automated: count2("automated"),
|
|
6652
|
+
duplicated: count2("duplicated"),
|
|
6653
|
+
possibleDuplicate: count2("possible-duplicate"),
|
|
6654
|
+
manualOnly: count2("manual-only"),
|
|
6655
|
+
untracked: analysis.create.length,
|
|
6656
|
+
adopted: analysis.adopted.length
|
|
6657
|
+
};
|
|
6658
|
+
}
|
|
6659
|
+
function sectionBreakdown(analysis) {
|
|
6660
|
+
const sections = /* @__PURE__ */ new Map();
|
|
6661
|
+
for (const entry of analysis.remote) {
|
|
6662
|
+
const name = entry.case.section ?? "(no section)";
|
|
6663
|
+
const bucket2 = sections.get(name) ?? { total: 0, automated: 0 };
|
|
6664
|
+
bucket2.total += 1;
|
|
6665
|
+
if (entry.classification === "automated") bucket2.automated += 1;
|
|
6666
|
+
sections.set(name, bucket2);
|
|
6667
|
+
}
|
|
6668
|
+
return [...sections.entries()].map(([name, value]) => ({ name, ...value })).sort((a, b) => b.automated - a.automated || b.total - a.total);
|
|
6669
|
+
}
|
|
6670
|
+
function renderCoverageText(analysis) {
|
|
6671
|
+
const summary = summarize2(analysis);
|
|
6672
|
+
const lines = [];
|
|
6673
|
+
lines.push(`${analysis.provider}: ${analysis.target ?? "(target not described)"} (${summary.totalCases} cases)`);
|
|
6674
|
+
lines.push("");
|
|
6675
|
+
lines.push(` ${pad(summary.automated)} automated already covered by a story`);
|
|
6676
|
+
lines.push(` ${pad(summary.duplicated)} duplicated manual case duplicates an automated story`);
|
|
6677
|
+
if (summary.possibleDuplicate > 0) {
|
|
6678
|
+
lines.push(` ${pad(summary.possibleDuplicate)} possible dupe similar to a story, needs a human to confirm`);
|
|
6679
|
+
}
|
|
6680
|
+
lines.push(` ${pad(summary.manualOnly)} manual only no automated equivalent`);
|
|
6681
|
+
lines.push(` ${pad(summary.untracked)} untracked story with no case`);
|
|
6682
|
+
if (summary.adopted > 0) {
|
|
6683
|
+
lines.push(` ${pad(summary.adopted)} linked story bound to a hand-authored case`);
|
|
6684
|
+
}
|
|
6685
|
+
const sections = sectionBreakdown(analysis).filter((s) => s.automated > 0);
|
|
6686
|
+
if (sections.length > 0) {
|
|
6687
|
+
const top = sections[0];
|
|
6688
|
+
lines.push("");
|
|
6689
|
+
lines.push(`Biggest overlap: "${top.name}" section, ${top.automated} of ${top.total} automated.`);
|
|
6690
|
+
}
|
|
6691
|
+
if (analysis.orphaned.length > 0) {
|
|
6692
|
+
lines.push("");
|
|
6693
|
+
lines.push(`${analysis.orphaned.length} case(s) bound to a story that no longer exists. Nothing was removed.`);
|
|
6694
|
+
}
|
|
6695
|
+
if (analysis.partialRunWarning) {
|
|
6696
|
+
lines.push("");
|
|
6697
|
+
lines.push(`Note: ${analysis.partialRunWarning}`);
|
|
6698
|
+
}
|
|
6699
|
+
return lines.join("\n");
|
|
6700
|
+
}
|
|
6701
|
+
function pad(value) {
|
|
6702
|
+
return String(value).padStart(4, " ");
|
|
6703
|
+
}
|
|
6704
|
+
function renderCoverageMarkdown(analysis) {
|
|
6705
|
+
const summary = summarize2(analysis);
|
|
6706
|
+
const lines = [];
|
|
6707
|
+
lines.push(`# Test coverage vs ${analysis.provider}`);
|
|
6708
|
+
lines.push("");
|
|
6709
|
+
if (analysis.target) lines.push(`**Target:** ${analysis.target}`, "");
|
|
6710
|
+
lines.push("| Cases | Count | Meaning |");
|
|
6711
|
+
lines.push("| --- | ---: | --- |");
|
|
6712
|
+
lines.push(`| Automated | ${summary.automated} | Already covered by a story |`);
|
|
6713
|
+
lines.push(`| Duplicated | ${summary.duplicated} | Manual case duplicates an automated story |`);
|
|
6714
|
+
lines.push(`| Possible duplicate | ${summary.possibleDuplicate} | Similar to a story, needs review |`);
|
|
6715
|
+
lines.push(`| Manual only | ${summary.manualOnly} | No automated equivalent |`);
|
|
6716
|
+
lines.push(`| Untracked stories | ${summary.untracked} | Story with no case |`);
|
|
6717
|
+
lines.push("");
|
|
6718
|
+
const duplicates = analysis.remote.filter(
|
|
6719
|
+
(c) => c.classification === "duplicated" || c.classification === "possible-duplicate"
|
|
6720
|
+
);
|
|
6721
|
+
if (duplicates.length > 0) {
|
|
6722
|
+
lines.push("## Retire these first");
|
|
6723
|
+
lines.push("");
|
|
6724
|
+
lines.push("Manual cases an automated story already covers.");
|
|
6725
|
+
lines.push("");
|
|
6726
|
+
lines.push("| Case | Title | Covered by | Confidence |");
|
|
6727
|
+
lines.push("| --- | --- | --- | --- |");
|
|
6728
|
+
for (const entry of duplicates) {
|
|
6729
|
+
const confidence = entry.classification === "duplicated" ? "exact title" : `similarity ${entry.similarity}`;
|
|
6730
|
+
lines.push(
|
|
6731
|
+
`| [${entry.case.id}](${entry.case.url}) | ${escapeCell2(entry.case.title)} | ${escapeCell2(entry.resembles ?? "")} | ${confidence} |`
|
|
6732
|
+
);
|
|
6733
|
+
}
|
|
6734
|
+
lines.push("");
|
|
6735
|
+
}
|
|
6736
|
+
const manualOnly = analysis.remote.filter((c) => c.classification === "manual-only");
|
|
6737
|
+
if (manualOnly.length > 0) {
|
|
6738
|
+
lines.push("## Not automated yet");
|
|
6739
|
+
lines.push("");
|
|
6740
|
+
lines.push("Cases with no automated equivalent. This is the backlog.");
|
|
6741
|
+
lines.push("");
|
|
6742
|
+
lines.push("| Case | Title | Section |");
|
|
6743
|
+
lines.push("| --- | --- | --- |");
|
|
6744
|
+
for (const entry of manualOnly.slice(0, MANUAL_ONLY_LIMIT)) {
|
|
6745
|
+
lines.push(
|
|
6746
|
+
`| [${entry.case.id}](${entry.case.url}) | ${escapeCell2(entry.case.title)} | ${escapeCell2(entry.case.section ?? "")} |`
|
|
6747
|
+
);
|
|
6748
|
+
}
|
|
6749
|
+
if (manualOnly.length > MANUAL_ONLY_LIMIT) {
|
|
6750
|
+
lines.push("");
|
|
6751
|
+
lines.push(
|
|
6752
|
+
`_${manualOnly.length - MANUAL_ONLY_LIMIT} more not listed here. The JSON artifact has all ${manualOnly.length}._`
|
|
6753
|
+
);
|
|
6754
|
+
}
|
|
6755
|
+
lines.push("");
|
|
6756
|
+
}
|
|
6757
|
+
if (analysis.create.length > 0) {
|
|
6758
|
+
lines.push("## Stories with no case");
|
|
6759
|
+
lines.push("");
|
|
6760
|
+
for (const planned of analysis.create) lines.push(`- ${planned.scenario}`);
|
|
6761
|
+
lines.push("");
|
|
6762
|
+
}
|
|
6763
|
+
if (analysis.orphaned.length > 0) {
|
|
6764
|
+
lines.push("## Cases with no story");
|
|
6765
|
+
lines.push("");
|
|
6766
|
+
lines.push("Bound to a story that has since been deleted. Nothing was removed automatically.");
|
|
6767
|
+
lines.push("");
|
|
6768
|
+
for (const orphan of analysis.orphaned) {
|
|
6769
|
+
lines.push(`- [${orphan.caseId}](${orphan.url}) ${escapeCell2(orphan.title)}`);
|
|
6770
|
+
}
|
|
6771
|
+
lines.push("");
|
|
6772
|
+
}
|
|
6773
|
+
const sections = sectionBreakdown(analysis);
|
|
6774
|
+
if (sections.length > 1) {
|
|
6775
|
+
lines.push("## By section");
|
|
6776
|
+
lines.push("");
|
|
6777
|
+
lines.push("| Section | Automated | Total |");
|
|
6778
|
+
lines.push("| --- | ---: | ---: |");
|
|
6779
|
+
for (const section of sections) {
|
|
6780
|
+
lines.push(`| ${escapeCell2(section.name)} | ${section.automated} | ${section.total} |`);
|
|
6781
|
+
}
|
|
6782
|
+
lines.push("");
|
|
6783
|
+
}
|
|
6784
|
+
if (analysis.partialRunWarning) {
|
|
6785
|
+
lines.push(`> ${analysis.partialRunWarning}`, "");
|
|
6786
|
+
}
|
|
6787
|
+
return lines.join("\n");
|
|
6788
|
+
}
|
|
6789
|
+
function escapeCell2(text2) {
|
|
6790
|
+
return text2.replace(/\|/g, "\\|");
|
|
6791
|
+
}
|
|
6792
|
+
function buildCoverageJson(analysis) {
|
|
6793
|
+
return {
|
|
6794
|
+
schema: "executable-stories/sync-coverage/v1",
|
|
6795
|
+
...summarize2(analysis),
|
|
6796
|
+
cases: analysis.remote.map((entry) => ({
|
|
6797
|
+
id: entry.case.id,
|
|
6798
|
+
url: entry.case.url,
|
|
6799
|
+
title: entry.case.title,
|
|
6800
|
+
section: entry.case.section,
|
|
6801
|
+
classification: entry.classification,
|
|
6802
|
+
resembles: entry.resembles,
|
|
6803
|
+
similarity: entry.similarity
|
|
6804
|
+
})),
|
|
6805
|
+
untrackedScenarios: analysis.create.map((c) => c.scenario),
|
|
6806
|
+
orphaned: analysis.orphaned.map((o) => ({ caseId: o.caseId, url: o.url, title: o.title })),
|
|
6807
|
+
sections: sectionBreakdown(analysis)
|
|
6808
|
+
};
|
|
6809
|
+
}
|
|
6810
|
+
function formatBytes(bytes) {
|
|
6811
|
+
if (bytes < 1024) return `${bytes} B`;
|
|
6812
|
+
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
|
|
6813
|
+
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
|
|
6814
|
+
}
|
|
6815
|
+
function renderPlan(analysis, opts) {
|
|
6816
|
+
const lines = [];
|
|
6817
|
+
lines.push(`${analysis.provider}: ${analysis.target ?? "(target not described)"}`);
|
|
6818
|
+
lines.push("");
|
|
6819
|
+
lines.push(` + create ${pad(analysis.create.length)} cases`);
|
|
6820
|
+
lines.push(` ~ update ${pad(analysis.update.length)} cases`);
|
|
6821
|
+
lines.push(` = unchanged ${pad(analysis.unchanged.length)} cases`);
|
|
6822
|
+
if (analysis.adopted.length > 0) {
|
|
6823
|
+
lines.push(` \xB7 linked ${pad(analysis.adopted.length)} cases (hand-authored, results only)`);
|
|
6824
|
+
}
|
|
6825
|
+
if (analysis.skipped.length > 0) {
|
|
6826
|
+
const edited = analysis.skipped.filter((s) => s.reason === "remote-edited").length;
|
|
6827
|
+
const missing = analysis.skipped.filter((s) => s.reason === "case-missing").length;
|
|
6828
|
+
if (edited > 0) {
|
|
6829
|
+
lines.push(` ! skipped ${pad(edited)} cases (edited in ${analysis.provider} since last sync)`);
|
|
6830
|
+
}
|
|
6831
|
+
if (missing > 0) {
|
|
6832
|
+
lines.push(` ! skipped ${pad(missing)} cases (bound case no longer exists)`);
|
|
6833
|
+
}
|
|
6834
|
+
}
|
|
6835
|
+
if (analysis.orphaned.length > 0) {
|
|
6836
|
+
lines.push(` ? orphaned ${pad(analysis.orphaned.length)} cases (story deleted from codebase, never removed)`);
|
|
6837
|
+
}
|
|
6838
|
+
lines.push(` \u2192 results ${pad(analysis.results.length)} executions`);
|
|
6839
|
+
if (analysis.attachments.files > 0) {
|
|
6840
|
+
const roles = Object.entries(analysis.attachments.byRole).map(([role, count2]) => `${count2} ${role}${count2 === 1 ? "" : "s"}`).join(", ");
|
|
6841
|
+
lines.push(
|
|
6842
|
+
` \u2191 upload ${pad(analysis.attachments.files)} attachments (${roles}, ${formatBytes(analysis.attachments.bytes)})`
|
|
6843
|
+
);
|
|
6844
|
+
}
|
|
6845
|
+
for (const oversized of analysis.attachments.oversized) {
|
|
6846
|
+
lines.push(
|
|
6847
|
+
` ! oversized ${oversized.filename} (${formatBytes(oversized.bytes)}, provider limit ${formatBytes(oversized.limit)})`
|
|
6848
|
+
);
|
|
6849
|
+
}
|
|
6850
|
+
for (const capability of analysis.unsupported) {
|
|
6851
|
+
lines.push(` ! ${analysis.provider} does not support ${capability} \u2014 those changes are not applied`);
|
|
6852
|
+
}
|
|
6853
|
+
if (analysis.driftUncheckable > 0) {
|
|
6854
|
+
lines.push(
|
|
6855
|
+
` ! ${analysis.driftUncheckable} case(s): ${analysis.provider} returned no body, so a hand edit to them cannot be detected`
|
|
6856
|
+
);
|
|
6857
|
+
}
|
|
6858
|
+
if (analysis.partialRunWarning) {
|
|
6859
|
+
lines.push("");
|
|
6860
|
+
lines.push(`Note: ${analysis.partialRunWarning}`);
|
|
6861
|
+
}
|
|
6862
|
+
if (analysis.skipped.some((s) => s.reason === "remote-edited")) {
|
|
6863
|
+
lines.push("");
|
|
6864
|
+
lines.push("Skipped cases were edited by hand after we last wrote them. Nothing overwrites them.");
|
|
6865
|
+
for (const skip of analysis.skipped.filter((s) => s.reason === "remote-edited")) {
|
|
6866
|
+
lines.push(` ${skip.caseId} ${skip.title} ${skip.url}`);
|
|
6867
|
+
}
|
|
6868
|
+
}
|
|
6869
|
+
if (opts.dryRun && hasWork(analysis)) {
|
|
6870
|
+
lines.push("");
|
|
6871
|
+
lines.push("Nothing was written. Run the same command with --apply to make these changes.");
|
|
6872
|
+
}
|
|
6873
|
+
return lines.join("\n");
|
|
6874
|
+
}
|
|
6875
|
+
function hasWork(analysis) {
|
|
6876
|
+
return analysis.create.length > 0 || analysis.update.length > 0 || analysis.results.length > 0;
|
|
6877
|
+
}
|
|
6878
|
+
function renderApplyResult(result) {
|
|
6879
|
+
const lines = [];
|
|
6880
|
+
for (const created of result.created) {
|
|
6881
|
+
lines.push(` + ${created.caseId} ${created.scenario} ${created.url}`);
|
|
6882
|
+
}
|
|
6883
|
+
for (const updated of result.updated) {
|
|
6884
|
+
lines.push(` ~ ${updated.caseId} ${updated.scenario}`);
|
|
6885
|
+
}
|
|
6886
|
+
lines.push("");
|
|
6887
|
+
lines.push(
|
|
6888
|
+
`Created ${result.created.length}, updated ${result.updated.length}, recorded ${result.resultsRecorded} execution(s), uploaded ${result.attachmentsUploaded} attachment(s).`
|
|
6889
|
+
);
|
|
6890
|
+
if (result.runUrl) lines.push(`Run: ${result.runUrl}`);
|
|
6891
|
+
for (const skipped of result.resultsSkipped) {
|
|
6892
|
+
lines.push(` ! result for case ${skipped.caseId} not recorded: ${skipped.reason}`);
|
|
6893
|
+
}
|
|
6894
|
+
for (const error of result.errors) {
|
|
6895
|
+
lines.push(` \u2717 ${error}`);
|
|
6896
|
+
}
|
|
6897
|
+
return lines.join("\n");
|
|
6898
|
+
}
|
|
6899
|
+
|
|
6900
|
+
// src/sync/adapters/case-text.ts
|
|
6901
|
+
var BDD_KEYWORDS = ["Given", "When", "Then", "And", "But"];
|
|
6902
|
+
function encodeStepText(step) {
|
|
6903
|
+
const keyword = step.keyword.trim();
|
|
6904
|
+
return keyword ? `${keyword} ${step.text}` : step.text;
|
|
6905
|
+
}
|
|
6906
|
+
function decodeStepText(content) {
|
|
6907
|
+
const trimmed = content.trim();
|
|
6908
|
+
const firstSpace = trimmed.indexOf(" ");
|
|
6909
|
+
if (firstSpace > 0) {
|
|
6910
|
+
const head = trimmed.slice(0, firstSpace);
|
|
6911
|
+
if (BDD_KEYWORDS.some((keyword) => keyword.toLowerCase() === head.toLowerCase())) {
|
|
6912
|
+
return { keyword: head, text: trimmed.slice(firstSpace + 1) };
|
|
6913
|
+
}
|
|
6914
|
+
}
|
|
6915
|
+
return { keyword: "", text: trimmed };
|
|
6916
|
+
}
|
|
6917
|
+
function encodeDescription(body) {
|
|
6918
|
+
if (body.links.length === 0) return body.description;
|
|
6919
|
+
const links = body.links.map((link2) => `- [${link2.label}](${link2.url})`).join("\n");
|
|
6920
|
+
return `${body.description}
|
|
6921
|
+
|
|
6922
|
+
${links}`;
|
|
6923
|
+
}
|
|
6924
|
+
function decodeDescription(raw) {
|
|
6925
|
+
const links = [];
|
|
6926
|
+
const lines = raw.split("\n");
|
|
6927
|
+
let cut = lines.length;
|
|
6928
|
+
for (let index = lines.length - 1; index >= 0; index--) {
|
|
6929
|
+
const line = lines[index].trim();
|
|
6930
|
+
if (line === "") continue;
|
|
6931
|
+
const match = /^- \[([^\]]+)\]\(([^)]+)\)$/.exec(line);
|
|
6932
|
+
if (!match) break;
|
|
6933
|
+
links.unshift({ label: match[1], url: match[2] });
|
|
6934
|
+
cut = index;
|
|
6935
|
+
}
|
|
6936
|
+
return { description: lines.slice(0, cut).join("\n").trim(), links };
|
|
6937
|
+
}
|
|
6938
|
+
|
|
6939
|
+
// src/sync/adapters/testrail.ts
|
|
6940
|
+
var DEFAULT_STEPS_FIELD = "custom_steps_separated";
|
|
6941
|
+
var DEFAULT_DESCRIPTION_FIELD = "custom_preconds";
|
|
6942
|
+
var DEFAULT_MAX_ATTACHMENT_BYTES = 64 * 1024 * 1024;
|
|
6943
|
+
var PAGE_LIMIT = 250;
|
|
6944
|
+
var MAX_ATTEMPTS = 3;
|
|
6945
|
+
function unwrapList(payload, key) {
|
|
6946
|
+
if (Array.isArray(payload)) return payload;
|
|
6947
|
+
if (payload && typeof payload === "object") {
|
|
6948
|
+
const list = payload[key];
|
|
6949
|
+
if (Array.isArray(list)) return list;
|
|
6950
|
+
}
|
|
6951
|
+
return [];
|
|
6952
|
+
}
|
|
6953
|
+
function encodeElapsed(durationMs) {
|
|
6954
|
+
const seconds = Math.floor(durationMs / 1e3);
|
|
6955
|
+
if (seconds < 1) return void 0;
|
|
6956
|
+
if (seconds < 60) return `${seconds}s`;
|
|
6957
|
+
const minutes = Math.floor(seconds / 60);
|
|
6958
|
+
const rest = seconds % 60;
|
|
6959
|
+
return rest === 0 ? `${minutes}m` : `${minutes}m ${rest}s`;
|
|
6960
|
+
}
|
|
6961
|
+
function authHint(status) {
|
|
6962
|
+
if (status === 401) {
|
|
6963
|
+
return "\n TESTRAIL_USERNAME must be the login email and TESTRAIL_API_KEY an API key from My Settings -> API Keys. A password fails here when the instance enforces API keys.";
|
|
6964
|
+
}
|
|
6965
|
+
if (status === 403) {
|
|
6966
|
+
return "\n The credentials were accepted but the request was refused. Usually the API is disabled: an admin enables it under Administration -> Site Settings -> API.";
|
|
6967
|
+
}
|
|
6968
|
+
return "";
|
|
6969
|
+
}
|
|
6970
|
+
function createTestRailProvider(config, auth, deps) {
|
|
6971
|
+
const base = config.url.replace(/\/$/, "");
|
|
6972
|
+
const stepsField = config.fields?.steps ?? DEFAULT_STEPS_FIELD;
|
|
6973
|
+
const descriptionField = config.fields?.description ?? DEFAULT_DESCRIPTION_FIELD;
|
|
6974
|
+
const basicAuth = Buffer.from(`${auth.username}:${auth.apiKey}`).toString("base64");
|
|
6975
|
+
let projectName;
|
|
6976
|
+
let suiteName;
|
|
6977
|
+
const sectionNames = /* @__PURE__ */ new Map();
|
|
6978
|
+
async function api(method, init) {
|
|
6979
|
+
const url = `${base}/index.php?/api/v2/${method}`;
|
|
6980
|
+
const headers = {
|
|
6981
|
+
Authorization: `Basic ${basicAuth}`
|
|
6982
|
+
};
|
|
6983
|
+
let body;
|
|
6984
|
+
if (init?.form) {
|
|
6985
|
+
body = init.form;
|
|
6986
|
+
} else if (init?.body !== void 0) {
|
|
6987
|
+
headers["Content-Type"] = "application/json";
|
|
6988
|
+
body = JSON.stringify(init.body);
|
|
6989
|
+
}
|
|
6990
|
+
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
6991
|
+
const response = await deps.fetch(url, {
|
|
6992
|
+
method: body === void 0 ? "GET" : "POST",
|
|
6993
|
+
headers,
|
|
6994
|
+
body
|
|
6995
|
+
});
|
|
6996
|
+
if (response.status === 429 && attempt < MAX_ATTEMPTS - 1) {
|
|
6997
|
+
const retryAfter = Number(response.headers.get("retry-after") ?? "1");
|
|
6998
|
+
deps.logger.warn(`TestRail rate limit hit, retrying in ${retryAfter}s`);
|
|
6999
|
+
await new Promise((resolve12) => setTimeout(resolve12, Math.max(1, retryAfter) * 1e3));
|
|
7000
|
+
continue;
|
|
7001
|
+
}
|
|
7002
|
+
const text2 = await response.text();
|
|
7003
|
+
if (text2.length === 0) {
|
|
7004
|
+
if (response.ok) return void 0;
|
|
7005
|
+
throw new Error(
|
|
7006
|
+
`TestRail ${method} failed (${response.status}) with an empty response${authHint(response.status)}`
|
|
7007
|
+
);
|
|
7008
|
+
}
|
|
7009
|
+
let parsed;
|
|
7010
|
+
try {
|
|
7011
|
+
parsed = JSON.parse(text2);
|
|
7012
|
+
} catch {
|
|
7013
|
+
throw new Error(
|
|
7014
|
+
`TestRail ${method} returned HTML rather than JSON (status ${response.status}).
|
|
7015
|
+
Check that sync.testrail.url is the instance root, e.g. https://acme.testrail.io, with no path after it.`
|
|
7016
|
+
);
|
|
7017
|
+
}
|
|
7018
|
+
if (!response.ok) {
|
|
7019
|
+
const detail = parsed.error ?? text2;
|
|
7020
|
+
throw new Error(
|
|
7021
|
+
`TestRail ${method} failed (${response.status}): ${detail}${authHint(response.status)}`
|
|
7022
|
+
);
|
|
7023
|
+
}
|
|
7024
|
+
return parsed;
|
|
7025
|
+
}
|
|
7026
|
+
throw new Error(`TestRail ${method} failed: rate limited after ${MAX_ATTEMPTS} attempts`);
|
|
7027
|
+
}
|
|
7028
|
+
async function paginate(method, key) {
|
|
7029
|
+
const all = [];
|
|
7030
|
+
for (let offset = 0; ; offset += PAGE_LIMIT) {
|
|
7031
|
+
const page = await api(`${method}&limit=${PAGE_LIMIT}&offset=${offset}`);
|
|
7032
|
+
const items = unwrapList(page, key);
|
|
7033
|
+
all.push(...items);
|
|
7034
|
+
if (items.length < PAGE_LIMIT) return all;
|
|
7035
|
+
}
|
|
7036
|
+
}
|
|
7037
|
+
function caseUrl(id) {
|
|
7038
|
+
return `${base}/index.php?/cases/view/${id}`;
|
|
7039
|
+
}
|
|
7040
|
+
function toRemoteCase(raw, context) {
|
|
7041
|
+
if (raw?.id === void 0) {
|
|
7042
|
+
throw new Error(
|
|
7043
|
+
`TestRail ${context} returned no case id. The response was: ${JSON.stringify(raw)?.slice(0, 200)}`
|
|
7044
|
+
);
|
|
7045
|
+
}
|
|
7046
|
+
const title = typeof raw.title === "string" ? raw.title : "";
|
|
7047
|
+
const steps = Array.isArray(raw[stepsField]) ? raw[stepsField] : [];
|
|
7048
|
+
const rawDescription = typeof raw[descriptionField] === "string" ? raw[descriptionField] : "";
|
|
7049
|
+
const { description, links } = decodeDescription(rawDescription);
|
|
7050
|
+
return {
|
|
7051
|
+
id: String(raw.id),
|
|
7052
|
+
url: caseUrl(raw.id),
|
|
7053
|
+
title,
|
|
7054
|
+
section: raw.section_id === void 0 ? void 0 : sectionNames.get(raw.section_id),
|
|
7055
|
+
body: {
|
|
7056
|
+
title,
|
|
7057
|
+
steps: steps.map((step) => decodeStepText(step.content ?? "")),
|
|
7058
|
+
description,
|
|
7059
|
+
links
|
|
7060
|
+
}
|
|
7061
|
+
};
|
|
7062
|
+
}
|
|
7063
|
+
function bodyToPayload(body) {
|
|
7064
|
+
const payload = {
|
|
7065
|
+
title: body.title,
|
|
7066
|
+
[stepsField]: body.steps.map((step) => ({ content: encodeStepText(step), expected: "" })),
|
|
7067
|
+
[descriptionField]: encodeDescription(body)
|
|
7068
|
+
};
|
|
7069
|
+
if (config.templateId !== void 0) payload["template_id"] = config.templateId;
|
|
7070
|
+
return payload;
|
|
7071
|
+
}
|
|
7072
|
+
const suiteQuery = config.suiteId === void 0 ? "" : `&suite_id=${config.suiteId}`;
|
|
7073
|
+
return {
|
|
7074
|
+
name: "testrail",
|
|
7075
|
+
maxAttachmentBytes: config.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES,
|
|
7076
|
+
describeTarget() {
|
|
7077
|
+
const project = projectName ?? `project ${config.projectId}`;
|
|
7078
|
+
const suite = suiteName ?? (config.suiteId === void 0 ? void 0 : `suite ${config.suiteId}`);
|
|
7079
|
+
return suite ? `${project} / ${suite}` : project;
|
|
7080
|
+
},
|
|
7081
|
+
async listCases() {
|
|
7082
|
+
try {
|
|
7083
|
+
const project = await api(`get_project/${config.projectId}`);
|
|
7084
|
+
projectName = project?.name;
|
|
7085
|
+
if (config.suiteId !== void 0) {
|
|
7086
|
+
const suite = await api(`get_suite/${config.suiteId}`);
|
|
7087
|
+
suiteName = suite?.name;
|
|
7088
|
+
}
|
|
7089
|
+
const sections = await paginate(
|
|
7090
|
+
`get_sections/${config.projectId}${suiteQuery}`,
|
|
7091
|
+
"sections"
|
|
7092
|
+
);
|
|
7093
|
+
for (const section of sections) sectionNames.set(section.id, section.name);
|
|
7094
|
+
} catch (err) {
|
|
7095
|
+
deps.logger.warn(`TestRail metadata lookup failed, continuing without names: ${err.message}`);
|
|
7096
|
+
}
|
|
7097
|
+
const cases = await paginate(`get_cases/${config.projectId}${suiteQuery}`, "cases");
|
|
7098
|
+
return cases.map((raw) => toRemoteCase(raw, "get_cases"));
|
|
7099
|
+
},
|
|
7100
|
+
async createCase(body) {
|
|
7101
|
+
if (config.sectionId === void 0) {
|
|
7102
|
+
throw new Error(
|
|
7103
|
+
"TestRail needs a sectionId to create cases. Set sync.testrail.sectionId to the section new cases should land in."
|
|
7104
|
+
);
|
|
7105
|
+
}
|
|
7106
|
+
const created = await api(`add_case/${config.sectionId}`, {
|
|
7107
|
+
body: bodyToPayload(body)
|
|
7108
|
+
});
|
|
7109
|
+
return toRemoteCase(created, `add_case/${config.sectionId}`);
|
|
7110
|
+
},
|
|
7111
|
+
async updateCase(id, body) {
|
|
7112
|
+
const updated = await api(`update_case/${id}`, { body: bodyToPayload(body) });
|
|
7113
|
+
return toRemoteCase(updated, `update_case/${id}`);
|
|
7114
|
+
},
|
|
7115
|
+
async recordResults(results) {
|
|
7116
|
+
const statusIds = {
|
|
7117
|
+
passed: config.statusIds?.passed ?? 1,
|
|
7118
|
+
failed: config.statusIds?.failed ?? 5,
|
|
7119
|
+
skipped: config.statusIds?.skipped
|
|
7120
|
+
};
|
|
7121
|
+
const skipped = [];
|
|
7122
|
+
const sendable = [];
|
|
7123
|
+
for (const result of results) {
|
|
7124
|
+
const caseId = Number(result.caseId);
|
|
7125
|
+
if (!Number.isFinite(caseId)) {
|
|
7126
|
+
skipped.push({ caseId: result.caseId, reason: "case id is not numeric" });
|
|
7127
|
+
continue;
|
|
7128
|
+
}
|
|
7129
|
+
const statusId = result.status === "skipped" ? statusIds.skipped : statusIds[result.status];
|
|
7130
|
+
if (statusId === void 0) {
|
|
7131
|
+
skipped.push({
|
|
7132
|
+
caseId: result.caseId,
|
|
7133
|
+
reason: "no TestRail status id configured for skipped (set sync.testrail.statusIds.skipped)"
|
|
7134
|
+
});
|
|
7135
|
+
continue;
|
|
7136
|
+
}
|
|
7137
|
+
sendable.push({ result, caseId, statusId });
|
|
7138
|
+
}
|
|
7139
|
+
if (sendable.length === 0) {
|
|
7140
|
+
return { recorded: 0, skipped, attachmentsUploaded: 0 };
|
|
7141
|
+
}
|
|
7142
|
+
let runId = config.runId;
|
|
7143
|
+
if (runId === void 0) {
|
|
7144
|
+
const name = `${config.runName ?? "executable-stories"} ${(/* @__PURE__ */ new Date()).toISOString()}`;
|
|
7145
|
+
const run = await api(`add_run/${config.projectId}`, {
|
|
7146
|
+
body: {
|
|
7147
|
+
...config.suiteId === void 0 ? {} : { suite_id: config.suiteId },
|
|
7148
|
+
name,
|
|
7149
|
+
include_all: false,
|
|
7150
|
+
// Deduplicated: two stories can carry the same ticket id, and
|
|
7151
|
+
// TestRail rejects a run whose case list repeats one.
|
|
7152
|
+
case_ids: [...new Set(sendable.map((s) => s.caseId))]
|
|
7153
|
+
}
|
|
7154
|
+
});
|
|
7155
|
+
runId = run.id;
|
|
7156
|
+
}
|
|
7157
|
+
const runUrl = `${base}/index.php?/runs/view/${runId}`;
|
|
7158
|
+
const payload = sendable.map(({ result, caseId, statusId }) => {
|
|
7159
|
+
const elapsed = encodeElapsed(result.durationMs);
|
|
7160
|
+
const comment = [result.message, result.url ? `Living documentation: ${result.url}` : void 0].filter(Boolean).join("\n\n");
|
|
7161
|
+
return {
|
|
7162
|
+
case_id: caseId,
|
|
7163
|
+
status_id: statusId,
|
|
7164
|
+
...comment ? { comment } : {},
|
|
7165
|
+
...elapsed ? { elapsed } : {}
|
|
7166
|
+
};
|
|
7167
|
+
});
|
|
7168
|
+
const recorded = unwrapList(
|
|
7169
|
+
await api(`add_results_for_cases/${runId}`, { body: { results: payload } }),
|
|
7170
|
+
"results"
|
|
7171
|
+
);
|
|
7172
|
+
let attachmentsUploaded = 0;
|
|
7173
|
+
for (const [index, entry] of recorded.entries()) {
|
|
7174
|
+
const source = sendable[index]?.result;
|
|
7175
|
+
for (const attachment of source?.attachments ?? []) {
|
|
7176
|
+
try {
|
|
7177
|
+
const form = new FormData();
|
|
7178
|
+
const bytes = attachment.body.buffer.slice(
|
|
7179
|
+
attachment.body.byteOffset,
|
|
7180
|
+
attachment.body.byteOffset + attachment.body.byteLength
|
|
7181
|
+
);
|
|
7182
|
+
form.append(
|
|
7183
|
+
"attachment",
|
|
7184
|
+
new Blob([bytes], { type: attachment.mediaType }),
|
|
7185
|
+
attachment.filename
|
|
7186
|
+
);
|
|
7187
|
+
await api(`add_attachment_to_result/${entry.id}`, { form });
|
|
7188
|
+
attachmentsUploaded += 1;
|
|
7189
|
+
} catch (err) {
|
|
7190
|
+
deps.logger.warn(
|
|
7191
|
+
`TestRail attachment "${attachment.filename}" failed: ${err.message}`
|
|
7192
|
+
);
|
|
7193
|
+
}
|
|
7194
|
+
}
|
|
7195
|
+
}
|
|
7196
|
+
if (config.closeRun && config.runId === void 0) {
|
|
7197
|
+
await api(`close_run/${runId}`, { body: {} });
|
|
7198
|
+
}
|
|
7199
|
+
return {
|
|
7200
|
+
runId: String(runId),
|
|
7201
|
+
runUrl,
|
|
7202
|
+
recorded: recorded.length,
|
|
7203
|
+
skipped,
|
|
7204
|
+
attachmentsUploaded
|
|
7205
|
+
};
|
|
7206
|
+
}
|
|
7207
|
+
};
|
|
7208
|
+
}
|
|
7209
|
+
|
|
7210
|
+
// src/sync/adapters/xray.ts
|
|
7211
|
+
var DEFAULT_XRAY_BASE = "https://xray.cloud.getxray.app";
|
|
7212
|
+
var DEFAULT_TEST_TYPE = "Manual";
|
|
7213
|
+
var DEFAULT_MAX_ATTACHMENT_BYTES2 = 32 * 1024 * 1024;
|
|
7214
|
+
var PAGE_LIMIT2 = 100;
|
|
7215
|
+
function toAdf(text2) {
|
|
7216
|
+
const paragraphs = text2.split("\n\n").filter((block) => block.trim() !== "");
|
|
7217
|
+
return {
|
|
7218
|
+
version: 1,
|
|
7219
|
+
type: "doc",
|
|
7220
|
+
content: paragraphs.length === 0 ? [{ type: "paragraph", content: [] }] : paragraphs.map((block) => ({
|
|
7221
|
+
type: "paragraph",
|
|
7222
|
+
content: [{ type: "text", text: block }]
|
|
7223
|
+
}))
|
|
7224
|
+
};
|
|
7225
|
+
}
|
|
7226
|
+
function fromAdf(value) {
|
|
7227
|
+
if (typeof value === "string") return value;
|
|
7228
|
+
if (!value || typeof value !== "object") return "";
|
|
7229
|
+
const blocks = [];
|
|
7230
|
+
const walk = (node, collected) => {
|
|
7231
|
+
if (!node || typeof node !== "object") return;
|
|
7232
|
+
const typed = node;
|
|
7233
|
+
if (typed.type === "text" && typeof typed.text === "string") collected.push(typed.text);
|
|
7234
|
+
for (const child of typed.content ?? []) walk(child, collected);
|
|
7235
|
+
};
|
|
7236
|
+
for (const node of value.content ?? []) {
|
|
7237
|
+
const collected = [];
|
|
7238
|
+
walk(node, collected);
|
|
7239
|
+
blocks.push(collected.join(""));
|
|
7240
|
+
}
|
|
7241
|
+
return blocks.join("\n\n").trim();
|
|
7242
|
+
}
|
|
7243
|
+
function createXrayProvider(config, auth, deps) {
|
|
7244
|
+
const xrayBase = (config.xrayBaseUrl ?? DEFAULT_XRAY_BASE).replace(/\/$/, "");
|
|
7245
|
+
const jiraBase = config.jiraBaseUrl.replace(/\/$/, "");
|
|
7246
|
+
const jql = config.jql ?? `project = "${config.projectKey}" AND issuetype = Test`;
|
|
7247
|
+
const issueIds = /* @__PURE__ */ new Map();
|
|
7248
|
+
let token;
|
|
7249
|
+
async function authenticate() {
|
|
7250
|
+
if (token) return token;
|
|
7251
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/authenticate`, {
|
|
7252
|
+
method: "POST",
|
|
7253
|
+
headers: { "Content-Type": "application/json" },
|
|
7254
|
+
body: JSON.stringify({ client_id: auth.clientId, client_secret: auth.clientSecret })
|
|
7255
|
+
});
|
|
7256
|
+
const text2 = await response.text();
|
|
7257
|
+
if (!response.ok) {
|
|
7258
|
+
throw new Error(
|
|
7259
|
+
`Xray authentication failed (${response.status}): ${text2}
|
|
7260
|
+
XRAY_CLIENT_ID and XRAY_CLIENT_SECRET come from Jira -> Apps -> Xray -> API Keys. A Jira API token is a different credential and is rejected here.`
|
|
7261
|
+
);
|
|
7262
|
+
}
|
|
7263
|
+
token = JSON.parse(text2).replace(/^"|"$/g, "");
|
|
7264
|
+
return token;
|
|
7265
|
+
}
|
|
7266
|
+
async function graphql(query, variables) {
|
|
7267
|
+
const bearer = await authenticate();
|
|
7268
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/graphql`, {
|
|
7269
|
+
method: "POST",
|
|
7270
|
+
headers: {
|
|
7271
|
+
Authorization: `Bearer ${bearer}`,
|
|
7272
|
+
"Content-Type": "application/json"
|
|
7273
|
+
},
|
|
7274
|
+
body: JSON.stringify({ query, variables })
|
|
7275
|
+
});
|
|
7276
|
+
const text2 = await response.text();
|
|
7277
|
+
if (!response.ok) throw new Error(`Xray GraphQL failed (${response.status}): ${text2}`);
|
|
7278
|
+
const payload = JSON.parse(text2);
|
|
7279
|
+
if (payload.errors?.length) {
|
|
7280
|
+
throw new Error(`Xray GraphQL error: ${payload.errors.map((e) => e.message).join("; ")}`);
|
|
7281
|
+
}
|
|
7282
|
+
return payload.data;
|
|
7283
|
+
}
|
|
7284
|
+
function issueUrl(key) {
|
|
7285
|
+
return `${jiraBase}/browse/${key}`;
|
|
7286
|
+
}
|
|
7287
|
+
function toRemoteCase(test) {
|
|
7288
|
+
const key = test.jira?.key ?? test.issueId;
|
|
7289
|
+
if (!key) {
|
|
7290
|
+
throw new Error(
|
|
7291
|
+
`Xray returned a test with neither a Jira key nor an issue id: ${JSON.stringify(test)?.slice(0, 200)}`
|
|
7292
|
+
);
|
|
7293
|
+
}
|
|
7294
|
+
if (test.jira?.key) issueIds.set(test.jira.key, test.issueId);
|
|
7295
|
+
const { description, links } = decodeDescription(fromAdf(test.jira?.description));
|
|
7296
|
+
return {
|
|
7297
|
+
id: key,
|
|
7298
|
+
url: issueUrl(key),
|
|
7299
|
+
title: test.jira?.summary ?? key,
|
|
7300
|
+
body: {
|
|
7301
|
+
title: test.jira?.summary ?? key,
|
|
7302
|
+
steps: (test.steps ?? []).map((step) => decodeStepText(step.action ?? "")),
|
|
7303
|
+
description,
|
|
7304
|
+
links
|
|
7305
|
+
}
|
|
7306
|
+
};
|
|
7307
|
+
}
|
|
7308
|
+
async function jiraUpdate(key, fields) {
|
|
7309
|
+
if (!auth.jiraEmail || !auth.jiraToken) {
|
|
7310
|
+
deps.logger.warn(
|
|
7311
|
+
`Xray: summary/description for ${key} left unchanged. Set JIRA_EMAIL and JIRA_TOKEN to update Jira fields (steps are updated either way).`
|
|
7312
|
+
);
|
|
7313
|
+
return;
|
|
7314
|
+
}
|
|
7315
|
+
const basic = Buffer.from(`${auth.jiraEmail}:${auth.jiraToken}`).toString("base64");
|
|
7316
|
+
const response = await deps.fetch(`${jiraBase}/rest/api/3/issue/${key}`, {
|
|
7317
|
+
method: "PUT",
|
|
7318
|
+
headers: { Authorization: `Basic ${basic}`, "Content-Type": "application/json" },
|
|
7319
|
+
body: JSON.stringify({ fields })
|
|
7320
|
+
});
|
|
7321
|
+
if (!response.ok) {
|
|
7322
|
+
throw new Error(`Jira update of ${key} failed (${response.status}): ${await response.text()}`);
|
|
7323
|
+
}
|
|
7324
|
+
}
|
|
7325
|
+
return {
|
|
7326
|
+
name: "xray",
|
|
7327
|
+
maxAttachmentBytes: config.maxAttachmentBytes ?? DEFAULT_MAX_ATTACHMENT_BYTES2,
|
|
7328
|
+
describeTarget() {
|
|
7329
|
+
return `${config.projectKey} (${jiraBase})`;
|
|
7330
|
+
},
|
|
7331
|
+
async listCases() {
|
|
7332
|
+
const tests = [];
|
|
7333
|
+
for (let start = 0; ; start += PAGE_LIMIT2) {
|
|
7334
|
+
const data = await graphql(
|
|
7335
|
+
`query($jql: String!, $limit: Int!, $start: Int!) {
|
|
7336
|
+
getTests(jql: $jql, limit: $limit, start: $start) {
|
|
7337
|
+
total
|
|
7338
|
+
results {
|
|
7339
|
+
issueId
|
|
7340
|
+
jira(fields: ["key", "summary", "description"])
|
|
7341
|
+
steps { id action data result }
|
|
7342
|
+
}
|
|
7343
|
+
}
|
|
7344
|
+
}`,
|
|
7345
|
+
{ jql, limit: PAGE_LIMIT2, start }
|
|
7346
|
+
);
|
|
7347
|
+
const page = data.getTests?.results ?? [];
|
|
7348
|
+
tests.push(...page);
|
|
7349
|
+
if (page.length < PAGE_LIMIT2) break;
|
|
7350
|
+
}
|
|
7351
|
+
return tests.map(toRemoteCase);
|
|
7352
|
+
},
|
|
7353
|
+
async createCase(body) {
|
|
7354
|
+
const data = await graphql(
|
|
7355
|
+
`mutation($testType: UpdateTestTypeInput!, $steps: [CreateStepInput], $jira: JSON!) {
|
|
7356
|
+
createTest(testType: $testType, steps: $steps, jira: $jira) {
|
|
7357
|
+
test { issueId jira(fields: ["key", "summary", "description"]) }
|
|
7358
|
+
warnings
|
|
7359
|
+
}
|
|
7360
|
+
}`,
|
|
7361
|
+
{
|
|
7362
|
+
testType: { name: config.testType ?? DEFAULT_TEST_TYPE },
|
|
7363
|
+
steps: body.steps.map((step) => ({ action: encodeStepText(step), result: "" })),
|
|
7364
|
+
jira: {
|
|
7365
|
+
fields: {
|
|
7366
|
+
summary: body.title,
|
|
7367
|
+
description: toAdf(encodeDescription(body)),
|
|
7368
|
+
project: { key: config.projectKey }
|
|
7369
|
+
}
|
|
7370
|
+
}
|
|
7371
|
+
}
|
|
7372
|
+
);
|
|
7373
|
+
const created = data.createTest?.test;
|
|
7374
|
+
if (!created) throw new Error(`Xray createTest returned no test for "${body.title}"`);
|
|
7375
|
+
for (const warning of data.createTest?.warnings ?? []) deps.logger.warn(`Xray: ${warning}`);
|
|
7376
|
+
return toRemoteCase(created);
|
|
7377
|
+
},
|
|
7378
|
+
async updateCase(id, body) {
|
|
7379
|
+
const issueId = issueIds.get(id);
|
|
7380
|
+
if (!issueId) {
|
|
7381
|
+
throw new Error(`Xray: no internal issue id cached for ${id}. Run listCases first.`);
|
|
7382
|
+
}
|
|
7383
|
+
const current = await graphql(
|
|
7384
|
+
`query($issueId: String!) { getTest(issueId: $issueId) { steps { id } } }`,
|
|
7385
|
+
{ issueId }
|
|
7386
|
+
);
|
|
7387
|
+
const existing = current.getTest?.steps ?? [];
|
|
7388
|
+
for (const [index, step] of body.steps.entries()) {
|
|
7389
|
+
const action = encodeStepText(step);
|
|
7390
|
+
const target = existing[index];
|
|
7391
|
+
if (target) {
|
|
7392
|
+
await graphql(
|
|
7393
|
+
`mutation($stepId: String!, $step: UpdateStepInput!) {
|
|
7394
|
+
updateTestStep(stepId: $stepId, step: $step)
|
|
7395
|
+
}`,
|
|
7396
|
+
{ stepId: target.id, step: { action, result: "" } }
|
|
7397
|
+
);
|
|
7398
|
+
} else {
|
|
7399
|
+
await graphql(
|
|
7400
|
+
`mutation($issueId: String!, $step: CreateStepInput!) {
|
|
7401
|
+
addTestStep(issueId: $issueId, step: $step) { id }
|
|
7402
|
+
}`,
|
|
7403
|
+
{ issueId, step: { action, result: "" } }
|
|
7404
|
+
);
|
|
7405
|
+
}
|
|
7406
|
+
}
|
|
7407
|
+
for (const surplus of existing.slice(body.steps.length).reverse()) {
|
|
7408
|
+
await graphql(`mutation($stepId: String!) { removeTestStep(stepId: $stepId) }`, {
|
|
7409
|
+
stepId: surplus.id
|
|
7410
|
+
});
|
|
7411
|
+
}
|
|
7412
|
+
await jiraUpdate(id, {
|
|
7413
|
+
summary: body.title,
|
|
7414
|
+
description: toAdf(encodeDescription(body))
|
|
7415
|
+
});
|
|
7416
|
+
return {
|
|
7417
|
+
id,
|
|
7418
|
+
url: issueUrl(id),
|
|
7419
|
+
title: body.title,
|
|
7420
|
+
body
|
|
7421
|
+
};
|
|
7422
|
+
},
|
|
7423
|
+
async recordResults(results) {
|
|
7424
|
+
const statuses = {
|
|
7425
|
+
passed: config.statuses?.passed ?? "PASSED",
|
|
7426
|
+
failed: config.statuses?.failed ?? "FAILED",
|
|
7427
|
+
skipped: config.statuses?.skipped ?? "TODO"
|
|
7428
|
+
};
|
|
7429
|
+
let attachmentsUploaded = 0;
|
|
7430
|
+
const tests = results.map((result) => {
|
|
7431
|
+
const evidence = (result.attachments ?? []).map((attachment) => {
|
|
7432
|
+
attachmentsUploaded += 1;
|
|
7433
|
+
return {
|
|
7434
|
+
data: Buffer.from(attachment.body).toString("base64"),
|
|
7435
|
+
filename: attachment.filename,
|
|
7436
|
+
contentType: attachment.mediaType
|
|
7437
|
+
};
|
|
7438
|
+
});
|
|
7439
|
+
const comment = [result.message, result.url ? `Living documentation: ${result.url}` : void 0].filter(Boolean).join("\n\n");
|
|
7440
|
+
return {
|
|
7441
|
+
testKey: result.caseId,
|
|
7442
|
+
status: statuses[result.status],
|
|
7443
|
+
...comment ? { comment } : {},
|
|
7444
|
+
...evidence.length > 0 ? { evidence } : {}
|
|
7445
|
+
};
|
|
7446
|
+
});
|
|
7447
|
+
const bearer = await authenticate();
|
|
7448
|
+
const payload = {
|
|
7449
|
+
...config.testExecutionKey ? { testExecutionKey: config.testExecutionKey } : {},
|
|
7450
|
+
info: {
|
|
7451
|
+
summary: `${config.executionSummary ?? "executable-stories"} ${(/* @__PURE__ */ new Date()).toISOString()}`,
|
|
7452
|
+
project: config.projectKey,
|
|
7453
|
+
...config.testPlanKey ? { testPlanKey: config.testPlanKey } : {}
|
|
7454
|
+
},
|
|
7455
|
+
tests
|
|
7456
|
+
};
|
|
7457
|
+
const response = await deps.fetch(`${xrayBase}/api/v2/import/execution`, {
|
|
7458
|
+
method: "POST",
|
|
7459
|
+
headers: { Authorization: `Bearer ${bearer}`, "Content-Type": "application/json" },
|
|
7460
|
+
body: JSON.stringify(payload)
|
|
7461
|
+
});
|
|
7462
|
+
const text2 = await response.text();
|
|
7463
|
+
if (!response.ok) {
|
|
7464
|
+
throw new Error(`Xray import execution failed (${response.status}): ${text2}`);
|
|
7465
|
+
}
|
|
7466
|
+
const imported = JSON.parse(text2);
|
|
7467
|
+
return {
|
|
7468
|
+
runId: imported.key,
|
|
7469
|
+
runUrl: imported.key ? issueUrl(imported.key) : void 0,
|
|
7470
|
+
recorded: tests.length,
|
|
7471
|
+
skipped: [],
|
|
7472
|
+
attachmentsUploaded
|
|
7473
|
+
};
|
|
7474
|
+
}
|
|
7475
|
+
};
|
|
7476
|
+
}
|
|
7477
|
+
|
|
7478
|
+
// src/sync/adapters/index.ts
|
|
7479
|
+
var PROVIDER_NAMES = ["testrail", "xray"];
|
|
7480
|
+
function isProviderName(value) {
|
|
7481
|
+
return PROVIDER_NAMES.includes(value);
|
|
7482
|
+
}
|
|
7483
|
+
function required(env, name, hint) {
|
|
7484
|
+
const value = env[name];
|
|
7485
|
+
if (!value) throw new Error(`Missing ${name}. ${hint}`);
|
|
7486
|
+
return value;
|
|
7487
|
+
}
|
|
7488
|
+
function buildProvider(args, deps) {
|
|
7489
|
+
const { name, targets, env } = args;
|
|
7490
|
+
if (name === "testrail") {
|
|
7491
|
+
const config2 = targets.testrail;
|
|
7492
|
+
if (!config2) {
|
|
7493
|
+
throw new Error(
|
|
7494
|
+
'No TestRail config found. Add a `sync: { testrail: { url, projectId } }` block to executable-stories.config.mjs, or run "executable-stories sync testrail --init".'
|
|
7495
|
+
);
|
|
7496
|
+
}
|
|
7497
|
+
const provider2 = createTestRailProvider(
|
|
7498
|
+
config2,
|
|
7499
|
+
{
|
|
7500
|
+
username: required(env, "TESTRAIL_USERNAME", "This is the login email of the TestRail account."),
|
|
7501
|
+
apiKey: required(
|
|
7502
|
+
env,
|
|
7503
|
+
"TESTRAIL_API_KEY",
|
|
7504
|
+
"Generate one in TestRail under My Settings -> API Keys. A password will not work when the instance requires API keys."
|
|
7505
|
+
)
|
|
7506
|
+
},
|
|
7507
|
+
deps
|
|
7508
|
+
);
|
|
7509
|
+
return {
|
|
7510
|
+
provider: provider2,
|
|
7511
|
+
// TestRail ids are numeric and conventionally written "C1234" in a ticket,
|
|
7512
|
+
// so the prefix is decoration and gets stripped.
|
|
7513
|
+
engineDefaults: { ticketPrefix: "C", ticketPrefixStrip: true }
|
|
7514
|
+
};
|
|
7515
|
+
}
|
|
7516
|
+
const config = targets.xray;
|
|
7517
|
+
if (!config) {
|
|
7518
|
+
throw new Error(
|
|
7519
|
+
'No Xray config found. Add a `sync: { xray: { jiraBaseUrl, projectKey } }` block to executable-stories.config.mjs, or run "executable-stories sync xray --init".'
|
|
7520
|
+
);
|
|
7521
|
+
}
|
|
7522
|
+
const provider = createXrayProvider(
|
|
7523
|
+
config,
|
|
7524
|
+
{
|
|
7525
|
+
clientId: required(env, "XRAY_CLIENT_ID", "Create an API key pair in Jira under Apps -> Xray -> API Keys."),
|
|
7526
|
+
clientSecret: required(env, "XRAY_CLIENT_SECRET", "This is the secret half of the Xray API key pair."),
|
|
7527
|
+
jiraEmail: env["JIRA_EMAIL"],
|
|
7528
|
+
jiraToken: env["JIRA_TOKEN"]
|
|
7529
|
+
},
|
|
7530
|
+
deps
|
|
7531
|
+
);
|
|
7532
|
+
return {
|
|
7533
|
+
provider,
|
|
7534
|
+
// An Xray case id is a Jira issue key, so the project prefix is part of the
|
|
7535
|
+
// id and must survive.
|
|
7536
|
+
engineDefaults: { ticketPrefix: `${config.projectKey}-`, ticketPrefixStrip: false }
|
|
7537
|
+
};
|
|
7538
|
+
}
|
|
7539
|
+
|
|
7540
|
+
// src/index.ts
|
|
7541
|
+
import {
|
|
7542
|
+
STORY_REPORT_SCHEMA_VERSION,
|
|
7543
|
+
STORY_REPORT_SCHEMA_MAJOR
|
|
7544
|
+
} from "executable-stories-core/types/story-report";
|
|
7545
|
+
import { ES_THEME_TOKENS_CSS as ES_THEME_TOKENS_CSS2, ES_THEME_TOKEN_VALUES } from "executable-stories-core/theme/tokens";
|
|
7546
|
+
import { canonicalizeRun as canonicalizeRun3 } from "executable-stories-core/converters/acl/index";
|
|
7547
|
+
import { normalizeStatus } from "executable-stories-core/converters/acl/index";
|
|
7548
|
+
import { generateTestCaseId } from "executable-stories-core/converters/acl/index";
|
|
7549
|
+
import { generateRunId } from "executable-stories-core/converters/acl/index";
|
|
7550
|
+
import { slugify as slugify2 } from "executable-stories-core/converters/acl/index";
|
|
7551
|
+
import { deriveStepResults } from "executable-stories-core/converters/acl/index";
|
|
7552
|
+
import { mergeStepResults } from "executable-stories-core/converters/acl/index";
|
|
7553
|
+
import { resolveAttachment } from "executable-stories-core/converters/acl/index";
|
|
7554
|
+
import { resolveAttachments } from "executable-stories-core/converters/acl/index";
|
|
7555
|
+
import {
|
|
7556
|
+
validateCanonicalRun,
|
|
7557
|
+
assertValidRun
|
|
7558
|
+
} from "executable-stories-core/converters/acl/validate";
|
|
7559
|
+
|
|
7560
|
+
// src/watch.ts
|
|
7561
|
+
import * as fs5 from "fs";
|
|
7562
|
+
import * as path6 from "path";
|
|
7563
|
+
import { canonicalizeRun } from "executable-stories-core/converters/acl/index";
|
|
7564
|
+
import { synthesizeStories } from "executable-stories-core/converters/synthesize";
|
|
7565
|
+
function toRun(data, inputType, synthesize) {
|
|
7566
|
+
if (inputType === "canonical") return data;
|
|
7567
|
+
let raw = data;
|
|
7568
|
+
if (synthesize) raw = synthesizeStories(raw);
|
|
7569
|
+
return canonicalizeRun(raw);
|
|
7570
|
+
}
|
|
7571
|
+
async function regenerateRun(options, deps = {}) {
|
|
7572
|
+
const read = deps.readFile ?? ((filePath) => fs5.readFileSync(filePath, "utf8"));
|
|
7573
|
+
const data = JSON.parse(read(path6.resolve(options.input)));
|
|
7574
|
+
const run = toRun(data, options.inputType ?? "raw", options.synthesize !== false);
|
|
7575
|
+
const generator = new ReportGenerator({
|
|
7576
|
+
formats: options.formats,
|
|
7577
|
+
outputDir: options.outputDir,
|
|
7578
|
+
outputName: options.outputName
|
|
7579
|
+
});
|
|
7580
|
+
const result = await generator.generate(run);
|
|
7581
|
+
return { files: [...result.values()].flat(), run };
|
|
7582
|
+
}
|
|
7583
|
+
async function regenerateArtifacts(options, deps = {}) {
|
|
7584
|
+
return (await regenerateRun(options, deps)).files;
|
|
7585
|
+
}
|
|
7586
|
+
function startWatch(options, deps = {}) {
|
|
7587
|
+
const log = deps.log ?? ((message) => console.log(message));
|
|
7588
|
+
const regenerate = deps.regenerate ?? ((input) => regenerateArtifacts({ ...options, input }, deps));
|
|
7589
|
+
const watchFn = deps.watch ?? ((filePath, listener) => fs5.watch(filePath, listener));
|
|
7590
|
+
const debounceMs = options.debounceMs ?? 150;
|
|
7591
|
+
let timer;
|
|
7592
|
+
let running = false;
|
|
7593
|
+
let pending = false;
|
|
7594
|
+
const run = async () => {
|
|
7595
|
+
if (running) {
|
|
7596
|
+
pending = true;
|
|
7597
|
+
return;
|
|
7598
|
+
}
|
|
7599
|
+
running = true;
|
|
7600
|
+
try {
|
|
7601
|
+
const files = await regenerate(options.input);
|
|
7602
|
+
log(`Regenerated ${files.length} artifact file(s) from ${options.input}`);
|
|
7603
|
+
} catch (error) {
|
|
7604
|
+
log(`Watch regeneration failed: ${error.message}`);
|
|
7605
|
+
} finally {
|
|
7606
|
+
running = false;
|
|
7607
|
+
if (pending) {
|
|
7608
|
+
pending = false;
|
|
7609
|
+
trigger();
|
|
7610
|
+
}
|
|
7611
|
+
}
|
|
7612
|
+
};
|
|
7613
|
+
const trigger = () => {
|
|
7614
|
+
if (timer) clearTimeout(timer);
|
|
7615
|
+
timer = setTimeout(() => void run(), debounceMs);
|
|
7616
|
+
};
|
|
7617
|
+
trigger();
|
|
7618
|
+
const watcher = watchFn(path6.resolve(options.input), trigger);
|
|
7619
|
+
return {
|
|
7620
|
+
close: () => {
|
|
7621
|
+
if (timer) clearTimeout(timer);
|
|
7622
|
+
watcher.close();
|
|
7623
|
+
}
|
|
7624
|
+
};
|
|
7625
|
+
}
|
|
7626
|
+
|
|
7627
|
+
// src/index.ts
|
|
7628
|
+
import { advanceState, initialRunState } from "executable-stories-core";
|
|
7629
|
+
import { toStoryReport as toStoryReport6 } from "executable-stories-core/converters/story-report";
|
|
7630
|
+
|
|
7631
|
+
// src/publishers/confluence.ts
|
|
7632
|
+
function parseAdf(adf) {
|
|
7633
|
+
let parsed;
|
|
7634
|
+
try {
|
|
7635
|
+
parsed = JSON.parse(adf);
|
|
7636
|
+
} catch (err) {
|
|
7637
|
+
throw new Error(
|
|
7638
|
+
`ADF payload is not valid JSON: ${err.message}`
|
|
7639
|
+
);
|
|
7640
|
+
}
|
|
7641
|
+
if (!parsed || typeof parsed !== "object" || parsed.type !== "doc" || !Array.isArray(parsed.content)) {
|
|
7642
|
+
throw new Error(
|
|
7643
|
+
`ADF payload must be an object with { version, type: "doc", content: [...] }`
|
|
7644
|
+
);
|
|
7645
|
+
}
|
|
7646
|
+
return parsed;
|
|
7647
|
+
}
|
|
7648
|
+
function basicAuthHeader(auth) {
|
|
7649
|
+
const raw = `${auth.email}:${auth.token}`;
|
|
7650
|
+
const encoded = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(raw);
|
|
7651
|
+
return `Basic ${encoded}`;
|
|
7652
|
+
}
|
|
7653
|
+
async function parseErrorBody(response) {
|
|
7654
|
+
try {
|
|
7655
|
+
const body = await response.text();
|
|
7656
|
+
return body ? body.slice(0, 800) : "";
|
|
7657
|
+
} catch {
|
|
7658
|
+
return "";
|
|
7659
|
+
}
|
|
7660
|
+
}
|
|
7661
|
+
async function publishConfluencePage(args, deps) {
|
|
7662
|
+
parseAdf(args.adf);
|
|
7663
|
+
if (!args.pageId && !args.spaceId) {
|
|
7664
|
+
throw new Error(
|
|
7665
|
+
"publishConfluencePage requires either pageId (update) or spaceId (create)"
|
|
7666
|
+
);
|
|
7667
|
+
}
|
|
7668
|
+
if (!args.pageId && !args.title) {
|
|
7669
|
+
throw new Error("Creating a new page requires a title");
|
|
7670
|
+
}
|
|
7671
|
+
const base = args.baseUrl.replace(/\/$/, "");
|
|
7672
|
+
const fetchFn = deps.fetch ?? globalThis.fetch;
|
|
7673
|
+
if (!fetchFn) {
|
|
7674
|
+
throw new Error("No fetch implementation available (Node >= 22 expected)");
|
|
7675
|
+
}
|
|
7676
|
+
const headers = {
|
|
7677
|
+
Authorization: basicAuthHeader(deps.auth),
|
|
7678
|
+
Accept: "application/json",
|
|
7679
|
+
"Content-Type": "application/json"
|
|
7680
|
+
};
|
|
7681
|
+
if (args.pageId) {
|
|
7682
|
+
return updatePage(args, base, headers, fetchFn);
|
|
7683
|
+
}
|
|
7684
|
+
return createPage(args, base, headers, fetchFn);
|
|
7685
|
+
}
|
|
7686
|
+
async function updatePage(args, base, headers, fetchFn) {
|
|
7687
|
+
const getUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
|
|
7688
|
+
const getResp = await fetchFn(getUrl, { method: "GET", headers });
|
|
7689
|
+
if (!getResp.ok) {
|
|
7690
|
+
const body = await parseErrorBody(getResp);
|
|
7691
|
+
throw new Error(
|
|
7692
|
+
`GET ${getUrl} failed with ${getResp.status} ${getResp.statusText}${body ? `: ${body}` : ""}`
|
|
7693
|
+
);
|
|
7694
|
+
}
|
|
7695
|
+
const current = await getResp.json();
|
|
7696
|
+
const nextVersion = current.version.number + 1;
|
|
7697
|
+
const title = args.title ?? current.title;
|
|
7698
|
+
const putUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
|
|
7699
|
+
const putResp = await fetchFn(putUrl, {
|
|
7700
|
+
method: "PUT",
|
|
7701
|
+
headers,
|
|
7702
|
+
body: JSON.stringify({
|
|
7703
|
+
id: args.pageId,
|
|
7704
|
+
status: "current",
|
|
7705
|
+
title,
|
|
7706
|
+
body: {
|
|
7707
|
+
representation: "atlas_doc_format",
|
|
7708
|
+
value: args.adf
|
|
7709
|
+
},
|
|
7710
|
+
version: { number: nextVersion }
|
|
7711
|
+
})
|
|
7712
|
+
});
|
|
7713
|
+
if (!putResp.ok) {
|
|
7714
|
+
const body = await parseErrorBody(putResp);
|
|
7715
|
+
throw new Error(
|
|
7716
|
+
`PUT ${putUrl} failed with ${putResp.status} ${putResp.statusText}${body ? `: ${body}` : ""}`
|
|
7717
|
+
);
|
|
7718
|
+
}
|
|
7719
|
+
const updated = await putResp.json();
|
|
7720
|
+
return {
|
|
7721
|
+
id: updated.id,
|
|
7722
|
+
title: updated.title,
|
|
7723
|
+
version: updated.version.number,
|
|
7724
|
+
url: buildPageUrl(base, updated._links?.webui, updated.id),
|
|
7725
|
+
action: "updated"
|
|
7726
|
+
};
|
|
7727
|
+
}
|
|
7728
|
+
async function createPage(args, base, headers, fetchFn) {
|
|
7729
|
+
const body = {
|
|
7730
|
+
spaceId: args.spaceId,
|
|
7731
|
+
status: "current",
|
|
7732
|
+
title: args.title,
|
|
7733
|
+
body: {
|
|
7734
|
+
representation: "atlas_doc_format",
|
|
7735
|
+
value: args.adf
|
|
7736
|
+
}
|
|
7737
|
+
};
|
|
7738
|
+
if (args.parentId) body.parentId = args.parentId;
|
|
7739
|
+
const postUrl = `${base}/api/v2/pages`;
|
|
7740
|
+
const resp = await fetchFn(postUrl, {
|
|
7741
|
+
method: "POST",
|
|
7742
|
+
headers,
|
|
7743
|
+
body: JSON.stringify(body)
|
|
7744
|
+
});
|
|
7745
|
+
if (!resp.ok) {
|
|
7746
|
+
const errBody = await parseErrorBody(resp);
|
|
7747
|
+
throw new Error(
|
|
7748
|
+
`POST ${postUrl} failed with ${resp.status} ${resp.statusText}${errBody ? `: ${errBody}` : ""}`
|
|
7749
|
+
);
|
|
7750
|
+
}
|
|
7751
|
+
const created = await resp.json();
|
|
7752
|
+
return {
|
|
7753
|
+
id: created.id,
|
|
7754
|
+
title: created.title,
|
|
7755
|
+
version: created.version.number,
|
|
7756
|
+
url: buildPageUrl(base, created._links?.webui, created.id),
|
|
7757
|
+
action: "created"
|
|
7758
|
+
};
|
|
7759
|
+
}
|
|
7760
|
+
function buildPageUrl(base, webui, id) {
|
|
7761
|
+
if (webui) {
|
|
7762
|
+
return webui.startsWith("http") ? webui : `${base}${webui}`;
|
|
7763
|
+
}
|
|
7764
|
+
return `${base}/pages/${id}`;
|
|
6384
7765
|
}
|
|
6385
7766
|
|
|
6386
7767
|
// src/publishers/jira.ts
|
|
@@ -7513,7 +8894,7 @@ function statusIcon2(status) {
|
|
|
7513
8894
|
return "\u2022";
|
|
7514
8895
|
}
|
|
7515
8896
|
}
|
|
7516
|
-
function
|
|
8897
|
+
function escapeCell3(value) {
|
|
7517
8898
|
return value.replace(/\|/g, "\\|").replace(/\n/g, " ");
|
|
7518
8899
|
}
|
|
7519
8900
|
function intentSummary(intent) {
|
|
@@ -7542,7 +8923,7 @@ function renderWeakBand(lines, files) {
|
|
|
7542
8923
|
lines.push(`## \u{1F7E1} Changed code with weak evidence (${weak.length})`);
|
|
7543
8924
|
lines.push("");
|
|
7544
8925
|
for (const file of weak) {
|
|
7545
|
-
const covered = file.claims.map((c) => `${
|
|
8926
|
+
const covered = file.claims.map((c) => `${escapeCell3(c.scenario)} (${c.strength})`).join(", ");
|
|
7546
8927
|
lines.push(`- \`${file.path}\` _(${file.changeKind})_ \u2014 only: ${covered}`);
|
|
7547
8928
|
}
|
|
7548
8929
|
lines.push("");
|
|
@@ -7567,7 +8948,7 @@ function renderClaim(lines, claim) {
|
|
|
7567
8948
|
);
|
|
7568
8949
|
}
|
|
7569
8950
|
if (claim.intent) {
|
|
7570
|
-
lines.push(`- Why: ${
|
|
8951
|
+
lines.push(`- Why: ${escapeCell3(intentSummary(claim.intent))}`);
|
|
7571
8952
|
}
|
|
7572
8953
|
lines.push("");
|
|
7573
8954
|
}
|
|
@@ -7624,7 +9005,7 @@ function renderCodeDiff(lines, evidence) {
|
|
|
7624
9005
|
} else {
|
|
7625
9006
|
for (const ref of annotation.scenarios) {
|
|
7626
9007
|
lines.push(
|
|
7627
|
-
ref.resolved && ref.status ? `- ${statusIcon2(ref.status)} ${
|
|
9008
|
+
ref.resolved && ref.status ? `- ${statusIcon2(ref.status)} ${escapeCell3(ref.scenario ?? ref.id)} (\`${ref.id}\`)` : `- \u26A0\uFE0F \`${ref.id}\` \u2014 unverified reference (scenario not in this run)`
|
|
7628
9009
|
);
|
|
7629
9010
|
}
|
|
7630
9011
|
}
|
|
@@ -7843,9 +9224,9 @@ function renderDiffHunk(file, hunk, anchoredStart, anchoredCount) {
|
|
|
7843
9224
|
const sign = line.kind === "add" ? "+" : line.kind === "del" ? "-" : " ";
|
|
7844
9225
|
return `<tr class="${cls}"><td class="diff-ln">${o}</td><td class="diff-ln">${n}</td><td class="diff-sign">${sign}</td><td class="diff-code">${escapeHtml3(line.text)}</td></tr>`;
|
|
7845
9226
|
});
|
|
7846
|
-
const
|
|
9227
|
+
const path20 = file.newPath ?? file.oldPath ?? "";
|
|
7847
9228
|
return `<div class="diff-hunk">
|
|
7848
|
-
<div class="diff-file-header"><code>${escapeHtml3(
|
|
9229
|
+
<div class="diff-file-header"><code>${escapeHtml3(path20)}</code> <span class="subtle">@@ -${hunk.oldStart} +${hunk.newStart} @@ ${escapeHtml3(hunk.header)}</span></div>
|
|
7849
9230
|
<table class="diff-table"><tbody>${rows.join("")}</tbody></table>
|
|
7850
9231
|
</div>`;
|
|
7851
9232
|
}
|
|
@@ -8080,8 +9461,8 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
8080
9461
|
};
|
|
8081
9462
|
|
|
8082
9463
|
// src/deploy/ledger.ts
|
|
8083
|
-
import * as
|
|
8084
|
-
import * as
|
|
9464
|
+
import * as fs6 from "fs";
|
|
9465
|
+
import * as path7 from "path";
|
|
8085
9466
|
function createEmptyLedger() {
|
|
8086
9467
|
return {
|
|
8087
9468
|
deployments: [],
|
|
@@ -8089,12 +9470,12 @@ function createEmptyLedger() {
|
|
|
8089
9470
|
};
|
|
8090
9471
|
}
|
|
8091
9472
|
function loadLedger(ledgerPath) {
|
|
8092
|
-
const resolved =
|
|
8093
|
-
if (!
|
|
9473
|
+
const resolved = path7.resolve(ledgerPath);
|
|
9474
|
+
if (!fs6.existsSync(resolved)) {
|
|
8094
9475
|
return createEmptyLedger();
|
|
8095
9476
|
}
|
|
8096
9477
|
try {
|
|
8097
|
-
const raw = JSON.parse(
|
|
9478
|
+
const raw = JSON.parse(fs6.readFileSync(resolved, "utf8"));
|
|
8098
9479
|
if (raw.schemaVersion !== 1) {
|
|
8099
9480
|
throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
|
|
8100
9481
|
}
|
|
@@ -8105,10 +9486,10 @@ function loadLedger(ledgerPath) {
|
|
|
8105
9486
|
}
|
|
8106
9487
|
}
|
|
8107
9488
|
function saveLedger(ledger, ledgerPath) {
|
|
8108
|
-
const resolved =
|
|
8109
|
-
const dir =
|
|
8110
|
-
|
|
8111
|
-
|
|
9489
|
+
const resolved = path7.resolve(ledgerPath);
|
|
9490
|
+
const dir = path7.dirname(resolved);
|
|
9491
|
+
fs6.mkdirSync(dir, { recursive: true });
|
|
9492
|
+
fs6.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
|
|
8112
9493
|
}
|
|
8113
9494
|
function getLatestDeployment(ledger, environment) {
|
|
8114
9495
|
return [...ledger.deployments].reverse().find((d) => d.environment === environment);
|
|
@@ -8235,11 +9616,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
8235
9616
|
const ext = FORMAT_EXTENSIONS[format];
|
|
8236
9617
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
8237
9618
|
if (mode === "aggregated") {
|
|
8238
|
-
return toPosix(
|
|
9619
|
+
return toPosix(path8.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
8239
9620
|
}
|
|
8240
9621
|
const normalizedSource = toPosix(sourceFile);
|
|
8241
|
-
const dirOfSource =
|
|
8242
|
-
let baseName =
|
|
9622
|
+
const dirOfSource = path8.posix.dirname(normalizedSource);
|
|
9623
|
+
let baseName = path8.posix.basename(normalizedSource);
|
|
8243
9624
|
for (const testExt of TEST_EXTENSIONS) {
|
|
8244
9625
|
if (baseName.endsWith(testExt)) {
|
|
8245
9626
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -8248,12 +9629,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
8248
9629
|
}
|
|
8249
9630
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
8250
9631
|
if (colocatedStyle === "adjacent") {
|
|
8251
|
-
return toPosix(
|
|
9632
|
+
return toPosix(path8.posix.join(dirOfSource, fileName));
|
|
8252
9633
|
}
|
|
8253
9634
|
if (colocatedStyle === "flat") {
|
|
8254
|
-
return toPosix(
|
|
9635
|
+
return toPosix(path8.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
|
|
8255
9636
|
}
|
|
8256
|
-
return toPosix(
|
|
9637
|
+
return toPosix(path8.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
8257
9638
|
}
|
|
8258
9639
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
8259
9640
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -8473,8 +9854,8 @@ var ReportGenerator = class {
|
|
|
8473
9854
|
if (astroPaths) {
|
|
8474
9855
|
for (const mdPath of astroPaths) {
|
|
8475
9856
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
8476
|
-
const mdDir =
|
|
8477
|
-
const assetsDir =
|
|
9857
|
+
const mdDir = path8.dirname(mdPath);
|
|
9858
|
+
const assetsDir = path8.resolve(this.options.astro.assetsDir);
|
|
8478
9859
|
const result = copyMarkdownAssets({
|
|
8479
9860
|
markdown: content,
|
|
8480
9861
|
markdownDir: mdDir,
|
|
@@ -8522,16 +9903,16 @@ var ReportGenerator = class {
|
|
|
8522
9903
|
bySourceFile.set(sourceFile, outputPath);
|
|
8523
9904
|
}
|
|
8524
9905
|
if (bySourceFile.size === 0) return void 0;
|
|
8525
|
-
const indexPath = toPosix(
|
|
9906
|
+
const indexPath = toPosix(path8.join(this.options.outputDir, "index.html"));
|
|
8526
9907
|
if (htmlPaths.some((p) => toPosix(p) === indexPath)) {
|
|
8527
9908
|
this.deps.logger.warn?.(
|
|
8528
9909
|
`Skipping colocated index: a report already occupies ${indexPath}.`
|
|
8529
9910
|
);
|
|
8530
9911
|
return void 0;
|
|
8531
9912
|
}
|
|
8532
|
-
const entries = buildIndexEntries(run, bySourceFile,
|
|
9913
|
+
const entries = buildIndexEntries(run, bySourceFile, path8.dirname(indexPath));
|
|
8533
9914
|
const html = renderColocatedIndex(entries, this.options.html.title);
|
|
8534
|
-
await fsPromises.mkdir(
|
|
9915
|
+
await fsPromises.mkdir(path8.dirname(indexPath), { recursive: true });
|
|
8535
9916
|
await this.deps.writeFile(indexPath, html);
|
|
8536
9917
|
return indexPath;
|
|
8537
9918
|
}
|
|
@@ -8550,9 +9931,9 @@ var ReportGenerator = class {
|
|
|
8550
9931
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
8551
9932
|
const ext = FORMAT_EXTENSIONS[format];
|
|
8552
9933
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
8553
|
-
const outputPath = toPosix(
|
|
9934
|
+
const outputPath = toPosix(path8.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
|
|
8554
9935
|
const content = await this.formatContent(run, format);
|
|
8555
|
-
const dir =
|
|
9936
|
+
const dir = path8.dirname(outputPath);
|
|
8556
9937
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
8557
9938
|
await this.deps.writeFile(outputPath, content);
|
|
8558
9939
|
return [outputPath];
|
|
@@ -8564,7 +9945,7 @@ var ReportGenerator = class {
|
|
|
8564
9945
|
testCases
|
|
8565
9946
|
};
|
|
8566
9947
|
const content = await this.formatContent(groupRun, format);
|
|
8567
|
-
const dir =
|
|
9948
|
+
const dir = path8.dirname(outputPath);
|
|
8568
9949
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
8569
9950
|
await this.deps.writeFile(outputPath, content);
|
|
8570
9951
|
writtenPaths.push(outputPath);
|
|
@@ -8752,7 +10133,7 @@ async function generateRunComparison(args) {
|
|
|
8752
10133
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
8753
10134
|
for (const format of args.formats) {
|
|
8754
10135
|
const ext = format === "html" ? ".html" : format === "changelog" ? ".changelog.md" : ".md";
|
|
8755
|
-
const outputPath = toPosix(
|
|
10136
|
+
const outputPath = toPosix(path8.join(outputDir, `${outputName}${ext}`));
|
|
8756
10137
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : format === "changelog" ? new RunDiffChangelogFormatter().format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
8757
10138
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
8758
10139
|
files.push(outputPath);
|
|
@@ -8765,8 +10146,8 @@ import { parseNdjson as parseNdjson2 } from "executable-stories-core/converters/
|
|
|
8765
10146
|
import { toCIInfo as toCIInfo2 } from "executable-stories-core/types/ci";
|
|
8766
10147
|
|
|
8767
10148
|
// src/artifacts-readme.ts
|
|
8768
|
-
import * as
|
|
8769
|
-
import * as
|
|
10149
|
+
import * as fs7 from "fs";
|
|
10150
|
+
import * as path9 from "path";
|
|
8770
10151
|
var README = `# executable-stories artifacts
|
|
8771
10152
|
|
|
8772
10153
|
Generated by your test reporter and the \`executable-stories\` CLI. Safe to
|
|
@@ -8792,10 +10173,10 @@ Useful commands (all take \`raw-run.json\`):
|
|
|
8792
10173
|
`;
|
|
8793
10174
|
function writeArtifactsReadme(outputDir) {
|
|
8794
10175
|
try {
|
|
8795
|
-
const target =
|
|
8796
|
-
if (
|
|
8797
|
-
|
|
8798
|
-
|
|
10176
|
+
const target = path9.join(outputDir, "README.md");
|
|
10177
|
+
if (fs7.existsSync(target)) return false;
|
|
10178
|
+
fs7.mkdirSync(outputDir, { recursive: true });
|
|
10179
|
+
fs7.writeFileSync(target, README, "utf8");
|
|
8799
10180
|
return true;
|
|
8800
10181
|
} catch {
|
|
8801
10182
|
return false;
|
|
@@ -8803,8 +10184,8 @@ function writeArtifactsReadme(outputDir) {
|
|
|
8803
10184
|
}
|
|
8804
10185
|
|
|
8805
10186
|
// src/explainers.ts
|
|
8806
|
-
import * as
|
|
8807
|
-
import * as
|
|
10187
|
+
import * as fs9 from "fs";
|
|
10188
|
+
import * as path11 from "path";
|
|
8808
10189
|
import Ajv2 from "ajv/dist/2020.js";
|
|
8809
10190
|
import { parse as parseYaml } from "yaml";
|
|
8810
10191
|
import {
|
|
@@ -8863,16 +10244,16 @@ var explainer_v1_default = {
|
|
|
8863
10244
|
};
|
|
8864
10245
|
|
|
8865
10246
|
// src/utils/markdown-files.ts
|
|
8866
|
-
import * as
|
|
8867
|
-
import * as
|
|
10247
|
+
import * as fs8 from "fs";
|
|
10248
|
+
import * as path10 from "path";
|
|
8868
10249
|
function collectMarkdownFiles(target) {
|
|
8869
|
-
if (!
|
|
8870
|
-
if (
|
|
10250
|
+
if (!fs8.existsSync(target)) return [];
|
|
10251
|
+
if (fs8.statSync(target).isFile()) return [target];
|
|
8871
10252
|
const out = [];
|
|
8872
10253
|
const walk = (dir) => {
|
|
8873
|
-
for (const entry of
|
|
10254
|
+
for (const entry of fs8.readdirSync(dir, { withFileTypes: true })) {
|
|
8874
10255
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
8875
|
-
const full =
|
|
10256
|
+
const full = path10.join(dir, entry.name);
|
|
8876
10257
|
if (entry.isDirectory()) walk(full);
|
|
8877
10258
|
else if (/\.mdx?$/u.test(entry.name)) out.push(full);
|
|
8878
10259
|
}
|
|
@@ -8924,9 +10305,9 @@ function buildExplainersReport(args, _deps = {}) {
|
|
|
8924
10305
|
const scenarios = report.features.flatMap((feature) => feature.scenarios);
|
|
8925
10306
|
const explainers = [];
|
|
8926
10307
|
for (const file of collectMarkdownFiles(args.dir).sort()) {
|
|
8927
|
-
const parsed = parseExplainerDoc(
|
|
10308
|
+
const parsed = parseExplainerDoc(fs9.readFileSync(file, "utf8"));
|
|
8928
10309
|
if (!parsed) continue;
|
|
8929
|
-
const rel =
|
|
10310
|
+
const rel = path11.relative(args.dir, file);
|
|
8930
10311
|
if (!parsed.explainer) {
|
|
8931
10312
|
explainers.push({ file: rel, status: "invalid", errors: parsed.errors });
|
|
8932
10313
|
continue;
|
|
@@ -8995,13 +10376,13 @@ function renderExplainersReport(report, format) {
|
|
|
8995
10376
|
}
|
|
8996
10377
|
|
|
8997
10378
|
// src/run-file.ts
|
|
8998
|
-
import
|
|
8999
|
-
import
|
|
10379
|
+
import fs10 from "fs";
|
|
10380
|
+
import path12 from "path";
|
|
9000
10381
|
var DEFAULT_RUN_FILES = [".executable-stories/raw-run.json", "reports/raw-run.json"];
|
|
9001
10382
|
var SUPPORTED_RAW_RUN_SCHEMA = 1;
|
|
9002
10383
|
function findDefaultRunFile(cwd = process.cwd()) {
|
|
9003
10384
|
for (const candidate of DEFAULT_RUN_FILES) {
|
|
9004
|
-
if (
|
|
10385
|
+
if (fs10.existsSync(path12.resolve(cwd, candidate))) return candidate;
|
|
9005
10386
|
}
|
|
9006
10387
|
return void 0;
|
|
9007
10388
|
}
|
|
@@ -9027,8 +10408,8 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
|
|
|
9027
10408
|
});
|
|
9028
10409
|
return { checks, healthy: false };
|
|
9029
10410
|
}
|
|
9030
|
-
const abs =
|
|
9031
|
-
if (!
|
|
10411
|
+
const abs = path12.resolve(cwd, resolved);
|
|
10412
|
+
if (!fs10.existsSync(abs)) {
|
|
9032
10413
|
checks.push({
|
|
9033
10414
|
label: "run file",
|
|
9034
10415
|
status: "fail",
|
|
@@ -9037,7 +10418,7 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
|
|
|
9037
10418
|
});
|
|
9038
10419
|
return { checks, healthy: false };
|
|
9039
10420
|
}
|
|
9040
|
-
const stat =
|
|
10421
|
+
const stat = fs10.statSync(abs);
|
|
9041
10422
|
checks.push({
|
|
9042
10423
|
label: "run file",
|
|
9043
10424
|
status: "ok",
|
|
@@ -9045,7 +10426,7 @@ function diagnoseRunFile(file, cwd = process.cwd()) {
|
|
|
9045
10426
|
});
|
|
9046
10427
|
let parsed;
|
|
9047
10428
|
try {
|
|
9048
|
-
parsed = JSON.parse(
|
|
10429
|
+
parsed = JSON.parse(fs10.readFileSync(abs, "utf8"));
|
|
9049
10430
|
} catch (err) {
|
|
9050
10431
|
checks.push({
|
|
9051
10432
|
label: "json",
|
|
@@ -9176,7 +10557,9 @@ var COMPLETION_SUBCOMMANDS = [
|
|
|
9176
10557
|
["init-astro", "Scaffold a thin Astro docs site"],
|
|
9177
10558
|
["new", "Scaffold a docs page from a template"],
|
|
9178
10559
|
["check-links", "Scan docs for broken links"],
|
|
9179
|
-
["push", "Send a run to
|
|
10560
|
+
["push", "Send a run to a cloud ingest endpoint"],
|
|
10561
|
+
["coverage", "Compare stories against a test-management system (read-only)"],
|
|
10562
|
+
["sync", "Push cases, executions, and evidence to TestRail or Xray"],
|
|
9180
10563
|
["import-openapi", "Generate API doc pages from an OpenAPI spec"],
|
|
9181
10564
|
["publish-confluence", "Publish an ADF JSON file to Confluence"],
|
|
9182
10565
|
["publish-jira", "Publish an ADF JSON file to a Jira issue"],
|
|
@@ -9198,8 +10581,16 @@ var COMMON_FLAGS = [
|
|
|
9198
10581
|
"--baseline",
|
|
9199
10582
|
"--baseline-dir",
|
|
9200
10583
|
"--emit-canonical",
|
|
10584
|
+
"--apply",
|
|
10585
|
+
"--attach",
|
|
10586
|
+
"--report-url",
|
|
9201
10587
|
"--help"
|
|
9202
10588
|
];
|
|
10589
|
+
var SUBCOMMAND_VALUES = {
|
|
10590
|
+
completion: ["bash", "zsh", "fish"],
|
|
10591
|
+
sync: [...PROVIDER_NAMES],
|
|
10592
|
+
coverage: [...PROVIDER_NAMES]
|
|
10593
|
+
};
|
|
9203
10594
|
var FLAG_VALUES = {
|
|
9204
10595
|
"--format": [
|
|
9205
10596
|
"html",
|
|
@@ -9221,12 +10612,13 @@ var FLAG_VALUES = {
|
|
|
9221
10612
|
"--preset": ["agent", "ci", "docs"],
|
|
9222
10613
|
"--input-type": ["raw", "canonical", "ndjson"],
|
|
9223
10614
|
"--list-format": ["text", "json", "csv", "markdown-table"],
|
|
9224
|
-
"--check-format": ["text", "json"]
|
|
10615
|
+
"--check-format": ["text", "json"],
|
|
10616
|
+
"--attach": ["failed", "all", "none"]
|
|
9225
10617
|
};
|
|
9226
10618
|
function bashScript() {
|
|
9227
10619
|
const subcommands = COMPLETION_SUBCOMMANDS.map(([name]) => name).join(" ");
|
|
9228
10620
|
const flags = COMMON_FLAGS.join(" ");
|
|
9229
|
-
const valueCases = Object.entries(FLAG_VALUES).map(([
|
|
10621
|
+
const valueCases = [...Object.entries(FLAG_VALUES), ...Object.entries(SUBCOMMAND_VALUES)].map(([word, values]) => ` ${word})
|
|
9230
10622
|
COMPREPLY=( $(compgen -W "${values.join(" ")}" -- "$cur") ); return 0 ;;`).join("\n");
|
|
9231
10623
|
return `# executable-stories bash completion
|
|
9232
10624
|
_executable_stories() {
|
|
@@ -9236,8 +10628,6 @@ _executable_stories() {
|
|
|
9236
10628
|
|
|
9237
10629
|
case "$prev" in
|
|
9238
10630
|
${valueCases}
|
|
9239
|
-
completion)
|
|
9240
|
-
COMPREPLY=( $(compgen -W "bash zsh fish" -- "$cur") ); return 0 ;;
|
|
9241
10631
|
esac
|
|
9242
10632
|
|
|
9243
10633
|
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
@@ -9257,8 +10647,8 @@ function zshScript() {
|
|
|
9257
10647
|
const subcommands = COMPLETION_SUBCOMMANDS.map(([name, desc]) => ` '${name}:${desc.replaceAll("'", "")}'`).join(
|
|
9258
10648
|
"\n"
|
|
9259
10649
|
);
|
|
9260
|
-
const valueCases = Object.entries(FLAG_VALUES).map(([
|
|
9261
|
-
_values '${
|
|
10650
|
+
const valueCases = [...Object.entries(FLAG_VALUES), ...Object.entries(SUBCOMMAND_VALUES)].map(([word, values]) => ` ${word})
|
|
10651
|
+
_values '${word.replace(/^--/, "")}' ${values.join(" ")} ;;`).join("\n");
|
|
9262
10652
|
return `#compdef executable-stories
|
|
9263
10653
|
# executable-stories zsh completion
|
|
9264
10654
|
|
|
@@ -9270,8 +10660,6 @@ ${subcommands}
|
|
|
9270
10660
|
|
|
9271
10661
|
case "\${words[CURRENT-1]}" in
|
|
9272
10662
|
${valueCases}
|
|
9273
|
-
completion)
|
|
9274
|
-
_values 'shell' bash zsh fish ;;
|
|
9275
10663
|
*)
|
|
9276
10664
|
if (( CURRENT == 2 )); then
|
|
9277
10665
|
_describe 'subcommand' subcommands
|
|
@@ -9300,6 +10688,11 @@ function fishScript() {
|
|
|
9300
10688
|
`complete -c executable-stories -l ${flag.slice(2)} -x -a '${values.join(" ")}'`
|
|
9301
10689
|
);
|
|
9302
10690
|
}
|
|
10691
|
+
for (const [subcommand, values] of Object.entries(SUBCOMMAND_VALUES)) {
|
|
10692
|
+
lines.push(
|
|
10693
|
+
`complete -c executable-stories -n '__fish_seen_subcommand_from ${subcommand}' -x -a '${values.join(" ")}'`
|
|
10694
|
+
);
|
|
10695
|
+
}
|
|
9303
10696
|
lines.push("complete -c executable-stories -l open -d 'Open the HTML report when done'");
|
|
9304
10697
|
lines.push("complete -c executable-stories -l help -d 'Show help'");
|
|
9305
10698
|
return `# executable-stories fish completion
|
|
@@ -9334,7 +10727,7 @@ function runCompletion(args) {
|
|
|
9334
10727
|
|
|
9335
10728
|
// src/open-report.ts
|
|
9336
10729
|
import { spawn } from "child_process";
|
|
9337
|
-
import
|
|
10730
|
+
import path13 from "path";
|
|
9338
10731
|
function openCommand(platform) {
|
|
9339
10732
|
if (platform === "darwin") return { command: "open", args: [] };
|
|
9340
10733
|
if (platform === "win32") return { command: "cmd", args: ["/c", "start", ""] };
|
|
@@ -9350,7 +10743,7 @@ function openInBrowser(file, platform = process.platform) {
|
|
|
9350
10743
|
}
|
|
9351
10744
|
const { command, args } = openCommand(platform);
|
|
9352
10745
|
try {
|
|
9353
|
-
const child = spawn(command, [...args,
|
|
10746
|
+
const child = spawn(command, [...args, path13.resolve(file)], { stdio: "ignore", detached: true });
|
|
9354
10747
|
child.on("error", (err) => {
|
|
9355
10748
|
console.error(`--open: could not open ${file}: ${err.message}`);
|
|
9356
10749
|
});
|
|
@@ -9378,14 +10771,14 @@ function summaryLine(counts, files, durationMs) {
|
|
|
9378
10771
|
|
|
9379
10772
|
// src/init-astro.ts
|
|
9380
10773
|
import { spawnSync } from "child_process";
|
|
9381
|
-
import * as
|
|
9382
|
-
import * as
|
|
10774
|
+
import * as fs11 from "fs";
|
|
10775
|
+
import * as path14 from "path";
|
|
9383
10776
|
import { fileURLToPath } from "url";
|
|
9384
|
-
var __dirname =
|
|
10777
|
+
var __dirname = path14.dirname(fileURLToPath(import.meta.url));
|
|
9385
10778
|
function detectPackageManager(cwd = process.cwd()) {
|
|
9386
|
-
if (
|
|
9387
|
-
if (
|
|
9388
|
-
if (
|
|
10779
|
+
if (fs11.existsSync(path14.join(cwd, "pnpm-lock.yaml"))) return "pnpm";
|
|
10780
|
+
if (fs11.existsSync(path14.join(cwd, "yarn.lock"))) return "yarn";
|
|
10781
|
+
if (fs11.existsSync(path14.join(cwd, "bun.lock")) || fs11.existsSync(path14.join(cwd, "bun.lockb"))) {
|
|
9389
10782
|
return "bun";
|
|
9390
10783
|
}
|
|
9391
10784
|
return "npm";
|
|
@@ -9406,7 +10799,7 @@ function runDocsDev(siteDir) {
|
|
|
9406
10799
|
return { kind: "not-scaffolded" };
|
|
9407
10800
|
}
|
|
9408
10801
|
const pm = detectPackageManager();
|
|
9409
|
-
if (!
|
|
10802
|
+
if (!fs11.existsSync(path14.join(siteDir, "node_modules"))) {
|
|
9410
10803
|
console.log(`Installing docs site dependencies with ${pm}\u2026`);
|
|
9411
10804
|
if (!installScaffoldDependencies(siteDir, pm)) {
|
|
9412
10805
|
return { kind: "install-failed", pm };
|
|
@@ -9417,14 +10810,14 @@ function runDocsDev(siteDir) {
|
|
|
9417
10810
|
}
|
|
9418
10811
|
var SCAFFOLD_MARKER = "executable-stories.config.mjs";
|
|
9419
10812
|
function isScaffoldedAstroSite(dir) {
|
|
9420
|
-
return
|
|
10813
|
+
return fs11.existsSync(path14.join(dir, SCAFFOLD_MARKER));
|
|
9421
10814
|
}
|
|
9422
10815
|
function initAstro(options = {}) {
|
|
9423
10816
|
const targetDir = options.targetDir ?? "./story-docs";
|
|
9424
10817
|
const force = options.force ?? false;
|
|
9425
10818
|
const update = options.update ?? false;
|
|
9426
|
-
const templateDir =
|
|
9427
|
-
if (!
|
|
10819
|
+
const templateDir = path14.resolve(__dirname, "..", "templates", "astro-thin");
|
|
10820
|
+
if (!fs11.existsSync(templateDir)) {
|
|
9428
10821
|
throw new Error(
|
|
9429
10822
|
`Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
|
|
9430
10823
|
);
|
|
@@ -9432,8 +10825,8 @@ function initAstro(options = {}) {
|
|
|
9432
10825
|
if (update) {
|
|
9433
10826
|
return updateScaffoldDeps(templateDir, targetDir);
|
|
9434
10827
|
}
|
|
9435
|
-
if (
|
|
9436
|
-
const entries =
|
|
10828
|
+
if (fs11.existsSync(targetDir)) {
|
|
10829
|
+
const entries = fs11.readdirSync(targetDir);
|
|
9437
10830
|
if (entries.length > 0 && !force) {
|
|
9438
10831
|
throw new Error(
|
|
9439
10832
|
`Directory "${targetDir}" already exists and is not empty. Use --force to overlay the template (existing files are kept; same-path template files are overwritten), or --update to refresh framework files only.`
|
|
@@ -9453,11 +10846,11 @@ function updateScaffoldDeps(templateDir, targetDir) {
|
|
|
9453
10846
|
return { targetDir };
|
|
9454
10847
|
}
|
|
9455
10848
|
function mergeDependencies(templateDir, targetDir) {
|
|
9456
|
-
const tmplPkgPath =
|
|
9457
|
-
const userPkgPath =
|
|
9458
|
-
if (!
|
|
9459
|
-
const tmpl = JSON.parse(
|
|
9460
|
-
const user = JSON.parse(
|
|
10849
|
+
const tmplPkgPath = path14.join(templateDir, "package.json");
|
|
10850
|
+
const userPkgPath = path14.join(targetDir, "package.json");
|
|
10851
|
+
if (!fs11.existsSync(tmplPkgPath) || !fs11.existsSync(userPkgPath)) return;
|
|
10852
|
+
const tmpl = JSON.parse(fs11.readFileSync(tmplPkgPath, "utf8"));
|
|
10853
|
+
const user = JSON.parse(fs11.readFileSync(userPkgPath, "utf8"));
|
|
9461
10854
|
user.dependencies = user.dependencies ?? {};
|
|
9462
10855
|
let changed = false;
|
|
9463
10856
|
for (const [name, version] of Object.entries(tmpl.dependencies ?? {})) {
|
|
@@ -9467,28 +10860,28 @@ function mergeDependencies(templateDir, targetDir) {
|
|
|
9467
10860
|
}
|
|
9468
10861
|
}
|
|
9469
10862
|
if (changed) {
|
|
9470
|
-
|
|
10863
|
+
fs11.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
|
|
9471
10864
|
`, "utf8");
|
|
9472
10865
|
}
|
|
9473
10866
|
}
|
|
9474
10867
|
function copyDirRecursive(src, dest) {
|
|
9475
|
-
|
|
9476
|
-
const entries =
|
|
10868
|
+
fs11.mkdirSync(dest, { recursive: true });
|
|
10869
|
+
const entries = fs11.readdirSync(src, { withFileTypes: true });
|
|
9477
10870
|
for (const entry of entries) {
|
|
9478
|
-
const srcPath =
|
|
10871
|
+
const srcPath = path14.join(src, entry.name);
|
|
9479
10872
|
const destName = entry.name === "gitignore" ? ".gitignore" : entry.name;
|
|
9480
|
-
const destPath =
|
|
10873
|
+
const destPath = path14.join(dest, destName);
|
|
9481
10874
|
if (entry.isDirectory()) {
|
|
9482
10875
|
copyDirRecursive(srcPath, destPath);
|
|
9483
10876
|
} else {
|
|
9484
|
-
|
|
10877
|
+
fs11.copyFileSync(srcPath, destPath);
|
|
9485
10878
|
}
|
|
9486
10879
|
}
|
|
9487
10880
|
}
|
|
9488
10881
|
|
|
9489
10882
|
// src/scaffold-doc.ts
|
|
9490
|
-
import * as
|
|
9491
|
-
import * as
|
|
10883
|
+
import * as fs12 from "fs";
|
|
10884
|
+
import * as path15 from "path";
|
|
9492
10885
|
var TEMPLATES = [
|
|
9493
10886
|
"adr",
|
|
9494
10887
|
"runbook",
|
|
@@ -9505,7 +10898,7 @@ function isoDate(today) {
|
|
|
9505
10898
|
function nextSeq(dir) {
|
|
9506
10899
|
let max = 0;
|
|
9507
10900
|
try {
|
|
9508
|
-
for (const entry of
|
|
10901
|
+
for (const entry of fs12.readdirSync(dir)) {
|
|
9509
10902
|
const match = /^(\d{1,4})-/.exec(entry);
|
|
9510
10903
|
if (match) max = Math.max(max, Number.parseInt(match[1], 10));
|
|
9511
10904
|
}
|
|
@@ -9662,12 +11055,12 @@ function scaffoldDoc(options) {
|
|
|
9662
11055
|
);
|
|
9663
11056
|
}
|
|
9664
11057
|
const spec = TEMPLATE_SPECS[template];
|
|
9665
|
-
const baseDir = options.baseDir ??
|
|
11058
|
+
const baseDir = options.baseDir ?? path15.join("src", "content", "docs");
|
|
9666
11059
|
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
9667
11060
|
const name = (options.name ?? "").trim() || defaultName(template);
|
|
9668
11061
|
const slug2 = slugify3(name);
|
|
9669
11062
|
const scenarioId = normalizeScenarioId(options.scenarioId);
|
|
9670
|
-
const dir =
|
|
11063
|
+
const dir = path15.join(baseDir, spec.subdir);
|
|
9671
11064
|
if (template === "scenario-note" && !scenarioId) {
|
|
9672
11065
|
throw new Error(`Template "scenario-note" requires --scenario-id.`);
|
|
9673
11066
|
}
|
|
@@ -9679,14 +11072,14 @@ function scaffoldDoc(options) {
|
|
|
9679
11072
|
seq: nextSeq(dir)
|
|
9680
11073
|
};
|
|
9681
11074
|
const filename = `${spec.filename(slug2, ctx)}.mdx`;
|
|
9682
|
-
const filePath =
|
|
9683
|
-
if (
|
|
11075
|
+
const filePath = path15.join(dir, filename);
|
|
11076
|
+
if (fs12.existsSync(filePath) && !options.force) {
|
|
9684
11077
|
throw new Error(
|
|
9685
11078
|
`File "${filePath}" already exists. Use --force to overwrite.`
|
|
9686
11079
|
);
|
|
9687
11080
|
}
|
|
9688
|
-
|
|
9689
|
-
|
|
11081
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
11082
|
+
fs12.writeFileSync(filePath, spec.content(ctx), "utf8");
|
|
9690
11083
|
return { template, path: filePath, title: titleFor2(template, ctx) };
|
|
9691
11084
|
}
|
|
9692
11085
|
function defaultName(template) {
|
|
@@ -9727,8 +11120,8 @@ function normalizeScenarioId(input) {
|
|
|
9727
11120
|
}
|
|
9728
11121
|
|
|
9729
11122
|
// src/check-links.ts
|
|
9730
|
-
import * as
|
|
9731
|
-
import * as
|
|
11123
|
+
import * as fs13 from "fs";
|
|
11124
|
+
import * as path16 from "path";
|
|
9732
11125
|
function stripCode(markdown) {
|
|
9733
11126
|
let out = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
|
|
9734
11127
|
out = out.replace(/(`+)(?:(?!\1).)+\1/g, "");
|
|
@@ -9742,8 +11135,8 @@ function extractLinks(markdown) {
|
|
|
9742
11135
|
while ((match = mdRe.exec(stripped)) !== null) {
|
|
9743
11136
|
found.push(match[1].trim());
|
|
9744
11137
|
}
|
|
9745
|
-
const
|
|
9746
|
-
while ((match =
|
|
11138
|
+
const attrRe = /\b(?:href|src)\s*=\s*["']([^"']+)["']/gi;
|
|
11139
|
+
while ((match = attrRe.exec(stripped)) !== null) {
|
|
9747
11140
|
found.push(match[1].trim());
|
|
9748
11141
|
}
|
|
9749
11142
|
return found.filter(Boolean);
|
|
@@ -9755,21 +11148,37 @@ function classifyLink(link2) {
|
|
|
9755
11148
|
if (link2.startsWith("/")) return "root";
|
|
9756
11149
|
return "internal";
|
|
9757
11150
|
}
|
|
9758
|
-
function resolutionCandidates(
|
|
9759
|
-
const
|
|
9760
|
-
if (!withoutAnchor) return [];
|
|
9761
|
-
const base = path15.resolve(path15.dirname(fromFile), withoutAnchor);
|
|
11151
|
+
function resolutionCandidates(fromDir, linkPath) {
|
|
11152
|
+
const base = path16.resolve(fromDir, linkPath);
|
|
9762
11153
|
const candidates = [base];
|
|
9763
|
-
if (!
|
|
11154
|
+
if (!path16.extname(base)) {
|
|
9764
11155
|
candidates.push(`${base}.md`, `${base}.mdx`);
|
|
9765
|
-
candidates.push(
|
|
11156
|
+
candidates.push(path16.join(base, "index.md"), path16.join(base, "index.mdx"));
|
|
9766
11157
|
}
|
|
9767
11158
|
return candidates;
|
|
9768
11159
|
}
|
|
9769
|
-
function
|
|
9770
|
-
return
|
|
9771
|
-
|
|
9772
|
-
|
|
11160
|
+
function linkPathOf(link2) {
|
|
11161
|
+
return link2.split("#")[0].split("?")[0];
|
|
11162
|
+
}
|
|
11163
|
+
function existsAsFile(candidate) {
|
|
11164
|
+
return fs13.existsSync(candidate) && fs13.statSync(candidate).isFile();
|
|
11165
|
+
}
|
|
11166
|
+
function resolvesFrom(dirs, linkPath) {
|
|
11167
|
+
return dirs.some((dir) => resolutionCandidates(dir, linkPath).some(existsAsFile));
|
|
11168
|
+
}
|
|
11169
|
+
function detectAssetRoots(target) {
|
|
11170
|
+
let dir = fs13.existsSync(target) && fs13.statSync(target).isDirectory() ? path16.resolve(target) : path16.dirname(path16.resolve(target));
|
|
11171
|
+
for (let depth = 0; depth < 6; depth++) {
|
|
11172
|
+
const hasConfig = ["mjs", "js", "ts", "mts"].some(
|
|
11173
|
+
(ext) => fs13.existsSync(path16.join(dir, `astro.config.${ext}`))
|
|
11174
|
+
);
|
|
11175
|
+
const publicDir = path16.join(dir, "public");
|
|
11176
|
+
if (hasConfig && fs13.existsSync(publicDir)) return [publicDir];
|
|
11177
|
+
const parent = path16.dirname(dir);
|
|
11178
|
+
if (parent === dir) break;
|
|
11179
|
+
dir = parent;
|
|
11180
|
+
}
|
|
11181
|
+
return [];
|
|
9773
11182
|
}
|
|
9774
11183
|
async function isExternalAlive(url, timeoutMs) {
|
|
9775
11184
|
const attempt = async (method) => {
|
|
@@ -9794,9 +11203,11 @@ async function isExternalAlive(url, timeoutMs) {
|
|
|
9794
11203
|
}
|
|
9795
11204
|
async function checkLinks(options) {
|
|
9796
11205
|
const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
|
|
9797
|
-
if (!
|
|
11206
|
+
if (!fs13.existsSync(target)) {
|
|
9798
11207
|
throw new Error(`Path not found: ${target}`);
|
|
9799
11208
|
}
|
|
11209
|
+
const siteRoot = path16.resolve(options.siteRoot ?? target);
|
|
11210
|
+
const rootDirs = [siteRoot, ...(options.assetRoots ?? detectAssetRoots(target)).map((d) => path16.resolve(d))];
|
|
9800
11211
|
const files = collectMarkdownFiles(target);
|
|
9801
11212
|
const broken = [];
|
|
9802
11213
|
let linksChecked = 0;
|
|
@@ -9804,13 +11215,25 @@ async function checkLinks(options) {
|
|
|
9804
11215
|
let skipped = 0;
|
|
9805
11216
|
const externalCache = /* @__PURE__ */ new Map();
|
|
9806
11217
|
for (const file of files) {
|
|
9807
|
-
const content =
|
|
11218
|
+
const content = fs13.readFileSync(file, "utf8");
|
|
9808
11219
|
for (const link2 of extractLinks(content)) {
|
|
9809
11220
|
const kind = classifyLink(link2);
|
|
9810
|
-
if (kind === "anchor" || kind === "mail"
|
|
11221
|
+
if (kind === "anchor" || kind === "mail") {
|
|
9811
11222
|
skipped += 1;
|
|
9812
11223
|
continue;
|
|
9813
11224
|
}
|
|
11225
|
+
if (kind === "root") {
|
|
11226
|
+
const linkPath2 = linkPathOf(link2).replace(/^\/+/, "");
|
|
11227
|
+
if (linkPath2 === "") {
|
|
11228
|
+
skipped += 1;
|
|
11229
|
+
continue;
|
|
11230
|
+
}
|
|
11231
|
+
linksChecked += 1;
|
|
11232
|
+
if (!resolvesFrom(rootDirs, linkPath2)) {
|
|
11233
|
+
broken.push({ file, link: link2, reason: "target file not found" });
|
|
11234
|
+
}
|
|
11235
|
+
continue;
|
|
11236
|
+
}
|
|
9814
11237
|
if (kind === "external") {
|
|
9815
11238
|
if (!checkExternal) {
|
|
9816
11239
|
skipped += 1;
|
|
@@ -9828,8 +11251,13 @@ async function checkLinks(options) {
|
|
|
9828
11251
|
}
|
|
9829
11252
|
continue;
|
|
9830
11253
|
}
|
|
11254
|
+
const linkPath = linkPathOf(link2);
|
|
11255
|
+
if (linkPath === "") {
|
|
11256
|
+
skipped += 1;
|
|
11257
|
+
continue;
|
|
11258
|
+
}
|
|
9831
11259
|
linksChecked += 1;
|
|
9832
|
-
if (!
|
|
11260
|
+
if (!resolvesFrom([path16.dirname(file)], linkPath)) {
|
|
9833
11261
|
broken.push({ file, link: link2, reason: "target file not found" });
|
|
9834
11262
|
}
|
|
9835
11263
|
}
|
|
@@ -9861,7 +11289,7 @@ function formatLinkReport(report) {
|
|
|
9861
11289
|
|
|
9862
11290
|
// src/push.ts
|
|
9863
11291
|
import { execFileSync } from "child_process";
|
|
9864
|
-
import * as
|
|
11292
|
+
import * as fs14 from "fs";
|
|
9865
11293
|
import { parseArgs } from "util";
|
|
9866
11294
|
import { canonicalizeRun as canonicalizeRun4 } from "executable-stories-core/converters/acl/index";
|
|
9867
11295
|
import { toStoryReport as toStoryReport8 } from "executable-stories-core/converters/story-report";
|
|
@@ -9869,10 +11297,11 @@ import { synthesizeStories as synthesizeStories2 } from "executable-stories-core
|
|
|
9869
11297
|
var EXIT_SUCCESS = 0;
|
|
9870
11298
|
var EXIT_PUSH_FAILED = 1;
|
|
9871
11299
|
var EXIT_USAGE = 4;
|
|
11300
|
+
var EXIT_GATE_BLOCKED = 5;
|
|
9872
11301
|
var HELP = `Usage:
|
|
9873
11302
|
executable-stories push <run.json> [options]
|
|
9874
11303
|
|
|
9875
|
-
Send a run to
|
|
11304
|
+
Send a run to a cloud ingest endpoint. <run.json> is either a StoryReport v1
|
|
9876
11305
|
(e.g. reports/index.story-report.json) or a raw run JSON, which is converted
|
|
9877
11306
|
through the standard pipeline first.
|
|
9878
11307
|
|
|
@@ -9886,12 +11315,20 @@ Options:
|
|
|
9886
11315
|
--git-sha <sha> Default: current git HEAD.
|
|
9887
11316
|
--base <ref> Send files changed since <ref> (e.g. origin/main) so the
|
|
9888
11317
|
cloud can recommend a test scope for the change.
|
|
11318
|
+
--gate After pushing, ask the cloud whether this commit is safe
|
|
11319
|
+
to release and exit 5 if it is blocked. The policy lives
|
|
11320
|
+
in your organization's settings, not in a file here.
|
|
9889
11321
|
-h, --help Show this help.
|
|
9890
11322
|
|
|
9891
|
-
|
|
11323
|
+
Under GitHub Actions, repo/branch/sha, the base commit, and PR metadata are
|
|
11324
|
+
read from the environment, the run URL and recommended scope are written to
|
|
11325
|
+
the job summary, and the run id is written to GITHUB_OUTPUT as ingest-run-id.
|
|
11326
|
+
|
|
11327
|
+
Exit codes: 0 pushed, 1 push rejected/failed, 4 usage error, 5 gate blocked.`;
|
|
9892
11328
|
function defaultDeps() {
|
|
9893
11329
|
return {
|
|
9894
|
-
readFile: (filePath) =>
|
|
11330
|
+
readFile: (filePath) => fs14.readFileSync(filePath, "utf8"),
|
|
11331
|
+
appendFile: (filePath, text2) => fs14.appendFileSync(filePath, text2),
|
|
9895
11332
|
fetchFn: fetch,
|
|
9896
11333
|
git: (args) => {
|
|
9897
11334
|
try {
|
|
@@ -9912,6 +11349,42 @@ function repoSlugFromRemote(remoteUrl) {
|
|
|
9912
11349
|
function isStoryReport(data) {
|
|
9913
11350
|
return typeof data.schemaVersion === "string";
|
|
9914
11351
|
}
|
|
11352
|
+
function githubContext(deps) {
|
|
11353
|
+
const env = deps.env;
|
|
11354
|
+
const context = {
|
|
11355
|
+
repo: env.GITHUB_REPOSITORY,
|
|
11356
|
+
// GITHUB_HEAD_REF is the source branch on a pull_request event, where
|
|
11357
|
+
// GITHUB_REF_NAME would be the synthetic "<n>/merge" ref.
|
|
11358
|
+
branch: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME,
|
|
11359
|
+
gitSha: env.GITHUB_SHA
|
|
11360
|
+
};
|
|
11361
|
+
let event;
|
|
11362
|
+
try {
|
|
11363
|
+
event = JSON.parse(deps.readFile(env.GITHUB_EVENT_PATH ?? ""));
|
|
11364
|
+
} catch {
|
|
11365
|
+
return context;
|
|
11366
|
+
}
|
|
11367
|
+
context.prNumber = event.pull_request?.number;
|
|
11368
|
+
context.prUrl = event.pull_request?.html_url;
|
|
11369
|
+
const baseSha = event.pull_request?.base?.sha ?? event.before;
|
|
11370
|
+
if (baseSha && !/^0+$/.test(baseSha)) {
|
|
11371
|
+
if (deps.git(["cat-file", "-e", baseSha]) === void 0) {
|
|
11372
|
+
deps.git(["fetch", "--depth=1", "origin", baseSha]);
|
|
11373
|
+
}
|
|
11374
|
+
context.baseSha = baseSha;
|
|
11375
|
+
}
|
|
11376
|
+
return context;
|
|
11377
|
+
}
|
|
11378
|
+
function summaryWriter(deps) {
|
|
11379
|
+
const summaryPath = deps.env.GITHUB_STEP_SUMMARY;
|
|
11380
|
+
return (markdown) => {
|
|
11381
|
+
if (summaryPath) deps.appendFile(summaryPath, `${markdown}
|
|
11382
|
+
`);
|
|
11383
|
+
};
|
|
11384
|
+
}
|
|
11385
|
+
function cell(text2) {
|
|
11386
|
+
return text2.replaceAll("|", "\\|");
|
|
11387
|
+
}
|
|
9915
11388
|
async function runPush(rawArgs, depsOverride = {}) {
|
|
9916
11389
|
const deps = { ...defaultDeps(), ...depsOverride };
|
|
9917
11390
|
let parsed;
|
|
@@ -9926,6 +11399,7 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
9926
11399
|
branch: { type: "string" },
|
|
9927
11400
|
"git-sha": { type: "string" },
|
|
9928
11401
|
base: { type: "string" },
|
|
11402
|
+
gate: { type: "boolean" },
|
|
9929
11403
|
help: { type: "boolean", short: "h" }
|
|
9930
11404
|
}
|
|
9931
11405
|
});
|
|
@@ -9947,7 +11421,7 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
9947
11421
|
const key = parsed.values.key ?? deps.env.EXECUTABLE_STORIES_API_KEY;
|
|
9948
11422
|
if (!key) {
|
|
9949
11423
|
deps.error(
|
|
9950
|
-
"push needs an API key: pass --key or set EXECUTABLE_STORIES_API_KEY. Create one in
|
|
11424
|
+
"push needs an API key: pass --key or set EXECUTABLE_STORIES_API_KEY. Create one in your cloud instance's settings (Ingest key)."
|
|
9951
11425
|
);
|
|
9952
11426
|
return EXIT_USAGE;
|
|
9953
11427
|
}
|
|
@@ -9971,16 +11445,20 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
9971
11445
|
return EXIT_USAGE;
|
|
9972
11446
|
}
|
|
9973
11447
|
}
|
|
9974
|
-
const
|
|
11448
|
+
const onActions = deps.env.GITHUB_ACTIONS === "true";
|
|
11449
|
+
const github = onActions ? githubContext(deps) : {};
|
|
11450
|
+
const summary = summaryWriter(deps);
|
|
11451
|
+
const repo = parsed.values.repo ?? github.repo ?? repoSlugFromRemote(deps.git(["config", "--get", "remote.origin.url"]) ?? "");
|
|
9975
11452
|
if (!repo) {
|
|
9976
11453
|
deps.error("Could not infer the repository slug from git. Pass --repo <org/name>.");
|
|
9977
11454
|
return EXIT_USAGE;
|
|
9978
11455
|
}
|
|
9979
|
-
const branch = parsed.values.branch ?? deps.git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
9980
|
-
const gitSha = parsed.values["git-sha"] ?? deps.git(["rev-parse", "HEAD"]);
|
|
11456
|
+
const branch = parsed.values.branch ?? github.branch ?? deps.git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
11457
|
+
const gitSha = parsed.values["git-sha"] ?? github.gitSha ?? deps.git(["rev-parse", "HEAD"]);
|
|
9981
11458
|
const baseUrl = parsed.values.url ?? deps.env.EXECUTABLE_STORIES_URL ?? "https://app.executablestories.com";
|
|
9982
11459
|
const base = parsed.values.base;
|
|
9983
|
-
const
|
|
11460
|
+
const baseSha = base ? deps.git(["rev-parse", base]) : github.baseSha;
|
|
11461
|
+
const changedFiles = baseSha ? deps.git(["diff", "--name-only", `${baseSha}...HEAD`])?.split("\n").filter(Boolean) ?? [] : [];
|
|
9984
11462
|
if (base && changedFiles.length === 0) {
|
|
9985
11463
|
deps.error(`Warning: no changed files found against ${base}; pushing without change metadata.`);
|
|
9986
11464
|
}
|
|
@@ -9996,9 +11474,13 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
9996
11474
|
repo,
|
|
9997
11475
|
branch,
|
|
9998
11476
|
gitSha,
|
|
9999
|
-
|
|
11477
|
+
// "serve" named a subcommand that no longer exists (ADR 0006). The
|
|
11478
|
+
// cloud accepts both; "local" is what this is.
|
|
11479
|
+
source: onActions ? "action" : "local",
|
|
10000
11480
|
report,
|
|
10001
|
-
...changedFiles.length > 0 ? { changedFiles, baseSha
|
|
11481
|
+
...changedFiles.length > 0 ? { changedFiles, baseSha } : {},
|
|
11482
|
+
...github.prNumber ? { prNumber: github.prNumber } : {},
|
|
11483
|
+
...github.prUrl ? { prUrl: github.prUrl } : {}
|
|
10002
11484
|
})
|
|
10003
11485
|
});
|
|
10004
11486
|
} catch (err) {
|
|
@@ -10011,18 +11493,428 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
10011
11493
|
deps.error(`Push rejected: HTTP ${response.status}${retryAfter ? ` (retry after ${retryAfter}s)` : ""}: ${body.slice(0, 500)}`);
|
|
10012
11494
|
return EXIT_PUSH_FAILED;
|
|
10013
11495
|
}
|
|
10014
|
-
let
|
|
11496
|
+
let result = {};
|
|
10015
11497
|
try {
|
|
10016
|
-
|
|
11498
|
+
result = JSON.parse(body);
|
|
10017
11499
|
} catch {
|
|
10018
11500
|
}
|
|
11501
|
+
const runId = String(result.runId ?? "");
|
|
10019
11502
|
deps.log(runId ? `Pushed run ${runId} (${repo}${branch ? `@${branch}` : ""})` : "Pushed run.");
|
|
11503
|
+
if (result.url) deps.log(result.url);
|
|
11504
|
+
summary(
|
|
11505
|
+
result.url ? `### Executable Stories
|
|
11506
|
+
|
|
11507
|
+
[View this run](${result.url})` : "### Executable Stories\n\nRun pushed."
|
|
11508
|
+
);
|
|
11509
|
+
if (runId && deps.env.GITHUB_OUTPUT) {
|
|
11510
|
+
deps.appendFile(deps.env.GITHUB_OUTPUT, `ingest-run-id=${runId}
|
|
11511
|
+
`);
|
|
11512
|
+
}
|
|
11513
|
+
const recommendations = result.recommendations ?? [];
|
|
11514
|
+
if (recommendations.length > 0) {
|
|
11515
|
+
deps.log(`
|
|
11516
|
+
Recommended scope for this change (${recommendations.length}):`);
|
|
11517
|
+
for (const item of recommendations) {
|
|
11518
|
+
deps.log(` [${item.confidence}] ${item.title} \u2014 ${item.reason}`);
|
|
11519
|
+
}
|
|
11520
|
+
summary(`
|
|
11521
|
+
**Recommended scope for this change (${recommendations.length})**
|
|
11522
|
+
`);
|
|
11523
|
+
summary("| Confidence | Case | Why |\n| --- | --- | --- |");
|
|
11524
|
+
for (const item of recommendations) {
|
|
11525
|
+
summary(`| ${cell(item.confidence)} | ${cell(item.title)} | ${cell(item.reason)} |`);
|
|
11526
|
+
}
|
|
11527
|
+
}
|
|
11528
|
+
if (!parsed.values.gate) return EXIT_SUCCESS;
|
|
11529
|
+
if (!gitSha) {
|
|
11530
|
+
deps.error("--gate needs a commit sha: pass --git-sha or run inside a git repository.");
|
|
11531
|
+
return EXIT_USAGE;
|
|
11532
|
+
}
|
|
11533
|
+
return await runGate({ baseUrl, key, repo, gitSha, onActions }, deps);
|
|
11534
|
+
}
|
|
11535
|
+
async function runGate({
|
|
11536
|
+
baseUrl,
|
|
11537
|
+
key,
|
|
11538
|
+
repo,
|
|
11539
|
+
gitSha,
|
|
11540
|
+
onActions
|
|
11541
|
+
}, deps) {
|
|
11542
|
+
const summary = summaryWriter(deps);
|
|
11543
|
+
const query = `repo=${encodeURIComponent(repo)}&sha=${encodeURIComponent(gitSha)}`;
|
|
11544
|
+
let response;
|
|
11545
|
+
try {
|
|
11546
|
+
response = await deps.fetchFn(new URL(`/api/v1/releases/gate?${query}`, baseUrl), {
|
|
11547
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
11548
|
+
});
|
|
11549
|
+
} catch (err) {
|
|
11550
|
+
deps.error(`Could not reach the gate: ${err instanceof Error ? err.message : String(err)}`);
|
|
11551
|
+
return EXIT_PUSH_FAILED;
|
|
11552
|
+
}
|
|
11553
|
+
const body = await response.text();
|
|
11554
|
+
if (!response.ok) {
|
|
11555
|
+
deps.error(`Gate check failed: HTTP ${response.status}: ${body.slice(0, 500)}`);
|
|
11556
|
+
return EXIT_PUSH_FAILED;
|
|
11557
|
+
}
|
|
11558
|
+
let gate = {};
|
|
11559
|
+
try {
|
|
11560
|
+
gate = JSON.parse(body);
|
|
11561
|
+
} catch {
|
|
11562
|
+
deps.error(`Gate returned a non-JSON body: ${body.slice(0, 200)}`);
|
|
11563
|
+
return EXIT_PUSH_FAILED;
|
|
11564
|
+
}
|
|
11565
|
+
for (const warning of gate.warnings ?? []) deps.log(` warning: ${warning}`);
|
|
11566
|
+
const commit = `${repo}@${gitSha.slice(0, 12)}`;
|
|
11567
|
+
if (gate.status === "no-release") {
|
|
11568
|
+
deps.log(`
|
|
11569
|
+
No release recorded for ${commit} \u2014 nothing to gate on.`);
|
|
11570
|
+
summary("\n**Release gate: no release recorded for this commit**");
|
|
11571
|
+
return EXIT_SUCCESS;
|
|
11572
|
+
}
|
|
11573
|
+
if (gate.status === "blocked") {
|
|
11574
|
+
deps.error(`
|
|
11575
|
+
Release gate: BLOCKED for ${commit}`);
|
|
11576
|
+
summary("\n**Release gate: blocked**\n");
|
|
11577
|
+
for (const reason of gate.blocking ?? []) {
|
|
11578
|
+
deps.error(` - ${reason}`);
|
|
11579
|
+
summary(`- ${reason}`);
|
|
11580
|
+
if (onActions) deps.error(`::error::Release gate: ${reason}`);
|
|
11581
|
+
}
|
|
11582
|
+
return EXIT_GATE_BLOCKED;
|
|
11583
|
+
}
|
|
11584
|
+
deps.log(`
|
|
11585
|
+
Release gate: clear for ${commit}`);
|
|
11586
|
+
summary("\n**Release gate: clear**");
|
|
10020
11587
|
return EXIT_SUCCESS;
|
|
10021
11588
|
}
|
|
10022
11589
|
|
|
11590
|
+
// src/sync/run.ts
|
|
11591
|
+
import * as fs15 from "fs";
|
|
11592
|
+
import * as path17 from "path";
|
|
11593
|
+
import { parseArgs as parseArgs2 } from "util";
|
|
11594
|
+
import { canonicalizeRun as canonicalizeRun5 } from "executable-stories-core/converters/acl/index";
|
|
11595
|
+
import { synthesizeStories as synthesizeStories3 } from "executable-stories-core/converters/synthesize";
|
|
11596
|
+
|
|
11597
|
+
// src/config.ts
|
|
11598
|
+
import { existsSync as existsSync11, readFileSync as readFileSync10 } from "fs";
|
|
11599
|
+
import { resolve as resolve9 } from "path";
|
|
11600
|
+
var CONFIG_CANDIDATES = [
|
|
11601
|
+
"executable-stories.config.mjs",
|
|
11602
|
+
"executable-stories.config.js",
|
|
11603
|
+
"executable-stories.config.json"
|
|
11604
|
+
];
|
|
11605
|
+
async function loadConfig(configPath) {
|
|
11606
|
+
let resolved;
|
|
11607
|
+
if (configPath) {
|
|
11608
|
+
resolved = resolve9(configPath);
|
|
11609
|
+
} else {
|
|
11610
|
+
const present = CONFIG_CANDIDATES.map((name) => resolve9(process.cwd(), name)).filter(existsSync11);
|
|
11611
|
+
if (present.length > 1) {
|
|
11612
|
+
throw new Error(
|
|
11613
|
+
`Multiple config files found in this directory:
|
|
11614
|
+
` + present.map((p) => ` - ${p}`).join("\n") + `
|
|
11615
|
+
Keep only one, or pass --config <path> to choose which to load.`
|
|
11616
|
+
);
|
|
11617
|
+
}
|
|
11618
|
+
resolved = present[0];
|
|
11619
|
+
}
|
|
11620
|
+
if (!resolved || !existsSync11(resolved)) return {};
|
|
11621
|
+
const isJson = resolved.endsWith(".json");
|
|
11622
|
+
let config;
|
|
11623
|
+
if (isJson) {
|
|
11624
|
+
try {
|
|
11625
|
+
config = JSON.parse(readFileSync10(resolved, "utf8"));
|
|
11626
|
+
} catch (err) {
|
|
11627
|
+
throw new Error(`Config file at ${resolved} is not valid JSON: ${err.message}`);
|
|
11628
|
+
}
|
|
11629
|
+
} else {
|
|
11630
|
+
config = (await import(resolved)).default;
|
|
11631
|
+
}
|
|
11632
|
+
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
11633
|
+
throw new Error(
|
|
11634
|
+
isJson ? `Config file at ${resolved} must contain a JSON object. Got: ${Array.isArray(config) ? "array" : typeof config}` : `Config file at ${resolved} must export a default object. Got: ${typeof config}`
|
|
11635
|
+
);
|
|
11636
|
+
}
|
|
11637
|
+
const { formatters, sync } = config;
|
|
11638
|
+
return {
|
|
11639
|
+
...formatters === void 0 ? {} : { formatters },
|
|
11640
|
+
...sync === void 0 ? {} : { sync }
|
|
11641
|
+
};
|
|
11642
|
+
}
|
|
11643
|
+
|
|
11644
|
+
// src/sync/run.ts
|
|
11645
|
+
var EXIT_SUCCESS2 = 0;
|
|
11646
|
+
var EXIT_FAILED = 1;
|
|
11647
|
+
var EXIT_USAGE2 = 4;
|
|
11648
|
+
var COVERAGE_HELP = `Usage:
|
|
11649
|
+
executable-stories coverage <provider> <run.json> [options]
|
|
11650
|
+
|
|
11651
|
+
Compares what your tests cover against what a test-management system holds.
|
|
11652
|
+
Read-only: needs nothing but a read-scoped API key, and writes nothing remote.
|
|
11653
|
+
|
|
11654
|
+
Providers: ${PROVIDER_NAMES.join(", ")}
|
|
11655
|
+
|
|
11656
|
+
Options:
|
|
11657
|
+
--config <path> Config file (default: executable-stories.config.mjs, .js, or .json)
|
|
11658
|
+
--output-dir <dir> Where the JSON and Markdown land (default: reports)
|
|
11659
|
+
--report-url <url> Published report URL, used for deep links
|
|
11660
|
+
--lockfile <path> Default: ${DEFAULT_LOCKFILE_PATH}
|
|
11661
|
+
--quiet Write the artifacts, skip the stdout summary
|
|
11662
|
+
-h, --help Show this help
|
|
11663
|
+
|
|
11664
|
+
Credentials come from the environment:
|
|
11665
|
+
TestRail TESTRAIL_USERNAME, TESTRAIL_API_KEY
|
|
11666
|
+
Xray XRAY_CLIENT_ID, XRAY_CLIENT_SECRET (JIRA_EMAIL/JIRA_TOKEN to edit Jira fields)
|
|
11667
|
+
|
|
11668
|
+
Exit codes: 0 report produced, 1 provider unreachable, 4 usage error.`;
|
|
11669
|
+
var SYNC_HELP = `Usage:
|
|
11670
|
+
executable-stories sync <provider> <run.json> [options]
|
|
11671
|
+
|
|
11672
|
+
Pushes stories into a test-management system: case bodies authored from the
|
|
11673
|
+
test, executions recorded against them, evidence attached.
|
|
11674
|
+
|
|
11675
|
+
Prints the plan and changes nothing unless --apply is passed.
|
|
11676
|
+
|
|
11677
|
+
Providers: ${PROVIDER_NAMES.join(", ")}
|
|
11678
|
+
|
|
11679
|
+
Options:
|
|
11680
|
+
--apply Actually write. Without it, this is a dry run.
|
|
11681
|
+
--attach <policy> failed (default), all, or none
|
|
11682
|
+
--config <path> Config file (default: executable-stories.config.mjs, .js, or .json)
|
|
11683
|
+
--lockfile <path> Default: ${DEFAULT_LOCKFILE_PATH}
|
|
11684
|
+
--report-url <url> Published report URL, used for deep links
|
|
11685
|
+
--output-dir <dir> Where coverage artifacts land (default: reports)
|
|
11686
|
+
--continue-on-error Exit 0 even when some writes failed
|
|
11687
|
+
--init Print a config block for this provider and exit
|
|
11688
|
+
-h, --help Show this help
|
|
11689
|
+
|
|
11690
|
+
The lockfile binds each story to its case. Commit it: the diff shows up in the
|
|
11691
|
+
pull request that created the case.
|
|
11692
|
+
|
|
11693
|
+
Exit codes: 0 applied (or planned), 1 some writes failed, 4 usage error.`;
|
|
11694
|
+
function defaultDeps2() {
|
|
11695
|
+
return {
|
|
11696
|
+
readFile: (filePath) => fs15.readFileSync(filePath, "utf8"),
|
|
11697
|
+
fileExists: (filePath) => fs15.existsSync(filePath),
|
|
11698
|
+
writeFile: (filePath, contents) => {
|
|
11699
|
+
fs15.mkdirSync(path17.dirname(path17.resolve(filePath)), { recursive: true });
|
|
11700
|
+
fs15.writeFileSync(filePath, contents, "utf8");
|
|
11701
|
+
},
|
|
11702
|
+
fetchFn: globalThis.fetch,
|
|
11703
|
+
env: process.env,
|
|
11704
|
+
log: console.log,
|
|
11705
|
+
error: console.error,
|
|
11706
|
+
loadConfigFn: loadConfig
|
|
11707
|
+
};
|
|
11708
|
+
}
|
|
11709
|
+
var CONFIG_TEMPLATES = {
|
|
11710
|
+
testrail: `export default {
|
|
11711
|
+
sync: {
|
|
11712
|
+
testrail: {
|
|
11713
|
+
url: "https://acme.testrail.io",
|
|
11714
|
+
projectId: 1,
|
|
11715
|
+
suiteId: 1,
|
|
11716
|
+
// Section that newly created cases land in. Without it, creation is refused.
|
|
11717
|
+
sectionId: 1,
|
|
11718
|
+
// TestRail ships no "skipped" status; set one to record skipped tests.
|
|
11719
|
+
// statusIds: { skipped: 6 },
|
|
11720
|
+
// Only needed if this instance uses a customised case template.
|
|
11721
|
+
// fields: { steps: "custom_steps_separated", description: "custom_preconds" },
|
|
11722
|
+
},
|
|
11723
|
+
},
|
|
11724
|
+
};
|
|
11725
|
+
|
|
11726
|
+
// Environment: TESTRAIL_USERNAME (login email), TESTRAIL_API_KEY (My Settings -> API Keys)`,
|
|
11727
|
+
xray: `export default {
|
|
11728
|
+
sync: {
|
|
11729
|
+
xray: {
|
|
11730
|
+
jiraBaseUrl: "https://acme.atlassian.net",
|
|
11731
|
+
projectKey: "PROJ",
|
|
11732
|
+
// testPlanKey: "PROJ-100",
|
|
11733
|
+
},
|
|
11734
|
+
},
|
|
11735
|
+
};
|
|
11736
|
+
|
|
11737
|
+
// Environment: XRAY_CLIENT_ID, XRAY_CLIENT_SECRET (Jira -> Apps -> Xray -> API Keys)
|
|
11738
|
+
// Optional: JIRA_EMAIL, JIRA_TOKEN (needed to update an existing test's summary/description)`
|
|
11739
|
+
};
|
|
11740
|
+
var JSON_CONFIG_TEMPLATES = {
|
|
11741
|
+
testrail: JSON.stringify(
|
|
11742
|
+
{ sync: { testrail: { url: "https://acme.testrail.io", projectId: 1, suiteId: 1, sectionId: 1 } } },
|
|
11743
|
+
null,
|
|
11744
|
+
2
|
|
11745
|
+
),
|
|
11746
|
+
xray: JSON.stringify(
|
|
11747
|
+
{ sync: { xray: { jiraBaseUrl: "https://acme.atlassian.net", projectKey: "PROJ" } } },
|
|
11748
|
+
null,
|
|
11749
|
+
2
|
|
11750
|
+
)
|
|
11751
|
+
};
|
|
11752
|
+
function isStoryReport2(data) {
|
|
11753
|
+
return typeof data.schemaVersion === "string";
|
|
11754
|
+
}
|
|
11755
|
+
function loadRun(inputPath, deps) {
|
|
11756
|
+
const data = JSON.parse(deps.readFile(inputPath));
|
|
11757
|
+
if (isStoryReport2(data)) {
|
|
11758
|
+
throw new Error(
|
|
11759
|
+
`${inputPath} is a StoryReport, which has already dropped the attachment bodies and source paths this needs. Point at the raw run instead (e.g. reports/raw-run.json).`
|
|
11760
|
+
);
|
|
11761
|
+
}
|
|
11762
|
+
return canonicalizeRun5(synthesizeStories3(data));
|
|
11763
|
+
}
|
|
11764
|
+
async function runSyncCommand(mode, rawArgs, depsOverride = {}) {
|
|
11765
|
+
const deps = { ...defaultDeps2(), ...depsOverride };
|
|
11766
|
+
const help = mode === "sync" ? SYNC_HELP : COVERAGE_HELP;
|
|
11767
|
+
let parsed;
|
|
11768
|
+
try {
|
|
11769
|
+
parsed = parseArgs2({
|
|
11770
|
+
args: rawArgs,
|
|
11771
|
+
allowPositionals: true,
|
|
11772
|
+
options: {
|
|
11773
|
+
apply: { type: "boolean", default: false },
|
|
11774
|
+
attach: { type: "string" },
|
|
11775
|
+
config: { type: "string" },
|
|
11776
|
+
lockfile: { type: "string" },
|
|
11777
|
+
"report-url": { type: "string" },
|
|
11778
|
+
"output-dir": { type: "string" },
|
|
11779
|
+
"continue-on-error": { type: "boolean", default: false },
|
|
11780
|
+
init: { type: "boolean", default: false },
|
|
11781
|
+
quiet: { type: "boolean", default: false },
|
|
11782
|
+
help: { type: "boolean", short: "h", default: false }
|
|
11783
|
+
}
|
|
11784
|
+
});
|
|
11785
|
+
} catch (err) {
|
|
11786
|
+
deps.error(err instanceof Error ? err.message : String(err));
|
|
11787
|
+
deps.error(help);
|
|
11788
|
+
return EXIT_USAGE2;
|
|
11789
|
+
}
|
|
11790
|
+
if (parsed.values.help) {
|
|
11791
|
+
deps.log(help);
|
|
11792
|
+
return EXIT_SUCCESS2;
|
|
11793
|
+
}
|
|
11794
|
+
const providerName = parsed.positionals[0];
|
|
11795
|
+
if (!providerName || !isProviderName(providerName)) {
|
|
11796
|
+
deps.error(
|
|
11797
|
+
providerName ? `Unknown provider "${providerName}". Available: ${PROVIDER_NAMES.join(", ")}.` : `${mode} needs a provider: executable-stories ${mode} <${PROVIDER_NAMES.join("|")}> <run.json>`
|
|
11798
|
+
);
|
|
11799
|
+
deps.error(help);
|
|
11800
|
+
return EXIT_USAGE2;
|
|
11801
|
+
}
|
|
11802
|
+
if (parsed.values.init) {
|
|
11803
|
+
deps.log(CONFIG_TEMPLATES[providerName]);
|
|
11804
|
+
deps.log("");
|
|
11805
|
+
deps.log(
|
|
11806
|
+
`Save the block above as executable-stories.config.mjs (or merge the \`sync\` key into the one you have), then run:
|
|
11807
|
+
executable-stories coverage ${providerName} reports/raw-run.json`
|
|
11808
|
+
);
|
|
11809
|
+
deps.log("");
|
|
11810
|
+
deps.log(
|
|
11811
|
+
`Not a JavaScript project? Put the same \`sync\` object in executable-stories.config.json instead:`
|
|
11812
|
+
);
|
|
11813
|
+
deps.log(JSON_CONFIG_TEMPLATES[providerName]);
|
|
11814
|
+
return EXIT_SUCCESS2;
|
|
11815
|
+
}
|
|
11816
|
+
const inputPath = parsed.positionals[1];
|
|
11817
|
+
if (!inputPath) {
|
|
11818
|
+
deps.error(`${mode} needs a run file: executable-stories ${mode} ${providerName} <run.json>`);
|
|
11819
|
+
deps.error(help);
|
|
11820
|
+
return EXIT_USAGE2;
|
|
11821
|
+
}
|
|
11822
|
+
const attach = parsed.values.attach;
|
|
11823
|
+
if (attach && !["failed", "all", "none"].includes(attach)) {
|
|
11824
|
+
deps.error(`--attach must be one of: failed, all, none (got "${attach}")`);
|
|
11825
|
+
return EXIT_USAGE2;
|
|
11826
|
+
}
|
|
11827
|
+
let run;
|
|
11828
|
+
try {
|
|
11829
|
+
run = loadRun(inputPath, deps);
|
|
11830
|
+
} catch (err) {
|
|
11831
|
+
deps.error(`Could not read ${inputPath}: ${err instanceof Error ? err.message : String(err)}`);
|
|
11832
|
+
return EXIT_USAGE2;
|
|
11833
|
+
}
|
|
11834
|
+
const logger = { warn: (message) => deps.error(`Warning: ${message}`) };
|
|
11835
|
+
let targets;
|
|
11836
|
+
try {
|
|
11837
|
+
const config = await deps.loadConfigFn(parsed.values.config);
|
|
11838
|
+
targets = config.sync ?? {};
|
|
11839
|
+
} catch (err) {
|
|
11840
|
+
deps.error(err instanceof Error ? err.message : String(err));
|
|
11841
|
+
return EXIT_USAGE2;
|
|
11842
|
+
}
|
|
11843
|
+
let built;
|
|
11844
|
+
try {
|
|
11845
|
+
built = buildProvider(
|
|
11846
|
+
{ name: providerName, targets, env: deps.env },
|
|
11847
|
+
{ fetch: deps.fetchFn, logger }
|
|
11848
|
+
);
|
|
11849
|
+
} catch (err) {
|
|
11850
|
+
deps.error(err instanceof Error ? err.message : String(err));
|
|
11851
|
+
return EXIT_USAGE2;
|
|
11852
|
+
}
|
|
11853
|
+
const targetConfig = targets[providerName] ?? {};
|
|
11854
|
+
const engineConfig = {
|
|
11855
|
+
...built.engineDefaults,
|
|
11856
|
+
...targetConfig,
|
|
11857
|
+
...parsed.values["report-url"] ? { reportUrl: parsed.values["report-url"] } : {},
|
|
11858
|
+
...attach ? { attach } : {}
|
|
11859
|
+
};
|
|
11860
|
+
const lockfilePath = parsed.values.lockfile ?? DEFAULT_LOCKFILE_PATH;
|
|
11861
|
+
const outputDir = parsed.values["output-dir"] ?? "reports";
|
|
11862
|
+
let lockfile;
|
|
11863
|
+
try {
|
|
11864
|
+
lockfile = deps.fileExists(lockfilePath) ? parseLockfile(deps.readFile(lockfilePath), lockfilePath) : emptyLockfile();
|
|
11865
|
+
} catch (err) {
|
|
11866
|
+
deps.error(err instanceof Error ? err.message : String(err));
|
|
11867
|
+
return EXIT_USAGE2;
|
|
11868
|
+
}
|
|
11869
|
+
let analysis;
|
|
11870
|
+
try {
|
|
11871
|
+
analysis = await analyzeSync({
|
|
11872
|
+
run,
|
|
11873
|
+
provider: built.provider,
|
|
11874
|
+
lockfile,
|
|
11875
|
+
config: engineConfig
|
|
11876
|
+
});
|
|
11877
|
+
} catch (err) {
|
|
11878
|
+
deps.error(`Could not read from ${providerName}: ${err instanceof Error ? err.message : String(err)}`);
|
|
11879
|
+
return EXIT_FAILED;
|
|
11880
|
+
}
|
|
11881
|
+
const jsonPath = path17.join(outputDir, `sync-coverage.${providerName}.json`);
|
|
11882
|
+
const markdownPath = path17.join(outputDir, `sync-coverage.${providerName}.md`);
|
|
11883
|
+
deps.writeFile(jsonPath, `${JSON.stringify(buildCoverageJson(analysis), null, 2)}
|
|
11884
|
+
`);
|
|
11885
|
+
deps.writeFile(markdownPath, `${renderCoverageMarkdown(analysis)}
|
|
11886
|
+
`);
|
|
11887
|
+
if (mode === "coverage") {
|
|
11888
|
+
if (!parsed.values.quiet) {
|
|
11889
|
+
deps.log(renderCoverageText(analysis));
|
|
11890
|
+
deps.log("");
|
|
11891
|
+
}
|
|
11892
|
+
deps.log(`Wrote ${jsonPath} and ${markdownPath}`);
|
|
11893
|
+
return EXIT_SUCCESS2;
|
|
11894
|
+
}
|
|
11895
|
+
const dryRun = !parsed.values.apply;
|
|
11896
|
+
deps.log(renderPlan(analysis, { dryRun }));
|
|
11897
|
+
if (dryRun) return EXIT_SUCCESS2;
|
|
11898
|
+
const applied = await applySync(
|
|
11899
|
+
{ analysis, provider: built.provider, lockfile, config: engineConfig },
|
|
11900
|
+
{ logger }
|
|
11901
|
+
);
|
|
11902
|
+
deps.writeFile(lockfilePath, serializeLockfile(lockfile));
|
|
11903
|
+
deps.log("");
|
|
11904
|
+
deps.log(renderApplyResult(applied));
|
|
11905
|
+
if (applied.errors.length > 0 && !parsed.values["continue-on-error"]) {
|
|
11906
|
+
deps.error(
|
|
11907
|
+
`
|
|
11908
|
+
${applied.errors.length} write(s) failed. A stale system of record is worse than a red build, so this exits non-zero. Pass --continue-on-error to treat it as advisory.`
|
|
11909
|
+
);
|
|
11910
|
+
return EXIT_FAILED;
|
|
11911
|
+
}
|
|
11912
|
+
return EXIT_SUCCESS2;
|
|
11913
|
+
}
|
|
11914
|
+
|
|
10023
11915
|
// src/import-openapi.ts
|
|
10024
|
-
import * as
|
|
10025
|
-
import * as
|
|
11916
|
+
import * as fs16 from "fs";
|
|
11917
|
+
import * as path18 from "path";
|
|
10026
11918
|
import { parse as parseYamlString } from "yaml";
|
|
10027
11919
|
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
10028
11920
|
function parseYaml2(raw, specPath) {
|
|
@@ -10035,9 +11927,9 @@ function parseYaml2(raw, specPath) {
|
|
|
10035
11927
|
}
|
|
10036
11928
|
}
|
|
10037
11929
|
function parseSpec(specPath) {
|
|
10038
|
-
if (!
|
|
10039
|
-
const raw =
|
|
10040
|
-
const ext =
|
|
11930
|
+
if (!fs16.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
|
|
11931
|
+
const raw = fs16.readFileSync(specPath, "utf8");
|
|
11932
|
+
const ext = path18.extname(specPath).toLowerCase();
|
|
10041
11933
|
if (ext === ".json") return JSON.parse(raw);
|
|
10042
11934
|
if (ext === ".yaml" || ext === ".yml") return parseYaml2(raw, specPath);
|
|
10043
11935
|
try {
|
|
@@ -10068,8 +11960,8 @@ function extractEndpoints(spec) {
|
|
|
10068
11960
|
}
|
|
10069
11961
|
function loadScenarios(runFile) {
|
|
10070
11962
|
if (!runFile) return [];
|
|
10071
|
-
if (!
|
|
10072
|
-
const report = JSON.parse(
|
|
11963
|
+
if (!fs16.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
|
|
11964
|
+
const report = JSON.parse(fs16.readFileSync(runFile, "utf8"));
|
|
10073
11965
|
return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
|
|
10074
11966
|
}
|
|
10075
11967
|
function endpointRefs(endpoint) {
|
|
@@ -10176,25 +12068,25 @@ async function importOpenApi(options) {
|
|
|
10176
12068
|
list.push(item);
|
|
10177
12069
|
groups.set(item.endpoint.tag, list);
|
|
10178
12070
|
}
|
|
10179
|
-
const outputDir = options.outputDir ??
|
|
10180
|
-
if (
|
|
10181
|
-
const entries =
|
|
12071
|
+
const outputDir = options.outputDir ?? path18.join("src", "content", "docs", "api");
|
|
12072
|
+
if (fs16.existsSync(outputDir) && !options.force) {
|
|
12073
|
+
const entries = fs16.readdirSync(outputDir);
|
|
10182
12074
|
if (entries.length > 0) {
|
|
10183
12075
|
throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
|
|
10184
12076
|
}
|
|
10185
12077
|
}
|
|
10186
|
-
|
|
12078
|
+
fs16.mkdirSync(outputDir, { recursive: true });
|
|
10187
12079
|
const coveredCount = coverage.filter((c) => c.status === "covered").length;
|
|
10188
12080
|
const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
|
|
10189
|
-
|
|
10190
|
-
|
|
12081
|
+
fs16.writeFileSync(
|
|
12082
|
+
path18.join(outputDir, "index.mdx"),
|
|
10191
12083
|
renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
|
|
10192
12084
|
"utf8"
|
|
10193
12085
|
);
|
|
10194
12086
|
for (const [tag, rows] of groups) {
|
|
10195
|
-
const dir =
|
|
10196
|
-
|
|
10197
|
-
|
|
12087
|
+
const dir = path18.join(outputDir, slug(tag));
|
|
12088
|
+
fs16.mkdirSync(dir, { recursive: true });
|
|
12089
|
+
fs16.writeFileSync(path18.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
|
|
10198
12090
|
}
|
|
10199
12091
|
return {
|
|
10200
12092
|
outputDir,
|
|
@@ -10205,43 +12097,12 @@ async function importOpenApi(options) {
|
|
|
10205
12097
|
};
|
|
10206
12098
|
}
|
|
10207
12099
|
|
|
10208
|
-
// src/config.ts
|
|
10209
|
-
import { existsSync as existsSync11 } from "fs";
|
|
10210
|
-
import { resolve as resolve8 } from "path";
|
|
10211
|
-
var CONFIG_CANDIDATES = ["executable-stories.config.mjs", "executable-stories.config.js"];
|
|
10212
|
-
async function loadConfig(configPath) {
|
|
10213
|
-
let resolved;
|
|
10214
|
-
if (configPath) {
|
|
10215
|
-
resolved = resolve8(configPath);
|
|
10216
|
-
} else {
|
|
10217
|
-
const present = CONFIG_CANDIDATES.map((name) => resolve8(process.cwd(), name)).filter(existsSync11);
|
|
10218
|
-
if (present.length > 1) {
|
|
10219
|
-
throw new Error(
|
|
10220
|
-
`Multiple config files found in this directory:
|
|
10221
|
-
` + present.map((p) => ` - ${p}`).join("\n") + `
|
|
10222
|
-
Keep only one, or pass --config <path> to choose which to load.`
|
|
10223
|
-
);
|
|
10224
|
-
}
|
|
10225
|
-
resolved = present[0];
|
|
10226
|
-
}
|
|
10227
|
-
if (!resolved || !existsSync11(resolved)) return {};
|
|
10228
|
-
const mod = await import(resolved);
|
|
10229
|
-
const config = mod.default;
|
|
10230
|
-
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
10231
|
-
throw new Error(
|
|
10232
|
-
`Config file at ${resolved} must export a default object. Got: ${typeof config}`
|
|
10233
|
-
);
|
|
10234
|
-
}
|
|
10235
|
-
const { formatters } = config;
|
|
10236
|
-
return formatters === void 0 ? {} : { formatters };
|
|
10237
|
-
}
|
|
10238
|
-
|
|
10239
12100
|
// src/cli.ts
|
|
10240
|
-
var
|
|
12101
|
+
var EXIT_SUCCESS3 = 0;
|
|
10241
12102
|
var EXIT_SCHEMA_VALIDATION = 1;
|
|
10242
12103
|
var EXIT_CANONICAL_VALIDATION = 2;
|
|
10243
12104
|
var EXIT_GENERATION = 3;
|
|
10244
|
-
var
|
|
12105
|
+
var EXIT_USAGE3 = 4;
|
|
10245
12106
|
var EXIT_COMPARE_GATE = 5;
|
|
10246
12107
|
var EXIT_REVIEW_GATE = 5;
|
|
10247
12108
|
var EXIT_AGENT_GATE = 5;
|
|
@@ -10270,6 +12131,8 @@ USAGE
|
|
|
10270
12131
|
executable-stories new <template> "<name>" [options]
|
|
10271
12132
|
executable-stories check-links <dir> [options]
|
|
10272
12133
|
executable-stories push <run.json> [--key <es_...>] [--url <base>] [--repo <org/name>]
|
|
12134
|
+
executable-stories coverage <testrail|xray> <run.json> [options]
|
|
12135
|
+
executable-stories sync <testrail|xray> <run.json> [--apply] [options]
|
|
10273
12136
|
executable-stories import-openapi <spec> [options]
|
|
10274
12137
|
executable-stories publish-confluence <file.adf.json> [options]
|
|
10275
12138
|
executable-stories publish-jira <file.adf.json> [options]
|
|
@@ -10294,7 +12157,9 @@ SUBCOMMANDS
|
|
|
10294
12157
|
init-astro Scaffold a thin Astro docs site (Starlight + executable-stories-astro; live stories at /stories)
|
|
10295
12158
|
new Scaffold a docs page from a template (adr, runbook, decision-log, incident, scenario-note)
|
|
10296
12159
|
check-links Scan docs for broken internal/external links (CI-friendly exit code)
|
|
10297
|
-
push Send a run (StoryReport or raw run JSON) to
|
|
12160
|
+
push Send a run (StoryReport or raw run JSON) to a cloud ingest endpoint
|
|
12161
|
+
coverage Compare your stories against a test-management system (read-only)
|
|
12162
|
+
sync Push cases, executions, and evidence to TestRail or Xray (dry run by default)
|
|
10298
12163
|
import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
|
|
10299
12164
|
publish-confluence Publish an ADF JSON file to a Confluence page via REST API
|
|
10300
12165
|
publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
|
|
@@ -10487,18 +12352,19 @@ EXIT CODES
|
|
|
10487
12352
|
function parseTextJsonFormat(flag, value) {
|
|
10488
12353
|
if (value !== "text" && value !== "json") {
|
|
10489
12354
|
console.error(`Error: ${flag} must be "text" or "json", got "${value}".`);
|
|
10490
|
-
process.exit(
|
|
12355
|
+
process.exit(EXIT_USAGE3);
|
|
10491
12356
|
}
|
|
10492
12357
|
return value;
|
|
10493
12358
|
}
|
|
10494
12359
|
async function parseCliArgs(argv) {
|
|
10495
12360
|
const args = argv.slice(2);
|
|
10496
|
-
|
|
12361
|
+
const SELF_DOCUMENTING = /* @__PURE__ */ new Set(["sync", "coverage"]);
|
|
12362
|
+
if (args.length === 0 || (args.includes("--help") || args.includes("-h")) && !SELF_DOCUMENTING.has(args[0] ?? "")) {
|
|
10497
12363
|
console.log(HELP_TEXT);
|
|
10498
|
-
process.exit(
|
|
12364
|
+
process.exit(EXIT_SUCCESS3);
|
|
10499
12365
|
}
|
|
10500
12366
|
const subcommand = args[0];
|
|
10501
|
-
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "check-explainers" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "doctor" && subcommand !== "completion" && subcommand !== "dev" && subcommand !== "init-astro" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "push" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
12367
|
+
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "check" && subcommand !== "check-explainers" && subcommand !== "goal" && subcommand !== "triage" && subcommand !== "validate" && subcommand !== "doctor" && subcommand !== "completion" && subcommand !== "dev" && subcommand !== "init-astro" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "push" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira" && subcommand !== "sync" && subcommand !== "coverage") {
|
|
10502
12368
|
if (subcommand === "serve" || subcommand === "build-docs") {
|
|
10503
12369
|
console.error(
|
|
10504
12370
|
`The "${subcommand}" subcommand was removed. Living docs are now an Astro site, rendered live from the run JSON (no Markdown generation step):
|
|
@@ -10507,12 +12373,12 @@ async function parseCliArgs(argv) {
|
|
|
10507
12373
|
3. run \`executable-stories dev\` in another \u2014 it hot-reloads the docs.
|
|
10508
12374
|
See: https://github.com/jagreehal/executable-stories (executable-stories-astro).`
|
|
10509
12375
|
);
|
|
10510
|
-
process.exit(
|
|
12376
|
+
process.exit(EXIT_USAGE3);
|
|
10511
12377
|
}
|
|
10512
12378
|
console.error(
|
|
10513
|
-
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "check-explainers", "goal", "triage", "validate", "doctor", "completion", "dev", "init-astro", "new", "check-links", "push", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
12379
|
+
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "check", "check-explainers", "goal", "triage", "validate", "doctor", "completion", "dev", "init-astro", "new", "check-links", "push", "sync", "coverage", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
10514
12380
|
);
|
|
10515
|
-
process.exit(
|
|
12381
|
+
process.exit(EXIT_USAGE3);
|
|
10516
12382
|
}
|
|
10517
12383
|
if (subcommand === "completion") {
|
|
10518
12384
|
process.exit(runCompletion(args.slice(1)));
|
|
@@ -10526,15 +12392,15 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10526
12392
|
} else {
|
|
10527
12393
|
console.log(formatDoctorReport(report));
|
|
10528
12394
|
}
|
|
10529
|
-
process.exit(report.healthy ?
|
|
12395
|
+
process.exit(report.healthy ? EXIT_SUCCESS3 : EXIT_USAGE3);
|
|
10530
12396
|
}
|
|
10531
12397
|
if (subcommand === "publish-confluence") {
|
|
10532
12398
|
await runPublishConfluence(args.slice(1));
|
|
10533
|
-
process.exit(
|
|
12399
|
+
process.exit(EXIT_SUCCESS3);
|
|
10534
12400
|
}
|
|
10535
12401
|
if (subcommand === "publish-jira") {
|
|
10536
12402
|
await runPublishJira(args.slice(1));
|
|
10537
|
-
process.exit(
|
|
12403
|
+
process.exit(EXIT_SUCCESS3);
|
|
10538
12404
|
}
|
|
10539
12405
|
if (subcommand === "deploy") {
|
|
10540
12406
|
process.exit(await runDeploy(args.slice(1)));
|
|
@@ -10548,7 +12414,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10548
12414
|
`No docs site found at ${siteDir}. Create one (scaffold + install) with:
|
|
10549
12415
|
npx executable-stories init-astro --install`
|
|
10550
12416
|
);
|
|
10551
|
-
process.exit(
|
|
12417
|
+
process.exit(EXIT_USAGE3);
|
|
10552
12418
|
}
|
|
10553
12419
|
if (dev.kind === "install-failed") {
|
|
10554
12420
|
console.error(`"${dev.pm} install" failed in ${siteDir} \u2014 run it manually, then retry.`);
|
|
@@ -10567,7 +12433,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10567
12433
|
if (update) {
|
|
10568
12434
|
console.log(`Updated ${result.targetDir} (content + config left untouched)`);
|
|
10569
12435
|
console.log(" Framework updates come via: pnpm update executable-stories-astro");
|
|
10570
|
-
process.exit(
|
|
12436
|
+
process.exit(EXIT_SUCCESS3);
|
|
10571
12437
|
}
|
|
10572
12438
|
console.log(`Scaffolded Astro docs site at ${result.targetDir}`);
|
|
10573
12439
|
const pm = detectPackageManager();
|
|
@@ -10594,17 +12460,19 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10594
12460
|
console.log("");
|
|
10595
12461
|
console.log("Everything is configured in one file: executable-stories.config.mjs");
|
|
10596
12462
|
console.log(" \u2014 sources, scenario selection (include/exclude), grouping (groupBy), docs, and theme.");
|
|
10597
|
-
process.exit(
|
|
12463
|
+
process.exit(EXIT_SUCCESS3);
|
|
10598
12464
|
} catch (err) {
|
|
10599
12465
|
console.error(`Error: ${err.message}`);
|
|
10600
|
-
process.exit(
|
|
12466
|
+
process.exit(EXIT_USAGE3);
|
|
10601
12467
|
}
|
|
10602
12468
|
}
|
|
10603
12469
|
if (subcommand === "new") process.exit(runNew(args.slice(1)));
|
|
10604
12470
|
if (subcommand === "check-links") process.exit(await runCheckLinks(args.slice(1)));
|
|
10605
12471
|
if (subcommand === "push") process.exit(await runPush(args.slice(1)));
|
|
12472
|
+
if (subcommand === "sync") process.exit(await runSyncCommand("sync", args.slice(1)));
|
|
12473
|
+
if (subcommand === "coverage") process.exit(await runSyncCommand("coverage", args.slice(1)));
|
|
10606
12474
|
if (subcommand === "import-openapi") process.exit(await runImportOpenApi(args.slice(1)));
|
|
10607
|
-
const { values, positionals } =
|
|
12475
|
+
const { values, positionals } = parseArgs3({
|
|
10608
12476
|
args: args.slice(1),
|
|
10609
12477
|
options: {
|
|
10610
12478
|
format: { type: "string", default: "html" },
|
|
@@ -10682,7 +12550,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10682
12550
|
});
|
|
10683
12551
|
if (values.help) {
|
|
10684
12552
|
console.log(HELP_TEXT);
|
|
10685
|
-
process.exit(
|
|
12553
|
+
process.exit(EXIT_SUCCESS3);
|
|
10686
12554
|
}
|
|
10687
12555
|
const userSetFormat = args.slice(1).some((a) => a === "--format" || a.startsWith("--format="));
|
|
10688
12556
|
const preset = expandPreset(
|
|
@@ -10692,7 +12560,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10692
12560
|
);
|
|
10693
12561
|
if (preset.error) {
|
|
10694
12562
|
console.error(`Error: ${preset.error}`);
|
|
10695
|
-
process.exit(
|
|
12563
|
+
process.exit(EXIT_USAGE3);
|
|
10696
12564
|
}
|
|
10697
12565
|
const useStdin = values.stdin;
|
|
10698
12566
|
const baselineValue = values.baseline;
|
|
@@ -10704,15 +12572,15 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10704
12572
|
if (isCompareLike) {
|
|
10705
12573
|
if (useStdin) {
|
|
10706
12574
|
console.error(`Error: ${subcommand} does not support --stdin. Pass baseline and current files.`);
|
|
10707
|
-
process.exit(
|
|
12575
|
+
process.exit(EXIT_USAGE3);
|
|
10708
12576
|
}
|
|
10709
12577
|
if (!currentFile) {
|
|
10710
12578
|
console.error(`Error: ${subcommand} requires <current-file>, and either <baseline-file> or --baseline auto.`);
|
|
10711
|
-
process.exit(
|
|
12579
|
+
process.exit(EXIT_USAGE3);
|
|
10712
12580
|
}
|
|
10713
12581
|
if (baselineMode === "explicit" && !baselineFile) {
|
|
10714
12582
|
console.error(`Error: ${subcommand} requires <baseline-file> and <current-file>, or use --baseline auto.`);
|
|
10715
|
-
process.exit(
|
|
12583
|
+
process.exit(EXIT_USAGE3);
|
|
10716
12584
|
}
|
|
10717
12585
|
}
|
|
10718
12586
|
let resolvedInputFile = inputFile;
|
|
@@ -10727,13 +12595,13 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10727
12595
|
Pass a path, use --stdin, or run your tests first (non-JS adapters write
|
|
10728
12596
|
${DEFAULT_RUN_FILES[0]}; set rawRunPath in a JS reporter to write ${DEFAULT_RUN_FILES[1]}).`
|
|
10729
12597
|
);
|
|
10730
|
-
process.exit(
|
|
12598
|
+
process.exit(EXIT_USAGE3);
|
|
10731
12599
|
}
|
|
10732
12600
|
}
|
|
10733
12601
|
const inputType = values["input-type"];
|
|
10734
12602
|
if (inputType !== "raw" && inputType !== "canonical" && inputType !== "ndjson") {
|
|
10735
12603
|
console.error(`Error: --input-type must be "raw", "canonical", or "ndjson", got "${inputType}".`);
|
|
10736
|
-
process.exit(
|
|
12604
|
+
process.exit(EXIT_USAGE3);
|
|
10737
12605
|
}
|
|
10738
12606
|
const pluginConfig = await loadConfig(values["config"]);
|
|
10739
12607
|
const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
|
|
@@ -10752,7 +12620,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10752
12620
|
if (unknownFormats.length > 0) {
|
|
10753
12621
|
const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
|
|
10754
12622
|
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: agent-text, astro-markdown, behavior-manifest-json, confluence, html, markdown, release-manifest, traceability-matrix, traceability-csv, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
|
|
10755
|
-
process.exit(
|
|
12623
|
+
process.exit(EXIT_USAGE3);
|
|
10756
12624
|
}
|
|
10757
12625
|
const formats = builtInRequested;
|
|
10758
12626
|
const noSynthesize = values["no-synthesize-stories"];
|
|
@@ -10761,19 +12629,19 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10761
12629
|
const validNotifyConditions = /* @__PURE__ */ new Set(["always", "on-failure", "never"]);
|
|
10762
12630
|
if (!validNotifyConditions.has(notifyValue)) {
|
|
10763
12631
|
console.error(`Error: --notify must be "always", "on-failure", or "never", got "${notifyValue}".`);
|
|
10764
|
-
process.exit(
|
|
12632
|
+
process.exit(EXIT_USAGE3);
|
|
10765
12633
|
}
|
|
10766
12634
|
const maxFailedTestsStr = values["max-failed-tests"];
|
|
10767
12635
|
const maxFailedTests = maxFailedTestsStr ? parseInt(maxFailedTestsStr, 10) : 5;
|
|
10768
12636
|
if (maxFailedTestsStr && (isNaN(maxFailedTests) || maxFailedTests < 0)) {
|
|
10769
12637
|
console.error(`Error: --max-failed-tests must be a non-negative integer, got "${maxFailedTestsStr}".`);
|
|
10770
|
-
process.exit(
|
|
12638
|
+
process.exit(EXIT_USAGE3);
|
|
10771
12639
|
}
|
|
10772
12640
|
const htmlStaleAfterDaysStr = values["html-stale-after-days"];
|
|
10773
12641
|
const htmlStaleAfterDays = htmlStaleAfterDaysStr ? parseInt(htmlStaleAfterDaysStr, 10) : 7;
|
|
10774
12642
|
if (htmlStaleAfterDaysStr && (isNaN(htmlStaleAfterDays) || htmlStaleAfterDays < 0)) {
|
|
10775
12643
|
console.error(`Error: --html-stale-after-days must be a non-negative integer, got "${htmlStaleAfterDaysStr}".`);
|
|
10776
|
-
process.exit(
|
|
12644
|
+
process.exit(EXIT_USAGE3);
|
|
10777
12645
|
}
|
|
10778
12646
|
const slackWebhook = values["slack-webhook"];
|
|
10779
12647
|
const teamsWebhook = values["teams-webhook"];
|
|
@@ -10800,7 +12668,7 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10800
12668
|
const upper = webhookMethodRaw.toUpperCase();
|
|
10801
12669
|
if (upper !== "POST" && upper !== "PUT") {
|
|
10802
12670
|
console.error(`Error: --webhook-method must be "POST" or "PUT", got "${webhookMethodRaw}".`);
|
|
10803
|
-
process.exit(
|
|
12671
|
+
process.exit(EXIT_USAGE3);
|
|
10804
12672
|
}
|
|
10805
12673
|
webhookMethod = upper;
|
|
10806
12674
|
}
|
|
@@ -10808,36 +12676,36 @@ See: https://github.com/jagreehal/executable-stories (executable-stories-astro).
|
|
|
10808
12676
|
const maxHistoryRuns = maxHistoryRunsStr ? parseInt(maxHistoryRunsStr, 10) : 10;
|
|
10809
12677
|
if (maxHistoryRunsStr && (isNaN(maxHistoryRuns) || maxHistoryRuns < 1)) {
|
|
10810
12678
|
console.error(`Error: --max-history-runs must be a positive integer, got "${maxHistoryRunsStr}".`);
|
|
10811
|
-
process.exit(
|
|
12679
|
+
process.exit(EXIT_USAGE3);
|
|
10812
12680
|
}
|
|
10813
12681
|
const maxRegressionsStr = values["max-regressions"];
|
|
10814
12682
|
const maxRegressions = maxRegressionsStr !== void 0 ? parseInt(maxRegressionsStr, 10) : void 0;
|
|
10815
12683
|
if (maxRegressionsStr !== void 0 && (isNaN(maxRegressions) || maxRegressions < 0)) {
|
|
10816
12684
|
console.error(`Error: --max-regressions must be a non-negative integer, got "${maxRegressionsStr}".`);
|
|
10817
|
-
process.exit(
|
|
12685
|
+
process.exit(EXIT_USAGE3);
|
|
10818
12686
|
}
|
|
10819
12687
|
const sortTestCasesRaw = values["sort-test-cases"];
|
|
10820
12688
|
const validSortModes = /* @__PURE__ */ new Set(["id", "source", "none"]);
|
|
10821
12689
|
if (!validSortModes.has(sortTestCasesRaw)) {
|
|
10822
12690
|
console.error(`Error: --sort-test-cases must be id, source, or none, got "${sortTestCasesRaw}".`);
|
|
10823
|
-
process.exit(
|
|
12691
|
+
process.exit(EXIT_USAGE3);
|
|
10824
12692
|
}
|
|
10825
12693
|
const assetModeRaw = values["asset-mode"];
|
|
10826
12694
|
const validAssetModes = /* @__PURE__ */ new Set(["none", "copy"]);
|
|
10827
12695
|
if (!validAssetModes.has(assetModeRaw)) {
|
|
10828
12696
|
console.error(`Error: --asset-mode must be "none" or "copy", got "${assetModeRaw}".`);
|
|
10829
|
-
process.exit(
|
|
12697
|
+
process.exit(EXIT_USAGE3);
|
|
10830
12698
|
}
|
|
10831
12699
|
const failOnRaw = values["fail-on"];
|
|
10832
12700
|
if (failOnRaw !== void 0 && failOnRaw !== "uncovered" && failOnRaw !== "weak") {
|
|
10833
12701
|
console.error(`Error: --fail-on must be "uncovered" or "weak", got "${failOnRaw}".`);
|
|
10834
|
-
process.exit(
|
|
12702
|
+
process.exit(EXIT_USAGE3);
|
|
10835
12703
|
}
|
|
10836
12704
|
const minEvidenceRaw = values["min-evidence"];
|
|
10837
12705
|
const validMinEvidence = /* @__PURE__ */ new Set(["weak", "moderate", "strong"]);
|
|
10838
12706
|
if (minEvidenceRaw !== void 0 && !validMinEvidence.has(minEvidenceRaw)) {
|
|
10839
12707
|
console.error(`Error: --min-evidence must be "weak", "moderate", or "strong", got "${minEvidenceRaw}".`);
|
|
10840
|
-
process.exit(
|
|
12708
|
+
process.exit(EXIT_USAGE3);
|
|
10841
12709
|
}
|
|
10842
12710
|
const checkFormat = parseTextJsonFormat("--check-format", values["check-format"]);
|
|
10843
12711
|
const goalFormat = parseTextJsonFormat("--goal-format", values["goal-format"]);
|
|
@@ -10921,27 +12789,27 @@ async function readInput(args) {
|
|
|
10921
12789
|
if (args.stdin) {
|
|
10922
12790
|
return readStdin();
|
|
10923
12791
|
}
|
|
10924
|
-
const filePath =
|
|
10925
|
-
if (!
|
|
12792
|
+
const filePath = path19.resolve(args.inputFile);
|
|
12793
|
+
if (!fs17.existsSync(filePath)) {
|
|
10926
12794
|
console.error(`Error: File not found: ${filePath}`);
|
|
10927
|
-
process.exit(
|
|
12795
|
+
process.exit(EXIT_USAGE3);
|
|
10928
12796
|
}
|
|
10929
|
-
return
|
|
12797
|
+
return fs17.readFileSync(filePath, "utf8");
|
|
10930
12798
|
}
|
|
10931
12799
|
function readFileInput(filePath) {
|
|
10932
|
-
const resolved =
|
|
10933
|
-
if (!
|
|
12800
|
+
const resolved = path19.resolve(filePath);
|
|
12801
|
+
if (!fs17.existsSync(resolved)) {
|
|
10934
12802
|
console.error(`Error: File not found: ${resolved}`);
|
|
10935
|
-
process.exit(
|
|
12803
|
+
process.exit(EXIT_USAGE3);
|
|
10936
12804
|
}
|
|
10937
|
-
return
|
|
12805
|
+
return fs17.readFileSync(resolved, "utf8");
|
|
10938
12806
|
}
|
|
10939
12807
|
function readStdin() {
|
|
10940
|
-
return new Promise((
|
|
12808
|
+
return new Promise((resolve12, reject) => {
|
|
10941
12809
|
const chunks = [];
|
|
10942
12810
|
process.stdin.setEncoding("utf8");
|
|
10943
12811
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
10944
|
-
process.stdin.on("end", () =>
|
|
12812
|
+
process.stdin.on("end", () => resolve12(chunks.join("")));
|
|
10945
12813
|
process.stdin.on("error", reject);
|
|
10946
12814
|
});
|
|
10947
12815
|
}
|
|
@@ -10951,7 +12819,7 @@ function parseJson(text2) {
|
|
|
10951
12819
|
} catch (err) {
|
|
10952
12820
|
const msg = err instanceof Error ? err.message : String(err);
|
|
10953
12821
|
console.error(`Error: Invalid JSON \u2014 ${msg}`);
|
|
10954
|
-
process.exit(
|
|
12822
|
+
process.exit(EXIT_USAGE3);
|
|
10955
12823
|
}
|
|
10956
12824
|
}
|
|
10957
12825
|
function tryParseJson(text2) {
|
|
@@ -10989,13 +12857,13 @@ ${msg}`);
|
|
|
10989
12857
|
let raw = data;
|
|
10990
12858
|
let droppedMissingStory = 0;
|
|
10991
12859
|
if (args.synthesizeStories) {
|
|
10992
|
-
raw =
|
|
12860
|
+
raw = synthesizeStories4(raw);
|
|
10993
12861
|
} else {
|
|
10994
12862
|
const before = raw.testCases.length;
|
|
10995
12863
|
const withStory = raw.testCases.filter((tc) => tc.story != null).length;
|
|
10996
12864
|
droppedMissingStory = before - withStory;
|
|
10997
12865
|
}
|
|
10998
|
-
const canonical =
|
|
12866
|
+
const canonical = canonicalizeRun6(raw);
|
|
10999
12867
|
try {
|
|
11000
12868
|
assertValidRun2(canonical);
|
|
11001
12869
|
} catch (err) {
|
|
@@ -11076,9 +12944,9 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
11076
12944
|
if (!schemaResult.valid) return void 0;
|
|
11077
12945
|
let raw = data;
|
|
11078
12946
|
if (args.synthesizeStories) {
|
|
11079
|
-
raw =
|
|
12947
|
+
raw = synthesizeStories4(raw);
|
|
11080
12948
|
}
|
|
11081
|
-
const canonical =
|
|
12949
|
+
const canonical = canonicalizeRun6(raw);
|
|
11082
12950
|
try {
|
|
11083
12951
|
assertValidRun2(canonical);
|
|
11084
12952
|
return canonical;
|
|
@@ -11087,14 +12955,14 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
11087
12955
|
}
|
|
11088
12956
|
}
|
|
11089
12957
|
function listBaselineCandidates(currentFile, args) {
|
|
11090
|
-
const baselineDir =
|
|
11091
|
-
const currentResolved =
|
|
11092
|
-
if (!
|
|
12958
|
+
const baselineDir = path19.resolve(args.baselineDir ?? path19.dirname(currentFile));
|
|
12959
|
+
const currentResolved = path19.resolve(currentFile);
|
|
12960
|
+
if (!fs17.existsSync(baselineDir)) {
|
|
11093
12961
|
console.error(`Error: baseline directory not found: ${baselineDir}`);
|
|
11094
|
-
process.exit(
|
|
12962
|
+
process.exit(EXIT_USAGE3);
|
|
11095
12963
|
}
|
|
11096
|
-
const entries =
|
|
11097
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
12964
|
+
const entries = fs17.readdirSync(baselineDir, { withFileTypes: true });
|
|
12965
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path19.join(baselineDir, entry.name)).filter((candidate) => path19.resolve(candidate) !== currentResolved).filter(
|
|
11098
12966
|
(candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
|
|
11099
12967
|
);
|
|
11100
12968
|
}
|
|
@@ -11102,21 +12970,21 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
11102
12970
|
const candidates = listBaselineCandidates(currentFile, args);
|
|
11103
12971
|
const comparable = [];
|
|
11104
12972
|
for (const candidate of candidates) {
|
|
11105
|
-
const run = tryNormalizeRunFromText(
|
|
12973
|
+
const run = tryNormalizeRunFromText(fs17.readFileSync(candidate, "utf8"), args);
|
|
11106
12974
|
if (run) {
|
|
11107
12975
|
comparable.push({ file: candidate, run });
|
|
11108
12976
|
}
|
|
11109
12977
|
}
|
|
11110
12978
|
if (comparable.length === 0) {
|
|
11111
12979
|
console.error(
|
|
11112
|
-
`Error: no compatible baseline files found in ${
|
|
12980
|
+
`Error: no compatible baseline files found in ${path19.resolve(args.baselineDir ?? path19.dirname(currentFile))}.`
|
|
11113
12981
|
);
|
|
11114
|
-
process.exit(
|
|
12982
|
+
process.exit(EXIT_USAGE3);
|
|
11115
12983
|
}
|
|
11116
12984
|
const picked = pickAutoBaseline(currentRun, comparable);
|
|
11117
12985
|
if (!picked) {
|
|
11118
12986
|
console.error("Error: unable to choose an automatic baseline.");
|
|
11119
|
-
process.exit(
|
|
12987
|
+
process.exit(EXIT_USAGE3);
|
|
11120
12988
|
}
|
|
11121
12989
|
return picked.file;
|
|
11122
12990
|
}
|
|
@@ -11126,7 +12994,7 @@ function resolveBaselineRun(args, currentRun) {
|
|
|
11126
12994
|
if (args.baselineArg === "auto") {
|
|
11127
12995
|
if (!args.inputFile) {
|
|
11128
12996
|
console.error("Error: --baseline auto requires a current input file (not --stdin).");
|
|
11129
|
-
process.exit(
|
|
12997
|
+
process.exit(EXIT_USAGE3);
|
|
11130
12998
|
}
|
|
11131
12999
|
baselineFile = resolveBaselineAuto(args.inputFile, currentRun, args);
|
|
11132
13000
|
} else {
|
|
@@ -11172,7 +13040,7 @@ async function runCompare(ctx) {
|
|
|
11172
13040
|
}
|
|
11173
13041
|
process.exit(EXIT_COMPARE_GATE);
|
|
11174
13042
|
}
|
|
11175
|
-
process.exit(
|
|
13043
|
+
process.exit(EXIT_SUCCESS3);
|
|
11176
13044
|
} catch (err) {
|
|
11177
13045
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11178
13046
|
console.error(`Comparison failed: ${msg}`);
|
|
@@ -11217,7 +13085,7 @@ async function runGateRelease(ctx) {
|
|
|
11217
13085
|
process.exit(EXIT_RELEASE_GATE);
|
|
11218
13086
|
}
|
|
11219
13087
|
console.error("Release gate passed: RC matches dev baseline.");
|
|
11220
|
-
process.exit(
|
|
13088
|
+
process.exit(EXIT_SUCCESS3);
|
|
11221
13089
|
} catch (err) {
|
|
11222
13090
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11223
13091
|
console.error(`Release gate check failed: ${msg}`);
|
|
@@ -11249,7 +13117,7 @@ async function runReview(ctx) {
|
|
|
11249
13117
|
}
|
|
11250
13118
|
process.exit(EXIT_REVIEW_GATE);
|
|
11251
13119
|
}
|
|
11252
|
-
process.exit(
|
|
13120
|
+
process.exit(EXIT_SUCCESS3);
|
|
11253
13121
|
} catch (err) {
|
|
11254
13122
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11255
13123
|
console.error(`Review failed: ${msg}`);
|
|
@@ -11265,7 +13133,7 @@ async function runList(ctx) {
|
|
|
11265
13133
|
const validListFormats = /* @__PURE__ */ new Set(["text", "json", "csv", "markdown-table"]);
|
|
11266
13134
|
if (!validListFormats.has(resolvedFormat)) {
|
|
11267
13135
|
console.error(`Error: Unknown list format "${resolvedFormat}". Valid: text, json, csv, markdown-table.`);
|
|
11268
|
-
process.exit(
|
|
13136
|
+
process.exit(EXIT_USAGE3);
|
|
11269
13137
|
}
|
|
11270
13138
|
const output = listScenarios(
|
|
11271
13139
|
{
|
|
@@ -11276,7 +13144,7 @@ async function runList(ctx) {
|
|
|
11276
13144
|
{}
|
|
11277
13145
|
);
|
|
11278
13146
|
console.log(output);
|
|
11279
|
-
process.exit(
|
|
13147
|
+
process.exit(EXIT_SUCCESS3);
|
|
11280
13148
|
}
|
|
11281
13149
|
async function runCheck(ctx) {
|
|
11282
13150
|
const { args } = ctx;
|
|
@@ -11291,17 +13159,17 @@ async function runCheck(ctx) {
|
|
|
11291
13159
|
if (report.summary.failed > 0 && !args.noFail) {
|
|
11292
13160
|
process.exit(EXIT_AGENT_GATE);
|
|
11293
13161
|
}
|
|
11294
|
-
process.exit(
|
|
13162
|
+
process.exit(EXIT_SUCCESS3);
|
|
11295
13163
|
}
|
|
11296
13164
|
async function runCheckExplainers(ctx) {
|
|
11297
13165
|
const { args } = ctx;
|
|
11298
13166
|
if (!args.explainersDir) {
|
|
11299
13167
|
console.error("Error: check-explainers requires --explainers-dir <dir> (the folder of explainer markdown).");
|
|
11300
|
-
process.exit(
|
|
13168
|
+
process.exit(EXIT_USAGE3);
|
|
11301
13169
|
}
|
|
11302
|
-
if (!
|
|
13170
|
+
if (!fs17.existsSync(args.explainersDir) || !fs17.statSync(args.explainersDir).isDirectory()) {
|
|
11303
13171
|
console.error(`Error: --explainers-dir "${args.explainersDir}" is not a directory.`);
|
|
11304
|
-
process.exit(
|
|
13172
|
+
process.exit(EXIT_USAGE3);
|
|
11305
13173
|
}
|
|
11306
13174
|
const text2 = await readInput(args);
|
|
11307
13175
|
const run = applySelection(normalizeRunFromText(text2, args).run, args);
|
|
@@ -11310,7 +13178,7 @@ async function runCheckExplainers(ctx) {
|
|
|
11310
13178
|
if (explainersGateFailed(report) && !args.noFail) {
|
|
11311
13179
|
process.exit(EXIT_AGENT_GATE);
|
|
11312
13180
|
}
|
|
11313
|
-
process.exit(
|
|
13181
|
+
process.exit(EXIT_SUCCESS3);
|
|
11314
13182
|
}
|
|
11315
13183
|
async function runGoal(ctx) {
|
|
11316
13184
|
const { args } = ctx;
|
|
@@ -11331,7 +13199,7 @@ async function runGoal(ctx) {
|
|
|
11331
13199
|
{}
|
|
11332
13200
|
);
|
|
11333
13201
|
console.log(renderGoal(report, args.goalFormat));
|
|
11334
|
-
process.exit(report.met ?
|
|
13202
|
+
process.exit(report.met ? EXIT_SUCCESS3 : EXIT_AGENT_GATE);
|
|
11335
13203
|
}
|
|
11336
13204
|
async function runTriage(ctx) {
|
|
11337
13205
|
const { args } = ctx;
|
|
@@ -11343,13 +13211,13 @@ async function runTriage(ctx) {
|
|
|
11343
13211
|
{}
|
|
11344
13212
|
);
|
|
11345
13213
|
console.log(renderTriage(report, args.triageFormat));
|
|
11346
|
-
process.exit(
|
|
13214
|
+
process.exit(EXIT_SUCCESS3);
|
|
11347
13215
|
}
|
|
11348
13216
|
async function runWatch(ctx) {
|
|
11349
13217
|
const { args } = ctx;
|
|
11350
13218
|
if (!args.inputFile) {
|
|
11351
13219
|
console.error("Error: watch requires an input file (the raw-run JSON the framework writes).");
|
|
11352
|
-
process.exit(
|
|
13220
|
+
process.exit(EXIT_USAGE3);
|
|
11353
13221
|
}
|
|
11354
13222
|
console.log(
|
|
11355
13223
|
`Watching ${args.inputFile} \u2192 regenerating [${args.formats.join(", ")}] into ${args.outputDir}/ (Ctrl+C to stop)`
|
|
@@ -11398,7 +13266,7 @@ async function runFormatOrValidate(ctx) {
|
|
|
11398
13266
|
}
|
|
11399
13267
|
}
|
|
11400
13268
|
console.log(`Valid NDJSON (${lines.length} envelopes).`);
|
|
11401
|
-
process.exit(
|
|
13269
|
+
process.exit(EXIT_SUCCESS3);
|
|
11402
13270
|
}
|
|
11403
13271
|
let run;
|
|
11404
13272
|
try {
|
|
@@ -11409,9 +13277,9 @@ async function runFormatOrValidate(ctx) {
|
|
|
11409
13277
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
11410
13278
|
}
|
|
11411
13279
|
if (args.emitCanonical) {
|
|
11412
|
-
const outPath =
|
|
11413
|
-
|
|
11414
|
-
|
|
13280
|
+
const outPath = path19.resolve(args.emitCanonical);
|
|
13281
|
+
fs17.mkdirSync(path19.dirname(outPath), { recursive: true });
|
|
13282
|
+
fs17.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
11415
13283
|
}
|
|
11416
13284
|
try {
|
|
11417
13285
|
const history = runHistoryPipeline(run, args);
|
|
@@ -11419,7 +13287,7 @@ async function runFormatOrValidate(ctx) {
|
|
|
11419
13287
|
runCustomFormatters(run, customRequested, pluginConfig.formatters ?? {}, args);
|
|
11420
13288
|
await dispatchNotifications(run, args);
|
|
11421
13289
|
printResult(result, args, startMs);
|
|
11422
|
-
process.exit(
|
|
13290
|
+
process.exit(EXIT_SUCCESS3);
|
|
11423
13291
|
} catch (err) {
|
|
11424
13292
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11425
13293
|
console.error(`Generation failed: ${msg}`);
|
|
@@ -11433,7 +13301,7 @@ async function runFormatOrValidate(ctx) {
|
|
|
11433
13301
|
assertValidRun2(data);
|
|
11434
13302
|
warnLargeStateDocs(data.testCases);
|
|
11435
13303
|
console.log("Valid canonical TestRunResult.");
|
|
11436
|
-
process.exit(
|
|
13304
|
+
process.exit(EXIT_SUCCESS3);
|
|
11437
13305
|
} catch (err) {
|
|
11438
13306
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11439
13307
|
console.error(msg);
|
|
@@ -11457,7 +13325,7 @@ async function runFormatOrValidate(ctx) {
|
|
|
11457
13325
|
}
|
|
11458
13326
|
warnLargeStateDocs(data.testCases);
|
|
11459
13327
|
console.log("Valid RawRun (schemaVersion 1).");
|
|
11460
|
-
process.exit(
|
|
13328
|
+
process.exit(EXIT_SUCCESS3);
|
|
11461
13329
|
}
|
|
11462
13330
|
if (args.inputType === "canonical") {
|
|
11463
13331
|
try {
|
|
@@ -11470,9 +13338,9 @@ ${msg}`);
|
|
|
11470
13338
|
}
|
|
11471
13339
|
const run = data;
|
|
11472
13340
|
if (args.emitCanonical) {
|
|
11473
|
-
const outPath =
|
|
11474
|
-
|
|
11475
|
-
|
|
13341
|
+
const outPath = path19.resolve(args.emitCanonical);
|
|
13342
|
+
fs17.mkdirSync(path19.dirname(outPath), { recursive: true });
|
|
13343
|
+
fs17.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
11476
13344
|
}
|
|
11477
13345
|
try {
|
|
11478
13346
|
const history = runHistoryPipeline(run, args);
|
|
@@ -11480,7 +13348,7 @@ ${msg}`);
|
|
|
11480
13348
|
runCustomFormatters(run, customRequested, pluginConfig.formatters ?? {}, args);
|
|
11481
13349
|
await dispatchNotifications(run, args);
|
|
11482
13350
|
printResult(result, args, startMs);
|
|
11483
|
-
process.exit(
|
|
13351
|
+
process.exit(EXIT_SUCCESS3);
|
|
11484
13352
|
} catch (err) {
|
|
11485
13353
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11486
13354
|
console.error(`Generation failed: ${msg}`);
|
|
@@ -11505,7 +13373,7 @@ ${msg}`);
|
|
|
11505
13373
|
let raw = data;
|
|
11506
13374
|
let droppedMissingStory = 0;
|
|
11507
13375
|
if (args.synthesizeStories) {
|
|
11508
|
-
raw =
|
|
13376
|
+
raw = synthesizeStories4(raw);
|
|
11509
13377
|
} else {
|
|
11510
13378
|
const before = raw.testCases.length;
|
|
11511
13379
|
const withStory = raw.testCases.filter(
|
|
@@ -11518,7 +13386,7 @@ ${msg}`);
|
|
|
11518
13386
|
);
|
|
11519
13387
|
}
|
|
11520
13388
|
}
|
|
11521
|
-
const canonical =
|
|
13389
|
+
const canonical = canonicalizeRun6(raw);
|
|
11522
13390
|
try {
|
|
11523
13391
|
assertValidRun2(canonical);
|
|
11524
13392
|
} catch (err) {
|
|
@@ -11528,9 +13396,9 @@ ${msg}`);
|
|
|
11528
13396
|
process.exit(EXIT_CANONICAL_VALIDATION);
|
|
11529
13397
|
}
|
|
11530
13398
|
if (args.emitCanonical) {
|
|
11531
|
-
const outPath =
|
|
11532
|
-
|
|
11533
|
-
|
|
13399
|
+
const outPath = path19.resolve(args.emitCanonical);
|
|
13400
|
+
fs17.mkdirSync(path19.dirname(outPath), { recursive: true });
|
|
13401
|
+
fs17.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
|
|
11534
13402
|
}
|
|
11535
13403
|
try {
|
|
11536
13404
|
const history = runHistoryPipeline(canonical, args);
|
|
@@ -11538,7 +13406,7 @@ ${msg}`);
|
|
|
11538
13406
|
runCustomFormatters(canonical, customRequested, pluginConfig.formatters ?? {}, args);
|
|
11539
13407
|
await dispatchNotifications(canonical, args);
|
|
11540
13408
|
printResult(result, args, startMs, droppedMissingStory);
|
|
11541
|
-
process.exit(
|
|
13409
|
+
process.exit(EXIT_SUCCESS3);
|
|
11542
13410
|
} catch (err) {
|
|
11543
13411
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11544
13412
|
console.error(`Generation failed: ${msg}`);
|
|
@@ -11555,9 +13423,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
|
|
|
11555
13423
|
const ext = formatter.fileExtension ?? formatName;
|
|
11556
13424
|
const baseName = args.outputName ?? "report";
|
|
11557
13425
|
const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
|
|
11558
|
-
const filepath =
|
|
11559
|
-
|
|
11560
|
-
|
|
13426
|
+
const filepath = path19.join(outputDir, filename);
|
|
13427
|
+
fs17.mkdirSync(outputDir, { recursive: true });
|
|
13428
|
+
fs17.writeFileSync(filepath, content, "utf8");
|
|
11561
13429
|
console.log(`Generated: ${filepath}`);
|
|
11562
13430
|
} catch (err) {
|
|
11563
13431
|
console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -11607,13 +13475,13 @@ async function dispatchNotifications(run, args) {
|
|
|
11607
13475
|
}
|
|
11608
13476
|
function runHistoryPipeline(run, args) {
|
|
11609
13477
|
if (!args.historyFile) return void 0;
|
|
11610
|
-
const historyPath =
|
|
13478
|
+
const historyPath = path19.resolve(args.historyFile);
|
|
11611
13479
|
const store = loadHistory(
|
|
11612
13480
|
{ filePath: historyPath },
|
|
11613
13481
|
{
|
|
11614
13482
|
readFile: (p) => {
|
|
11615
13483
|
try {
|
|
11616
|
-
return
|
|
13484
|
+
return fs17.readFileSync(p, "utf8");
|
|
11617
13485
|
} catch {
|
|
11618
13486
|
return void 0;
|
|
11619
13487
|
}
|
|
@@ -11626,11 +13494,11 @@ function runHistoryPipeline(run, args) {
|
|
|
11626
13494
|
run,
|
|
11627
13495
|
maxRuns: args.maxHistoryRuns
|
|
11628
13496
|
});
|
|
11629
|
-
const dir =
|
|
11630
|
-
|
|
13497
|
+
const dir = path19.dirname(historyPath);
|
|
13498
|
+
fs17.mkdirSync(dir, { recursive: true });
|
|
11631
13499
|
saveHistory(
|
|
11632
13500
|
{ filePath: historyPath, store: updated },
|
|
11633
|
-
{ writeFile: (p, content) =>
|
|
13501
|
+
{ writeFile: (p, content) => fs17.writeFileSync(p, content, "utf8") }
|
|
11634
13502
|
);
|
|
11635
13503
|
let metricsCount = 0;
|
|
11636
13504
|
for (const testId of Object.keys(updated.tests)) {
|
|
@@ -11781,7 +13649,7 @@ function loadReviewContext(args) {
|
|
|
11781
13649
|
console.error(
|
|
11782
13650
|
"Error: --code-diff requires --patch <file> (generate it with: git diff --histogram > changes.patch)."
|
|
11783
13651
|
);
|
|
11784
|
-
process.exit(
|
|
13652
|
+
process.exit(EXIT_USAGE3);
|
|
11785
13653
|
}
|
|
11786
13654
|
const sidecar = JSON.parse(readFileInput(args.codeDiffPath));
|
|
11787
13655
|
const patch = readFileInput(args.patchPath);
|
|
@@ -11801,11 +13669,11 @@ function writeReviewReport(review, args) {
|
|
|
11801
13669
|
const outputDir = args.outputDir ?? "reports";
|
|
11802
13670
|
const baseName = args.outputName ?? "evidence-review";
|
|
11803
13671
|
const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
|
|
11804
|
-
|
|
11805
|
-
const mdPath =
|
|
11806
|
-
const htmlPath =
|
|
11807
|
-
|
|
11808
|
-
|
|
13672
|
+
fs17.mkdirSync(outputDir, { recursive: true });
|
|
13673
|
+
const mdPath = path19.join(outputDir, `${baseName}${suffix}.md`);
|
|
13674
|
+
const htmlPath = path19.join(outputDir, `${baseName}${suffix}.html`);
|
|
13675
|
+
fs17.writeFileSync(mdPath, markdown, "utf8");
|
|
13676
|
+
fs17.writeFileSync(htmlPath, html, "utf8");
|
|
11809
13677
|
return [mdPath, htmlPath];
|
|
11810
13678
|
}
|
|
11811
13679
|
function evaluateReviewGate(review, args) {
|
|
@@ -11860,9 +13728,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
11860
13728
|
function printCompareResult(result, args, startMs) {
|
|
11861
13729
|
const durationMs = Date.now() - startMs;
|
|
11862
13730
|
if (result.prSummary && args.prSummaryFile) {
|
|
11863
|
-
const outputPath =
|
|
11864
|
-
|
|
11865
|
-
|
|
13731
|
+
const outputPath = path19.resolve(args.prSummaryFile);
|
|
13732
|
+
fs17.mkdirSync(path19.dirname(outputPath), { recursive: true });
|
|
13733
|
+
fs17.writeFileSync(outputPath, result.prSummary, "utf8");
|
|
11866
13734
|
}
|
|
11867
13735
|
if (args.jsonSummary) {
|
|
11868
13736
|
console.log(
|
|
@@ -11896,13 +13764,13 @@ function printCompareResult(result, args, startMs) {
|
|
|
11896
13764
|
}
|
|
11897
13765
|
}
|
|
11898
13766
|
function loadReleasePolicy(policyPath) {
|
|
11899
|
-
const resolved =
|
|
11900
|
-
if (!
|
|
13767
|
+
const resolved = path19.resolve(policyPath);
|
|
13768
|
+
if (!fs17.existsSync(resolved)) {
|
|
11901
13769
|
console.error(`Error: release policy file not found: ${resolved}`);
|
|
11902
|
-
process.exit(
|
|
13770
|
+
process.exit(EXIT_USAGE3);
|
|
11903
13771
|
}
|
|
11904
13772
|
try {
|
|
11905
|
-
const raw = JSON.parse(
|
|
13773
|
+
const raw = JSON.parse(fs17.readFileSync(resolved, "utf8"));
|
|
11906
13774
|
return {
|
|
11907
13775
|
allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
|
|
11908
13776
|
allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
|
|
@@ -11911,7 +13779,7 @@ function loadReleasePolicy(policyPath) {
|
|
|
11911
13779
|
} catch (err) {
|
|
11912
13780
|
const msg = err instanceof Error ? err.message : String(err);
|
|
11913
13781
|
console.error(`Error reading release policy: ${msg}`);
|
|
11914
|
-
process.exit(
|
|
13782
|
+
process.exit(EXIT_USAGE3);
|
|
11915
13783
|
}
|
|
11916
13784
|
}
|
|
11917
13785
|
function applyReleasePolicy(result, policy) {
|
|
@@ -11963,7 +13831,7 @@ function evaluateCompareGate(result, args) {
|
|
|
11963
13831
|
return failures;
|
|
11964
13832
|
}
|
|
11965
13833
|
async function runPublishConfluence(rawArgs) {
|
|
11966
|
-
const { values, positionals } =
|
|
13834
|
+
const { values, positionals } = parseArgs3({
|
|
11967
13835
|
args: rawArgs,
|
|
11968
13836
|
options: {
|
|
11969
13837
|
"page-id": { type: "string" },
|
|
@@ -11999,16 +13867,16 @@ Optional:
|
|
|
11999
13867
|
--help Show this help
|
|
12000
13868
|
|
|
12001
13869
|
Generate an API token at https://id.atlassian.com/manage-profile/security/api-tokens`);
|
|
12002
|
-
process.exit(
|
|
13870
|
+
process.exit(EXIT_SUCCESS3);
|
|
12003
13871
|
}
|
|
12004
13872
|
const inputFile = positionals[0];
|
|
12005
13873
|
if (!inputFile) {
|
|
12006
13874
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
12007
|
-
process.exit(
|
|
13875
|
+
process.exit(EXIT_USAGE3);
|
|
12008
13876
|
}
|
|
12009
|
-
if (!
|
|
13877
|
+
if (!fs17.existsSync(inputFile)) {
|
|
12010
13878
|
console.error(`Error: file not found: ${inputFile}`);
|
|
12011
|
-
process.exit(
|
|
13879
|
+
process.exit(EXIT_USAGE3);
|
|
12012
13880
|
}
|
|
12013
13881
|
const baseUrl = values["base-url"] ?? process.env.CONFLUENCE_BASE_URL;
|
|
12014
13882
|
const email = values.email ?? process.env.CONFLUENCE_EMAIL;
|
|
@@ -12022,19 +13890,19 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12022
13890
|
console.error(
|
|
12023
13891
|
"Error: --base-url or CONFLUENCE_BASE_URL is required (e.g. https://acme.atlassian.net/wiki)"
|
|
12024
13892
|
);
|
|
12025
|
-
process.exit(
|
|
13893
|
+
process.exit(EXIT_USAGE3);
|
|
12026
13894
|
}
|
|
12027
13895
|
if (!pageId && !spaceId) {
|
|
12028
13896
|
console.error(
|
|
12029
13897
|
"Error: specify either --page-id (to update) or --space-id (to create)"
|
|
12030
13898
|
);
|
|
12031
|
-
process.exit(
|
|
13899
|
+
process.exit(EXIT_USAGE3);
|
|
12032
13900
|
}
|
|
12033
13901
|
if (!pageId && !title) {
|
|
12034
13902
|
console.error("Error: --title is required when creating a new page");
|
|
12035
|
-
process.exit(
|
|
13903
|
+
process.exit(EXIT_USAGE3);
|
|
12036
13904
|
}
|
|
12037
|
-
const adf =
|
|
13905
|
+
const adf = fs17.readFileSync(path19.resolve(inputFile), "utf8");
|
|
12038
13906
|
if (dryRun) {
|
|
12039
13907
|
console.log(
|
|
12040
13908
|
JSON.stringify(
|
|
@@ -12051,13 +13919,13 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12051
13919
|
2
|
|
12052
13920
|
)
|
|
12053
13921
|
);
|
|
12054
|
-
process.exit(
|
|
13922
|
+
process.exit(EXIT_SUCCESS3);
|
|
12055
13923
|
}
|
|
12056
13924
|
if (!email || !token) {
|
|
12057
13925
|
console.error(
|
|
12058
13926
|
"Error: --email/CONFLUENCE_EMAIL and --token/CONFLUENCE_TOKEN are required unless --dry-run is set"
|
|
12059
13927
|
);
|
|
12060
|
-
process.exit(
|
|
13928
|
+
process.exit(EXIT_USAGE3);
|
|
12061
13929
|
}
|
|
12062
13930
|
try {
|
|
12063
13931
|
const result = await publishConfluencePage(
|
|
@@ -12067,14 +13935,14 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12067
13935
|
console.log(
|
|
12068
13936
|
`${result.action === "created" ? "Created" : "Updated"} "${result.title}" (v${result.version}) \u2192 ${result.url}`
|
|
12069
13937
|
);
|
|
12070
|
-
process.exit(
|
|
13938
|
+
process.exit(EXIT_SUCCESS3);
|
|
12071
13939
|
} catch (err) {
|
|
12072
13940
|
console.error(`Error: ${err.message}`);
|
|
12073
13941
|
process.exit(EXIT_GENERATION);
|
|
12074
13942
|
}
|
|
12075
13943
|
}
|
|
12076
13944
|
async function runPublishJira(rawArgs) {
|
|
12077
|
-
const { values, positionals } =
|
|
13945
|
+
const { values, positionals } = parseArgs3({
|
|
12078
13946
|
args: rawArgs,
|
|
12079
13947
|
options: {
|
|
12080
13948
|
issue: { type: "string" },
|
|
@@ -12106,16 +13974,16 @@ Optional:
|
|
|
12106
13974
|
--help Show this help
|
|
12107
13975
|
|
|
12108
13976
|
Generate an API token at https://id.atlassian.com/manage-profile/security/api-tokens`);
|
|
12109
|
-
process.exit(
|
|
13977
|
+
process.exit(EXIT_SUCCESS3);
|
|
12110
13978
|
}
|
|
12111
13979
|
const inputFile = positionals[0];
|
|
12112
13980
|
if (!inputFile) {
|
|
12113
13981
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
12114
|
-
process.exit(
|
|
13982
|
+
process.exit(EXIT_USAGE3);
|
|
12115
13983
|
}
|
|
12116
|
-
if (!
|
|
13984
|
+
if (!fs17.existsSync(inputFile)) {
|
|
12117
13985
|
console.error(`Error: file not found: ${inputFile}`);
|
|
12118
|
-
process.exit(
|
|
13986
|
+
process.exit(EXIT_USAGE3);
|
|
12119
13987
|
}
|
|
12120
13988
|
const baseUrl = values["base-url"] ?? process.env.JIRA_BASE_URL;
|
|
12121
13989
|
const email = values.email ?? process.env.JIRA_EMAIL;
|
|
@@ -12127,20 +13995,20 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12127
13995
|
console.error(
|
|
12128
13996
|
"Error: --base-url or JIRA_BASE_URL is required (e.g. https://acme.atlassian.net)"
|
|
12129
13997
|
);
|
|
12130
|
-
process.exit(
|
|
13998
|
+
process.exit(EXIT_USAGE3);
|
|
12131
13999
|
}
|
|
12132
14000
|
if (!issueKey) {
|
|
12133
14001
|
console.error("Error: --issue <KEY> is required (e.g. --issue PROJ-123)");
|
|
12134
|
-
process.exit(
|
|
14002
|
+
process.exit(EXIT_USAGE3);
|
|
12135
14003
|
}
|
|
12136
14004
|
if (modeRaw !== "comment" && modeRaw !== "description") {
|
|
12137
14005
|
console.error(
|
|
12138
14006
|
`Error: --mode must be "comment" or "description" (got "${modeRaw}")`
|
|
12139
14007
|
);
|
|
12140
|
-
process.exit(
|
|
14008
|
+
process.exit(EXIT_USAGE3);
|
|
12141
14009
|
}
|
|
12142
14010
|
const mode = modeRaw;
|
|
12143
|
-
const adf =
|
|
14011
|
+
const adf = fs17.readFileSync(path19.resolve(inputFile), "utf8");
|
|
12144
14012
|
if (dryRun) {
|
|
12145
14013
|
console.log(
|
|
12146
14014
|
JSON.stringify(
|
|
@@ -12155,13 +14023,13 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12155
14023
|
2
|
|
12156
14024
|
)
|
|
12157
14025
|
);
|
|
12158
|
-
process.exit(
|
|
14026
|
+
process.exit(EXIT_SUCCESS3);
|
|
12159
14027
|
}
|
|
12160
14028
|
if (!email || !token) {
|
|
12161
14029
|
console.error(
|
|
12162
14030
|
"Error: --email/JIRA_EMAIL and --token/JIRA_TOKEN are required unless --dry-run is set"
|
|
12163
14031
|
);
|
|
12164
|
-
process.exit(
|
|
14032
|
+
process.exit(EXIT_USAGE3);
|
|
12165
14033
|
}
|
|
12166
14034
|
try {
|
|
12167
14035
|
const result = await publishJiraIssue(
|
|
@@ -12175,14 +14043,14 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
12175
14043
|
} else {
|
|
12176
14044
|
console.log(`Updated description for ${result.issueKey} \u2192 ${result.url}`);
|
|
12177
14045
|
}
|
|
12178
|
-
process.exit(
|
|
14046
|
+
process.exit(EXIT_SUCCESS3);
|
|
12179
14047
|
} catch (err) {
|
|
12180
14048
|
console.error(`Error: ${err.message}`);
|
|
12181
14049
|
process.exit(EXIT_GENERATION);
|
|
12182
14050
|
}
|
|
12183
14051
|
}
|
|
12184
14052
|
function runNew(rawArgs) {
|
|
12185
|
-
const { values, positionals } =
|
|
14053
|
+
const { values, positionals } = parseArgs3({
|
|
12186
14054
|
args: rawArgs,
|
|
12187
14055
|
options: {
|
|
12188
14056
|
dir: { type: "string" },
|
|
@@ -12199,7 +14067,7 @@ function runNew(rawArgs) {
|
|
|
12199
14067
|
`Usage: executable-stories new <template> "<name>" [--dir <docs-dir>] [--scenario-id <id>] [--force]`
|
|
12200
14068
|
);
|
|
12201
14069
|
console.error(`Templates: ${TEMPLATES.join(", ")}`);
|
|
12202
|
-
return
|
|
14070
|
+
return EXIT_USAGE3;
|
|
12203
14071
|
}
|
|
12204
14072
|
try {
|
|
12205
14073
|
const result = scaffoldDoc({
|
|
@@ -12213,36 +14081,41 @@ function runNew(rawArgs) {
|
|
|
12213
14081
|
console.log(` Title: ${result.title}`);
|
|
12214
14082
|
console.log("");
|
|
12215
14083
|
console.log("Next: fill in the content and link verifying stories in `verifiedBy`.");
|
|
12216
|
-
return
|
|
14084
|
+
return EXIT_SUCCESS3;
|
|
12217
14085
|
} catch (err) {
|
|
12218
14086
|
console.error(`Error: ${err.message}`);
|
|
12219
|
-
return
|
|
14087
|
+
return EXIT_USAGE3;
|
|
12220
14088
|
}
|
|
12221
14089
|
}
|
|
12222
14090
|
async function runCheckLinks(rawArgs) {
|
|
12223
|
-
const { values, positionals } =
|
|
14091
|
+
const { values, positionals } = parseArgs3({
|
|
12224
14092
|
args: rawArgs,
|
|
12225
14093
|
options: {
|
|
12226
14094
|
external: { type: "boolean", default: false },
|
|
12227
|
-
json: { type: "boolean", default: false }
|
|
14095
|
+
json: { type: "boolean", default: false },
|
|
14096
|
+
"site-root": { type: "string" },
|
|
14097
|
+
assets: { type: "string", multiple: true }
|
|
12228
14098
|
},
|
|
12229
14099
|
allowPositionals: true,
|
|
12230
14100
|
strict: true
|
|
12231
14101
|
});
|
|
12232
14102
|
try {
|
|
14103
|
+
const assets = values.assets;
|
|
12233
14104
|
const report = await checkLinks({
|
|
12234
14105
|
target: positionals[0] ?? ".",
|
|
12235
|
-
checkExternal: values.external
|
|
14106
|
+
checkExternal: values.external,
|
|
14107
|
+
...values["site-root"] ? { siteRoot: values["site-root"] } : {},
|
|
14108
|
+
...assets && assets.length > 0 ? { assetRoots: assets } : {}
|
|
12236
14109
|
});
|
|
12237
14110
|
console.log(values.json ? JSON.stringify(report, null, 2) : formatLinkReport(report));
|
|
12238
|
-
return report.brokenCount > 0 ? EXIT_GENERATION :
|
|
14111
|
+
return report.brokenCount > 0 ? EXIT_GENERATION : EXIT_SUCCESS3;
|
|
12239
14112
|
} catch (err) {
|
|
12240
14113
|
console.error(`Error: ${err.message}`);
|
|
12241
|
-
return
|
|
14114
|
+
return EXIT_USAGE3;
|
|
12242
14115
|
}
|
|
12243
14116
|
}
|
|
12244
14117
|
async function runImportOpenApi(rawArgs) {
|
|
12245
|
-
const { values, positionals } =
|
|
14118
|
+
const { values, positionals } = parseArgs3({
|
|
12246
14119
|
args: rawArgs,
|
|
12247
14120
|
options: {
|
|
12248
14121
|
"output-dir": { type: "string" },
|
|
@@ -12255,7 +14128,7 @@ async function runImportOpenApi(rawArgs) {
|
|
|
12255
14128
|
const spec = positionals[0];
|
|
12256
14129
|
if (!spec) {
|
|
12257
14130
|
console.error(`Usage: executable-stories import-openapi <spec.json|yaml> [--output-dir <dir>] [--run <story-report.json>] [--force]`);
|
|
12258
|
-
return
|
|
14131
|
+
return EXIT_USAGE3;
|
|
12259
14132
|
}
|
|
12260
14133
|
try {
|
|
12261
14134
|
const result = await importOpenApi({
|
|
@@ -12269,10 +14142,10 @@ async function runImportOpenApi(rawArgs) {
|
|
|
12269
14142
|
if (result.uncoveredCount > 0) {
|
|
12270
14143
|
console.log(` \u26A0 ${result.uncoveredCount} endpoint(s) have no verifying story`);
|
|
12271
14144
|
}
|
|
12272
|
-
return
|
|
14145
|
+
return EXIT_SUCCESS3;
|
|
12273
14146
|
} catch (err) {
|
|
12274
14147
|
console.error(`Error: ${err.message}`);
|
|
12275
|
-
return
|
|
14148
|
+
return EXIT_USAGE3;
|
|
12276
14149
|
}
|
|
12277
14150
|
}
|
|
12278
14151
|
async function runDeploy(rawArgs) {
|
|
@@ -12282,9 +14155,9 @@ async function runDeploy(rawArgs) {
|
|
|
12282
14155
|
console.error(" deploy record <file> --env <env> [--tag <tag>] [--ledger <path>]");
|
|
12283
14156
|
console.error(" deploy status [--ledger <path>] [--json]");
|
|
12284
14157
|
console.error(" deploy diff <env-a> <env-b> [--ledger <path>] [--json]");
|
|
12285
|
-
return
|
|
14158
|
+
return EXIT_USAGE3;
|
|
12286
14159
|
}
|
|
12287
|
-
const { values, positionals } =
|
|
14160
|
+
const { values, positionals } = parseArgs3({
|
|
12288
14161
|
args: rawArgs.slice(1),
|
|
12289
14162
|
options: {
|
|
12290
14163
|
env: { type: "string" },
|
|
@@ -12309,19 +14182,19 @@ OPTIONS
|
|
|
12309
14182
|
--tag <tag> Optional Git tag for this deployment (e.g. v1.2.3)
|
|
12310
14183
|
--ledger <path> Path to deployment ledger JSON (default: .executable-stories/deployments.json)
|
|
12311
14184
|
--json Output as JSON instead of text`);
|
|
12312
|
-
return
|
|
14185
|
+
return EXIT_SUCCESS3;
|
|
12313
14186
|
}
|
|
12314
14187
|
const ledgerPath = values.ledger;
|
|
12315
14188
|
if (mode === "record") {
|
|
12316
14189
|
const inputFile = positionals[0];
|
|
12317
14190
|
if (!inputFile) {
|
|
12318
14191
|
console.error("Error: deploy record requires an input file.");
|
|
12319
|
-
return
|
|
14192
|
+
return EXIT_USAGE3;
|
|
12320
14193
|
}
|
|
12321
14194
|
const env = values.env;
|
|
12322
14195
|
if (!env) {
|
|
12323
14196
|
console.error("Error: deploy record requires --env <environment>.");
|
|
12324
|
-
return
|
|
14197
|
+
return EXIT_USAGE3;
|
|
12325
14198
|
}
|
|
12326
14199
|
const text2 = readFileInput(inputFile);
|
|
12327
14200
|
const { run } = normalizeRunFromText(text2, {
|
|
@@ -12347,14 +14220,14 @@ OPTIONS
|
|
|
12347
14220
|
console.error(` Tag: ${result.entry.tag}`);
|
|
12348
14221
|
}
|
|
12349
14222
|
console.error(` Ledger: ${result.ledgerPath}`);
|
|
12350
|
-
return
|
|
14223
|
+
return EXIT_SUCCESS3;
|
|
12351
14224
|
}
|
|
12352
14225
|
if (mode === "status") {
|
|
12353
14226
|
const status = getDeploymentStatus(ledgerPath);
|
|
12354
14227
|
const envs = Object.keys(status.environments);
|
|
12355
14228
|
if (envs.length === 0) {
|
|
12356
14229
|
console.error("No deployments recorded yet.");
|
|
12357
|
-
return
|
|
14230
|
+
return EXIT_SUCCESS3;
|
|
12358
14231
|
}
|
|
12359
14232
|
if (values.json) {
|
|
12360
14233
|
console.log(JSON.stringify(status, null, 2));
|
|
@@ -12380,14 +14253,14 @@ OPTIONS
|
|
|
12380
14253
|
}
|
|
12381
14254
|
console.log(`Ledger: ${ledgerPath}`);
|
|
12382
14255
|
}
|
|
12383
|
-
return
|
|
14256
|
+
return EXIT_SUCCESS3;
|
|
12384
14257
|
}
|
|
12385
14258
|
if (mode === "diff") {
|
|
12386
14259
|
const envA = positionals[0];
|
|
12387
14260
|
const envB = positionals[1];
|
|
12388
14261
|
if (!envA || !envB) {
|
|
12389
14262
|
console.error("Error: deploy diff requires two environment names.");
|
|
12390
|
-
return
|
|
14263
|
+
return EXIT_USAGE3;
|
|
12391
14264
|
}
|
|
12392
14265
|
try {
|
|
12393
14266
|
const drift = getEnvironmentDrift(ledgerPath, envA, envB);
|
|
@@ -12433,11 +14306,11 @@ OPTIONS
|
|
|
12433
14306
|
}
|
|
12434
14307
|
} catch (err) {
|
|
12435
14308
|
console.error(`Error: ${err.message}`);
|
|
12436
|
-
return
|
|
14309
|
+
return EXIT_USAGE3;
|
|
12437
14310
|
}
|
|
12438
|
-
return
|
|
14311
|
+
return EXIT_SUCCESS3;
|
|
12439
14312
|
}
|
|
12440
|
-
return
|
|
14313
|
+
return EXIT_USAGE3;
|
|
12441
14314
|
}
|
|
12442
14315
|
function createDefaultCliArgs() {
|
|
12443
14316
|
return {
|
|
@@ -12493,6 +14366,6 @@ function createDefaultCliArgs() {
|
|
|
12493
14366
|
}
|
|
12494
14367
|
main().catch((err) => {
|
|
12495
14368
|
console.error(err);
|
|
12496
|
-
process.exit(
|
|
14369
|
+
process.exit(EXIT_USAGE3);
|
|
12497
14370
|
});
|
|
12498
14371
|
//# sourceMappingURL=cli.js.map
|