@rightkit/release 0.2.150 → 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 +119 -8
- package/build-release.mjs +16 -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/macos-notarization.mjs +18 -2
- 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/notarize-macos.mjs +4 -3
- 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 +324 -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 +212 -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,10 +197,21 @@ 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`);
|
|
203
|
+
if (target.notarize.staple === false && (target.notarize.app || /\.(dmg|pkg)$/i.test(target.notarize.file))) {
|
|
204
|
+
fail(`${config.app ?? "app"} mac notarize.staple: false applies only to an archive of bare executables (no notarize.app, not a .dmg/.pkg)`);
|
|
205
|
+
}
|
|
163
206
|
const swiftDmgError = swiftMacDmgConfigError({ target, projectType: resolveProjectType({ appRoot: root, config, target }), appName: config.app ?? "app" });
|
|
164
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
|
+
}
|
|
165
215
|
}
|
|
166
216
|
if (opts.upload && target.publishBlocked) fail(`${config.app ?? "app"} ${opts.platform} publish blocked: ${target.publishBlocked}`);
|
|
167
217
|
/**
|
|
@@ -251,6 +301,61 @@ if (opts.doctor) {
|
|
|
251
301
|
process.exit(0);
|
|
252
302
|
}
|
|
253
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
|
+
|
|
254
359
|
const releaseLock = acquireReleaseLock(root, config.app ?? path.basename(root), opts.platform, opts.tier);
|
|
255
360
|
const mode = "package";
|
|
256
361
|
const command = target.package;
|
|
@@ -280,15 +385,21 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
280
385
|
// packs the exact signed bytes, signs the package through sign-windows, verifies signer == Publisher,
|
|
281
386
|
// writes the App Installer route and the seal record. Never a second signer.
|
|
282
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 });
|
|
283
390
|
const options = msixBuildOptions({ root, target, app: config.app, dryRun: opts.dryRun, receipts: {
|
|
284
391
|
pe: windowsReceiptPath(root, "raw-exe"), msix: windowsReceiptPath(root, "installer"),
|
|
285
392
|
dir: path.dirname(windowsReceiptPath(root, "raw-exe")) } });
|
|
286
393
|
const built = buildMsix(options, { log: (event) => console.log(`right-release: ${JSON.stringify(event)}`) });
|
|
287
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 });
|
|
288
397
|
} else if (opts.platform === "win") {
|
|
289
398
|
const rawFiles = target.sign.prePackageFiles.map((p) => path.resolve(root, p));
|
|
290
399
|
await runCommand(target.prePackage, root);
|
|
291
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 });
|
|
292
403
|
// The marker must reach its FINAL value before signing. Tauri rewrites it during
|
|
293
404
|
// bundling, and doing that to signed bytes invalidates Authenticode — the exact
|
|
294
405
|
// defect that shipped invalid binaries. Idempotent, so an app that still patches
|
|
@@ -314,10 +425,17 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
314
425
|
} else if (opts.platform === "mac") {
|
|
315
426
|
if (runMacPhase("prepackage")) await runCommand(target.prePackage, root);
|
|
316
427
|
// Resolve after prePackage: with storage governance the artifact may live only in RIGHT_RELEASE_ARTIFACT_DIR.
|
|
317
|
-
const
|
|
318
|
-
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
|
+
}
|
|
319
437
|
if (runMacPhase("sign")) {
|
|
320
|
-
await signMacos(
|
|
438
|
+
await signMacos(signingEntries, root, "signing", macSigning.nestedCode);
|
|
321
439
|
// Opt-in (notarize.appBeforePackage): staple the .app itself before the package step copies it into the DMG.
|
|
322
440
|
if (target.notarize.appBeforePackage) await notarizeAppBeforePackage(root);
|
|
323
441
|
}
|
|
@@ -334,15 +452,23 @@ if (opts.platform === "win" && target.signingContract === MSIX_SIGNING_CONTRACT)
|
|
|
334
452
|
}
|
|
335
453
|
}
|
|
336
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
|
+
|
|
337
461
|
if (opts.platform === "mac" && runMacPhase("package")) {
|
|
338
462
|
// SwiftPM + appBeforePackage: the .app is stapled by now, so right-release packs it into the DMG itself.
|
|
339
463
|
const swiftDmg = swiftMacDmgPlan({ target, projectType: resolveProjectType({ appRoot: root, config, target }), appName: config.app ?? path.basename(root), resolve: (p) => resolveReleaseArtifactPath(root, p) });
|
|
340
464
|
if (swiftDmg) {
|
|
341
465
|
if (opts.dryRun) console.log(`dry-run: build DMG ${swiftDmg.out} from stapled app ${swiftDmg.app}, then sign it`);
|
|
342
|
-
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") });
|
|
343
467
|
}
|
|
344
468
|
}
|
|
345
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 });
|
|
346
472
|
|
|
347
473
|
for (const rel of target.artifacts ?? []) {
|
|
348
474
|
await mustExist(resolveReleaseArtifactPath(root, rel), `missing release artifact: ${rel}`);
|
|
@@ -361,7 +487,7 @@ if (opts.platform === "win" && target.signingContract === WINDOWS_NSIS_SIGNING_C
|
|
|
361
487
|
if (target.postSign) await runCommand(target.postSign, root);
|
|
362
488
|
const updaterFiles = [...new Set((target.updater?.artifacts ?? []).map((artifact) => resolveReleaseArtifactPath(root, artifact.file)))];
|
|
363
489
|
if (!updaterFiles.length) fail(`${config.app} win must declare updater.artifacts for post-code-signing minisign`);
|
|
364
|
-
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);
|
|
365
491
|
}
|
|
366
492
|
|
|
367
493
|
if (target.postSign && opts.platform !== "win" && (opts.platform !== "mac" || runMacPhase("post-sign"))) {
|
|
@@ -384,6 +510,10 @@ if (opts.platform !== "mac" || runMacPhase("harden")) {
|
|
|
384
510
|
hardeningEvidence = path.join(receiptRoot, opts.platform + "-hardening-evidence.json");
|
|
385
511
|
materializeHardeningEvidence({ root, allowances: target.hardeningAllowances, outputPath: hardeningEvidence });
|
|
386
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)));
|
|
387
517
|
await run("node", [
|
|
388
518
|
HARDENING_SCAN,
|
|
389
519
|
...(hardeningEvidence ? ["--allow-evidence", hardeningEvidence] : []),
|
|
@@ -422,6 +552,51 @@ function usage(code, message) {
|
|
|
422
552
|
// Product package scripts may emit final installer bytes directly into the
|
|
423
553
|
// managed artifact directory. Keep legacy target-relative paths working while
|
|
424
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
|
+
|
|
425
600
|
function resolveReleaseArtifactPath(root, declared) {
|
|
426
601
|
const local = path.resolve(root, declared);
|
|
427
602
|
if (existsSync(local)) return local;
|
|
@@ -720,16 +895,105 @@ async function validateRightKitPackageContract(root, appName, hostedWorkflows) {
|
|
|
720
895
|
fail(`${appName ?? pkg.name ?? "app"} ${name} must come from the published npm package, not ${specifier}`);
|
|
721
896
|
}
|
|
722
897
|
const allowed = RIGHTKIT_ALLOWED.get(name);
|
|
723
|
-
if (allowed && !allowed
|
|
898
|
+
if (allowed && !rightKitVersionAllowed(allowed, specifier)) {
|
|
724
899
|
fail(`${appName ?? pkg.name ?? "app"} ${name} must be one of ${[...allowed].join(", ")}, got ${specifier}`);
|
|
725
900
|
}
|
|
726
901
|
}
|
|
727
902
|
const releaseAllowed = RIGHTKIT_ALLOWED.get("@rightkit/release");
|
|
728
|
-
if (!releaseAllowed
|
|
903
|
+
if (!releaseAllowed || !rightKitVersionAllowed(releaseAllowed, pkg.devDependencies?.["@rightkit/release"])) {
|
|
729
904
|
fail(`${appName ?? pkg.name ?? "app"} must depend on a supported @rightkit/release version in devDependencies`);
|
|
730
905
|
}
|
|
731
906
|
}
|
|
732
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
|
+
|
|
733
997
|
async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
734
998
|
if (policy !== "right-git-ci-only") {
|
|
735
999
|
fail(`${appName} hosted workflow files are forbidden: ${workflows.join(", ")}`);
|
|
@@ -742,7 +1006,7 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
742
1006
|
fail(`${appName} right-git-ci-only policy requires a valid .rightgit.json`);
|
|
743
1007
|
}
|
|
744
1008
|
const lanes = manifest?.lanes;
|
|
745
|
-
// Mirrors the `lanes` enum in @rightkit/
|
|
1009
|
+
// Mirrors the `lanes` enum in @rightkit/ci manifest.schema.json.
|
|
746
1010
|
const safeLanes = new Set(["ci", "qualification", "package-smoke", "release-candidate", "publish-npm", "publish-package-managers", "regenerate"]);
|
|
747
1011
|
if (manifest?.schemaVersion !== 1
|
|
748
1012
|
|| manifest?.compile !== "github-actions-only"
|
|
@@ -772,7 +1036,7 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
772
1036
|
};
|
|
773
1037
|
const inside = (index, span) => span && index >= span.start && index < span.end;
|
|
774
1038
|
const requestedSecrets = [...source.matchAll(/\bsecrets\.([A-Z0-9_]+)/gi)].map((match) => ({ name: match[1], index: match.index }));
|
|
775
|
-
const allowedMacosSecrets = new Set(
|
|
1039
|
+
const allowedMacosSecrets = new Set(APPLE_RELEASE_SECRETS);
|
|
776
1040
|
const windowsSpan = jobSpan("windows-sign");
|
|
777
1041
|
const macosSpan = jobSpan("macos-sign");
|
|
778
1042
|
const publishSpan = jobSpan("publish");
|
|
@@ -799,10 +1063,10 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
799
1063
|
const protectedMacosSigning = workflow === "release-candidate.yml"
|
|
800
1064
|
&& manifest?.candidate?.signMacos === true
|
|
801
1065
|
&& (legacySigningGate("macos-sign").test(macosSource) || admittedSigningGate("macos-sign").test(macosSource));
|
|
802
|
-
// @rightkit/
|
|
1066
|
+
// @rightkit/ci `devArtifact.mac: "sign"` renders a `dev-mac-sign` job into
|
|
803
1067
|
// the managed ci.yml: push to the default branch only, release environment,
|
|
804
1068
|
// Developer ID certificate secrets only (no notarization keys).
|
|
805
|
-
// @rightkit/
|
|
1069
|
+
// @rightkit/ci `regenerate` lane: manual-only, opens a pull request from a
|
|
806
1070
|
// right-git/regenerate-<run> branch and never writes to the default branch.
|
|
807
1071
|
// `contents: write` and `pull-requests: write` are accepted only inside that
|
|
808
1072
|
// job, only when the manifest declares `regenerate`, and only when the
|
|
@@ -828,6 +1092,8 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
828
1092
|
const protectedDevMacSigning = workflow === "ci.yml"
|
|
829
1093
|
&& (manifest?.devArtifact?.mac === "sign" || manifest?.devArtifact?.mac === "notarize")
|
|
830
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.
|
|
831
1097
|
const releaseChain = workflow === "release-candidate.yml" ? manifest?.candidate?.releaseChain : undefined;
|
|
832
1098
|
const allowedReleaseSecrets = new Set(releaseChain?.publishSecrets ?? []);
|
|
833
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 }));
|
|
@@ -844,6 +1110,14 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
844
1110
|
const protectedReleasePublish = releaseChain
|
|
845
1111
|
&& (legacyReleasePublishGate || admittedReleasePublishGate)
|
|
846
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);
|
|
847
1121
|
if (/^\s*permissions:\s*write-all\s*$/mi.test(source)
|
|
848
1122
|
|| writePermissions.some(({ name, index }) => {
|
|
849
1123
|
if (protectedRegenerate && inside(index, regenerateSpan) && allowedRegeneratePermissions.has(name)) return false;
|
|
@@ -853,20 +1127,21 @@ async function validateHostedWorkflows(root, appName, workflows, policy) {
|
|
|
853
1127
|
})
|
|
854
1128
|
|| /pull_request_target\s*:/i.test(source)
|
|
855
1129
|
|| /\bsecrets\s*\[/i.test(source)
|
|
856
|
-
||
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
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("; ")}` : ""}`);
|
|
860
1134
|
}
|
|
861
1135
|
}
|
|
862
1136
|
}
|
|
863
1137
|
|
|
864
|
-
function buildAllowedVersions(published = {}, staged = {}, legacy = {}) {
|
|
1138
|
+
function buildAllowedVersions(published = {}, staged = {}, legacy = {}, registry = {}) {
|
|
865
1139
|
const allowed = new Map();
|
|
866
1140
|
for (const [name, value] of [
|
|
867
1141
|
...Object.entries(published),
|
|
868
1142
|
...Object.entries(staged),
|
|
869
1143
|
...Object.entries(legacy),
|
|
1144
|
+
...Object.entries(registry),
|
|
870
1145
|
]) {
|
|
871
1146
|
const versions = allowed.get(name) ?? new Set();
|
|
872
1147
|
for (const version of Array.isArray(value) ? value : [value]) versions.add(version);
|
|
@@ -905,24 +1180,37 @@ function windowsReceiptPath(root, phase) {
|
|
|
905
1180
|
return signingReceiptPath({ appRoot: root, target, platform: "win", phase, stateRoot: process.env.RIGHT_RELEASE_STATE_ROOT });
|
|
906
1181
|
}
|
|
907
1182
|
|
|
908
|
-
|
|
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);
|
|
909
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`);
|
|
910
|
-
// 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.
|
|
911
1197
|
let nested = [];
|
|
912
|
-
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
|
+
}
|
|
913
1202
|
if (nested.length) {
|
|
914
1203
|
const nestedReceipt = path.resolve(path.dirname(receipt), `macos-${phase}-nested.json`);
|
|
915
1204
|
if (existsSync(nestedReceipt)) unlinkSync(nestedReceipt);
|
|
916
1205
|
console.log(`right-release: signing ${nested.length} nested code item(s) inside-out`);
|
|
917
|
-
await run("node",
|
|
1206
|
+
await run("node", macSignInvocation(nested, nestedReceipt), root);
|
|
1207
|
+
verifyMacosSigningSettings(nestedReceipt, nested);
|
|
918
1208
|
}
|
|
919
|
-
const args = [
|
|
920
|
-
if (opts.dryRun) args.push("--dry-run");
|
|
921
|
-
args.push("--receipt", receipt, ...files);
|
|
1209
|
+
const args = macSignInvocation(entries, receipt, opts.dryRun ? ["--dry-run"] : []);
|
|
922
1210
|
if (!opts.dryRun && existsSync(receipt)) unlinkSync(receipt);
|
|
923
1211
|
console.log("right-release: signing macOS Developer ID files");
|
|
924
1212
|
await run("node", args, root);
|
|
925
|
-
if (!opts.dryRun) verifyMacosSigningReceipt(receipt, files);
|
|
1213
|
+
if (!opts.dryRun) { verifyMacosSigningReceipt(receipt, files); verifyMacosSigningSettings(receipt, entries); }
|
|
926
1214
|
}
|
|
927
1215
|
|
|
928
1216
|
async function notarizeMacos(archive, root) {
|
|
@@ -931,6 +1219,7 @@ async function notarizeMacos(archive, root) {
|
|
|
931
1219
|
if (opts.dryRun) args.push("--dry-run");
|
|
932
1220
|
args.push("--receipt", receipt);
|
|
933
1221
|
if (target.notarize.profile) args.push("--profile", target.notarize.profile);
|
|
1222
|
+
if (target.notarize.staple === false) args.push("--no-staple");
|
|
934
1223
|
// With appBeforePackage the app already carries its own stapled ticket inside the packaged DMG.
|
|
935
1224
|
if (target.notarize.app && !target.notarize.appBeforePackage) args.push("--app", resolveReleaseArtifactPath(root, target.notarize.app));
|
|
936
1225
|
args.push(archive);
|
|
@@ -973,6 +1262,15 @@ function verifyMacosSigningReceipt(receipt, files) {
|
|
|
973
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}`);
|
|
974
1263
|
}
|
|
975
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
|
+
|
|
976
1274
|
function verifyMacosNotarizationReceipt(receipt, archive) {
|
|
977
1275
|
try { validateNotarizationReceipt({ receiptPath: receipt, archive }); }
|
|
978
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
|
+
]);
|