@deployanyway/ship-it-meter 0.1.0 → 0.1.1
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 +12 -6
- package/LICENSE +21 -21
- package/README.md +152 -151
- package/bin/cli.js +67 -67
- package/package.json +52 -41
- package/src/index.js +125 -125
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
|
-
# Changelog
|
|
2
|
-
|
|
3
|
-
## 0.1.
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
-
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 0.1.1 — 2026-10-08
|
|
4
|
+
|
|
5
|
+
- Correct npm installation and npx documentation after the initial publication.
|
|
6
|
+
- Add a searchable, humorous package description and relevant npm keywords.
|
|
7
|
+
- No API, CLI behavior, or dependency changes.
|
|
8
|
+
|
|
9
|
+
## 0.1.0 — 2026-10-08
|
|
10
|
+
|
|
11
|
+
- Deterministic readiness score with explanations and technical blocker caps.
|
|
12
|
+
- CLI, documented scoring rules, tests, linting, formatting, and Node 22/24 CI.
|
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 Deploy Anyway
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Deploy Anyway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,151 +1,152 @@
|
|
|
1
|
-
# ship-it-meter
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
```text
|
|
6
|
-
59/100 — Questionable
|
|
7
|
-
- 2 of 125 tests are failing (-21)
|
|
8
|
-
- Coverage is 82% (-4)
|
|
9
|
-
- Build passed
|
|
10
|
-
- Friday: the weekend would like a quiet entrance (-3)
|
|
11
|
-
- Failing tests cap the score at 59
|
|
12
|
-
```
|
|
13
|
-
|
|
14
|
-
## Installation
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
|
60
|
-
|
|
|
61
|
-
| `
|
|
62
|
-
| `
|
|
63
|
-
| `
|
|
64
|
-
| `
|
|
65
|
-
| `
|
|
66
|
-
| `
|
|
67
|
-
| `
|
|
68
|
-
| `
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
or
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
|
81
|
-
|
|
|
82
|
-
|
|
|
83
|
-
|
|
|
84
|
-
|
|
|
85
|
-
|
|
|
86
|
-
|
|
|
87
|
-
|
|
|
88
|
-
|
|
|
89
|
-
|
|
|
90
|
-
|
|
|
91
|
-
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
|
100
|
-
|
|
|
101
|
-
|
|
|
102
|
-
|
|
|
103
|
-
|
|
|
104
|
-
|
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
`--
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
npm
|
|
128
|
-
npm run
|
|
129
|
-
npm
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
- [
|
|
149
|
-
- [
|
|
150
|
-
- [
|
|
151
|
-
- [
|
|
1
|
+
# ship-it-meter
|
|
2
|
+
|
|
3
|
+
Explainable deployment readiness scoring for tests, coverage, and builds. Turns questionable confidence into a number.
|
|
4
|
+
|
|
5
|
+
```text
|
|
6
|
+
59/100 — Questionable
|
|
7
|
+
- 2 of 125 tests are failing (-21)
|
|
8
|
+
- Coverage is 82% (-4)
|
|
9
|
+
- Build passed
|
|
10
|
+
- Friday: the weekend would like a quiet entrance (-3)
|
|
11
|
+
- Failing tests cap the score at 59
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Installation
|
|
15
|
+
|
|
16
|
+
Available on npm. Requires Node 22 or later.
|
|
17
|
+
You can also run from source with Node 22 or 24:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
git clone https://github.com/DeployAnyway/ship-it-meter.git
|
|
21
|
+
cd ship-it-meter
|
|
22
|
+
git checkout main
|
|
23
|
+
npm ci
|
|
24
|
+
node examples/basic.js
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
Install from npm: `npm install @deployanyway/ship-it-meter`.
|
|
28
|
+
|
|
29
|
+
## Quick start
|
|
30
|
+
|
|
31
|
+
```js
|
|
32
|
+
import { shipIt } from "@deployanyway/ship-it-meter";
|
|
33
|
+
|
|
34
|
+
const result = shipIt({
|
|
35
|
+
tests: 125,
|
|
36
|
+
failingTests: 2,
|
|
37
|
+
coverage: 82,
|
|
38
|
+
day: "Friday",
|
|
39
|
+
branch: "main",
|
|
40
|
+
build: true,
|
|
41
|
+
});
|
|
42
|
+
console.log(result); // { score: 59, verdict: 'Questionable', reasons: [...] }
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## CLI example
|
|
46
|
+
|
|
47
|
+
```sh
|
|
48
|
+
node bin/cli.js --tests 125 --failing 2 --coverage 82 --build pass --day Friday
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Run with npx: `npx @deployanyway/ship-it-meter --tests 125 --coverage 82 --build pass`.
|
|
52
|
+
|
|
53
|
+
## API and inputs
|
|
54
|
+
|
|
55
|
+
`shipIt(input = {})` returns `{ score, verdict, reasons }`. It never reads the
|
|
56
|
+
filesystem, branch, clock, tests, CI, or issue tracker; all evidence is supplied
|
|
57
|
+
by the caller. It never logs or exits. Inputs are not mutated and results are fresh.
|
|
58
|
+
|
|
59
|
+
| Input | Meaning and default |
|
|
60
|
+
| -------------------- | ----------------------------------------------------------------- |
|
|
61
|
+
| `tests` | Nonnegative safe integer; missing/zero penalized |
|
|
62
|
+
| `failingTests` | Nonnegative safe integer, default 0; cannot exceed tests |
|
|
63
|
+
| `coverage` | Finite percentage 0–100; missing penalized |
|
|
64
|
+
| `build` | Boolean; missing penalized |
|
|
65
|
+
| `criticalIssues` | Nonnegative safe integer, default 0 |
|
|
66
|
+
| `lintFailures` | Nonnegative safe integer, default 0 |
|
|
67
|
+
| `uncommittedChanges` | Boolean, default false |
|
|
68
|
+
| `branch` | Nonempty string; omitted means no branch penalty |
|
|
69
|
+
| `day` | Full weekday name, case insensitive; omitted means no day penalty |
|
|
70
|
+
|
|
71
|
+
Positive failingTests requires tests. Numeric strings are not library numbers.
|
|
72
|
+
Invalid types or unknown keys throw TypeError; invalid ranges, counts, day names,
|
|
73
|
+
or inconsistent test totals throw RangeError. Unknown optional issue/lint/dirty
|
|
74
|
+
data is treated as no reported problems, not verified clean evidence.
|
|
75
|
+
|
|
76
|
+
## Scoring algorithm (0.1.0)
|
|
77
|
+
|
|
78
|
+
Start at 100. Subtract the following, then clamp to 0–100 and apply blocker caps:
|
|
79
|
+
|
|
80
|
+
| Condition | Penalty |
|
|
81
|
+
| ---------------------------------------------------------------- | --------------------------------------------- |
|
|
82
|
+
| Missing test count or zero tests | 25 |
|
|
83
|
+
| Failing tests | min(50, 20 + ceil(30 × failingTests / tests)) |
|
|
84
|
+
| Missing coverage | 15 |
|
|
85
|
+
| Known coverage | ceil((100 − coverage) × 0.2) |
|
|
86
|
+
| Missing build status | 15 |
|
|
87
|
+
| Failed build | 40 |
|
|
88
|
+
| Critical issues | min(50, 25 × count) |
|
|
89
|
+
| Lint failures | min(20, 5 × count) |
|
|
90
|
+
| Uncommitted changes | 10 |
|
|
91
|
+
| Supplied branch other than main/master (case sensitive, trimmed) | 5 |
|
|
92
|
+
| Friday | 3 |
|
|
93
|
+
|
|
94
|
+
Failed builds or any critical issue cap the final score at **29**. Otherwise,
|
|
95
|
+
any failing test caps it at **59**. Friday is only a small joke; technical blockers
|
|
96
|
+
always dominate. Every applied penalty/cap appears in reasons, alongside reported
|
|
97
|
+
passing tests/build. Caps are ceilings, not additional deductions.
|
|
98
|
+
|
|
99
|
+
| Final score | Verdict |
|
|
100
|
+
| ----------- | ------------------ |
|
|
101
|
+
| 0–29 | Absolutely Not |
|
|
102
|
+
| 30–59 | Questionable |
|
|
103
|
+
| 60–79 | Probably Fine |
|
|
104
|
+
| 80–94 | Ship It |
|
|
105
|
+
| 95–100 | Suspiciously Ready |
|
|
106
|
+
|
|
107
|
+
`shipIt()` scores 45 (Questionable) because key evidence is missing. A passing
|
|
108
|
+
build, nonzero tests with no failures, and 100% coverage score 100. This is an
|
|
109
|
+
illustrative heuristic, not a deployment gate or a guarantee of reliability.
|
|
110
|
+
Its assumptions and scoring may evolve between versions; pin versions for repeatable reports.
|
|
111
|
+
|
|
112
|
+
## CLI reference
|
|
113
|
+
|
|
114
|
+
Flags: `--tests`, `--failing`, `--coverage`, `--critical`, `--lint` take numbers.
|
|
115
|
+
`--build pass|fail`, `--day weekday`, and `--branch name` take text. `--dirty`
|
|
116
|
+
reports uncommitted changes. `--json` emits the structured result. `--help`/`-h`
|
|
117
|
+
and `--version`/`-v` show help/version. No positional arguments or stdin support.
|
|
118
|
+
|
|
119
|
+
Exit 0 means scoring succeeded, even for Absolutely Not. Exit 2 means invalid
|
|
120
|
+
arguments. Consumers choose their own deployment thresholds.
|
|
121
|
+
|
|
122
|
+
## Development and examples
|
|
123
|
+
|
|
124
|
+
```sh
|
|
125
|
+
npm ci
|
|
126
|
+
node examples/basic.js
|
|
127
|
+
npm test
|
|
128
|
+
npm run lint
|
|
129
|
+
npm run format:check
|
|
130
|
+
npm pack --dry-run
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
ES modules, zero production dependencies, Node's test runner. CI runs Node 22/24;
|
|
134
|
+
development tooling requires Node 22.13+ or 24.
|
|
135
|
+
|
|
136
|
+
## Contributing
|
|
137
|
+
|
|
138
|
+
See [CONTRIBUTING.md](CONTRIBUTING.md).
|
|
139
|
+
|
|
140
|
+
## License
|
|
141
|
+
|
|
142
|
+
[MIT](LICENSE).
|
|
143
|
+
|
|
144
|
+
## More from DeployAnyway
|
|
145
|
+
|
|
146
|
+
**Tools for developers who probably know better.**
|
|
147
|
+
|
|
148
|
+
- [error-translator](https://github.com/DeployAnyway/error-translator)
|
|
149
|
+
- [excuse-js](https://github.com/DeployAnyway/excuse-js)
|
|
150
|
+
- [doggo-log](https://github.com/DeployAnyway/doggo-log)
|
|
151
|
+
- [ship-it-meter](https://github.com/DeployAnyway/ship-it-meter)
|
|
152
|
+
- [bro-say](https://github.com/DeployAnyway/bro-say)
|
package/bin/cli.js
CHANGED
|
@@ -1,67 +1,67 @@
|
|
|
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 } 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
|
-
tests: { type: "string" },
|
|
14
|
-
failing: { type: "string" },
|
|
15
|
-
coverage: { type: "string" },
|
|
16
|
-
day: { type: "string" },
|
|
17
|
-
branch: { type: "string" },
|
|
18
|
-
critical: { type: "string" },
|
|
19
|
-
lint: { type: "string" },
|
|
20
|
-
build: { type: "string" },
|
|
21
|
-
dirty: { type: "boolean" },
|
|
22
|
-
},
|
|
23
|
-
});
|
|
24
|
-
if (values.help) {
|
|
25
|
-
console.log(
|
|
26
|
-
"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 --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.",
|
|
27
|
-
);
|
|
28
|
-
} else if (values.version) {
|
|
29
|
-
console.log(
|
|
30
|
-
JSON.parse(
|
|
31
|
-
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
32
|
-
).version,
|
|
33
|
-
);
|
|
34
|
-
} else {
|
|
35
|
-
const input = {};
|
|
36
|
-
for (const [flag, key] of Object.entries({
|
|
37
|
-
tests: "tests",
|
|
38
|
-
failing: "failingTests",
|
|
39
|
-
coverage: "coverage",
|
|
40
|
-
critical: "criticalIssues",
|
|
41
|
-
lint: "lintFailures",
|
|
42
|
-
})) {
|
|
43
|
-
if (values[flag] !== undefined) {
|
|
44
|
-
if (!/^\d+(?:\.\d+)?$/.test(values[flag]))
|
|
45
|
-
throw new TypeError(`${flag} must be a nonnegative decimal number.`);
|
|
46
|
-
input[key] = Number(values[flag]);
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
if (values.build !== undefined) {
|
|
50
|
-
if (!["pass", "fail"].includes(values.build))
|
|
51
|
-
throw new RangeError("--build must be pass or fail.");
|
|
52
|
-
input.build = values.build === "pass";
|
|
53
|
-
}
|
|
54
|
-
for (const key of ["day", "branch"])
|
|
55
|
-
if (values[key] !== undefined) input[key] = values[key];
|
|
56
|
-
if (values.dirty) input.uncommittedChanges = true;
|
|
57
|
-
const result = shipIt(input);
|
|
58
|
-
console.log(
|
|
59
|
-
values.json
|
|
60
|
-
? JSON.stringify(result, null, 2)
|
|
61
|
-
: `${result.score}/100 — ${result.verdict}\n${result.reasons.map((reason) => `- ${reason}`).join("\n")}`,
|
|
62
|
-
);
|
|
63
|
-
}
|
|
64
|
-
} catch (error) {
|
|
65
|
-
console.error(`ship-it-meter: ${error.message}\nRun with --help for usage.`);
|
|
66
|
-
process.exitCode = 2;
|
|
67
|
-
}
|
|
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 } 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
|
+
tests: { type: "string" },
|
|
14
|
+
failing: { type: "string" },
|
|
15
|
+
coverage: { type: "string" },
|
|
16
|
+
day: { type: "string" },
|
|
17
|
+
branch: { type: "string" },
|
|
18
|
+
critical: { type: "string" },
|
|
19
|
+
lint: { type: "string" },
|
|
20
|
+
build: { type: "string" },
|
|
21
|
+
dirty: { type: "boolean" },
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
if (values.help) {
|
|
25
|
+
console.log(
|
|
26
|
+
"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 --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.",
|
|
27
|
+
);
|
|
28
|
+
} else if (values.version) {
|
|
29
|
+
console.log(
|
|
30
|
+
JSON.parse(
|
|
31
|
+
readFileSync(new URL("../package.json", import.meta.url), "utf8"),
|
|
32
|
+
).version,
|
|
33
|
+
);
|
|
34
|
+
} else {
|
|
35
|
+
const input = {};
|
|
36
|
+
for (const [flag, key] of Object.entries({
|
|
37
|
+
tests: "tests",
|
|
38
|
+
failing: "failingTests",
|
|
39
|
+
coverage: "coverage",
|
|
40
|
+
critical: "criticalIssues",
|
|
41
|
+
lint: "lintFailures",
|
|
42
|
+
})) {
|
|
43
|
+
if (values[flag] !== undefined) {
|
|
44
|
+
if (!/^\d+(?:\.\d+)?$/.test(values[flag]))
|
|
45
|
+
throw new TypeError(`${flag} must be a nonnegative decimal number.`);
|
|
46
|
+
input[key] = Number(values[flag]);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (values.build !== undefined) {
|
|
50
|
+
if (!["pass", "fail"].includes(values.build))
|
|
51
|
+
throw new RangeError("--build must be pass or fail.");
|
|
52
|
+
input.build = values.build === "pass";
|
|
53
|
+
}
|
|
54
|
+
for (const key of ["day", "branch"])
|
|
55
|
+
if (values[key] !== undefined) input[key] = values[key];
|
|
56
|
+
if (values.dirty) input.uncommittedChanges = true;
|
|
57
|
+
const result = shipIt(input);
|
|
58
|
+
console.log(
|
|
59
|
+
values.json
|
|
60
|
+
? JSON.stringify(result, null, 2)
|
|
61
|
+
: `${result.score}/100 — ${result.verdict}\n${result.reasons.map((reason) => `- ${reason}`).join("\n")}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error(`ship-it-meter: ${error.message}\nRun with --help for usage.`);
|
|
66
|
+
process.exitCode = 2;
|
|
67
|
+
}
|
package/package.json
CHANGED
|
@@ -1,41 +1,52 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@deployanyway/ship-it-meter",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
5
|
-
"type": "module",
|
|
6
|
-
"exports": "./src/index.js",
|
|
7
|
-
"bin": {
|
|
8
|
-
"ship-it-meter": "./bin/cli.js"
|
|
9
|
-
},
|
|
10
|
-
"files": [
|
|
11
|
-
"src",
|
|
12
|
-
"bin",
|
|
13
|
-
"README.md",
|
|
14
|
-
"LICENSE",
|
|
15
|
-
"CHANGELOG.md"
|
|
16
|
-
],
|
|
17
|
-
"engines": {
|
|
18
|
-
"node": ">=22"
|
|
19
|
-
},
|
|
20
|
-
"scripts": {
|
|
21
|
-
"test": "node --test",
|
|
22
|
-
"lint": "eslint .",
|
|
23
|
-
"format": "prettier --write .",
|
|
24
|
-
"format:check": "prettier --check ."
|
|
25
|
-
},
|
|
26
|
-
"repository": {
|
|
27
|
-
"type": "git",
|
|
28
|
-
"url": "git+https://github.com/DeployAnyway/ship-it-meter.git"
|
|
29
|
-
},
|
|
30
|
-
"bugs": {
|
|
31
|
-
"url": "https://github.com/DeployAnyway/ship-it-meter/issues"
|
|
32
|
-
},
|
|
33
|
-
"license": "MIT",
|
|
34
|
-
"publishConfig": {
|
|
35
|
-
"access": "public"
|
|
36
|
-
},
|
|
37
|
-
"devDependencies": {
|
|
38
|
-
"eslint": "^10.12.0",
|
|
39
|
-
"prettier": "^3.6.2"
|
|
40
|
-
}
|
|
41
|
-
|
|
1
|
+
{
|
|
2
|
+
"name": "@deployanyway/ship-it-meter",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Explainable deployment readiness scoring for tests, coverage, and builds. Turns questionable confidence into a number.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"exports": "./src/index.js",
|
|
7
|
+
"bin": {
|
|
8
|
+
"ship-it-meter": "./bin/cli.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"bin",
|
|
13
|
+
"README.md",
|
|
14
|
+
"LICENSE",
|
|
15
|
+
"CHANGELOG.md"
|
|
16
|
+
],
|
|
17
|
+
"engines": {
|
|
18
|
+
"node": ">=22"
|
|
19
|
+
},
|
|
20
|
+
"scripts": {
|
|
21
|
+
"test": "node --test",
|
|
22
|
+
"lint": "eslint .",
|
|
23
|
+
"format": "prettier --write .",
|
|
24
|
+
"format:check": "prettier --check ."
|
|
25
|
+
},
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/DeployAnyway/ship-it-meter.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/DeployAnyway/ship-it-meter/issues"
|
|
32
|
+
},
|
|
33
|
+
"license": "MIT",
|
|
34
|
+
"publishConfig": {
|
|
35
|
+
"access": "public"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"eslint": "^10.12.0",
|
|
39
|
+
"prettier": "^3.6.2"
|
|
40
|
+
},
|
|
41
|
+
"keywords": [
|
|
42
|
+
"nodejs",
|
|
43
|
+
"javascript",
|
|
44
|
+
"cli",
|
|
45
|
+
"developer-tools",
|
|
46
|
+
"deployment",
|
|
47
|
+
"readiness",
|
|
48
|
+
"test-coverage",
|
|
49
|
+
"build",
|
|
50
|
+
"scoring"
|
|
51
|
+
]
|
|
52
|
+
}
|
package/src/index.js
CHANGED
|
@@ -1,125 +1,125 @@
|
|
|
1
|
-
const days = [
|
|
2
|
-
"monday",
|
|
3
|
-
"tuesday",
|
|
4
|
-
"wednesday",
|
|
5
|
-
"thursday",
|
|
6
|
-
"friday",
|
|
7
|
-
"saturday",
|
|
8
|
-
"sunday",
|
|
9
|
-
];
|
|
10
|
-
const counts = ["tests", "failingTests", "criticalIssues", "lintFailures"];
|
|
11
|
-
const allowed = [
|
|
12
|
-
...counts,
|
|
13
|
-
"coverage",
|
|
14
|
-
"build",
|
|
15
|
-
"uncommittedChanges",
|
|
16
|
-
"branch",
|
|
17
|
-
"day",
|
|
18
|
-
];
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Calculate a stateless readiness score. Missing evidence reduces the score.
|
|
22
|
-
* @param {{tests?: number, failingTests?: number, coverage?: number, build?: boolean, uncommittedChanges?: boolean, branch?: string, day?: string, criticalIssues?: number, lintFailures?: number}} [input]
|
|
23
|
-
* @returns {{score: number, verdict: string, reasons: string[]}}
|
|
24
|
-
*/
|
|
25
|
-
export function shipIt(input = {}) {
|
|
26
|
-
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
27
|
-
throw new TypeError("Input must be an object.");
|
|
28
|
-
for (const key of Object.keys(input))
|
|
29
|
-
if (!allowed.includes(key)) throw new TypeError(`Unknown input: ${key}.`);
|
|
30
|
-
for (const key of counts) {
|
|
31
|
-
if (
|
|
32
|
-
input[key] !== undefined &&
|
|
33
|
-
(!Number.isSafeInteger(input[key]) || input[key] < 0)
|
|
34
|
-
)
|
|
35
|
-
throw new RangeError(`${key} must be a nonnegative safe integer.`);
|
|
36
|
-
}
|
|
37
|
-
if (
|
|
38
|
-
input.coverage !== undefined &&
|
|
39
|
-
(typeof input.coverage !== "number" ||
|
|
40
|
-
!Number.isFinite(input.coverage) ||
|
|
41
|
-
input.coverage < 0 ||
|
|
42
|
-
input.coverage > 100)
|
|
43
|
-
)
|
|
44
|
-
throw new RangeError("coverage must be between 0 and 100.");
|
|
45
|
-
for (const key of ["build", "uncommittedChanges"])
|
|
46
|
-
if (input[key] !== undefined && typeof input[key] !== "boolean")
|
|
47
|
-
throw new TypeError(`${key} must be a boolean.`);
|
|
48
|
-
for (const key of ["branch", "day"])
|
|
49
|
-
if (
|
|
50
|
-
input[key] !== undefined &&
|
|
51
|
-
(typeof input[key] !== "string" || !input[key].trim())
|
|
52
|
-
)
|
|
53
|
-
throw new TypeError(`${key} must be a nonempty string.`);
|
|
54
|
-
const day = input.day?.trim().toLowerCase();
|
|
55
|
-
if (day !== undefined && !days.includes(day))
|
|
56
|
-
throw new RangeError("day must be a full weekday name.");
|
|
57
|
-
const failing = input.failingTests ?? 0;
|
|
58
|
-
if (failing > 0 && input.tests === undefined)
|
|
59
|
-
throw new RangeError("tests is required when failingTests is positive.");
|
|
60
|
-
if (input.tests !== undefined && failing > input.tests)
|
|
61
|
-
throw new RangeError("failingTests cannot exceed tests.");
|
|
62
|
-
let score = 100;
|
|
63
|
-
const reasons = [];
|
|
64
|
-
const deduct = (points, reason) => {
|
|
65
|
-
score -= points;
|
|
66
|
-
reasons.push(`${reason} (-${points})`);
|
|
67
|
-
};
|
|
68
|
-
if (!input.tests)
|
|
69
|
-
deduct(
|
|
70
|
-
25,
|
|
71
|
-
input.tests === 0 ? "No tests reported" : "Test count not provided",
|
|
72
|
-
);
|
|
73
|
-
else if (failing)
|
|
74
|
-
deduct(
|
|
75
|
-
Math.min(50, 20 + Math.ceil(30 * (failing / input.tests))),
|
|
76
|
-
`${failing} of ${input.tests} tests are failing`,
|
|
77
|
-
);
|
|
78
|
-
else reasons.push(`${input.tests} tests reported passing`);
|
|
79
|
-
if (input.coverage === undefined) deduct(15, "Coverage not provided");
|
|
80
|
-
else {
|
|
81
|
-
const penalty = Math.ceil((100 - input.coverage) * 0.2);
|
|
82
|
-
if (penalty) deduct(penalty, `Coverage is ${input.coverage}%`);
|
|
83
|
-
else reasons.push("Coverage is 100%");
|
|
84
|
-
}
|
|
85
|
-
if (input.build === undefined) deduct(15, "Build status not provided");
|
|
86
|
-
else if (!input.build) deduct(40, "Build failed");
|
|
87
|
-
else reasons.push("Build passed");
|
|
88
|
-
if (input.criticalIssues)
|
|
89
|
-
deduct(
|
|
90
|
-
Math.min(50, 25 * input.criticalIssues),
|
|
91
|
-
`${input.criticalIssues} open critical issues`,
|
|
92
|
-
);
|
|
93
|
-
if (input.lintFailures)
|
|
94
|
-
deduct(
|
|
95
|
-
Math.min(20, 5 * input.lintFailures),
|
|
96
|
-
`${input.lintFailures} lint failures`,
|
|
97
|
-
);
|
|
98
|
-
if (input.uncommittedChanges) deduct(10, "Uncommitted changes reported");
|
|
99
|
-
if (
|
|
100
|
-
input.branch !== undefined &&
|
|
101
|
-
!["main", "master"].includes(input.branch.trim())
|
|
102
|
-
)
|
|
103
|
-
deduct(5, `Branch is ${input.branch.trim()}`);
|
|
104
|
-
if (day === "friday")
|
|
105
|
-
deduct(3, "Friday: the weekend would like a quiet entrance");
|
|
106
|
-
score = Math.max(0, Math.min(100, score));
|
|
107
|
-
if (input.build === false || input.criticalIssues > 0) {
|
|
108
|
-
score = Math.min(score, 29);
|
|
109
|
-
reasons.push("Failed build or critical issues cap the score at 29");
|
|
110
|
-
} else if (failing > 0) {
|
|
111
|
-
score = Math.min(score, 59);
|
|
112
|
-
reasons.push("Failing tests cap the score at 59");
|
|
113
|
-
}
|
|
114
|
-
const verdict =
|
|
115
|
-
score < 30
|
|
116
|
-
? "Absolutely Not"
|
|
117
|
-
: score < 60
|
|
118
|
-
? "Questionable"
|
|
119
|
-
: score < 80
|
|
120
|
-
? "Probably Fine"
|
|
121
|
-
: score < 95
|
|
122
|
-
? "Ship It"
|
|
123
|
-
: "Suspiciously Ready";
|
|
124
|
-
return { score, verdict, reasons };
|
|
125
|
-
}
|
|
1
|
+
const days = [
|
|
2
|
+
"monday",
|
|
3
|
+
"tuesday",
|
|
4
|
+
"wednesday",
|
|
5
|
+
"thursday",
|
|
6
|
+
"friday",
|
|
7
|
+
"saturday",
|
|
8
|
+
"sunday",
|
|
9
|
+
];
|
|
10
|
+
const counts = ["tests", "failingTests", "criticalIssues", "lintFailures"];
|
|
11
|
+
const allowed = [
|
|
12
|
+
...counts,
|
|
13
|
+
"coverage",
|
|
14
|
+
"build",
|
|
15
|
+
"uncommittedChanges",
|
|
16
|
+
"branch",
|
|
17
|
+
"day",
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Calculate a stateless readiness score. Missing evidence reduces the score.
|
|
22
|
+
* @param {{tests?: number, failingTests?: number, coverage?: number, build?: boolean, uncommittedChanges?: boolean, branch?: string, day?: string, criticalIssues?: number, lintFailures?: number}} [input]
|
|
23
|
+
* @returns {{score: number, verdict: string, reasons: string[]}}
|
|
24
|
+
*/
|
|
25
|
+
export function shipIt(input = {}) {
|
|
26
|
+
if (!input || typeof input !== "object" || Array.isArray(input))
|
|
27
|
+
throw new TypeError("Input must be an object.");
|
|
28
|
+
for (const key of Object.keys(input))
|
|
29
|
+
if (!allowed.includes(key)) throw new TypeError(`Unknown input: ${key}.`);
|
|
30
|
+
for (const key of counts) {
|
|
31
|
+
if (
|
|
32
|
+
input[key] !== undefined &&
|
|
33
|
+
(!Number.isSafeInteger(input[key]) || input[key] < 0)
|
|
34
|
+
)
|
|
35
|
+
throw new RangeError(`${key} must be a nonnegative safe integer.`);
|
|
36
|
+
}
|
|
37
|
+
if (
|
|
38
|
+
input.coverage !== undefined &&
|
|
39
|
+
(typeof input.coverage !== "number" ||
|
|
40
|
+
!Number.isFinite(input.coverage) ||
|
|
41
|
+
input.coverage < 0 ||
|
|
42
|
+
input.coverage > 100)
|
|
43
|
+
)
|
|
44
|
+
throw new RangeError("coverage must be between 0 and 100.");
|
|
45
|
+
for (const key of ["build", "uncommittedChanges"])
|
|
46
|
+
if (input[key] !== undefined && typeof input[key] !== "boolean")
|
|
47
|
+
throw new TypeError(`${key} must be a boolean.`);
|
|
48
|
+
for (const key of ["branch", "day"])
|
|
49
|
+
if (
|
|
50
|
+
input[key] !== undefined &&
|
|
51
|
+
(typeof input[key] !== "string" || !input[key].trim())
|
|
52
|
+
)
|
|
53
|
+
throw new TypeError(`${key} must be a nonempty string.`);
|
|
54
|
+
const day = input.day?.trim().toLowerCase();
|
|
55
|
+
if (day !== undefined && !days.includes(day))
|
|
56
|
+
throw new RangeError("day must be a full weekday name.");
|
|
57
|
+
const failing = input.failingTests ?? 0;
|
|
58
|
+
if (failing > 0 && input.tests === undefined)
|
|
59
|
+
throw new RangeError("tests is required when failingTests is positive.");
|
|
60
|
+
if (input.tests !== undefined && failing > input.tests)
|
|
61
|
+
throw new RangeError("failingTests cannot exceed tests.");
|
|
62
|
+
let score = 100;
|
|
63
|
+
const reasons = [];
|
|
64
|
+
const deduct = (points, reason) => {
|
|
65
|
+
score -= points;
|
|
66
|
+
reasons.push(`${reason} (-${points})`);
|
|
67
|
+
};
|
|
68
|
+
if (!input.tests)
|
|
69
|
+
deduct(
|
|
70
|
+
25,
|
|
71
|
+
input.tests === 0 ? "No tests reported" : "Test count not provided",
|
|
72
|
+
);
|
|
73
|
+
else if (failing)
|
|
74
|
+
deduct(
|
|
75
|
+
Math.min(50, 20 + Math.ceil(30 * (failing / input.tests))),
|
|
76
|
+
`${failing} of ${input.tests} tests are failing`,
|
|
77
|
+
);
|
|
78
|
+
else reasons.push(`${input.tests} tests reported passing`);
|
|
79
|
+
if (input.coverage === undefined) deduct(15, "Coverage not provided");
|
|
80
|
+
else {
|
|
81
|
+
const penalty = Math.ceil((100 - input.coverage) * 0.2);
|
|
82
|
+
if (penalty) deduct(penalty, `Coverage is ${input.coverage}%`);
|
|
83
|
+
else reasons.push("Coverage is 100%");
|
|
84
|
+
}
|
|
85
|
+
if (input.build === undefined) deduct(15, "Build status not provided");
|
|
86
|
+
else if (!input.build) deduct(40, "Build failed");
|
|
87
|
+
else reasons.push("Build passed");
|
|
88
|
+
if (input.criticalIssues)
|
|
89
|
+
deduct(
|
|
90
|
+
Math.min(50, 25 * input.criticalIssues),
|
|
91
|
+
`${input.criticalIssues} open critical issues`,
|
|
92
|
+
);
|
|
93
|
+
if (input.lintFailures)
|
|
94
|
+
deduct(
|
|
95
|
+
Math.min(20, 5 * input.lintFailures),
|
|
96
|
+
`${input.lintFailures} lint failures`,
|
|
97
|
+
);
|
|
98
|
+
if (input.uncommittedChanges) deduct(10, "Uncommitted changes reported");
|
|
99
|
+
if (
|
|
100
|
+
input.branch !== undefined &&
|
|
101
|
+
!["main", "master"].includes(input.branch.trim())
|
|
102
|
+
)
|
|
103
|
+
deduct(5, `Branch is ${input.branch.trim()}`);
|
|
104
|
+
if (day === "friday")
|
|
105
|
+
deduct(3, "Friday: the weekend would like a quiet entrance");
|
|
106
|
+
score = Math.max(0, Math.min(100, score));
|
|
107
|
+
if (input.build === false || input.criticalIssues > 0) {
|
|
108
|
+
score = Math.min(score, 29);
|
|
109
|
+
reasons.push("Failed build or critical issues cap the score at 29");
|
|
110
|
+
} else if (failing > 0) {
|
|
111
|
+
score = Math.min(score, 59);
|
|
112
|
+
reasons.push("Failing tests cap the score at 59");
|
|
113
|
+
}
|
|
114
|
+
const verdict =
|
|
115
|
+
score < 30
|
|
116
|
+
? "Absolutely Not"
|
|
117
|
+
: score < 60
|
|
118
|
+
? "Questionable"
|
|
119
|
+
: score < 80
|
|
120
|
+
? "Probably Fine"
|
|
121
|
+
: score < 95
|
|
122
|
+
? "Ship It"
|
|
123
|
+
: "Suspiciously Ready";
|
|
124
|
+
return { score, verdict, reasons };
|
|
125
|
+
}
|