@rightkit/release 0.2.70 → 0.2.72
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/build-release.mjs +19 -14
- package/cache-command.mjs +7 -4
- package/cli/right-release.mjs +0 -0
- package/direct-bootstrap.mjs +482 -0
- package/github-release.mjs +160 -2
- package/hardening-evidence.mjs +54 -0
- package/hardeningscan.mjs +78 -55
- package/native-cargo-layout.mjs +239 -0
- package/native-release-finalization.mjs +259 -0
- package/package.json +9 -11
- package/preflight.mjs +6 -3
- package/registry-parity.mjs +2 -2
- package/release-invocation.mjs +10 -2
- package/release-state.mjs +5 -2
- package/release.mjs +235 -16
- package/rightkit-versions.json +8 -3
- package/sign-release-manifest.mjs +223 -0
- package/supply-chain-evidence.mjs +175 -0
package/release.mjs
CHANGED
|
@@ -9,10 +9,19 @@ import { validateRightKitCargoContract } from "./cargo-contract.mjs";
|
|
|
9
9
|
import { assertQaBackdoorContract } from "./qa-contract.mjs";
|
|
10
10
|
import { assertLegalReleaseContract } from "./legal-contract.mjs";
|
|
11
11
|
import { assertPrimaryReleaseCheckout, dirtyBuildInputs, resolveReleaseBuildInputs } from "./release-invocation.mjs";
|
|
12
|
+
import { resolveNativeCargoLayout } from "./native-cargo-layout.mjs";
|
|
13
|
+
import {
|
|
14
|
+
canonicalPackageIdentity,
|
|
15
|
+
createNativeFinalizationReceipt,
|
|
16
|
+
mintNativeProvenance,
|
|
17
|
+
resolveNativeFinalizerConfig,
|
|
18
|
+
validateNativeFinalizationOutput,
|
|
19
|
+
} from "./native-release-finalization.mjs";
|
|
12
20
|
import { collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
|
|
13
21
|
import { patchTauriBundleType } from "./tauri-bundle-marker.mjs";
|
|
14
22
|
import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
|
|
15
23
|
import { terminateProcessTree } from "./heavy-command.mjs";
|
|
24
|
+
import { materializeHardeningEvidence } from "./hardening-evidence.mjs";
|
|
16
25
|
|
|
17
26
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
18
27
|
const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
|
|
@@ -41,7 +50,6 @@ const opts = {
|
|
|
41
50
|
install: false,
|
|
42
51
|
dryRun: false,
|
|
43
52
|
skipChecks: false,
|
|
44
|
-
skipHardening: false,
|
|
45
53
|
doctor: false,
|
|
46
54
|
tier: null,
|
|
47
55
|
};
|
|
@@ -56,7 +64,7 @@ for (let i = 0; i < args.length; i++) {
|
|
|
56
64
|
else if (arg === "--install") opts.install = true;
|
|
57
65
|
else if (arg === "--dry-run") opts.dryRun = true;
|
|
58
66
|
else if (arg === "--skip-checks") opts.skipChecks = true;
|
|
59
|
-
else if (arg === "--skip-hardening")
|
|
67
|
+
else if (arg === "--skip-hardening") fail("--skip-hardening is forbidden; use exact artifact/source-bound hardeningAllowances");
|
|
60
68
|
else if (arg === "--doctor") opts.doctor = true;
|
|
61
69
|
else if (arg === "--tier") opts.tier = args[++i];
|
|
62
70
|
else if (arg.startsWith("--tier=")) opts.tier = arg.slice("--tier=".length);
|
|
@@ -92,6 +100,9 @@ if (config.schema !== 1) fail(`unsupported config schema: ${config.schema ?? "<m
|
|
|
92
100
|
|
|
93
101
|
const root = path.dirname(configPath);
|
|
94
102
|
const workdir = path.resolve(root, config.workdir ?? ".");
|
|
103
|
+
const nativeLayout = resolveNativeCargoLayout({ appRoot: root, config });
|
|
104
|
+
const nativeAssembly = config.nativeAssembly;
|
|
105
|
+
const nativeFinalizer = resolveNativeFinalizerConfig(nativeAssembly);
|
|
95
106
|
await validateRightKitPackageContract(root, config.app, config.hostedWorkflows);
|
|
96
107
|
validateRightKitCargoContract(root, RIGHTKIT_CARGO_ALLOWED, config.app ?? path.basename(root));
|
|
97
108
|
const target = config.targets?.[opts.platform];
|
|
@@ -118,7 +129,7 @@ if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.pl
|
|
|
118
129
|
* Assemble preflight inputs from the app's own files. Kept here (not in
|
|
119
130
|
* preflight.mjs) so the check module stays free of release.mjs's config shape.
|
|
120
131
|
*/
|
|
121
|
-
function releasePreflight({ config, configPath, root, repoRoot, platform }) {
|
|
132
|
+
function releasePreflight({ config, configPath, root, repoRoot, platform, nativeLayout: suppliedNativeLayout = nativeLayout }) {
|
|
122
133
|
let version;
|
|
123
134
|
try {
|
|
124
135
|
version = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8")).version;
|
|
@@ -128,10 +139,9 @@ function releasePreflight({ config, configPath, root, repoRoot, platform }) {
|
|
|
128
139
|
// Every Cargo.lock the build compiles from; a vendored-OpenSSL edge in any of
|
|
129
140
|
// them means the build needs a real Windows Perl.
|
|
130
141
|
const cargoLockPaths = [
|
|
131
|
-
|
|
132
|
-
path.join(root, "Cargo.lock"),
|
|
142
|
+
...(suppliedNativeLayout?.lockCandidates ?? []),
|
|
133
143
|
...(config.cargoLocks ?? []).map((entry) => path.resolve(root, entry)),
|
|
134
|
-
];
|
|
144
|
+
].filter((entry, index, entries) => entries.indexOf(entry) === index);
|
|
135
145
|
return collectPreflight({
|
|
136
146
|
platform,
|
|
137
147
|
appRoot: root,
|
|
@@ -140,11 +150,13 @@ function releasePreflight({ config, configPath, root, repoRoot, platform }) {
|
|
|
140
150
|
version,
|
|
141
151
|
configPath,
|
|
142
152
|
cargoLockPaths,
|
|
153
|
+
nativeLayout: suppliedNativeLayout,
|
|
154
|
+
targetLink: suppliedNativeLayout?.targetLink,
|
|
143
155
|
});
|
|
144
156
|
}
|
|
145
157
|
|
|
146
158
|
if (opts.doctor) {
|
|
147
|
-
const { buildInputs } = resolveReleaseBuildInputs({
|
|
159
|
+
const { buildInputs, nativeLayout: resolvedNativeLayout } = resolveReleaseBuildInputs({
|
|
148
160
|
repoRoot: doctorInvocation.repoRoot,
|
|
149
161
|
appRoot: root,
|
|
150
162
|
configPath,
|
|
@@ -174,7 +186,7 @@ if (opts.doctor) {
|
|
|
174
186
|
|
|
175
187
|
// Config above, MACHINE below. Printing config never told anyone whether this
|
|
176
188
|
// box could finish a build; these checks do.
|
|
177
|
-
const checks = releasePreflight({ config, configPath, root, repoRoot: doctorInvocation.repoRoot, platform: opts.platform });
|
|
189
|
+
const checks = releasePreflight({ config, configPath, root, repoRoot: doctorInvocation.repoRoot, platform: opts.platform, nativeLayout: resolvedNativeLayout });
|
|
178
190
|
console.log("preflight:");
|
|
179
191
|
console.log(formatPreflight(checks));
|
|
180
192
|
const failures = preflightFailures(checks);
|
|
@@ -200,6 +212,7 @@ if (!opts.skipChecks) {
|
|
|
200
212
|
for (const script of config.checks ?? []) await runPackageScript(config.packageManager, script, workdir);
|
|
201
213
|
}
|
|
202
214
|
|
|
215
|
+
let nativeFinalization = null;
|
|
203
216
|
let bundleMarkerReceipts = [];
|
|
204
217
|
if (opts.platform === "win") {
|
|
205
218
|
const rawFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
|
|
@@ -215,6 +228,29 @@ if (opts.platform === "win") {
|
|
|
215
228
|
console.log(`right-release: bundle marker ${receipt.bundle} ${receipt.alreadyPatched ? "already applied" : "applied"} at offset ${receipt.offset} in ${path.basename(receipt.file)}`);
|
|
216
229
|
}
|
|
217
230
|
await signWindows(rawFiles, "raw-exe", root);
|
|
231
|
+
if (nativeAssembly?.packageHook && !hasNativeTargetSpecificPrePackage(target, nativeAssembly.packageHook)) {
|
|
232
|
+
await runCommand(nativeAssembly.packageHook, root);
|
|
233
|
+
}
|
|
234
|
+
if (nativeFinalizer) {
|
|
235
|
+
nativeFinalization = await runNativeFinalizer({
|
|
236
|
+
finalizer: nativeFinalizer,
|
|
237
|
+
nativeAssembly,
|
|
238
|
+
target,
|
|
239
|
+
root,
|
|
240
|
+
signedFiles: rawFiles,
|
|
241
|
+
});
|
|
242
|
+
}
|
|
243
|
+
} else if (nativeAssembly) {
|
|
244
|
+
if (nativeAssembly.packageHook) await runCommand(nativeAssembly.packageHook, root);
|
|
245
|
+
if (nativeFinalizer) {
|
|
246
|
+
nativeFinalization = await runNativeFinalizer({
|
|
247
|
+
finalizer: nativeFinalizer,
|
|
248
|
+
nativeAssembly,
|
|
249
|
+
target,
|
|
250
|
+
root,
|
|
251
|
+
signedFiles: target.sign?.prePackageFiles ?? target.sign?.files ?? [],
|
|
252
|
+
});
|
|
253
|
+
}
|
|
218
254
|
}
|
|
219
255
|
|
|
220
256
|
await runCommand(command, root);
|
|
@@ -223,6 +259,8 @@ for (const rel of target.artifacts ?? []) {
|
|
|
223
259
|
await mustExist(path.resolve(root, rel), `missing release artifact: ${rel}`);
|
|
224
260
|
}
|
|
225
261
|
|
|
262
|
+
if (nativeFinalization) await completeNativeFinalization(nativeFinalization, { root, target, nativeAssembly });
|
|
263
|
+
|
|
226
264
|
if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT) {
|
|
227
265
|
const files = target.sign.files.map((p) => path.resolve(root, p));
|
|
228
266
|
for (const file of files) await mustExist(file, `missing signing artifact: ${file}`);
|
|
@@ -241,10 +279,21 @@ if (target.postSign && opts.platform !== "win") {
|
|
|
241
279
|
await runCommand(target.postSign, root);
|
|
242
280
|
}
|
|
243
281
|
|
|
244
|
-
|
|
282
|
+
{
|
|
245
283
|
const scanTargets = target.hardening ?? target.artifacts ?? [];
|
|
246
284
|
if (scanTargets.length) {
|
|
247
|
-
|
|
285
|
+
let hardeningEvidence = null;
|
|
286
|
+
if (target.hardeningAllowances?.length) {
|
|
287
|
+
const receiptRoot = path.resolve(root, ".right-release", "receipts");
|
|
288
|
+
mkdirSync(receiptRoot, { recursive: true });
|
|
289
|
+
hardeningEvidence = path.join(receiptRoot, opts.platform + "-hardening-evidence.json");
|
|
290
|
+
materializeHardeningEvidence({ root, allowances: target.hardeningAllowances, outputPath: hardeningEvidence });
|
|
291
|
+
}
|
|
292
|
+
await run("node", [
|
|
293
|
+
HARDENING_SCAN,
|
|
294
|
+
...(hardeningEvidence ? ["--allow-evidence", hardeningEvidence] : []),
|
|
295
|
+
...scanTargets.map((p) => path.resolve(root, p)),
|
|
296
|
+
], root);
|
|
248
297
|
}
|
|
249
298
|
}
|
|
250
299
|
|
|
@@ -311,13 +360,180 @@ async function mustExist(file, message) {
|
|
|
311
360
|
await access(file).catch(() => fail(message));
|
|
312
361
|
}
|
|
313
362
|
|
|
363
|
+
async function runNativeFinalizer({ finalizer, nativeAssembly, target, root, signedFiles }) {
|
|
364
|
+
if (!finalizer?.cmd) fail(`${config.app} ${opts.platform} nativeAssembly.finalizer must declare a command`);
|
|
365
|
+
const signedArtifacts = nativeSignedArtifacts(signedFiles, root);
|
|
366
|
+
const packageIdentity = nativePackageIdentity({ nativeAssembly, target });
|
|
367
|
+
const provenance = mintNativeProvenance({
|
|
368
|
+
app: config.app,
|
|
369
|
+
version: config.version,
|
|
370
|
+
platform: opts.platform,
|
|
371
|
+
architecture: target.selectedArchitecture ?? target.architecture ?? nativeAssembly?.architecture ?? process.arch,
|
|
372
|
+
targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget,
|
|
373
|
+
signedArtifacts,
|
|
374
|
+
packageIdentity,
|
|
375
|
+
});
|
|
376
|
+
const outputPath = nativeFinalizerOutputPath({ finalizer, target, root });
|
|
377
|
+
const finalizerCommand = {
|
|
378
|
+
...finalizer,
|
|
379
|
+
args: finalizer.args.map((arg) => expandFinalizerToken(arg, { provenance, outputPath, packageIdentity })),
|
|
380
|
+
};
|
|
381
|
+
const stdout = await runCommand(finalizerCommand, root, {
|
|
382
|
+
captureStdout: true,
|
|
383
|
+
env: {
|
|
384
|
+
RIGHT_RELEASE_NATIVE_PROVENANCE: provenance,
|
|
385
|
+
RIGHT_RELEASE_NATIVE_FINALIZATION_RECEIPT: outputPath,
|
|
386
|
+
RIGHT_RELEASE_NATIVE_PACKAGE_IDENTITY: JSON.stringify(packageIdentity),
|
|
387
|
+
RIGHT_RELEASE_NATIVE_SIGNED_ARTIFACTS: JSON.stringify(signedArtifacts),
|
|
388
|
+
},
|
|
389
|
+
});
|
|
390
|
+
if (opts.dryRun) return { outputPath, provenance, signedArtifacts, packageIdentity, output: null };
|
|
391
|
+
|
|
392
|
+
let output = null;
|
|
393
|
+
if (existsSync(outputPath)) {
|
|
394
|
+
try { output = JSON.parse(readFileSync(outputPath, "utf8")); } catch (error) { fail(`native finalizer output is not valid JSON: ${outputPath}: ${error.message}`); }
|
|
395
|
+
} else if (stdout?.trim()) {
|
|
396
|
+
try { output = JSON.parse(stdout); } catch { fail(`native finalizer output missing: ${outputPath}`); }
|
|
397
|
+
} else {
|
|
398
|
+
fail(`native finalizer output missing: ${outputPath}`);
|
|
399
|
+
}
|
|
400
|
+
output = enrichNativeFinalizerOutput(output, root);
|
|
401
|
+
try {
|
|
402
|
+
output = validateNativeFinalizationOutput(output, { expectedProvenance: provenance });
|
|
403
|
+
} catch (error) {
|
|
404
|
+
fail(`invalid native finalizer output: ${error.message}`);
|
|
405
|
+
}
|
|
406
|
+
return { outputPath, provenance, signedArtifacts, packageIdentity, output };
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function enrichNativeFinalizerOutput(output, root) {
|
|
410
|
+
if (output?.provenance) return output;
|
|
411
|
+
if (output?.runtime?.provenance) return { ...output, provenance: output.runtime.provenance };
|
|
412
|
+
if (!output?.output) return output;
|
|
413
|
+
const manifestPath = path.join(path.resolve(root, output.output), "share", "legion", "release.json");
|
|
414
|
+
if (!existsSync(manifestPath)) return output;
|
|
415
|
+
try {
|
|
416
|
+
const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
|
|
417
|
+
const provenance = manifest.runtime?.provenance;
|
|
418
|
+
if (provenance) return { ...output, provenance };
|
|
419
|
+
} catch {
|
|
420
|
+
return output;
|
|
421
|
+
}
|
|
422
|
+
return output;
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
async function completeNativeFinalization(finalization, { root, target, nativeAssembly }) {
|
|
426
|
+
if (opts.dryRun) {
|
|
427
|
+
console.log(`dry-run: native finalization receipt binds archive identity at ${finalization.outputPath}`);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
const archivePath = nativeArchiveCandidates({ root, target, nativeAssembly }).find((file) => existsSync(file));
|
|
431
|
+
if (!archivePath) fail(`${config.app} ${opts.platform} native finalization cannot find final archive`);
|
|
432
|
+
let receipt;
|
|
433
|
+
try {
|
|
434
|
+
receipt = createNativeFinalizationReceipt({
|
|
435
|
+
app: config.app,
|
|
436
|
+
version: config.version,
|
|
437
|
+
platform: opts.platform,
|
|
438
|
+
architecture: target.selectedArchitecture ?? target.architecture ?? nativeAssembly?.architecture ?? process.arch,
|
|
439
|
+
targetTriple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget,
|
|
440
|
+
signedArtifacts: finalization.signedArtifacts,
|
|
441
|
+
packageIdentity: finalization.packageIdentity,
|
|
442
|
+
archivePath,
|
|
443
|
+
finalizerOutput: finalization.output,
|
|
444
|
+
root,
|
|
445
|
+
});
|
|
446
|
+
} catch (error) {
|
|
447
|
+
fail(`native finalization archive binding failed: ${error.message}`);
|
|
448
|
+
}
|
|
449
|
+
mkdirSync(path.dirname(finalization.outputPath), { recursive: true });
|
|
450
|
+
writeFileSync(finalization.outputPath, `${JSON.stringify(receipt, null, 2)}\n`);
|
|
451
|
+
console.log(`right-release: native finalization verified ${receipt.provenance} archive=${receipt.archiveSha256}`);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
function nativeSignedArtifacts(files, root) {
|
|
455
|
+
const values = Array.isArray(files) ? files : [];
|
|
456
|
+
return values.map((value) => {
|
|
457
|
+
const declared = typeof value === "string" ? value : value?.path ?? value?.file;
|
|
458
|
+
if (!declared) fail("native finalizer signed artifact path is missing");
|
|
459
|
+
const absolute = path.resolve(root, declared);
|
|
460
|
+
if (opts.dryRun) return { path: path.relative(root, absolute).replaceAll("\\", "/") };
|
|
461
|
+
if (!existsSync(absolute)) fail(`native finalizer signed artifact missing: ${absolute}`);
|
|
462
|
+
return {
|
|
463
|
+
path: path.relative(root, absolute).replaceAll("\\", "/"),
|
|
464
|
+
sha256: createHash("sha256").update(readFileSync(absolute)).digest("hex"),
|
|
465
|
+
sizeBytes: readFileSync(absolute).length,
|
|
466
|
+
};
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function nativePackageIdentity({ nativeAssembly, target }) {
|
|
471
|
+
const declared = nativeAssembly?.packageIdentity ?? target.packageIdentity;
|
|
472
|
+
if (declared) return canonicalPackageIdentity(declared);
|
|
473
|
+
const artifacts = [
|
|
474
|
+
...(target.artifacts ?? []),
|
|
475
|
+
...(target.installer?.artifacts ?? []).map((artifact) => artifact.file),
|
|
476
|
+
...(target.updater?.artifacts ?? []).map((artifact) => artifact.file),
|
|
477
|
+
].filter(Boolean);
|
|
478
|
+
return canonicalPackageIdentity({
|
|
479
|
+
kind: target.packageKind ?? "release",
|
|
480
|
+
platform: opts.platform,
|
|
481
|
+
architecture: target.selectedArchitecture ?? target.architecture ?? nativeAssembly?.architecture ?? process.arch,
|
|
482
|
+
...(artifacts.length ? { artifacts } : {}),
|
|
483
|
+
});
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
function nativeArchiveCandidates({ root, target, nativeAssembly }) {
|
|
487
|
+
const declared = [
|
|
488
|
+
nativeAssembly?.archive,
|
|
489
|
+
nativeAssembly?.archivePath,
|
|
490
|
+
nativeAssembly?.packageIdentity?.path,
|
|
491
|
+
target.archive,
|
|
492
|
+
target.archivePath,
|
|
493
|
+
...(target.artifacts ?? []),
|
|
494
|
+
...(target.installer?.artifacts ?? []).map((artifact) => artifact.file),
|
|
495
|
+
...(target.updater?.artifacts ?? []).map((artifact) => artifact.file),
|
|
496
|
+
].filter((value) => typeof value === "string" && value.trim());
|
|
497
|
+
return [...new Set(declared.map((value) => path.resolve(root, value)))];
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function nativeFinalizerOutputPath({ finalizer, target, root }) {
|
|
501
|
+
const declared = finalizer.output
|
|
502
|
+
?? target.evidence?.provenance
|
|
503
|
+
?? target.provenance
|
|
504
|
+
?? `.right-release/receipts/native-finalization-${opts.platform}.json`;
|
|
505
|
+
return path.resolve(root, declared);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function expandFinalizerToken(value, { provenance, outputPath, packageIdentity }) {
|
|
509
|
+
const replacements = {
|
|
510
|
+
"{{provenance}}": provenance,
|
|
511
|
+
"{provenance}": provenance,
|
|
512
|
+
"${RIGHT_RELEASE_NATIVE_PROVENANCE}": provenance,
|
|
513
|
+
"$RIGHT_RELEASE_NATIVE_PROVENANCE": provenance,
|
|
514
|
+
"{{receipt}}": outputPath,
|
|
515
|
+
"{receipt}": outputPath,
|
|
516
|
+
"${RIGHT_RELEASE_NATIVE_FINALIZATION_RECEIPT}": outputPath,
|
|
517
|
+
"$RIGHT_RELEASE_NATIVE_FINALIZATION_RECEIPT": outputPath,
|
|
518
|
+
"{{packageIdentity}}": JSON.stringify(packageIdentity),
|
|
519
|
+
"{packageIdentity}": JSON.stringify(packageIdentity),
|
|
520
|
+
};
|
|
521
|
+
return Object.entries(replacements).reduce((result, [token, replacement]) => result.replaceAll(token, replacement), String(value));
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function hasNativeTargetSpecificPrePackage(target, packageHook) {
|
|
525
|
+
if (!target?.prePackage || !packageHook || target.prePackage.cmd !== packageHook.cmd) return false;
|
|
526
|
+
const args = target.prePackage.args ?? [];
|
|
527
|
+
return args.some((arg) => ["--platform", "--architecture", "--target", "--out"].includes(arg));
|
|
528
|
+
}
|
|
529
|
+
|
|
314
530
|
// Post-release target/ hygiene: `cargo sweep --installed` deletes artifacts left
|
|
315
531
|
// by toolchains rustup no longer has (each update orphans a multi-GB pile per
|
|
316
532
|
// app) and never touches anything the current toolchains produced. Runs after
|
|
317
533
|
// publish so it cannot race an artifact, and is best-effort: a missing
|
|
318
534
|
// cargo-sweep or a sweep failure must never fail a release.
|
|
319
535
|
function sweepStaleRustArtifacts() {
|
|
320
|
-
const candidates = [...new Set([root, workdir
|
|
536
|
+
const candidates = [...new Set([root, workdir, nativeLayout.manifestDir, nativeLayout.workspaceRoot])];
|
|
321
537
|
const projects = candidates.filter((dir) => existsSync(path.join(dir, "target")));
|
|
322
538
|
for (const dir of projects) {
|
|
323
539
|
if (opts.dryRun) {
|
|
@@ -348,10 +564,11 @@ async function runPackageScript(pm, script, cwd) {
|
|
|
348
564
|
await run(pm, ["run", script], cwd);
|
|
349
565
|
}
|
|
350
566
|
|
|
351
|
-
async function runCommand(command, root) {
|
|
567
|
+
async function runCommand(command, root, { env = {}, captureStdout = false } = {}) {
|
|
352
568
|
const cwd = path.resolve(root, command.cwd ?? ".");
|
|
353
|
-
|
|
569
|
+
return run(command.cmd, command.args ?? [], cwd, { ...(await commandEnv(command, root)), ...env }, {
|
|
354
570
|
...command,
|
|
571
|
+
captureStdout,
|
|
355
572
|
timeoutMs: commandTimeoutMs(command),
|
|
356
573
|
});
|
|
357
574
|
}
|
|
@@ -550,7 +767,7 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
|
|
|
550
767
|
const timeout = options.timeoutMs ? ` timeout=${options.timeoutMs}ms` : "";
|
|
551
768
|
const tier = opts.tier ? `RIGHT_RELEASE_TIER=${opts.tier} ` : "";
|
|
552
769
|
console.log(`dry-run: (${cwd}) ${tier}${printable}${timeout}`);
|
|
553
|
-
return Promise.resolve();
|
|
770
|
+
return Promise.resolve("");
|
|
554
771
|
}
|
|
555
772
|
const started = Date.now();
|
|
556
773
|
return new Promise((resolve) => {
|
|
@@ -558,10 +775,12 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
|
|
|
558
775
|
const child = spawn(cmd, runArgs, {
|
|
559
776
|
cwd,
|
|
560
777
|
env: { ...process.env, ...releaseEnv, ...env },
|
|
561
|
-
stdio: "inherit",
|
|
778
|
+
stdio: options.captureStdout ? ["inherit", "pipe", "inherit"] : "inherit",
|
|
562
779
|
shell: useShell,
|
|
563
780
|
windowsHide: true,
|
|
564
781
|
});
|
|
782
|
+
let stdout = "";
|
|
783
|
+
if (options.captureStdout) child.stdout?.on("data", (chunk) => { stdout += String(chunk); });
|
|
565
784
|
const timer = options.timeoutMs
|
|
566
785
|
? setTimeout(() => {
|
|
567
786
|
killProcessTree(child.pid);
|
|
@@ -579,7 +798,7 @@ function run(cmd, runArgs, cwd, env = {}, options = {}) {
|
|
|
579
798
|
if (timer) clearTimeout(timer);
|
|
580
799
|
console.log(`right-release step: ${printable} (${Date.now() - started}ms)`);
|
|
581
800
|
if (code !== 0) process.exit(code ?? 1);
|
|
582
|
-
resolve();
|
|
801
|
+
resolve(options.captureStdout ? stdout : "");
|
|
583
802
|
});
|
|
584
803
|
});
|
|
585
804
|
}
|
package/rightkit-versions.json
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"@rightkit/updates": "0.2.3"
|
|
14
14
|
},
|
|
15
15
|
"stagedNpm": {
|
|
16
|
-
"@rightkit/ax": "0.2.
|
|
16
|
+
"@rightkit/ax": "0.2.1",
|
|
17
17
|
"@rightkit/git": "0.2.1",
|
|
18
18
|
"@rightkit/hooks": "0.1.1",
|
|
19
19
|
"@rightkit/legal": "0.3.1",
|
|
@@ -22,11 +22,14 @@
|
|
|
22
22
|
"@rightkit/logs": "0.1.4",
|
|
23
23
|
"@rightkit/platform-ui": "0.1.1",
|
|
24
24
|
"@rightkit/qa": "0.2.1",
|
|
25
|
-
"@rightkit/release": "0.2.
|
|
25
|
+
"@rightkit/release": "0.2.72",
|
|
26
26
|
"@rightkit/tauri": "0.1.1",
|
|
27
27
|
"@rightkit/updates": "0.2.4"
|
|
28
28
|
},
|
|
29
29
|
"legacyNpm": {
|
|
30
|
+
"@rightkit/ax": [
|
|
31
|
+
"0.2.0"
|
|
32
|
+
],
|
|
30
33
|
"@rightkit/legal-ui": [
|
|
31
34
|
"0.1.0"
|
|
32
35
|
],
|
|
@@ -65,7 +68,9 @@
|
|
|
65
68
|
"0.2.66",
|
|
66
69
|
"0.2.67",
|
|
67
70
|
"0.2.68",
|
|
68
|
-
"0.2.69"
|
|
71
|
+
"0.2.69",
|
|
72
|
+
"0.2.70",
|
|
73
|
+
"0.2.71"
|
|
69
74
|
],
|
|
70
75
|
"@rightkit/qa": [
|
|
71
76
|
"0.1.0",
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
|
|
7
|
+
export const RIGHTRELEASE_MANIFEST_SIGNER = Object.freeze({
|
|
8
|
+
id: "azure-artifact-signing-damned-ventures-v1",
|
|
9
|
+
subject: "CN=Damned Ventures LLC",
|
|
10
|
+
algorithm: "cms-sha256",
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
const EXCLUDE_CREDENTIALS = [
|
|
14
|
+
"ManagedIdentityCredential",
|
|
15
|
+
"WorkloadIdentityCredential",
|
|
16
|
+
"SharedTokenCacheCredential",
|
|
17
|
+
"VisualStudioCredential",
|
|
18
|
+
"VisualStudioCodeCredential",
|
|
19
|
+
"AzurePowerShellCredential",
|
|
20
|
+
"AzureDeveloperCliCredential",
|
|
21
|
+
"InteractiveBrowserCredential",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const CERTIFICATE_SHA256 = /^[a-f0-9]{64}$/i;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Verify detached CMS over exact manifest bytes & bind signer identity to the
|
|
28
|
+
* shared Azure Artifact Signing result. `commandRunner` is an injectable
|
|
29
|
+
* system-process seam; it cannot replace CMS verification because production
|
|
30
|
+
* verification script/arguments are always issued here.
|
|
31
|
+
*/
|
|
32
|
+
export function verifyDetachedCmsSignature({
|
|
33
|
+
manifestPath,
|
|
34
|
+
signaturePath,
|
|
35
|
+
expectedSigner,
|
|
36
|
+
commandRunner = spawnSync,
|
|
37
|
+
platform = process.platform,
|
|
38
|
+
powershellPath = "powershell",
|
|
39
|
+
opensslPath = "openssl",
|
|
40
|
+
}) {
|
|
41
|
+
const manifest = resolve(manifestPath);
|
|
42
|
+
const signature = resolve(signaturePath);
|
|
43
|
+
if (!existsSync(manifest) || !statSync(manifest).isFile()) throw new Error("CMS manifest bytes are missing");
|
|
44
|
+
if (!existsSync(signature) || !statSync(signature).isFile() || statSync(signature).size < 1) throw new Error("detached CMS signature is missing");
|
|
45
|
+
if (!expectedSigner || expectedSigner.id !== RIGHTRELEASE_MANIFEST_SIGNER.id || expectedSigner.subject !== RIGHTRELEASE_MANIFEST_SIGNER.subject || expectedSigner.algorithm !== RIGHTRELEASE_MANIFEST_SIGNER.algorithm || !CERTIFICATE_SHA256.test(expectedSigner.certificateSha256 ?? "")) {
|
|
46
|
+
throw new Error("CMS verification requires exact Azure signer identity");
|
|
47
|
+
}
|
|
48
|
+
const inspection = platform === "win32"
|
|
49
|
+
? verifyCmsWithPowerShell({ manifestPath: manifest, signaturePath: signature, commandRunner, powershellPath })
|
|
50
|
+
: verifyCmsWithOpenSsl({ manifestPath: manifest, signaturePath: signature, commandRunner, opensslPath });
|
|
51
|
+
if (!inspection.verified) throw new Error("detached CMS signature is invalid");
|
|
52
|
+
if (normalizeSubject(inspection.subject) !== normalizeSubject(expectedSigner.subject)) throw new Error("CMS signer subject mismatch");
|
|
53
|
+
const certificateSha256 = String(inspection.certificateSha256 ?? "").replaceAll(":", "").toLowerCase();
|
|
54
|
+
if (!CERTIFICATE_SHA256.test(certificateSha256) || certificateSha256 !== String(expectedSigner.certificateSha256).toLowerCase()) throw new Error("CMS signer certificate fingerprint mismatch");
|
|
55
|
+
return { verified: true, manifestPath: manifest, signaturePath: signature, signer: { ...expectedSigner, certificateSha256 } };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function signReleaseManifestWithAzure({
|
|
59
|
+
manifestPath,
|
|
60
|
+
signaturePath,
|
|
61
|
+
commandRunner = spawnSync,
|
|
62
|
+
environment = process.env,
|
|
63
|
+
signtoolPath,
|
|
64
|
+
dlibPath,
|
|
65
|
+
metadataPath,
|
|
66
|
+
}) {
|
|
67
|
+
if (process.platform !== "win32" && commandRunner === spawnSync) {
|
|
68
|
+
throw new Error("RightRelease manifest signing must run on protected Windows release host");
|
|
69
|
+
}
|
|
70
|
+
const userEnv = (name) => readUserEnvironment(name, commandRunner);
|
|
71
|
+
const env = (name) => environment[name] || userEnv(name);
|
|
72
|
+
const signtool = signtoolPath || firstExisting([
|
|
73
|
+
env("AZURE_SIGNTOOL_PATH"),
|
|
74
|
+
env("SIGNTOOL_PATH"),
|
|
75
|
+
...signtoolCandidates(env),
|
|
76
|
+
]);
|
|
77
|
+
if (!signtool) throw new Error("RightRelease manifest signer could not find signtool.exe");
|
|
78
|
+
const dlib = dlibPath || firstExisting([
|
|
79
|
+
env("AZURE_CODESIGN_DLIB_PATH"),
|
|
80
|
+
env("AZURE_ARTIFACT_SIGNING_DLIB_PATH"),
|
|
81
|
+
join(env("LOCALAPPDATA") || "", "AzureArtifactSigningTools", "Microsoft.ArtifactSigning.Client", "bin", "x64", "Azure.CodeSigning.Dlib.dll"),
|
|
82
|
+
"C:\\Program Files\\Microsoft Azure Artifact Signing Client Tools\\x64\\Azure.CodeSigning.Dlib.dll",
|
|
83
|
+
"C:\\Program Files (x86)\\Microsoft\\ArtifactSigningClientTools\\bin\\x64\\Azure.CodeSigning.Dlib.dll",
|
|
84
|
+
]);
|
|
85
|
+
if (!dlib) throw new Error("RightRelease manifest signer could not find Azure Artifact Signing dlib");
|
|
86
|
+
|
|
87
|
+
const temp = mkdtempSync(join(tmpdir(), "rightrelease-manifest-sign-"));
|
|
88
|
+
const metadata = metadataPath || materializeMetadata(temp, env);
|
|
89
|
+
const outputDir = join(temp, "signature");
|
|
90
|
+
mkdirSync(outputDir, { recursive: true });
|
|
91
|
+
try {
|
|
92
|
+
const result = commandRunner(signtool, [
|
|
93
|
+
"sign", "/v", "/fd", "SHA256", "/tr", "http://timestamp.acs.microsoft.com", "/td", "SHA256",
|
|
94
|
+
"/dlib", dlib, "/dmdf", metadata,
|
|
95
|
+
"/p7", outputDir, "/p7ce", "DetachedSignedData", "/p7co", "1.2.840.113549.1.7.1",
|
|
96
|
+
resolve(manifestPath),
|
|
97
|
+
], { encoding: "utf8", windowsHide: true, env: environment });
|
|
98
|
+
if (result?.status !== 0) throw new Error(`RightRelease manifest signing failed: ${String(result?.stderr ?? "").trim()}`);
|
|
99
|
+
const generated = join(outputDir, `${basename(manifestPath)}.p7`);
|
|
100
|
+
if (!existsSync(generated)) throw new Error("RightRelease manifest signer did not produce detached CMS signature");
|
|
101
|
+
const inspector = join(temp, "inspect-signer.ps1");
|
|
102
|
+
writeFileSync(inspector, [
|
|
103
|
+
"param([string]$ManifestPath, [string]$SignaturePath)",
|
|
104
|
+
"Add-Type -AssemblyName System.Security",
|
|
105
|
+
"$Content = New-Object Security.Cryptography.Pkcs.ContentInfo(,[IO.File]::ReadAllBytes($ManifestPath))",
|
|
106
|
+
"$Cms = New-Object Security.Cryptography.Pkcs.SignedCms($Content, $true)",
|
|
107
|
+
"$Cms.Decode([IO.File]::ReadAllBytes($SignaturePath))",
|
|
108
|
+
"$Cms.CheckSignature($true)",
|
|
109
|
+
"if ($Cms.SignerInfos.Count -ne 1) { throw 'Expected exactly one manifest signer' }",
|
|
110
|
+
"$Certificate = $Cms.SignerInfos[0].Certificate",
|
|
111
|
+
"$Sha256 = [Security.Cryptography.SHA256]::Create()",
|
|
112
|
+
"try { $Fingerprint = ([BitConverter]::ToString($Sha256.ComputeHash($Certificate.RawData))).Replace('-','').ToLowerInvariant() } finally { $Sha256.Dispose() }",
|
|
113
|
+
"@{ subject=$Certificate.Subject; certificateSha256=$Fingerprint } | ConvertTo-Json -Compress",
|
|
114
|
+
].join("\r\n") + "\r\n");
|
|
115
|
+
const inspection = commandRunner("powershell", ["-NoProfile", "-File", inspector, resolve(manifestPath), generated], { encoding: "utf8", windowsHide: true, env: environment });
|
|
116
|
+
if (inspection?.status !== 0) throw new Error(`RightRelease could not inspect manifest signer: ${String(inspection?.stderr ?? "").trim()}`);
|
|
117
|
+
let certificate;
|
|
118
|
+
try { certificate = JSON.parse(String(inspection.stdout ?? "").trim()); } catch { throw new Error("RightRelease manifest signer inspection returned invalid JSON"); }
|
|
119
|
+
if (certificate.subject !== RIGHTRELEASE_MANIFEST_SIGNER.subject || !/^[a-f0-9]{64}$/.test(certificate.certificateSha256 ?? "")) {
|
|
120
|
+
throw new Error("RightRelease manifest signer identity does not match protected Azure profile");
|
|
121
|
+
}
|
|
122
|
+
mkdirSync(dirname(signaturePath), { recursive: true });
|
|
123
|
+
writeFileSync(signaturePath, readFileSync(generated));
|
|
124
|
+
return { signaturePath: resolve(signaturePath), signer: { ...RIGHTRELEASE_MANIFEST_SIGNER, certificateSha256: certificate.certificateSha256 } };
|
|
125
|
+
} finally {
|
|
126
|
+
rmSync(temp, { recursive: true, force: true });
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function verifyCmsWithPowerShell({ manifestPath, signaturePath, commandRunner, powershellPath }) {
|
|
131
|
+
const temp = mkdtempSync(join(tmpdir(), "rightrelease-cms-verify-"));
|
|
132
|
+
const verifier = join(temp, "verify-cms.ps1");
|
|
133
|
+
writeFileSync(verifier, [
|
|
134
|
+
"param([string]$ManifestPath, [string]$SignaturePath)",
|
|
135
|
+
"Add-Type -AssemblyName System.Security",
|
|
136
|
+
"$Content = New-Object Security.Cryptography.Pkcs.ContentInfo(,[IO.File]::ReadAllBytes($ManifestPath))",
|
|
137
|
+
"$Cms = New-Object Security.Cryptography.Pkcs.SignedCms($Content, $true)",
|
|
138
|
+
"$Cms.Decode([IO.File]::ReadAllBytes($SignaturePath))",
|
|
139
|
+
"$Cms.CheckSignature($true)",
|
|
140
|
+
"if ($Cms.SignerInfos.Count -ne 1) { throw 'Expected exactly one manifest signer' }",
|
|
141
|
+
"$Certificate = $Cms.SignerInfos[0].Certificate",
|
|
142
|
+
"$Sha256 = [Security.Cryptography.SHA256]::Create()",
|
|
143
|
+
"try { $Fingerprint = ([BitConverter]::ToString($Sha256.ComputeHash($Certificate.RawData))).Replace('-','').ToLowerInvariant() } finally { $Sha256.Dispose() }",
|
|
144
|
+
"@{ verified=$true; subject=$Certificate.Subject; certificateSha256=$Fingerprint } | ConvertTo-Json -Compress",
|
|
145
|
+
].join("\r\n") + "\r\n");
|
|
146
|
+
try {
|
|
147
|
+
const result = commandRunner(powershellPath, ["-NoProfile", "-File", verifier, manifestPath, signaturePath], { encoding: "utf8", windowsHide: true });
|
|
148
|
+
if (result?.status !== 0) throw new Error(`CMS signature verification failed: ${String(result?.stderr ?? "").trim()}`);
|
|
149
|
+
return parseCmsInspection(result?.stdout);
|
|
150
|
+
} finally {
|
|
151
|
+
rmSync(temp, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function verifyCmsWithOpenSsl({ manifestPath, signaturePath, commandRunner, opensslPath }) {
|
|
156
|
+
const temp = mkdtempSync(join(tmpdir(), "rightrelease-cms-verify-"));
|
|
157
|
+
const verified = join(temp, "verified-manifest");
|
|
158
|
+
const certificate = join(temp, "signer.pem");
|
|
159
|
+
try {
|
|
160
|
+
const result = commandRunner(opensslPath, ["cms", "-verify", "-binary", "-inform", "DER", "-in", signaturePath, "-content", manifestPath, "-noverify", "-certsout", certificate, "-out", verified], { encoding: "utf8", windowsHide: true });
|
|
161
|
+
if (result?.status !== 0) throw new Error(`CMS signature verification failed: ${String(result?.stderr ?? "").trim()}`);
|
|
162
|
+
if (!existsSync(verified) || !readFileSync(verified).equals(readFileSync(manifestPath))) throw new Error("CMS signature content does not match manifest bytes");
|
|
163
|
+
if (!existsSync(certificate)) throw new Error("CMS signer certificate is missing");
|
|
164
|
+
const inspection = commandRunner(opensslPath, ["x509", "-in", certificate, "-noout", "-subject", "-fingerprint", "-sha256"], { encoding: "utf8", windowsHide: true });
|
|
165
|
+
if (inspection?.status !== 0) throw new Error(`CMS signer inspection failed: ${String(inspection?.stderr ?? "").trim()}`);
|
|
166
|
+
const output = String(inspection?.stdout ?? "");
|
|
167
|
+
const subject = output.match(/^\s*subject\s*=\s*(.+)$/im)?.[1]?.trim();
|
|
168
|
+
const certificateSha256 = output.match(/^\s*sha256\s+Fingerprint\s*=\s*([0-9a-f:]+)\s*$/im)?.[1];
|
|
169
|
+
return parseCmsInspection(JSON.stringify({ verified: true, subject, certificateSha256 }));
|
|
170
|
+
} finally {
|
|
171
|
+
rmSync(temp, { recursive: true, force: true });
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function parseCmsInspection(output) {
|
|
176
|
+
let inspection;
|
|
177
|
+
try { inspection = JSON.parse(String(output ?? "").trim()); } catch { throw new Error("CMS verifier returned invalid JSON"); }
|
|
178
|
+
if (inspection.verified !== true || typeof inspection.subject !== "string" || typeof inspection.certificateSha256 !== "string") throw new Error("CMS verifier returned incomplete identity");
|
|
179
|
+
return inspection;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function normalizeSubject(subject) {
|
|
183
|
+
return String(subject).trim().replace(/\s*=\s*/g, "=").replace(/\s*,\s*/g, ",");
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function materializeMetadata(dir, env) {
|
|
187
|
+
const existing = env("AZURE_ARTIFACT_SIGNING_METADATA") || env("AZURE_SIGNING_METADATA");
|
|
188
|
+
if (existing) {
|
|
189
|
+
if (!existsSync(existing)) throw new Error(`Azure signing metadata file not found: ${existing}`);
|
|
190
|
+
return existing;
|
|
191
|
+
}
|
|
192
|
+
const Endpoint = env("AZURE_ARTIFACT_SIGNING_ENDPOINT") || env("AZURE_SIGNING_ENDPOINT") || env("AZURE_ENDPOINT");
|
|
193
|
+
const CodeSigningAccountName = env("AZURE_ARTIFACT_SIGNING_ACCOUNT") || env("AZURE_SIGNING_ACCOUNT_NAME") || env("AZURE_ACCOUNT");
|
|
194
|
+
const CertificateProfileName = env("AZURE_ARTIFACT_SIGNING_PROFILE") || env("AZURE_CERTIFICATE_PROFILE_NAME") || env("AZURE_PROFILE");
|
|
195
|
+
if (!Endpoint || !CodeSigningAccountName || !CertificateProfileName) {
|
|
196
|
+
throw new Error("RightRelease manifest signer requires existing Azure Artifact Signing configuration");
|
|
197
|
+
}
|
|
198
|
+
const path = join(dir, "metadata.json");
|
|
199
|
+
writeFileSync(path, `${JSON.stringify({ Endpoint, CodeSigningAccountName, CertificateProfileName, ExcludeCredentials: EXCLUDE_CREDENTIALS }, null, 2)}\n`);
|
|
200
|
+
return path;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function signtoolCandidates(env) {
|
|
204
|
+
const roots = [
|
|
205
|
+
join(env("LOCALAPPDATA") || "", "AzureArtifactSigningTools", "Microsoft.Windows.SDK.BuildTools", "bin"),
|
|
206
|
+
join(env("ProgramFiles(x86)") || "C:\\Program Files (x86)", "Windows Kits", "10", "bin"),
|
|
207
|
+
];
|
|
208
|
+
return roots.flatMap((root) => existsSync(root)
|
|
209
|
+
? readdirSync(root, { withFileTypes: true }).filter((entry) => entry.isDirectory()).map((entry) => join(root, entry.name, "x64", "signtool.exe")).sort().reverse()
|
|
210
|
+
: []);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function firstExisting(candidates) {
|
|
214
|
+
return candidates.find((candidate) => candidate && existsSync(candidate));
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function readUserEnvironment(name, commandRunner) {
|
|
218
|
+
const result = commandRunner("powershell", ["-NoProfile", "-Command", `[Environment]::GetEnvironmentVariable('${name.replaceAll("'", "''")}','User')`], {
|
|
219
|
+
encoding: "utf8",
|
|
220
|
+
windowsHide: true,
|
|
221
|
+
});
|
|
222
|
+
return result?.status === 0 ? String(result.stdout ?? "").trim() : "";
|
|
223
|
+
}
|