executable-stories-formatters 0.11.0 → 0.11.1
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 +722 -193
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +245 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +88 -2
- package/dist/index.d.ts +88 -2
- package/dist/index.js +237 -41
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
// src/cli.ts
|
|
4
4
|
import { parseArgs } from "util";
|
|
5
|
-
import * as
|
|
6
|
-
import * as
|
|
5
|
+
import * as fs14 from "fs";
|
|
6
|
+
import * as path15 from "path";
|
|
7
7
|
|
|
8
8
|
// src/validation/schema-validator.ts
|
|
9
9
|
import Ajv from "ajv/dist/2020.js";
|
|
@@ -558,17 +558,17 @@ function validateRawRun(data) {
|
|
|
558
558
|
return { valid: true, errors: [] };
|
|
559
559
|
}
|
|
560
560
|
const errors = (validate.errors ?? []).map((err) => {
|
|
561
|
-
const
|
|
561
|
+
const path16 = err.instancePath || "/";
|
|
562
562
|
const message = err.message ?? "unknown error";
|
|
563
563
|
if (err.keyword === "additionalProperties") {
|
|
564
564
|
const extra = err.params.additionalProperty;
|
|
565
|
-
return `${
|
|
565
|
+
return `${path16}: ${message} \u2014 '${extra}'`;
|
|
566
566
|
}
|
|
567
567
|
if (err.keyword === "enum") {
|
|
568
568
|
const allowed = err.params.allowedValues;
|
|
569
|
-
return `${
|
|
569
|
+
return `${path16}: ${message} \u2014 allowed: ${JSON.stringify(allowed)}`;
|
|
570
570
|
}
|
|
571
|
-
return `${
|
|
571
|
+
return `${path16}: ${message}`;
|
|
572
572
|
});
|
|
573
573
|
return { valid: false, errors };
|
|
574
574
|
}
|
|
@@ -1043,7 +1043,7 @@ ${result.errors.join("\n")}`);
|
|
|
1043
1043
|
|
|
1044
1044
|
// src/index.ts
|
|
1045
1045
|
import "fs";
|
|
1046
|
-
import * as
|
|
1046
|
+
import * as path9 from "path";
|
|
1047
1047
|
import * as fsPromises from "fs/promises";
|
|
1048
1048
|
|
|
1049
1049
|
// src/converters/acl/lines.ts
|
|
@@ -1631,23 +1631,23 @@ function buildFeature(relSourceFile, group) {
|
|
|
1631
1631
|
function ensureUniqueFeatureIds(features) {
|
|
1632
1632
|
const seen = /* @__PURE__ */ new Map();
|
|
1633
1633
|
for (const f of features) {
|
|
1634
|
-
const
|
|
1635
|
-
if (
|
|
1636
|
-
seen.set(f.id,
|
|
1634
|
+
const count2 = seen.get(f.id) ?? 0;
|
|
1635
|
+
if (count2 > 0) f.id = `${f.id}-${count2 + 1}`;
|
|
1636
|
+
seen.set(f.id, count2 + 1);
|
|
1637
1637
|
}
|
|
1638
1638
|
}
|
|
1639
1639
|
function ensureUniqueScenarioIds(feature) {
|
|
1640
1640
|
const seen = /* @__PURE__ */ new Map();
|
|
1641
1641
|
for (const s of feature.scenarios) {
|
|
1642
|
-
const
|
|
1643
|
-
if (
|
|
1644
|
-
const newId = `${s.id}-${
|
|
1642
|
+
const count2 = seen.get(s.id) ?? 0;
|
|
1643
|
+
if (count2 > 0) {
|
|
1644
|
+
const newId = `${s.id}-${count2 + 1}`;
|
|
1645
1645
|
for (const step of s.steps) {
|
|
1646
1646
|
step.id = step.id.replace(s.id, newId);
|
|
1647
1647
|
}
|
|
1648
1648
|
s.id = newId;
|
|
1649
1649
|
}
|
|
1650
|
-
seen.set(s.id,
|
|
1650
|
+
seen.set(s.id, count2 + 1);
|
|
1651
1651
|
}
|
|
1652
1652
|
}
|
|
1653
1653
|
function toStoryReport(run) {
|
|
@@ -16145,6 +16145,88 @@ function groupBy6(items, keyFn) {
|
|
|
16145
16145
|
return map;
|
|
16146
16146
|
}
|
|
16147
16147
|
|
|
16148
|
+
// src/formatters/release-manifest.ts
|
|
16149
|
+
import { createHash as createHash2 } from "crypto";
|
|
16150
|
+
var ReleaseManifestFormatter = class {
|
|
16151
|
+
format(run) {
|
|
16152
|
+
const manifest = toReleaseManifest(run);
|
|
16153
|
+
const lines = [];
|
|
16154
|
+
lines.push("# Release Manifest");
|
|
16155
|
+
lines.push("");
|
|
16156
|
+
lines.push(`Generated: ${manifest.generatedAt}`);
|
|
16157
|
+
lines.push(`Run: ${manifest.run.startedAt} to ${manifest.run.finishedAt}`);
|
|
16158
|
+
if (manifest.run.branch) lines.push(`Branch: ${manifest.run.branch}`);
|
|
16159
|
+
if (manifest.run.gitSha) lines.push(`Commit: ${manifest.run.gitSha}`);
|
|
16160
|
+
lines.push(`Tested-together hash: \`${manifest.testedTogetherHash}\``);
|
|
16161
|
+
lines.push("");
|
|
16162
|
+
lines.push("| Scenarios | Passed | Failed | Skipped | Pending |");
|
|
16163
|
+
lines.push("| ---: | ---: | ---: | ---: | ---: |");
|
|
16164
|
+
lines.push(`| ${manifest.run.total} | ${manifest.run.passed} | ${manifest.run.failed} | ${manifest.run.skipped} | ${manifest.run.pending} |`);
|
|
16165
|
+
lines.push("");
|
|
16166
|
+
lines.push("## Scenarios");
|
|
16167
|
+
lines.push("");
|
|
16168
|
+
lines.push("| Status | Scenario | Source | Tags |");
|
|
16169
|
+
lines.push("| --- | --- | --- | --- |");
|
|
16170
|
+
for (const scenario of manifest.scenarios) {
|
|
16171
|
+
const source = `${scenario.sourceFile}:${scenario.sourceLine}`;
|
|
16172
|
+
const tags = scenario.tags.length > 0 ? scenario.tags.map((tag) => `\`${tag}\``).join(", ") : "";
|
|
16173
|
+
lines.push(`| ${renderStatus(scenario.status)} | ${escapePipe(scenario.title)} | \`${source}\` | ${tags} |`);
|
|
16174
|
+
}
|
|
16175
|
+
return lines.join("\n");
|
|
16176
|
+
}
|
|
16177
|
+
};
|
|
16178
|
+
function toReleaseManifest(run) {
|
|
16179
|
+
const scenarios = [...run.testCases].sort((a, b) => a.id.localeCompare(b.id)).map((tc) => ({
|
|
16180
|
+
id: tc.id,
|
|
16181
|
+
title: tc.story.scenario,
|
|
16182
|
+
status: tc.status,
|
|
16183
|
+
sourceFile: tc.sourceFile,
|
|
16184
|
+
sourceLine: tc.sourceLine,
|
|
16185
|
+
tags: tc.tags
|
|
16186
|
+
}));
|
|
16187
|
+
const fingerprint = scenarios.map((scenario) => `${scenario.id}:${scenario.status}`).join("\n");
|
|
16188
|
+
return {
|
|
16189
|
+
schemaVersion: "1.0",
|
|
16190
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
16191
|
+
run: {
|
|
16192
|
+
startedAt: new Date(run.startedAtMs).toISOString(),
|
|
16193
|
+
finishedAt: new Date(run.finishedAtMs).toISOString(),
|
|
16194
|
+
gitSha: run.gitSha,
|
|
16195
|
+
branch: getBranch(run),
|
|
16196
|
+
total: run.testCases.length,
|
|
16197
|
+
passed: count(run.testCases, "passed"),
|
|
16198
|
+
failed: count(run.testCases, "failed"),
|
|
16199
|
+
skipped: count(run.testCases, "skipped"),
|
|
16200
|
+
pending: count(run.testCases, "pending")
|
|
16201
|
+
},
|
|
16202
|
+
testedTogetherHash: createHash2("sha256").update(fingerprint).digest("hex"),
|
|
16203
|
+
scenarios
|
|
16204
|
+
};
|
|
16205
|
+
}
|
|
16206
|
+
function getBranch(run) {
|
|
16207
|
+
return run.ci?.branch;
|
|
16208
|
+
}
|
|
16209
|
+
function count(testCases, status) {
|
|
16210
|
+
return testCases.filter((tc) => tc.status === status).length;
|
|
16211
|
+
}
|
|
16212
|
+
function renderStatus(status) {
|
|
16213
|
+
switch (status) {
|
|
16214
|
+
case "passed":
|
|
16215
|
+
return "passed";
|
|
16216
|
+
case "failed":
|
|
16217
|
+
return "failed";
|
|
16218
|
+
case "skipped":
|
|
16219
|
+
return "skipped";
|
|
16220
|
+
case "pending":
|
|
16221
|
+
return "pending";
|
|
16222
|
+
default:
|
|
16223
|
+
return status;
|
|
16224
|
+
}
|
|
16225
|
+
}
|
|
16226
|
+
function escapePipe(value) {
|
|
16227
|
+
return value.replace(/\|/g, "\\|");
|
|
16228
|
+
}
|
|
16229
|
+
|
|
16148
16230
|
// src/formatters/cucumber-messages/synthesize-feature.ts
|
|
16149
16231
|
function extractFeatureName(testCases, uri) {
|
|
16150
16232
|
for (const tc of testCases) {
|
|
@@ -16211,7 +16293,7 @@ function synthesizeFeature(uri, testCases) {
|
|
|
16211
16293
|
}
|
|
16212
16294
|
|
|
16213
16295
|
// src/utils/cucumber-messages.ts
|
|
16214
|
-
import { createHash as
|
|
16296
|
+
import { createHash as createHash3 } from "crypto";
|
|
16215
16297
|
function msToTimestamp(ms) {
|
|
16216
16298
|
const seconds = Math.floor(ms / 1e3);
|
|
16217
16299
|
const nanos = Math.round(ms % 1e3 * 1e6);
|
|
@@ -16277,7 +16359,7 @@ function statusToCucumberStatus(status) {
|
|
|
16277
16359
|
}
|
|
16278
16360
|
function deterministicId(kind, salt, ...parts) {
|
|
16279
16361
|
const input = [salt, kind, ...parts].join("::");
|
|
16280
|
-
return
|
|
16362
|
+
return createHash3("sha1").update(input).digest("hex").slice(0, 36);
|
|
16281
16363
|
}
|
|
16282
16364
|
|
|
16283
16365
|
// src/formatters/cucumber-messages/build-gherkin-document.ts
|
|
@@ -16765,8 +16847,8 @@ function extractDocAttachments(step) {
|
|
|
16765
16847
|
}
|
|
16766
16848
|
return attachments;
|
|
16767
16849
|
}
|
|
16768
|
-
function guessMediaType(
|
|
16769
|
-
const lower =
|
|
16850
|
+
function guessMediaType(path16) {
|
|
16851
|
+
const lower = path16.toLowerCase();
|
|
16770
16852
|
if (lower.endsWith(".png")) return "image/png";
|
|
16771
16853
|
if (lower.endsWith(".jpg") || lower.endsWith(".jpeg")) return "image/jpeg";
|
|
16772
16854
|
if (lower.endsWith(".gif")) return "image/gif";
|
|
@@ -16907,11 +16989,11 @@ var CucumberHtmlFormatter = class {
|
|
|
16907
16989
|
for (const envelope of envelopes) {
|
|
16908
16990
|
const accepted = htmlStream.write(envelope);
|
|
16909
16991
|
if (!accepted) {
|
|
16910
|
-
await new Promise((
|
|
16992
|
+
await new Promise((resolve12) => htmlStream.once("drain", resolve12));
|
|
16911
16993
|
}
|
|
16912
16994
|
}
|
|
16913
|
-
await new Promise((
|
|
16914
|
-
collector.on("finish",
|
|
16995
|
+
await new Promise((resolve12, reject) => {
|
|
16996
|
+
collector.on("finish", resolve12);
|
|
16915
16997
|
collector.on("error", reject);
|
|
16916
16998
|
htmlStream.end();
|
|
16917
16999
|
});
|
|
@@ -17005,15 +17087,15 @@ function createPrCommentSummary(diff, maxScenarios = 10) {
|
|
|
17005
17087
|
function getCommit(run) {
|
|
17006
17088
|
return run.ci?.commitSha ?? run.gitSha;
|
|
17007
17089
|
}
|
|
17008
|
-
function
|
|
17090
|
+
function getBranch2(run) {
|
|
17009
17091
|
return run.ci?.branch;
|
|
17010
17092
|
}
|
|
17011
17093
|
function pickAutoBaseline(currentRun, candidates) {
|
|
17012
|
-
const currentBranch =
|
|
17094
|
+
const currentBranch = getBranch2(currentRun);
|
|
17013
17095
|
const currentCommit = getCommit(currentRun);
|
|
17014
17096
|
return [...candidates].sort((a, b) => {
|
|
17015
|
-
const aSameBranch = Boolean(currentBranch &&
|
|
17016
|
-
const bSameBranch = Boolean(currentBranch &&
|
|
17097
|
+
const aSameBranch = Boolean(currentBranch && getBranch2(a.run) && currentBranch === getBranch2(a.run));
|
|
17098
|
+
const bSameBranch = Boolean(currentBranch && getBranch2(b.run) && currentBranch === getBranch2(b.run));
|
|
17017
17099
|
if (aSameBranch !== bSameBranch) {
|
|
17018
17100
|
return Number(bSameBranch) - Number(aSameBranch);
|
|
17019
17101
|
}
|
|
@@ -18005,8 +18087,8 @@ ${body}`;
|
|
|
18005
18087
|
}
|
|
18006
18088
|
buildFrontmatter(run) {
|
|
18007
18089
|
const badge = _AstroFormatter.computeBadge(run.testCases);
|
|
18008
|
-
const
|
|
18009
|
-
const description = `${
|
|
18090
|
+
const count2 = run.testCases.length;
|
|
18091
|
+
const description = `${count2} scenario${count2 !== 1 ? "s" : ""} \u2014 ${badge.text.toLowerCase()}`;
|
|
18010
18092
|
const lines = [
|
|
18011
18093
|
"---",
|
|
18012
18094
|
`title: ${yamlScalar(this.deriveTitle(run))}`,
|
|
@@ -19988,18 +20070,18 @@ function deriveChangeType(tags) {
|
|
|
19988
20070
|
}
|
|
19989
20071
|
return "unknown";
|
|
19990
20072
|
}
|
|
19991
|
-
function extensionOf(
|
|
19992
|
-
const base =
|
|
20073
|
+
function extensionOf(path16) {
|
|
20074
|
+
const base = path16.split("/").pop() ?? path16;
|
|
19993
20075
|
const dot = base.lastIndexOf(".");
|
|
19994
20076
|
return dot === -1 ? "" : base.slice(dot + 1).toLowerCase();
|
|
19995
20077
|
}
|
|
19996
|
-
function isTestFile(
|
|
19997
|
-
return TEST_INFIX.test(
|
|
20078
|
+
function isTestFile(path16) {
|
|
20079
|
+
return TEST_INFIX.test(path16);
|
|
19998
20080
|
}
|
|
19999
|
-
function isReviewableSource(
|
|
20000
|
-
if (isTestFile(
|
|
20001
|
-
if (
|
|
20002
|
-
return CODE_EXTENSIONS.has(extensionOf(
|
|
20081
|
+
function isReviewableSource(path16) {
|
|
20082
|
+
if (isTestFile(path16)) return false;
|
|
20083
|
+
if (path16.endsWith(".d.ts")) return false;
|
|
20084
|
+
return CODE_EXTENSIONS.has(extensionOf(path16));
|
|
20003
20085
|
}
|
|
20004
20086
|
function testBaseKey(testFile) {
|
|
20005
20087
|
return testFile.replace(TEST_INFIX, "");
|
|
@@ -20103,7 +20185,7 @@ function toClaim(testCase, changedSourcePaths) {
|
|
|
20103
20185
|
const { strength, reasons } = gradeEvidence(testCase, audience);
|
|
20104
20186
|
const key = testBaseKey(testCase.sourceFile);
|
|
20105
20187
|
const coversFiles = changedSourcePaths.filter(
|
|
20106
|
-
(
|
|
20188
|
+
(path16) => sourceBaseKey(path16) === key
|
|
20107
20189
|
);
|
|
20108
20190
|
return {
|
|
20109
20191
|
id: testCase.id,
|
|
@@ -20633,11 +20715,116 @@ applyTheme(getEffectiveTheme());` : "";
|
|
|
20633
20715
|
}
|
|
20634
20716
|
};
|
|
20635
20717
|
|
|
20718
|
+
// src/deploy/ledger.ts
|
|
20719
|
+
import * as fs7 from "fs";
|
|
20720
|
+
import * as path8 from "path";
|
|
20721
|
+
function createEmptyLedger() {
|
|
20722
|
+
return {
|
|
20723
|
+
deployments: [],
|
|
20724
|
+
schemaVersion: 1
|
|
20725
|
+
};
|
|
20726
|
+
}
|
|
20727
|
+
function loadLedger(ledgerPath) {
|
|
20728
|
+
const resolved = path8.resolve(ledgerPath);
|
|
20729
|
+
if (!fs7.existsSync(resolved)) {
|
|
20730
|
+
return createEmptyLedger();
|
|
20731
|
+
}
|
|
20732
|
+
try {
|
|
20733
|
+
const raw = JSON.parse(fs7.readFileSync(resolved, "utf8"));
|
|
20734
|
+
if (raw.schemaVersion !== 1) {
|
|
20735
|
+
throw new Error(`Unsupported ledger schemaVersion: ${raw.schemaVersion}`);
|
|
20736
|
+
}
|
|
20737
|
+
return raw;
|
|
20738
|
+
} catch (err) {
|
|
20739
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
20740
|
+
throw new Error(`Failed to load deployment ledger at ${resolved}: ${msg}`);
|
|
20741
|
+
}
|
|
20742
|
+
}
|
|
20743
|
+
function saveLedger(ledger, ledgerPath) {
|
|
20744
|
+
const resolved = path8.resolve(ledgerPath);
|
|
20745
|
+
const dir = path8.dirname(resolved);
|
|
20746
|
+
fs7.mkdirSync(dir, { recursive: true });
|
|
20747
|
+
fs7.writeFileSync(resolved, JSON.stringify(ledger, null, 2), "utf8");
|
|
20748
|
+
}
|
|
20749
|
+
function getLatestDeployment(ledger, environment) {
|
|
20750
|
+
return [...ledger.deployments].reverse().find((d) => d.environment === environment);
|
|
20751
|
+
}
|
|
20752
|
+
|
|
20753
|
+
// src/deploy/index.ts
|
|
20754
|
+
function recordDeployment(args) {
|
|
20755
|
+
const ledger = loadLedger(args.ledgerPath);
|
|
20756
|
+
const previous = getLatestDeployment(ledger, args.environment);
|
|
20757
|
+
const entry = {
|
|
20758
|
+
environment: args.environment,
|
|
20759
|
+
tag: args.tag,
|
|
20760
|
+
sha: args.run.gitSha,
|
|
20761
|
+
runFile: args.runFilePath,
|
|
20762
|
+
scenarioIds: args.run.testCases.map((tc) => tc.id),
|
|
20763
|
+
scenarioStatuses: Object.fromEntries(args.run.testCases.map((tc) => [tc.id, tc.status])),
|
|
20764
|
+
timestamp: new Date(args.run.finishedAtMs).toISOString(),
|
|
20765
|
+
summary: countStatuses(args.run)
|
|
20766
|
+
};
|
|
20767
|
+
ledger.deployments.push(entry);
|
|
20768
|
+
if (previous) {
|
|
20769
|
+
const previousIds = new Set(previous.scenarioIds);
|
|
20770
|
+
const added = entry.scenarioIds.filter((id) => !previousIds.has(id)).length;
|
|
20771
|
+
const removed = previous.scenarioIds.filter((id) => !entry.scenarioIds.includes(id)).length;
|
|
20772
|
+
if (added > 0 || removed > 0) {
|
|
20773
|
+
}
|
|
20774
|
+
}
|
|
20775
|
+
saveLedger(ledger, args.ledgerPath);
|
|
20776
|
+
return { entry, ledgerPath: args.ledgerPath };
|
|
20777
|
+
}
|
|
20778
|
+
function getDeploymentStatus(ledgerPath) {
|
|
20779
|
+
const ledger = loadLedger(ledgerPath);
|
|
20780
|
+
const environments = {};
|
|
20781
|
+
for (const entry of ledger.deployments) {
|
|
20782
|
+
environments[entry.environment] = {
|
|
20783
|
+
latest: entry,
|
|
20784
|
+
previousDeployment: environments[entry.environment]?.latest
|
|
20785
|
+
};
|
|
20786
|
+
}
|
|
20787
|
+
return { environments, ledgerPath };
|
|
20788
|
+
}
|
|
20789
|
+
function getEnvironmentDrift(ledgerPath, envA, envB) {
|
|
20790
|
+
const ledger = loadLedger(ledgerPath);
|
|
20791
|
+
const aEntry = getLatestDeployment(ledger, envA);
|
|
20792
|
+
const bEntry = getLatestDeployment(ledger, envB);
|
|
20793
|
+
if (!aEntry) {
|
|
20794
|
+
throw new Error(`No deployment found for environment "${envA}"`);
|
|
20795
|
+
}
|
|
20796
|
+
if (!bEntry) {
|
|
20797
|
+
throw new Error(`No deployment found for environment "${envB}"`);
|
|
20798
|
+
}
|
|
20799
|
+
const aIds = new Set(aEntry.scenarioIds);
|
|
20800
|
+
const bIds = new Set(bEntry.scenarioIds);
|
|
20801
|
+
const onlyInA = aEntry.scenarioIds.filter((id) => !bIds.has(id));
|
|
20802
|
+
const onlyInB = bEntry.scenarioIds.filter((id) => !aIds.has(id));
|
|
20803
|
+
const inBoth = aEntry.scenarioIds.filter((id) => bIds.has(id));
|
|
20804
|
+
const statusChanged = inBoth.map((id) => ({
|
|
20805
|
+
id,
|
|
20806
|
+
statusA: aEntry.scenarioStatuses?.[id] ?? "unknown",
|
|
20807
|
+
statusB: bEntry.scenarioStatuses?.[id] ?? "unknown"
|
|
20808
|
+
})).filter((item) => item.statusA !== item.statusB);
|
|
20809
|
+
return { environmentA: envA, environmentB: envB, onlyInA, onlyInB, inBoth, statusChanged, aEntry, bEntry };
|
|
20810
|
+
}
|
|
20811
|
+
function countStatuses(run) {
|
|
20812
|
+
const summary = { total: 0, passed: 0, failed: 0, skipped: 0, pending: 0 };
|
|
20813
|
+
for (const tc of run.testCases) {
|
|
20814
|
+
summary.total++;
|
|
20815
|
+
if (tc.status in summary) {
|
|
20816
|
+
summary[tc.status]++;
|
|
20817
|
+
}
|
|
20818
|
+
}
|
|
20819
|
+
return summary;
|
|
20820
|
+
}
|
|
20821
|
+
|
|
20636
20822
|
// src/index.ts
|
|
20637
20823
|
var FORMAT_EXTENSIONS = {
|
|
20638
20824
|
astro: ".md",
|
|
20639
20825
|
"behavior-manifest-json": ".behavior-manifest.json",
|
|
20640
20826
|
markdown: ".md",
|
|
20827
|
+
"release-manifest": ".release-manifest.md",
|
|
20641
20828
|
html: ".html",
|
|
20642
20829
|
"cucumber-html": ".cucumber.html",
|
|
20643
20830
|
junit: ".junit.xml",
|
|
@@ -20676,11 +20863,11 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
20676
20863
|
const ext = FORMAT_EXTENSIONS[format];
|
|
20677
20864
|
const effectiveName = outputName + (outputNameSuffix ?? "");
|
|
20678
20865
|
if (mode === "aggregated") {
|
|
20679
|
-
return toPosix(
|
|
20866
|
+
return toPosix(path9.join(baseOutputDir, joinNameAndExt(effectiveName, ext)));
|
|
20680
20867
|
}
|
|
20681
20868
|
const normalizedSource = toPosix(sourceFile);
|
|
20682
|
-
const dirOfSource =
|
|
20683
|
-
let baseName =
|
|
20869
|
+
const dirOfSource = path9.posix.dirname(normalizedSource);
|
|
20870
|
+
let baseName = path9.posix.basename(normalizedSource);
|
|
20684
20871
|
for (const testExt of TEST_EXTENSIONS) {
|
|
20685
20872
|
if (baseName.endsWith(testExt)) {
|
|
20686
20873
|
baseName = baseName.slice(0, -testExt.length);
|
|
@@ -20689,12 +20876,12 @@ function computeOutputPath(sourceFile, format, mode, colocatedStyle, baseOutputD
|
|
|
20689
20876
|
}
|
|
20690
20877
|
const fileName = `${baseName}.${effectiveName}${ext}`;
|
|
20691
20878
|
if (colocatedStyle === "adjacent") {
|
|
20692
|
-
return toPosix(
|
|
20879
|
+
return toPosix(path9.posix.join(dirOfSource, fileName));
|
|
20693
20880
|
}
|
|
20694
20881
|
if (colocatedStyle === "flat") {
|
|
20695
|
-
return toPosix(
|
|
20882
|
+
return toPosix(path9.posix.join(baseOutputDir, `${cleanTestStem(normalizedSource)}${ext}`));
|
|
20696
20883
|
}
|
|
20697
|
-
return toPosix(
|
|
20884
|
+
return toPosix(path9.posix.join(baseOutputDir, dirOfSource, fileName));
|
|
20698
20885
|
}
|
|
20699
20886
|
function groupTestCasesByOutput(testCases, format, options, logger, outputNameSuffix) {
|
|
20700
20887
|
const groups = /* @__PURE__ */ new Map();
|
|
@@ -20901,8 +21088,8 @@ var ReportGenerator = class {
|
|
|
20901
21088
|
if (astroPaths) {
|
|
20902
21089
|
for (const mdPath of astroPaths) {
|
|
20903
21090
|
const content = await fsPromises.readFile(mdPath, "utf8");
|
|
20904
|
-
const mdDir =
|
|
20905
|
-
const assetsDir =
|
|
21091
|
+
const mdDir = path9.dirname(mdPath);
|
|
21092
|
+
const assetsDir = path9.resolve(this.options.astro.assetsDir);
|
|
20906
21093
|
const result = copyMarkdownAssets({
|
|
20907
21094
|
markdown: content,
|
|
20908
21095
|
markdownDir: mdDir,
|
|
@@ -20933,9 +21120,9 @@ var ReportGenerator = class {
|
|
|
20933
21120
|
if (groups.size === 0 && this.options.output.mode === "aggregated") {
|
|
20934
21121
|
const ext = FORMAT_EXTENSIONS[format];
|
|
20935
21122
|
const effectiveName = this.options.outputName + (outputNameSuffix ?? "");
|
|
20936
|
-
const outputPath = toPosix(
|
|
21123
|
+
const outputPath = toPosix(path9.join(this.options.outputDir, joinNameAndExt(effectiveName, ext)));
|
|
20937
21124
|
const content = await this.formatContent(run, format);
|
|
20938
|
-
const dir =
|
|
21125
|
+
const dir = path9.dirname(outputPath);
|
|
20939
21126
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
20940
21127
|
await this.deps.writeFile(outputPath, content);
|
|
20941
21128
|
return [outputPath];
|
|
@@ -20947,7 +21134,7 @@ var ReportGenerator = class {
|
|
|
20947
21134
|
testCases
|
|
20948
21135
|
};
|
|
20949
21136
|
const content = await this.formatContent(groupRun, format);
|
|
20950
|
-
const dir =
|
|
21137
|
+
const dir = path9.dirname(outputPath);
|
|
20951
21138
|
await fsPromises.mkdir(dir, { recursive: true });
|
|
20952
21139
|
await this.deps.writeFile(outputPath, content);
|
|
20953
21140
|
writtenPaths.push(outputPath);
|
|
@@ -21056,6 +21243,10 @@ var ReportGenerator = class {
|
|
|
21056
21243
|
});
|
|
21057
21244
|
return formatter.format(run);
|
|
21058
21245
|
}
|
|
21246
|
+
case "release-manifest": {
|
|
21247
|
+
const formatter = new ReleaseManifestFormatter();
|
|
21248
|
+
return formatter.format(run);
|
|
21249
|
+
}
|
|
21059
21250
|
case "story-report-json": {
|
|
21060
21251
|
const formatter = new StoryReportJsonFormatter({
|
|
21061
21252
|
pretty: this.options.storyReportJson.pretty
|
|
@@ -21087,7 +21278,7 @@ async function generateRunComparison(args) {
|
|
|
21087
21278
|
await fsPromises.mkdir(outputDir, { recursive: true });
|
|
21088
21279
|
for (const format of args.formats) {
|
|
21089
21280
|
const ext = format === "html" ? ".html" : ".md";
|
|
21090
|
-
const outputPath = toPosix(
|
|
21281
|
+
const outputPath = toPosix(path9.join(outputDir, `${outputName}${ext}`));
|
|
21091
21282
|
const content = format === "html" ? new RunDiffHtmlFormatter({ title: args.title }).format(diff) : new RunDiffMarkdownFormatter({ title: args.title }).format(diff);
|
|
21092
21283
|
await fsPromises.writeFile(outputPath, content, "utf8");
|
|
21093
21284
|
files.push(outputPath);
|
|
@@ -21096,18 +21287,18 @@ async function generateRunComparison(args) {
|
|
|
21096
21287
|
}
|
|
21097
21288
|
|
|
21098
21289
|
// src/init-astro.ts
|
|
21099
|
-
import * as
|
|
21100
|
-
import * as
|
|
21290
|
+
import * as fs9 from "fs";
|
|
21291
|
+
import * as path10 from "path";
|
|
21101
21292
|
import { fileURLToPath } from "url";
|
|
21102
|
-
var __dirname =
|
|
21293
|
+
var __dirname = path10.dirname(fileURLToPath(import.meta.url));
|
|
21103
21294
|
var FRAMEWORK_DIRS = ["src/components", "src/lib", "src/styles", "src/pages"];
|
|
21104
21295
|
var FRAMEWORK_FILES = ["tsconfig.json"];
|
|
21105
21296
|
function initAstro(options = {}) {
|
|
21106
21297
|
const targetDir = options.targetDir ?? "./story-docs";
|
|
21107
21298
|
const force = options.force ?? false;
|
|
21108
21299
|
const update = options.update ?? false;
|
|
21109
|
-
const templateDir =
|
|
21110
|
-
if (!
|
|
21300
|
+
const templateDir = path10.resolve(__dirname, "..", "templates", "astro-starlight");
|
|
21301
|
+
if (!fs9.existsSync(templateDir)) {
|
|
21111
21302
|
throw new Error(
|
|
21112
21303
|
`Template directory not found at ${templateDir}. Ensure the package is installed correctly.`
|
|
21113
21304
|
);
|
|
@@ -21115,8 +21306,8 @@ function initAstro(options = {}) {
|
|
|
21115
21306
|
if (update) {
|
|
21116
21307
|
return updateFrameworkFiles(templateDir, targetDir);
|
|
21117
21308
|
}
|
|
21118
|
-
if (
|
|
21119
|
-
const entries =
|
|
21309
|
+
if (fs9.existsSync(targetDir)) {
|
|
21310
|
+
const entries = fs9.readdirSync(targetDir);
|
|
21120
21311
|
if (entries.length > 0 && !force) {
|
|
21121
21312
|
throw new Error(
|
|
21122
21313
|
`Directory "${targetDir}" already exists and is not empty. Use --force to overwrite, or --update to refresh framework files only.`
|
|
@@ -21127,32 +21318,32 @@ function initAstro(options = {}) {
|
|
|
21127
21318
|
return { targetDir };
|
|
21128
21319
|
}
|
|
21129
21320
|
function updateFrameworkFiles(templateDir, targetDir) {
|
|
21130
|
-
if (!
|
|
21321
|
+
if (!fs9.existsSync(targetDir) || !fs9.existsSync(path10.join(targetDir, "astro.config.mjs"))) {
|
|
21131
21322
|
throw new Error(
|
|
21132
21323
|
`"${targetDir}" does not look like a scaffolded docs site. Run init-astro (without --update) first.`
|
|
21133
21324
|
);
|
|
21134
21325
|
}
|
|
21135
21326
|
const updated = [];
|
|
21136
21327
|
for (const dir of FRAMEWORK_DIRS) {
|
|
21137
|
-
const src =
|
|
21138
|
-
if (!
|
|
21139
|
-
copyDirRecursive(src,
|
|
21328
|
+
const src = path10.join(templateDir, dir);
|
|
21329
|
+
if (!fs9.existsSync(src)) continue;
|
|
21330
|
+
copyDirRecursive(src, path10.join(targetDir, dir), (rel) => updated.push(path10.join(dir, rel)));
|
|
21140
21331
|
}
|
|
21141
21332
|
for (const file of FRAMEWORK_FILES) {
|
|
21142
|
-
const src =
|
|
21143
|
-
if (!
|
|
21144
|
-
|
|
21333
|
+
const src = path10.join(templateDir, file);
|
|
21334
|
+
if (!fs9.existsSync(src)) continue;
|
|
21335
|
+
fs9.copyFileSync(src, path10.join(targetDir, file));
|
|
21145
21336
|
updated.push(file);
|
|
21146
21337
|
}
|
|
21147
21338
|
if (mergeDependencies(templateDir, targetDir)) updated.push("package.json (deps)");
|
|
21148
21339
|
return { targetDir, updatedFiles: updated };
|
|
21149
21340
|
}
|
|
21150
21341
|
function mergeDependencies(templateDir, targetDir) {
|
|
21151
|
-
const tmplPkgPath =
|
|
21152
|
-
const userPkgPath =
|
|
21153
|
-
if (!
|
|
21154
|
-
const tmpl = JSON.parse(
|
|
21155
|
-
const user = JSON.parse(
|
|
21342
|
+
const tmplPkgPath = path10.join(templateDir, "package.json");
|
|
21343
|
+
const userPkgPath = path10.join(targetDir, "package.json");
|
|
21344
|
+
if (!fs9.existsSync(tmplPkgPath) || !fs9.existsSync(userPkgPath)) return false;
|
|
21345
|
+
const tmpl = JSON.parse(fs9.readFileSync(tmplPkgPath, "utf8"));
|
|
21346
|
+
const user = JSON.parse(fs9.readFileSync(userPkgPath, "utf8"));
|
|
21156
21347
|
user.dependencies = user.dependencies ?? {};
|
|
21157
21348
|
let changed = false;
|
|
21158
21349
|
for (const [name, version] of Object.entries(tmpl.dependencies ?? {})) {
|
|
@@ -21162,29 +21353,29 @@ function mergeDependencies(templateDir, targetDir) {
|
|
|
21162
21353
|
}
|
|
21163
21354
|
}
|
|
21164
21355
|
if (changed) {
|
|
21165
|
-
|
|
21356
|
+
fs9.writeFileSync(userPkgPath, `${JSON.stringify(user, null, 2)}
|
|
21166
21357
|
`, "utf8");
|
|
21167
21358
|
}
|
|
21168
21359
|
return changed;
|
|
21169
21360
|
}
|
|
21170
21361
|
function copyDirRecursive(src, dest, onFile, baseSrc = src) {
|
|
21171
|
-
|
|
21172
|
-
const entries =
|
|
21362
|
+
fs9.mkdirSync(dest, { recursive: true });
|
|
21363
|
+
const entries = fs9.readdirSync(src, { withFileTypes: true });
|
|
21173
21364
|
for (const entry of entries) {
|
|
21174
|
-
const srcPath =
|
|
21175
|
-
const destPath =
|
|
21365
|
+
const srcPath = path10.join(src, entry.name);
|
|
21366
|
+
const destPath = path10.join(dest, entry.name);
|
|
21176
21367
|
if (entry.isDirectory()) {
|
|
21177
21368
|
copyDirRecursive(srcPath, destPath, onFile, baseSrc);
|
|
21178
21369
|
} else {
|
|
21179
|
-
|
|
21180
|
-
onFile?.(
|
|
21370
|
+
fs9.copyFileSync(srcPath, destPath);
|
|
21371
|
+
onFile?.(path10.relative(baseSrc, srcPath));
|
|
21181
21372
|
}
|
|
21182
21373
|
}
|
|
21183
21374
|
}
|
|
21184
21375
|
|
|
21185
21376
|
// src/scaffold-doc.ts
|
|
21186
|
-
import * as
|
|
21187
|
-
import * as
|
|
21377
|
+
import * as fs10 from "fs";
|
|
21378
|
+
import * as path11 from "path";
|
|
21188
21379
|
var TEMPLATES = ["adr", "runbook", "decision-log", "incident"];
|
|
21189
21380
|
function slugify3(input) {
|
|
21190
21381
|
return input.toLowerCase().trim().replace(/['"]/g, "").replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "untitled";
|
|
@@ -21195,7 +21386,7 @@ function isoDate(today) {
|
|
|
21195
21386
|
function nextSeq(dir) {
|
|
21196
21387
|
let max = 0;
|
|
21197
21388
|
try {
|
|
21198
|
-
for (const entry of
|
|
21389
|
+
for (const entry of fs10.readdirSync(dir)) {
|
|
21199
21390
|
const match = /^(\d{1,4})-/.exec(entry);
|
|
21200
21391
|
if (match) max = Math.max(max, Number.parseInt(match[1], 10));
|
|
21201
21392
|
}
|
|
@@ -21328,11 +21519,11 @@ function scaffoldDoc(options) {
|
|
|
21328
21519
|
);
|
|
21329
21520
|
}
|
|
21330
21521
|
const spec = TEMPLATE_SPECS[template];
|
|
21331
|
-
const baseDir = options.baseDir ??
|
|
21522
|
+
const baseDir = options.baseDir ?? path11.join("src", "content", "docs");
|
|
21332
21523
|
const today = options.today ?? /* @__PURE__ */ new Date();
|
|
21333
21524
|
const name = (options.name ?? "").trim() || defaultName(template);
|
|
21334
21525
|
const slug2 = slugify3(name);
|
|
21335
|
-
const dir =
|
|
21526
|
+
const dir = path11.join(baseDir, spec.subdir);
|
|
21336
21527
|
const ctx = {
|
|
21337
21528
|
name,
|
|
21338
21529
|
slug: slug2,
|
|
@@ -21340,14 +21531,14 @@ function scaffoldDoc(options) {
|
|
|
21340
21531
|
seq: nextSeq(dir)
|
|
21341
21532
|
};
|
|
21342
21533
|
const filename = `${spec.filename(slug2, ctx)}.mdx`;
|
|
21343
|
-
const filePath =
|
|
21344
|
-
if (
|
|
21534
|
+
const filePath = path11.join(dir, filename);
|
|
21535
|
+
if (fs10.existsSync(filePath) && !options.force) {
|
|
21345
21536
|
throw new Error(
|
|
21346
21537
|
`File "${filePath}" already exists. Use --force to overwrite.`
|
|
21347
21538
|
);
|
|
21348
21539
|
}
|
|
21349
|
-
|
|
21350
|
-
|
|
21540
|
+
fs10.mkdirSync(dir, { recursive: true });
|
|
21541
|
+
fs10.writeFileSync(filePath, spec.content(ctx), "utf8");
|
|
21351
21542
|
return { template, path: filePath, title: titleFor2(template, ctx) };
|
|
21352
21543
|
}
|
|
21353
21544
|
function defaultName(template) {
|
|
@@ -21376,8 +21567,8 @@ function titleFor2(template, ctx) {
|
|
|
21376
21567
|
}
|
|
21377
21568
|
|
|
21378
21569
|
// src/check-links.ts
|
|
21379
|
-
import * as
|
|
21380
|
-
import * as
|
|
21570
|
+
import * as fs11 from "fs";
|
|
21571
|
+
import * as path12 from "path";
|
|
21381
21572
|
function stripCode(markdown) {
|
|
21382
21573
|
let out = markdown.replace(/^[ \t]*(`{3,}|~{3,})[^\n]*\n[\s\S]*?^[ \t]*\1\s*$/gm, "");
|
|
21383
21574
|
out = out.replace(/(`+)(?:(?!\1).)+\1/g, "");
|
|
@@ -21407,27 +21598,27 @@ function classifyLink(link2) {
|
|
|
21407
21598
|
function resolutionCandidates(fromFile, link2) {
|
|
21408
21599
|
const withoutAnchor = link2.split("#")[0];
|
|
21409
21600
|
if (!withoutAnchor) return [];
|
|
21410
|
-
const base =
|
|
21601
|
+
const base = path12.resolve(path12.dirname(fromFile), withoutAnchor);
|
|
21411
21602
|
const candidates = [base];
|
|
21412
|
-
if (!
|
|
21603
|
+
if (!path12.extname(base)) {
|
|
21413
21604
|
candidates.push(`${base}.md`, `${base}.mdx`);
|
|
21414
|
-
candidates.push(
|
|
21605
|
+
candidates.push(path12.join(base, "index.md"), path12.join(base, "index.mdx"));
|
|
21415
21606
|
}
|
|
21416
21607
|
return candidates;
|
|
21417
21608
|
}
|
|
21418
21609
|
function resolvesOnDisk(fromFile, link2) {
|
|
21419
21610
|
return resolutionCandidates(fromFile, link2).some(
|
|
21420
|
-
(candidate) =>
|
|
21611
|
+
(candidate) => fs11.existsSync(candidate) && fs11.statSync(candidate).isFile()
|
|
21421
21612
|
);
|
|
21422
21613
|
}
|
|
21423
21614
|
function collectDocFiles(target) {
|
|
21424
|
-
const stat =
|
|
21615
|
+
const stat = fs11.statSync(target);
|
|
21425
21616
|
if (stat.isFile()) return [target];
|
|
21426
21617
|
const out = [];
|
|
21427
21618
|
const walk = (dir) => {
|
|
21428
|
-
for (const entry of
|
|
21619
|
+
for (const entry of fs11.readdirSync(dir, { withFileTypes: true })) {
|
|
21429
21620
|
if (entry.name.startsWith(".") || entry.name === "node_modules") continue;
|
|
21430
|
-
const full =
|
|
21621
|
+
const full = path12.join(dir, entry.name);
|
|
21431
21622
|
if (entry.isDirectory()) walk(full);
|
|
21432
21623
|
else if (/\.mdx?$/.test(entry.name)) out.push(full);
|
|
21433
21624
|
}
|
|
@@ -21458,7 +21649,7 @@ async function isExternalAlive(url, timeoutMs) {
|
|
|
21458
21649
|
}
|
|
21459
21650
|
async function checkLinks(options) {
|
|
21460
21651
|
const { target, checkExternal = false, externalTimeoutMs = 8e3 } = options;
|
|
21461
|
-
if (!
|
|
21652
|
+
if (!fs11.existsSync(target)) {
|
|
21462
21653
|
throw new Error(`Path not found: ${target}`);
|
|
21463
21654
|
}
|
|
21464
21655
|
const files = collectDocFiles(target);
|
|
@@ -21468,7 +21659,7 @@ async function checkLinks(options) {
|
|
|
21468
21659
|
let skipped = 0;
|
|
21469
21660
|
const externalCache = /* @__PURE__ */ new Map();
|
|
21470
21661
|
for (const file of files) {
|
|
21471
|
-
const content =
|
|
21662
|
+
const content = fs11.readFileSync(file, "utf8");
|
|
21472
21663
|
for (const link2 of extractLinks(content)) {
|
|
21473
21664
|
const kind = classifyLink(link2);
|
|
21474
21665
|
if (kind === "anchor" || kind === "mail" || kind === "root") {
|
|
@@ -21524,8 +21715,8 @@ function formatLinkReport(report) {
|
|
|
21524
21715
|
}
|
|
21525
21716
|
|
|
21526
21717
|
// src/import-openapi.ts
|
|
21527
|
-
import * as
|
|
21528
|
-
import * as
|
|
21718
|
+
import * as fs12 from "fs";
|
|
21719
|
+
import * as path13 from "path";
|
|
21529
21720
|
import { parse as parseYamlString } from "yaml";
|
|
21530
21721
|
var HTTP_METHODS = ["get", "post", "put", "patch", "delete", "options", "head"];
|
|
21531
21722
|
function parseYaml(raw, specPath) {
|
|
@@ -21538,9 +21729,9 @@ function parseYaml(raw, specPath) {
|
|
|
21538
21729
|
}
|
|
21539
21730
|
}
|
|
21540
21731
|
function parseSpec(specPath) {
|
|
21541
|
-
if (!
|
|
21542
|
-
const raw =
|
|
21543
|
-
const ext =
|
|
21732
|
+
if (!fs12.existsSync(specPath)) throw new Error(`Spec not found: ${specPath}`);
|
|
21733
|
+
const raw = fs12.readFileSync(specPath, "utf8");
|
|
21734
|
+
const ext = path13.extname(specPath).toLowerCase();
|
|
21544
21735
|
if (ext === ".json") return JSON.parse(raw);
|
|
21545
21736
|
if (ext === ".yaml" || ext === ".yml") return parseYaml(raw, specPath);
|
|
21546
21737
|
try {
|
|
@@ -21571,8 +21762,8 @@ function extractEndpoints(spec) {
|
|
|
21571
21762
|
}
|
|
21572
21763
|
function loadScenarios(runFile) {
|
|
21573
21764
|
if (!runFile) return [];
|
|
21574
|
-
if (!
|
|
21575
|
-
const report = JSON.parse(
|
|
21765
|
+
if (!fs12.existsSync(runFile)) throw new Error(`Run file not found: ${runFile}`);
|
|
21766
|
+
const report = JSON.parse(fs12.readFileSync(runFile, "utf8"));
|
|
21576
21767
|
return (report.features ?? []).flatMap((f) => f.scenarios ?? []);
|
|
21577
21768
|
}
|
|
21578
21769
|
function endpointRefs(endpoint) {
|
|
@@ -21679,25 +21870,25 @@ async function importOpenApi(options) {
|
|
|
21679
21870
|
list.push(item);
|
|
21680
21871
|
groups.set(item.endpoint.tag, list);
|
|
21681
21872
|
}
|
|
21682
|
-
const outputDir = options.outputDir ??
|
|
21683
|
-
if (
|
|
21684
|
-
const entries =
|
|
21873
|
+
const outputDir = options.outputDir ?? path13.join("src", "content", "docs", "api");
|
|
21874
|
+
if (fs12.existsSync(outputDir) && !options.force) {
|
|
21875
|
+
const entries = fs12.readdirSync(outputDir);
|
|
21685
21876
|
if (entries.length > 0) {
|
|
21686
21877
|
throw new Error(`Output directory "${outputDir}" is not empty. Use --force to overwrite.`);
|
|
21687
21878
|
}
|
|
21688
21879
|
}
|
|
21689
|
-
|
|
21880
|
+
fs12.mkdirSync(outputDir, { recursive: true });
|
|
21690
21881
|
const coveredCount = coverage.filter((c) => c.status === "covered").length;
|
|
21691
21882
|
const uncoveredCount = coverage.filter((c) => c.status === "uncovered").length;
|
|
21692
|
-
|
|
21693
|
-
|
|
21883
|
+
fs12.writeFileSync(
|
|
21884
|
+
path13.join(outputDir, "index.mdx"),
|
|
21694
21885
|
renderIndex(groups, hasRun, { endpointCount: endpoints.length, coveredCount, uncoveredCount }),
|
|
21695
21886
|
"utf8"
|
|
21696
21887
|
);
|
|
21697
21888
|
for (const [tag, rows] of groups) {
|
|
21698
|
-
const dir =
|
|
21699
|
-
|
|
21700
|
-
|
|
21889
|
+
const dir = path13.join(outputDir, slug(tag));
|
|
21890
|
+
fs12.mkdirSync(dir, { recursive: true });
|
|
21891
|
+
fs12.writeFileSync(path13.join(dir, "index.mdx"), renderTagPage(tag, rows, hasRun), "utf8");
|
|
21701
21892
|
}
|
|
21702
21893
|
return {
|
|
21703
21894
|
outputDir,
|
|
@@ -21709,8 +21900,8 @@ async function importOpenApi(options) {
|
|
|
21709
21900
|
}
|
|
21710
21901
|
|
|
21711
21902
|
// src/build-docs.ts
|
|
21712
|
-
import * as
|
|
21713
|
-
import * as
|
|
21903
|
+
import * as fs13 from "fs";
|
|
21904
|
+
import * as path14 from "path";
|
|
21714
21905
|
var BuildDocsError = class extends Error {
|
|
21715
21906
|
constructor(message, kind) {
|
|
21716
21907
|
super(message);
|
|
@@ -21720,22 +21911,22 @@ var BuildDocsError = class extends Error {
|
|
|
21720
21911
|
};
|
|
21721
21912
|
var isRemote = (p) => /^(?:https?:|data:)/i.test(p);
|
|
21722
21913
|
function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets") {
|
|
21723
|
-
if (!
|
|
21724
|
-
const report = JSON.parse(
|
|
21914
|
+
if (!fs13.existsSync(reportPath)) return 0;
|
|
21915
|
+
const report = JSON.parse(fs13.readFileSync(reportPath, "utf8"));
|
|
21725
21916
|
let copied = 0;
|
|
21726
21917
|
const bundle = (value) => {
|
|
21727
|
-
const rel = copyAsset(
|
|
21918
|
+
const rel = copyAsset(path14.resolve(value), assetsDir);
|
|
21728
21919
|
copied++;
|
|
21729
|
-
return `${baseUrl}/${
|
|
21920
|
+
return `${baseUrl}/${path14.basename(rel)}`;
|
|
21730
21921
|
};
|
|
21731
21922
|
const visit = (entries) => {
|
|
21732
21923
|
for (const entry of entries ?? []) {
|
|
21733
21924
|
const e = entry;
|
|
21734
21925
|
if (e.kind === "screenshot" || e.kind === "video") {
|
|
21735
|
-
if (typeof e.path === "string" && !isRemote(e.path) &&
|
|
21926
|
+
if (typeof e.path === "string" && !isRemote(e.path) && fs13.existsSync(e.path)) {
|
|
21736
21927
|
e.path = bundle(e.path);
|
|
21737
21928
|
}
|
|
21738
|
-
if (typeof e.poster === "string" && !isRemote(e.poster) &&
|
|
21929
|
+
if (typeof e.poster === "string" && !isRemote(e.poster) && fs13.existsSync(e.poster)) {
|
|
21739
21930
|
e.poster = bundle(e.poster);
|
|
21740
21931
|
}
|
|
21741
21932
|
}
|
|
@@ -21748,25 +21939,25 @@ function bundleExplorerAssets(reportPath, assetsDir, baseUrl = "/stories/assets"
|
|
|
21748
21939
|
}
|
|
21749
21940
|
}
|
|
21750
21941
|
if (copied > 0) {
|
|
21751
|
-
|
|
21942
|
+
fs13.writeFileSync(reportPath, JSON.stringify(report, null, 2), "utf8");
|
|
21752
21943
|
}
|
|
21753
21944
|
return copied;
|
|
21754
21945
|
}
|
|
21755
21946
|
function clearGeneratedPages(dir) {
|
|
21756
|
-
if (!
|
|
21757
|
-
for (const entry of
|
|
21758
|
-
const full =
|
|
21947
|
+
if (!fs13.existsSync(dir)) return;
|
|
21948
|
+
for (const entry of fs13.readdirSync(dir, { withFileTypes: true })) {
|
|
21949
|
+
const full = path14.join(dir, entry.name);
|
|
21759
21950
|
if (entry.isDirectory()) {
|
|
21760
21951
|
clearGeneratedPages(full);
|
|
21761
|
-
if (
|
|
21952
|
+
if (fs13.readdirSync(full).length === 0) fs13.rmdirSync(full);
|
|
21762
21953
|
} else if (/\.mdx?$/.test(entry.name)) {
|
|
21763
|
-
|
|
21954
|
+
fs13.rmSync(full);
|
|
21764
21955
|
}
|
|
21765
21956
|
}
|
|
21766
21957
|
}
|
|
21767
21958
|
function loadCanonicalRun(rawRunPath, synthesize) {
|
|
21768
21959
|
try {
|
|
21769
|
-
const data = JSON.parse(
|
|
21960
|
+
const data = JSON.parse(fs13.readFileSync(path14.resolve(rawRunPath), "utf8"));
|
|
21770
21961
|
if (data.schemaVersion !== 1) {
|
|
21771
21962
|
throw new BuildDocsError(`Unsupported schemaVersion ${data.schemaVersion}. Supported: 1.`, "schema");
|
|
21772
21963
|
}
|
|
@@ -21789,12 +21980,12 @@ ${schemaResult.errors.map((e) => ` ${e}`).join("\n")}`,
|
|
|
21789
21980
|
}
|
|
21790
21981
|
}
|
|
21791
21982
|
async function buildDocs(options) {
|
|
21792
|
-
const siteDir =
|
|
21793
|
-
const storiesPublicDir =
|
|
21794
|
-
const assetsDir =
|
|
21795
|
-
const storyPagesDir =
|
|
21796
|
-
const apiDir =
|
|
21797
|
-
const reportPath =
|
|
21983
|
+
const siteDir = path14.resolve(options.siteDir);
|
|
21984
|
+
const storiesPublicDir = path14.join(siteDir, "public", "stories");
|
|
21985
|
+
const assetsDir = path14.join(storiesPublicDir, "assets");
|
|
21986
|
+
const storyPagesDir = path14.join(siteDir, "src", "content", "docs", "stories");
|
|
21987
|
+
const apiDir = path14.join(siteDir, "src", "content", "docs", "api");
|
|
21988
|
+
const reportPath = path14.join(storiesPublicDir, "story-report.json");
|
|
21798
21989
|
const canonical = loadCanonicalRun(options.rawRunPath, options.synthesizeStories ?? true);
|
|
21799
21990
|
try {
|
|
21800
21991
|
await new ReportGenerator({
|
|
@@ -21815,7 +22006,7 @@ async function buildDocs(options) {
|
|
|
21815
22006
|
let apiPages = 0;
|
|
21816
22007
|
if (options.openapiPath) {
|
|
21817
22008
|
const res = await importOpenApi({
|
|
21818
|
-
specPath:
|
|
22009
|
+
specPath: path14.resolve(options.openapiPath),
|
|
21819
22010
|
outputDir: apiDir,
|
|
21820
22011
|
runFile: reportPath,
|
|
21821
22012
|
force: true
|
|
@@ -21830,11 +22021,11 @@ async function buildDocs(options) {
|
|
|
21830
22021
|
}
|
|
21831
22022
|
|
|
21832
22023
|
// src/config.ts
|
|
21833
|
-
import { existsSync as
|
|
21834
|
-
import { resolve as
|
|
22024
|
+
import { existsSync as existsSync12 } from "fs";
|
|
22025
|
+
import { resolve as resolve10 } from "path";
|
|
21835
22026
|
async function loadConfig(configPath) {
|
|
21836
|
-
const resolved = configPath ?
|
|
21837
|
-
if (!
|
|
22027
|
+
const resolved = configPath ? resolve10(configPath) : resolve10(process.cwd(), "executable-stories.config.js");
|
|
22028
|
+
if (!existsSync12(resolved)) return {};
|
|
21838
22029
|
const mod = await import(resolved);
|
|
21839
22030
|
const config = mod.default;
|
|
21840
22031
|
if (!config || typeof config !== "object" || Array.isArray(config)) {
|
|
@@ -21853,6 +22044,7 @@ var EXIT_GENERATION = 3;
|
|
|
21853
22044
|
var EXIT_USAGE = 4;
|
|
21854
22045
|
var EXIT_COMPARE_GATE = 5;
|
|
21855
22046
|
var EXIT_REVIEW_GATE = 5;
|
|
22047
|
+
var EXIT_RELEASE_GATE = 6;
|
|
21856
22048
|
var HELP_TEXT = `
|
|
21857
22049
|
executable-stories \u2014 Generate reports from test results JSON.
|
|
21858
22050
|
|
|
@@ -21860,6 +22052,7 @@ USAGE
|
|
|
21860
22052
|
executable-stories format <file> [options]
|
|
21861
22053
|
executable-stories format --stdin [options]
|
|
21862
22054
|
executable-stories compare <baseline-file> <current-file> [options]
|
|
22055
|
+
executable-stories gate-release <dev-run.json> <rc-run.json> [options]
|
|
21863
22056
|
executable-stories review <file> --changed-files <path> [options]
|
|
21864
22057
|
executable-stories list <file> [options]
|
|
21865
22058
|
executable-stories validate <file>
|
|
@@ -21870,11 +22063,15 @@ USAGE
|
|
|
21870
22063
|
executable-stories import-openapi <spec> [options]
|
|
21871
22064
|
executable-stories publish-confluence <file.adf.json> [options]
|
|
21872
22065
|
executable-stories publish-jira <file.adf.json> [options]
|
|
22066
|
+
executable-stories deploy record <file> --env <env> [--tag <tag>] [options]
|
|
22067
|
+
executable-stories deploy status [options]
|
|
22068
|
+
executable-stories deploy diff <env-a> <env-b> [options]
|
|
21873
22069
|
|
|
21874
22070
|
SUBCOMMANDS
|
|
21875
22071
|
format Read raw test results and generate reports
|
|
21876
22072
|
watch Regenerate reports whenever the raw-run file changes (live agent index)
|
|
21877
22073
|
compare Compare two runs and generate a diff report
|
|
22074
|
+
gate-release Verify a release candidate against the dev test baseline (RC gate)
|
|
21878
22075
|
review Generate an Evidence Review of AI-authored changes (correlate a run to the diff)
|
|
21879
22076
|
list List scenarios from a test run (text table or JSON)
|
|
21880
22077
|
validate Validate a JSON file against the schema (no output generated)
|
|
@@ -21884,9 +22081,10 @@ SUBCOMMANDS
|
|
|
21884
22081
|
import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
|
|
21885
22082
|
publish-confluence Publish an ADF JSON file to a Confluence page via REST API
|
|
21886
22083
|
publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
|
|
22084
|
+
deploy Record deployments, show environment status, detect drift
|
|
21887
22085
|
|
|
21888
22086
|
OPTIONS
|
|
21889
|
-
--format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, scenario-index-json, behavior-manifest-json, or custom names from config (default: html)
|
|
22087
|
+
--format <formats> Comma-separated formats: html, markdown, release-manifest, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, story-report-json, scenario-index-json, behavior-manifest-json, or custom names from config (default: html)
|
|
21890
22088
|
astro Themed Markdown (for Astro docs sites with matching CSS)
|
|
21891
22089
|
confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
|
|
21892
22090
|
behavior-manifest-json Agent-readable behavior manifest and debugger warnings
|
|
@@ -21930,7 +22128,10 @@ OPTIONS
|
|
|
21930
22128
|
--pr-summary-file <path> Write the PR-friendly markdown summary to a file
|
|
21931
22129
|
--fail-on-regression Exit non-zero when any regression is detected in compare
|
|
21932
22130
|
--fail-on-added-failures Exit non-zero when newly added scenarios are failing
|
|
22131
|
+
--fail-on-removal Exit non-zero when scenarios are removed from the baseline
|
|
22132
|
+
--fail-on-new Exit non-zero when new scenarios appear that weren't in the baseline
|
|
21933
22133
|
--max-regressions <n> Exit non-zero when regressions exceed threshold
|
|
22134
|
+
--release-policy <path> (gate-release) Path to JSON policy file with allowed exceptions
|
|
21934
22135
|
--changed-files <path> (review) Changed files: JSON (ChangedFile[] or {changedFiles,baseRef,headRef}) or "git diff --name-status" text
|
|
21935
22136
|
--base-ref <ref> (review) Base ref label shown in the report (informational)
|
|
21936
22137
|
--head-ref <ref> (review) Head ref label shown in the report (informational)
|
|
@@ -21949,6 +22150,24 @@ COMPARE
|
|
|
21949
22150
|
compare supports --format html,markdown
|
|
21950
22151
|
compare uses the same --input-type for both baseline and current files
|
|
21951
22152
|
|
|
22153
|
+
GATE-RELEASE
|
|
22154
|
+
gate-release compares a dev environment test run (baseline) against a
|
|
22155
|
+
release candidate test run to verify the RC matches what was tested in dev.
|
|
22156
|
+
By default, fails if scenarios are omitted or regressed.
|
|
22157
|
+
--fail-on-regression and --fail-on-removal are enabled by default.
|
|
22158
|
+
Supports --release-policy for exception lists.
|
|
22159
|
+
|
|
22160
|
+
DEPLOY
|
|
22161
|
+
executable-stories deploy record <file> --env <env> [--tag <tag>]
|
|
22162
|
+
Record a deployment of a test run to an environment (e.g. dev, staging, prod).
|
|
22163
|
+
The deployment ledger is at .executable-stories/deployments.json by default.
|
|
22164
|
+
|
|
22165
|
+
executable-stories deploy status [--ledger <path>]
|
|
22166
|
+
Show the latest deployment for each environment.
|
|
22167
|
+
|
|
22168
|
+
executable-stories deploy diff <env-a> <env-b> [--ledger <path>]
|
|
22169
|
+
Show scenario drift between two environments (what's in one but not the other).
|
|
22170
|
+
|
|
21952
22171
|
INIT-ASTRO
|
|
21953
22172
|
executable-stories init-astro [directory] Scaffold into directory (default: ./story-docs)
|
|
21954
22173
|
--force Overwrite existing directory
|
|
@@ -22000,6 +22219,7 @@ EXIT CODES
|
|
|
22000
22219
|
3 Formatter/generation failure
|
|
22001
22220
|
4 Bad arguments / usage error
|
|
22002
22221
|
5 Compare gate failed
|
|
22222
|
+
6 Release gate failed
|
|
22003
22223
|
`.trim();
|
|
22004
22224
|
async function parseCliArgs(argv) {
|
|
22005
22225
|
const args = argv.slice(2);
|
|
@@ -22008,9 +22228,9 @@ async function parseCliArgs(argv) {
|
|
|
22008
22228
|
process.exit(EXIT_SUCCESS);
|
|
22009
22229
|
}
|
|
22010
22230
|
const subcommand = args[0];
|
|
22011
|
-
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
22231
|
+
if (subcommand !== "format" && subcommand !== "watch" && subcommand !== "compare" && subcommand !== "gate-release" && subcommand !== "deploy" && subcommand !== "review" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "build-docs" && subcommand !== "new" && subcommand !== "check-links" && subcommand !== "import-openapi" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
22012
22232
|
console.error(
|
|
22013
|
-
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "review", "list", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
22233
|
+
`Unknown subcommand: "${subcommand}". Use "format", "watch", "compare", "gate-release", "deploy", "review", "list", "validate", "init-astro", "build-docs", "new", "check-links", "import-openapi", "publish-confluence", or "publish-jira".`
|
|
22014
22234
|
);
|
|
22015
22235
|
process.exit(EXIT_USAGE);
|
|
22016
22236
|
}
|
|
@@ -22022,6 +22242,9 @@ async function parseCliArgs(argv) {
|
|
|
22022
22242
|
await runPublishJira(args.slice(1));
|
|
22023
22243
|
process.exit(EXIT_SUCCESS);
|
|
22024
22244
|
}
|
|
22245
|
+
if (subcommand === "deploy") {
|
|
22246
|
+
process.exit(await runDeploy(args.slice(1)));
|
|
22247
|
+
}
|
|
22025
22248
|
if (subcommand === "init-astro") {
|
|
22026
22249
|
const initArgs = args.slice(1);
|
|
22027
22250
|
const targetDir = initArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
|
|
@@ -22115,7 +22338,10 @@ async function parseCliArgs(argv) {
|
|
|
22115
22338
|
"pr-summary-file": { type: "string" },
|
|
22116
22339
|
"fail-on-regression": { type: "boolean", default: false },
|
|
22117
22340
|
"fail-on-added-failures": { type: "boolean", default: false },
|
|
22341
|
+
"fail-on-removal": { type: "boolean", default: false },
|
|
22342
|
+
"fail-on-new": { type: "boolean", default: false },
|
|
22118
22343
|
"max-regressions": { type: "string" },
|
|
22344
|
+
"release-policy": { type: "string" },
|
|
22119
22345
|
"changed-files": { type: "string" },
|
|
22120
22346
|
"base-ref": { type: "string" },
|
|
22121
22347
|
"head-ref": { type: "string" },
|
|
@@ -22134,20 +22360,21 @@ async function parseCliArgs(argv) {
|
|
|
22134
22360
|
const useStdin = values.stdin;
|
|
22135
22361
|
const baselineValue = values.baseline;
|
|
22136
22362
|
const baselineMode = baselineValue === "auto" ? "auto" : "explicit";
|
|
22137
|
-
const
|
|
22138
|
-
const
|
|
22139
|
-
const
|
|
22140
|
-
|
|
22363
|
+
const isCompareLike = subcommand === "compare" || subcommand === "gate-release";
|
|
22364
|
+
const inputFile = isCompareLike ? void 0 : positionals[0];
|
|
22365
|
+
const baselineFile = isCompareLike ? baselineMode === "auto" ? baselineValue && baselineValue !== "auto" ? baselineValue : positionals.length > 1 ? positionals[0] : void 0 : baselineValue && baselineValue !== "auto" ? baselineValue : positionals.length > 1 ? positionals[0] : void 0 : void 0;
|
|
22366
|
+
const currentFile = isCompareLike ? positionals.length > 1 ? positionals[1] : positionals[0] : void 0;
|
|
22367
|
+
if (isCompareLike) {
|
|
22141
22368
|
if (useStdin) {
|
|
22142
|
-
console.error(
|
|
22369
|
+
console.error(`Error: ${subcommand} does not support --stdin. Pass baseline and current files.`);
|
|
22143
22370
|
process.exit(EXIT_USAGE);
|
|
22144
22371
|
}
|
|
22145
22372
|
if (!currentFile) {
|
|
22146
|
-
console.error(
|
|
22373
|
+
console.error(`Error: ${subcommand} requires <current-file>, and either <baseline-file> or --baseline auto.`);
|
|
22147
22374
|
process.exit(EXIT_USAGE);
|
|
22148
22375
|
}
|
|
22149
22376
|
if (baselineMode === "explicit" && !baselineFile) {
|
|
22150
|
-
console.error(
|
|
22377
|
+
console.error(`Error: ${subcommand} requires <baseline-file> and <current-file>, or use --baseline auto.`);
|
|
22151
22378
|
process.exit(EXIT_USAGE);
|
|
22152
22379
|
}
|
|
22153
22380
|
} else if (!useStdin && !inputFile) {
|
|
@@ -22161,7 +22388,7 @@ async function parseCliArgs(argv) {
|
|
|
22161
22388
|
}
|
|
22162
22389
|
const pluginConfig = await loadConfig(values["config"]);
|
|
22163
22390
|
const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
|
|
22164
|
-
const builtInFormats = /* @__PURE__ */ new Set(["astro", "behavior-manifest-json", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "scenario-index-json", "story-report-json"]);
|
|
22391
|
+
const builtInFormats = /* @__PURE__ */ new Set(["astro", "behavior-manifest-json", "confluence", "html", "markdown", "release-manifest", "junit", "cucumber-json", "cucumber-messages", "cucumber-html", "scenario-index-json", "story-report-json"]);
|
|
22165
22392
|
const formatStr = values.format;
|
|
22166
22393
|
const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
|
|
22167
22394
|
const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
|
|
@@ -22169,7 +22396,7 @@ async function parseCliArgs(argv) {
|
|
|
22169
22396
|
const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
|
|
22170
22397
|
if (unknownFormats.length > 0) {
|
|
22171
22398
|
const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
|
|
22172
|
-
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, behavior-manifest-json, confluence, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
|
|
22399
|
+
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, behavior-manifest-json, confluence, html, markdown, release-manifest, junit, cucumber-json, cucumber-messages, cucumber-html, scenario-index-json, story-report-json${knownCustom}.`);
|
|
22173
22400
|
process.exit(EXIT_USAGE);
|
|
22174
22401
|
}
|
|
22175
22402
|
const formats = builtInRequested;
|
|
@@ -22307,7 +22534,10 @@ async function parseCliArgs(argv) {
|
|
|
22307
22534
|
prSummaryFile: values["pr-summary-file"],
|
|
22308
22535
|
failOnRegression: values["fail-on-regression"],
|
|
22309
22536
|
failOnAddedFailures: values["fail-on-added-failures"],
|
|
22537
|
+
failOnRemoval: values["fail-on-removal"],
|
|
22538
|
+
failOnNew: values["fail-on-new"],
|
|
22310
22539
|
maxRegressions,
|
|
22540
|
+
releasePolicy: values["release-policy"],
|
|
22311
22541
|
changedFilesPath: values["changed-files"],
|
|
22312
22542
|
baseRef: values["base-ref"],
|
|
22313
22543
|
headRef: values["head-ref"],
|
|
@@ -22321,27 +22551,27 @@ async function readInput(args) {
|
|
|
22321
22551
|
if (args.stdin) {
|
|
22322
22552
|
return readStdin();
|
|
22323
22553
|
}
|
|
22324
|
-
const filePath =
|
|
22325
|
-
if (!
|
|
22554
|
+
const filePath = path15.resolve(args.inputFile);
|
|
22555
|
+
if (!fs14.existsSync(filePath)) {
|
|
22326
22556
|
console.error(`Error: File not found: ${filePath}`);
|
|
22327
22557
|
process.exit(EXIT_USAGE);
|
|
22328
22558
|
}
|
|
22329
|
-
return
|
|
22559
|
+
return fs14.readFileSync(filePath, "utf8");
|
|
22330
22560
|
}
|
|
22331
22561
|
function readFileInput(filePath) {
|
|
22332
|
-
const resolved =
|
|
22333
|
-
if (!
|
|
22562
|
+
const resolved = path15.resolve(filePath);
|
|
22563
|
+
if (!fs14.existsSync(resolved)) {
|
|
22334
22564
|
console.error(`Error: File not found: ${resolved}`);
|
|
22335
22565
|
process.exit(EXIT_USAGE);
|
|
22336
22566
|
}
|
|
22337
|
-
return
|
|
22567
|
+
return fs14.readFileSync(resolved, "utf8");
|
|
22338
22568
|
}
|
|
22339
22569
|
function readStdin() {
|
|
22340
|
-
return new Promise((
|
|
22570
|
+
return new Promise((resolve12, reject) => {
|
|
22341
22571
|
const chunks = [];
|
|
22342
22572
|
process.stdin.setEncoding("utf8");
|
|
22343
22573
|
process.stdin.on("data", (chunk) => chunks.push(chunk));
|
|
22344
|
-
process.stdin.on("end", () =>
|
|
22574
|
+
process.stdin.on("end", () => resolve12(chunks.join("")));
|
|
22345
22575
|
process.stdin.on("error", reject);
|
|
22346
22576
|
});
|
|
22347
22577
|
}
|
|
@@ -22467,14 +22697,14 @@ function tryNormalizeRunFromText(text2, args) {
|
|
|
22467
22697
|
}
|
|
22468
22698
|
}
|
|
22469
22699
|
function listBaselineCandidates(currentFile, args) {
|
|
22470
|
-
const baselineDir =
|
|
22471
|
-
const currentResolved =
|
|
22472
|
-
if (!
|
|
22700
|
+
const baselineDir = path15.resolve(args.baselineDir ?? path15.dirname(currentFile));
|
|
22701
|
+
const currentResolved = path15.resolve(currentFile);
|
|
22702
|
+
if (!fs14.existsSync(baselineDir)) {
|
|
22473
22703
|
console.error(`Error: baseline directory not found: ${baselineDir}`);
|
|
22474
22704
|
process.exit(EXIT_USAGE);
|
|
22475
22705
|
}
|
|
22476
|
-
const entries =
|
|
22477
|
-
return entries.filter((entry) => entry.isFile()).map((entry) =>
|
|
22706
|
+
const entries = fs14.readdirSync(baselineDir, { withFileTypes: true });
|
|
22707
|
+
return entries.filter((entry) => entry.isFile()).map((entry) => path15.join(baselineDir, entry.name)).filter((candidate) => path15.resolve(candidate) !== currentResolved).filter(
|
|
22478
22708
|
(candidate) => args.inputType === "ndjson" ? candidate.endsWith(".ndjson") : candidate.endsWith(".json")
|
|
22479
22709
|
);
|
|
22480
22710
|
}
|
|
@@ -22482,14 +22712,14 @@ function resolveBaselineAuto(currentFile, currentRun, args) {
|
|
|
22482
22712
|
const candidates = listBaselineCandidates(currentFile, args);
|
|
22483
22713
|
const comparable = [];
|
|
22484
22714
|
for (const candidate of candidates) {
|
|
22485
|
-
const run = tryNormalizeRunFromText(
|
|
22715
|
+
const run = tryNormalizeRunFromText(fs14.readFileSync(candidate, "utf8"), args);
|
|
22486
22716
|
if (run) {
|
|
22487
22717
|
comparable.push({ file: candidate, run });
|
|
22488
22718
|
}
|
|
22489
22719
|
}
|
|
22490
22720
|
if (comparable.length === 0) {
|
|
22491
22721
|
console.error(
|
|
22492
|
-
`Error: no compatible baseline files found in ${
|
|
22722
|
+
`Error: no compatible baseline files found in ${path15.resolve(args.baselineDir ?? path15.dirname(currentFile))}.`
|
|
22493
22723
|
);
|
|
22494
22724
|
process.exit(EXIT_USAGE);
|
|
22495
22725
|
}
|
|
@@ -22526,6 +22756,50 @@ async function main() {
|
|
|
22526
22756
|
process.exit(EXIT_GENERATION);
|
|
22527
22757
|
}
|
|
22528
22758
|
}
|
|
22759
|
+
if (args.subcommand === "gate-release") {
|
|
22760
|
+
const gatedArgs = {
|
|
22761
|
+
...args,
|
|
22762
|
+
failOnRegression: true,
|
|
22763
|
+
failOnRemoval: true
|
|
22764
|
+
// failOnNew is opt-in via --fail-on-new flag
|
|
22765
|
+
};
|
|
22766
|
+
let policy;
|
|
22767
|
+
if (args.releasePolicy) {
|
|
22768
|
+
policy = loadReleasePolicy(args.releasePolicy);
|
|
22769
|
+
}
|
|
22770
|
+
const currentText = readFileInput(gatedArgs.currentFile);
|
|
22771
|
+
const current = applySelection(normalizeRunFromText(currentText, gatedArgs).run, gatedArgs);
|
|
22772
|
+
const baselineFile = gatedArgs.baselineMode === "auto" ? resolveBaselineAuto(gatedArgs.currentFile, current, gatedArgs) : gatedArgs.baselineFile;
|
|
22773
|
+
const baselineText = readFileInput(baselineFile);
|
|
22774
|
+
const baseline = applySelection(normalizeRunFromText(baselineText, gatedArgs).run, gatedArgs);
|
|
22775
|
+
try {
|
|
22776
|
+
const result = await generateCompareReports(baseline, current, baselineFile, gatedArgs);
|
|
22777
|
+
const effectiveResult = policy ? applyReleasePolicy(result, policy) : result;
|
|
22778
|
+
printCompareResult(effectiveResult, gatedArgs, startMs);
|
|
22779
|
+
const gateFailures = evaluateCompareGate(effectiveResult, gatedArgs);
|
|
22780
|
+
if (gateFailures.length > 0) {
|
|
22781
|
+
for (const failure of gateFailures) {
|
|
22782
|
+
console.error(`Release gate failed: ${failure}`);
|
|
22783
|
+
}
|
|
22784
|
+
if (policy) {
|
|
22785
|
+
console.error(`Release policy: ${args.releasePolicy}`);
|
|
22786
|
+
if (policy.allowedOmissions && policy.allowedOmissions.length > 0) {
|
|
22787
|
+
console.error(` Allowed omissions: ${policy.allowedOmissions.join(", ")}`);
|
|
22788
|
+
}
|
|
22789
|
+
if (policy.allowedRegressions && policy.allowedRegressions.length > 0) {
|
|
22790
|
+
console.error(` Allowed regressions: ${policy.allowedRegressions.join(", ")}`);
|
|
22791
|
+
}
|
|
22792
|
+
}
|
|
22793
|
+
process.exit(EXIT_RELEASE_GATE);
|
|
22794
|
+
}
|
|
22795
|
+
console.error("Release gate passed: RC matches dev baseline.");
|
|
22796
|
+
process.exit(EXIT_SUCCESS);
|
|
22797
|
+
} catch (err) {
|
|
22798
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
22799
|
+
console.error(`Release gate check failed: ${msg}`);
|
|
22800
|
+
process.exit(EXIT_GENERATION);
|
|
22801
|
+
}
|
|
22802
|
+
}
|
|
22529
22803
|
if (args.subcommand === "review") {
|
|
22530
22804
|
const text3 = await readInput(args);
|
|
22531
22805
|
const run = applySelection(normalizeRunFromText(text3, args).run, args);
|
|
@@ -22627,9 +22901,9 @@ async function main() {
|
|
|
22627
22901
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
22628
22902
|
}
|
|
22629
22903
|
if (args.emitCanonical) {
|
|
22630
|
-
const outPath =
|
|
22631
|
-
|
|
22632
|
-
|
|
22904
|
+
const outPath = path15.resolve(args.emitCanonical);
|
|
22905
|
+
fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
|
|
22906
|
+
fs14.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
22633
22907
|
}
|
|
22634
22908
|
try {
|
|
22635
22909
|
const result = await generateReports(run, args);
|
|
@@ -22686,9 +22960,9 @@ ${msg}`);
|
|
|
22686
22960
|
}
|
|
22687
22961
|
const run = data;
|
|
22688
22962
|
if (args.emitCanonical) {
|
|
22689
|
-
const outPath =
|
|
22690
|
-
|
|
22691
|
-
|
|
22963
|
+
const outPath = path15.resolve(args.emitCanonical);
|
|
22964
|
+
fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
|
|
22965
|
+
fs14.writeFileSync(outPath, JSON.stringify(run, null, 2), "utf8");
|
|
22692
22966
|
}
|
|
22693
22967
|
try {
|
|
22694
22968
|
const result = await generateReports(run, args);
|
|
@@ -22744,9 +23018,9 @@ ${msg}`);
|
|
|
22744
23018
|
process.exit(EXIT_CANONICAL_VALIDATION);
|
|
22745
23019
|
}
|
|
22746
23020
|
if (args.emitCanonical) {
|
|
22747
|
-
const outPath =
|
|
22748
|
-
|
|
22749
|
-
|
|
23021
|
+
const outPath = path15.resolve(args.emitCanonical);
|
|
23022
|
+
fs14.mkdirSync(path15.dirname(outPath), { recursive: true });
|
|
23023
|
+
fs14.writeFileSync(outPath, JSON.stringify(canonical, null, 2), "utf8");
|
|
22750
23024
|
}
|
|
22751
23025
|
try {
|
|
22752
23026
|
const result = await generateReports(canonical, args, droppedMissingStory);
|
|
@@ -22771,9 +23045,9 @@ function runCustomFormatters(run, customRequested, formatters, args) {
|
|
|
22771
23045
|
const ext = formatter.fileExtension ?? formatName;
|
|
22772
23046
|
const baseName = args.outputName ?? "report";
|
|
22773
23047
|
const filename = args.outputNameTimestamp ? `${baseName}-${Math.floor(run.startedAtMs / 1e3)}.${ext}` : `${baseName}.${ext}`;
|
|
22774
|
-
const filepath =
|
|
22775
|
-
|
|
22776
|
-
|
|
23048
|
+
const filepath = path15.join(outputDir, filename);
|
|
23049
|
+
fs14.mkdirSync(outputDir, { recursive: true });
|
|
23050
|
+
fs14.writeFileSync(filepath, content, "utf8");
|
|
22777
23051
|
console.log(`Generated: ${filepath}`);
|
|
22778
23052
|
} catch (err) {
|
|
22779
23053
|
console.error(`Error running custom formatter "${formatName}": ${err instanceof Error ? err.message : String(err)}`);
|
|
@@ -22823,13 +23097,13 @@ async function dispatchNotifications(run, args) {
|
|
|
22823
23097
|
}
|
|
22824
23098
|
function runHistoryPipeline(run, args) {
|
|
22825
23099
|
if (!args.historyFile) return;
|
|
22826
|
-
const historyPath =
|
|
23100
|
+
const historyPath = path15.resolve(args.historyFile);
|
|
22827
23101
|
const store = loadHistory(
|
|
22828
23102
|
{ filePath: historyPath },
|
|
22829
23103
|
{
|
|
22830
23104
|
readFile: (p) => {
|
|
22831
23105
|
try {
|
|
22832
|
-
return
|
|
23106
|
+
return fs14.readFileSync(p, "utf8");
|
|
22833
23107
|
} catch {
|
|
22834
23108
|
return void 0;
|
|
22835
23109
|
}
|
|
@@ -22842,11 +23116,11 @@ function runHistoryPipeline(run, args) {
|
|
|
22842
23116
|
run,
|
|
22843
23117
|
maxRuns: args.maxHistoryRuns
|
|
22844
23118
|
});
|
|
22845
|
-
const dir =
|
|
22846
|
-
|
|
23119
|
+
const dir = path15.dirname(historyPath);
|
|
23120
|
+
fs14.mkdirSync(dir, { recursive: true });
|
|
22847
23121
|
saveHistory(
|
|
22848
23122
|
{ filePath: historyPath, store: updated },
|
|
22849
|
-
{ writeFile: (p, content) =>
|
|
23123
|
+
{ writeFile: (p, content) => fs14.writeFileSync(p, content, "utf8") }
|
|
22850
23124
|
);
|
|
22851
23125
|
let metricsCount = 0;
|
|
22852
23126
|
for (const testId of Object.keys(updated.tests)) {
|
|
@@ -22923,6 +23197,7 @@ async function generateCompareReports(baseline, current, baselineFile, args) {
|
|
|
22923
23197
|
(scenario) => scenario.kind === "added" && scenario.current?.status === "failed"
|
|
22924
23198
|
).length,
|
|
22925
23199
|
summary: result.diff.summary,
|
|
23200
|
+
scenarios: result.diff.scenarios,
|
|
22926
23201
|
prSummary: args.prSummary || args.prSummaryFile ? createPrCommentSummary(result.diff) : void 0
|
|
22927
23202
|
};
|
|
22928
23203
|
}
|
|
@@ -22993,11 +23268,11 @@ function writeReviewReport(review, args) {
|
|
|
22993
23268
|
const outputDir = args.outputDir ?? "reports";
|
|
22994
23269
|
const baseName = args.outputName ?? "evidence-review";
|
|
22995
23270
|
const suffix = args.outputNameTimestamp ? `-${Math.floor(review.run.startedAtMs / 1e3)}` : "";
|
|
22996
|
-
|
|
22997
|
-
const mdPath =
|
|
22998
|
-
const htmlPath =
|
|
22999
|
-
|
|
23000
|
-
|
|
23271
|
+
fs14.mkdirSync(outputDir, { recursive: true });
|
|
23272
|
+
const mdPath = path15.join(outputDir, `${baseName}${suffix}.md`);
|
|
23273
|
+
const htmlPath = path15.join(outputDir, `${baseName}${suffix}.html`);
|
|
23274
|
+
fs14.writeFileSync(mdPath, markdown, "utf8");
|
|
23275
|
+
fs14.writeFileSync(htmlPath, html, "utf8");
|
|
23001
23276
|
return [mdPath, htmlPath];
|
|
23002
23277
|
}
|
|
23003
23278
|
function evaluateReviewGate(review, args) {
|
|
@@ -23043,9 +23318,9 @@ function printResult(result, args, startMs, droppedMissingStory = 0) {
|
|
|
23043
23318
|
function printCompareResult(result, args, startMs) {
|
|
23044
23319
|
const durationMs = Date.now() - startMs;
|
|
23045
23320
|
if (result.prSummary && args.prSummaryFile) {
|
|
23046
|
-
const outputPath =
|
|
23047
|
-
|
|
23048
|
-
|
|
23321
|
+
const outputPath = path15.resolve(args.prSummaryFile);
|
|
23322
|
+
fs14.mkdirSync(path15.dirname(outputPath), { recursive: true });
|
|
23323
|
+
fs14.writeFileSync(outputPath, result.prSummary, "utf8");
|
|
23049
23324
|
}
|
|
23050
23325
|
if (args.jsonSummary) {
|
|
23051
23326
|
console.log(
|
|
@@ -23073,6 +23348,44 @@ function printCompareResult(result, args, startMs) {
|
|
|
23073
23348
|
console.log(result.prSummary);
|
|
23074
23349
|
}
|
|
23075
23350
|
}
|
|
23351
|
+
function loadReleasePolicy(policyPath) {
|
|
23352
|
+
const resolved = path15.resolve(policyPath);
|
|
23353
|
+
if (!fs14.existsSync(resolved)) {
|
|
23354
|
+
console.error(`Error: release policy file not found: ${resolved}`);
|
|
23355
|
+
process.exit(EXIT_USAGE);
|
|
23356
|
+
}
|
|
23357
|
+
try {
|
|
23358
|
+
const raw = JSON.parse(fs14.readFileSync(resolved, "utf8"));
|
|
23359
|
+
return {
|
|
23360
|
+
allowedOmissions: Array.isArray(raw.allowedOmissions) ? raw.allowedOmissions : [],
|
|
23361
|
+
allowedRegressions: Array.isArray(raw.allowedRegressions) ? raw.allowedRegressions : [],
|
|
23362
|
+
allowNewScenarios: Boolean(raw.allowNewScenarios)
|
|
23363
|
+
};
|
|
23364
|
+
} catch (err) {
|
|
23365
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
23366
|
+
console.error(`Error reading release policy: ${msg}`);
|
|
23367
|
+
process.exit(EXIT_USAGE);
|
|
23368
|
+
}
|
|
23369
|
+
}
|
|
23370
|
+
function applyReleasePolicy(result, policy) {
|
|
23371
|
+
const allowedOmissionSet = new Set(policy.allowedOmissions ?? []);
|
|
23372
|
+
const allowedRegressionSet = new Set(policy.allowedRegressions ?? []);
|
|
23373
|
+
const adjustedOmissions = result.scenarios.filter(
|
|
23374
|
+
(scenario) => scenario.kind === "removed" && !allowedOmissionSet.has(scenario.id)
|
|
23375
|
+
).length;
|
|
23376
|
+
const adjustedRegressions = result.scenarios.filter(
|
|
23377
|
+
(scenario) => scenario.kind === "regressed" && !allowedRegressionSet.has(scenario.id)
|
|
23378
|
+
).length;
|
|
23379
|
+
const adjustedSummary = {
|
|
23380
|
+
...result.summary,
|
|
23381
|
+
removed: adjustedOmissions,
|
|
23382
|
+
regressed: adjustedRegressions
|
|
23383
|
+
};
|
|
23384
|
+
return {
|
|
23385
|
+
...result,
|
|
23386
|
+
summary: adjustedSummary
|
|
23387
|
+
};
|
|
23388
|
+
}
|
|
23076
23389
|
function evaluateCompareGate(result, args) {
|
|
23077
23390
|
const failures = [];
|
|
23078
23391
|
if (args.failOnRegression && result.summary.regressed > 0) {
|
|
@@ -23085,6 +23398,16 @@ function evaluateCompareGate(result, args) {
|
|
|
23085
23398
|
`new failing scenarios detected (${result.addedFailures}) with --fail-on-added-failures`
|
|
23086
23399
|
);
|
|
23087
23400
|
}
|
|
23401
|
+
if (args.failOnRemoval && result.summary.removed > 0) {
|
|
23402
|
+
failures.push(
|
|
23403
|
+
`removed scenarios detected (${result.summary.removed}) with --fail-on-removal`
|
|
23404
|
+
);
|
|
23405
|
+
}
|
|
23406
|
+
if (args.failOnNew && result.summary.added > 0) {
|
|
23407
|
+
failures.push(
|
|
23408
|
+
`new scenarios detected (${result.summary.added}) with --fail-on-new`
|
|
23409
|
+
);
|
|
23410
|
+
}
|
|
23088
23411
|
if (args.maxRegressions !== void 0 && result.summary.regressed > args.maxRegressions) {
|
|
23089
23412
|
failures.push(
|
|
23090
23413
|
`regressions ${result.summary.regressed} exceed --max-regressions ${args.maxRegressions}`
|
|
@@ -23136,7 +23459,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
23136
23459
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
23137
23460
|
process.exit(EXIT_USAGE);
|
|
23138
23461
|
}
|
|
23139
|
-
if (!
|
|
23462
|
+
if (!fs14.existsSync(inputFile)) {
|
|
23140
23463
|
console.error(`Error: file not found: ${inputFile}`);
|
|
23141
23464
|
process.exit(EXIT_USAGE);
|
|
23142
23465
|
}
|
|
@@ -23164,7 +23487,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
23164
23487
|
console.error("Error: --title is required when creating a new page");
|
|
23165
23488
|
process.exit(EXIT_USAGE);
|
|
23166
23489
|
}
|
|
23167
|
-
const adf =
|
|
23490
|
+
const adf = fs14.readFileSync(path15.resolve(inputFile), "utf8");
|
|
23168
23491
|
if (dryRun) {
|
|
23169
23492
|
console.log(
|
|
23170
23493
|
JSON.stringify(
|
|
@@ -23243,7 +23566,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
23243
23566
|
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
23244
23567
|
process.exit(EXIT_USAGE);
|
|
23245
23568
|
}
|
|
23246
|
-
if (!
|
|
23569
|
+
if (!fs14.existsSync(inputFile)) {
|
|
23247
23570
|
console.error(`Error: file not found: ${inputFile}`);
|
|
23248
23571
|
process.exit(EXIT_USAGE);
|
|
23249
23572
|
}
|
|
@@ -23270,7 +23593,7 @@ Generate an API token at https://id.atlassian.com/manage-profile/security/api-to
|
|
|
23270
23593
|
process.exit(EXIT_USAGE);
|
|
23271
23594
|
}
|
|
23272
23595
|
const mode = modeRaw;
|
|
23273
|
-
const adf =
|
|
23596
|
+
const adf = fs14.readFileSync(path15.resolve(inputFile), "utf8");
|
|
23274
23597
|
if (dryRun) {
|
|
23275
23598
|
console.log(
|
|
23276
23599
|
JSON.stringify(
|
|
@@ -23432,7 +23755,7 @@ async function runBuildDocs(rawArgs) {
|
|
|
23432
23755
|
if (result.apiPages > 0) {
|
|
23433
23756
|
console.log(` \u2022 API pages \u2192 src/content/docs/api (${result.apiPages})`);
|
|
23434
23757
|
}
|
|
23435
|
-
const rel =
|
|
23758
|
+
const rel = path15.relative(process.cwd(), result.siteDir) || ".";
|
|
23436
23759
|
console.log(`
|
|
23437
23760
|
Preview: cd ${rel} && npm run dev`);
|
|
23438
23761
|
return EXIT_SUCCESS;
|
|
@@ -23447,6 +23770,212 @@ Preview: cd ${rel} && npm run dev`);
|
|
|
23447
23770
|
return EXIT_USAGE;
|
|
23448
23771
|
}
|
|
23449
23772
|
}
|
|
23773
|
+
async function runDeploy(rawArgs) {
|
|
23774
|
+
const mode = rawArgs[0];
|
|
23775
|
+
if (!mode || !["record", "status", "diff"].includes(mode)) {
|
|
23776
|
+
console.error("Usage: executable-stories deploy <record|status|diff> [options]");
|
|
23777
|
+
console.error(" deploy record <file> --env <env> [--tag <tag>] [--ledger <path>]");
|
|
23778
|
+
console.error(" deploy status [--ledger <path>] [--json]");
|
|
23779
|
+
console.error(" deploy diff <env-a> <env-b> [--ledger <path>] [--json]");
|
|
23780
|
+
return EXIT_USAGE;
|
|
23781
|
+
}
|
|
23782
|
+
const { values, positionals } = parseArgs({
|
|
23783
|
+
args: rawArgs.slice(1),
|
|
23784
|
+
options: {
|
|
23785
|
+
env: { type: "string" },
|
|
23786
|
+
tag: { type: "string" },
|
|
23787
|
+
ledger: { type: "string", default: ".executable-stories/deployments.json" },
|
|
23788
|
+
json: { type: "boolean", default: false },
|
|
23789
|
+
help: { type: "boolean", default: false }
|
|
23790
|
+
},
|
|
23791
|
+
allowPositionals: true,
|
|
23792
|
+
strict: true
|
|
23793
|
+
});
|
|
23794
|
+
if (values.help) {
|
|
23795
|
+
console.log(`executable-stories deploy \u2014 Track deployments across environments.
|
|
23796
|
+
|
|
23797
|
+
USAGE
|
|
23798
|
+
executable-stories deploy record <file> --env <env> [--tag <tag>] [--ledger <path>]
|
|
23799
|
+
executable-stories deploy status [--ledger <path>] [--json]
|
|
23800
|
+
executable-stories deploy diff <env-a> <env-b> [--ledger <path>] [--json]
|
|
23801
|
+
|
|
23802
|
+
OPTIONS
|
|
23803
|
+
--env <env> Environment name (e.g. dev, staging, production)
|
|
23804
|
+
--tag <tag> Optional Git tag for this deployment (e.g. v1.2.3)
|
|
23805
|
+
--ledger <path> Path to deployment ledger JSON (default: .executable-stories/deployments.json)
|
|
23806
|
+
--json Output as JSON instead of text`);
|
|
23807
|
+
return EXIT_SUCCESS;
|
|
23808
|
+
}
|
|
23809
|
+
const ledgerPath = values.ledger;
|
|
23810
|
+
if (mode === "record") {
|
|
23811
|
+
const inputFile = positionals[0];
|
|
23812
|
+
if (!inputFile) {
|
|
23813
|
+
console.error("Error: deploy record requires an input file.");
|
|
23814
|
+
return EXIT_USAGE;
|
|
23815
|
+
}
|
|
23816
|
+
const env = values.env;
|
|
23817
|
+
if (!env) {
|
|
23818
|
+
console.error("Error: deploy record requires --env <environment>.");
|
|
23819
|
+
return EXIT_USAGE;
|
|
23820
|
+
}
|
|
23821
|
+
const text2 = readFileInput(inputFile);
|
|
23822
|
+
const { run } = normalizeRunFromText(text2, {
|
|
23823
|
+
...createDefaultCliArgs(),
|
|
23824
|
+
inputType: "raw",
|
|
23825
|
+
inputFile
|
|
23826
|
+
});
|
|
23827
|
+
const applied = applySelection(run, createDefaultCliArgs());
|
|
23828
|
+
const result = recordDeployment({
|
|
23829
|
+
run: applied,
|
|
23830
|
+
environment: env,
|
|
23831
|
+
tag: values.tag,
|
|
23832
|
+
ledgerPath,
|
|
23833
|
+
runFilePath: inputFile
|
|
23834
|
+
});
|
|
23835
|
+
console.error(
|
|
23836
|
+
`Recorded deployment to "${result.entry.environment}" at ${result.entry.timestamp}`
|
|
23837
|
+
);
|
|
23838
|
+
console.error(
|
|
23839
|
+
` Scenarios: ${result.entry.summary.total} (${result.entry.summary.passed} passed, ${result.entry.summary.failed} failed)`
|
|
23840
|
+
);
|
|
23841
|
+
if (result.entry.tag) {
|
|
23842
|
+
console.error(` Tag: ${result.entry.tag}`);
|
|
23843
|
+
}
|
|
23844
|
+
console.error(` Ledger: ${result.ledgerPath}`);
|
|
23845
|
+
return EXIT_SUCCESS;
|
|
23846
|
+
}
|
|
23847
|
+
if (mode === "status") {
|
|
23848
|
+
const status = getDeploymentStatus(ledgerPath);
|
|
23849
|
+
const envs = Object.keys(status.environments);
|
|
23850
|
+
if (envs.length === 0) {
|
|
23851
|
+
console.error("No deployments recorded yet.");
|
|
23852
|
+
return EXIT_SUCCESS;
|
|
23853
|
+
}
|
|
23854
|
+
if (values.json) {
|
|
23855
|
+
console.log(JSON.stringify(status, null, 2));
|
|
23856
|
+
} else {
|
|
23857
|
+
for (const envName of envs) {
|
|
23858
|
+
const env = status.environments[envName];
|
|
23859
|
+
if (!env) continue;
|
|
23860
|
+
const e = env.latest;
|
|
23861
|
+
console.log(`${envName}:`);
|
|
23862
|
+
console.log(` Deployed: ${e.timestamp}`);
|
|
23863
|
+
console.log(` SHA: ${e.sha ?? "unknown"}`);
|
|
23864
|
+
console.log(` Tag: ${e.tag ?? "none"}`);
|
|
23865
|
+
console.log(` Scenarios: ${e.summary.total} (${e.summary.passed} passed, ${e.summary.failed} failed, ${e.summary.skipped} skipped, ${e.summary.pending} pending)`);
|
|
23866
|
+
if (env.previousDeployment) {
|
|
23867
|
+
const prev = env.previousDeployment;
|
|
23868
|
+
const added = e.scenarioIds.filter((id) => !new Set(prev.scenarioIds).has(id)).length;
|
|
23869
|
+
const removed = prev.scenarioIds.filter((id) => !new Set(e.scenarioIds).has(id)).length;
|
|
23870
|
+
if (added > 0 || removed > 0) {
|
|
23871
|
+
console.log(` Drift from previous: +${added} added, -${removed} removed`);
|
|
23872
|
+
}
|
|
23873
|
+
}
|
|
23874
|
+
console.log();
|
|
23875
|
+
}
|
|
23876
|
+
console.log(`Ledger: ${ledgerPath}`);
|
|
23877
|
+
}
|
|
23878
|
+
return EXIT_SUCCESS;
|
|
23879
|
+
}
|
|
23880
|
+
if (mode === "diff") {
|
|
23881
|
+
const envA = positionals[0];
|
|
23882
|
+
const envB = positionals[1];
|
|
23883
|
+
if (!envA || !envB) {
|
|
23884
|
+
console.error("Error: deploy diff requires two environment names.");
|
|
23885
|
+
return EXIT_USAGE;
|
|
23886
|
+
}
|
|
23887
|
+
try {
|
|
23888
|
+
const drift = getEnvironmentDrift(ledgerPath, envA, envB);
|
|
23889
|
+
if (values.json) {
|
|
23890
|
+
console.log(JSON.stringify(drift, null, 2));
|
|
23891
|
+
} else {
|
|
23892
|
+
console.log(`Environment drift: ${envA} \u2194 ${envB}`);
|
|
23893
|
+
console.log(` ${envA}: ${drift.aEntry.summary.total} scenarios (${drift.aEntry.timestamp})`);
|
|
23894
|
+
console.log(` ${envB}: ${drift.bEntry.summary.total} scenarios (${drift.bEntry.timestamp})`);
|
|
23895
|
+
console.log(` In both: ${drift.inBoth.length}`);
|
|
23896
|
+
console.log(` Only in ${envA}: ${drift.onlyInA.length}`);
|
|
23897
|
+
console.log(` Only in ${envB}: ${drift.onlyInB.length}`);
|
|
23898
|
+
console.log(` Status changed: ${drift.statusChanged.length}`);
|
|
23899
|
+
if (drift.onlyInA.length > 0) {
|
|
23900
|
+
console.log(`
|
|
23901
|
+
Only in ${envA}:`);
|
|
23902
|
+
for (const id of drift.onlyInA.slice(0, 20)) {
|
|
23903
|
+
console.log(` - ${id}`);
|
|
23904
|
+
}
|
|
23905
|
+
if (drift.onlyInA.length > 20) {
|
|
23906
|
+
console.log(` ... and ${drift.onlyInA.length - 20} more`);
|
|
23907
|
+
}
|
|
23908
|
+
}
|
|
23909
|
+
if (drift.onlyInB.length > 0) {
|
|
23910
|
+
console.log(`
|
|
23911
|
+
Only in ${envB}:`);
|
|
23912
|
+
for (const id of drift.onlyInB.slice(0, 20)) {
|
|
23913
|
+
console.log(` - ${id}`);
|
|
23914
|
+
}
|
|
23915
|
+
if (drift.onlyInB.length > 20) {
|
|
23916
|
+
console.log(` ... and ${drift.onlyInB.length - 20} more`);
|
|
23917
|
+
}
|
|
23918
|
+
}
|
|
23919
|
+
if (drift.statusChanged.length > 0) {
|
|
23920
|
+
console.log("\n Status changed:");
|
|
23921
|
+
for (const item of drift.statusChanged.slice(0, 20)) {
|
|
23922
|
+
console.log(` - ${item.id}: ${item.statusA} -> ${item.statusB}`);
|
|
23923
|
+
}
|
|
23924
|
+
if (drift.statusChanged.length > 20) {
|
|
23925
|
+
console.log(` ... and ${drift.statusChanged.length - 20} more`);
|
|
23926
|
+
}
|
|
23927
|
+
}
|
|
23928
|
+
}
|
|
23929
|
+
} catch (err) {
|
|
23930
|
+
console.error(`Error: ${err.message}`);
|
|
23931
|
+
return EXIT_USAGE;
|
|
23932
|
+
}
|
|
23933
|
+
return EXIT_SUCCESS;
|
|
23934
|
+
}
|
|
23935
|
+
return EXIT_USAGE;
|
|
23936
|
+
}
|
|
23937
|
+
function createDefaultCliArgs() {
|
|
23938
|
+
return {
|
|
23939
|
+
subcommand: "format",
|
|
23940
|
+
stdin: false,
|
|
23941
|
+
formats: [],
|
|
23942
|
+
inputType: "raw",
|
|
23943
|
+
outputDir: "reports",
|
|
23944
|
+
outputName: "index",
|
|
23945
|
+
outputNameTimestamp: false,
|
|
23946
|
+
sortTestCases: "none",
|
|
23947
|
+
include: [],
|
|
23948
|
+
exclude: [],
|
|
23949
|
+
includeTags: [],
|
|
23950
|
+
excludeTags: [],
|
|
23951
|
+
synthesizeStories: true,
|
|
23952
|
+
htmlTitle: "Test Results",
|
|
23953
|
+
htmlTheme: "default",
|
|
23954
|
+
htmlNoSyntaxHighlighting: false,
|
|
23955
|
+
htmlNoMermaid: false,
|
|
23956
|
+
htmlNoMarkdown: false,
|
|
23957
|
+
htmlNoToc: false,
|
|
23958
|
+
htmlThemePicker: false,
|
|
23959
|
+
jsonSummary: false,
|
|
23960
|
+
listFormat: "text",
|
|
23961
|
+
notify: "never",
|
|
23962
|
+
maxFailedTests: 5,
|
|
23963
|
+
maxHistoryRuns: 10,
|
|
23964
|
+
webhookUrls: [],
|
|
23965
|
+
webhookHeaders: {},
|
|
23966
|
+
webhookMethod: "POST",
|
|
23967
|
+
webhookHmacHeader: "X-Signature",
|
|
23968
|
+
webhookHmacTimestamp: false,
|
|
23969
|
+
assetMode: "none",
|
|
23970
|
+
allowMissingAssets: false,
|
|
23971
|
+
prSummary: false,
|
|
23972
|
+
failOnRegression: false,
|
|
23973
|
+
failOnAddedFailures: false,
|
|
23974
|
+
failOnRemoval: false,
|
|
23975
|
+
failOnNew: false,
|
|
23976
|
+
baselineMode: "explicit"
|
|
23977
|
+
};
|
|
23978
|
+
}
|
|
23450
23979
|
main().catch((err) => {
|
|
23451
23980
|
console.error(err);
|
|
23452
23981
|
process.exit(EXIT_USAGE);
|