@deployanyway/ship-it-meter 0.3.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 +8 -0
- package/MIGRATION.md +6 -0
- package/README.md +24 -1
- package/dist/index.cjs +234 -0
- package/index.d.cts +22 -0
- package/index.d.ts +22 -0
- package/package.json +1 -1
- package/src/cli.js +57 -17
- package/src/index.js +110 -0
- package/src/scenarios.js +133 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
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
|
+
|
|
3
10
|
## 0.3.0 — 2026-10-08
|
|
4
11
|
|
|
5
12
|
- Useful structured API/CLI additions described in README.
|
|
@@ -21,3 +28,4 @@
|
|
|
21
28
|
|
|
22
29
|
- Deterministic readiness score with explanations and technical blocker caps.
|
|
23
30
|
- CLI, documented scoring rules, tests, linting, formatting, and Node 22/24 CI.
|
|
31
|
+
```
|
package/MIGRATION.md
CHANGED
|
@@ -3,3 +3,9 @@
|
|
|
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.
|
package/README.md
CHANGED
|
@@ -1,6 +1,29 @@
|
|
|
1
1
|
# ship-it-meter
|
|
2
2
|
|
|
3
|
-
|
|
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.
|
|
4
27
|
|
|
5
28
|
[](https://www.npmjs.com/package/@deployanyway/ship-it-meter)
|
|
6
29
|
[](https://github.com/DeployAnyway/ship-it-meter/actions/workflows/ci.yml)
|
package/dist/index.cjs
CHANGED
|
@@ -22,9 +22,136 @@ var index_exports = {};
|
|
|
22
22
|
__export(index_exports, {
|
|
23
23
|
preflight: () => preflight,
|
|
24
24
|
releaseGate: () => releaseGate,
|
|
25
|
+
releasePlan: () => releasePlan,
|
|
26
|
+
releaseScenarios: () => releaseScenarios,
|
|
27
|
+
scenarioEvidence: () => scenarioEvidence,
|
|
25
28
|
shipIt: () => shipIt
|
|
26
29
|
});
|
|
27
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
|
|
28
155
|
var days = [
|
|
29
156
|
"monday",
|
|
30
157
|
"tuesday",
|
|
@@ -175,9 +302,116 @@ function preflight(input = {}) {
|
|
|
175
302
|
);
|
|
176
303
|
return { ...result, actions };
|
|
177
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
|
+
}
|
|
178
409
|
// Annotate the CommonJS export names for ESM import in node:
|
|
179
410
|
0 && (module.exports = {
|
|
180
411
|
preflight,
|
|
181
412
|
releaseGate,
|
|
413
|
+
releasePlan,
|
|
414
|
+
releaseScenarios,
|
|
415
|
+
scenarioEvidence,
|
|
182
416
|
shipIt
|
|
183
417
|
});
|
package/index.d.cts
CHANGED
|
@@ -41,3 +41,25 @@ export function releaseGate(
|
|
|
41
41
|
input?: Evidence,
|
|
42
42
|
options?: { minScore?: number },
|
|
43
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
CHANGED
|
@@ -41,3 +41,25 @@ export function releaseGate(
|
|
|
41
41
|
input?: Evidence,
|
|
42
42
|
options?: { minScore?: number },
|
|
43
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
package/src/cli.js
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { URL } from "node:url";
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
shipIt,
|
|
6
|
+
preflight,
|
|
7
|
+
releaseGate,
|
|
8
|
+
releasePlan,
|
|
9
|
+
releaseScenarios,
|
|
10
|
+
scenarioEvidence,
|
|
11
|
+
} from "./index.js";
|
|
5
12
|
|
|
6
13
|
import { readStdin } from "./input.js";
|
|
7
14
|
|
|
@@ -13,6 +20,9 @@ try {
|
|
|
13
20
|
json: { type: "boolean" },
|
|
14
21
|
stdin: { type: "boolean" },
|
|
15
22
|
gate: { type: "boolean" },
|
|
23
|
+
plan: { type: "boolean" },
|
|
24
|
+
scenario: { type: "string" },
|
|
25
|
+
"list-scenarios": { type: "boolean" },
|
|
16
26
|
"min-score": { type: "string" },
|
|
17
27
|
checklist: { type: "boolean" },
|
|
18
28
|
tests: { type: "string" },
|
|
@@ -28,7 +38,7 @@ try {
|
|
|
28
38
|
});
|
|
29
39
|
if (values.help) {
|
|
30
40
|
console.log(
|
|
31
|
-
"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\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.",
|
|
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.",
|
|
32
42
|
);
|
|
33
43
|
} else if (values.version) {
|
|
34
44
|
console.log(
|
|
@@ -36,15 +46,32 @@ try {
|
|
|
36
46
|
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
37
47
|
).version,
|
|
38
48
|
);
|
|
49
|
+
} else if (values["list-scenarios"]) {
|
|
50
|
+
console.log(
|
|
51
|
+
releaseScenarios()
|
|
52
|
+
.map((item) => item.name + " — " + item.description)
|
|
53
|
+
.join("\n"),
|
|
54
|
+
);
|
|
39
55
|
} else {
|
|
40
|
-
const input = values.stdin
|
|
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.");
|
|
41
65
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
42
66
|
throw new TypeError("stdin must contain an evidence object.");
|
|
43
67
|
if (
|
|
44
68
|
values["min-score"] !== undefined &&
|
|
45
|
-
(!values.gate ||
|
|
69
|
+
(!(values.gate || values.plan) ||
|
|
70
|
+
!/^\d+(?:\.\d+)?$/.test(values["min-score"]))
|
|
46
71
|
)
|
|
47
|
-
throw new TypeError(
|
|
72
|
+
throw new TypeError(
|
|
73
|
+
"--min-score requires --gate or --plan and a decimal number.",
|
|
74
|
+
);
|
|
48
75
|
for (const [flag, key] of Object.entries({
|
|
49
76
|
tests: "tests",
|
|
50
77
|
failing: "failingTests",
|
|
@@ -66,21 +93,34 @@ try {
|
|
|
66
93
|
for (const key of ["day", "branch"])
|
|
67
94
|
if (values[key] !== undefined) input[key] = values[key];
|
|
68
95
|
if (values.dirty) input.uncommittedChanges = true;
|
|
69
|
-
const result =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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
|
+
);
|
|
80
120
|
console.log(
|
|
81
121
|
values.json
|
|
82
122
|
? JSON.stringify(result, null, 2)
|
|
83
|
-
: `${result.score}/100 — ${result.verdict}${values.gate ? (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") : ""}`,
|
|
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") : ""}`,
|
|
84
124
|
);
|
|
85
125
|
}
|
|
86
126
|
} catch (error) {
|
package/src/index.js
CHANGED
|
@@ -187,3 +187,113 @@ export function preflight(input = {}) {
|
|
|
187
187
|
);
|
|
188
188
|
return { ...result, actions };
|
|
189
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/scenarios.js
ADDED
|
@@ -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
|
+
}
|