spectrum-cli 5.0.2 → 6.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 +28 -0
- package/README.md +228 -22
- package/package.json +4 -2
- package/src/automation/index.js +297 -0
- package/src/changelog/commands.js +1 -1
- package/src/changelog/open-release.js +9 -1
- package/src/chart/deploy.js +3 -3
- package/src/chart/start.js +2 -2
- package/src/chart/verify.js +5 -5
- package/src/cli/command-executor.js +33 -4
- package/src/cli/commands.js +101 -2
- package/src/cli/help.js +4 -1
- package/src/cli/run-action.js +19 -3
- package/src/git/index.js +1 -1
- package/src/hotfix/checks.js +27 -4
- package/src/hotfix/close.js +3 -3
- package/src/hotfix/start.js +2 -2
- package/src/nextjs/index.js +184 -0
- package/src/preflight/automation.js +291 -0
- package/src/preflight/helmrelease.js +11 -5
- package/src/preflight/source.js +13 -14
- package/src/preflight/tags.js +9 -1
- package/src/release/start.js +14 -9
- package/src/sbom/index.js +4 -0
- package/src/sbom/populate.js +607 -0
- package/src/sbom/provenance.js +467 -0
- package/src/sbom/resolve.js +418 -0
- package/src/sbom/schema.js +170 -0
- package/src/sbom/tools.js +74 -0
- package/src/security/findings.js +180 -0
- package/src/security/index.js +36 -0
- package/src/security/repair.js +553 -0
- package/src/security/report.js +65 -0
- package/src/security/runtime.js +192 -0
- package/src/security/scan.js +300 -0
- package/src/security/tracker.js +248 -0
- package/src/security/tracking.js +238 -0
- package/src/token/rotate.js +2 -2
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
// Shared bounded IO for noninteractive CI commands. Never execute shell strings.
|
|
2
|
+
const fs = require("node:fs");
|
|
3
|
+
const os = require("node:os");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const crypto = require("node:crypto");
|
|
6
|
+
const { spawnSync } = require("node:child_process");
|
|
7
|
+
const { gunzipSync } = require("node:zlib");
|
|
8
|
+
const { isDeepStrictEqual } = require("node:util");
|
|
9
|
+
|
|
10
|
+
const SEMVER =
|
|
11
|
+
/^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?:\+[0-9A-Za-z-]+(?:\.[0-9A-Za-z-]+)*)?(?![\s\S])/;
|
|
12
|
+
const DIGEST = /^sha256:[a-f0-9]{64}(?![\s\S])/;
|
|
13
|
+
const HEX64 = /^[a-f0-9]{64}(?![\s\S])/;
|
|
14
|
+
const COMMIT = /^[a-f0-9]{40}(?![\s\S])/;
|
|
15
|
+
function ensure(condition, message) {
|
|
16
|
+
if (!condition) throw new Error(message);
|
|
17
|
+
}
|
|
18
|
+
function sha(data, algorithm = "sha256") {
|
|
19
|
+
return crypto.createHash(algorithm).update(data).digest("hex");
|
|
20
|
+
}
|
|
21
|
+
function sorted(value) {
|
|
22
|
+
if (Array.isArray(value)) return value.map(sorted);
|
|
23
|
+
if (value && typeof value === "object" && !Buffer.isBuffer(value))
|
|
24
|
+
return Object.fromEntries(
|
|
25
|
+
Object.keys(value)
|
|
26
|
+
.sort()
|
|
27
|
+
.map((key) => [key, sorted(value[key])]),
|
|
28
|
+
);
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
function stable(value) {
|
|
32
|
+
return JSON.stringify(sorted(value));
|
|
33
|
+
}
|
|
34
|
+
function json(value) {
|
|
35
|
+
try {
|
|
36
|
+
return JSON.parse(Buffer.isBuffer(value) ? value.toString("utf8") : value);
|
|
37
|
+
} catch {
|
|
38
|
+
throw new Error("Invalid JSON data");
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function read(file) {
|
|
42
|
+
return json(fs.readFileSync(file));
|
|
43
|
+
}
|
|
44
|
+
function save(file, value) {
|
|
45
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
46
|
+
const tmp = file + ".tmp";
|
|
47
|
+
fs.writeFileSync(tmp, JSON.stringify(value, null, 2) + "\n");
|
|
48
|
+
fs.renameSync(tmp, file);
|
|
49
|
+
}
|
|
50
|
+
function temp(prefix) {
|
|
51
|
+
return fs.mkdtempSync(path.join(os.tmpdir(), prefix));
|
|
52
|
+
}
|
|
53
|
+
function remove(directory) {
|
|
54
|
+
fs.rmSync(directory, { recursive: true, force: true });
|
|
55
|
+
}
|
|
56
|
+
async function temporary(prefix, action) {
|
|
57
|
+
const dir = temp(prefix);
|
|
58
|
+
try {
|
|
59
|
+
return await action(dir);
|
|
60
|
+
} finally {
|
|
61
|
+
remove(dir);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function safePath(value, absolute = false) {
|
|
65
|
+
return (
|
|
66
|
+
typeof value === "string" &&
|
|
67
|
+
!!value &&
|
|
68
|
+
!/[\\\x00-\x1f]/.test(value) &&
|
|
69
|
+
value.startsWith("/") === absolute &&
|
|
70
|
+
value
|
|
71
|
+
.replace(/^\//, "")
|
|
72
|
+
.split("/")
|
|
73
|
+
.every((part) => part && part !== "." && part !== "..")
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
function repositoryPath(root, relative) {
|
|
77
|
+
ensure(safePath(relative), "Expected a repository-relative path");
|
|
78
|
+
const base = fs.realpathSync(root);
|
|
79
|
+
const target = path.resolve(base, relative);
|
|
80
|
+
let cursor = base;
|
|
81
|
+
for (const part of relative.split("/")) {
|
|
82
|
+
cursor = path.join(cursor, part);
|
|
83
|
+
if (
|
|
84
|
+
fs.existsSync(cursor) ||
|
|
85
|
+
(() => {
|
|
86
|
+
try {
|
|
87
|
+
fs.lstatSync(cursor);
|
|
88
|
+
return true;
|
|
89
|
+
} catch {
|
|
90
|
+
return false;
|
|
91
|
+
}
|
|
92
|
+
})()
|
|
93
|
+
) {
|
|
94
|
+
const real = fs.realpathSync(cursor);
|
|
95
|
+
ensure(real.startsWith(base + path.sep), "Path leaves repository");
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return target;
|
|
99
|
+
}
|
|
100
|
+
function exec(
|
|
101
|
+
args,
|
|
102
|
+
{
|
|
103
|
+
cwd,
|
|
104
|
+
env = process.env,
|
|
105
|
+
input,
|
|
106
|
+
allowed = [0],
|
|
107
|
+
timeout = 1800000,
|
|
108
|
+
maxBuffer = 32 * 1024 * 1024,
|
|
109
|
+
binary = false,
|
|
110
|
+
label,
|
|
111
|
+
} = {},
|
|
112
|
+
) {
|
|
113
|
+
ensure(
|
|
114
|
+
Array.isArray(args) &&
|
|
115
|
+
args.length &&
|
|
116
|
+
args.every((arg) => typeof arg === "string" && !arg.includes("\0")),
|
|
117
|
+
"Expected command argv",
|
|
118
|
+
);
|
|
119
|
+
const result = spawnSync(args[0], args.slice(1), {
|
|
120
|
+
cwd,
|
|
121
|
+
env,
|
|
122
|
+
input,
|
|
123
|
+
timeout,
|
|
124
|
+
maxBuffer,
|
|
125
|
+
encoding: binary ? null : "utf8",
|
|
126
|
+
windowsHide: true,
|
|
127
|
+
});
|
|
128
|
+
ensure(
|
|
129
|
+
!result.error && allowed.includes(result.status),
|
|
130
|
+
`${label || path.basename(args[0]) + " " + (args[1] || "")} failed (exit ${result.status})`,
|
|
131
|
+
);
|
|
132
|
+
return {
|
|
133
|
+
stdout: result.stdout,
|
|
134
|
+
stderr: result.stderr,
|
|
135
|
+
status: result.status,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
139
|
+
async function responseBytes(response, limit) {
|
|
140
|
+
const chunks = [];
|
|
141
|
+
let size = 0;
|
|
142
|
+
for await (const chunk of response.body || []) {
|
|
143
|
+
size += chunk.length;
|
|
144
|
+
if (size > limit) {
|
|
145
|
+
await response.body?.cancel?.().catch(() => {});
|
|
146
|
+
throw new Error("Download limit exceeded");
|
|
147
|
+
}
|
|
148
|
+
chunks.push(Buffer.from(chunk));
|
|
149
|
+
}
|
|
150
|
+
return Buffer.concat(chunks);
|
|
151
|
+
}
|
|
152
|
+
async function request(
|
|
153
|
+
url,
|
|
154
|
+
{
|
|
155
|
+
method = "GET",
|
|
156
|
+
headers = {},
|
|
157
|
+
body,
|
|
158
|
+
limit = 32 * 1024 * 1024,
|
|
159
|
+
timeout = 60000,
|
|
160
|
+
fetchImpl = fetch,
|
|
161
|
+
} = {},
|
|
162
|
+
) {
|
|
163
|
+
const controller = new AbortController();
|
|
164
|
+
const timer = setTimeout(() => controller.abort(), timeout);
|
|
165
|
+
try {
|
|
166
|
+
const response = await fetchImpl(url, {
|
|
167
|
+
method,
|
|
168
|
+
headers,
|
|
169
|
+
body,
|
|
170
|
+
redirect: "manual",
|
|
171
|
+
signal: controller.signal,
|
|
172
|
+
});
|
|
173
|
+
const bytes = await responseBytes(response, limit);
|
|
174
|
+
return { status: response.status, headers: response.headers, body: bytes };
|
|
175
|
+
} finally {
|
|
176
|
+
clearTimeout(timer);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
// Extract data only. No archive entry is ever written to the filesystem.
|
|
180
|
+
function tarEntries(
|
|
181
|
+
gzip,
|
|
182
|
+
{
|
|
183
|
+
maxUnpacked = 1024 * 1024 * 1024,
|
|
184
|
+
maxFile = 32 * 1024 * 1024,
|
|
185
|
+
maxEntries = 30000,
|
|
186
|
+
} = {},
|
|
187
|
+
) {
|
|
188
|
+
const bytes = gunzipSync(gzip, {
|
|
189
|
+
maxOutputLength: maxUnpacked + maxEntries * 1024,
|
|
190
|
+
});
|
|
191
|
+
const entries = [];
|
|
192
|
+
let offset = 0;
|
|
193
|
+
let total = 0;
|
|
194
|
+
let pax = {};
|
|
195
|
+
let longName;
|
|
196
|
+
const str = (data) => data.toString("utf8").replace(/\0.*(?![\s\S])/s, "");
|
|
197
|
+
while (offset + 512 <= bytes.length) {
|
|
198
|
+
const header = bytes.subarray(offset, offset + 512);
|
|
199
|
+
offset += 512;
|
|
200
|
+
if (header.every((byte) => byte === 0)) break;
|
|
201
|
+
const sizeText = str(header.subarray(124, 136)).trim();
|
|
202
|
+
ensure(/^[0-7]+(?![\s\S])/.test(sizeText), "Invalid tar entry size");
|
|
203
|
+
const size = parseInt(sizeText, 8);
|
|
204
|
+
const expected = parseInt(str(header.subarray(148, 156)).trim(), 8);
|
|
205
|
+
const sum = header.reduce(
|
|
206
|
+
(sum, b, i) => sum + (i >= 148 && i < 156 ? 32 : b),
|
|
207
|
+
0,
|
|
208
|
+
);
|
|
209
|
+
ensure(
|
|
210
|
+
expected === sum && size <= maxFile && offset + size <= bytes.length,
|
|
211
|
+
"Invalid or oversized tar entry",
|
|
212
|
+
);
|
|
213
|
+
const data = bytes.subarray(offset, offset + size);
|
|
214
|
+
offset += Math.ceil(size / 512) * 512;
|
|
215
|
+
const type = String.fromCharCode(header[156] || 48);
|
|
216
|
+
if (type === "x") {
|
|
217
|
+
pax = {};
|
|
218
|
+
let pos = 0;
|
|
219
|
+
while (pos < data.length) {
|
|
220
|
+
const space = data.indexOf(32, pos);
|
|
221
|
+
ensure(space > pos, "Invalid PAX record");
|
|
222
|
+
const len = Number(data.subarray(pos, space).toString());
|
|
223
|
+
ensure(
|
|
224
|
+
Number.isInteger(len) &&
|
|
225
|
+
len > space - pos + 1 &&
|
|
226
|
+
pos + len <= data.length &&
|
|
227
|
+
data[pos + len - 1] === 10,
|
|
228
|
+
"Invalid PAX length",
|
|
229
|
+
);
|
|
230
|
+
const row = data.subarray(space + 1, pos + len - 1).toString();
|
|
231
|
+
const split = row.indexOf("=");
|
|
232
|
+
ensure(split > 0, "Invalid PAX value");
|
|
233
|
+
pax[row.slice(0, split)] = row.slice(split + 1);
|
|
234
|
+
pos += len;
|
|
235
|
+
}
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
if (type === "L") {
|
|
239
|
+
longName = str(data).replace(/\n(?![\s\S])/, "");
|
|
240
|
+
continue;
|
|
241
|
+
}
|
|
242
|
+
const prefix =
|
|
243
|
+
str(header.subarray(257, 263)) === "ustar"
|
|
244
|
+
? str(header.subarray(345, 500))
|
|
245
|
+
: "";
|
|
246
|
+
const name =
|
|
247
|
+
pax.path ||
|
|
248
|
+
longName ||
|
|
249
|
+
(prefix ? prefix + "/" : "") + str(header.subarray(0, 100));
|
|
250
|
+
ensure(
|
|
251
|
+
pax.size === undefined || Number(pax.size) === size,
|
|
252
|
+
"Unsupported PAX size",
|
|
253
|
+
);
|
|
254
|
+
pax = {};
|
|
255
|
+
longName = undefined;
|
|
256
|
+
ensure(
|
|
257
|
+
type === "0" || type === "5",
|
|
258
|
+
"Links and special archive entries are forbidden",
|
|
259
|
+
);
|
|
260
|
+
total += size;
|
|
261
|
+
ensure(
|
|
262
|
+
total <= maxUnpacked && entries.length < maxEntries,
|
|
263
|
+
"Archive expansion limit exceeded",
|
|
264
|
+
);
|
|
265
|
+
entries.push({
|
|
266
|
+
name: name.replace(/\/(?![\s\S])/, ""),
|
|
267
|
+
directory: type === "5",
|
|
268
|
+
data,
|
|
269
|
+
});
|
|
270
|
+
}
|
|
271
|
+
return entries;
|
|
272
|
+
}
|
|
273
|
+
module.exports = {
|
|
274
|
+
fs,
|
|
275
|
+
path,
|
|
276
|
+
SEMVER,
|
|
277
|
+
DIGEST,
|
|
278
|
+
HEX64,
|
|
279
|
+
COMMIT,
|
|
280
|
+
ensure,
|
|
281
|
+
sha,
|
|
282
|
+
stable,
|
|
283
|
+
sorted,
|
|
284
|
+
json,
|
|
285
|
+
read,
|
|
286
|
+
save,
|
|
287
|
+
temp,
|
|
288
|
+
remove,
|
|
289
|
+
temporary,
|
|
290
|
+
safePath,
|
|
291
|
+
repositoryPath,
|
|
292
|
+
exec,
|
|
293
|
+
sleep,
|
|
294
|
+
request,
|
|
295
|
+
tarEntries,
|
|
296
|
+
equal: isDeepStrictEqual,
|
|
297
|
+
};
|
|
@@ -9,7 +9,7 @@ function changelogAppend(message) {
|
|
|
9
9
|
checks: [
|
|
10
10
|
{ name: 'git-repo', run: requireGitRepo },
|
|
11
11
|
{ name: 'changelog-exists', run: () => requireFileExists(CHANGELOG_FILE) },
|
|
12
|
-
{ name: 'prepare-fragment', dependsOn: ['git-repo', 'changelog-exists'], run: () => prepareChangelogEntry(message) }
|
|
12
|
+
{ name: 'prepare-fragment', prepare: true, dependsOn: ['git-repo', 'changelog-exists'], run: () => prepareChangelogEntry(message) }
|
|
13
13
|
],
|
|
14
14
|
steps: [
|
|
15
15
|
{ name: 'write-fragment', run: appendPreparedChangelogEntry }
|
|
@@ -41,6 +41,14 @@ function requireReleaseState(context) {
|
|
|
41
41
|
}
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
+
function readLocalReleaseState(context) {
|
|
45
|
+
const local = splitOpenRelease(fs.readFileSync(CHANGELOG_FILE, 'utf8'), context.stableVersion);
|
|
46
|
+
return { ok: true, data: {
|
|
47
|
+
openReleaseVersion: local.open?.version || null,
|
|
48
|
+
releaseChangelogState: { local, production: { ...local, open: null, entries: [] } }
|
|
49
|
+
} };
|
|
50
|
+
}
|
|
51
|
+
|
|
44
52
|
function requireReleaseFragments(context) {
|
|
45
53
|
try {
|
|
46
54
|
// Empty is valid only for an already prepared release, including retry after a failed push.
|
|
@@ -77,4 +85,4 @@ function appendOpenRelease(context) {
|
|
|
77
85
|
}
|
|
78
86
|
}
|
|
79
87
|
|
|
80
|
-
module.exports = { resolveReleaseState, requireReleaseState, requireReleaseFragments, appendOpenRelease };
|
|
88
|
+
module.exports = { resolveReleaseState, readLocalReleaseState, requireReleaseState, requireReleaseFragments, appendOpenRelease };
|
package/src/chart/deploy.js
CHANGED
|
@@ -30,10 +30,10 @@ function chartDeploy(options = {}) {
|
|
|
30
30
|
{ name: 'branch-up-to-date', run: requireCurrentBranchUpToDateWithRemote },
|
|
31
31
|
{ name: 'remote-origin', run: requireRemoteOrigin },
|
|
32
32
|
{ name: 'remote-reachable', run: requireRemoteReachable },
|
|
33
|
-
{ name: 'single-chart', run: requireSingleChart },
|
|
34
|
-
{ name: 'helmrelease-files', run: requireHelmReleaseFiles },
|
|
33
|
+
{ name: 'single-chart', prepare: true, run: requireSingleChart },
|
|
34
|
+
{ name: 'helmrelease-files', prepare: true, run: requireHelmReleaseFiles },
|
|
35
35
|
{
|
|
36
|
-
name: 'filter-instances',
|
|
36
|
+
name: 'filter-instances', prepare: true,
|
|
37
37
|
run: (ctx) => {
|
|
38
38
|
const requested = parseInstancesOption(instancesOption);
|
|
39
39
|
if (requested.length === 0) {
|
package/src/chart/start.js
CHANGED
|
@@ -15,9 +15,9 @@ function chartStart(options = {}) {
|
|
|
15
15
|
{ name: 'clean-working-tree', run: requireCleanWorkingTree },
|
|
16
16
|
{ name: 'on-main-branch', run: requireOnMainBranch },
|
|
17
17
|
{ name: 'branch-up-to-date', run: requireCurrentBranchUpToDateWithRemote },
|
|
18
|
-
{ name: 'single-chart', run: requireSingleChart },
|
|
18
|
+
{ name: 'single-chart', prepare: true, run: requireSingleChart },
|
|
19
19
|
{
|
|
20
|
-
name: 'chart-changelog-version',
|
|
20
|
+
name: 'chart-changelog-version', prepare: true,
|
|
21
21
|
requires: ['chartFilePath'],
|
|
22
22
|
run: (ctx) => requireChartChangelogVersion(path.dirname(ctx.chartFilePath))
|
|
23
23
|
},
|
package/src/chart/verify.js
CHANGED
|
@@ -3,7 +3,7 @@ const path = require('path');
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const { logSuccess, logError, colors } = require('../common/utils');
|
|
5
5
|
const { runCommand } = require('../cli/command-executor');
|
|
6
|
-
const { requireGitRepo, requireCurrentBranchUpToDateWithRemote, requireSingleValuesYaml, requireIngressPathSections, requireSourcePathDirectory, requireNextProject, requireBuildCommandSupport } = require('../preflight');
|
|
6
|
+
const { requireGitRepo, requireCurrentBranchUpToDateWithRemote, requireSingleValuesYaml, readIngressPathSections, requireIngressPathSections, requireSourcePathDirectory, readSourceProject, requireNextProject, requireBuildCommandSupport } = require('../preflight');
|
|
7
7
|
const { normalizeList, normalizeToBeList, collectRoutesFromFilesystem, collectRoutesFromBuildArtifacts } = require('./routes');
|
|
8
8
|
const FIXED_ASSET_PATHS = [
|
|
9
9
|
'/_next/image$',
|
|
@@ -96,10 +96,10 @@ function chartVerify(sourcePath) {
|
|
|
96
96
|
checks: [
|
|
97
97
|
{ name: 'git-repo', run: requireGitRepo },
|
|
98
98
|
{ name: 'branch-up-to-date', run: requireCurrentBranchUpToDateWithRemote },
|
|
99
|
-
{ name: 'single-values-yaml', run: requireSingleValuesYaml },
|
|
100
|
-
{ name: 'values-ingress-sections', requires: ['valuesYamlPath'], run: (ctx) => requireIngressPathSections(ctx.valuesYamlPath) },
|
|
101
|
-
{ name: 'source-path-directory', run: () => requireSourcePathDirectory(sourcePath) },
|
|
102
|
-
{ name: 'next-project', requires: ['sourcePath'], run: (ctx) => requireNextProject(ctx.sourcePath) },
|
|
99
|
+
{ name: 'single-values-yaml', prepare: true, run: requireSingleValuesYaml },
|
|
100
|
+
{ name: 'values-ingress-sections', prepare: (ctx) => readIngressPathSections(ctx.valuesYamlPath), requires: ['valuesYamlPath'], run: (ctx) => requireIngressPathSections(ctx.valuesYamlPath) },
|
|
101
|
+
{ name: 'source-path-directory', prepare: true, run: () => requireSourcePathDirectory(sourcePath) },
|
|
102
|
+
{ name: 'next-project', prepare: (ctx) => readSourceProject(ctx.sourcePath), requires: ['sourcePath'], run: (ctx) => requireNextProject(ctx.sourcePath) },
|
|
103
103
|
{ name: 'build-command-support', requires: ['sourcePath'], run: (ctx) => requireBuildCommandSupport(ctx.sourcePath) }
|
|
104
104
|
],
|
|
105
105
|
steps: [
|
|
@@ -59,7 +59,14 @@ function describePreflight(checkName, context) {
|
|
|
59
59
|
}
|
|
60
60
|
|
|
61
61
|
function withCommandOptions(options, action) {
|
|
62
|
-
|
|
62
|
+
if (options?.dry && options?.verify === false) {
|
|
63
|
+
throw new Error('--dry и --no-verify нельзя использовать вместе.');
|
|
64
|
+
}
|
|
65
|
+
return commandContext.run({
|
|
66
|
+
dry: Boolean(options?.dry),
|
|
67
|
+
silence: Boolean(options?.silence),
|
|
68
|
+
verify: options?.verify !== false
|
|
69
|
+
}, action);
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
function withDryRun(dry, action) {
|
|
@@ -70,18 +77,22 @@ function isDryRun() {
|
|
|
70
77
|
return Boolean(commandContext.getStore()?.dry);
|
|
71
78
|
}
|
|
72
79
|
|
|
80
|
+
function isVerificationSkipped() {
|
|
81
|
+
return commandContext.getStore()?.verify === false;
|
|
82
|
+
}
|
|
83
|
+
|
|
73
84
|
function reportDryRun(_name) {
|
|
74
85
|
logSuccess('✅', 'Все предпроверки пройдены, проблем нет. Действия не выполнялись.');
|
|
75
86
|
}
|
|
76
87
|
|
|
77
88
|
function reportPreflight(_commandName, checkName, description = PREFLIGHT_DESCRIPTIONS[checkName] || checkName) {
|
|
78
|
-
if (!commandContext.getStore()?.silence) {
|
|
89
|
+
if (!isVerificationSkipped() && !commandContext.getStore()?.silence) {
|
|
79
90
|
logSuccess('🔎', 'Проверено (%s): %s.', checkName, description);
|
|
80
91
|
}
|
|
81
92
|
}
|
|
82
93
|
|
|
83
94
|
function reportNoPreflights(_name) {
|
|
84
|
-
logSuccess('🔎', 'Предпроверок нет.');
|
|
95
|
+
if (!isVerificationSkipped()) logSuccess('🔎', 'Предпроверок нет.');
|
|
85
96
|
}
|
|
86
97
|
|
|
87
98
|
function normalizeCheckResult(result, fallbackReason) {
|
|
@@ -120,6 +131,24 @@ async function runCommand(spec) {
|
|
|
120
131
|
let failedPreflights = 0;
|
|
121
132
|
const results = new Map();
|
|
122
133
|
for (const check of checks) {
|
|
134
|
+
// Some checks also read inputs needed by the action. In no-verify mode
|
|
135
|
+
// only their explicit preparation runs, without policy checks or reports.
|
|
136
|
+
if (isVerificationSkipped()) {
|
|
137
|
+
if (!check.prepare) continue;
|
|
138
|
+
const prepare = check.prepare === true ? check.run : check.prepare;
|
|
139
|
+
let result;
|
|
140
|
+
try {
|
|
141
|
+
result = normalizeCheckResult(await prepare(context), 'Не удалось получить данные команды.');
|
|
142
|
+
} catch (error) {
|
|
143
|
+
result = { ok: false, reason: error?.message || String(error) };
|
|
144
|
+
}
|
|
145
|
+
if (!result.ok) {
|
|
146
|
+
logError('❌', 'Не удалось подготовить команду (%s): %s', check.name, result.reason);
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
if (result.data) Object.assign(context, result.data);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
123
152
|
const checkName = check.name || 'unknown-check';
|
|
124
153
|
const failedDependencies = (check.dependsOn || []).filter((dependency) => results.get(dependency) !== true);
|
|
125
154
|
const missingInputs = (check.requires || []).filter((key) => context[key] === undefined || context[key] === null);
|
|
@@ -178,4 +207,4 @@ async function runCommand(spec) {
|
|
|
178
207
|
return true;
|
|
179
208
|
}
|
|
180
209
|
|
|
181
|
-
module.exports = { runCommand, withCommandOptions, withDryRun, isDryRun, reportDryRun, reportPreflight, reportNoPreflights };
|
|
210
|
+
module.exports = { runCommand, withCommandOptions, withDryRun, isDryRun, isVerificationSkipped, reportDryRun, reportPreflight, reportNoPreflights };
|
package/src/cli/commands.js
CHANGED
|
@@ -4,9 +4,12 @@ const hotfix = require('../hotfix');
|
|
|
4
4
|
const changelog = require('../changelog');
|
|
5
5
|
const chart = require('../chart');
|
|
6
6
|
const token = require('../token');
|
|
7
|
+
const nextjs = require('../nextjs');
|
|
8
|
+
const sbom = require('../sbom');
|
|
9
|
+
const security = require('../security');
|
|
7
10
|
const { upgrade } = require('./update-check');
|
|
8
11
|
const { version: pkgVersion } = require('../../package.json');
|
|
9
|
-
const { runAction } = require('./run-action');
|
|
12
|
+
const { runAction, runAutomationAction } = require('./run-action');
|
|
10
13
|
|
|
11
14
|
function registerCommands(program) {
|
|
12
15
|
program
|
|
@@ -17,10 +20,93 @@ program
|
|
|
17
20
|
program
|
|
18
21
|
.command("upgrade")
|
|
19
22
|
.description("Обновить Spectrum CLI через npm install -g spectrum-cli")
|
|
23
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
20
24
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
21
25
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
22
26
|
.action((command) => runAction(upgrade, command));
|
|
23
27
|
|
|
28
|
+
// Команды Next.js
|
|
29
|
+
const nextjsCmd = program
|
|
30
|
+
.command("nextjs")
|
|
31
|
+
.description("Обработка Next.js build artifacts");
|
|
32
|
+
|
|
33
|
+
nextjsCmd
|
|
34
|
+
.command("licenses")
|
|
35
|
+
.description("Сохранить LICENSE встроенных пакетов в standalone")
|
|
36
|
+
.allowExcessArguments(false)
|
|
37
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
38
|
+
.option("-d, --dry", "Запустить только предпроверки")
|
|
39
|
+
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
40
|
+
.action((command) => runAutomationAction(nextjs.licenses, command));
|
|
41
|
+
|
|
42
|
+
// Команды SBOM
|
|
43
|
+
const sbomCmd = program
|
|
44
|
+
.command("sbom")
|
|
45
|
+
.description("Работа с SBOM");
|
|
46
|
+
|
|
47
|
+
const catalogCmd = sbomCmd
|
|
48
|
+
.command("catalog")
|
|
49
|
+
.description("Каталоги встроенных зависимостей Next.js");
|
|
50
|
+
|
|
51
|
+
catalogCmd
|
|
52
|
+
.command("resolve")
|
|
53
|
+
.description("Найти проверенные OCI-каталоги для образа приложения")
|
|
54
|
+
.allowExcessArguments(false)
|
|
55
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
56
|
+
.option("-d, --dry", "Запустить только предпроверки")
|
|
57
|
+
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
58
|
+
.action((command) => runAutomationAction(sbom.resolve, command));
|
|
59
|
+
|
|
60
|
+
catalogCmd
|
|
61
|
+
.command("populate")
|
|
62
|
+
.description("Построить и опубликовать каталог из официальных исходников")
|
|
63
|
+
.allowExcessArguments(false)
|
|
64
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
65
|
+
.option("-d, --dry", "Запустить только предпроверки")
|
|
66
|
+
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
67
|
+
.action((command) => runAutomationAction(sbom.populate, command));
|
|
68
|
+
|
|
69
|
+
// Команды security
|
|
70
|
+
const securityCmd = program
|
|
71
|
+
.command("security")
|
|
72
|
+
.description("Ночная проверка и исправление HIGH/CRITICAL");
|
|
73
|
+
|
|
74
|
+
securityCmd
|
|
75
|
+
.command("scan")
|
|
76
|
+
.description("Проверить production npm-зависимости и runtime-образ")
|
|
77
|
+
.allowExcessArguments(false)
|
|
78
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
79
|
+
.option("-d, --dry", "Запустить только предпроверки")
|
|
80
|
+
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
81
|
+
.action((command) => runAutomationAction(security.scan, command));
|
|
82
|
+
|
|
83
|
+
securityCmd
|
|
84
|
+
.command("track")
|
|
85
|
+
.description("Создать или найти YouTrack-задачи в центральном pipeline")
|
|
86
|
+
.allowExcessArguments(false)
|
|
87
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
88
|
+
.option("-d, --dry", "Запустить только предпроверки")
|
|
89
|
+
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
90
|
+
.action((command) => runAutomationAction(security.track, command));
|
|
91
|
+
|
|
92
|
+
securityCmd
|
|
93
|
+
.command("reconcile")
|
|
94
|
+
.description("Дождаться центрального tracker и получить номера задач")
|
|
95
|
+
.allowExcessArguments(false)
|
|
96
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
97
|
+
.option("-d, --dry", "Запустить только предпроверки")
|
|
98
|
+
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
99
|
+
.action((command) => runAutomationAction(security.reconcile, command));
|
|
100
|
+
|
|
101
|
+
securityCmd
|
|
102
|
+
.command("repair")
|
|
103
|
+
.description("Исправить, проверить и создать отдельный MR на проектную задачу")
|
|
104
|
+
.allowExcessArguments(false)
|
|
105
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
106
|
+
.option("-d, --dry", "Запустить только предпроверки")
|
|
107
|
+
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
108
|
+
.action((command) => runAutomationAction(security.repair, command));
|
|
109
|
+
|
|
24
110
|
// Команды релиза
|
|
25
111
|
const releaseCmd = program
|
|
26
112
|
.command("release")
|
|
@@ -30,6 +116,7 @@ releaseCmd
|
|
|
30
116
|
.command("start")
|
|
31
117
|
.description("Схлопнуть fragments и атомарно отправить release commit в dev и main/master")
|
|
32
118
|
.option("--stable", "Явно перейти с 0.x на первый стабильный релиз 1.0.0")
|
|
119
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
33
120
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
34
121
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
35
122
|
.action((command) => runAction(release.releaseStart, command));
|
|
@@ -37,6 +124,7 @@ releaseCmd
|
|
|
37
124
|
releaseCmd
|
|
38
125
|
.command("close")
|
|
39
126
|
.description("Свести стабильный релиз из main/master в dev")
|
|
127
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
40
128
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
41
129
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
42
130
|
.action((command) => runAction(release.releaseClose, command));
|
|
@@ -44,6 +132,7 @@ releaseCmd
|
|
|
44
132
|
releaseCmd
|
|
45
133
|
.command("deploy")
|
|
46
134
|
.description("Создать и отправить только стабильный тег release/X.Y.Z")
|
|
135
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
47
136
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
48
137
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
49
138
|
.action((command) => runAction(git.gitCreateTagAndPush, command));
|
|
@@ -52,16 +141,19 @@ const hotfixCmd = program.command("hotfix").description("Изолированн
|
|
|
52
141
|
|
|
53
142
|
hotfixCmd.command("start")
|
|
54
143
|
.description("Подготовить 🩹 CHANGELOG на hotfix/* или main/master, без commit, push и merge")
|
|
144
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
55
145
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
56
146
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
57
147
|
.action((command) => runAction(hotfix.hotfixStart, command));
|
|
58
148
|
hotfixCmd.command("deploy")
|
|
59
149
|
.description("После merge хотфикса отправить stable-тег с main/master")
|
|
150
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
60
151
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
61
152
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
62
153
|
.action((command) => runAction(hotfix.hotfixDeploy, command));
|
|
63
154
|
hotfixCmd.command("close")
|
|
64
155
|
.description("После stable pipeline свести production в dev")
|
|
156
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
65
157
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
66
158
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
67
159
|
.action((command) => runAction(hotfix.hotfixClose, command));
|
|
@@ -74,6 +166,7 @@ const changelogCmd = program
|
|
|
74
166
|
changelogCmd
|
|
75
167
|
.command("append <message>")
|
|
76
168
|
.description("Создать changelog fragment с номером задачи из ветки")
|
|
169
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
77
170
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
78
171
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
79
172
|
.action((message, command) => runAction(() => changelog.changelogAppend(message), command));
|
|
@@ -81,6 +174,7 @@ changelogCmd
|
|
|
81
174
|
changelogCmd
|
|
82
175
|
.command("check")
|
|
83
176
|
.description("Проверить CHANGELOG.md и changelog fragments")
|
|
177
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
84
178
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
85
179
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
86
180
|
.action((command) => runAction(changelog.changelogCheck, command));
|
|
@@ -88,6 +182,7 @@ changelogCmd
|
|
|
88
182
|
changelogCmd
|
|
89
183
|
.command("write")
|
|
90
184
|
.description("Привести заголовки CHANGELOG.md к формату релиза и запустить Prettier")
|
|
185
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
91
186
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
92
187
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
93
188
|
.action((command) => runAction(changelog.changelogWrite, command));
|
|
@@ -102,7 +197,8 @@ chartCmd
|
|
|
102
197
|
.description("Создать и отправить chart-тег по верхней версии CHANGELOG.md чарта")
|
|
103
198
|
.allowExcessArguments(false)
|
|
104
199
|
.option("-f, --force", "Разрешить версию не больше последней версии тега на origin")
|
|
105
|
-
.option("
|
|
200
|
+
.option("--no-wait", "Не ждать публикации версии чарта в Helm-registry")
|
|
201
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
106
202
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
107
203
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
108
204
|
.action((command) => runAction(chart.chartStart, command));
|
|
@@ -110,6 +206,7 @@ chartCmd
|
|
|
110
206
|
chartCmd
|
|
111
207
|
.command("verify <source_path>")
|
|
112
208
|
.description("Проверить пути ingress chart относительно исходников Next.js")
|
|
209
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
113
210
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
114
211
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
115
212
|
.action((sourcePath, command) => runAction(() => chart.chartVerify(sourcePath), command));
|
|
@@ -118,6 +215,7 @@ chartCmd
|
|
|
118
215
|
.command("deploy")
|
|
119
216
|
.description("Задеплоить последнюю версию chart в файлы helmrelease")
|
|
120
217
|
.option("-i, --instances <names>", "Список инстансов через запятую (по умолчанию все)")
|
|
218
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
121
219
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
122
220
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
123
221
|
.action((command) => runAction(chart.chartDeploy, command));
|
|
@@ -129,6 +227,7 @@ const tokenCmd = program
|
|
|
129
227
|
tokenCmd
|
|
130
228
|
.command("rotate")
|
|
131
229
|
.description("Пролить SC_BOT_PAT в CI variables через owner PAT")
|
|
230
|
+
.option("-n, --no-verify", "Выполнить команду без предпроверок")
|
|
132
231
|
.option("-d, --dry", "Запустить только предпроверки")
|
|
133
232
|
.option("-s, --silence", "Скрыть сообщения об успешно пройденных предпроверках")
|
|
134
233
|
.action((command) => runAction(token.tokenRotate, command));
|