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 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((cell) => ({
3486
+ cells: table2.rows[r].map((cell2) => ({
3487
3487
  location: { line: rowLine },
3488
- value: cell
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((cell) => ({ value: cell }))
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((cell) => `<td>${escapeHtml2(cell)}</td>`).join("")}</tr>`).join("");
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((cell) => tableCell(cell)))
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 Executable Stories Cloud"],
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 Executable Stories Cloud. <run.json> is either a StoryReport v1
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
- Exit codes: 0 pushed, 1 push rejected/failed, 4 usage error.`;
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 Executable Stories Cloud (Settings -> Ingest key)."
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 repo = parsed.values.repo ?? repoSlugFromRemote(deps.git(["config", "--get", "remote.origin.url"]) ?? "");
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 changedFiles = base ? deps.git(["diff", "--name-only", `${base}...HEAD`])?.split("\n").filter(Boolean) ?? [] : [];
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
- source: "serve",
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: deps.git(["rev-parse", base]) } : {}
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 runId = "";
11496
+ let result = {};
11443
11497
  try {
11444
- runId = String(JSON.parse(body).runId ?? "");
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 Executable Stories Cloud
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