executable-stories-formatters 1.10.0 → 1.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +157 -18
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -3483,9 +3483,9 @@ function buildDataTable(table2, line) {
|
|
|
3483
3483
|
const rowLine = line + 1 + r;
|
|
3484
3484
|
rows.push({
|
|
3485
3485
|
location: { line: rowLine },
|
|
3486
|
-
cells: table2.rows[r].map((
|
|
3486
|
+
cells: table2.rows[r].map((cell2) => ({
|
|
3487
3487
|
location: { line: rowLine },
|
|
3488
|
-
value:
|
|
3488
|
+
value: cell2
|
|
3489
3489
|
})),
|
|
3490
3490
|
id: ""
|
|
3491
3491
|
});
|
|
@@ -3586,7 +3586,7 @@ function buildPickleTable(table2) {
|
|
|
3586
3586
|
});
|
|
3587
3587
|
for (const row of table2.rows) {
|
|
3588
3588
|
rows.push({
|
|
3589
|
-
cells: row.map((
|
|
3589
|
+
cells: row.map((cell2) => ({ value: cell2 }))
|
|
3590
3590
|
});
|
|
3591
3591
|
}
|
|
3592
3592
|
return { rows };
|
|
@@ -4685,7 +4685,7 @@ function formatDocEntry(doc) {
|
|
|
4685
4685
|
return `${escapeHtml2(doc.label)}${doc.lang ? ` (${escapeHtml2(doc.lang)})` : ""}: <code>${escapeHtml2(doc.content)}</code>`;
|
|
4686
4686
|
case "table": {
|
|
4687
4687
|
const header = `<tr>${doc.columns.map((c) => `<th>${escapeHtml2(c)}</th>`).join("")}</tr>`;
|
|
4688
|
-
const rows = doc.rows.map((row) => `<tr>${row.map((
|
|
4688
|
+
const rows = doc.rows.map((row) => `<tr>${row.map((cell2) => `<td>${escapeHtml2(cell2)}</td>`).join("")}</tr>`).join("");
|
|
4689
4689
|
return `${escapeHtml2(doc.label)}<table>${header}${rows}</table>`;
|
|
4690
4690
|
}
|
|
4691
4691
|
case "link":
|
|
@@ -5722,7 +5722,7 @@ ${tc.errorStack}` : "");
|
|
|
5722
5722
|
table([
|
|
5723
5723
|
tableRow(entry.columns.map((c) => tableHeader(c))),
|
|
5724
5724
|
...entry.rows.map(
|
|
5725
|
-
(row) => tableRow(row.map((
|
|
5725
|
+
(row) => tableRow(row.map((cell2) => tableCell(cell2)))
|
|
5726
5726
|
)
|
|
5727
5727
|
])
|
|
5728
5728
|
);
|
|
@@ -10557,7 +10557,7 @@ var COMPLETION_SUBCOMMANDS = [
|
|
|
10557
10557
|
["init-astro", "Scaffold a thin Astro docs site"],
|
|
10558
10558
|
["new", "Scaffold a docs page from a template"],
|
|
10559
10559
|
["check-links", "Scan docs for broken links"],
|
|
10560
|
-
["push", "Send a run to
|
|
10560
|
+
["push", "Send a run to a cloud ingest endpoint"],
|
|
10561
10561
|
["coverage", "Compare stories against a test-management system (read-only)"],
|
|
10562
10562
|
["sync", "Push cases, executions, and evidence to TestRail or Xray"],
|
|
10563
10563
|
["import-openapi", "Generate API doc pages from an OpenAPI spec"],
|
|
@@ -11297,10 +11297,11 @@ import { synthesizeStories as synthesizeStories2 } from "executable-stories-core
|
|
|
11297
11297
|
var EXIT_SUCCESS = 0;
|
|
11298
11298
|
var EXIT_PUSH_FAILED = 1;
|
|
11299
11299
|
var EXIT_USAGE = 4;
|
|
11300
|
+
var EXIT_GATE_BLOCKED = 5;
|
|
11300
11301
|
var HELP = `Usage:
|
|
11301
11302
|
executable-stories push <run.json> [options]
|
|
11302
11303
|
|
|
11303
|
-
Send a run to
|
|
11304
|
+
Send a run to a cloud ingest endpoint. <run.json> is either a StoryReport v1
|
|
11304
11305
|
(e.g. reports/index.story-report.json) or a raw run JSON, which is converted
|
|
11305
11306
|
through the standard pipeline first.
|
|
11306
11307
|
|
|
@@ -11314,12 +11315,20 @@ Options:
|
|
|
11314
11315
|
--git-sha <sha> Default: current git HEAD.
|
|
11315
11316
|
--base <ref> Send files changed since <ref> (e.g. origin/main) so the
|
|
11316
11317
|
cloud can recommend a test scope for the change.
|
|
11318
|
+
--gate After pushing, ask the cloud whether this commit is safe
|
|
11319
|
+
to release and exit 5 if it is blocked. The policy lives
|
|
11320
|
+
in your organization's settings, not in a file here.
|
|
11317
11321
|
-h, --help Show this help.
|
|
11318
11322
|
|
|
11319
|
-
|
|
11323
|
+
Under GitHub Actions, repo/branch/sha, the base commit, and PR metadata are
|
|
11324
|
+
read from the environment, the run URL and recommended scope are written to
|
|
11325
|
+
the job summary, and the run id is written to GITHUB_OUTPUT as ingest-run-id.
|
|
11326
|
+
|
|
11327
|
+
Exit codes: 0 pushed, 1 push rejected/failed, 4 usage error, 5 gate blocked.`;
|
|
11320
11328
|
function defaultDeps() {
|
|
11321
11329
|
return {
|
|
11322
11330
|
readFile: (filePath) => fs14.readFileSync(filePath, "utf8"),
|
|
11331
|
+
appendFile: (filePath, text2) => fs14.appendFileSync(filePath, text2),
|
|
11323
11332
|
fetchFn: fetch,
|
|
11324
11333
|
git: (args) => {
|
|
11325
11334
|
try {
|
|
@@ -11340,6 +11349,42 @@ function repoSlugFromRemote(remoteUrl) {
|
|
|
11340
11349
|
function isStoryReport(data) {
|
|
11341
11350
|
return typeof data.schemaVersion === "string";
|
|
11342
11351
|
}
|
|
11352
|
+
function githubContext(deps) {
|
|
11353
|
+
const env = deps.env;
|
|
11354
|
+
const context = {
|
|
11355
|
+
repo: env.GITHUB_REPOSITORY,
|
|
11356
|
+
// GITHUB_HEAD_REF is the source branch on a pull_request event, where
|
|
11357
|
+
// GITHUB_REF_NAME would be the synthetic "<n>/merge" ref.
|
|
11358
|
+
branch: env.GITHUB_HEAD_REF || env.GITHUB_REF_NAME,
|
|
11359
|
+
gitSha: env.GITHUB_SHA
|
|
11360
|
+
};
|
|
11361
|
+
let event;
|
|
11362
|
+
try {
|
|
11363
|
+
event = JSON.parse(deps.readFile(env.GITHUB_EVENT_PATH ?? ""));
|
|
11364
|
+
} catch {
|
|
11365
|
+
return context;
|
|
11366
|
+
}
|
|
11367
|
+
context.prNumber = event.pull_request?.number;
|
|
11368
|
+
context.prUrl = event.pull_request?.html_url;
|
|
11369
|
+
const baseSha = event.pull_request?.base?.sha ?? event.before;
|
|
11370
|
+
if (baseSha && !/^0+$/.test(baseSha)) {
|
|
11371
|
+
if (deps.git(["cat-file", "-e", baseSha]) === void 0) {
|
|
11372
|
+
deps.git(["fetch", "--depth=1", "origin", baseSha]);
|
|
11373
|
+
}
|
|
11374
|
+
context.baseSha = baseSha;
|
|
11375
|
+
}
|
|
11376
|
+
return context;
|
|
11377
|
+
}
|
|
11378
|
+
function summaryWriter(deps) {
|
|
11379
|
+
const summaryPath = deps.env.GITHUB_STEP_SUMMARY;
|
|
11380
|
+
return (markdown) => {
|
|
11381
|
+
if (summaryPath) deps.appendFile(summaryPath, `${markdown}
|
|
11382
|
+
`);
|
|
11383
|
+
};
|
|
11384
|
+
}
|
|
11385
|
+
function cell(text2) {
|
|
11386
|
+
return text2.replaceAll("|", "\\|");
|
|
11387
|
+
}
|
|
11343
11388
|
async function runPush(rawArgs, depsOverride = {}) {
|
|
11344
11389
|
const deps = { ...defaultDeps(), ...depsOverride };
|
|
11345
11390
|
let parsed;
|
|
@@ -11354,6 +11399,7 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
11354
11399
|
branch: { type: "string" },
|
|
11355
11400
|
"git-sha": { type: "string" },
|
|
11356
11401
|
base: { type: "string" },
|
|
11402
|
+
gate: { type: "boolean" },
|
|
11357
11403
|
help: { type: "boolean", short: "h" }
|
|
11358
11404
|
}
|
|
11359
11405
|
});
|
|
@@ -11375,7 +11421,7 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
11375
11421
|
const key = parsed.values.key ?? deps.env.EXECUTABLE_STORIES_API_KEY;
|
|
11376
11422
|
if (!key) {
|
|
11377
11423
|
deps.error(
|
|
11378
|
-
"push needs an API key: pass --key or set EXECUTABLE_STORIES_API_KEY. Create one in
|
|
11424
|
+
"push needs an API key: pass --key or set EXECUTABLE_STORIES_API_KEY. Create one in your cloud instance's settings (Ingest key)."
|
|
11379
11425
|
);
|
|
11380
11426
|
return EXIT_USAGE;
|
|
11381
11427
|
}
|
|
@@ -11399,16 +11445,20 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
11399
11445
|
return EXIT_USAGE;
|
|
11400
11446
|
}
|
|
11401
11447
|
}
|
|
11402
|
-
const
|
|
11448
|
+
const onActions = deps.env.GITHUB_ACTIONS === "true";
|
|
11449
|
+
const github = onActions ? githubContext(deps) : {};
|
|
11450
|
+
const summary = summaryWriter(deps);
|
|
11451
|
+
const repo = parsed.values.repo ?? github.repo ?? repoSlugFromRemote(deps.git(["config", "--get", "remote.origin.url"]) ?? "");
|
|
11403
11452
|
if (!repo) {
|
|
11404
11453
|
deps.error("Could not infer the repository slug from git. Pass --repo <org/name>.");
|
|
11405
11454
|
return EXIT_USAGE;
|
|
11406
11455
|
}
|
|
11407
|
-
const branch = parsed.values.branch ?? deps.git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
11408
|
-
const gitSha = parsed.values["git-sha"] ?? deps.git(["rev-parse", "HEAD"]);
|
|
11456
|
+
const branch = parsed.values.branch ?? github.branch ?? deps.git(["rev-parse", "--abbrev-ref", "HEAD"]);
|
|
11457
|
+
const gitSha = parsed.values["git-sha"] ?? github.gitSha ?? deps.git(["rev-parse", "HEAD"]);
|
|
11409
11458
|
const baseUrl = parsed.values.url ?? deps.env.EXECUTABLE_STORIES_URL ?? "https://app.executablestories.com";
|
|
11410
11459
|
const base = parsed.values.base;
|
|
11411
|
-
const
|
|
11460
|
+
const baseSha = base ? deps.git(["rev-parse", base]) : github.baseSha;
|
|
11461
|
+
const changedFiles = baseSha ? deps.git(["diff", "--name-only", `${baseSha}...HEAD`])?.split("\n").filter(Boolean) ?? [] : [];
|
|
11412
11462
|
if (base && changedFiles.length === 0) {
|
|
11413
11463
|
deps.error(`Warning: no changed files found against ${base}; pushing without change metadata.`);
|
|
11414
11464
|
}
|
|
@@ -11424,9 +11474,13 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
11424
11474
|
repo,
|
|
11425
11475
|
branch,
|
|
11426
11476
|
gitSha,
|
|
11427
|
-
|
|
11477
|
+
// "serve" named a subcommand that no longer exists (ADR 0006). The
|
|
11478
|
+
// cloud accepts both; "local" is what this is.
|
|
11479
|
+
source: onActions ? "action" : "local",
|
|
11428
11480
|
report,
|
|
11429
|
-
...changedFiles.length > 0 ? { changedFiles, baseSha
|
|
11481
|
+
...changedFiles.length > 0 ? { changedFiles, baseSha } : {},
|
|
11482
|
+
...github.prNumber ? { prNumber: github.prNumber } : {},
|
|
11483
|
+
...github.prUrl ? { prUrl: github.prUrl } : {}
|
|
11430
11484
|
})
|
|
11431
11485
|
});
|
|
11432
11486
|
} catch (err) {
|
|
@@ -11439,12 +11493,97 @@ async function runPush(rawArgs, depsOverride = {}) {
|
|
|
11439
11493
|
deps.error(`Push rejected: HTTP ${response.status}${retryAfter ? ` (retry after ${retryAfter}s)` : ""}: ${body.slice(0, 500)}`);
|
|
11440
11494
|
return EXIT_PUSH_FAILED;
|
|
11441
11495
|
}
|
|
11442
|
-
let
|
|
11496
|
+
let result = {};
|
|
11443
11497
|
try {
|
|
11444
|
-
|
|
11498
|
+
result = JSON.parse(body);
|
|
11445
11499
|
} catch {
|
|
11446
11500
|
}
|
|
11501
|
+
const runId = String(result.runId ?? "");
|
|
11447
11502
|
deps.log(runId ? `Pushed run ${runId} (${repo}${branch ? `@${branch}` : ""})` : "Pushed run.");
|
|
11503
|
+
if (result.url) deps.log(result.url);
|
|
11504
|
+
summary(
|
|
11505
|
+
result.url ? `### Executable Stories
|
|
11506
|
+
|
|
11507
|
+
[View this run](${result.url})` : "### Executable Stories\n\nRun pushed."
|
|
11508
|
+
);
|
|
11509
|
+
if (runId && deps.env.GITHUB_OUTPUT) {
|
|
11510
|
+
deps.appendFile(deps.env.GITHUB_OUTPUT, `ingest-run-id=${runId}
|
|
11511
|
+
`);
|
|
11512
|
+
}
|
|
11513
|
+
const recommendations = result.recommendations ?? [];
|
|
11514
|
+
if (recommendations.length > 0) {
|
|
11515
|
+
deps.log(`
|
|
11516
|
+
Recommended scope for this change (${recommendations.length}):`);
|
|
11517
|
+
for (const item of recommendations) {
|
|
11518
|
+
deps.log(` [${item.confidence}] ${item.title} \u2014 ${item.reason}`);
|
|
11519
|
+
}
|
|
11520
|
+
summary(`
|
|
11521
|
+
**Recommended scope for this change (${recommendations.length})**
|
|
11522
|
+
`);
|
|
11523
|
+
summary("| Confidence | Case | Why |\n| --- | --- | --- |");
|
|
11524
|
+
for (const item of recommendations) {
|
|
11525
|
+
summary(`| ${cell(item.confidence)} | ${cell(item.title)} | ${cell(item.reason)} |`);
|
|
11526
|
+
}
|
|
11527
|
+
}
|
|
11528
|
+
if (!parsed.values.gate) return EXIT_SUCCESS;
|
|
11529
|
+
if (!gitSha) {
|
|
11530
|
+
deps.error("--gate needs a commit sha: pass --git-sha or run inside a git repository.");
|
|
11531
|
+
return EXIT_USAGE;
|
|
11532
|
+
}
|
|
11533
|
+
return await runGate({ baseUrl, key, repo, gitSha, onActions }, deps);
|
|
11534
|
+
}
|
|
11535
|
+
async function runGate({
|
|
11536
|
+
baseUrl,
|
|
11537
|
+
key,
|
|
11538
|
+
repo,
|
|
11539
|
+
gitSha,
|
|
11540
|
+
onActions
|
|
11541
|
+
}, deps) {
|
|
11542
|
+
const summary = summaryWriter(deps);
|
|
11543
|
+
const query = `repo=${encodeURIComponent(repo)}&sha=${encodeURIComponent(gitSha)}`;
|
|
11544
|
+
let response;
|
|
11545
|
+
try {
|
|
11546
|
+
response = await deps.fetchFn(new URL(`/api/v1/releases/gate?${query}`, baseUrl), {
|
|
11547
|
+
headers: { Authorization: `Bearer ${key}` }
|
|
11548
|
+
});
|
|
11549
|
+
} catch (err) {
|
|
11550
|
+
deps.error(`Could not reach the gate: ${err instanceof Error ? err.message : String(err)}`);
|
|
11551
|
+
return EXIT_PUSH_FAILED;
|
|
11552
|
+
}
|
|
11553
|
+
const body = await response.text();
|
|
11554
|
+
if (!response.ok) {
|
|
11555
|
+
deps.error(`Gate check failed: HTTP ${response.status}: ${body.slice(0, 500)}`);
|
|
11556
|
+
return EXIT_PUSH_FAILED;
|
|
11557
|
+
}
|
|
11558
|
+
let gate = {};
|
|
11559
|
+
try {
|
|
11560
|
+
gate = JSON.parse(body);
|
|
11561
|
+
} catch {
|
|
11562
|
+
deps.error(`Gate returned a non-JSON body: ${body.slice(0, 200)}`);
|
|
11563
|
+
return EXIT_PUSH_FAILED;
|
|
11564
|
+
}
|
|
11565
|
+
for (const warning of gate.warnings ?? []) deps.log(` warning: ${warning}`);
|
|
11566
|
+
const commit = `${repo}@${gitSha.slice(0, 12)}`;
|
|
11567
|
+
if (gate.status === "no-release") {
|
|
11568
|
+
deps.log(`
|
|
11569
|
+
No release recorded for ${commit} \u2014 nothing to gate on.`);
|
|
11570
|
+
summary("\n**Release gate: no release recorded for this commit**");
|
|
11571
|
+
return EXIT_SUCCESS;
|
|
11572
|
+
}
|
|
11573
|
+
if (gate.status === "blocked") {
|
|
11574
|
+
deps.error(`
|
|
11575
|
+
Release gate: BLOCKED for ${commit}`);
|
|
11576
|
+
summary("\n**Release gate: blocked**\n");
|
|
11577
|
+
for (const reason of gate.blocking ?? []) {
|
|
11578
|
+
deps.error(` - ${reason}`);
|
|
11579
|
+
summary(`- ${reason}`);
|
|
11580
|
+
if (onActions) deps.error(`::error::Release gate: ${reason}`);
|
|
11581
|
+
}
|
|
11582
|
+
return EXIT_GATE_BLOCKED;
|
|
11583
|
+
}
|
|
11584
|
+
deps.log(`
|
|
11585
|
+
Release gate: clear for ${commit}`);
|
|
11586
|
+
summary("\n**Release gate: clear**");
|
|
11448
11587
|
return EXIT_SUCCESS;
|
|
11449
11588
|
}
|
|
11450
11589
|
|
|
@@ -12018,7 +12157,7 @@ SUBCOMMANDS
|
|
|
12018
12157
|
init-astro Scaffold a thin Astro docs site (Starlight + executable-stories-astro; live stories at /stories)
|
|
12019
12158
|
new Scaffold a docs page from a template (adr, runbook, decision-log, incident, scenario-note)
|
|
12020
12159
|
check-links Scan docs for broken internal/external links (CI-friendly exit code)
|
|
12021
|
-
push Send a run (StoryReport or raw run JSON) to
|
|
12160
|
+
push Send a run (StoryReport or raw run JSON) to a cloud ingest endpoint
|
|
12022
12161
|
coverage Compare your stories against a test-management system (read-only)
|
|
12023
12162
|
sync Push cases, executions, and evidence to TestRail or Xray (dry run by default)
|
|
12024
12163
|
import-openapi Generate API doc pages from an OpenAPI spec, linked to verifying stories
|