@deployanyway/ship-it-meter 0.3.0 → 1.0.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
+ ## 1.0.0
4
+
5
+ - Show Your Receipts: release policies from actual reports.
6
+ - Typed API, CLI integration, runnable codebase example and meaningful workflow tests.
7
+ - Stable contracts and migration guidance; original humor stays around accurate facts.
8
+
9
+ ## 0.4.0
10
+
11
+ 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.
12
+
13
+ ```sh
14
+
15
+
3
16
  ## 0.3.0 — 2026-10-08
4
17
 
5
18
  - Useful structured API/CLI additions described in README.
@@ -21,3 +34,4 @@
21
34
 
22
35
  - Deterministic readiness score with explanations and technical blocker caps.
23
36
  - CLI, documented scoring rules, tests, linting, formatting, and Node 22/24 CI.
37
+ ```
package/MIGRATION.md CHANGED
@@ -3,3 +3,15 @@
3
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
4
 
5
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.
12
+
13
+ ## 0.4.0 to stable 1.0.0
14
+
15
+ Report APIs and CLI receipt mode are additive. Existing supplied-evidence scoring remains a heuristic and named scenarios remain examples. Use report policies for actual CI gating; do not confuse demonstration scenarios with measured artifacts.
16
+
17
+ See README for exact contracts, bounds and failure behavior.
package/README.md CHANGED
@@ -1,6 +1,79 @@
1
1
  # ship-it-meter
2
2
 
