executable-stories-formatters 0.7.8 → 0.7.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/dist/cli.js +1010 -78
- package/dist/cli.js.map +1 -1
- package/dist/index.cjs +725 -52
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +202 -5
- package/dist/index.d.ts +202 -5
- package/dist/index.js +722 -52
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
package/dist/cli.js
CHANGED
|
@@ -596,8 +596,8 @@ function generateRunId(startedAtMs, projectRoot) {
|
|
|
596
596
|
const input = `${startedAtMs}::${projectRoot}`;
|
|
597
597
|
return createHash("sha1").update(input).digest("hex").slice(0, 16);
|
|
598
598
|
}
|
|
599
|
-
function slugify(
|
|
600
|
-
return
|
|
599
|
+
function slugify(text2) {
|
|
600
|
+
return text2.toLowerCase().replace(/[/\\]+/g, "-").replace(/[^\w\s-]/g, "").replace(/[\s_]+/g, "-").replace(/-+/g, "-").replace(/^-+|-+$/g, "");
|
|
601
601
|
}
|
|
602
602
|
function generateFeatureId(uri) {
|
|
603
603
|
const pathWithoutExt = uri.replace(/\.[^.]+$/, "");
|
|
@@ -13696,7 +13696,7 @@ function renderDocEntry(entry, deps) {
|
|
|
13696
13696
|
// src/formatters/html/renderers/steps.ts
|
|
13697
13697
|
var CONTINUATION_KEYWORDS = ["And", "But", "*"];
|
|
13698
13698
|
function renderStep(step, stepResult, index, deps) {
|
|
13699
|
-
const
|
|
13699
|
+
const statusIcon2 = stepResult ? deps.getStatusIcon(stepResult.status) : "\u25CB";
|
|
13700
13700
|
const statusClass = stepResult ? `status-${stepResult.status}` : "";
|
|
13701
13701
|
const duration = stepResult && stepResult.durationMs > 0 ? `${stepResult.durationMs}ms` : "";
|
|
13702
13702
|
const keywordTrimmed = step.keyword.trim();
|
|
@@ -13705,7 +13705,7 @@ function renderStep(step, stepResult, index, deps) {
|
|
|
13705
13705
|
const stepDocs = deps.renderDocs(step.docs, "step-docs");
|
|
13706
13706
|
const textHtml = deps.highlightStepParams ? deps.highlightStepParams(step.text) : deps.escapeHtml(step.text);
|
|
13707
13707
|
return `<div class="${stepClass}" data-keyword="${deps.escapeHtml(keywordTrimmed)}" data-text="${deps.escapeHtml(step.text)}">
|
|
13708
|
-
<span class="step-status ${statusClass}">${
|
|
13708
|
+
<span class="step-status ${statusClass}">${statusIcon2}</span>
|
|
13709
13709
|
<span class="step-keyword">${deps.escapeHtml(step.keyword)}</span>
|
|
13710
13710
|
<span class="step-text">${textHtml}</span>
|
|
13711
13711
|
<span class="step-duration">${duration}</span>
|
|
@@ -13721,10 +13721,10 @@ function renderSteps(args, deps) {
|
|
|
13721
13721
|
|
|
13722
13722
|
// src/formatters/html/renderers/step-params.ts
|
|
13723
13723
|
var STEP_PARAM_PATTERN = /"[^"]*"|(?<![\w.\-])\d+(?:\.\d+)?(?![\w.\-])/g;
|
|
13724
|
-
function highlightStepParams(
|
|
13725
|
-
const matches = Array.from(
|
|
13724
|
+
function highlightStepParams(text2, deps) {
|
|
13725
|
+
const matches = Array.from(text2.matchAll(STEP_PARAM_PATTERN));
|
|
13726
13726
|
if (matches.length === 0) {
|
|
13727
|
-
return deps.escapeHtml(
|
|
13727
|
+
return deps.escapeHtml(text2);
|
|
13728
13728
|
}
|
|
13729
13729
|
let result = "";
|
|
13730
13730
|
let lastIndex = 0;
|
|
@@ -13732,13 +13732,13 @@ function highlightStepParams(text, deps) {
|
|
|
13732
13732
|
const matchStart = match.index;
|
|
13733
13733
|
const matchEnd = matchStart + match[0].length;
|
|
13734
13734
|
if (matchStart > lastIndex) {
|
|
13735
|
-
result += deps.escapeHtml(
|
|
13735
|
+
result += deps.escapeHtml(text2.slice(lastIndex, matchStart));
|
|
13736
13736
|
}
|
|
13737
13737
|
result += `<span class="step-param">${deps.escapeHtml(match[0])}</span>`;
|
|
13738
13738
|
lastIndex = matchEnd;
|
|
13739
13739
|
}
|
|
13740
|
-
if (lastIndex <
|
|
13741
|
-
result += deps.escapeHtml(
|
|
13740
|
+
if (lastIndex < text2.length) {
|
|
13741
|
+
result += deps.escapeHtml(text2.slice(lastIndex));
|
|
13742
13742
|
}
|
|
13743
13743
|
return result;
|
|
13744
13744
|
}
|
|
@@ -13756,7 +13756,7 @@ function renderTicket(ticket, template, escapeHtml3) {
|
|
|
13756
13756
|
}
|
|
13757
13757
|
function renderScenario(args, deps) {
|
|
13758
13758
|
const { tc } = args;
|
|
13759
|
-
const
|
|
13759
|
+
const statusIcon2 = deps.getStatusIcon(tc.status);
|
|
13760
13760
|
const statusClass = `status-${tc.status}`;
|
|
13761
13761
|
const duration = tc.durationMs > 0 ? `${(tc.durationMs / 1e3).toFixed(2)}s` : "";
|
|
13762
13762
|
const tags = tc.tags.map((t) => `<span class="tag">${deps.escapeHtml(t)}</span>`).join("");
|
|
@@ -13825,7 +13825,7 @@ function renderScenario(args, deps) {
|
|
|
13825
13825
|
<div class="scenario-header" role="button" tabindex="0" aria-expanded="${ariaExpanded}">
|
|
13826
13826
|
<div class="scenario-info">
|
|
13827
13827
|
<div class="scenario-title">
|
|
13828
|
-
<span class="status-icon ${statusClass}">${
|
|
13828
|
+
<span class="status-icon ${statusClass}">${statusIcon2}</span>
|
|
13829
13829
|
<span class="scenario-name">${deps.escapeHtml(tc.story.scenario)}</span>
|
|
13830
13830
|
</div>
|
|
13831
13831
|
<div class="scenario-meta">${tags}${tickets}${sourceLink}${traceBadge}${metricBadges}</div>
|
|
@@ -13964,11 +13964,11 @@ function buildTooltip(span, escapeHtml3) {
|
|
|
13964
13964
|
parts.push(`${key}=${formatted}`);
|
|
13965
13965
|
}
|
|
13966
13966
|
}
|
|
13967
|
-
let
|
|
13968
|
-
if (
|
|
13969
|
-
|
|
13967
|
+
let text2 = parts.join("\n");
|
|
13968
|
+
if (text2.length > TOOLTIP_MAX_LENGTH) {
|
|
13969
|
+
text2 = text2.slice(0, TOOLTIP_MAX_LENGTH - 3) + "...";
|
|
13970
13970
|
}
|
|
13971
|
-
return escapeHtml3(
|
|
13971
|
+
return escapeHtml3(text2);
|
|
13972
13972
|
}
|
|
13973
13973
|
function renderTraceView(args, deps) {
|
|
13974
13974
|
if (!args.spans || args.spans.length === 0) return "";
|
|
@@ -14191,11 +14191,11 @@ function renderToc(args, deps) {
|
|
|
14191
14191
|
const featureName = suitePaths.length > 0 && suitePaths[0].length > 0 ? suitePaths[0][0] : file.split("/").pop()?.replace(/\.[^.]+$/, "") ?? file;
|
|
14192
14192
|
const featureSlug = `feature-${slugify(file)}`;
|
|
14193
14193
|
const scenarios = testCases.map((tc) => {
|
|
14194
|
-
const
|
|
14194
|
+
const statusIcon2 = deps.getStatusIcon(tc.status);
|
|
14195
14195
|
const statusClass = `status-${tc.status}`;
|
|
14196
14196
|
const failedClass = tc.status === "failed" ? " toc-failed" : "";
|
|
14197
14197
|
return `<a class="toc-scenario${failedClass}" href="#scenario-${tc.id}">
|
|
14198
|
-
<span class="toc-status ${statusClass}">${
|
|
14198
|
+
<span class="toc-status ${statusClass}">${statusIcon2}</span>
|
|
14199
14199
|
${deps.escapeHtml(tc.story.scenario)}
|
|
14200
14200
|
</a>`;
|
|
14201
14201
|
}).join("\n");
|
|
@@ -14253,7 +14253,7 @@ function createHtmlFormatter(options = {}) {
|
|
|
14253
14253
|
escapeHtml,
|
|
14254
14254
|
getStatusIcon,
|
|
14255
14255
|
renderDocs,
|
|
14256
|
-
highlightStepParams: (
|
|
14256
|
+
highlightStepParams: (text2) => highlightStepParams(text2, { escapeHtml })
|
|
14257
14257
|
};
|
|
14258
14258
|
const scenarioDeps = {
|
|
14259
14259
|
escapeHtml,
|
|
@@ -15188,7 +15188,7 @@ function deterministicId(kind, salt, ...parts) {
|
|
|
15188
15188
|
|
|
15189
15189
|
// src/formatters/cucumber-messages/build-gherkin-document.ts
|
|
15190
15190
|
function buildGherkinDocumentEnvelopes(uri, testCases, synthesized, salt) {
|
|
15191
|
-
const { lineMap, featureName, featureTags, text } = synthesized;
|
|
15191
|
+
const { lineMap, featureName, featureTags, text: text2 } = synthesized;
|
|
15192
15192
|
const featureTagNodes = featureTags.map((tag, i) => ({
|
|
15193
15193
|
location: {
|
|
15194
15194
|
line: lineMap.featureTagLine ?? 1,
|
|
@@ -15257,7 +15257,7 @@ function buildGherkinDocumentEnvelopes(uri, testCases, synthesized, salt) {
|
|
|
15257
15257
|
sourceEnvelope: {
|
|
15258
15258
|
source: {
|
|
15259
15259
|
uri,
|
|
15260
|
-
data:
|
|
15260
|
+
data: text2,
|
|
15261
15261
|
mediaType: "text/x.cucumber.gherkin+plain"
|
|
15262
15262
|
}
|
|
15263
15263
|
},
|
|
@@ -15268,8 +15268,8 @@ function buildStepArguments(step, stepLine) {
|
|
|
15268
15268
|
if (!step.docs || step.docs.length === 0) return {};
|
|
15269
15269
|
const tableDocs = step.docs.filter((d) => d.kind === "table");
|
|
15270
15270
|
if (tableDocs.length > 0) {
|
|
15271
|
-
const
|
|
15272
|
-
return { dataTable: buildDataTable(
|
|
15271
|
+
const table2 = tableDocs[0];
|
|
15272
|
+
return { dataTable: buildDataTable(table2, stepLine + 1) };
|
|
15273
15273
|
}
|
|
15274
15274
|
for (const doc of step.docs) {
|
|
15275
15275
|
const ds = docEntryToDocString(doc, stepLine + 1);
|
|
@@ -15340,21 +15340,21 @@ function docEntryToDocString(doc, line) {
|
|
|
15340
15340
|
return void 0;
|
|
15341
15341
|
}
|
|
15342
15342
|
}
|
|
15343
|
-
function buildDataTable(
|
|
15343
|
+
function buildDataTable(table2, line) {
|
|
15344
15344
|
const rows = [];
|
|
15345
15345
|
rows.push({
|
|
15346
15346
|
location: { line },
|
|
15347
|
-
cells:
|
|
15347
|
+
cells: table2.columns.map((col) => ({
|
|
15348
15348
|
location: { line },
|
|
15349
15349
|
value: col
|
|
15350
15350
|
})),
|
|
15351
15351
|
id: ""
|
|
15352
15352
|
});
|
|
15353
|
-
for (let r = 0; r <
|
|
15353
|
+
for (let r = 0; r < table2.rows.length; r++) {
|
|
15354
15354
|
const rowLine = line + 1 + r;
|
|
15355
15355
|
rows.push({
|
|
15356
15356
|
location: { line: rowLine },
|
|
15357
|
-
cells:
|
|
15357
|
+
cells: table2.rows[r].map((cell) => ({
|
|
15358
15358
|
location: { line: rowLine },
|
|
15359
15359
|
value: cell
|
|
15360
15360
|
})),
|
|
@@ -15445,12 +15445,12 @@ function docEntryToPickleDocString(doc) {
|
|
|
15445
15445
|
return void 0;
|
|
15446
15446
|
}
|
|
15447
15447
|
}
|
|
15448
|
-
function buildPickleTable(
|
|
15448
|
+
function buildPickleTable(table2) {
|
|
15449
15449
|
const rows = [];
|
|
15450
15450
|
rows.push({
|
|
15451
|
-
cells:
|
|
15451
|
+
cells: table2.columns.map((col) => ({ value: col }))
|
|
15452
15452
|
});
|
|
15453
|
-
for (const row of
|
|
15453
|
+
for (const row of table2.rows) {
|
|
15454
15454
|
rows.push({
|
|
15455
15455
|
cells: row.map((cell) => ({ value: cell }))
|
|
15456
15456
|
});
|
|
@@ -16896,6 +16896,414 @@ ${body}`;
|
|
|
16896
16896
|
}
|
|
16897
16897
|
};
|
|
16898
16898
|
|
|
16899
|
+
// src/formatters/confluence.ts
|
|
16900
|
+
var ConfluenceFormatter = class {
|
|
16901
|
+
options;
|
|
16902
|
+
constructor(options = {}) {
|
|
16903
|
+
this.options = {
|
|
16904
|
+
title: options.title ?? "User Stories",
|
|
16905
|
+
includeStatusIcons: options.includeStatusIcons ?? true,
|
|
16906
|
+
includeMetadata: options.includeMetadata ?? true,
|
|
16907
|
+
includeSummaryTable: options.includeSummaryTable ?? true,
|
|
16908
|
+
includeErrors: options.includeErrors ?? true,
|
|
16909
|
+
scenarioHeadingLevel: options.scenarioHeadingLevel ?? 3,
|
|
16910
|
+
groupBy: options.groupBy ?? "file",
|
|
16911
|
+
sortScenarios: options.sortScenarios ?? "source",
|
|
16912
|
+
pretty: options.pretty ?? true,
|
|
16913
|
+
permalinkBaseUrl: options.permalinkBaseUrl,
|
|
16914
|
+
ticketUrlTemplate: options.ticketUrlTemplate
|
|
16915
|
+
};
|
|
16916
|
+
}
|
|
16917
|
+
/** Build the ADF document tree. Returns the JS object (not stringified). */
|
|
16918
|
+
formatToAdf(run) {
|
|
16919
|
+
const content = [];
|
|
16920
|
+
content.push(heading(1, [text(this.options.title)]));
|
|
16921
|
+
if (this.options.includeMetadata) {
|
|
16922
|
+
const metaTable = this.renderMetadataTable(run);
|
|
16923
|
+
if (metaTable) content.push(metaTable);
|
|
16924
|
+
}
|
|
16925
|
+
if (this.options.includeSummaryTable) {
|
|
16926
|
+
content.push(this.renderSummaryTable(run));
|
|
16927
|
+
}
|
|
16928
|
+
switch (this.options.groupBy) {
|
|
16929
|
+
case "none":
|
|
16930
|
+
this.renderFlat(content, run.testCases);
|
|
16931
|
+
break;
|
|
16932
|
+
case "suite":
|
|
16933
|
+
this.renderBySuite(content, run.testCases);
|
|
16934
|
+
break;
|
|
16935
|
+
case "file":
|
|
16936
|
+
default:
|
|
16937
|
+
this.renderByFile(content, run.testCases);
|
|
16938
|
+
break;
|
|
16939
|
+
}
|
|
16940
|
+
return { version: 1, type: "doc", content };
|
|
16941
|
+
}
|
|
16942
|
+
/** Format a test run as an ADF JSON string. */
|
|
16943
|
+
format(run) {
|
|
16944
|
+
const adf = this.formatToAdf(run);
|
|
16945
|
+
return this.options.pretty ? JSON.stringify(adf, null, 2) : JSON.stringify(adf);
|
|
16946
|
+
}
|
|
16947
|
+
// --------------------------------------------------------------------------
|
|
16948
|
+
// Metadata / summary tables
|
|
16949
|
+
// --------------------------------------------------------------------------
|
|
16950
|
+
renderMetadataTable(run) {
|
|
16951
|
+
const rows = [];
|
|
16952
|
+
rows.push(["Date", new Date(run.startedAtMs).toISOString()]);
|
|
16953
|
+
if (run.packageVersion) rows.push(["Version", run.packageVersion]);
|
|
16954
|
+
if (run.gitSha) {
|
|
16955
|
+
const shortSha = run.gitSha.length > 7 ? run.gitSha.slice(0, 7) : run.gitSha;
|
|
16956
|
+
rows.push(["Git SHA", shortSha]);
|
|
16957
|
+
}
|
|
16958
|
+
if (rows.length === 0) return null;
|
|
16959
|
+
return table([
|
|
16960
|
+
tableRow([tableHeader("Key"), tableHeader("Value")]),
|
|
16961
|
+
...rows.map(([k, v]) => tableRow([tableCell(k), tableCell(v)]))
|
|
16962
|
+
]);
|
|
16963
|
+
}
|
|
16964
|
+
renderSummaryTable(run) {
|
|
16965
|
+
const total = run.testCases.length;
|
|
16966
|
+
const steps = run.testCases.reduce(
|
|
16967
|
+
(acc, tc) => acc + tc.story.steps.length,
|
|
16968
|
+
0
|
|
16969
|
+
);
|
|
16970
|
+
const passed = run.testCases.filter((tc) => tc.status === "passed").length;
|
|
16971
|
+
const failed = run.testCases.filter((tc) => tc.status === "failed").length;
|
|
16972
|
+
const skipped = run.testCases.filter((tc) => tc.status === "skipped").length;
|
|
16973
|
+
const pending = run.testCases.filter((tc) => tc.status === "pending").length;
|
|
16974
|
+
return table([
|
|
16975
|
+
tableRow([
|
|
16976
|
+
tableHeader("Scenarios"),
|
|
16977
|
+
tableHeader("Steps"),
|
|
16978
|
+
tableHeader("Passed"),
|
|
16979
|
+
tableHeader("Failed"),
|
|
16980
|
+
tableHeader("Skipped"),
|
|
16981
|
+
tableHeader("Pending"),
|
|
16982
|
+
tableHeader("Duration")
|
|
16983
|
+
]),
|
|
16984
|
+
tableRow([
|
|
16985
|
+
tableCell(String(total)),
|
|
16986
|
+
tableCell(String(steps)),
|
|
16987
|
+
tableCell(String(passed)),
|
|
16988
|
+
tableCell(String(failed)),
|
|
16989
|
+
tableCell(String(skipped)),
|
|
16990
|
+
tableCell(String(pending)),
|
|
16991
|
+
tableCell(formatDuration2(run.durationMs))
|
|
16992
|
+
])
|
|
16993
|
+
]);
|
|
16994
|
+
}
|
|
16995
|
+
// --------------------------------------------------------------------------
|
|
16996
|
+
// Grouping
|
|
16997
|
+
// --------------------------------------------------------------------------
|
|
16998
|
+
renderByFile(content, testCases) {
|
|
16999
|
+
const byFile = groupBy7(testCases, (tc) => tc.sourceFile);
|
|
17000
|
+
for (const [file, fileCases] of byFile) {
|
|
17001
|
+
content.push(heading(2, [codeInline(file)]));
|
|
17002
|
+
this.renderSuiteGroups(content, fileCases, 3);
|
|
17003
|
+
}
|
|
17004
|
+
}
|
|
17005
|
+
renderBySuite(content, testCases) {
|
|
17006
|
+
this.renderSuiteGroups(content, testCases, 2);
|
|
17007
|
+
}
|
|
17008
|
+
renderFlat(content, testCases) {
|
|
17009
|
+
const sorted = this.sortCases(testCases);
|
|
17010
|
+
for (const tc of sorted) this.renderScenario(content, tc);
|
|
17011
|
+
}
|
|
17012
|
+
renderSuiteGroups(content, testCases, baseLevel) {
|
|
17013
|
+
const bySuite = groupBy7(testCases, (tc) => tc.titlePath.join(" - "));
|
|
17014
|
+
const entries = this.sortSuiteGroups([...bySuite.entries()]);
|
|
17015
|
+
for (const [suitePath, cases] of entries) {
|
|
17016
|
+
if (suitePath) {
|
|
17017
|
+
content.push(
|
|
17018
|
+
heading(clampHeadingLevel(baseLevel), [text(suitePath)])
|
|
17019
|
+
);
|
|
17020
|
+
}
|
|
17021
|
+
for (const tc of this.sortCases(cases)) {
|
|
17022
|
+
this.renderScenario(content, tc);
|
|
17023
|
+
}
|
|
17024
|
+
}
|
|
17025
|
+
}
|
|
17026
|
+
// --------------------------------------------------------------------------
|
|
17027
|
+
// Scenario
|
|
17028
|
+
// --------------------------------------------------------------------------
|
|
17029
|
+
renderScenario(content, tc) {
|
|
17030
|
+
const level = clampHeadingLevel(this.options.scenarioHeadingLevel);
|
|
17031
|
+
const headingNodes = [];
|
|
17032
|
+
if (this.options.includeStatusIcons) {
|
|
17033
|
+
headingNodes.push(text(`${statusIcon(tc.status)} `));
|
|
17034
|
+
}
|
|
17035
|
+
headingNodes.push(text(tc.story.scenario));
|
|
17036
|
+
content.push(heading(level, headingNodes));
|
|
17037
|
+
const metaChildren = [];
|
|
17038
|
+
if (tc.tags.length > 0) {
|
|
17039
|
+
metaChildren.push(text("Tags: ", strong()));
|
|
17040
|
+
tc.tags.forEach((t, i) => {
|
|
17041
|
+
if (i > 0) metaChildren.push(text(", "));
|
|
17042
|
+
metaChildren.push(codeInline(t));
|
|
17043
|
+
});
|
|
17044
|
+
}
|
|
17045
|
+
if (tc.story.tickets && tc.story.tickets.length > 0) {
|
|
17046
|
+
if (metaChildren.length > 0) metaChildren.push(text(" | "));
|
|
17047
|
+
metaChildren.push(text("Tickets: ", strong()));
|
|
17048
|
+
tc.story.tickets.forEach((ticket, i) => {
|
|
17049
|
+
if (i > 0) metaChildren.push(text(", "));
|
|
17050
|
+
const url = ticket.url ?? (this.options.ticketUrlTemplate ? this.options.ticketUrlTemplate.replace("{ticket}", ticket.id) : void 0);
|
|
17051
|
+
metaChildren.push(
|
|
17052
|
+
url ? link(ticket.id, url) : codeInline(ticket.id)
|
|
17053
|
+
);
|
|
17054
|
+
});
|
|
17055
|
+
}
|
|
17056
|
+
if (this.options.permalinkBaseUrl && tc.sourceFile !== "unknown" && tc.sourceFile) {
|
|
17057
|
+
if (metaChildren.length > 0) metaChildren.push(text(" | "));
|
|
17058
|
+
metaChildren.push(text("Source: ", strong()));
|
|
17059
|
+
const base = this.options.permalinkBaseUrl.replace(/\/$/, "");
|
|
17060
|
+
const url = `${base}/${tc.sourceFile}${tc.sourceLine > 0 ? `#L${tc.sourceLine}` : ""}`;
|
|
17061
|
+
metaChildren.push(link(tc.sourceFile, url));
|
|
17062
|
+
}
|
|
17063
|
+
if (metaChildren.length > 0) {
|
|
17064
|
+
content.push(paragraph(metaChildren));
|
|
17065
|
+
}
|
|
17066
|
+
if (tc.story.docs && tc.story.docs.length > 0) {
|
|
17067
|
+
for (const doc of tc.story.docs) this.renderDocEntry(content, doc);
|
|
17068
|
+
}
|
|
17069
|
+
if (tc.story.steps.length > 0) {
|
|
17070
|
+
content.push(this.renderStepsList(tc.story.steps));
|
|
17071
|
+
for (const step of tc.story.steps) {
|
|
17072
|
+
if (step.docs && step.docs.length > 0) {
|
|
17073
|
+
for (const doc of step.docs) this.renderDocEntry(content, doc);
|
|
17074
|
+
}
|
|
17075
|
+
}
|
|
17076
|
+
}
|
|
17077
|
+
if (tc.status === "failed" && tc.errorMessage && this.options.includeErrors) {
|
|
17078
|
+
const errorContent = (tc.errorMessage ?? "") + (tc.errorStack ? `
|
|
17079
|
+
|
|
17080
|
+
${tc.errorStack}` : "");
|
|
17081
|
+
content.push(
|
|
17082
|
+
panel("warning", [paragraph([text("Failure", strong())])])
|
|
17083
|
+
);
|
|
17084
|
+
content.push(codeBlock(errorContent, "text"));
|
|
17085
|
+
}
|
|
17086
|
+
}
|
|
17087
|
+
renderStepsList(steps) {
|
|
17088
|
+
return {
|
|
17089
|
+
type: "bulletList",
|
|
17090
|
+
content: steps.map((step) => {
|
|
17091
|
+
const children = [text(`${step.keyword} `, strong()), text(step.text)];
|
|
17092
|
+
if (step.mode && step.mode !== "normal") {
|
|
17093
|
+
children.push(text(` (${step.mode})`, em()));
|
|
17094
|
+
}
|
|
17095
|
+
return {
|
|
17096
|
+
type: "listItem",
|
|
17097
|
+
content: [paragraph(children)]
|
|
17098
|
+
};
|
|
17099
|
+
})
|
|
17100
|
+
};
|
|
17101
|
+
}
|
|
17102
|
+
// --------------------------------------------------------------------------
|
|
17103
|
+
// Doc entries
|
|
17104
|
+
// --------------------------------------------------------------------------
|
|
17105
|
+
renderDocEntry(content, entry) {
|
|
17106
|
+
switch (entry.kind) {
|
|
17107
|
+
case "note":
|
|
17108
|
+
content.push(panel("info", [paragraph([text(entry.text)])]));
|
|
17109
|
+
break;
|
|
17110
|
+
case "tag": {
|
|
17111
|
+
const kids = [];
|
|
17112
|
+
entry.names.forEach((name, i) => {
|
|
17113
|
+
if (i > 0) kids.push(text(" "));
|
|
17114
|
+
kids.push(codeInline(name));
|
|
17115
|
+
});
|
|
17116
|
+
if (kids.length > 0) content.push(paragraph(kids));
|
|
17117
|
+
break;
|
|
17118
|
+
}
|
|
17119
|
+
case "kv": {
|
|
17120
|
+
const val = typeof entry.value === "string" ? entry.value : JSON.stringify(entry.value);
|
|
17121
|
+
content.push(
|
|
17122
|
+
paragraph([text(`${entry.label}: `, strong()), codeInline(val)])
|
|
17123
|
+
);
|
|
17124
|
+
break;
|
|
17125
|
+
}
|
|
17126
|
+
case "code":
|
|
17127
|
+
if (entry.label) {
|
|
17128
|
+
content.push(paragraph([text(entry.label, strong())]));
|
|
17129
|
+
}
|
|
17130
|
+
content.push(codeBlock(entry.content ?? "", entry.lang));
|
|
17131
|
+
break;
|
|
17132
|
+
case "table":
|
|
17133
|
+
if (entry.label) {
|
|
17134
|
+
content.push(paragraph([text(entry.label, strong())]));
|
|
17135
|
+
}
|
|
17136
|
+
content.push(
|
|
17137
|
+
table([
|
|
17138
|
+
tableRow(entry.columns.map((c) => tableHeader(c))),
|
|
17139
|
+
...entry.rows.map(
|
|
17140
|
+
(row) => tableRow(row.map((cell) => tableCell(cell)))
|
|
17141
|
+
)
|
|
17142
|
+
])
|
|
17143
|
+
);
|
|
17144
|
+
break;
|
|
17145
|
+
case "link":
|
|
17146
|
+
content.push(paragraph([link(entry.label, entry.url)]));
|
|
17147
|
+
break;
|
|
17148
|
+
case "section":
|
|
17149
|
+
if (entry.title) {
|
|
17150
|
+
content.push(paragraph([text(entry.title, strong())]));
|
|
17151
|
+
}
|
|
17152
|
+
if (entry.markdown) {
|
|
17153
|
+
for (const para of entry.markdown.split(/\n{2,}/)) {
|
|
17154
|
+
const trimmed = para.trim();
|
|
17155
|
+
if (trimmed) content.push(paragraph([text(trimmed)]));
|
|
17156
|
+
}
|
|
17157
|
+
}
|
|
17158
|
+
break;
|
|
17159
|
+
case "mermaid":
|
|
17160
|
+
if (entry.title) {
|
|
17161
|
+
content.push(paragraph([text(entry.title, strong())]));
|
|
17162
|
+
}
|
|
17163
|
+
content.push(codeBlock(entry.code ?? "", "mermaid"));
|
|
17164
|
+
break;
|
|
17165
|
+
case "screenshot":
|
|
17166
|
+
content.push(
|
|
17167
|
+
paragraph([
|
|
17168
|
+
text(entry.alt ?? "Screenshot", strong()),
|
|
17169
|
+
text(": "),
|
|
17170
|
+
link(entry.path, entry.path)
|
|
17171
|
+
])
|
|
17172
|
+
);
|
|
17173
|
+
break;
|
|
17174
|
+
case "custom":
|
|
17175
|
+
content.push(paragraph([text(`[${entry.type}]`, strong())]));
|
|
17176
|
+
content.push(codeBlock(JSON.stringify(entry.data ?? null, null, 2), "json"));
|
|
17177
|
+
break;
|
|
17178
|
+
}
|
|
17179
|
+
if (entry.children && entry.children.length > 0) {
|
|
17180
|
+
for (const child of entry.children) {
|
|
17181
|
+
this.renderDocEntry(content, child);
|
|
17182
|
+
}
|
|
17183
|
+
}
|
|
17184
|
+
}
|
|
17185
|
+
// --------------------------------------------------------------------------
|
|
17186
|
+
// Sorting
|
|
17187
|
+
// --------------------------------------------------------------------------
|
|
17188
|
+
sortCases(cases) {
|
|
17189
|
+
if (this.options.sortScenarios === "alpha") {
|
|
17190
|
+
return [...cases].sort(
|
|
17191
|
+
(a, b) => a.story.scenario.localeCompare(b.story.scenario)
|
|
17192
|
+
);
|
|
17193
|
+
}
|
|
17194
|
+
if (this.options.sortScenarios === "source") {
|
|
17195
|
+
return [...cases].sort(
|
|
17196
|
+
(a, b) => (a.story.sourceOrder ?? 0) - (b.story.sourceOrder ?? 0)
|
|
17197
|
+
);
|
|
17198
|
+
}
|
|
17199
|
+
return cases;
|
|
17200
|
+
}
|
|
17201
|
+
sortSuiteGroups(entries) {
|
|
17202
|
+
if (this.options.sortScenarios === "alpha") {
|
|
17203
|
+
return entries.sort(([a], [b]) => a.localeCompare(b));
|
|
17204
|
+
}
|
|
17205
|
+
if (this.options.sortScenarios === "source") {
|
|
17206
|
+
return entries.sort(([, a], [, b]) => {
|
|
17207
|
+
const minA = Math.min(...a.map((s) => s.story.sourceOrder ?? Infinity));
|
|
17208
|
+
const minB = Math.min(...b.map((s) => s.story.sourceOrder ?? Infinity));
|
|
17209
|
+
return minA - minB;
|
|
17210
|
+
});
|
|
17211
|
+
}
|
|
17212
|
+
return entries;
|
|
17213
|
+
}
|
|
17214
|
+
};
|
|
17215
|
+
function text(value, mark) {
|
|
17216
|
+
const node = { type: "text", text: value };
|
|
17217
|
+
if (mark) {
|
|
17218
|
+
node.marks = Array.isArray(mark) ? mark : [mark];
|
|
17219
|
+
}
|
|
17220
|
+
return node;
|
|
17221
|
+
}
|
|
17222
|
+
function strong() {
|
|
17223
|
+
return { type: "strong" };
|
|
17224
|
+
}
|
|
17225
|
+
function em() {
|
|
17226
|
+
return { type: "em" };
|
|
17227
|
+
}
|
|
17228
|
+
function codeMark() {
|
|
17229
|
+
return { type: "code" };
|
|
17230
|
+
}
|
|
17231
|
+
function codeInline(value) {
|
|
17232
|
+
return text(value, codeMark());
|
|
17233
|
+
}
|
|
17234
|
+
function link(label, href) {
|
|
17235
|
+
return text(label, { type: "link", attrs: { href } });
|
|
17236
|
+
}
|
|
17237
|
+
function paragraph(content) {
|
|
17238
|
+
return { type: "paragraph", content };
|
|
17239
|
+
}
|
|
17240
|
+
function heading(level, content) {
|
|
17241
|
+
return {
|
|
17242
|
+
type: "heading",
|
|
17243
|
+
attrs: { level: clampHeadingLevel(level) },
|
|
17244
|
+
content
|
|
17245
|
+
};
|
|
17246
|
+
}
|
|
17247
|
+
function codeBlock(content, lang) {
|
|
17248
|
+
return {
|
|
17249
|
+
type: "codeBlock",
|
|
17250
|
+
attrs: lang ? { language: lang } : {},
|
|
17251
|
+
content: content ? [{ type: "text", text: content }] : []
|
|
17252
|
+
};
|
|
17253
|
+
}
|
|
17254
|
+
function panel(panelType, content) {
|
|
17255
|
+
return { type: "panel", attrs: { panelType }, content };
|
|
17256
|
+
}
|
|
17257
|
+
function table(rows) {
|
|
17258
|
+
return {
|
|
17259
|
+
type: "table",
|
|
17260
|
+
attrs: { isNumberColumnEnabled: false, layout: "default" },
|
|
17261
|
+
content: rows
|
|
17262
|
+
};
|
|
17263
|
+
}
|
|
17264
|
+
function tableRow(cells) {
|
|
17265
|
+
return { type: "tableRow", content: cells };
|
|
17266
|
+
}
|
|
17267
|
+
function tableHeader(value) {
|
|
17268
|
+
return { type: "tableHeader", content: [paragraph([text(value)])] };
|
|
17269
|
+
}
|
|
17270
|
+
function tableCell(value) {
|
|
17271
|
+
return { type: "tableCell", content: [paragraph([text(value)])] };
|
|
17272
|
+
}
|
|
17273
|
+
function clampHeadingLevel(level) {
|
|
17274
|
+
if (level < 1) return 1;
|
|
17275
|
+
if (level > 6) return 6;
|
|
17276
|
+
return level;
|
|
17277
|
+
}
|
|
17278
|
+
function statusIcon(status) {
|
|
17279
|
+
switch (status) {
|
|
17280
|
+
case "passed":
|
|
17281
|
+
return "\u2705";
|
|
17282
|
+
case "failed":
|
|
17283
|
+
return "\u274C";
|
|
17284
|
+
case "skipped":
|
|
17285
|
+
return "\u23E9";
|
|
17286
|
+
case "pending":
|
|
17287
|
+
return "\u{1F4DD}";
|
|
17288
|
+
default:
|
|
17289
|
+
return "\u26A0\uFE0F";
|
|
17290
|
+
}
|
|
17291
|
+
}
|
|
17292
|
+
function formatDuration2(ms) {
|
|
17293
|
+
if (ms < 1e3) return `${ms}ms`;
|
|
17294
|
+
return `${(ms / 1e3).toFixed(2)}s`;
|
|
17295
|
+
}
|
|
17296
|
+
function groupBy7(items, keyFn) {
|
|
17297
|
+
const map = /* @__PURE__ */ new Map();
|
|
17298
|
+
for (const item of items) {
|
|
17299
|
+
const key = keyFn(item);
|
|
17300
|
+
const existing = map.get(key);
|
|
17301
|
+
if (existing) existing.push(item);
|
|
17302
|
+
else map.set(key, [item]);
|
|
17303
|
+
}
|
|
17304
|
+
return map;
|
|
17305
|
+
}
|
|
17306
|
+
|
|
16899
17307
|
// src/formatters/astro-assets.ts
|
|
16900
17308
|
import * as fs4 from "fs";
|
|
16901
17309
|
import * as path4 from "path";
|
|
@@ -17020,6 +17428,235 @@ function copyMarkdownAssets(options) {
|
|
|
17020
17428
|
};
|
|
17021
17429
|
}
|
|
17022
17430
|
|
|
17431
|
+
// src/publishers/confluence.ts
|
|
17432
|
+
function parseAdf(adf) {
|
|
17433
|
+
let parsed;
|
|
17434
|
+
try {
|
|
17435
|
+
parsed = JSON.parse(adf);
|
|
17436
|
+
} catch (err) {
|
|
17437
|
+
throw new Error(
|
|
17438
|
+
`ADF payload is not valid JSON: ${err.message}`
|
|
17439
|
+
);
|
|
17440
|
+
}
|
|
17441
|
+
if (!parsed || typeof parsed !== "object" || parsed.type !== "doc" || !Array.isArray(parsed.content)) {
|
|
17442
|
+
throw new Error(
|
|
17443
|
+
`ADF payload must be an object with { version, type: "doc", content: [...] }`
|
|
17444
|
+
);
|
|
17445
|
+
}
|
|
17446
|
+
return parsed;
|
|
17447
|
+
}
|
|
17448
|
+
function basicAuthHeader(auth) {
|
|
17449
|
+
const raw = `${auth.email}:${auth.token}`;
|
|
17450
|
+
const encoded = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(raw);
|
|
17451
|
+
return `Basic ${encoded}`;
|
|
17452
|
+
}
|
|
17453
|
+
async function parseErrorBody(response) {
|
|
17454
|
+
try {
|
|
17455
|
+
const body = await response.text();
|
|
17456
|
+
return body ? body.slice(0, 800) : "";
|
|
17457
|
+
} catch {
|
|
17458
|
+
return "";
|
|
17459
|
+
}
|
|
17460
|
+
}
|
|
17461
|
+
async function publishConfluencePage(args, deps) {
|
|
17462
|
+
parseAdf(args.adf);
|
|
17463
|
+
if (!args.pageId && !args.spaceId) {
|
|
17464
|
+
throw new Error(
|
|
17465
|
+
"publishConfluencePage requires either pageId (update) or spaceId (create)"
|
|
17466
|
+
);
|
|
17467
|
+
}
|
|
17468
|
+
if (!args.pageId && !args.title) {
|
|
17469
|
+
throw new Error("Creating a new page requires a title");
|
|
17470
|
+
}
|
|
17471
|
+
const base = args.baseUrl.replace(/\/$/, "");
|
|
17472
|
+
const fetchFn = deps.fetch ?? globalThis.fetch;
|
|
17473
|
+
if (!fetchFn) {
|
|
17474
|
+
throw new Error("No fetch implementation available (Node >= 22 expected)");
|
|
17475
|
+
}
|
|
17476
|
+
const headers = {
|
|
17477
|
+
Authorization: basicAuthHeader(deps.auth),
|
|
17478
|
+
Accept: "application/json",
|
|
17479
|
+
"Content-Type": "application/json"
|
|
17480
|
+
};
|
|
17481
|
+
if (args.pageId) {
|
|
17482
|
+
return updatePage(args, base, headers, fetchFn);
|
|
17483
|
+
}
|
|
17484
|
+
return createPage(args, base, headers, fetchFn);
|
|
17485
|
+
}
|
|
17486
|
+
async function updatePage(args, base, headers, fetchFn) {
|
|
17487
|
+
const getUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
|
|
17488
|
+
const getResp = await fetchFn(getUrl, { method: "GET", headers });
|
|
17489
|
+
if (!getResp.ok) {
|
|
17490
|
+
const body = await parseErrorBody(getResp);
|
|
17491
|
+
throw new Error(
|
|
17492
|
+
`GET ${getUrl} failed with ${getResp.status} ${getResp.statusText}${body ? `: ${body}` : ""}`
|
|
17493
|
+
);
|
|
17494
|
+
}
|
|
17495
|
+
const current = await getResp.json();
|
|
17496
|
+
const nextVersion = current.version.number + 1;
|
|
17497
|
+
const title = args.title ?? current.title;
|
|
17498
|
+
const putUrl = `${base}/api/v2/pages/${encodeURIComponent(args.pageId)}`;
|
|
17499
|
+
const putResp = await fetchFn(putUrl, {
|
|
17500
|
+
method: "PUT",
|
|
17501
|
+
headers,
|
|
17502
|
+
body: JSON.stringify({
|
|
17503
|
+
id: args.pageId,
|
|
17504
|
+
status: "current",
|
|
17505
|
+
title,
|
|
17506
|
+
body: {
|
|
17507
|
+
representation: "atlas_doc_format",
|
|
17508
|
+
value: args.adf
|
|
17509
|
+
},
|
|
17510
|
+
version: { number: nextVersion }
|
|
17511
|
+
})
|
|
17512
|
+
});
|
|
17513
|
+
if (!putResp.ok) {
|
|
17514
|
+
const body = await parseErrorBody(putResp);
|
|
17515
|
+
throw new Error(
|
|
17516
|
+
`PUT ${putUrl} failed with ${putResp.status} ${putResp.statusText}${body ? `: ${body}` : ""}`
|
|
17517
|
+
);
|
|
17518
|
+
}
|
|
17519
|
+
const updated = await putResp.json();
|
|
17520
|
+
return {
|
|
17521
|
+
id: updated.id,
|
|
17522
|
+
title: updated.title,
|
|
17523
|
+
version: updated.version.number,
|
|
17524
|
+
url: buildPageUrl(base, updated._links?.webui, updated.id),
|
|
17525
|
+
action: "updated"
|
|
17526
|
+
};
|
|
17527
|
+
}
|
|
17528
|
+
async function createPage(args, base, headers, fetchFn) {
|
|
17529
|
+
const body = {
|
|
17530
|
+
spaceId: args.spaceId,
|
|
17531
|
+
status: "current",
|
|
17532
|
+
title: args.title,
|
|
17533
|
+
body: {
|
|
17534
|
+
representation: "atlas_doc_format",
|
|
17535
|
+
value: args.adf
|
|
17536
|
+
}
|
|
17537
|
+
};
|
|
17538
|
+
if (args.parentId) body.parentId = args.parentId;
|
|
17539
|
+
const postUrl = `${base}/api/v2/pages`;
|
|
17540
|
+
const resp = await fetchFn(postUrl, {
|
|
17541
|
+
method: "POST",
|
|
17542
|
+
headers,
|
|
17543
|
+
body: JSON.stringify(body)
|
|
17544
|
+
});
|
|
17545
|
+
if (!resp.ok) {
|
|
17546
|
+
const errBody = await parseErrorBody(resp);
|
|
17547
|
+
throw new Error(
|
|
17548
|
+
`POST ${postUrl} failed with ${resp.status} ${resp.statusText}${errBody ? `: ${errBody}` : ""}`
|
|
17549
|
+
);
|
|
17550
|
+
}
|
|
17551
|
+
const created = await resp.json();
|
|
17552
|
+
return {
|
|
17553
|
+
id: created.id,
|
|
17554
|
+
title: created.title,
|
|
17555
|
+
version: created.version.number,
|
|
17556
|
+
url: buildPageUrl(base, created._links?.webui, created.id),
|
|
17557
|
+
action: "created"
|
|
17558
|
+
};
|
|
17559
|
+
}
|
|
17560
|
+
function buildPageUrl(base, webui, id) {
|
|
17561
|
+
if (webui) {
|
|
17562
|
+
return webui.startsWith("http") ? webui : `${base}${webui}`;
|
|
17563
|
+
}
|
|
17564
|
+
return `${base}/pages/${id}`;
|
|
17565
|
+
}
|
|
17566
|
+
|
|
17567
|
+
// src/publishers/jira.ts
|
|
17568
|
+
function parseAdf2(adf) {
|
|
17569
|
+
let parsed;
|
|
17570
|
+
try {
|
|
17571
|
+
parsed = JSON.parse(adf);
|
|
17572
|
+
} catch (err) {
|
|
17573
|
+
throw new Error(
|
|
17574
|
+
`ADF payload is not valid JSON: ${err.message}`
|
|
17575
|
+
);
|
|
17576
|
+
}
|
|
17577
|
+
if (!parsed || typeof parsed !== "object" || parsed.type !== "doc" || !Array.isArray(parsed.content)) {
|
|
17578
|
+
throw new Error(
|
|
17579
|
+
`ADF payload must be an object with { version, type: "doc", content: [...] }`
|
|
17580
|
+
);
|
|
17581
|
+
}
|
|
17582
|
+
return parsed;
|
|
17583
|
+
}
|
|
17584
|
+
function basicAuthHeader2(auth) {
|
|
17585
|
+
const raw = `${auth.email}:${auth.token}`;
|
|
17586
|
+
const encoded = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(raw);
|
|
17587
|
+
return `Basic ${encoded}`;
|
|
17588
|
+
}
|
|
17589
|
+
async function parseErrorBody2(resp) {
|
|
17590
|
+
try {
|
|
17591
|
+
const body = await resp.text();
|
|
17592
|
+
return body ? body.slice(0, 800) : "";
|
|
17593
|
+
} catch {
|
|
17594
|
+
return "";
|
|
17595
|
+
}
|
|
17596
|
+
}
|
|
17597
|
+
async function publishJiraIssue(args, deps) {
|
|
17598
|
+
const adfObject = parseAdf2(args.adf);
|
|
17599
|
+
if (!args.issueKey) {
|
|
17600
|
+
throw new Error("publishJiraIssue requires an issueKey, e.g. PROJ-123");
|
|
17601
|
+
}
|
|
17602
|
+
const base = args.baseUrl.replace(/\/$/, "");
|
|
17603
|
+
const fetchFn = deps.fetch ?? globalThis.fetch;
|
|
17604
|
+
if (!fetchFn) {
|
|
17605
|
+
throw new Error("No fetch implementation available (Node >= 22 expected)");
|
|
17606
|
+
}
|
|
17607
|
+
const headers = {
|
|
17608
|
+
Authorization: basicAuthHeader2(deps.auth),
|
|
17609
|
+
Accept: "application/json",
|
|
17610
|
+
"Content-Type": "application/json"
|
|
17611
|
+
};
|
|
17612
|
+
const mode = args.mode ?? "comment";
|
|
17613
|
+
if (mode === "description") {
|
|
17614
|
+
return updateDescription(args.issueKey, base, adfObject, headers, fetchFn);
|
|
17615
|
+
}
|
|
17616
|
+
return addComment(args.issueKey, base, adfObject, headers, fetchFn);
|
|
17617
|
+
}
|
|
17618
|
+
async function addComment(issueKey, base, adf, headers, fetchFn) {
|
|
17619
|
+
const url = `${base}/rest/api/3/issue/${encodeURIComponent(issueKey)}/comment`;
|
|
17620
|
+
const resp = await fetchFn(url, {
|
|
17621
|
+
method: "POST",
|
|
17622
|
+
headers,
|
|
17623
|
+
body: JSON.stringify({ body: adf })
|
|
17624
|
+
});
|
|
17625
|
+
if (!resp.ok) {
|
|
17626
|
+
const body = await parseErrorBody2(resp);
|
|
17627
|
+
throw new Error(
|
|
17628
|
+
`POST ${url} failed with ${resp.status} ${resp.statusText}${body ? `: ${body}` : ""}`
|
|
17629
|
+
);
|
|
17630
|
+
}
|
|
17631
|
+
const comment = await resp.json();
|
|
17632
|
+
const issueUrl = `${base}/browse/${encodeURIComponent(issueKey)}`;
|
|
17633
|
+
return {
|
|
17634
|
+
issueKey,
|
|
17635
|
+
action: "comment-added",
|
|
17636
|
+
url: `${issueUrl}?focusedCommentId=${encodeURIComponent(comment.id)}`,
|
|
17637
|
+
commentId: comment.id
|
|
17638
|
+
};
|
|
17639
|
+
}
|
|
17640
|
+
async function updateDescription(issueKey, base, adf, headers, fetchFn) {
|
|
17641
|
+
const url = `${base}/rest/api/3/issue/${encodeURIComponent(issueKey)}`;
|
|
17642
|
+
const resp = await fetchFn(url, {
|
|
17643
|
+
method: "PUT",
|
|
17644
|
+
headers,
|
|
17645
|
+
body: JSON.stringify({ fields: { description: adf } })
|
|
17646
|
+
});
|
|
17647
|
+
if (!resp.ok) {
|
|
17648
|
+
const body = await parseErrorBody2(resp);
|
|
17649
|
+
throw new Error(
|
|
17650
|
+
`PUT ${url} failed with ${resp.status} ${resp.statusText}${body ? `: ${body}` : ""}`
|
|
17651
|
+
);
|
|
17652
|
+
}
|
|
17653
|
+
return {
|
|
17654
|
+
issueKey,
|
|
17655
|
+
action: "description-updated",
|
|
17656
|
+
url: `${base}/browse/${encodeURIComponent(issueKey)}`
|
|
17657
|
+
};
|
|
17658
|
+
}
|
|
17659
|
+
|
|
17023
17660
|
// src/converters/ndjson-parser.ts
|
|
17024
17661
|
function parseNdjson(ndjson) {
|
|
17025
17662
|
const lines = ndjson.trim().split("\n").filter(Boolean);
|
|
@@ -17320,10 +17957,10 @@ function pickleStepArgumentToDocs(ps) {
|
|
|
17320
17957
|
const docs = [];
|
|
17321
17958
|
const phase = "static";
|
|
17322
17959
|
if (ps.argument.dataTable) {
|
|
17323
|
-
const
|
|
17324
|
-
if (
|
|
17325
|
-
const columns =
|
|
17326
|
-
const rows =
|
|
17960
|
+
const table2 = ps.argument.dataTable;
|
|
17961
|
+
if (table2.rows.length > 0) {
|
|
17962
|
+
const columns = table2.rows[0].cells.map((c) => c.value);
|
|
17963
|
+
const rows = table2.rows.slice(1).map((r) => r.cells.map((c) => c.value));
|
|
17327
17964
|
docs.push({
|
|
17328
17965
|
kind: "table",
|
|
17329
17966
|
label: "",
|
|
@@ -17386,16 +18023,16 @@ function pickleStepArgumentToDocs(ps) {
|
|
|
17386
18023
|
}
|
|
17387
18024
|
|
|
17388
18025
|
// src/notifiers/ansi-strip.ts
|
|
17389
|
-
function stripAnsi(
|
|
17390
|
-
return
|
|
18026
|
+
function stripAnsi(text2) {
|
|
18027
|
+
return text2.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, "");
|
|
17391
18028
|
}
|
|
17392
18029
|
|
|
17393
18030
|
// src/notifiers/slack.ts
|
|
17394
|
-
function truncate(
|
|
17395
|
-
if (
|
|
17396
|
-
return
|
|
18031
|
+
function truncate(text2, maxLen) {
|
|
18032
|
+
if (text2.length <= maxLen) return text2;
|
|
18033
|
+
return text2.slice(0, maxLen - 3) + "...";
|
|
17397
18034
|
}
|
|
17398
|
-
function
|
|
18035
|
+
function formatDuration3(ms) {
|
|
17399
18036
|
const seconds = ms / 1e3;
|
|
17400
18037
|
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
17401
18038
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -17422,7 +18059,7 @@ function buildSlackPayload(summary, maxFailedTests) {
|
|
|
17422
18059
|
{ type: "mrkdwn", text: `*Passed:* ${summary.passed}` },
|
|
17423
18060
|
{ type: "mrkdwn", text: `*Failed:* ${summary.failed}` },
|
|
17424
18061
|
{ type: "mrkdwn", text: `*Skipped:* ${summary.skipped}` },
|
|
17425
|
-
{ type: "mrkdwn", text: `*Duration:* ${
|
|
18062
|
+
{ type: "mrkdwn", text: `*Duration:* ${formatDuration3(summary.durationMs)}` },
|
|
17426
18063
|
{ type: "mrkdwn", text: `*Status:* ${statusText}` }
|
|
17427
18064
|
]
|
|
17428
18065
|
});
|
|
@@ -17437,9 +18074,9 @@ function buildSlackPayload(summary, maxFailedTests) {
|
|
|
17437
18074
|
}
|
|
17438
18075
|
return `*${name}*`;
|
|
17439
18076
|
});
|
|
17440
|
-
let
|
|
18077
|
+
let text2 = lines.join("\n\n");
|
|
17441
18078
|
if (summary.failedTests.length > maxFailedTests) {
|
|
17442
|
-
|
|
18079
|
+
text2 += `
|
|
17443
18080
|
|
|
17444
18081
|
_...and ${summary.failedTests.length - maxFailedTests} more_`;
|
|
17445
18082
|
}
|
|
@@ -17447,7 +18084,7 @@ _...and ${summary.failedTests.length - maxFailedTests} more_`;
|
|
|
17447
18084
|
type: "section",
|
|
17448
18085
|
text: {
|
|
17449
18086
|
type: "mrkdwn",
|
|
17450
|
-
text
|
|
18087
|
+
text: text2
|
|
17451
18088
|
}
|
|
17452
18089
|
});
|
|
17453
18090
|
}
|
|
@@ -17524,11 +18161,11 @@ async function sendSlackNotification(args, deps) {
|
|
|
17524
18161
|
}
|
|
17525
18162
|
|
|
17526
18163
|
// src/notifiers/teams.ts
|
|
17527
|
-
function truncate2(
|
|
17528
|
-
if (
|
|
17529
|
-
return
|
|
18164
|
+
function truncate2(text2, maxLen) {
|
|
18165
|
+
if (text2.length <= maxLen) return text2;
|
|
18166
|
+
return text2.slice(0, maxLen - 3) + "...";
|
|
17530
18167
|
}
|
|
17531
|
-
function
|
|
18168
|
+
function formatDuration4(ms) {
|
|
17532
18169
|
const seconds = ms / 1e3;
|
|
17533
18170
|
if (seconds < 60) return `${seconds.toFixed(1)}s`;
|
|
17534
18171
|
const minutes = Math.floor(seconds / 60);
|
|
@@ -17554,7 +18191,7 @@ function buildTeamsPayload(summary, maxFailedTests) {
|
|
|
17554
18191
|
{ title: "Passed", value: String(summary.passed) },
|
|
17555
18192
|
{ title: "Failed", value: String(summary.failed) },
|
|
17556
18193
|
{ title: "Skipped", value: String(summary.skipped) },
|
|
17557
|
-
{ title: "Duration", value:
|
|
18194
|
+
{ title: "Duration", value: formatDuration4(summary.durationMs) }
|
|
17558
18195
|
]
|
|
17559
18196
|
});
|
|
17560
18197
|
if (summary.failedTests.length > 0) {
|
|
@@ -18051,7 +18688,8 @@ var FORMAT_EXTENSIONS = {
|
|
|
18051
18688
|
"cucumber-html": ".cucumber.html",
|
|
18052
18689
|
junit: ".junit.xml",
|
|
18053
18690
|
"cucumber-json": ".cucumber.json",
|
|
18054
|
-
"cucumber-messages": ".ndjson"
|
|
18691
|
+
"cucumber-messages": ".ndjson",
|
|
18692
|
+
confluence: ".adf.json"
|
|
18055
18693
|
};
|
|
18056
18694
|
var TEST_EXTENSIONS = [
|
|
18057
18695
|
".test.ts",
|
|
@@ -18219,6 +18857,19 @@ var ReportGenerator = class {
|
|
|
18219
18857
|
includeSourceLinks: options.markdown?.includeSourceLinks ?? true,
|
|
18220
18858
|
customRenderers: options.markdown?.customRenderers
|
|
18221
18859
|
},
|
|
18860
|
+
confluence: {
|
|
18861
|
+
title: options.confluence?.title ?? "User Stories",
|
|
18862
|
+
includeStatusIcons: options.confluence?.includeStatusIcons ?? true,
|
|
18863
|
+
includeMetadata: options.confluence?.includeMetadata ?? true,
|
|
18864
|
+
includeSummaryTable: options.confluence?.includeSummaryTable ?? true,
|
|
18865
|
+
includeErrors: options.confluence?.includeErrors ?? true,
|
|
18866
|
+
scenarioHeadingLevel: options.confluence?.scenarioHeadingLevel ?? 3,
|
|
18867
|
+
groupBy: options.confluence?.groupBy ?? "file",
|
|
18868
|
+
sortScenarios: options.confluence?.sortScenarios ?? "source",
|
|
18869
|
+
pretty: options.confluence?.pretty ?? true,
|
|
18870
|
+
permalinkBaseUrl: options.confluence?.permalinkBaseUrl,
|
|
18871
|
+
ticketUrlTemplate: options.confluence?.ticketUrlTemplate
|
|
18872
|
+
},
|
|
18222
18873
|
astro: {
|
|
18223
18874
|
assetsDir: options.astro?.assetsDir ?? "public/stories/assets",
|
|
18224
18875
|
assetsBaseUrl: options.astro?.assetsBaseUrl ?? "/stories/assets",
|
|
@@ -18394,6 +19045,22 @@ var ReportGenerator = class {
|
|
|
18394
19045
|
});
|
|
18395
19046
|
return formatter.format(run);
|
|
18396
19047
|
}
|
|
19048
|
+
case "confluence": {
|
|
19049
|
+
const formatter = new ConfluenceFormatter({
|
|
19050
|
+
title: this.options.confluence.title,
|
|
19051
|
+
includeStatusIcons: this.options.confluence.includeStatusIcons,
|
|
19052
|
+
includeMetadata: this.options.confluence.includeMetadata,
|
|
19053
|
+
includeSummaryTable: this.options.confluence.includeSummaryTable,
|
|
19054
|
+
includeErrors: this.options.confluence.includeErrors,
|
|
19055
|
+
scenarioHeadingLevel: this.options.confluence.scenarioHeadingLevel,
|
|
19056
|
+
groupBy: this.options.confluence.groupBy,
|
|
19057
|
+
sortScenarios: this.options.confluence.sortScenarios,
|
|
19058
|
+
pretty: this.options.confluence.pretty,
|
|
19059
|
+
permalinkBaseUrl: this.options.confluence.permalinkBaseUrl,
|
|
19060
|
+
ticketUrlTemplate: this.options.confluence.ticketUrlTemplate
|
|
19061
|
+
});
|
|
19062
|
+
return formatter.format(run);
|
|
19063
|
+
}
|
|
18397
19064
|
case "markdown": {
|
|
18398
19065
|
const formatter = new MarkdownFormatter({
|
|
18399
19066
|
title: this.options.markdown.title,
|
|
@@ -18508,17 +19175,22 @@ USAGE
|
|
|
18508
19175
|
executable-stories validate <file>
|
|
18509
19176
|
executable-stories validate --stdin
|
|
18510
19177
|
executable-stories init-astro [directory]
|
|
19178
|
+
executable-stories publish-confluence <file.adf.json> [options]
|
|
19179
|
+
executable-stories publish-jira <file.adf.json> [options]
|
|
18511
19180
|
|
|
18512
19181
|
SUBCOMMANDS
|
|
18513
|
-
format
|
|
18514
|
-
compare
|
|
18515
|
-
list
|
|
18516
|
-
validate
|
|
18517
|
-
init-astro
|
|
19182
|
+
format Read raw test results and generate reports
|
|
19183
|
+
compare Compare two runs and generate a diff report
|
|
19184
|
+
list List scenarios from a test run (text table or JSON)
|
|
19185
|
+
validate Validate a JSON file against the schema (no output generated)
|
|
19186
|
+
init-astro Scaffold an Astro docs site for story output (Starlight with themed CSS)
|
|
19187
|
+
publish-confluence Publish an ADF JSON file to a Confluence page via REST API
|
|
19188
|
+
publish-jira Publish an ADF JSON file to a Jira issue (as comment or description)
|
|
18518
19189
|
|
|
18519
19190
|
OPTIONS
|
|
18520
|
-
--format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, or custom names from config (default: html)
|
|
18521
|
-
astro
|
|
19191
|
+
--format <formats> Comma-separated formats: html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html, astro, confluence, or custom names from config (default: html)
|
|
19192
|
+
astro Themed Markdown (for Astro docs sites with matching CSS)
|
|
19193
|
+
confluence Atlassian Document Format (ADF) JSON for Confluence / Jira
|
|
18522
19194
|
html Custom HTML report (accessible, dark mode, mermaid)
|
|
18523
19195
|
cucumber-html Official Cucumber HTML report
|
|
18524
19196
|
markdown Markdown documentation
|
|
@@ -18572,6 +19244,26 @@ INIT-ASTRO
|
|
|
18572
19244
|
executable-stories init-astro [directory] Scaffold into directory (default: ./story-docs)
|
|
18573
19245
|
--force Overwrite existing directory
|
|
18574
19246
|
|
|
19247
|
+
PUBLISH-CONFLUENCE
|
|
19248
|
+
executable-stories publish-confluence <file.adf.json> [options]
|
|
19249
|
+
--page-id <id> Update an existing page (alternative to --space-id)
|
|
19250
|
+
--space-id <id> Create a new page (requires --title)
|
|
19251
|
+
--parent-id <id> Parent page ID (for new pages)
|
|
19252
|
+
--title <title> Page title
|
|
19253
|
+
--base-url <url> Confluence base URL (env: CONFLUENCE_BASE_URL)
|
|
19254
|
+
--email <email> Atlassian email (env: CONFLUENCE_EMAIL)
|
|
19255
|
+
--token <token> API token (env: CONFLUENCE_TOKEN)
|
|
19256
|
+
--dry-run Validate and print request plan, don't POST
|
|
19257
|
+
|
|
19258
|
+
PUBLISH-JIRA
|
|
19259
|
+
executable-stories publish-jira <file.adf.json> [options]
|
|
19260
|
+
--issue <KEY> Issue key, e.g. PROJ-123 (required)
|
|
19261
|
+
--mode <mode> "comment" (default) or "description"
|
|
19262
|
+
--base-url <url> Jira base URL (env: JIRA_BASE_URL)
|
|
19263
|
+
--email <email> Atlassian email (env: JIRA_EMAIL)
|
|
19264
|
+
--token <token> API token (env: JIRA_TOKEN)
|
|
19265
|
+
--dry-run Validate and print request plan, don't POST
|
|
19266
|
+
|
|
18575
19267
|
NOTIFICATIONS
|
|
18576
19268
|
--slack-webhook <url> Slack incoming webhook URL (fallback: SLACK_WEBHOOK_URL env var)
|
|
18577
19269
|
--teams-webhook <url> Teams incoming webhook URL (fallback: TEAMS_WEBHOOK_URL env var)
|
|
@@ -18606,17 +19298,38 @@ async function parseCliArgs(argv) {
|
|
|
18606
19298
|
process.exit(EXIT_SUCCESS);
|
|
18607
19299
|
}
|
|
18608
19300
|
const subcommand = args[0];
|
|
18609
|
-
if (subcommand !== "format" && subcommand !== "compare" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro") {
|
|
18610
|
-
console.error(
|
|
19301
|
+
if (subcommand !== "format" && subcommand !== "compare" && subcommand !== "list" && subcommand !== "validate" && subcommand !== "init-astro" && subcommand !== "publish-confluence" && subcommand !== "publish-jira") {
|
|
19302
|
+
console.error(
|
|
19303
|
+
`Unknown subcommand: "${subcommand}". Use "format", "compare", "list", "validate", "init-astro", "publish-confluence", or "publish-jira".`
|
|
19304
|
+
);
|
|
18611
19305
|
process.exit(EXIT_USAGE);
|
|
18612
19306
|
}
|
|
19307
|
+
if (subcommand === "publish-confluence") {
|
|
19308
|
+
await runPublishConfluence(args.slice(1));
|
|
19309
|
+
process.exit(EXIT_SUCCESS);
|
|
19310
|
+
}
|
|
19311
|
+
if (subcommand === "publish-jira") {
|
|
19312
|
+
await runPublishJira(args.slice(1));
|
|
19313
|
+
process.exit(EXIT_SUCCESS);
|
|
19314
|
+
}
|
|
18613
19315
|
if (subcommand === "init-astro") {
|
|
18614
19316
|
const initArgs = args.slice(1);
|
|
18615
19317
|
const targetDir = initArgs.find((a) => !a.startsWith("--")) ?? "./story-docs";
|
|
18616
19318
|
const force = initArgs.includes("--force");
|
|
18617
19319
|
try {
|
|
18618
19320
|
const result = initAstro({ targetDir, force });
|
|
18619
|
-
console.log(`Scaffolded Astro
|
|
19321
|
+
console.log(`Scaffolded Astro docs site at ${result.targetDir}`);
|
|
19322
|
+
console.log("");
|
|
19323
|
+
console.log("Themes available in src/styles/themes/:");
|
|
19324
|
+
console.log(" default.css IBM Plex Sans, cucumber green (default)");
|
|
19325
|
+
console.log(" corporate.css DM Sans, navy accent");
|
|
19326
|
+
console.log(" terminal.css JetBrains Mono, green-on-dark");
|
|
19327
|
+
console.log(" minimal.css DM Sans, warm teal");
|
|
19328
|
+
console.log(" dashboard.css DM Sans, blue accent");
|
|
19329
|
+
console.log(" playful.css Source Sans, coral pastels");
|
|
19330
|
+
console.log("");
|
|
19331
|
+
console.log("To change theme, edit astro.config.mjs customCss array.");
|
|
19332
|
+
console.log("");
|
|
18620
19333
|
console.log("");
|
|
18621
19334
|
console.log("Next steps:");
|
|
18622
19335
|
console.log(` cd ${result.targetDir}`);
|
|
@@ -18718,7 +19431,7 @@ async function parseCliArgs(argv) {
|
|
|
18718
19431
|
}
|
|
18719
19432
|
const pluginConfig = await loadConfig(values["config"]);
|
|
18720
19433
|
const customFormatterNames = new Set(Object.keys(pluginConfig.formatters ?? {}));
|
|
18721
|
-
const builtInFormats = /* @__PURE__ */ new Set(["astro", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
|
|
19434
|
+
const builtInFormats = /* @__PURE__ */ new Set(["astro", "confluence", "html", "markdown", "junit", "cucumber-json", "cucumber-messages", "cucumber-html"]);
|
|
18722
19435
|
const formatStr = values.format;
|
|
18723
19436
|
const allRequestedFormats = formatStr.split(",").map((f) => f.trim());
|
|
18724
19437
|
const builtInRequested = allRequestedFormats.filter((f) => builtInFormats.has(f));
|
|
@@ -18726,7 +19439,7 @@ async function parseCliArgs(argv) {
|
|
|
18726
19439
|
const unknownFormats = allRequestedFormats.filter((f) => !builtInFormats.has(f) && !customFormatterNames.has(f));
|
|
18727
19440
|
if (unknownFormats.length > 0) {
|
|
18728
19441
|
const knownCustom = customFormatterNames.size > 0 ? `, ${[...customFormatterNames].join(", ")}` : "";
|
|
18729
|
-
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html${knownCustom}.`);
|
|
19442
|
+
console.error(`Error: Unknown format(s): ${unknownFormats.join(", ")}. Valid built-in: astro, confluence, html, markdown, junit, cucumber-json, cucumber-messages, cucumber-html${knownCustom}.`);
|
|
18730
19443
|
process.exit(EXIT_USAGE);
|
|
18731
19444
|
}
|
|
18732
19445
|
const formats = builtInRequested;
|
|
@@ -18877,18 +19590,18 @@ function readStdin() {
|
|
|
18877
19590
|
process.stdin.on("error", reject);
|
|
18878
19591
|
});
|
|
18879
19592
|
}
|
|
18880
|
-
function parseJson(
|
|
19593
|
+
function parseJson(text2) {
|
|
18881
19594
|
try {
|
|
18882
|
-
return JSON.parse(
|
|
19595
|
+
return JSON.parse(text2);
|
|
18883
19596
|
} catch (err) {
|
|
18884
19597
|
const msg = err instanceof Error ? err.message : String(err);
|
|
18885
19598
|
console.error(`Error: Invalid JSON \u2014 ${msg}`);
|
|
18886
19599
|
process.exit(EXIT_USAGE);
|
|
18887
19600
|
}
|
|
18888
19601
|
}
|
|
18889
|
-
function tryParseJson(
|
|
19602
|
+
function tryParseJson(text2) {
|
|
18890
19603
|
try {
|
|
18891
|
-
return JSON.parse(
|
|
19604
|
+
return JSON.parse(text2);
|
|
18892
19605
|
} catch {
|
|
18893
19606
|
return void 0;
|
|
18894
19607
|
}
|
|
@@ -18938,17 +19651,17 @@ ${msg}`);
|
|
|
18938
19651
|
}
|
|
18939
19652
|
return { run: canonical, droppedMissingStory };
|
|
18940
19653
|
}
|
|
18941
|
-
function normalizeRunFromText(
|
|
19654
|
+
function normalizeRunFromText(text2, args) {
|
|
18942
19655
|
if (args.inputType === "ndjson") {
|
|
18943
19656
|
try {
|
|
18944
|
-
return { run: parseNdjson(
|
|
19657
|
+
return { run: parseNdjson(text2), droppedMissingStory: 0 };
|
|
18945
19658
|
} catch (err) {
|
|
18946
19659
|
const msg = err instanceof Error ? err.message : String(err);
|
|
18947
19660
|
console.error(`NDJSON parse failed: ${msg}`);
|
|
18948
19661
|
process.exit(EXIT_SCHEMA_VALIDATION);
|
|
18949
19662
|
}
|
|
18950
19663
|
}
|
|
18951
|
-
return normalizeRunFromJsonData(parseJson(
|
|
19664
|
+
return normalizeRunFromJsonData(parseJson(text2), args);
|
|
18952
19665
|
}
|
|
18953
19666
|
function applySelection(run, args) {
|
|
18954
19667
|
const testCases = selectTestCases(
|
|
@@ -18964,15 +19677,15 @@ function applySelection(run, args) {
|
|
|
18964
19677
|
);
|
|
18965
19678
|
return { ...run, testCases };
|
|
18966
19679
|
}
|
|
18967
|
-
function tryNormalizeRunFromText(
|
|
19680
|
+
function tryNormalizeRunFromText(text2, args) {
|
|
18968
19681
|
if (args.inputType === "ndjson") {
|
|
18969
19682
|
try {
|
|
18970
|
-
return parseNdjson(
|
|
19683
|
+
return parseNdjson(text2);
|
|
18971
19684
|
} catch {
|
|
18972
19685
|
return void 0;
|
|
18973
19686
|
}
|
|
18974
19687
|
}
|
|
18975
|
-
const data = tryParseJson(
|
|
19688
|
+
const data = tryParseJson(text2);
|
|
18976
19689
|
if (data === void 0) return void 0;
|
|
18977
19690
|
if (args.inputType === "canonical") {
|
|
18978
19691
|
try {
|
|
@@ -19052,8 +19765,8 @@ async function main() {
|
|
|
19052
19765
|
}
|
|
19053
19766
|
}
|
|
19054
19767
|
if (args.subcommand === "list") {
|
|
19055
|
-
const
|
|
19056
|
-
const run = applySelection(normalizeRunFromText(
|
|
19768
|
+
const text3 = await readInput(args);
|
|
19769
|
+
const run = applySelection(normalizeRunFromText(text3, args).run, args);
|
|
19057
19770
|
const resolvedFormat = args.jsonSummary ? "json" : args.listFormat;
|
|
19058
19771
|
const validListFormats = /* @__PURE__ */ new Set(["text", "json", "csv", "markdown-table"]);
|
|
19059
19772
|
if (!validListFormats.has(resolvedFormat)) {
|
|
@@ -19067,10 +19780,10 @@ async function main() {
|
|
|
19067
19780
|
console.log(output);
|
|
19068
19781
|
process.exit(EXIT_SUCCESS);
|
|
19069
19782
|
}
|
|
19070
|
-
const
|
|
19783
|
+
const text2 = await readInput(args);
|
|
19071
19784
|
if (args.inputType === "ndjson") {
|
|
19072
19785
|
if (args.subcommand === "validate") {
|
|
19073
|
-
const lines =
|
|
19786
|
+
const lines = text2.trim().split("\n").filter(Boolean);
|
|
19074
19787
|
const validKeys = /* @__PURE__ */ new Set([
|
|
19075
19788
|
"meta",
|
|
19076
19789
|
"source",
|
|
@@ -19103,7 +19816,7 @@ async function main() {
|
|
|
19103
19816
|
}
|
|
19104
19817
|
let run;
|
|
19105
19818
|
try {
|
|
19106
|
-
run = parseNdjson(
|
|
19819
|
+
run = parseNdjson(text2);
|
|
19107
19820
|
} catch (err) {
|
|
19108
19821
|
const msg = err instanceof Error ? err.message : String(err);
|
|
19109
19822
|
console.error(`NDJSON parse failed: ${msg}`);
|
|
@@ -19127,7 +19840,7 @@ async function main() {
|
|
|
19127
19840
|
process.exit(EXIT_GENERATION);
|
|
19128
19841
|
}
|
|
19129
19842
|
}
|
|
19130
|
-
const data = parseJson(
|
|
19843
|
+
const data = parseJson(text2);
|
|
19131
19844
|
if (args.subcommand === "validate") {
|
|
19132
19845
|
if (args.inputType === "canonical") {
|
|
19133
19846
|
try {
|
|
@@ -19456,6 +20169,225 @@ function printCompareResult(result, args, startMs) {
|
|
|
19456
20169
|
console.log(result.prSummary);
|
|
19457
20170
|
}
|
|
19458
20171
|
}
|
|
20172
|
+
async function runPublishConfluence(rawArgs) {
|
|
20173
|
+
const { values, positionals } = parseArgs({
|
|
20174
|
+
args: rawArgs,
|
|
20175
|
+
options: {
|
|
20176
|
+
"page-id": { type: "string" },
|
|
20177
|
+
"space-id": { type: "string" },
|
|
20178
|
+
"parent-id": { type: "string" },
|
|
20179
|
+
title: { type: "string" },
|
|
20180
|
+
"base-url": { type: "string" },
|
|
20181
|
+
email: { type: "string" },
|
|
20182
|
+
token: { type: "string" },
|
|
20183
|
+
"dry-run": { type: "boolean", default: false },
|
|
20184
|
+
help: { type: "boolean", default: false }
|
|
20185
|
+
},
|
|
20186
|
+
allowPositionals: true,
|
|
20187
|
+
strict: true
|
|
20188
|
+
});
|
|
20189
|
+
if (values.help) {
|
|
20190
|
+
console.log(`Usage: executable-stories publish-confluence <file.adf.json> [options]
|
|
20191
|
+
|
|
20192
|
+
Publishes an ADF JSON document to a Confluence Cloud page.
|
|
20193
|
+
|
|
20194
|
+
Required (one of):
|
|
20195
|
+
--page-id <id> Update an existing page in place
|
|
20196
|
+
--space-id <id> Create a new page in a space (also requires --title)
|
|
20197
|
+
|
|
20198
|
+
Optional:
|
|
20199
|
+
--parent-id <id> Parent page ID (new pages only)
|
|
20200
|
+
--title <title> Page title (required for create; overrides current title on update)
|
|
20201
|
+
--base-url <url> Confluence base URL, e.g. https://acme.atlassian.net/wiki
|
|
20202
|
+
(env: CONFLUENCE_BASE_URL)
|
|
20203
|
+
--email <email> Atlassian account email (env: CONFLUENCE_EMAIL)
|
|
20204
|
+
--token <token> Atlassian API token (env: CONFLUENCE_TOKEN)
|
|
20205
|
+
--dry-run Validate inputs and print the request plan, don't POST
|
|
20206
|
+
--help Show this help
|
|
20207
|
+
|
|
20208
|
+
Generate an API token at https://id.atlassian.com/manage-profile/security/api-tokens`);
|
|
20209
|
+
process.exit(EXIT_SUCCESS);
|
|
20210
|
+
}
|
|
20211
|
+
const inputFile = positionals[0];
|
|
20212
|
+
if (!inputFile) {
|
|
20213
|
+
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
20214
|
+
process.exit(EXIT_USAGE);
|
|
20215
|
+
}
|
|
20216
|
+
if (!fs7.existsSync(inputFile)) {
|
|
20217
|
+
console.error(`Error: file not found: ${inputFile}`);
|
|
20218
|
+
process.exit(EXIT_USAGE);
|
|
20219
|
+
}
|
|
20220
|
+
const baseUrl = values["base-url"] ?? process.env.CONFLUENCE_BASE_URL;
|
|
20221
|
+
const email = values.email ?? process.env.CONFLUENCE_EMAIL;
|
|
20222
|
+
const token = values.token ?? process.env.CONFLUENCE_TOKEN;
|
|
20223
|
+
const pageId = values["page-id"];
|
|
20224
|
+
const spaceId = values["space-id"];
|
|
20225
|
+
const parentId = values["parent-id"];
|
|
20226
|
+
const title = values.title;
|
|
20227
|
+
const dryRun = values["dry-run"];
|
|
20228
|
+
if (!baseUrl) {
|
|
20229
|
+
console.error(
|
|
20230
|
+
"Error: --base-url or CONFLUENCE_BASE_URL is required (e.g. https://acme.atlassian.net/wiki)"
|
|
20231
|
+
);
|
|
20232
|
+
process.exit(EXIT_USAGE);
|
|
20233
|
+
}
|
|
20234
|
+
if (!pageId && !spaceId) {
|
|
20235
|
+
console.error(
|
|
20236
|
+
"Error: specify either --page-id (to update) or --space-id (to create)"
|
|
20237
|
+
);
|
|
20238
|
+
process.exit(EXIT_USAGE);
|
|
20239
|
+
}
|
|
20240
|
+
if (!pageId && !title) {
|
|
20241
|
+
console.error("Error: --title is required when creating a new page");
|
|
20242
|
+
process.exit(EXIT_USAGE);
|
|
20243
|
+
}
|
|
20244
|
+
const adf = fs7.readFileSync(path7.resolve(inputFile), "utf8");
|
|
20245
|
+
if (dryRun) {
|
|
20246
|
+
console.log(
|
|
20247
|
+
JSON.stringify(
|
|
20248
|
+
{
|
|
20249
|
+
action: pageId ? "update" : "create",
|
|
20250
|
+
baseUrl,
|
|
20251
|
+
pageId,
|
|
20252
|
+
spaceId,
|
|
20253
|
+
parentId,
|
|
20254
|
+
title,
|
|
20255
|
+
adfBytes: adf.length
|
|
20256
|
+
},
|
|
20257
|
+
null,
|
|
20258
|
+
2
|
|
20259
|
+
)
|
|
20260
|
+
);
|
|
20261
|
+
process.exit(EXIT_SUCCESS);
|
|
20262
|
+
}
|
|
20263
|
+
if (!email || !token) {
|
|
20264
|
+
console.error(
|
|
20265
|
+
"Error: --email/CONFLUENCE_EMAIL and --token/CONFLUENCE_TOKEN are required unless --dry-run is set"
|
|
20266
|
+
);
|
|
20267
|
+
process.exit(EXIT_USAGE);
|
|
20268
|
+
}
|
|
20269
|
+
try {
|
|
20270
|
+
const result = await publishConfluencePage(
|
|
20271
|
+
{ adf, pageId, spaceId, parentId, title, baseUrl },
|
|
20272
|
+
{ auth: { email, token } }
|
|
20273
|
+
);
|
|
20274
|
+
console.log(
|
|
20275
|
+
`${result.action === "created" ? "Created" : "Updated"} "${result.title}" (v${result.version}) \u2192 ${result.url}`
|
|
20276
|
+
);
|
|
20277
|
+
process.exit(EXIT_SUCCESS);
|
|
20278
|
+
} catch (err) {
|
|
20279
|
+
console.error(`Error: ${err.message}`);
|
|
20280
|
+
process.exit(EXIT_GENERATION);
|
|
20281
|
+
}
|
|
20282
|
+
}
|
|
20283
|
+
async function runPublishJira(rawArgs) {
|
|
20284
|
+
const { values, positionals } = parseArgs({
|
|
20285
|
+
args: rawArgs,
|
|
20286
|
+
options: {
|
|
20287
|
+
issue: { type: "string" },
|
|
20288
|
+
mode: { type: "string", default: "comment" },
|
|
20289
|
+
"base-url": { type: "string" },
|
|
20290
|
+
email: { type: "string" },
|
|
20291
|
+
token: { type: "string" },
|
|
20292
|
+
"dry-run": { type: "boolean", default: false },
|
|
20293
|
+
help: { type: "boolean", default: false }
|
|
20294
|
+
},
|
|
20295
|
+
allowPositionals: true,
|
|
20296
|
+
strict: true
|
|
20297
|
+
});
|
|
20298
|
+
if (values.help) {
|
|
20299
|
+
console.log(`Usage: executable-stories publish-jira <file.adf.json> [options]
|
|
20300
|
+
|
|
20301
|
+
Publishes an ADF JSON document to a Jira Cloud issue.
|
|
20302
|
+
|
|
20303
|
+
Required:
|
|
20304
|
+
--issue <KEY> Issue key, e.g. PROJ-123
|
|
20305
|
+
|
|
20306
|
+
Optional:
|
|
20307
|
+
--mode <mode> "comment" (default, append-only) or "description" (replaces field)
|
|
20308
|
+
--base-url <url> Jira base URL, e.g. https://acme.atlassian.net
|
|
20309
|
+
(env: JIRA_BASE_URL)
|
|
20310
|
+
--email <email> Atlassian account email (env: JIRA_EMAIL)
|
|
20311
|
+
--token <token> Atlassian API token (env: JIRA_TOKEN)
|
|
20312
|
+
--dry-run Validate inputs and print the request plan, don't POST
|
|
20313
|
+
--help Show this help
|
|
20314
|
+
|
|
20315
|
+
Generate an API token at https://id.atlassian.com/manage-profile/security/api-tokens`);
|
|
20316
|
+
process.exit(EXIT_SUCCESS);
|
|
20317
|
+
}
|
|
20318
|
+
const inputFile = positionals[0];
|
|
20319
|
+
if (!inputFile) {
|
|
20320
|
+
console.error("Error: missing ADF file argument. Run with --help for usage.");
|
|
20321
|
+
process.exit(EXIT_USAGE);
|
|
20322
|
+
}
|
|
20323
|
+
if (!fs7.existsSync(inputFile)) {
|
|
20324
|
+
console.error(`Error: file not found: ${inputFile}`);
|
|
20325
|
+
process.exit(EXIT_USAGE);
|
|
20326
|
+
}
|
|
20327
|
+
const baseUrl = values["base-url"] ?? process.env.JIRA_BASE_URL;
|
|
20328
|
+
const email = values.email ?? process.env.JIRA_EMAIL;
|
|
20329
|
+
const token = values.token ?? process.env.JIRA_TOKEN;
|
|
20330
|
+
const issueKey = values.issue;
|
|
20331
|
+
const modeRaw = values.mode;
|
|
20332
|
+
const dryRun = values["dry-run"];
|
|
20333
|
+
if (!baseUrl) {
|
|
20334
|
+
console.error(
|
|
20335
|
+
"Error: --base-url or JIRA_BASE_URL is required (e.g. https://acme.atlassian.net)"
|
|
20336
|
+
);
|
|
20337
|
+
process.exit(EXIT_USAGE);
|
|
20338
|
+
}
|
|
20339
|
+
if (!issueKey) {
|
|
20340
|
+
console.error("Error: --issue <KEY> is required (e.g. --issue PROJ-123)");
|
|
20341
|
+
process.exit(EXIT_USAGE);
|
|
20342
|
+
}
|
|
20343
|
+
if (modeRaw !== "comment" && modeRaw !== "description") {
|
|
20344
|
+
console.error(
|
|
20345
|
+
`Error: --mode must be "comment" or "description" (got "${modeRaw}")`
|
|
20346
|
+
);
|
|
20347
|
+
process.exit(EXIT_USAGE);
|
|
20348
|
+
}
|
|
20349
|
+
const mode = modeRaw;
|
|
20350
|
+
const adf = fs7.readFileSync(path7.resolve(inputFile), "utf8");
|
|
20351
|
+
if (dryRun) {
|
|
20352
|
+
console.log(
|
|
20353
|
+
JSON.stringify(
|
|
20354
|
+
{
|
|
20355
|
+
action: mode === "description" ? "description-updated" : "comment-added",
|
|
20356
|
+
baseUrl,
|
|
20357
|
+
issueKey,
|
|
20358
|
+
mode,
|
|
20359
|
+
adfBytes: adf.length
|
|
20360
|
+
},
|
|
20361
|
+
null,
|
|
20362
|
+
2
|
|
20363
|
+
)
|
|
20364
|
+
);
|
|
20365
|
+
process.exit(EXIT_SUCCESS);
|
|
20366
|
+
}
|
|
20367
|
+
if (!email || !token) {
|
|
20368
|
+
console.error(
|
|
20369
|
+
"Error: --email/JIRA_EMAIL and --token/JIRA_TOKEN are required unless --dry-run is set"
|
|
20370
|
+
);
|
|
20371
|
+
process.exit(EXIT_USAGE);
|
|
20372
|
+
}
|
|
20373
|
+
try {
|
|
20374
|
+
const result = await publishJiraIssue(
|
|
20375
|
+
{ adf, issueKey, baseUrl, mode },
|
|
20376
|
+
{ auth: { email, token } }
|
|
20377
|
+
);
|
|
20378
|
+
if (result.action === "comment-added") {
|
|
20379
|
+
console.log(
|
|
20380
|
+
`Added comment to ${result.issueKey} (comment ${result.commentId}) \u2192 ${result.url}`
|
|
20381
|
+
);
|
|
20382
|
+
} else {
|
|
20383
|
+
console.log(`Updated description for ${result.issueKey} \u2192 ${result.url}`);
|
|
20384
|
+
}
|
|
20385
|
+
process.exit(EXIT_SUCCESS);
|
|
20386
|
+
} catch (err) {
|
|
20387
|
+
console.error(`Error: ${err.message}`);
|
|
20388
|
+
process.exit(EXIT_GENERATION);
|
|
20389
|
+
}
|
|
20390
|
+
}
|
|
19459
20391
|
main().catch((err) => {
|
|
19460
20392
|
console.error(err);
|
|
19461
20393
|
process.exit(EXIT_USAGE);
|