@deployanyway/ship-it-meter 0.2.0 → 0.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.4.0
4
+
5
+ Twelve named demonstration scenarios show passing evidence, Friday releases, missing evidence, failed builds, failing tests, low coverage, critical issues, lint failures, dirty worktrees, feature branches, zero tests and weekend incidents. These are examples, never evidence about your real project.
6
+
7
+ ```sh
8
+
9
+
10
+ ## 0.3.0 — 2026-10-08
11
+
12
+ - Useful structured API/CLI additions described in README.
13
+ - TypeScript declarations, CommonJS entry, coverage gates and installed archive checks.
14
+ - Linux Node 22/24 plus Windows/macOS Node 24 CI.
15
+
3
16
  ## 0.2.0 — 2026-10-08
4
17
 
5
18
  - Preflight, with a parachute: `preflight(input)` returns the same score, verdict and reasons as `shipIt`, plus actionable `actions`. CLI `--checklist` includes them in text or JSON output. It uses only supplied evidence and is not a deployment guarantee.
@@ -15,3 +28,4 @@
15
28
 
16
29
  - Deterministic readiness score with explanations and technical blocker caps.
17
30
  - CLI, documented scoring rules, tests, linting, formatting, and Node 22/24 CI.
31
+ ```
package/MIGRATION.md ADDED
@@ -0,0 +1,11 @@
1
+ # 0.3.0 migration
2
+
3
+ Node 22.13+ is required. Existing core APIs remain available; TypeScript declarations and CommonJS exports are new. The CLI now lives in src/cli.js behind the same executable path. Input/stdout behavior for new options is documented in README.
4
+
5
+ Install 0.3.0 with npm. Seeds and exact humorous wording are version-specific. Do not treat jokes or heuristic scores as production evidence.
6
+
7
+ ## 0.3.0 to 0.4.0
8
+
9
+ 12 discoverable evidence scenarios and releasePlan with ordered blocker/review/release tasks and observable completion criteria; API and CLI scenarios/plans; passing/blocked gate exit codes preserved.
10
+
11
+ Existing defaults and entry points remain available. The new release plan and demonstration scenarios are opt-in; the original scoring and gate rules are retained.
package/README.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # ship-it-meter
2
2
 