3
- > **Version 0.3.0:** install from npm with Node 22.13+ or Node 24. See MIGRATION.md for changes from 0.2.0.
3
+ ## Show Your Receipts: release policies from actual reports (1.0.0)
4
+
5
+ evaluateReports(bundle, policy) turns actual test, coverage and build receipts into a release gate and prioritized repair tasks. parseTestReport supports completed Node TAP v13 and Jest JSON. parseCoverageReport supports c8/Istanbul coverage-summary.json. Skipped/todo tests are excluded from executed evidence; failed/cancelled tests, unsuccessful Jest suites and collected-only/interrupted runs do not count as successful execution. Coverage counts must reconcile; empty lines/statements are not measured evidence.
6
+
7
+ ```js
8
+ import { evaluateReports } from "@deployanyway/ship-it-meter";
9
+ const result = evaluateReports(
10
+ {
11
+ commit: "abc123",
12
+ tests: {
13
+ commit: "abc123",
14
+ capturedAt: "2026-10-09T02:00:00Z",
15
+ format: "node-tap",
16
+ data: tapOutput,
17
+ },
18
+ coverage: {
19
+ commit: "abc123",
20
+ capturedAt: "2026-10-09T02:00:00Z",
21
+ format: "istanbul-summary",
22
+ data: coverageSummary,
23
+ },
24
+ build: {
25
+ commit: "abc123",
26
+ capturedAt: "2026-10-09T02:00:00Z",
27
+ exitCode: buildExitCode,
28
+ },
29
+ },
30
+ { minTests: 1, minCoverage: 80, minScore: 80, maxAgeMs: 3600000 },
31
+ );
32
+ if (!result.passed) console.error(result.blockers);
33
+ ```
34
+
35
+ All three receipts are required, must identify the same nonempty bundle commit, and use valid UTC ISO capturedAt timestamps. Future/stale receipts block. Default policy: at least one executed test, all four coverage percentages >=80, passing build and score >=80, max age one hour. now can be supplied for deterministic evaluation. Unknown policy keys throw, so misspelled thresholds do not silently apply defaults. The policy is configurable; it does not alter your artifacts.
36
+
37
+ Result adds evidence, receipts, commit, policy and evaluatedAt to a release plan. Each receipt exposes acceptance and issues; rejected receipts add blocker tasks with verification criteria. The final summary/passed value follows receipt blockers, even when the heuristic score looks good. Parse helpers throw on malformed reports; evaluateReports converts report problems into blockers. Invalid policy/bundle configuration throws.
38
+
39
+ ```sh
40
+ ship-it-meter --report-file receipts.json --policy '{"minCoverage":90,"maxAgeMs":1800000}' --json
41
+ cat receipts.json | ship-it-meter --reports --json
42
+ node node_modules/@deployanyway/ship-it-meter/examples/report-gate.mjs receipts.json
43
+ ```
44
+
45
+ Report CLI input is bounded to 10 MiB and exits 1 when blocked, 2 for invalid arguments/configuration. Receipt mode rejects manual evidence overrides and conflicting gate/scenario flags. The browser demo uses pure parsers; file reading and CI collection happen in Node. Metadata is a claim from your trusted pipeline, not a cryptographic attestation; do not relabel old reports or trust arbitrary submitted bundles. The gate neither runs your CI nor deploys anything. See examples/collect-receipts.mjs for collecting new Node test and c8 coverage reports together, followed by your actual npm build, inside an npm script. It requires c8 as a project dev dependency, Node-discoverable tests (or explicit test file arguments), npm run build, an unchanged Git commit and clean tracked source. Generated/untracked files must be excluded from your test configuration; receipt metadata remains a trusted pipeline claim.
46
+
47
+ ## Stable v1 contract
48
+
49
+ Node 22.13+ or Node 24. MIT licensed. CLI flags, structured fields, ESM/CommonJS exports and declarations are covered by tests and installed-package checks. Existing 0.4 APIs remain available except the explicitly documented doggo-log redaction/text-context changes. Future incompatible public API changes require a major release; callers should consume structured fields rather than parse jokes. Exact humorous wording and seeded catalog choices are version-specific. No telemetry, external API keys or network service is needed for core use.
50
+
51
+ Run npm test, npm run lint, npm run format:check, npm run coverage, npm run test:types and npm run verify:package from a source checkout. Runnable examples are shipped under examples/. The root demo is https://deployanyway.github.io/.
52
+
53
+ ## Evidence into an actual release plan (0.4.0)
54
+
55
+ 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.
56
+
57
+ ```sh
58
+ npx @deployanyway/ship-it-meter --list-scenarios
59
+ npx @deployanyway/ship-it-meter --scenario failed-build --plan
60
+ npx @deployanyway/ship-it-meter --scenario ready --plan --json
61
+ ```
62
+
63
+ ```js
64
+ import {
65
+ releasePlan,
66
+ releaseScenarios,
67
+ scenarioEvidence,
68
+ } from "@deployanyway/ship-it-meter";
69
+ console.log(releaseScenarios());
70
+ const plan = releasePlan(scenarioEvidence("failing-tests"), { minScore: 80 });
71
+ console.log(plan.passed, plan.summary, plan.tasks);
72
+ ```
73
+
74
+ `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.
75
+
76
+ > **Version 0.4.0:** install from npm with Node 22.13+ or Node 24. See MIGRATION.md for changes from 0.2.0.
4
77
 
5
78
  [![npm version](https://img.shields.io/npm/v/%40deployanyway%2Fship-it-meter)](https://www.npmjs.com/package/@deployanyway/ship-it-meter)
6
79
  [![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)
package/dist/index.cjs CHANGED
@@ -20,11 +20,349 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
20
20
  // src/index.js
21
21
  var index_exports = {};
22
22
  __export(index_exports, {
23
+ evaluateReports: () => evaluateReports,
24
+ parseCoverageReport: () => parseCoverageReport,
25
+ parseTestReport: () => parseTestReport,
23
26
  preflight: () => preflight,
24
27
  releaseGate: () => releaseGate,
28
+ releasePlan: () => releasePlan,
29
+ releaseScenarios: () => releaseScenarios,
30
+ scenarioEvidence: () => scenarioEvidence,
25
31
  shipIt: () => shipIt
26
32
  });
27
33
  module.exports = __toCommonJS(index_exports);
34
+
35
+ // src/scenarios.js
36
+ var scenarios = {
37
+ ready: {
38
+ description: "Passing evidence, clean worktree and main branch. Boring is a compliment.",
39
+ evidence: {
40
+ tests: 125,
41
+ coverage: 96,
42
+ build: true,
43
+ branch: "main",
44
+ day: "tuesday"
45
+ }
46
+ },
47
+ friday: {
48
+ description: "Good evidence on Friday. Confirm rollback and on-call before the victory lap.",
49
+ evidence: {
50
+ tests: 125,
51
+ coverage: 92,
52
+ build: true,
53
+ branch: "main",
54
+ day: "friday"
55
+ }
56
+ },
57
+ "no-evidence": {
58
+ description: "Confidence is present. Test, coverage and build evidence have not arrived.",
59
+ evidence: {}
60
+ },
61
+ "failed-build": {
62
+ description: "The build is red. The deploy button is not a color correction tool.",
63
+ evidence: {
64
+ tests: 125,
65
+ coverage: 92,
66
+ build: false
67
+ }
68
+ },
69
+ "failing-tests": {
70
+ description: "Some tests fail. Green deployment lights will not repair assertions.",
71
+ evidence: {
72
+ tests: 125,
73
+ failingTests: 3,
74
+ coverage: 92,
75
+ build: true
76
+ }
77
+ },
78
+ "low-coverage": {
79
+ description: "A passing build with many untested paths. The bugs appreciate the privacy.",
80
+ evidence: {
81
+ tests: 125,
82
+ coverage: 45,
83
+ build: true
84
+ }
85
+ },
86
+ "critical-issue": {
87
+ description: "A critical issue blocks the gate even when everything else looks cheerful.",
88
+ evidence: {
89
+ tests: 125,
90
+ coverage: 96,
91
+ build: true,
92
+ criticalIssues: 1
93
+ }
94
+ },
95
+ "lint-failures": {
96
+ description: "Lint failures are still failures, however decorative the semicolons feel.",
97
+ evidence: {
98
+ tests: 125,
99
+ coverage: 96,
100
+ build: true,
101
+ lintFailures: 2
102
+ }
103
+ },
104
+ "dirty-worktree": {
105
+ description: "The laptop contains uncommitted changes. It is not the release archive.",
106
+ evidence: {
107
+ tests: 125,
108
+ coverage: 96,
109
+ build: true,
110
+ uncommittedChanges: true
111
+ }
112
+ },
113
+ "feature-branch": {
114
+ description: "Healthy evidence on a feature branch. Verify the intended release source.",
115
+ evidence: {
116
+ tests: 125,
117
+ coverage: 96,
118
+ build: true,
119
+ branch: "feature/zoomies"
120
+ }
121
+ },
122
+ "zero-tests": {
123
+ description: "No tests ran. Zero failures is an especially small achievement here.",
124
+ evidence: {
125
+ tests: 0,
126
+ coverage: 0,
127
+ build: true
128
+ }
129
+ },
130
+ "weekend-incident": {
131
+ description: "A failed build, failing tests and a critical issue. Bring evidence, not a bigger confidence hat.",
132
+ evidence: {
133
+ tests: 125,
134
+ failingTests: 12,
135
+ coverage: 60,
136
+ build: false,
137
+ criticalIssues: 1,
138
+ day: "saturday"
139
+ }
140
+ }
141
+ };
142
+ function releaseScenarios() {
143
+ return Object.entries(scenarios).map(([name, value]) => ({
144
+ name,
145
+ description: value.description,
146
+ evidence: { ...value.evidence }
147
+ }));
148
+ }
149
+ function scenarioEvidence(name) {
150
+ if (typeof name !== "string" || !Object.hasOwn(scenarios, name))
151
+ throw new RangeError(
152
+ "Unknown scenario. Choose: " + Object.keys(scenarios).join(", ")
153
+ );
154
+ return { ...scenarios[name].evidence };
155
+ }
156
+
157
+ // src/reports.js
158
+ var object = (value, label) => {
159
+ if (!value || typeof value !== "object" || Array.isArray(value))
160
+ throw new TypeError(`${label} must be an object.`);
161
+ return value;
162
+ };
163
+ var count = (value, label) => {
164
+ if (!Number.isSafeInteger(value) || value < 0)
165
+ throw new TypeError(`${label} must be a nonnegative safe integer.`);
166
+ return value;
167
+ };
168
+ var json = (value) => typeof value === "string" ? JSON.parse(value) : value;
169
+ var date = (value, label) => {
170
+ if (typeof value !== "string" || !/^\d{4}-\d\d-\d\dT\d\d:\d\d:\d\d(?:\.\d{1,3})?Z$/.test(value) || !Number.isFinite(Date.parse(value)) || new Date(value).toISOString().slice(0, 19) !== value.slice(0, 19))
171
+ throw new TypeError(`${label} must be a UTC ISO timestamp.`);
172
+ return Date.parse(value);
173
+ };
174
+ function parseTestReport(data, format = "node-tap") {
175
+ if (format === "node-tap") {
176
+ if (typeof data !== "string" || !data.startsWith("TAP version 13") || /^Bail out!/m.test(data))
177
+ throw new TypeError("Expected a completed Node TAP v13 report.");
178
+ const values = {};
179
+ for (const key of [
180
+ "tests",
181
+ "pass",
182
+ "fail",
183
+ "cancelled",
184
+ "skipped",
185
+ "todo"
186
+ ]) {
187
+ const matches = [
188
+ ...data.matchAll(new RegExp(`^# ${key} (\\d+)\\r?$`, "gm"))
189
+ ];
190
+ if (matches.length !== 1)
191
+ throw new TypeError(`TAP requires one ${key} summary.`);
192
+ values[key] = count(Number(matches[0][1]), key);
193
+ }
194
+ if (values.pass + values.fail + values.cancelled + values.skipped + values.todo !== values.tests)
195
+ throw new TypeError("TAP counts do not reconcile.");
196
+ return {
197
+ tests: values.tests - values.skipped - values.todo,
198
+ passedTests: values.pass,
199
+ failingTests: values.fail + values.cancelled,
200
+ skippedTests: values.skipped,
201
+ todoTests: values.todo,
202
+ successful: values.fail + values.cancelled === 0
203
+ };
204
+ }
205
+ if (format === "jest") {
206
+ const report = object(json(data), "Jest report");
207
+ const total = count(report.numTotalTests, "numTotalTests"), passed = count(report.numPassedTests, "numPassedTests"), failed = count(report.numFailedTests, "numFailedTests"), pending = count(report.numPendingTests, "numPendingTests"), todo = count(report.numTodoTests ?? 0, "numTodoTests");
208
+ if (total !== passed + failed + pending + todo || typeof report.success !== "boolean")
209
+ throw new TypeError("Jest counts/success do not reconcile.");
210
+ if (report.wasInterrupted || report.testResults?.some(
211
+ (result) => result.assertionResults?.some(
212
+ (assertion) => assertion.wouldRun === true
213
+ )
214
+ ))
215
+ throw new TypeError(
216
+ "Interrupted or collected-only Jest output is not executed evidence."
217
+ );
218
+ const failedSuites = count(
219
+ report.numFailedTestSuites ?? 0,
220
+ "numFailedTestSuites"
221
+ );
222
+ return {
223
+ tests: total - pending - todo,
224
+ passedTests: passed,
225
+ failingTests: failed,
226
+ skippedTests: pending,
227
+ todoTests: todo,
228
+ successful: report.success && failed === 0 && failedSuites === 0
229
+ };
230
+ }
231
+ throw new RangeError("Supported test formats: node-tap, jest.");
232
+ }
233
+ function parseCoverageReport(data) {
234
+ const total = object(
235
+ object(json(data), "Coverage report").total,
236
+ "Coverage total"
237
+ ), result = {};
238
+ for (const key of ["lines", "statements", "functions", "branches"]) {
239
+ const metric = object(total[key], `Coverage ${key}`), all = count(metric.total, `${key}.total`), covered = count(metric.covered, `${key}.covered`);
240
+ if (all === 0 && ["lines", "statements"].includes(key) || covered > all || typeof metric.pct !== "number" || !Number.isFinite(metric.pct) || metric.pct < 0 || metric.pct > 100)
241
+ throw new TypeError(
242
+ `Coverage ${key} must contain measured code and a percentage.`
243
+ );
244
+ const computed = all === 0 ? 100 : covered / all * 100;
245
+ if (Math.abs(computed - metric.pct) > 0.02)
246
+ throw new TypeError(`Coverage ${key} percentage disagrees with counts.`);
247
+ result[key] = metric.pct;
248
+ }
249
+ return result;
250
+ }
251
+ function evaluateReports(bundle, options = {}) {
252
+ object(bundle, "Report bundle");
253
+ object(options, "Policy");
254
+ for (const key of Object.keys(options))
255
+ if (!["minTests", "minCoverage", "minScore", "maxAgeMs", "now"].includes(key))
256
+ throw new TypeError(`Unknown policy option: ${key}.`);
257
+ if (typeof bundle.commit !== "string" || !bundle.commit.trim())
258
+ throw new TypeError("Report bundle requires a nonempty commit.");
259
+ const policy = {
260
+ minTests: 1,
261
+ minCoverage: 80,
262
+ minScore: 80,
263
+ maxAgeMs: 36e5,
264
+ ...options
265
+ };
266
+ for (const [key, value] of Object.entries({
267
+ minTests: 1,
268
+ minCoverage: 80,
269
+ minScore: 80,
270
+ maxAgeMs: 36e5
271
+ }))
272
+ if (policy[key] === void 0) policy[key] = value;
273
+ count(policy.minTests, "minTests");
274
+ if (policy.minTests < 1) throw new RangeError("minTests must be positive.");
275
+ for (const key of ["minCoverage", "minScore"])
276
+ if (typeof policy[key] !== "number" || !Number.isFinite(policy[key]) || policy[key] < 0 || policy[key] > 100)
277
+ throw new RangeError(`${key} must be from 0 to 100.`);
278
+ if (!Number.isSafeInteger(policy.maxAgeMs) || policy.maxAgeMs < 1)
279
+ throw new RangeError("maxAgeMs must be a positive safe integer.");
280
+ const now = options.now ?? (/* @__PURE__ */ new Date()).toISOString();
281
+ const nowMs = date(now, "now");
282
+ const evidence = {}, receipts = [], blockers = [];
283
+ for (const kind of ["tests", "coverage", "build"]) {
284
+ const issues = [], report = bundle[kind];
285
+ if (!report) {
286
+ issues.push(`${kind}: report is missing.`);
287
+ } else
288
+ try {
289
+ object(report, kind);
290
+ const captured = date(report.capturedAt, `${kind}.capturedAt`);
291
+ if (report.commit !== bundle.commit)
292
+ issues.push(`${kind}: commit does not match ${bundle.commit}.`);
293
+ if (captured > nowMs)
294
+ issues.push(`${kind}: timestamp is in the future.`);
295
+ if (nowMs - captured > policy.maxAgeMs)
296
+ issues.push(`${kind}: report is stale.`);
297
+ if (kind === "tests") {
298
+ if (!["node-tap", "jest"].includes(report.format))
299
+ throw new RangeError("Test format must be node-tap or jest.");
300
+ const parsed = parseTestReport(report.data, report.format);
301
+ evidence.tests = parsed.tests;
302
+ evidence.failingTests = parsed.failingTests;
303
+ if (!parsed.successful)
304
+ issues.push("tests: test execution was unsuccessful.");
305
+ if (parsed.tests < policy.minTests)
306
+ issues.push(
307
+ `tests: ${parsed.tests} executed tests is below ${policy.minTests}.`
308
+ );
309
+ } else if (kind === "coverage") {
310
+ if (report.format !== "istanbul-summary")
311
+ throw new RangeError("Coverage format must be istanbul-summary.");
312
+ const coverage = parseCoverageReport(report.data);
313
+ evidence.coverage = coverage.lines;
314
+ for (const [metric, value] of Object.entries(coverage))
315
+ if (value < policy.minCoverage)
316
+ issues.push(
317
+ `coverage: ${metric} ${value}% is below ${policy.minCoverage}%.`
318
+ );
319
+ } else {
320
+ count(report.exitCode, "build.exitCode");
321
+ evidence.build = report.exitCode === 0;
322
+ if (report.exitCode !== 0)
323
+ issues.push(`build: command exited ${report.exitCode}.`);
324
+ }
325
+ } catch (error) {
326
+ issues.push(`${kind}: ${error.message}`);
327
+ }
328
+ receipts.push({
329
+ kind,
330
+ format: kind === "build" ? "exit-code" : report?.format ?? "missing",
331
+ commit: typeof report?.commit === "string" ? report.commit : null,
332
+ capturedAt: typeof report?.capturedAt === "string" ? report.capturedAt : null,
333
+ accepted: issues.length === 0,
334
+ issues
335
+ });
336
+ blockers.push(...issues);
337
+ }
338
+ const plan = releasePlan(evidence, { minScore: policy.minScore });
339
+ const combined = [.../* @__PURE__ */ new Set([...blockers, ...plan.blockers])];
340
+ const receiptTasks = receipts.filter((item) => !item.accepted).map((item) => ({
341
+ id: "receipt-" + item.kind,
342
+ priority: "blocker",
343
+ title: "Repair " + item.kind + " evidence",
344
+ verify: `Generate a completed ${item.kind} report for ${bundle.commit} with a current UTC timestamp; resolve: ${item.issues.join(" ")}`
345
+ }));
346
+ return {
347
+ ...plan,
348
+ summary: combined.length ? "The receipts say stop. Confidence must wait." : plan.summary,
349
+ tasks: [...receiptTasks, ...plan.tasks],
350
+ passed: combined.length === 0,
351
+ blockers: combined,
352
+ evidence,
353
+ receipts,
354
+ commit: bundle.commit,
355
+ policy: {
356
+ minTests: policy.minTests,
357
+ minCoverage: policy.minCoverage,
358
+ minScore: policy.minScore,
359
+ maxAgeMs: policy.maxAgeMs
360
+ },
361
+ evaluatedAt: now
362
+ };
363
+ }
364
+
365
+ // src/index.js
28
366
  var days = [
29
367
  "monday",
30
368
  "tuesday",
@@ -175,9 +513,119 @@ function preflight(input = {}) {
175
513
  );
176
514
  return { ...result, actions };
177
515
  }
