@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 +14 -0
- package/MIGRATION.md +12 -0
- package/README.md +74 -1
- package/dist/index.cjs +448 -0
- package/examples/basic.js +12 -0
- package/examples/collect-receipts.mjs +85 -0
- package/examples/report-gate.mjs +13 -0
- package/index.d.cts +74 -0
- package/index.d.ts +74 -0
- package/package.json +9 -4
- package/src/cli.js +98 -18
- package/src/index.js +115 -0
- package/src/input.js +9 -2
- package/src/reports.js +262 -0
- package/src/scenarios.js +133 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { evaluateReports } from "@deployanyway/ship-it-meter";
|
|
3
|
+
try {
|
|
4
|
+
if (!process.argv[2]) throw new TypeError("Provide receipts.json.");
|
|
5
|
+
const result = evaluateReports(
|
|
6
|
+
JSON.parse(readFileSync(process.argv[2], "utf8")),
|
|
7
|
+
);
|
|
8
|
+
console.log(JSON.stringify(result, null, 2));
|
|
9
|
+
process.exitCode = result.passed ? 0 : 1;
|
|
10
|
+
} catch (error) {
|
|
11
|
+
console.error(error.message);
|
|
12
|
+
process.exitCode = 2;
|
|
13
|
+
}
|
package/index.d.cts
CHANGED
|
@@ -41,3 +41,77 @@ 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;
|
|
66
|
+
export type TestReportFormat = "node-tap" | "jest";
|
|
67
|
+
export interface TestReportSummary {
|
|
68
|
+
tests: number;
|
|
69
|
+
passedTests: number;
|
|
70
|
+
failingTests: number;
|
|
71
|
+
skippedTests: number;
|
|
72
|
+
todoTests: number;
|
|
73
|
+
successful: boolean;
|
|
74
|
+
}
|
|
75
|
+
export function parseTestReport(
|
|
76
|
+
data: unknown,
|
|
77
|
+
format?: TestReportFormat,
|
|
78
|
+
): TestReportSummary;
|
|
79
|
+
export function parseCoverageReport(
|
|
80
|
+
data: unknown,
|
|
81
|
+
): Record<"lines" | "statements" | "functions" | "branches", number>;
|
|
82
|
+
export interface ReportMetadata {
|
|
83
|
+
commit: string;
|
|
84
|
+
capturedAt: string;
|
|
85
|
+
}
|
|
86
|
+
export interface ReportBundle {
|
|
87
|
+
commit: string;
|
|
88
|
+
tests?: ReportMetadata & { format: TestReportFormat; data: unknown };
|
|
89
|
+
coverage?: ReportMetadata & { format: "istanbul-summary"; data: unknown };
|
|
90
|
+
build?: ReportMetadata & { exitCode: number };
|
|
91
|
+
}
|
|
92
|
+
export interface ReportPolicy {
|
|
93
|
+
minTests?: number;
|
|
94
|
+
minCoverage?: number;
|
|
95
|
+
minScore?: number;
|
|
96
|
+
maxAgeMs?: number;
|
|
97
|
+
now?: string;
|
|
98
|
+
}
|
|
99
|
+
export interface ReportReceipt {
|
|
100
|
+
kind: "tests" | "coverage" | "build";
|
|
101
|
+
format: string;
|
|
102
|
+
commit: string | null;
|
|
103
|
+
capturedAt: string | null;
|
|
104
|
+
accepted: boolean;
|
|
105
|
+
issues: string[];
|
|
106
|
+
}
|
|
107
|
+
export interface ReportEvaluation extends ReleasePlan {
|
|
108
|
+
evidence: Evidence;
|
|
109
|
+
receipts: ReportReceipt[];
|
|
110
|
+
commit: string;
|
|
111
|
+
policy: Required<Omit<ReportPolicy, "now">>;
|
|
112
|
+
evaluatedAt: string;
|
|
113
|
+
}
|
|
114
|
+
export function evaluateReports(
|
|
115
|
+
bundle: ReportBundle,
|
|
116
|
+
options?: ReportPolicy,
|
|
117
|
+
): ReportEvaluation;
|
package/index.d.ts
CHANGED
|
@@ -41,3 +41,77 @@ 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;
|
|
66
|
+
export type TestReportFormat = "node-tap" | "jest";
|
|
67
|
+
export interface TestReportSummary {
|
|
68
|
+
tests: number;
|
|
69
|
+
passedTests: number;
|
|
70
|
+
failingTests: number;
|
|
71
|
+
skippedTests: number;
|
|
72
|
+
todoTests: number;
|
|
73
|
+
successful: boolean;
|
|
74
|
+
}
|
|
75
|
+
export function parseTestReport(
|
|
76
|
+
data: unknown,
|
|
77
|
+
format?: TestReportFormat,
|
|
78
|
+
): TestReportSummary;
|
|
79
|
+
export function parseCoverageReport(
|
|
80
|
+
data: unknown,
|
|
81
|
+
): Record<"lines" | "statements" | "functions" | "branches", number>;
|
|
82
|
+
export interface ReportMetadata {
|
|
83
|
+
commit: string;
|
|
84
|
+
capturedAt: string;
|
|
85
|
+
}
|
|
86
|
+
export interface ReportBundle {
|
|
87
|
+
commit: string;
|
|
88
|
+
tests?: ReportMetadata & { format: TestReportFormat; data: unknown };
|
|
89
|
+
coverage?: ReportMetadata & { format: "istanbul-summary"; data: unknown };
|
|
90
|
+
build?: ReportMetadata & { exitCode: number };
|
|
91
|
+
}
|
|
92
|
+
export interface ReportPolicy {
|
|
93
|
+
minTests?: number;
|
|
94
|
+
minCoverage?: number;
|
|
95
|
+
minScore?: number;
|
|
96
|
+
maxAgeMs?: number;
|
|
97
|
+
now?: string;
|
|
98
|
+
}
|
|
99
|
+
export interface ReportReceipt {
|
|
100
|
+
kind: "tests" | "coverage" | "build";
|
|
101
|
+
format: string;
|
|
102
|
+
commit: string | null;
|
|
103
|
+
capturedAt: string | null;
|
|
104
|
+
accepted: boolean;
|
|
105
|
+
issues: string[];
|
|
106
|
+
}
|
|
107
|
+
export interface ReportEvaluation extends ReleasePlan {
|
|
108
|
+
evidence: Evidence;
|
|
109
|
+
receipts: ReportReceipt[];
|
|
110
|
+
commit: string;
|
|
111
|
+
policy: Required<Omit<ReportPolicy, "now">>;
|
|
112
|
+
evaluatedAt: string;
|
|
113
|
+
}
|
|
114
|
+
export function evaluateReports(
|
|
115
|
+
bundle: ReportBundle,
|
|
116
|
+
options?: ReportPolicy,
|
|
117
|
+
): ReportEvaluation;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@deployanyway/ship-it-meter",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Report-backed release policies, prioritized plans and explainable readiness scores. Confidence is not a build artifact; bring your receipts.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"dist",
|
|
28
28
|
"index.d.ts",
|
|
29
29
|
"index.d.cts",
|
|
30
|
-
"MIGRATION.md"
|
|
30
|
+
"MIGRATION.md",
|
|
31
|
+
"examples"
|
|
31
32
|
],
|
|
32
33
|
"engines": {
|
|
33
34
|
"node": ">=22.13"
|
|
@@ -75,7 +76,11 @@
|
|
|
75
76
|
"scoring",
|
|
76
77
|
"typescript",
|
|
77
78
|
"humor",
|
|
78
|
-
"deployanyway"
|
|
79
|
+
"deployanyway",
|
|
80
|
+
"ci",
|
|
81
|
+
"release-policy",
|
|
82
|
+
"test-reports",
|
|
83
|
+
"istanbul"
|
|
79
84
|
],
|
|
80
85
|
"main": "./dist/index.cjs",
|
|
81
86
|
"types": "./index.d.ts",
|
package/src/cli.js
CHANGED
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
-
import { readFileSync } from "node:fs";
|
|
2
|
+
import { readFileSync, statSync } 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
|
+
evaluateReports,
|
|
12
|
+
} from "./index.js";
|
|
5
13
|
|
|
6
14
|
import { readStdin } from "./input.js";
|
|
7
15
|
|
|
@@ -12,7 +20,13 @@ try {
|
|
|
12
20
|
version: { type: "boolean", short: "v" },
|
|
13
21
|
json: { type: "boolean" },
|
|
14
22
|
stdin: { type: "boolean" },
|
|
23
|
+
reports: { type: "boolean" },
|
|
24
|
+
"report-file": { type: "string" },
|
|
25
|
+
policy: { type: "string" },
|
|
15
26
|
gate: { type: "boolean" },
|
|
27
|
+
plan: { type: "boolean" },
|
|
28
|
+
scenario: { type: "string" },
|
|
29
|
+
"list-scenarios": { type: "boolean" },
|
|
16
30
|
"min-score": { type: "string" },
|
|
17
31
|
checklist: { type: "boolean" },
|
|
18
32
|
tests: { type: "string" },
|
|
@@ -28,7 +42,7 @@ try {
|
|
|
28
42
|
});
|
|
29
43
|
if (values.help) {
|
|
30
44
|
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.",
|
|
45
|
+
"Usage: ship-it-meter [options]\n\nOptions:\n --reports Read report bundle JSON from stdin (10 MiB)\n --report-file path Read report bundle JSON file\n --policy JSON Receipt thresholds/freshness; receipt mode only\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
46
|
);
|
|
33
47
|
} else if (values.version) {
|
|
34
48
|
console.log(
|
|
@@ -36,15 +50,68 @@ try {
|
|
|
36
50
|
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
37
51
|
).version,
|
|
38
52
|
);
|
|
53
|
+
} else if (values["list-scenarios"]) {
|
|
54
|
+
console.log(
|
|
55
|
+
releaseScenarios()
|
|
56
|
+
.map((item) => item.name + " — " + item.description)
|
|
57
|
+
.join("\n"),
|
|
58
|
+
);
|
|
59
|
+
} else if (values.reports || values["report-file"] !== undefined) {
|
|
60
|
+
if (values.reports && values["report-file"] !== undefined)
|
|
61
|
+
throw new TypeError("Choose --reports stdin or --report-file.");
|
|
62
|
+
const allowed = new Set(["reports", "report-file", "policy", "json"]);
|
|
63
|
+
for (const [key, value] of Object.entries(values))
|
|
64
|
+
if (value !== undefined && !allowed.has(key))
|
|
65
|
+
throw new TypeError(`--${key} cannot combine with report evaluation.`);
|
|
66
|
+
if (
|
|
67
|
+
values["report-file"] !== undefined &&
|
|
68
|
+
statSync(values["report-file"]).size > 10485760
|
|
69
|
+
)
|
|
70
|
+
throw new RangeError("Report bundle exceeds 10 MiB.");
|
|
71
|
+
const text =
|
|
72
|
+
values["report-file"] !== undefined
|
|
73
|
+
? readFileSync(values["report-file"], "utf8")
|
|
74
|
+
: await readStdin(process.stdin, 10485760);
|
|
75
|
+
if (Buffer.byteLength(text) > 10485760)
|
|
76
|
+
throw new RangeError("Report bundle exceeds 10 MiB.");
|
|
77
|
+
const policy = values.policy === undefined ? {} : JSON.parse(values.policy);
|
|
78
|
+
const result = evaluateReports(JSON.parse(text), policy);
|
|
79
|
+
if (!result.passed) process.exitCode = 1;
|
|
80
|
+
console.log(
|
|
81
|
+
values.json
|
|
82
|
+
? JSON.stringify(result, null, 2)
|
|
83
|
+
: `Receipt gate: ${result.passed ? "PASS" : "BLOCKED"} for ${result.commit}\n` +
|
|
84
|
+
result.receipts
|
|
85
|
+
.map(
|
|
86
|
+
(item) =>
|
|
87
|
+
`${item.kind}: ${item.accepted ? "accepted" : "rejected"}`,
|
|
88
|
+
)
|
|
89
|
+
.join("\n") +
|
|
90
|
+
"\n" +
|
|
91
|
+
result.blockers.map((item) => "- " + item).join("\n"),
|
|
92
|
+
);
|
|
93
|
+
} else if (values.policy !== undefined) {
|
|
94
|
+
throw new TypeError("--policy requires --reports or --report-file.");
|
|
39
95
|
} else {
|
|
40
|
-
const input = values.stdin
|
|
96
|
+
const input = values.stdin
|
|
97
|
+
? JSON.parse(await readStdin())
|
|
98
|
+
: values.scenario !== undefined
|
|
99
|
+
? scenarioEvidence(values.scenario)
|
|
100
|
+
: {};
|
|
101
|
+
if (values.stdin && values.scenario !== undefined)
|
|
102
|
+
throw new TypeError("Choose --stdin or --scenario, not both.");
|
|
103
|
+
if ([values.gate, values.plan, values.checklist].filter(Boolean).length > 1)
|
|
104
|
+
throw new TypeError("Choose one of --gate, --plan or --checklist.");
|
|
41
105
|
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
42
106
|
throw new TypeError("stdin must contain an evidence object.");
|
|
43
107
|
if (
|
|
44
108
|
values["min-score"] !== undefined &&
|
|
45
|
-
(!values.gate ||
|
|
109
|
+
(!(values.gate || values.plan) ||
|
|
110
|
+
!/^\d+(?:\.\d+)?$/.test(values["min-score"]))
|
|
46
111
|
)
|
|
47
|
-
throw new TypeError(
|
|
112
|
+
throw new TypeError(
|
|
113
|
+
"--min-score requires --gate or --plan and a decimal number.",
|
|
114
|
+
);
|
|
48
115
|
for (const [flag, key] of Object.entries({
|
|
49
116
|
tests: "tests",
|
|
50
117
|
failing: "failingTests",
|
|
@@ -66,21 +133,34 @@ try {
|
|
|
66
133
|
for (const key of ["day", "branch"])
|
|
67
134
|
if (values[key] !== undefined) input[key] = values[key];
|
|
68
135
|
if (values.dirty) input.uncommittedChanges = true;
|
|
69
|
-
const result =
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
136
|
+
const result =
|
|
137
|
+
values.gate || values.plan
|
|
138
|
+
? (values.plan ? releasePlan : releaseGate)(input, {
|
|
139
|
+
minScore:
|
|
140
|
+
values["min-score"] === undefined
|
|
141
|
+
? undefined
|
|
142
|
+
: Number(values["min-score"]),
|
|
143
|
+
})
|
|
144
|
+
: values.checklist
|
|
145
|
+
? preflight(input)
|
|
146
|
+
: shipIt(input);
|
|
147
|
+
if ((values.gate || values.plan) && !result.passed) process.exitCode = 1;
|
|
148
|
+
if (values.plan && !values.json)
|
|
149
|
+
console.log(
|
|
150
|
+
result.summary +
|
|
151
|
+
"\n" +
|
|
152
|
+
result.tasks
|
|
153
|
+
.map(
|
|
154
|
+
(task) =>
|
|
155
|
+
`[${task.priority}] ${task.title}\n Verify: ${task.verify}`,
|
|
156
|
+
)
|
|
157
|
+
.join("\n") +
|
|
158
|
+
"\n",
|
|
159
|
+
);
|
|
80
160
|
console.log(
|
|
81
161
|
values.json
|
|
82
162
|
? 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") : ""}`,
|
|
163
|
+
: `${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
164
|
);
|
|
85
165
|
}
|
|
86
166
|
} catch (error) {
|
package/src/index.js
CHANGED
|
@@ -187,3 +187,118 @@ 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
|
+
}
|
|
300
|
+
export {
|
|
301
|
+
parseTestReport,
|
|
302
|
+
parseCoverageReport,
|
|
303
|
+
evaluateReports,
|
|
304
|
+
} from "./reports.js";
|
package/src/input.js
CHANGED
|
@@ -1,12 +1,19 @@
|
|
|
1
1
|
/** Read bounded UTF-8 stdin without swallowing upstream failures. */
|
|
2
|
-
export async function readStdin(stream = process.stdin) {
|
|
2
|
+
export async function readStdin(stream = process.stdin, maxBytes = 262144) {
|
|
3
|
+
if (!Number.isSafeInteger(maxBytes) || maxBytes < 1)
|
|
4
|
+
throw new RangeError("Input limit must be a positive safe integer.");
|
|
3
5
|
if (stream.isTTY) throw new TypeError("Pipe or redirect input.");
|
|
4
6
|
let size = 0;
|
|
5
7
|
const chunks = [];
|
|
6
8
|
for await (const chunk of stream) {
|
|
7
9
|
const buffer = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
|
|
8
10
|
size += buffer.length;
|
|
9
|
-
if (size >
|
|
11
|
+
if (size > maxBytes)
|
|
12
|
+
throw new RangeError(
|
|
13
|
+
maxBytes === 262144
|
|
14
|
+
? "stdin exceeds 256 KiB."
|
|
15
|
+
: `stdin exceeds ${maxBytes} bytes.`,
|
|
16
|
+
);
|
|
10
17
|
chunks.push(buffer);
|
|
11
18
|
}
|
|
12
19
|
const text = Buffer.concat(chunks).toString("utf8").trim();
|