3
+ ## Evidence into an actual release plan (0.4.0)
4
+
5
+ Twelve named demonstration scenarios show passing evidence, Friday releases, missing evidence, failed builds, failing tests, low coverage, critical issues, lint failures, dirty worktrees, feature branches, zero tests and weekend incidents. These are examples, never evidence about your real project.
6
+
7
+ ```sh
8
+ npx @deployanyway/ship-it-meter --list-scenarios
9
+ npx @deployanyway/ship-it-meter --scenario failed-build --plan
10
+ npx @deployanyway/ship-it-meter --scenario ready --plan --json
11
+ ```
12
+
13
+ ```js
14
+ import {
15
+ releasePlan,
16
+ releaseScenarios,
17
+ scenarioEvidence,
18
+ } from "@deployanyway/ship-it-meter";
19
+ console.log(releaseScenarios());
20
+ const plan = releasePlan(scenarioEvidence("failing-tests"), { minScore: 80 });
21
+ console.log(plan.passed, plan.summary, plan.tasks);
22
+ ```
23
+
24
+ `releasePlan(evidence?, options?)` includes the existing gate plus prioritized tasks with stable IDs and observable verification criteria. Priorities are blocker, review and release. `scenarioEvidence(name)` and `releaseScenarios()` return independent evidence copies. CLI flags override scenario fields; stdin and scenario cannot be combined. Choose one of plan, gate or checklist. A blocked plan/gate exits 1; invalid input exits 2. Readiness remains a heuristic based on caller-supplied evidence; it does not run CI or approve deployment automatically. Coverage below 80 adds a review task, not a separate hard blocker; the configured score threshold and existing gate rules still decide the gate.
25
+
26
+ > **Version 0.4.0:** install from npm with Node 22.13+ or Node 24. See MIGRATION.md for changes from 0.2.0.
27
+
3
28
  [![npm version](https://img.shields.io/npm/v/%40deployanyway%2Fship-it-meter)](https://www.npmjs.com/package/@deployanyway/ship-it-meter)
4
29
  [![CI](https://github.com/DeployAnyway/ship-it-meter/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/DeployAnyway/ship-it-meter/actions/workflows/ci.yml)
5
30
 
@@ -167,3 +192,43 @@ API (import the named functions from this package):
167
192
  ```js
168
193
  preflight({ tests: 125, coverage: 82, build: true, day: "friday" });
169
194
  ```
195
+
196
+ ## An explicit gate for automation
197
+
198
+ ```js
199
+ import { releaseGate } from "@deployanyway/ship-it-meter";
200
+ const result = releaseGate(
201
+ { tests: 42, coverage: 92, build: true },
202
+ { minScore: 80 },
203
+ );
204
+ console.log(result.passed, result.blockers);
205
+ ```
206
+
207
+ A gate requires positive test count, coverage evidence and a passing build. Failing tests, critical issues, lint failures and dirty changes block it even when a low score threshold is chosen. Scores remain a heuristic, not authorization to deploy. The result adds passed, blockers and minScore to the preflight checklist.
208
+
209
+ ```sh
210
+ node bin/cli.js --tests 42 --coverage 92 --build pass --gate --json
211
+ node bin/cli.js --stdin --gate --min-score 90 --json < evidence.json
212
+ ```
213
+
214
+ --stdin accepts a bounded JSON evidence object (256 KiB); explicit flags override fields. --min-score requires --gate and a 0–100 threshold, default 80. Exit 0: gate passes or ordinary scoring succeeds; 1: gate blocked; 2: malformed input. Upstream pipeline status is your shell's responsibility.
215
+
216
+ ## Run from source
217
+
218
+ ```sh
219
+ git clone --branch main https://github.com/DeployAnyway/ship-it-meter.git
220
+ cd ship-it-meter
221
+ npm ci
222
+ npm run build
223
+ node bin/cli.js --help
224
+ ```
225
+
226
+ ## Candidate quality standard
227
+
228
+ Version 0.3 provides useful declaration types, ESM/CommonJS exports, installed-archive checks, and coverage gates (90% statements/lines/functions, 85% branches). CI covers Linux Node 22/24 and Windows/macOS Node 24. Node 22.13+ is required. No runtime dependencies, telemetry or network requests.
229
+
230
+ From a source checkout: npm ci, npm run build, npm run coverage, npm run test:types, npm run verify:package. Pack verification installs a temporary local archive and checks module entries, types, executable and offline npm exec.
231
+
232
+ [Contribution guide](CONTRIBUTING.md) · [Conduct](CODE_OF_CONDUCT.md) · [Security](SECURITY.md) · [Roadmap](ROADMAP.md) · [Migration](MIGRATION.md).
233
+
234
+ **Tools for developers who probably know better.** Software nobody requested, built with questionable priorities, and shipped with absolute confidence!
package/bin/cli.js CHANGED
@@ -1,68 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import { parseArgs } from "node:util";
3
- import { readFileSync } from "node:fs";
4
- import { URL } from "node:url";
5
- import { shipIt, preflight } from "../src/index.js";
6
-
7
- try {
8
- const { values } = parseArgs({
9
- options: {
10
- help: { type: "boolean", short: "h" },
11
- version: { type: "boolean", short: "v" },
12
- json: { type: "boolean" },
13
- checklist: { type: "boolean" },
14
- tests: { type: "string" },
15
- failing: { type: "string" },
16
- coverage: { type: "string" },
17
- day: { type: "string" },
18
- branch: { type: "string" },
19
- critical: { type: "string" },
20
- lint: { type: "string" },
21
- build: { type: "string" },
22
- dirty: { type: "boolean" },
23
- },
24
- });
25
- if (values.help) {
26
- console.log(
27
- "Usage: ship-it-meter [options]\n\nOptions:\n --tests number Total tests\n --failing number Failing tests\n --coverage number Coverage percent\n --build pass|fail Build result\n --critical number Open critical issues\n --lint number Lint failures\n --dirty Uncommitted changes\n --branch name Branch name\n --day weekday Full weekday name\n --checklist Actionable, mildly concerned preflight\n --json Structured output\n -h, --help Help\n -v, --version Version\n\nMissing test, coverage, and build evidence reduces readiness.\nExit codes: 0 scored successfully; 2 invalid arguments.",
28
- );
29
- } else if (values.version) {
30
- console.log(
31
- JSON.parse(
32
- readFileSync(new URL("../package.json", import.meta.url), "utf8"),
33
- ).version,
34
- );
35
- } else {
36
- const input = {};
37
- for (const [flag, key] of Object.entries({
38
- tests: "tests",
39
- failing: "failingTests",
40
- coverage: "coverage",
41
- critical: "criticalIssues",
42
- lint: "lintFailures",
43
- })) {
44
- if (values[flag] !== undefined) {
45
- if (!/^\d+(?:\.\d+)?$/.test(values[flag]))
46
- throw new TypeError(`${flag} must be a nonnegative decimal number.`);
47
- input[key] = Number(values[flag]);
48
- }
49
- }
50
- if (values.build !== undefined) {
51
- if (!["pass", "fail"].includes(values.build))
52
- throw new RangeError("--build must be pass or fail.");
53
- input.build = values.build === "pass";
54
- }
55
- for (const key of ["day", "branch"])
56
- if (values[key] !== undefined) input[key] = values[key];
57
- if (values.dirty) input.uncommittedChanges = true;
58
- const result = values.checklist ? preflight(input) : shipIt(input);
59
- console.log(
60
- values.json
61
- ? JSON.stringify(result, null, 2)
62
- : `${result.score}/100 — ${result.verdict}\n${result.reasons.map((reason) => `- ${reason}`).join("\n")}${result.actions ? "\n\nBefore you ship:\n" + result.actions.map((action) => `[ ] ${action}`).join("\n") : ""}`,
63
- );
64
- }
65
- } catch (error) {
66
- console.error(`ship-it-meter: ${error.message}\nRun with --help for usage.`);
67
- process.exitCode = 2;
68
- }
2
+ import "../src/cli.js";
package/dist/index.cjs ADDED
@@ -0,0 +1,417 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.js
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ preflight: () => preflight,
24
+ releaseGate: () => releaseGate,
25
+ releasePlan: () => releasePlan,
26
+ releaseScenarios: () => releaseScenarios,
27
+ scenarioEvidence: () => scenarioEvidence,
28
+ shipIt: () => shipIt
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/scenarios.js
33
+ var scenarios = {
34
+ ready: {
35
+ description: "Passing evidence, clean worktree and main branch. Boring is a compliment.",
36
+ evidence: {
37
+ tests: 125,
38
+ coverage: 96,
39
+ build: true,
40
+ branch: "main",
41
+ day: "tuesday"
42
+ }
43
+ },
44
+ friday: {
45
+ description: "Good evidence on Friday. Confirm rollback and on-call before the victory lap.",
46
+ evidence: {
47
+ tests: 125,
48
+ coverage: 92,
49
+ build: true,
50
+ branch: "main",
51
+ day: "friday"
52
+ }
53
+ },
54
+ "no-evidence": {
55
+ description: "Confidence is present. Test, coverage and build evidence have not arrived.",
56
+ evidence: {}
57
+ },
58
+ "failed-build": {
59
+ description: "The build is red. The deploy button is not a color correction tool.",
60
+ evidence: {
61
+ tests: 125,
62
+ coverage: 92,
63
+ build: false
64
+ }
65
+ },
66
+ "failing-tests": {
67
+ description: "Some tests fail. Green deployment lights will not repair assertions.",
68
+ evidence: {
69
+ tests: 125,
70
+ failingTests: 3,
71
+ coverage: 92,
72
+ build: true
73
+ }
74
+ },
75
+ "low-coverage": {
76
+ description: "A passing build with many untested paths. The bugs appreciate the privacy.",
77
+ evidence: {
78
+ tests: 125,
79
+ coverage: 45,
80
+ build: true
81
+ }
82
+ },
83
+ "critical-issue": {
84
+ description: "A critical issue blocks the gate even when everything else looks cheerful.",
85
+ evidence: {
86
+ tests: 125,
87
+ coverage: 96,
88
+ build: true,
89
+ criticalIssues: 1
90
+ }
91
+ },
92
+ "lint-failures": {
93
+ description: "Lint failures are still failures, however decorative the semicolons feel.",
94
+ evidence: {
95
+ tests: 125,
96
+ coverage: 96,
97
+ build: true,
98
+ lintFailures: 2
99
+ }
100
+ },
101
+ "dirty-worktree": {
102
+ description: "The laptop contains uncommitted changes. It is not the release archive.",
103
+ evidence: {
104
+ tests: 125,
105
+ coverage: 96,
106
+ build: true,
107
+ uncommittedChanges: true
108
+ }
109
+ },
110
+ "feature-branch": {
111
+ description: "Healthy evidence on a feature branch. Verify the intended release source.",
112
+ evidence: {
113
+ tests: 125,
114
+ coverage: 96,
115
+ build: true,
116
+ branch: "feature/zoomies"
117
+ }
118
+ },
119
+ "zero-tests": {
120
+ description: "No tests ran. Zero failures is an especially small achievement here.",
121
+ evidence: {
122
+ tests: 0,
123
+ coverage: 0,
124
+ build: true
125
+ }
126
+ },
127
+ "weekend-incident": {
128
+ description: "A failed build, failing tests and a critical issue. Bring evidence, not a bigger confidence hat.",
129
+ evidence: {
130
+ tests: 125,
131
+ failingTests: 12,
132
+ coverage: 60,
133
+ build: false,
134
+ criticalIssues: 1,
135
+ day: "saturday"
136
+ }
137
+ }
138
+ };
139
+ function releaseScenarios() {
140
+ return Object.entries(scenarios).map(([name, value]) => ({
141
+ name,
142
+ description: value.description,
143
+ evidence: { ...value.evidence }
144
+ }));
145
+ }
146
+ function scenarioEvidence(name) {
147
+ if (typeof name !== "string" || !Object.hasOwn(scenarios, name))
148
+ throw new RangeError(
149
+ "Unknown scenario. Choose: " + Object.keys(scenarios).join(", ")
150
+ );
151
+ return { ...scenarios[name].evidence };
152
+ }
153
+
154
+ // src/index.js
155
+ var days = [
156
+ "monday",
157
+ "tuesday",
158
+ "wednesday",
159
+ "thursday",
160
+ "friday",
161
+ "saturday",
162
+ "sunday"
163
+ ];
164
+ var counts = ["tests", "failingTests", "criticalIssues", "lintFailures"];
165
+ var allowed = [
166
+ ...counts,
167
+ "coverage",
168
+ "build",
169
+ "uncommittedChanges",
170
+ "branch",
171
+ "day"
172
+ ];
173
+ function releaseGate(input = {}, options = {}) {
174
+ if (!options || typeof options !== "object" || Array.isArray(options))
175
+ throw new TypeError("Gate options must be an object.");
176
+ const minScore = options.minScore ?? 80;
177
+ if (typeof minScore !== "number" || !Number.isFinite(minScore) || minScore < 0 || minScore > 100)
178
+ throw new RangeError("minScore must be between 0 and 100.");
179
+ const result = preflight(input);
180
+ const blockers = [];
181
+ if (!input.tests)
182
+ blockers.push("Provide a positive test count. Vibes cannot pass CI.");
183
+ if (input.build !== true) blockers.push("A passing build is required.");
184
+ if (input.coverage === void 0)
185
+ blockers.push("Coverage evidence is required.");
186
+ if (input.failingTests) blockers.push("Failing tests block the gate.");
187
+ if (input.criticalIssues) blockers.push("Critical issues block the gate.");
188
+ if (input.lintFailures) blockers.push("Lint failures block the gate.");
189
+ if (input.uncommittedChanges)
190
+ blockers.push("Uncommitted changes block the gate.");
191
+ if (result.score < minScore)
192
+ blockers.push(`Score ${result.score} is below ${minScore}.`);
193
+ return { ...result, minScore, passed: blockers.length === 0, blockers };
194
+ }
195
+ function shipIt(input = {}) {
196
+ if (!input || typeof input !== "object" || Array.isArray(input))
197
+ throw new TypeError("Input must be an object.");
198
+ for (const key of Object.keys(input))
199
+ if (!allowed.includes(key)) throw new TypeError(`Unknown input: ${key}.`);
200
+ for (const key of counts) {
201
+ if (input[key] !== void 0 && (!Number.isSafeInteger(input[key]) || input[key] < 0))
202
+ throw new RangeError(`${key} must be a nonnegative safe integer.`);
203
+ }
204
+ if (input.coverage !== void 0 && (typeof input.coverage !== "number" || !Number.isFinite(input.coverage) || input.coverage < 0 || input.coverage > 100))
205
+ throw new RangeError("coverage must be between 0 and 100.");
206
+ for (const key of ["build", "uncommittedChanges"])
207
+ if (input[key] !== void 0 && typeof input[key] !== "boolean")
208
+ throw new TypeError(`${key} must be a boolean.`);
209
+ for (const key of ["branch", "day"])
210
+ if (input[key] !== void 0 && (typeof input[key] !== "string" || !input[key].trim()))
211
+ throw new TypeError(`${key} must be a nonempty string.`);
212
+ const day = input.day?.trim().toLowerCase();
213
+ if (day !== void 0 && !days.includes(day))
214
+ throw new RangeError("day must be a full weekday name.");
215
+ const failing = input.failingTests ?? 0;
216
+ if (failing > 0 && input.tests === void 0)
217
+ throw new RangeError("tests is required when failingTests is positive.");
218
+ if (input.tests !== void 0 && failing > input.tests)
219
+ throw new RangeError("failingTests cannot exceed tests.");
220
+ let score = 100;
221
+ const reasons = [];
222
+ const deduct = (points, reason) => {
223
+ score -= points;
224
+ reasons.push(`${reason} (-${points})`);
225
+ };
226
+ if (!input.tests)
227
+ deduct(
228
+ 25,
229
+ input.tests === 0 ? "No tests reported" : "Test count not provided"
230
+ );
231
+ else if (failing)
232
+ deduct(
233
+ Math.min(50, 20 + Math.ceil(30 * (failing / input.tests))),
234
+ `${failing} of ${input.tests} tests are failing`
235
+ );
236
+ else reasons.push(`${input.tests} tests reported passing`);
237
+ if (input.coverage === void 0) deduct(15, "Coverage not provided");
238
+ else {
239
+ const penalty = Math.ceil((100 - input.coverage) * 0.2);
240
+ if (penalty) deduct(penalty, `Coverage is ${input.coverage}%`);
241
+ else reasons.push("Coverage is 100%");
242
+ }
243
+ if (input.build === void 0) deduct(15, "Build status not provided");
244
+ else if (!input.build) deduct(40, "Build failed");
245
+ else reasons.push("Build passed");
246
+ if (input.criticalIssues)
247
+ deduct(
248
+ Math.min(50, 25 * input.criticalIssues),
249
+ `${input.criticalIssues} open critical issues`
250
+ );
251
+ if (input.lintFailures)
252
+ deduct(
253
+ Math.min(20, 5 * input.lintFailures),
254
+ `${input.lintFailures} lint failures`
255
+ );
256
+ if (input.uncommittedChanges) deduct(10, "Uncommitted changes reported");
257
+ if (input.branch !== void 0 && !["main", "master"].includes(input.branch.trim()))
258
+ deduct(5, `Branch is ${input.branch.trim()}`);
259
+ if (day === "friday")
260
+ deduct(3, "Friday: the weekend would like a quiet entrance");
261
+ score = Math.max(0, Math.min(100, score));
262
+ if (input.build === false || input.criticalIssues > 0) {
263
+ score = Math.min(score, 29);
264
+ reasons.push("Failed build or critical issues cap the score at 29");
265
+ } else if (failing > 0) {
266
+ score = Math.min(score, 59);
267
+ reasons.push("Failing tests cap the score at 59");
268
+ }
269
+ const verdict = score < 30 ? "Absolutely Not" : score < 60 ? "Questionable" : score < 80 ? "Probably Fine" : score < 95 ? "Ship It" : "Suspiciously Ready";
270
+ return { score, verdict, reasons };
271
+ }
272
+ function preflight(input = {}) {
273
+ const result = shipIt(input);
274
+ const actions = [];
275
+ if (input.build !== true)
276
+ actions.push("Get a passing build. Confidence is not a build artifact.");
277
+ if (!input.tests)
278
+ actions.push("Run tests and report the count. Vibes are not assertions.");
279
+ if (input.failingTests)
280
+ actions.push("Fix failing tests. Red is not a festive deployment theme.");
281
+ if (input.coverage === void 0 || input.coverage < 80)
282
+ actions.push(
283
+ "Measure coverage and review untested paths. The bugs enjoy privacy."
284
+ );
285
+ if (input.criticalIssues)
286
+ actions.push(
287
+ "Resolve critical issues. The incident channel deserves a quiet afternoon."
288
+ );
289
+ if (input.lintFailures)
290
+ actions.push("Fix lint failures. Let the semicolons retire in peace.");
291
+ if (input.uncommittedChanges)
292
+ actions.push(
293
+ "Commit or stash local changes. Your laptop is not the release archive."
294
+ );
295
+ if (input.day?.trim().toLowerCase() === "friday")
296
+ actions.push(
297
+ "Confirm rollback and on-call cover. The weekend has other plans."
298
+ );
299
+ if (!actions.length)
300
+ actions.push(
301
+ "Confirm monitoring and rollback. Even excellent evidence needs a parachute."
302
+ );
303
+ return { ...result, actions };
304
+ }
305
+ function releasePlan(input = {}, options = {}) {
306
+ const gate = releaseGate(input, options);
307
+ const tasks = [];
308
+ const add = (id, priority, title, verify) => tasks.push({ id, priority, title, verify });
309
+ if (input.build !== true)
310
+ add(
311
+ "build",
312
+ "blocker",
313
+ "Get a passing build",
314
+ "Run the release build for the exact commit and record its successful result."
315
+ );
316
+ if (!input.tests)
317
+ add(
318
+ "tests",
319
+ "blocker",
320
+ "Run a nonempty test suite",
321
+ "Record the test count and outcome for the release commit."
322
+ );
323
+ if (input.failingTests)
324
+ add(
325
+ "failing-tests",
326
+ "blocker",
327
+ "Repair failing tests",
328
+ "Reproduce each failure, fix its cause and rerun the suite."
329
+ );
330
+ if (input.coverage === void 0)
331
+ add(
332
+ "coverage-evidence",
333
+ "blocker",
334
+ "Measure coverage",
335
+ "Generate coverage for the release commit and record the percentage."
336
+ );
337
+ else if (input.coverage < 80)
338
+ add(
339
+ "coverage-review",
340
+ "review",
341
+ "Review untested paths",
342
+ "Inspect uncovered critical paths and add meaningful tests where needed."
343
+ );
344
+ if (input.criticalIssues)
345
+ add(
346
+ "critical-issues",
347
+ "blocker",
348
+ "Resolve critical issues",
349
+ "Record the resolution and verify the affected behavior."
350
+ );
351
+ if (input.lintFailures)
352
+ add(
353
+ "lint",
354
+ "blocker",
355
+ "Fix lint failures",
356
+ "Run the configured lint checks successfully."
357
+ );
358
+ if (input.uncommittedChanges)
359
+ add(
360
+ "worktree",
361
+ "blocker",
362
+ "Reconcile local changes",
363
+ "Commit or stash intentionally and verify the exact release source."
364
+ );
365
+ if (input.branch && !["main", "master"].includes(input.branch.trim()))
366
+ add(
367
+ "branch",
368
+ "review",
369
+ "Confirm release branch",
370
+ "Verify that this branch is the intended release source and that its CI passed."
371
+ );
372
+ if (input.day?.trim().toLowerCase() === "friday")
373
+ add(
374
+ "friday",
375
+ "review",
376
+ "Confirm weekend support",
377
+ "Identify monitoring ownership and a tested rollback path."
378
+ );
379
+ if (gate.score < gate.minScore)
380
+ add(
381
+ "score",
382
+ "blocker",
383
+ "Improve evidence to the configured threshold",
384
+ "Address score deductions, rerun the checks and reevaluate the gate."
385
+ );
386
+ add(
387
+ "rollback",
388
+ "release",
389
+ "Prepare the return route",
390
+ "Verify the rollback artifact and procedure before deployment."
391
+ );
392
+ add(
393
+ "monitoring",
394
+ "release",
395
+ "Watch the actual release",
396
+ "Check health and error metrics after deployment and name an owner."
397
+ );
398
+ const summaries = {
399
+ "Absolutely Not": "The evidence says stop. The confidence hat can wait.",
400
+ Questionable: "The release has questions. Answer them before adding confetti.",
401
+ "Probably Fine": "Close is a useful direction, not a deployment receipt.",
402
+ "Ship It": "Good evidence. Keep the parachute and watch the landing.",
403
+ "Suspiciously Ready": "Boring, verified readiness. Give the checks their share of the applause."
404
+ };
405
+ const rank = { blocker: 0, review: 1, release: 2 };
406
+ tasks.sort((a, b) => rank[a.priority] - rank[b.priority]);
407
+ return { ...gate, summary: summaries[gate.verdict], tasks };
408
+ }
409
+ // Annotate the CommonJS export names for ESM import in node:
410
+ 0 && (module.exports = {
411
+ preflight,
412
+ releaseGate,
413
+ releasePlan,
414
+ releaseScenarios,
415
+ scenarioEvidence,
416
+ shipIt
417
+ });
package/index.d.cts ADDED
@@ -0,0 +1,65 @@
1
+ export type Weekday =
2
+ | "monday"
3
+ | "tuesday"
4
+ | "wednesday"
5
+ | "thursday"
6
+ | "friday"
7
+ | "saturday"
8
+ | "sunday";
9
+ export interface Evidence {
10
+ tests?: number;
11
+ failingTests?: number;
12
+ coverage?: number;
13
+ build?: boolean;
14
+ uncommittedChanges?: boolean;
15
+ branch?: string;
16
+ day?: Weekday;
17
+ criticalIssues?: number;
18
+ lintFailures?: number;
19
+ }
20
+ export interface Readiness {
21
+ score: number;
22
+ verdict:
23
+ | "Absolutely Not"
24
+ | "Questionable"
25
+ | "Probably Fine"
26
+ | "Ship It"
27
+ | "Suspiciously Ready";
28
+ reasons: string[];
29
+ }
30
+ export interface Preflight extends Readiness {
31
+ actions: string[];
32
+ }
33
+ export interface Gate extends Preflight {
34
+ minScore: number;
35
+ passed: boolean;
36
+ blockers: string[];
37
+ }
38
+ export function shipIt(input?: Evidence): Readiness;
39
+ export function preflight(input?: Evidence): Preflight;
40
+ export function releaseGate(
41
+ input?: Evidence,
42
+ options?: { minScore?: number },
43
+ ): Gate;
44
+
45
+ export interface ReleaseTask {
46
+ id: string;
47
+ priority: "blocker" | "review" | "release";
48
+ title: string;
49
+ verify: string;
50
+ }
51
+ export interface ReleasePlan extends Gate {
52
+ summary: string;
53
+ tasks: ReleaseTask[];
54
+ }
55
+ export interface ReleaseScenario {
56
+ name: string;
57
+ description: string;
58
+ evidence: Evidence;
59
+ }
60
+ export function releaseScenarios(): ReleaseScenario[];
61
+ export function scenarioEvidence(name: string): Evidence;
62
+ export function releasePlan(
63
+ input?: Evidence,
64
+ options?: { minScore?: number },
65
+ ): ReleasePlan;
package/index.d.ts ADDED
@@ -0,0 +1,65 @@
1
+ export type Weekday =
2
+ | "monday"
3
+ | "tuesday"
4
+ | "wednesday"
5
+ | "thursday"
6
+ | "friday"
7
+ | "saturday"
8
+ | "sunday";
9
+ export interface Evidence {
10
+ tests?: number;
11
+ failingTests?: number;
12
+ coverage?: number;
13
+ build?: boolean;
14
+ uncommittedChanges?: boolean;
15
+ branch?: string;
16
+ day?: Weekday;
17
+ criticalIssues?: number;
18
+ lintFailures?: number;
19
+ }
20
+ export interface Readiness {
21
+ score: number;
22
+ verdict:
23
+ | "Absolutely Not"
24
+ | "Questionable"
25
+ | "Probably Fine"
26
+ | "Ship It"
27
+ | "Suspiciously Ready";
28
+ reasons: string[];
29
+ }
30
+ export interface Preflight extends Readiness {
31
+ actions: string[];
32
+ }
33
+ export interface Gate extends Preflight {
34
+ minScore: number;
35
+ passed: boolean;
36
+ blockers: string[];
37
+ }
38
+ export function shipIt(input?: Evidence): Readiness;
39
+ export function preflight(input?: Evidence): Preflight;
40
+ export function releaseGate(
41
+ input?: Evidence,
42
+ options?: { minScore?: number },
43
+ ): Gate;
44
+
45
+ export interface ReleaseTask {
46
+ id: string;
47
+ priority: "blocker" | "review" | "release";
48
+ title: string;
49
+ verify: string;
50
+ }
51
+ export interface ReleasePlan extends Gate {
52
+ summary: string;
53
+ tasks: ReleaseTask[];
54
+ }
55
+ export interface ReleaseScenario {
56
+ name: string;
57
+ description: string;
58
+ evidence: Evidence;
59
+ }
60
+ export function releaseScenarios(): ReleaseScenario[];
61
+ export function scenarioEvidence(name: string): Evidence;
62
+ export function releasePlan(
63
+ input?: Evidence,
64
+ options?: { minScore?: number },
65
+ ): ReleasePlan;
package/package.json CHANGED
@@ -1,9 +1,20 @@
1
1
  {
2
2
  "name": "@deployanyway/ship-it-meter",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Explainable deployment readiness scoring for tests, coverage, and builds. Turns questionable confidence into a number.",
5
5
  "type": "module",
6
- "exports": "./src/index.js",
6
+ "exports": {
7
+ ".": {
8
+ "import": {
9
+ "types": "./index.d.ts",
10
+ "default": "./src/index.js"
11
+ },
12
+ "require": {
13
+ "types": "./index.d.cts",
14
+ "default": "./dist/index.cjs"
15
+ }
16
+ }
17
+ },
7
18
  "bin": {
8
19
  "ship-it-meter": "./bin/cli.js"
9
20
  },
@@ -12,16 +23,26 @@
12
23
  "bin",
13
24
  "README.md",
14
25
  "LICENSE",
15
- "CHANGELOG.md"
26
+ "CHANGELOG.md",
27
+ "dist",
28
+ "index.d.ts",
29
+ "index.d.cts",
30
+ "MIGRATION.md"
16
31
  ],
17
32
  "engines": {
18
- "node": ">=22"
33
+ "node": ">=22.13"
19
34
  },
20
35
  "scripts": {
21
- "test": "node --test",
36
+ "test": "node --test test/*.test.js",
22
37
  "lint": "eslint .",
23
38
  "format": "prettier --write .",
24
- "format:check": "prettier --check ."
39
+ "format:check": "prettier --check .",
40
+ "build": "node scripts/build.mjs",
41
+ "pretest": "npm run build",
42
+ "prepack": "npm run build",
43
+ "coverage": "c8 --all --include=src/**/*.js --reporter=text --reporter=json-summary --reporter=lcov --check-coverage --lines=90 --statements=90 --functions=90 --branches=85 node --test test/*.test.js",
44
+ "test:types": "tsc -p tsconfig.json",
45
+ "verify:package": "node scripts/verify-package.mjs"
25
46
  },
26
47
  "repository": {
27
48
  "type": "git",
@@ -35,8 +56,12 @@
35
56
  "access": "public"
36
57
  },
37
58
  "devDependencies": {
59
+ "@types/node": "^26.6.4",
60
+ "c8": "^12.0.0",
61
+ "esbuild": "^0.28.2",
38
62
  "eslint": "^10.12.0",
39
- "prettier": "^3.6.2"
63
+ "prettier": "^3.6.2",
64
+ "typescript": "^7.0.2"
40
65
  },
41
66
  "keywords": [
42
67
  "nodejs",
@@ -47,6 +72,12 @@
47
72
  "readiness",
48
73
  "test-coverage",
49
74
  "build",
50
- "scoring"
51
- ]
75
+ "scoring",
76
+ "typescript",
77
+ "humor",
78
+ "deployanyway"
79
+ ],
80
+ "main": "./dist/index.cjs",
81
+ "types": "./index.d.ts",
82
+ "homepage": "https://deployanyway.github.io/"
52
83
  }
package/src/cli.js ADDED
@@ -0,0 +1,129 @@
1
+ import { parseArgs } from "node:util";
2
+ import { readFileSync } from "node:fs";
3
+ import { URL } from "node:url";
4
+ import {
5
+ shipIt,
6
+ preflight,
7
+ releaseGate,
8
+ releasePlan,
9
+ releaseScenarios,
10
+ scenarioEvidence,
11
+ } from "./index.js";
12
+
13
+ import { readStdin } from "./input.js";
14
+
15
+ try {
16
+ const { values } = parseArgs({
17
+ options: {
18
+ help: { type: "boolean", short: "h" },
19
+ version: { type: "boolean", short: "v" },
20
+ json: { type: "boolean" },
21
+ stdin: { type: "boolean" },
22
+ gate: { type: "boolean" },
23
+ plan: { type: "boolean" },
24
+ scenario: { type: "string" },
25
+ "list-scenarios": { type: "boolean" },
26
+ "min-score": { type: "string" },
27
+ checklist: { type: "boolean" },
28
+ tests: { type: "string" },
29
+ failing: { type: "string" },
30
+ coverage: { type: "string" },
31
+ day: { type: "string" },
32
+ branch: { type: "string" },
33
+ critical: { type: "string" },
34
+ lint: { type: "string" },
35
+ build: { type: "string" },
36
+ dirty: { type: "boolean" },
37
+ },
38
+ });
39
+ if (values.help) {
40
+ console.log(
41
+ "Usage: ship-it-meter [options]\n\nOptions:\n --tests number Total tests\n --failing number Failing tests\n --coverage number Coverage percent\n --build pass|fail Build result\n --critical number Open critical issues\n --lint number Lint failures\n --dirty Uncommitted changes\n --branch name Branch name\n --day weekday Full weekday name\n --scenario name Explore a named evidence scenario\n --list-scenarios List sample scenarios\n --plan Gate plus prioritized tasks and verification criteria\n --checklist Actionable, mildly concerned preflight\n --json Structured output\n -h, --help Help\n -v, --version Version\n\n--stdin reads a JSON evidence object (256 KiB); explicit flags override it. --gate requires passing evidence; --min-score 80 selects its threshold. A blocked gate exits 1. Missing test, coverage, and build evidence reduces readiness.\nExit codes: 0 scored successfully; 2 invalid arguments.",
42
+ );
43
+ } else if (values.version) {
44
+ console.log(
45
+ JSON.parse(
46
+ readFileSync(new URL("../package.json", import.meta.url), "utf8"),
47
+ ).version,
48
+ );
49
+ } else if (values["list-scenarios"]) {
50
+ console.log(
51
+ releaseScenarios()
52
+ .map((item) => item.name + " — " + item.description)
53
+ .join("\n"),
54
+ );
55
+ } else {
56
+ const input = values.stdin
57
+ ? JSON.parse(await readStdin())
58
+ : values.scenario !== undefined
59
+ ? scenarioEvidence(values.scenario)
60
+ : {};
61
+ if (values.stdin && values.scenario !== undefined)
62
+ throw new TypeError("Choose --stdin or --scenario, not both.");
63
+ if ([values.gate, values.plan, values.checklist].filter(Boolean).length > 1)
64
+ throw new TypeError("Choose one of --gate, --plan or --checklist.");
65
+ if (!input || typeof input !== "object" || Array.isArray(input))
66
+ throw new TypeError("stdin must contain an evidence object.");
67
+ if (
68
+ values["min-score"] !== undefined &&
69
+ (!(values.gate || values.plan) ||
70
+ !/^\d+(?:\.\d+)?$/.test(values["min-score"]))
71
+ )
72
+ throw new TypeError(
73
+ "--min-score requires --gate or --plan and a decimal number.",
74
+ );
75
+ for (const [flag, key] of Object.entries({
76
+ tests: "tests",
77
+ failing: "failingTests",
78
+ coverage: "coverage",
79
+ critical: "criticalIssues",
80
+ lint: "lintFailures",
81
+ })) {
82
+ if (values[flag] !== undefined) {
83
+ if (!/^\d+(?:\.\d+)?$/.test(values[flag]))
84
+ throw new TypeError(`${flag} must be a nonnegative decimal number.`);
85
+ input[key] = Number(values[flag]);
86
+ }
87
+ }
88
+ if (values.build !== undefined) {
89
+ if (!["pass", "fail"].includes(values.build))
90
+ throw new RangeError("--build must be pass or fail.");
91
+ input.build = values.build === "pass";
92
+ }
93
+ for (const key of ["day", "branch"])
94
+ if (values[key] !== undefined) input[key] = values[key];
95
+ if (values.dirty) input.uncommittedChanges = true;
96
+ const result =
97
+ values.gate || values.plan
98
+ ? (values.plan ? releasePlan : releaseGate)(input, {
99
+ minScore:
100
+ values["min-score"] === undefined
101
+ ? undefined
102
+ : Number(values["min-score"]),
103
+ })
104
+ : values.checklist
105
+ ? preflight(input)
106
+ : shipIt(input);
107
+ if ((values.gate || values.plan) && !result.passed) process.exitCode = 1;
108
+ if (values.plan && !values.json)
109
+ console.log(
110
+ result.summary +
111
+ "\n" +
112
+ result.tasks
113
+ .map(
114
+ (task) =>
115
+ `[${task.priority}] ${task.title}\n Verify: ${task.verify}`,
116
+ )
117
+ .join("\n") +
118
+ "\n",
119
+ );
120
+ console.log(
121
+ values.json
122
+ ? JSON.stringify(result, null, 2)
123
+ : `${result.score}/100 — ${result.verdict}${values.gate || values.plan ? (result.passed ? " — gate passed" : " — gate blocked") : ""}\n${result.reasons.map((reason) => `- ${reason}`).join("\n")}${result.actions ? "\n\nBefore you ship:\n" + result.actions.map((action) => `[ ] ${action}`).join("\n") : ""}`,
124
+ );
125
+ }
126
+ } catch (error) {
127
+ console.error(`ship-it-meter: ${error.message}\nRun with --help for usage.`);
128
+ process.exitCode = 2;
129
+ }
package/src/index.js CHANGED
@@ -17,6 +17,35 @@ const allowed = [
17
17
  "day",
18
18
  ];
19
19
 
20
+ /** Explicit automation gate; a score alone is never a deployment approval. */
21
+ export function releaseGate(input = {}, options = {}) {
22
+ if (!options || typeof options !== "object" || Array.isArray(options))
23
+ throw new TypeError("Gate options must be an object.");
24
+ const minScore = options.minScore ?? 80;
25
+ if (
26
+ typeof minScore !== "number" ||
27
+ !Number.isFinite(minScore) ||
28
+ minScore < 0 ||
29
+ minScore > 100
30
+ )
31
+ throw new RangeError("minScore must be between 0 and 100.");
32
+ const result = preflight(input);
33
+ const blockers = [];
34
+ if (!input.tests)
35
+ blockers.push("Provide a positive test count. Vibes cannot pass CI.");
36
+ if (input.build !== true) blockers.push("A passing build is required.");
37
+ if (input.coverage === undefined)
38
+ blockers.push("Coverage evidence is required.");
39
+ if (input.failingTests) blockers.push("Failing tests block the gate.");
40
+ if (input.criticalIssues) blockers.push("Critical issues block the gate.");
41
+ if (input.lintFailures) blockers.push("Lint failures block the gate.");
42
+ if (input.uncommittedChanges)
43
+ blockers.push("Uncommitted changes block the gate.");
44
+ if (result.score < minScore)
45
+ blockers.push(`Score ${result.score} is below ${minScore}.`);
46
+ return { ...result, minScore, passed: blockers.length === 0, blockers };
47
+ }
48
+
20
49
  /**
21
50
  * Calculate a stateless readiness score. Missing evidence reduces the score.
22
51
  * @param {{tests?: number, failingTests?: number, coverage?: number, build?: boolean, uncommittedChanges?: boolean, branch?: string, day?: string, criticalIssues?: number, lintFailures?: number}} [input]
@@ -158,3 +187,113 @@ export function preflight(input = {}) {
158
187
  );
159
188
  return { ...result, actions };
160
189
  }
190
+
191
+ export { releaseScenarios, scenarioEvidence } from "./scenarios.js";
192
+ /** Explain the gate as ordered work with observable completion criteria. */
193
+ export function releasePlan(input = {}, options = {}) {
194
+ const gate = releaseGate(input, options);
195
+ const tasks = [];
196
+ const add = (id, priority, title, verify) =>
197
+ tasks.push({ id, priority, title, verify });
198
+ if (input.build !== true)
199
+ add(
200
+ "build",
201
+ "blocker",
202
+ "Get a passing build",
203
+ "Run the release build for the exact commit and record its successful result.",
204
+ );
205
+ if (!input.tests)
206
+ add(
207
+ "tests",
208
+ "blocker",
209
+ "Run a nonempty test suite",
210
+ "Record the test count and outcome for the release commit.",
211
+ );
212
+ if (input.failingTests)
213
+ add(
214
+ "failing-tests",
215
+ "blocker",
216
+ "Repair failing tests",
217
+ "Reproduce each failure, fix its cause and rerun the suite.",
218
+ );
219
+ if (input.coverage === undefined)
220
+ add(
221
+ "coverage-evidence",
222
+ "blocker",
223
+ "Measure coverage",
224
+ "Generate coverage for the release commit and record the percentage.",
225
+ );
226
+ else if (input.coverage < 80)
227
+ add(
228
+ "coverage-review",
229
+ "review",
230
+ "Review untested paths",
231
+ "Inspect uncovered critical paths and add meaningful tests where needed.",
232
+ );
233
+ if (input.criticalIssues)
234
+ add(
235
+ "critical-issues",
236
+ "blocker",
237
+ "Resolve critical issues",
238
+ "Record the resolution and verify the affected behavior.",
239
+ );
240
+ if (input.lintFailures)
241
+ add(
242
+ "lint",
243
+ "blocker",
244
+ "Fix lint failures",
245
+ "Run the configured lint checks successfully.",
246
+ );
247
+ if (input.uncommittedChanges)
248
+ add(
249
+ "worktree",
250
+ "blocker",
251
+ "Reconcile local changes",
252
+ "Commit or stash intentionally and verify the exact release source.",
253
+ );
254
+ if (input.branch && !["main", "master"].includes(input.branch.trim()))
255
+ add(
256
+ "branch",
257
+ "review",
258
+ "Confirm release branch",
259
+ "Verify that this branch is the intended release source and that its CI passed.",
260
+ );
261
+ if (input.day?.trim().toLowerCase() === "friday")
262
+ add(
263
+ "friday",
264
+ "review",
265
+ "Confirm weekend support",
266
+ "Identify monitoring ownership and a tested rollback path.",
267
+ );
268
+ if (gate.score < gate.minScore)
269
+ add(
270
+ "score",
271
+ "blocker",
272
+ "Improve evidence to the configured threshold",
273
+ "Address score deductions, rerun the checks and reevaluate the gate.",
274
+ );
275
+ add(
276
+ "rollback",
277
+ "release",
278
+ "Prepare the return route",
279
+ "Verify the rollback artifact and procedure before deployment.",
280
+ );
281
+ add(
282
+ "monitoring",
283
+ "release",
284
+ "Watch the actual release",
285
+ "Check health and error metrics after deployment and name an owner.",
286
+ );
287
+ const summaries = {
288
+ "Absolutely Not": "The evidence says stop. The confidence hat can wait.",
289
+ Questionable:
290
+ "The release has questions. Answer them before adding confetti.",
291
+ "Probably Fine": "Close is a useful direction, not a deployment receipt.",
292
+ "Ship It": "Good evidence. Keep the parachute and watch the landing.",
293
+ "Suspiciously Ready":
294
+ "Boring, verified readiness. Give the checks their share of the applause.",
295
+ };
296
+ const rank = { blocker: 0, review: 1, release: 2 };
297
+ tasks.sort((a, b) => rank[a.priority] - rank[b.priority]);
298
+ return { ...gate, summary: summaries[gate.verdict], tasks };
299
+ }
package/src/input.js ADDED
@@ -0,0 +1,15 @@
1
+ /** Read bounded UTF-8 stdin without swallowing upstream failures. */
2
+ export async function readStdin(stream = process.stdin) {
3
+ if (stream.isTTY) throw new TypeError("Pipe or redirect input.");
4
+ let size = 0;
5
+ const chunks = [];
6
+ for await (const chunk of stream) {
7
+ const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
8
+ size += buffer.length;
9
+ if (size > 262144) throw new RangeError("stdin exceeds 256 KiB.");
10
+ chunks.push(buffer);
11
+ }
12
+ const text = Buffer.concat(chunks).toString("utf8").trim();
13
+ if (!text) throw new TypeError("Provide nonempty input.");
14
+ return text;
15
+ }
@@ -0,0 +1,133 @@
1
+ // Demonstration inputs, never a claim about a real release.
2
+ const scenarios = {
3
+ ready: {
4
+ description:
5
+ "Passing evidence, clean worktree and main branch. Boring is a compliment.",
6
+ evidence: {
7
+ tests: 125,
8
+ coverage: 96,
9
+ build: true,
10
+ branch: "main",
11
+ day: "tuesday",
12
+ },
13
+ },
14
+ friday: {
15
+ description:
16
+ "Good evidence on Friday. Confirm rollback and on-call before the victory lap.",
17
+ evidence: {
18
+ tests: 125,
19
+ coverage: 92,
20
+ build: true,
21
+ branch: "main",
22
+ day: "friday",
23
+ },
24
+ },
25
+ "no-evidence": {
26
+ description:
27
+ "Confidence is present. Test, coverage and build evidence have not arrived.",
28
+ evidence: {},
29
+ },
30
+ "failed-build": {
31
+ description:
32
+ "The build is red. The deploy button is not a color correction tool.",
33
+ evidence: {
34
+ tests: 125,
35
+ coverage: 92,
36
+ build: false,
37
+ },
38
+ },
39
+ "failing-tests": {
40
+ description:
41
+ "Some tests fail. Green deployment lights will not repair assertions.",
42
+ evidence: {
43
+ tests: 125,
44
+ failingTests: 3,
45
+ coverage: 92,
46
+ build: true,
47
+ },
48
+ },
49
+ "low-coverage": {
50
+ description:
51
+ "A passing build with many untested paths. The bugs appreciate the privacy.",
52
+ evidence: {
53
+ tests: 125,
54
+ coverage: 45,
55
+ build: true,
56
+ },
57
+ },
58
+ "critical-issue": {
59
+ description:
60
+ "A critical issue blocks the gate even when everything else looks cheerful.",
61
+ evidence: {
62
+ tests: 125,
63
+ coverage: 96,
64
+ build: true,
65
+ criticalIssues: 1,
66
+ },
67
+ },
68
+ "lint-failures": {
69
+ description:
70
+ "Lint failures are still failures, however decorative the semicolons feel.",
71
+ evidence: {
72
+ tests: 125,
73
+ coverage: 96,
74
+ build: true,
75
+ lintFailures: 2,
76
+ },
77
+ },
78
+ "dirty-worktree": {
79
+ description:
80
+ "The laptop contains uncommitted changes. It is not the release archive.",
81
+ evidence: {
82
+ tests: 125,
83
+ coverage: 96,
84
+ build: true,
85
+ uncommittedChanges: true,
86
+ },
87
+ },
88
+ "feature-branch": {
89
+ description:
90
+ "Healthy evidence on a feature branch. Verify the intended release source.",
91
+ evidence: {
92
+ tests: 125,
93
+ coverage: 96,
94
+ build: true,
95
+ branch: "feature/zoomies",
96
+ },
97
+ },
98
+ "zero-tests": {
99
+ description:
100
+ "No tests ran. Zero failures is an especially small achievement here.",
101
+ evidence: {
102
+ tests: 0,
103
+ coverage: 0,
104
+ build: true,
105
+ },
106
+ },
107
+ "weekend-incident": {
108
+ description:
109
+ "A failed build, failing tests and a critical issue. Bring evidence, not a bigger confidence hat.",
110
+ evidence: {
111
+ tests: 125,
112
+ failingTests: 12,
113
+ coverage: 60,
114
+ build: false,
115
+ criticalIssues: 1,
116
+ day: "saturday",
117
+ },
118
+ },
119
+ };
120
+ export function releaseScenarios() {
121
+ return Object.entries(scenarios).map(([name, value]) => ({
122
+ name,
123
+ description: value.description,
124
+ evidence: { ...value.evidence },
125
+ }));
126
+ }
127
+ export function scenarioEvidence(name) {
128
+ if (typeof name !== "string" || !Object.hasOwn(scenarios, name))
129
+ throw new RangeError(
130
+ "Unknown scenario. Choose: " + Object.keys(scenarios).join(", "),
131
+ );
132
+ return { ...scenarios[name].evidence };
133
+ }