516
+ function releasePlan(input = {}, options = {}) {
517
+ const gate = releaseGate(input, options);
518
+ const tasks = [];
519
+ const add = (id, priority, title, verify) => tasks.push({ id, priority, title, verify });
520
+ if (input.build !== true)
521
+ add(
522
+ "build",
523
+ "blocker",
524
+ "Get a passing build",
525
+ "Run the release build for the exact commit and record its successful result."
526
+ );
527
+ if (!input.tests)
528
+ add(
529
+ "tests",
530
+ "blocker",
531
+ "Run a nonempty test suite",
532
+ "Record the test count and outcome for the release commit."
533
+ );
534
+ if (input.failingTests)
535
+ add(
536
+ "failing-tests",
537
+ "blocker",
538
+ "Repair failing tests",
539
+ "Reproduce each failure, fix its cause and rerun the suite."
540
+ );
541
+ if (input.coverage === void 0)
542
+ add(
543
+ "coverage-evidence",
544
+ "blocker",
545
+ "Measure coverage",
546
+ "Generate coverage for the release commit and record the percentage."
547
+ );
548
+ else if (input.coverage < 80)
549
+ add(
550
+ "coverage-review",
551
+ "review",
552
+ "Review untested paths",
553
+ "Inspect uncovered critical paths and add meaningful tests where needed."
554
+ );
555
+ if (input.criticalIssues)
556
+ add(
557
+ "critical-issues",
558
+ "blocker",
559
+ "Resolve critical issues",
560
+ "Record the resolution and verify the affected behavior."
561
+ );
562
+ if (input.lintFailures)
563
+ add(
564
+ "lint",
565
+ "blocker",
566
+ "Fix lint failures",
567
+ "Run the configured lint checks successfully."
568
+ );
569
+ if (input.uncommittedChanges)
570
+ add(
571
+ "worktree",
572
+ "blocker",
573
+ "Reconcile local changes",
574
+ "Commit or stash intentionally and verify the exact release source."
575
+ );
576
+ if (input.branch && !["main", "master"].includes(input.branch.trim()))
577
+ add(
578
+ "branch",
579
+ "review",
580
+ "Confirm release branch",
581
+ "Verify that this branch is the intended release source and that its CI passed."
582
+ );
583
+ if (input.day?.trim().toLowerCase() === "friday")
584
+ add(
585
+ "friday",
586
+ "review",
587
+ "Confirm weekend support",
588
+ "Identify monitoring ownership and a tested rollback path."
589
+ );
590
+ if (gate.score < gate.minScore)
591
+ add(
592
+ "score",
593
+ "blocker",
594
+ "Improve evidence to the configured threshold",
595
+ "Address score deductions, rerun the checks and reevaluate the gate."
596
+ );
597
+ add(
598
+ "rollback",
599
+ "release",
600
+ "Prepare the return route",
601
+ "Verify the rollback artifact and procedure before deployment."
602
+ );
603
+ add(
604
+ "monitoring",
605
+ "release",
606
+ "Watch the actual release",
607
+ "Check health and error metrics after deployment and name an owner."
608
+ );
609
+ const summaries = {
610
+ "Absolutely Not": "The evidence says stop. The confidence hat can wait.",
611
+ Questionable: "The release has questions. Answer them before adding confetti.",
612
+ "Probably Fine": "Close is a useful direction, not a deployment receipt.",
613
+ "Ship It": "Good evidence. Keep the parachute and watch the landing.",
614
+ "Suspiciously Ready": "Boring, verified readiness. Give the checks their share of the applause."
615
+ };
616
+ const rank = { blocker: 0, review: 1, release: 2 };
617
+ tasks.sort((a, b) => rank[a.priority] - rank[b.priority]);
618
+ return { ...gate, summary: summaries[gate.verdict], tasks };
619
+ }
178
620
  // Annotate the CommonJS export names for ESM import in node:
