@rightkit/release 0.2.151 → 0.2.153
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/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +117 -8
- package/build-release.mjs +7 -2
- package/cli/right-release.mjs +18 -5
- package/committed-source.mjs +19 -1
- package/crate-graph.mjs +121 -0
- package/development-signing.mjs +8 -0
- package/development.mjs +46 -13
- package/devtools-policy.json +5 -0
- package/devtools-policy.mjs +119 -0
- package/direct-bootstrap.mjs +9 -3
- package/dmg-build.mjs +19 -5
- package/dmg-layout.mjs +2 -2
- package/dmg-spec.mjs +97 -0
- package/execution-policy.mjs +17 -1
- package/github-release.mjs +25 -7
- package/github-upload-gate.mjs +84 -0
- package/hardeningscan.mjs +3 -0
- package/inventory-check.mjs +4 -4
- package/mac-bundle-signing.mjs +1 -1
- package/mac-sign-core.mjs +97 -0
- package/mac-signing-matrix.mjs +186 -0
- package/msix-appinstaller.mjs +4 -1
- package/msix-config.mjs +5 -1
- package/msix-repair-guard.mjs +24 -0
- package/msix-repair-guard.ps1 +356 -0
- package/ownership-catalog-config.json +6 -7
- package/ownership-command.mjs +69 -8
- package/ownership-evidence.mjs +1 -1
- package/ownership-key-authority.mjs +1 -1
- package/ownership-key-command.mjs +15 -3
- package/ownership-key-elevation.mjs +11 -2
- package/ownership-mandatory-set.json +420 -0
- package/ownership-mandatory-set.mjs +273 -0
- package/ownership.mjs +2 -2
- package/package.json +15 -3
- package/packaged-app-identity.mjs +88 -0
- package/preflight.mjs +25 -3
- package/prune-r2.mjs +28 -3
- package/publication-gate.mjs +49 -10
- package/publish-cargo.mjs +21 -1
- package/publish-swift.mjs +78 -141
- package/publish-update.mjs +24 -2
- package/qa-build-features.mjs +216 -0
- package/qa-control-scan.mjs +435 -0
- package/registry-parity.mjs +18 -1
- package/release-invocation.mjs +2 -1
- package/release-state.mjs +9 -0
- package/release-switch-registry.json +33 -0
- package/release-switch-scan.mjs +452 -0
- package/release.mjs +320 -26
- package/renamed-away.mjs +29 -0
- package/rightkit-ownership.json +682 -709
- package/rightkit-sync.mjs +8 -4
- package/rightkit-version-policy.mjs +18 -0
- package/rightkit-versions.json +211 -127
- package/runtime-budget-classes.json +4 -0
- package/runtime-budgets.mjs +870 -0
- package/runtime-bundle-scan.mjs +61 -0
- package/sign-macos.mjs +28 -30
- package/sign-updater.mjs +5 -2
- package/swift-mac-dmg.mjs +7 -2
- package/swift-package-command.mjs +1 -1
- package/upload-release.mjs +10 -37
package/release.mjs
CHANGED
|
@@ -21,6 +21,7 @@ import { packageCommandOptional, resolveProjectType, targetUsesMsix } from "./pr
|
|
|
21
21
|
import { MSIX_SIGNING_CONTRACT, assertValidMsixTarget } from "./msix-config.mjs";
|
|
22
22
|
import { buildMsix } from "./msix-build.mjs";
|
|
23
23
|
import { msixBuildOptions } from "./msix-target.mjs";
|
|
24
|
+
import { rightKitVersionAllowed } from "./rightkit-version-policy.mjs";
|
|
24
25
|
import { collectPreflight, formatPreflight, preflightFailures } from "./preflight.mjs";
|
|
25
26
|
import { patchTauriBundleType } from "./tauri-bundle-marker.mjs";
|
|
26
27
|
import { verifyNsisEmbeddedBinary } from "./nsis-payload.mjs";
|
|
@@ -30,12 +31,17 @@ import { signingReceiptPath } from "./signing-receipts.mjs";
|
|
|
30
31
|
import { notarizeAppBundle, validateNotarizationReceipt } from "./macos-notarization.mjs";
|
|
31
32
|
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
32
33
|
import { fileEvidence } from "./macos-file-evidence.mjs";
|
|
33
|
-
import {
|
|
34
|
+
import { readDmgSpec } from "./dmg-spec.mjs";
|
|
35
|
+
import { hasNonDefaultSettings, matrixReceiptProblems, planMacSigning, validateMacSigningConfig } from "./mac-signing-matrix.mjs";
|
|
34
36
|
import { buildStapledSwiftMacDmg, swiftMacDmgConfigError, swiftMacDmgPlan } from "./swift-mac-dmg.mjs";
|
|
35
37
|
import { runConfiguredChecks } from "./release-checks.mjs";
|
|
36
38
|
import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
|
|
37
39
|
import { DEFAULT_STORAGE_THRESHOLD_BYTES, findInstallerCopyScripts, findLargeRepositorySubdirectories } from "./installer-storage-doctor.mjs";
|
|
38
40
|
import { macosCodesignEnvironment } from "./codesign-environment.mjs";
|
|
41
|
+
import { assertNoQaControl, assertNoWebBundleMocks } from "./qa-control-scan.mjs";
|
|
42
|
+
import { assertNoForbiddenBuildFeatures } from "./qa-build-features.mjs";
|
|
43
|
+
import { assertNoReleaseSwitches, commandTokens, resolveFeatureClosure } from "./release-switch-scan.mjs";
|
|
44
|
+
import { devtoolsEvidence, resolveDevtoolsAllowance } from "./devtools-policy.mjs";
|
|
39
45
|
|
|
40
46
|
const TOOL_ROOT = path.dirname(fileURLToPath(import.meta.url));
|
|
41
47
|
const HARDENING_SCAN = path.resolve(TOOL_ROOT, "hardeningscan.mjs");
|
|
@@ -51,6 +57,7 @@ const RIGHTKIT_ALLOWED = buildAllowedVersions(
|
|
|
51
57
|
RIGHTKIT_VERSIONS.npm,
|
|
52
58
|
RIGHTKIT_VERSIONS.stagedNpm,
|
|
53
59
|
RIGHTKIT_VERSIONS.legacyNpm,
|
|
60
|
+
RIGHTKIT_VERSIONS.registry?.npm,
|
|
54
61
|
);
|
|
55
62
|
const RIGHTKIT_CARGO_ALLOWED = buildAllowedVersions(RIGHTKIT_VERSIONS.cargo, RIGHTKIT_VERSIONS.stagedCargo, RIGHTKIT_VERSIONS.legacyCargo);
|
|
56
63
|
const FORBIDDEN_RIGHTKIT_SPEC = /^(?:git|file|link|workspace):|github|github\.com/i;
|
|
@@ -58,6 +65,35 @@ const WINDOWS_NSIS_SIGNING_CONTRACT = "windows-raw-exe-authenticode-before-nsis-
|
|
|
58
65
|
const WINDOWS_PORTABLE_SIGNING_CONTRACT = "windows-raw-exe-authenticode-before-portable-v1";
|
|
59
66
|
const DEFAULT_NOTARIZATION_TIMEOUT_MS = 30 * 60 * 1000;
|
|
60
67
|
|
|
68
|
+
// Apple Developer ID and notary secrets: the only secrets the signing jobs of the managed release chain
|
|
69
|
+
// (macos-sign, and dev-mac-sign when devArtifact.mac is "notarize") may reference.
|
|
70
|
+
const APPLE_RELEASE_SECRETS = ["APPLE_CERTIFICATE_BASE64", "APPLE_CERTIFICATE_PASSWORD", "APPLE_KEYCHAIN_PASSWORD", "APPLE_API_KEY_BASE64", "APPLE_API_KEY", "APPLE_API_ISSUER"];
|
|
71
|
+
// Package-manager publication tokens of the managed publish-package-managers lane.
|
|
72
|
+
const PACKAGE_MANAGER_PUBLISH_SECRETS = ["HOMEBREW_TAP_TOKEN", "WINGET_CREATE_GITHUB_TOKEN"];
|
|
73
|
+
|
|
74
|
+
// @rightkit/ci `swift.privatePackages` (0.2.41) renders one configure step and one cleanup step, with the
|
|
75
|
+
// token passed through step `env` as `RIGHT_GIT_PRIVATE_TOKEN_<n>: ${{ secrets.<NAME> }}`, into exactly these
|
|
76
|
+
// workflow/job pairs. Nothing else may reference the declared secret: not another job of these workflows, not
|
|
77
|
+
// package-smoke, publish-npm or publish-package-managers, and not a signing, qualification or publish job.
|
|
78
|
+
const SWIFT_PRIVATE_STEP_NAMES = new Set(["Configure private Swift package access", "Remove private Swift package access"]);
|
|
79
|
+
const SWIFT_PRIVATE_ANY_PROFILE = { only: "any profile", allows: () => true };
|
|
80
|
+
const SWIFT_PRIVATE_NODE_PROFILE = { only: "the node profile", allows: (manifest) => (manifest?.profile ?? "node") === "node" };
|
|
81
|
+
const SWIFT_PRIVATE_PARALLEL_DEV_MAC = {
|
|
82
|
+
only: "profile rust-hybrid with devArtifact.parallel and a devArtifact.mac of sign, notarize or unsigned",
|
|
83
|
+
allows: (manifest) => manifest?.profile === "rust-hybrid"
|
|
84
|
+
&& manifest?.devArtifact?.parallel === true
|
|
85
|
+
&& ["sign", "notarize", "unsigned"].includes(manifest.devArtifact.mac),
|
|
86
|
+
};
|
|
87
|
+
const SWIFT_PRIVATE_JOBS = new Map([
|
|
88
|
+
["ci.yml", new Map([["ci", SWIFT_PRIVATE_ANY_PROFILE], ["dev-artifact-mac", SWIFT_PRIVATE_PARALLEL_DEV_MAC], ["test", SWIFT_PRIVATE_NODE_PROFILE]])],
|
|
89
|
+
["qualification.yml", new Map([["qualification", SWIFT_PRIVATE_ANY_PROFILE]])],
|
|
90
|
+
["release-candidate.yml", new Map([["candidate", SWIFT_PRIVATE_ANY_PROFILE]])],
|
|
91
|
+
["regenerate.yml", new Map([["regenerate", SWIFT_PRIVATE_ANY_PROFILE]])],
|
|
92
|
+
]);
|
|
93
|
+
// release-candidate.yml and regenerate.yml are dispatch lanes by design. In ci.yml and qualification.yml a
|
|
94
|
+
// workflow_dispatch trigger would let a manual run of any ref reach the credential.
|
|
95
|
+
const SWIFT_PRIVATE_DISPATCH_WORKFLOWS = new Set(["release-candidate.yml", "regenerate.yml"]);
|
|
96
|
+
|
|
61
97
|
const args = process.argv.slice(2);
|
|
62
98
|
const opts = {
|
|
63
99
|
config: "right-release.config.mjs",
|
|
@@ -103,7 +139,8 @@ if (opts.resumeFrom && !MAC_RESUME_PHASES.includes(opts.resumeFrom)) usage(2, `i
|
|
|
103
139
|
const sccacheVersion = assertSharedSccachePrerequisite();
|
|
104
140
|
|
|
105
141
|
const requestedConfigPath = path.resolve(opts.config);
|
|
106
|
-
|
|
142
|
+
// Native: expands a Windows short name (RUNNER~1), so the path compares with Git's long root below.
|
|
143
|
+
const configPath = opts.doctor ? realpathSync.native(requestedConfigPath) : requestedConfigPath;
|
|
107
144
|
let doctorInvocation;
|
|
108
145
|
if (opts.doctor) {
|
|
109
146
|
const invocationRoot = path.dirname(configPath);
|
|
@@ -138,6 +175,7 @@ const legalContract = config.legal
|
|
|
138
175
|
if (target.signed !== true) {
|
|
139
176
|
fail(`${config.app ?? "app"} ${opts.platform} must declare signed: true; unsigned release targets are forbidden`);
|
|
140
177
|
}
|
|
178
|
+
let macSigning; // validated macOS sign.prePackageFiles / sign.nestedCode (mac only)
|
|
141
179
|
if (opts.platform === "win") {
|
|
142
180
|
const isNsis = target.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT;
|
|
143
181
|
const isPortable = target.signingContract === WINDOWS_PORTABLE_SIGNING_CONTRACT;
|
|
@@ -148,6 +186,7 @@ if (opts.platform === "win") {
|
|
|
148
186
|
} else {
|
|
149
187
|
if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} win must declare prePackage for the raw EXE build`);
|
|
150
188
|
if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} win must declare at least one sign.prePackageFiles raw EXE`);
|
|
189
|
+
if (target.sign.prePackageFiles.some((file) => typeof file !== "string") || target.sign.nestedCode !== undefined) fail(`${config.app ?? "app"} win sign.prePackageFiles entries must be path strings; object entries and sign.nestedCode are macOS-only`);
|
|
151
190
|
if (isNsis && target.sign.prePackageFiles.length !== 1) fail(`${config.app ?? "app"} win NSIS must declare exactly one sign.prePackageFiles raw EXE`);
|
|
152
191
|
if (isNsis && !target.sign?.files?.length) fail(`${config.app ?? "app"} win NSIS must declare sign.files for installer signing`);
|
|
153
192
|
if (isNsis && target.sign.prePackageFiles.some((file) => target.sign.files.includes(file))) fail(`${config.app ?? "app"} win raw EXE and installer signing files must be distinct`);
|
|
@@ -158,6 +197,7 @@ if (opts.platform === "mac") {
|
|
|
158
197
|
if (target.signingContract !== MACOS_PORTABLE_SIGNING_CONTRACT) fail(`${config.app ?? "app"} mac must declare signingContract: ${MACOS_PORTABLE_SIGNING_CONTRACT}`);
|
|
159
198
|
if (!target.prePackage?.cmd) fail(`${config.app ?? "app"} mac must declare prePackage to materialize Developer ID signing files`);
|
|
160
199
|
if (!target.sign?.prePackageFiles?.length) fail(`${config.app ?? "app"} mac must declare at least one sign.prePackageFiles`);
|
|
200
|
+
try { macSigning = validateMacSigningConfig(target.sign, { root }); } catch (error) { fail(`${config.app ?? "app"} mac ${error.message}`); }
|
|
161
201
|
if (!target.notarize?.file) fail(`${config.app ?? "app"} mac must declare notarize.file`);
|
|
162
202
|
if (target.notarize.appBeforePackage && !target.notarize.app) fail(`${config.app ?? "app"} mac notarize.appBeforePackage requires notarize.app`);
|
|
163
203
|
if (target.notarize.staple === false && (target.notarize.app || /\.(dmg|pkg)$/i.test(target.notarize.file))) {
|
|
@@ -165,6 +205,13 @@ if (opts.platform === "mac") {
|
|
|
165
205
|
}
|
|
166
206
|
const swiftDmgError = swiftMacDmgConfigError({ target, projectType: resolveProjectType({ appRoot: root, config, target }), appName: config.app ?? "app" });
|
|
167
207
|
if (swiftDmgError) fail(swiftDmgError);
|
|
208
|
+
if (target.dmg && typeof target.dmg === "object" && target.dmg.spec !== undefined) {
|
|
209
|
+
try {
|
|
210
|
+
if (typeof target.dmg.spec !== "string" || !target.dmg.spec) throw new Error("dmg.spec must be a path string");
|
|
211
|
+
const specOptions = readDmgSpec(path.resolve(root, target.dmg.spec), { app: target.notarize.app });
|
|
212
|
+
for (const asset of [specOptions.background, specOptions.volumeIcon]) if (asset && !existsSync(asset)) throw new Error(`referenced file not found: ${asset}`);
|
|
213
|
+
} catch (error) { fail(`${config.app ?? "app"} mac targets.mac.dmg.spec: ${error.message}`); }
|
|
214
|
+
}
|
|
168
215
|
}
|
|
169
216
|
if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
|
|
170
217
|
/**
|
|
@@ -254,6 +301,61 @@ if (opts.doctor) {
|
|
|
254
301
|
process.exit(0);
|
|
255
302
|
}
|
|
256
303
|
|
|
304
|
+
let prePackScanned = false; // set once a pre-pack payload scan has seen a native binary
|
|
305
|
+
// Source-level layer of "no test-automation surface ships": refuse a release or update build whose build command
|
|
306
|
+
// or app Cargo manifest enables devtools, qa-native, qa-control or dev-native (also in --dry-run).
|
|
307
|
+
// DevTools: refused for every app except an allow-listed one that declares `devtools: "content-only"` (devtools-policy.mjs).
|
|
308
|
+
let devtoolsAllowance = { mode: "denied" };
|
|
309
|
+
try {
|
|
310
|
+
devtoolsAllowance = resolveDevtoolsAllowance({ config, root });
|
|
311
|
+
if (devtoolsAllowance.mode === "content-only") console.log(`right-release: devtools allowance recorded: ${JSON.stringify(devtoolsEvidence(devtoolsAllowance))}`);
|
|
312
|
+
assertNoForbiddenBuildFeatures({ root, tier: "release/update", devtools: devtoolsAllowance, cargoManifest: target.cargoManifest, manifest: config.releaseSwitches?.manifest, commands: [
|
|
313
|
+
{ label: "target.prePackage", command: target.prePackage },
|
|
314
|
+
{ label: "target.package", command: target.package },
|
|
315
|
+
{ label: "nativeAssembly.packageHook", command: nativeAssembly?.packageHook },
|
|
316
|
+
{ label: "nativeAssembly.finalizer", command: nativeFinalizer },
|
|
317
|
+
{ label: "target.postSign", command: target.postSign },
|
|
318
|
+
] });
|
|
319
|
+
} catch (error) { fail(error.message); }
|
|
320
|
+
|
|
321
|
+
// D4 (M21/M23) source layer: every environment/argument read in app source is registered in the app's switches.json and
|
|
322
|
+
// every S1-S4 selector is compiled out of release. The inventory is `config.releaseSwitches.inventory` (relative to the
|
|
323
|
+
// app root) or `switches.json`; a release without one fails (a dry run only checks one that exists).
|
|
324
|
+
// The resolved feature graph (`cargo tree -e features`) is the AUTHORITATIVE test-mode check: it runs here, in the app
|
|
325
|
+
// repository, for the release target, and a release that cannot run it fails (a dry run reports and continues).
|
|
326
|
+
// The binary marker/name scan below is secondary evidence.
|
|
327
|
+
const releaseSwitchInventoryPath = path.resolve(root, config.releaseSwitches?.inventory ?? "switches.json");
|
|
328
|
+
const releaseSwitchInventoryText = existsSync(releaseSwitchInventoryPath) ? readFileSync(releaseSwitchInventoryPath, "utf8") : null;
|
|
329
|
+
try {
|
|
330
|
+
let treeText;
|
|
331
|
+
try {
|
|
332
|
+
const closure = resolveFeatureClosure({
|
|
333
|
+
root,
|
|
334
|
+
manifest: config.releaseSwitches?.manifest,
|
|
335
|
+
cargoManifest: target.cargoManifest,
|
|
336
|
+
rust: config.releaseSwitches?.rust,
|
|
337
|
+
features: config.releaseSwitches?.features ?? [],
|
|
338
|
+
triple: target.cargoTarget ?? target.targetTriple ?? target.rustTarget,
|
|
339
|
+
commandTexts: [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, target.postSign],
|
|
340
|
+
});
|
|
341
|
+
if (closure.skipped) console.log(`right-release: feature closure skipped (${closure.skipped})`);
|
|
342
|
+
else treeText = closure.treeText;
|
|
343
|
+
} catch (error) {
|
|
344
|
+
if (!opts.dryRun) throw error;
|
|
345
|
+
console.log(`right-release: dry run: ${error.message}`);
|
|
346
|
+
}
|
|
347
|
+
// remoteHost: rightkit-daemon `remote` is refused in the closure unless the app declares releaseSwitches.remoteHost: true.
|
|
348
|
+
const closureInput = { treeText, remoteHost: config.releaseSwitches?.remoteHost, commands: [target.prePackage, target.package, nativeAssembly?.packageHook, nativeFinalizer, target.postSign].filter(Boolean).map((c) => commandTokens(c)) };
|
|
349
|
+
if (releaseSwitchInventoryText != null) {
|
|
350
|
+
assertNoReleaseSwitches({ ...closureInput, sourceRoot: root, inventoryText: releaseSwitchInventoryText, appForbid: config.releaseSwitches?.forbid ?? [] }, "source switch inventory");
|
|
351
|
+
} else if (treeText != null || closureInput.commands.length) {
|
|
352
|
+
assertNoReleaseSwitches({ ...closureInput, appForbid: config.releaseSwitches?.forbid ?? [] }, "feature closure");
|
|
353
|
+
}
|
|
354
|
+
if (releaseSwitchInventoryText == null && !opts.dryRun) {
|
|
355
|
+
throw new Error(`source switch inventory: ${path.relative(root, releaseSwitchInventoryPath) || releaseSwitchInventoryPath} is missing; a release build needs the app's switches.json (see packages/release/README.md, "switches.json")`);
|
|
356
|
+
}
|
|
357
|
+
} catch (error) { fail(error.message); }
|
|
358
|
+
|
|
257
359
|
const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
|
|
258
360
|
const mode = "package";
|
|
259
361
|
const command = target.package;
|
|
@@ -283,15 +385,21 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
283
385
|
// packs the exact signed bytes, signs the package through sign-windows, verifies signer == Publisher,
|
|
284
386
|
// writes the App Installer route and the seal record. Never a second signer.
|
|
285
387
|
await runCommand(target.prePackage, root);
|
|
388
|
+
// Raw payload scan BEFORE the package is packed (a packed MSIX is only readable through its ZIP directory).
|
|
389
|
+
assertNoQaControlBinaries("msix staged payload", [path.resolve(root, target.msix.stageDir)], { requireBinary: true, prePack: true });
|
|
286
390
|
const options = msixBuildOptions({ root, target, app: config.app, dryRun: opts.dryRun, receipts: {
|
|
287
391
|
pe: windowsReceiptPath(root, "raw-exe"), msix: windowsReceiptPath(root, "installer"),
|
|
288
392
|
dir: path.dirname(windowsReceiptPath(root, "raw-exe")) } });
|
|
289
393
|
const built = buildMsix(options, { log: (event) => console.log(`right-release: ${JSON.stringify(event)}`) });
|
|
290
394
|
console.log(`right-release: msix ${built.dryRun ? "dry-run" : "built"} ${options.out}`);
|
|
395
|
+
// Second layer: read the finished package through its ZIP central directory (stored and deflate entries).
|
|
396
|
+
if (!built.dryRun) assertNoQaControlBinaries("msix package", [options.out], { requireBinary: true });
|
|
291
397
|
} else if (opts.platform === "win") {
|
|
292
398
|
const rawFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
|
|
293
399
|
await runCommand(target.prePackage, root);
|
|
294
400
|
for (const file of rawFiles) await mustExist(file, `missing raw EXE signing artifact: ${file}`);
|
|
401
|
+
// Before signing and packaging: the raw EXE is uncompressed, so a QA control server is visible here.
|
|
402
|
+
assertNoQaControlBinaries("windows raw EXE", rawFiles, { requireBinary: true, prePack: true });
|
|
295
403
|
// The marker must reach its FINAL value before signing. Tauri rewrites it during
|
|
296
404
|
// bundling, and doing that to signed bytes invalidates Authenticode — the exact
|
|
297
405
|
// defect that shipped invalid binaries. Idempotent, so an app that still patches
|
|
@@ -317,10 +425,17 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
317
425
|
} else if (opts.platform === "mac") {
|
|
318
426
|
if (runMacPhase("prepackage")) await runCommand(target.prePackage, root);
|
|
319
427
|
// Resolve after prePackage: with storage governance the artifact may live only in RIGHT_RELEASE_ARTIFACT_DIR.
|
|
320
|
-
const
|
|
321
|
-
for (const file of
|
|
428
|
+
const signingEntries = macSigning.entries.map((entry) => ({ ...entry, file: resolveReleaseArtifactPath(root, entry.file) }));
|
|
429
|
+
for (const { file } of signingEntries) await mustExist(file, `missing macOS Developer ID signing artifact: ${file}`);
|
|
430
|
+
assertNoQaControlBinaries("macOS signing set", signingEntries.map(({ file }) => file), { requireBinary: true, prePack: true });
|
|
431
|
+
// The DMG is built from the .app (notarize.app / the signing set); a compressed DMG cannot be scanned afterwards.
|
|
432
|
+
if (target.notarize.app) {
|
|
433
|
+
const stagedApp = resolveReleaseArtifactPath(root, target.notarize.app);
|
|
434
|
+
await mustExist(stagedApp, `missing macOS app to be packed into the DMG: ${stagedApp}`);
|
|
435
|
+
assertNoQaControlBinaries("macOS app staged for the DMG", [stagedApp], { requireBinary: true, prePack: true });
|
|
436
|
+
}
|
|
322
437
|
if (runMacPhase("sign")) {
|
|
323
|
-
await signMacos(
|
|
438
|
+
await signMacos(signingEntries, root, "signing", macSigning.nestedCode);
|
|
324
439
|
// Opt-in (notarize.appBeforePackage): staple the .app itself before the package step copies it into the DMG.
|
|
325
440
|
if (target.notarize.appBeforePackage) await notarizeAppBeforePackage(root);
|
|
326
441
|
}
|
|
@@ -337,15 +452,23 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
337
452
|
}
|
|
338
453
|
}
|
|
339
454
|
|
|
455
|
+
// Extra files or directories the app declares (for example the unpacked bundle or a staged payload).
|
|
456
|
+
assertNoQaControlBinaries("target.qaControlScan", (target.qaControlScan ?? []).map((p) => resolveReleaseArtifactPath(root, p)), { requireBinary: (target.qaControlScan ?? []).length > 0, prePack: true });
|
|
457
|
+
|
|
458
|
+
// Frontend bundle: no browser-QA mock may ship. Read-only, so it also runs under --dry-run (against a dist that already exists).
|
|
459
|
+
assertNoWebBundleMocksInFrontend("pre-package", { required: false });
|
|
460
|
+
|
|
340
461
|
if (opts.platform === "mac" && runMacPhase("package")) {
|
|
341
462
|
// SwiftPM + appBeforePackage: the .app is stapled by now, so right-release packs it into the DMG itself.
|
|
342
463
|
const swiftDmg = swiftMacDmgPlan({ target, projectType: resolveProjectType({ appRoot: root, config, target }), appName: config.app ?? path.basename(root), resolve: (p) => resolveReleaseArtifactPath(root, p) });
|
|
343
464
|
if (swiftDmg) {
|
|
344
465
|
if (opts.dryRun) console.log(`dry-run: build DMG ${swiftDmg.out} from stapled app ${swiftDmg.app}, then sign it`);
|
|
345
|
-
else await buildStapledSwiftMacDmg(swiftDmg, { remove: (file) => rmSync(file, { force: true }), signDmg: (dmg) => signMacos([dmg], root, "dmg-signing") });
|
|
466
|
+
else await buildStapledSwiftMacDmg(swiftDmg, { remove: (file) => rmSync(file, { force: true }), signDmg: (dmg) => signMacos([{ file: dmg, options: [] }], root, "dmg-signing") });
|
|
346
467
|
}
|
|
347
468
|
}
|
|
348
469
|
if (command && (opts.platform !== "mac" || runMacPhase("package"))) await runCommand(command, root);
|
|
470
|
+
// The package command builds the frontend; a real run must now find and clean it.
|
|
471
|
+
assertNoWebBundleMocksInFrontend("post-package", { required: !opts.dryRun });
|
|
349
472
|
|
|
350
473
|
for (const rel of target.artifacts ?? []) {
|
|
351
474
|
await mustExist(resolveReleaseArtifactPath(root, rel), `missing release artifact: ${rel}`);
|
|
@@ -364,7 +487,7 @@ if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_C
|
|
|
364
487
|
if (target.postSign) await runCommand(target.postSign, root);
|
|
365
488
|
const updaterFiles = [...new Set((target.updater?.artifacts ?? []).map((artifact) => resolveReleaseArtifactPath(root, artifact.file)))];
|
|
366
489
|
if (!updaterFiles.length) fail(`${config.app} win must declare updater.artifacts for post-code-signing minisign`);
|
|
367
|
-
await run("node", [SIGN_UPDATER, ...(opts.dryRun ? ["--dry-run"] : []), ...updaterFiles.flatMap((file) => ["--file", file])], root);
|
|
490
|
+
await run("node", [SIGN_UPDATER, ...(opts.dryRun ? ["--dry-run"] : []), ...(target.updater?.keyFile ? ["--key-file", String(target.updater.keyFile)] : []), ...updaterFiles.flatMap((file) => ["--file", file])], root);
|
|
368
491
|
}
|
|
369
492
|
|
|
370
493
|
if (target.postSign && opts.platform !== "win" && (opts.platform !== "mac" || runMacPhase("post-sign"))) {
|
|
@@ -387,6 +510,10 @@ if (opts.platform !== "mac" || runMacPhase("harden")) {
|
|
|
387
510
|
hardeningEvidence = path.join(receiptRoot, opts.platform + "-hardening-evidence.json");
|
|
388
511
|
materializeHardeningEvidence({ root, allowances: target.hardeningAllowances, outputPath: hardeningEvidence });
|
|
389
512
|
}
|
|
513
|
+
// Fail closed: the installer itself (NSIS LZMA, compressed DMG) cannot be read, so the raw/staged payload must
|
|
514
|
+
// have been scanned before packing. Inputs that are only ZIP containers (MSIX) are scanned in place.
|
|
515
|
+
if (!opts.dryRun && !prePackScanned) fail("no pre-pack payload scan ran for this release (raw EXE, staged payload or .app); an installer cannot be scanned after packing");
|
|
516
|
+
assertNoQaControlBinaries("hardening targets", scanTargets.map((p) => resolveReleaseArtifactPath(root, p)));
|
|
390
517
|
await run("node", [
|
|
391
518
|
HARDENING_SCAN,
|
|
392
519
|
...(hardeningEvidence ? ["--allow-evidence", hardeningEvidence] : []),
|
|
@@ -425,6 +552,51 @@ function usage(code, message) {
|
|
|
425
552
|
// Product package scripts may emit final installer bytes directly into the
|
|
426
553
|
// managed artifact directory. Keep legacy target-relative paths working while
|
|
427
554
|
// preferring managed output when the local path does not exist.
|
|
555
|
+
// Release and update builds never contain the QA control server (rightkit-qa-driver `qa-control`).
|
|
556
|
+
// The development lane (development.mjs) does not run this file and is allowed to contain it.
|
|
557
|
+
// `requireBinary`: an input holding no native binary (missing or empty staging dir) is a failure, not a pass.
|
|
558
|
+
// `prePack`: marks that the raw payload was scanned before packing. What stays unscannable after packing
|
|
559
|
+
// (NSIS LZMA payload, compressed DMG) is logged so the release record says so.
|
|
560
|
+
function assertNoQaControlBinaries(label, files, { requireBinary = false, prePack = false } = {}) {
|
|
561
|
+
if (opts.dryRun || files.length === 0) return;
|
|
562
|
+
try {
|
|
563
|
+
const stats = assertNoQaControl(files, label, { requireBinary, allowContentOnlyDevtools: devtoolsAllowance.mode === "content-only" });
|
|
564
|
+
// D4 binary layer, same inputs: no test-mode marker and no S1-S4 selector name in any native binary.
|
|
565
|
+
assertNoReleaseSwitches({ binaryPaths: files, inventoryText: releaseSwitchInventoryText ?? undefined, appForbid: config.releaseSwitches?.forbid ?? [], requireBinary: false }, `${label} (release switches)`);
|
|
566
|
+
if (prePack && stats.binaries > 0) prePackScanned = true;
|
|
567
|
+
for (const opaque of stats.opaque) {
|
|
568
|
+
console.log(`right-release: ${label}: ${opaque.kind.toUpperCase()} payload is compressed and cannot be scanned (${opaque.file}); the raw payload was scanned before packing`);
|
|
569
|
+
}
|
|
570
|
+
} catch (error) {
|
|
571
|
+
fail(error.message);
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Frontend dist directories to scan: `target.webBundleScan` (explicit, relative to the app root) or, when absent, the
|
|
576
|
+
// Tauri `build.frontendDist` / `build.distDir` of src-tauri/tauri.conf.json. A URL (dev server) is not a bundle.
|
|
577
|
+
function frontendBundleDirs() {
|
|
578
|
+
if (Array.isArray(target.webBundleScan)) return target.webBundleScan.map((p) => path.resolve(root, p));
|
|
579
|
+
const confPath = path.join(root, "src-tauri", "tauri.conf.json");
|
|
580
|
+
if (!existsSync(confPath)) return [];
|
|
581
|
+
try {
|
|
582
|
+
const build = JSON.parse(readFileSync(confPath, "utf8")).build ?? {};
|
|
583
|
+
const dist = build.frontendDist ?? build.distDir;
|
|
584
|
+
if (typeof dist !== "string" || /^[a-z][a-z0-9+.-]*:\/\//i.test(dist)) return [];
|
|
585
|
+
return [path.resolve(root, "src-tauri", dist)];
|
|
586
|
+
} catch (error) { fail(`src-tauri/tauri.conf.json is unreadable, so the frontend bundle cannot be located for the mock scan: ${error.message}`); }
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
function assertNoWebBundleMocksInFrontend(phase, { required }) {
|
|
590
|
+
const dirs = frontendBundleDirs() ?? [];
|
|
591
|
+
const present = dirs.filter((dir) => existsSync(dir));
|
|
592
|
+
if (!required && present.length === 0) return;
|
|
593
|
+
if (dirs.length === 0) return;
|
|
594
|
+
try {
|
|
595
|
+
const { files } = assertNoWebBundleMocks(required ? dirs : present, `${phase} frontend bundle`, { requireFiles: true });
|
|
596
|
+
console.log(`right-release: ${phase} frontend bundle: ${files} web file(s) scanned, no browser-QA mock marker`);
|
|
597
|
+
} catch (error) { fail(error.message); }
|
|
598
|
+
}
|
|
599
|
+
|
|
428
600
|
function resolveReleaseArtifactPath(root, declared) {
|
|
429
601
|
const local = path.resolve(root, declared);
|
|
430
602
|
if (existsSync(local)) return local;
|
|
@@ -723,16 +895,105 @@ async function validateRightKitPackageContract(root, appName, hostedWorkflows) {
|
|
|
723
895
|
fail(`${appName ?? pkg.name ?? "app"} ${name} must come from the published npm package, not ${specifier}`);
|
|
724
896
|
}
|
|
725
897
|
const allowed = RIGHTKIT_ALLOWED.get(name);
|
|
726
|
-
if (allowed && !allowed
|
|
898
|
+
if (allowed && !rightKitVersionAllowed(allowed, specifier)) {
|
|
727
899
|
fail(`${appName ?? pkg.name ?? "app"} ${name} must be one of ${[...allowed].join(", ")}, got ${specifier}`);
|
|
728
900
|
}
|
|
729
901
|
}
|
|
730
902
|
const releaseAllowed = RIGHTKIT_ALLOWED.get("@rightkit/release");
|
|
731
|
-
if (!releaseAllowed
|
|
903
|
+
if (!releaseAllowed || !rightKitVersionAllowed(releaseAllowed, pkg.devDependencies?.["@rightkit/release"])) {
|
|
732
904
|
fail(`${appName ?? pkg.name ?? "app"} must depend on a supported @rightkit/release version in devDependencies`);
|
|
733
905
|
}
|
|
734
906
|
}
|
|
735
907
|
|
|
908
|
+
// The top-level `on:` block(s) of a workflow: the `on` line and every indented, blank or comment line after it.
|
|
909
|
+
function workflowTriggerBlocks(source) {
|
|
910
|
+
const lines = source.split("\n");
|
|
911
|
+
const blocks = [];
|
|
912
|
+
for (let index = 0; index < lines.length; index += 1) {
|
|
913
|
+
if (!/^(?:on|"on"|'on')[ \t]*:/.test(lines[index])) continue;
|
|
914
|
+
let end = index + 1;
|
|
915
|
+
while (end < lines.length && (lines[end] === "" || /^[\s#]/.test(lines[end]))) end += 1;
|
|
916
|
+
blocks.push(lines.slice(index, end).join("\n"));
|
|
917
|
+
}
|
|
918
|
+
return blocks;
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// The job of a managed workflow that holds `index` ({ name, start }), or null when the offset sits outside every job.
|
|
922
|
+
function workflowJobAt(source, index) {
|
|
923
|
+
const marker = "\njobs:\n";
|
|
924
|
+
const jobsAt = source.indexOf(marker);
|
|
925
|
+
if (jobsAt < 0) return null;
|
|
926
|
+
const bodyStart = jobsAt + marker.length;
|
|
927
|
+
const unindented = /^[^\s#]/m.exec(source.slice(bodyStart));
|
|
928
|
+
const bodyEnd = unindented ? bodyStart + unindented.index : source.length;
|
|
929
|
+
if (index < bodyStart || index >= bodyEnd) return null;
|
|
930
|
+
let job = null;
|
|
931
|
+
for (const header of source.slice(bodyStart, bodyEnd).matchAll(/^ {2}(?![\s#])([^\n]*)$/gm)) {
|
|
932
|
+
const start = bodyStart + header.index;
|
|
933
|
+
if (start > index) break;
|
|
934
|
+
const key = /^(?:"([^"\n]*)"|'([^'\n]*)'|([^\s:"'][^\s:]*))[ \t]*:/.exec(header[1]);
|
|
935
|
+
job = { name: key ? (key[1] ?? key[2] ?? key[3]) : header[1], start };
|
|
936
|
+
}
|
|
937
|
+
return job;
|
|
938
|
+
}
|
|
939
|
+
|
|
940
|
+
// Why a `secrets.<name>` reference at `index` is not the declared swift.privatePackages credential in its
|
|
941
|
+
// exact rendered place, or null when it is. Callers handle the signing and publish allowances first.
|
|
942
|
+
function swiftPrivateSecretRefusal({ source, workflow, manifest, lanes, name, index }) {
|
|
943
|
+
const refuse = (reason) => `secrets.${name}: ${reason}`;
|
|
944
|
+
const entries = manifest?.swift?.privatePackages;
|
|
945
|
+
const declared = Array.isArray(entries)
|
|
946
|
+
&& entries.length > 0
|
|
947
|
+
&& entries.every((entry) => typeof entry?.secret === "string" && /^[A-Za-z_][A-Za-z0-9_]{0,99}$/.test(entry.secret) && !/^github_/i.test(entry.secret));
|
|
948
|
+
if (!declared || !entries.some((entry) => entry.secret === name)) {
|
|
949
|
+
return refuse(Array.isArray(entries) && entries.length > 0
|
|
950
|
+
? "not an allowed release secret and not declared in .rightgit.json swift.privatePackages"
|
|
951
|
+
: "not an allowed release secret for this workflow");
|
|
952
|
+
}
|
|
953
|
+
const publishSecrets = manifest?.candidate?.releaseChain?.publishSecrets;
|
|
954
|
+
const reserved = new Set([...APPLE_RELEASE_SECRETS, ...PACKAGE_MANAGER_PUBLISH_SECRETS, ...(Array.isArray(publishSecrets) ? publishSecrets : [])]);
|
|
955
|
+
const collisions = entries.map((entry) => entry.secret).filter((secret) => reserved.has(secret));
|
|
956
|
+
if (collisions.length > 0) {
|
|
957
|
+
return refuse(`.rightgit.json swift.privatePackages declares a signing or publishing secret name (${collisions.join(", ")})`);
|
|
958
|
+
}
|
|
959
|
+
const jobs = SWIFT_PRIVATE_JOBS.get(workflow);
|
|
960
|
+
if (!jobs) return refuse(`swift.privatePackages credentials are never rendered into ${workflow}`);
|
|
961
|
+
const lane = workflow.replace(/\.yml$/, "");
|
|
962
|
+
if (!lanes.includes(lane)) return refuse(`lane ${lane} is not listed in .rightgit.json`);
|
|
963
|
+
if (manifest?.devArtifact?.dispatch === true) return refuse("devArtifact.dispatch is true");
|
|
964
|
+
const triggerBlocks = workflowTriggerBlocks(source);
|
|
965
|
+
if (triggerBlocks.length === 0) return refuse(`${workflow} has no on: block to check its triggers`);
|
|
966
|
+
const untrusted = /\b(pull_request_target|workflow_run)\b/.exec(triggerBlocks.join("\n"));
|
|
967
|
+
if (untrusted) return refuse(`${workflow} is triggered by ${untrusted[1]}`);
|
|
968
|
+
if (!SWIFT_PRIVATE_DISPATCH_WORKFLOWS.has(workflow) && /workflow_dispatch/.test(source)) {
|
|
969
|
+
return refuse(`${workflow} has a workflow_dispatch trigger`);
|
|
970
|
+
}
|
|
971
|
+
const job = workflowJobAt(source, index);
|
|
972
|
+
if (!job) return refuse(`reference sits outside every job of ${workflow}`);
|
|
973
|
+
const rule = jobs.get(job.name);
|
|
974
|
+
if (!rule) return refuse(`job '${job.name}' of ${workflow} may not hold the swift.privatePackages credential (allowed: ${[...jobs.keys()].join(", ")})`);
|
|
975
|
+
if (!rule.allows(manifest)) return refuse(`job '${job.name}' of ${workflow} is rendered only for ${rule.only}`);
|
|
976
|
+
const lineStart = source.lastIndexOf("\n", index) + 1;
|
|
977
|
+
const lineEnd = source.indexOf("\n", index);
|
|
978
|
+
const line = source.slice(lineStart, lineEnd < 0 ? source.length : lineEnd);
|
|
979
|
+
const envLine = /^ {10}RIGHT_GIT_PRIVATE_TOKEN_(0|[1-9]\d*): \$\{\{ secrets\.([A-Za-z0-9_]+) \}\}$/.exec(line);
|
|
980
|
+
if (!envLine || envLine[2] !== name || entries[Number(envLine[1])]?.secret !== name) {
|
|
981
|
+
return refuse("not a RIGHT_GIT_PRIVATE_TOKEN_<n> env line naming entry <n> of swift.privatePackages");
|
|
982
|
+
}
|
|
983
|
+
// The owning step is the nearest preceding step line (` - ...`) of the same job, and the line must
|
|
984
|
+
// sit in that step's own `env:` block.
|
|
985
|
+
const beforeLine = source.slice(job.start, lineStart);
|
|
986
|
+
const owner = [...beforeLine.matchAll(/^ {6}- [^\n]*$/gm)].at(-1);
|
|
987
|
+
const ownerName = /^ {6}- name: (.+)$/.exec(owner?.[0] ?? "")?.[1];
|
|
988
|
+
const sinceOwner = owner ? beforeLine.slice(owner.index).split("\n") : [];
|
|
989
|
+
let header = sinceOwner.length - 1;
|
|
990
|
+
while (header >= 0 && (sinceOwner[header] === "" || /^ {10}/.test(sinceOwner[header]))) header -= 1;
|
|
991
|
+
if (!SWIFT_PRIVATE_STEP_NAMES.has(ownerName) || sinceOwner[header] !== " env:") {
|
|
992
|
+
return refuse('not in the env of the "Configure private Swift package access" or "Remove private Swift package access" step');
|
|
993
|
+
}
|
|
994
|
+
return null;
|
|
995
|
+
}
|
|
996
|
+
|
|
736
997
|
async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
737
998
|
if (policy !== "right-git-ci-only") {
|
|
738
999
|
fail(`${appName} hosted workflow files are forbidden: ${workflows.join(", ")}`);
|
|
@@ -745,7 +1006,7 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
745
1006
|
fail(`${appName} right-git-ci-only policy requires a valid .rightgit.json`);
|
|
746
1007
|
}
|
|
747
1008
|
const lanes = manifest?.lanes;
|
|
748
|
-
// Mirrors the `lanes` enum in @rightkit/
|
|
1009
|
+
// Mirrors the `lanes` enum in @rightkit/ci manifest.schema.json.
|
|
749
1010
|
const safeLanes = new Set(["ci", "qualification", "package-smoke", "release-candidate", "publish-npm", "publish-package-managers", "regenerate"]);
|
|
750
1011
|
if (manifest?.schemaVersion !== 1
|
|
751
1012
|
|| manifest?.compile !== "github-actions-only"
|
|
@@ -775,7 +1036,7 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
775
1036
|
};
|
|
776
1037
|
const inside = (index, span) => span && index >= span.start && index < span.end;
|
|
777
1038
|
const requestedSecrets = [...source.matchAll(/\bsecrets\.([A-Z0-9_]+)/gi)].map((match) => ({ name: match[1], index: match.index }));
|
|
778
|
-
const allowedMacosSecrets = new Set(
|
|
1039
|
+
const allowedMacosSecrets = new Set(APPLE_RELEASE_SECRETS);
|
|
779
1040
|
const windowsSpan = jobSpan("windows-sign");
|
|
780
1041
|
const macosSpan = jobSpan("macos-sign");
|
|
781
1042
|
const publishSpan = jobSpan("publish");
|
|
@@ -802,10 +1063,10 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
802
1063
|
const protectedMacosSigning = workflow === "release-candidate.yml"
|
|
803
1064
|
&& manifest?.candidate?.signMacos === true
|
|
804
1065
|
&& (legacySigningGate("macos-sign").test(macosSource) || admittedSigningGate("macos-sign").test(macosSource));
|
|
805
|
-
// @rightkit/
|
|
1066
|
+
// @rightkit/ci `devArtifact.mac: "sign"` renders a `dev-mac-sign` job into
|
|
806
1067
|
// the managed ci.yml: push to the default branch only, release environment,
|
|
807
1068
|
// Developer ID certificate secrets only (no notarization keys).
|
|
808
|
-
// @rightkit/
|
|
1069
|
+
// @rightkit/ci `regenerate` lane: manual-only, opens a pull request from a
|
|
809
1070
|
// right-git/regenerate-<run> branch and never writes to the default branch.
|
|
810
1071
|
// `contents: write` and `pull-requests: write` are accepted only inside that
|
|
811
1072
|
// job, only when the manifest declares `regenerate`, and only when the
|
|
@@ -831,6 +1092,8 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
831
1092
|
const protectedDevMacSigning = workflow === "ci.yml"
|
|
832
1093
|
&& (manifest?.devArtifact?.mac === "sign" || manifest?.devArtifact?.mac === "notarize")
|
|
833
1094
|
&& /dev-mac-sign:\n(?: #[^\n]*\n)* needs: \[ci\]\n if: \$\{\{ github\.event_name == 'push' && github\.ref_name == github\.event\.repository\.default_branch \}\}\n environment: /.test(devMacSource);
|
|
1095
|
+
// @rightkit/ci `swift.privatePackages` credential: allowed only in the workflow/job pairs and on the
|
|
1096
|
+
// env lines listed with SWIFT_PRIVATE_JOBS; swiftPrivateSecretRefusal explains every other reference.
|
|
834
1097
|
const releaseChain = workflow === "release-candidate.yml" ? manifest?.candidate?.releaseChain : undefined;
|
|
835
1098
|
const allowedReleaseSecrets = new Set(releaseChain?.publishSecrets ?? []);
|
|
836
1099
|
const writePermissions = [...source.matchAll(/^\s*(actions|attestations|checks|contents|deployments|discussions|id-token|issues|models|packages|pages|pull-requests|security-events|statuses):\s*write\s*$/gmi)].map((match) => ({ name: match[1].toLowerCase(), index: match.index }));
|
|
@@ -847,6 +1110,14 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
847
1110
|
const protectedReleasePublish = releaseChain
|
|
848
1111
|
&& (legacyReleasePublishGate || admittedReleasePublishGate)
|
|
849
1112
|
&& /\n permissions:\n contents: write\n/.test(publishSource);
|
|
1113
|
+
// A secret reference is refused unless a signing or publish allowance above covers it or it is the
|
|
1114
|
+
// declared swift.privatePackages credential in its exact rendered place.
|
|
1115
|
+
const refusedSecrets = requestedSecrets
|
|
1116
|
+
.filter(({ name, index }) => !(protectedMacosSigning && inside(index, macosSpan) && allowedMacosSecrets.has(name))
|
|
1117
|
+
&& !(protectedDevMacSigning && inside(index, devMacSpan) && allowedDevMacSecrets.has(name))
|
|
1118
|
+
&& !(protectedReleasePublish && inside(index, publishSpan) && allowedReleaseSecrets.has(name)))
|
|
1119
|
+
.map(({ name, index }) => swiftPrivateSecretRefusal({ source, workflow, manifest, lanes, name, index }))
|
|
1120
|
+
.filter((reason) => reason !== null);
|
|
850
1121
|
if (/^\s*permissions:\s*write-all\s*$/mi.test(source)
|
|
851
1122
|
|| writePermissions.some(({ name, index }) => {
|
|
852
1123
|
if (protectedRegenerate && inside(index, regenerateSpan) && allowedRegeneratePermissions.has(name)) return false;
|
|
@@ -856,20 +1127,21 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
856
1127
|
})
|
|
857
1128
|
|| /pull_request_target\s*:/i.test(source)
|
|
858
1129
|
|| /\bsecrets\s*\[/i.test(source)
|
|
859
|
-
||
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
fail(`${appName} right-git managed ${workflow} requests release-capable permissions or secrets`);
|
|
1130
|
+
|| refusedSecrets.length > 0) {
|
|
1131
|
+
const reasons = [...new Set(refusedSecrets)];
|
|
1132
|
+
const detail = reasons.length > 10 ? [...reasons.slice(0, 10), `${reasons.length - 10} more`] : reasons;
|
|
1133
|
+
fail(`${appName} right-git managed ${workflow} requests release-capable permissions or secrets${detail.length > 0 ? `: ${detail.join("; ")}` : ""}`);
|
|
863
1134
|
}
|
|
864
1135
|
}
|
|
865
1136
|
}
|
|
866
1137
|
|
|
867
|
-
function buildAllowedVersions(published = {}, staged = {}, legacy = {}) {
|
|
1138
|
+
function buildAllowedVersions(published = {}, staged = {}, legacy = {}, registry = {}) {
|
|
868
1139
|
const allowed = new Map();
|
|
869
1140
|
for (const [name, value] of [
|
|
870
1141
|
...Object.entries(published),
|
|
871
1142
|
...Object.entries(staged),
|
|
872
1143
|
...Object.entries(legacy),
|
|
1144
|
+
...Object.entries(registry),
|
|
873
1145
|
]) {
|
|
874
1146
|
const versions = allowed.get(name) ?? new Set();
|
|
875
1147
|
for (const version of Array.isArray(value) ? value : [value]) versions.add(version);
|
|
@@ -908,24 +1180,37 @@ function windowsReceiptPath(root, phase) {
|
|
|
908
1180
|
return signingReceiptPath({ appRoot: root, target, platform: "win", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
|
|
909
1181
|
}
|
|
910
1182
|
|
|
911
|
-
|
|
1183
|
+
// A signing step with per-file settings (entitlements, codesign options) goes to sign-macos.mjs as a manifest;
|
|
1184
|
+
// a step with only defaults keeps the historical positional invocation.
|
|
1185
|
+
function macSignInvocation(items, receiptPath, extra = []) {
|
|
1186
|
+
if (opts.dryRun || !items.some(hasNonDefaultSettings)) return [SIGN_MACOS, ...extra, "--receipt", receiptPath, ...items.map((item) => item.file)];
|
|
1187
|
+
const manifest = path.resolve(path.dirname(receiptPath), `${path.basename(receiptPath, ".json")}.manifest.json`);
|
|
1188
|
+
mkdirSync(path.dirname(manifest), { recursive: true });
|
|
1189
|
+
writeFileSync(manifest, `${JSON.stringify({ schema: 1, entries: items.map((item) => ({ file: item.file, ...(item.entitlements ? { entitlements: item.entitlements } : {}), options: item.options ?? [] })) }, null, 2)}\n`);
|
|
1190
|
+
return [SIGN_MACOS, ...extra, "--receipt", receiptPath, "--manifest", manifest];
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
async function signMacos(entries, root, phase = "signing", nestedCode = []) {
|
|
1194
|
+
const files = entries.map((entry) => entry.file);
|
|
912
1195
|
const receipt = phase === "signing" ? macosReceiptPath(root, "signing") : path.resolve(process.env.RIGHT_RELEASE_STATE_ROOT ?? path.join(root, ".right-release", "receipts"), `macos-${phase}.json`);
|
|
913
|
-
// Directory entries (.app bundles): nested code first
|
|
1196
|
+
// Directory entries (.app bundles): nested code first (default settings unless sign.nestedCode names it), then the bundle itself.
|
|
914
1197
|
let nested = [];
|
|
915
|
-
try { ({ nested } = opts.dryRun ? { nested: [] } :
|
|
1198
|
+
try { ({ nested } = opts.dryRun ? { nested: [] } : planMacSigning({ entries, nestedCode })); } catch (error) { fail(error.message); }
|
|
1199
|
+
if (opts.dryRun && (nestedCode.length || entries.some(hasNonDefaultSettings))) {
|
|
1200
|
+
console.log(`right-release: dry-run: per-file signing matrix (${nestedCode.length} nestedCode rule(s), ${entries.filter(hasNonDefaultSettings).length} entry setting(s)); nested code is resolved against the built bundle at sign time`);
|
|
1201
|
+
}
|
|
916
1202
|
if (nested.length) {
|
|
917
1203
|
const nestedReceipt = path.resolve(path.dirname(receipt), `macos-${phase}-nested.json`);
|
|
918
1204
|
if (existsSync(nestedReceipt)) unlinkSync(nestedReceipt);
|
|
919
1205
|
console.log(`right-release: signing ${nested.length} nested code item(s) inside-out`);
|
|
920
|
-
await run("node",
|
|
1206
|
+
await run("node", macSignInvocation(nested, nestedReceipt), root);
|
|
1207
|
+
verifyMacosSigningSettings(nestedReceipt, nested);
|
|
921
1208
|
}
|
|
922
|
-
const args = [
|
|
923
|
-
if (opts.dryRun) args.push("--dry-run");
|
|
924
|
-
args.push("--receipt", receipt, ...files);
|
|
1209
|
+
const args = macSignInvocation(entries, receipt, opts.dryRun ? ["--dry-run"] : []);
|
|
925
1210
|
if (!opts.dryRun && existsSync(receipt)) unlinkSync(receipt);
|
|
926
1211
|
console.log("right-release: signing macOS Developer ID files");
|
|
927
1212
|
await run("node", args, root);
|
|
928
|
-
if (!opts.dryRun) verifyMacosSigningReceipt(receipt, files);
|
|
1213
|
+
if (!opts.dryRun) { verifyMacosSigningReceipt(receipt, files); verifyMacosSigningSettings(receipt, entries); }
|
|
929
1214
|
}
|
|
930
1215
|
|
|
931
1216
|
async function notarizeMacos(archive, root) {
|
|
@@ -977,6 +1262,15 @@ function verifyMacosSigningReceipt(receipt, files) {
|
|
|
977
1262
|
|| evidence.files.some((item) => item.codesign !== "strict-valid" || item.identity !== expectedIdentity || item.timestampPresent !== true || item.after?.sha256 !== hashes.get(path.resolve(item.file)))) fail(`invalid macOS Developer ID signing receipt evidence: ${receipt}`);
|
|
978
1263
|
}
|
|
979
1264
|
|
|
1265
|
+
// The receipt must record the entitlements digest and codesign options the plan asked for.
|
|
1266
|
+
function verifyMacosSigningSettings(receipt, items) {
|
|
1267
|
+
if (!items.some(hasNonDefaultSettings)) return;
|
|
1268
|
+
let evidence;
|
|
1269
|
+
try { evidence = JSON.parse(readFileSync(receipt, "utf8")); } catch { fail(`invalid macOS Developer ID signing receipt: ${receipt}`); }
|
|
1270
|
+
const problems = matrixReceiptProblems(evidence.files, items, (file) => fileEvidence(file).sha256);
|
|
1271
|
+
if (problems.length) fail(`macOS signing receipt does not match the signing matrix (${receipt}):\n${problems.map((line) => `- ${line}`).join("\n")}`);
|
|
1272
|
+
}
|
|
1273
|
+
|
|
980
1274
|
function verifyMacosNotarizationReceipt(receipt, archive) {
|
|
981
1275
|
try { validateNotarizationReceipt({ receiptPath: receipt, archive }); }
|
|
982
1276
|
catch (error) { fail(error.message); }
|
package/renamed-away.mjs
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Crates renamed in the owner naming batch (2026-10-14). The old names stay in `cargo` as published history that apps
|
|
2
|
+
// still pin until they migrate, but the tree has only the new directory and the old name is never staged again.
|
|
3
|
+
// Remove an entry here when the last app pin moves and `cargo` drops the old name.
|
|
4
|
+
export const RENAMED_AWAY = new Map([
|
|
5
|
+
["rightkit-shell", "rightkit-tauri-shell"],
|
|
6
|
+
["rightkit-tauri", "rightkit-device"],
|
|
7
|
+
["rightkit-control", "rightkit-qa-driver"],
|
|
8
|
+
["rightkit-jobs", "rightkit-job-queue"],
|
|
9
|
+
["rightkit-process", "rightkit-child-process"],
|
|
10
|
+
["rightkit-framed-sidecar", "rightkit-sidecar"],
|
|
11
|
+
["rightkit-secrets", "rightkit-credentials"],
|
|
12
|
+
["rightkit-settings", "rightkit-app-settings"],
|
|
13
|
+
["rightkit-ort", "rightkit-onnx"],
|
|
14
|
+
["rightkit-media", "rightkit-ffmpeg"],
|
|
15
|
+
["rightkit-watch", "rightkit-file-watch"],
|
|
16
|
+
["rightkit-scenario", "rightkit-workflow"],
|
|
17
|
+
["rightkit-qts", "rightkit-tts-qwen"],
|
|
18
|
+
["rightkit-qts-ggml-sys", "rightkit-tts-qwen-sys"],
|
|
19
|
+
["rightkit-qts-ggml", null],
|
|
20
|
+
]);
|
|
21
|
+
|
|
22
|
+
// npm packages renamed in the same batch. Old names stay in rightkit-versions.json (npm/stagedNpm/legacyNpm) as
|
|
23
|
+
// published history that apps still pin; the tree has only the successor directory.
|
|
24
|
+
export const RENAMED_AWAY_NPM = new Map([
|
|
25
|
+
["@rightkit/git", "@rightkit/ci"],
|
|
26
|
+
["@rightkit/updates", "@rightkit/app-update"],
|
|
27
|
+
["@rightkit/shell", "@rightkit/tauri-shell"],
|
|
28
|
+
["@rightkit/app-shell", "@rightkit/react-shell"],
|
|
29
|
+
]);
|