179
621
  0 && (module.exports = {
622
+ evaluateReports,
623
+ parseCoverageReport,
624
+ parseTestReport,
180
625
  preflight,
181
626
  releaseGate,
627
+ releasePlan,
628
+ releaseScenarios,
629
+ scenarioEvidence,
182
630
  shipIt
183
631
  });
@@ -0,0 +1,12 @@
1
+ import { shipIt } from "@deployanyway/ship-it-meter";
2
+
3
+ console.log(
4
+ shipIt({
5
+ tests: 125,
6
+ failingTests: 2,
7
+ coverage: 82,
8
+ day: "Friday",
9
+ branch: "main",
10
+ build: true,
11
+ }),
12
+ );
@@ -0,0 +1,85 @@
1
+ import { spawnSync } from "node:child_process";
2
+ import { readFileSync, writeFileSync, mkdtempSync, rmSync } from "node:fs";
3
+ import { join, resolve } from "node:path";
4
+ import { tmpdir } from "node:os";
5
+ import { evaluateReports } from "@deployanyway/ship-it-meter";
6
+ // Configure in your project: "release:receipts": "node node_modules/@deployanyway/ship-it-meter/examples/collect-receipts.mjs"
7
+ // Requires your project's c8 dev dependency, Node tests using Node test discovery, and npm run build.
8
+ const run = (args) =>
9
+ spawnSync(process.execPath, args, {
10
+ encoding: "utf8",
11
+ maxBuffer: 10 * 1024 * 1024,
12
+ });
13
+ const git = (args) => spawnSync("git", args, { encoding: "utf8" });
14
+ let temp;
15
+ try {
16
+ if (!process.env.npm_execpath)
17
+ throw new Error(
18
+ "Run through an npm script so the build uses the current npm CLI.",
19
+ );
20
+ const head = git(["rev-parse", "HEAD"]);
21
+ if (head.status !== 0)
22
+ throw new Error("Cannot identify the exact Git commit.");
23
+ const commit = head.stdout.trim();
24
+ const clean = () => {
25
+ const status = git(["status", "--porcelain", "--untracked-files=no"]);
26
+ if (status.status !== 0 || status.stdout.trim())
27
+ throw new Error(
28
+ "Tracked source changes must be committed before collecting receipts.",
29
+ );
30
+ };
31
+ clean();
32
+ temp = mkdtempSync(join(tmpdir(), "deployanyway-receipts-"));
33
+ const tests = run([
34
+ resolve("node_modules/c8/bin/c8.js"),
35
+ "--reporter=json-summary",
36
+ "--reports-dir",
37
+ temp,
38
+ "--temp-directory",
39
+ join(temp, "v8"),
40
+ process.execPath,
41
+ "--test",
42
+ "--test-reporter=tap",
43
+ ...process.argv.slice(2),
44
+ ]);
45
+ if (tests.error) throw tests.error;
46
+ const testTime = new Date().toISOString(),
47
+ coverage = JSON.parse(
48
+ readFileSync(join(temp, "coverage-summary.json"), "utf8"),
49
+ );
50
+ const build = run([process.env.npm_execpath, "run", "build"]);
51
+ if (build.error) throw build.error;
52
+ const after = git(["rev-parse", "HEAD"]);
53
+ if (after.status !== 0 || after.stdout.trim() !== commit)
54
+ throw new Error("Commit changed during collection.");
55
+ clean();
56
+ const bundle = {
57
+ commit,
58
+ tests: {
59
+ commit,
60
+ capturedAt: testTime,
61
+ format: "node-tap",
62
+ data: tests.stdout,
63
+ },
64
+ coverage: {
65
+ commit,
66
+ capturedAt: testTime,
67
+ format: "istanbul-summary",
68
+ data: coverage,
69
+ },
70
+ build: {
71
+ commit,
72
+ capturedAt: new Date().toISOString(),
73
+ exitCode: build.status,
74
+ },
75
+ };
76
+ writeFileSync("receipts.json", JSON.stringify(bundle, null, 2));
77
+ const result = evaluateReports(bundle);
78
+ console.log(JSON.stringify(result, null, 2));
79
+ process.exitCode = result.passed ? 0 : 1;
80
+ } catch (error) {
81
+ console.error(error.message);
82
+ process.exitCode = 2;
83
+ } finally {
84
+ if (temp) rmSync(temp, { recursive: true, force: true });
85
+ }