@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
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Fails when a built or installed bundle carries a Node or Python runtime,
|
|
3
|
+
// interpreter libraries, or script sources (M19 owner gap 2; replaces the
|
|
4
|
+
// per-app scan-bundle.mjs copies in CutRight and GenRight).
|
|
5
|
+
// Usage: right-release bundle-scan [--allow-name <basename>]... <bundle-root>...
|
|
6
|
+
import { readdirSync, statSync } from "node:fs";
|
|
7
|
+
import { basename, extname, join } from "node:path";
|
|
8
|
+
import { fileURLToPath } from "node:url";
|
|
9
|
+
|
|
10
|
+
export const BANNED_NAMES = Object.freeze([
|
|
11
|
+
/^node(js)?(\.exe)?$/i, /^libnode/i, /^python[\d.]*w?(\.exe)?$/i, /^libpython/i,
|
|
12
|
+
/^python\d+\.dll$/i, /^pythonw?\d*\.zip$/i,
|
|
13
|
+
]);
|
|
14
|
+
export const BANNED_EXTENSIONS = Object.freeze(new Set([".js", ".mjs", ".cjs", ".py", ".pyc", ".pyo", ".pyd", ".pyz"]));
|
|
15
|
+
export const BANNED_DIRECTORIES = Object.freeze(new Set(["node_modules", "site-packages", "__pycache__"]));
|
|
16
|
+
|
|
17
|
+
/** Scan one bundle root. `allowNames` exempts exact basenames (product-declared exceptions). */
|
|
18
|
+
export function scanRuntimeBundle(root, { allowNames = [] } = {}) {
|
|
19
|
+
const allowed = new Set(allowNames);
|
|
20
|
+
const offenders = [];
|
|
21
|
+
let files = 0;
|
|
22
|
+
const walk = (dir) => {
|
|
23
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
24
|
+
const path = join(dir, entry.name);
|
|
25
|
+
if (entry.isSymbolicLink()) continue;
|
|
26
|
+
if (entry.isDirectory()) {
|
|
27
|
+
if (BANNED_DIRECTORIES.has(entry.name) && !allowed.has(entry.name)) offenders.push(path);
|
|
28
|
+
walk(path);
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
files += 1;
|
|
32
|
+
const name = basename(path);
|
|
33
|
+
if (allowed.has(name)) continue;
|
|
34
|
+
if (BANNED_EXTENSIONS.has(extname(name).toLowerCase()) || BANNED_NAMES.some((re) => re.test(name))) offenders.push(path);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
if (statSync(root).isDirectory()) walk(root); else files = 1;
|
|
38
|
+
return { root, files, offenders };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function runtimeBundleScanFailed(results) {
|
|
42
|
+
return results.some((r) => r.offenders.length > 0 || r.files === 0);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function main(argv) {
|
|
46
|
+
const roots = [];
|
|
47
|
+
const allowNames = [];
|
|
48
|
+
for (let i = 0; i < argv.length; i++) {
|
|
49
|
+
if (argv[i] === "--allow-name") allowNames.push(argv[++i] ?? "");
|
|
50
|
+
else roots.push(argv[i]);
|
|
51
|
+
}
|
|
52
|
+
if (!roots.length || allowNames.includes("")) {
|
|
53
|
+
console.error("usage: right-release bundle-scan [--allow-name <basename>]... <bundle-root>...");
|
|
54
|
+
return 2;
|
|
55
|
+
}
|
|
56
|
+
const results = roots.map((root) => scanRuntimeBundle(root, { allowNames }));
|
|
57
|
+
console.log(JSON.stringify(results, null, 2));
|
|
58
|
+
return runtimeBundleScanFailed(results) ? 1 : 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]) process.exit(main(process.argv.slice(2)));
|
package/sign-macos.mjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { readSignManifest, signMacosEntries } from "./mac-sign-core.mjs";
|
|
6
6
|
import { MACOS_PORTABLE_SIGNING_CONTRACT } from "./macos-signing-contract.mjs";
|
|
7
7
|
import { resolveMacosDeveloperIdIdentity } from "./macos-signing-identity.mjs";
|
|
8
8
|
import { runMacCodesignCommands } from "./macos-signing-session.mjs";
|
|
@@ -13,44 +13,46 @@ const receiptIndex = args.indexOf("--receipt");
|
|
|
13
13
|
const receiptPath = receiptIndex >= 0 ? args[receiptIndex + 1] : null;
|
|
14
14
|
const entitlementsIndex = args.indexOf("--entitlements");
|
|
15
15
|
const entitlementsPath = entitlementsIndex >= 0 ? args[entitlementsIndex + 1] : null;
|
|
16
|
+
const manifestIndex = args.indexOf("--manifest");
|
|
17
|
+
const manifestPath = manifestIndex >= 0 ? args[manifestIndex + 1] : null;
|
|
16
18
|
if (entitlementsIndex >= 0 && (!entitlementsPath || entitlementsPath.startsWith("--"))) fail("--entitlements requires a path");
|
|
19
|
+
if (manifestIndex >= 0 && (!manifestPath || manifestPath.startsWith("--"))) fail("--manifest requires a path");
|
|
17
20
|
const files = args
|
|
18
|
-
.filter((arg, index) => arg !== "--dry-run" && arg !== "--receipt" && arg !== "--entitlements"
|
|
21
|
+
.filter((arg, index) => arg !== "--dry-run" && arg !== "--receipt" && arg !== "--entitlements" && arg !== "--manifest"
|
|
19
22
|
&& (receiptIndex < 0 || index !== receiptIndex + 1)
|
|
20
|
-
&& (entitlementsIndex < 0 || index !== entitlementsIndex + 1)
|
|
23
|
+
&& (entitlementsIndex < 0 || index !== entitlementsIndex + 1)
|
|
24
|
+
&& (manifestIndex < 0 || index !== manifestIndex + 1))
|
|
21
25
|
.map((arg) => path.resolve(arg));
|
|
22
26
|
|
|
23
|
-
|
|
27
|
+
// A manifest carries per-file settings (entitlements, codesign options) in signing order; the plain form
|
|
28
|
+
// signs every positional file with the same optional --entitlements.
|
|
29
|
+
if (manifestPath && (files.length || entitlementsPath)) fail("--manifest cannot be combined with positional files or --entitlements");
|
|
30
|
+
if (!manifestPath && !files.length) fail("usage: node sign-macos.mjs [--dry-run] [--entitlements <path>] --receipt <path> <file>... | --manifest <json>");
|
|
24
31
|
if (!receiptPath) fail("--receipt requires a path");
|
|
32
|
+
let entries;
|
|
33
|
+
if (manifestPath) {
|
|
34
|
+
try { entries = readSignManifest(path.resolve(manifestPath)); } catch (error) { fail(error.message); }
|
|
35
|
+
} else {
|
|
36
|
+
entries = files.map((file) => ({ file, ...(entitlementsPath ? { entitlements: path.resolve(entitlementsPath) } : {}), options: [] }));
|
|
37
|
+
}
|
|
25
38
|
if (dryRun) {
|
|
26
|
-
console.log(`dry-run: Developer ID signing ${
|
|
39
|
+
console.log(`dry-run: Developer ID signing ${entries.map((entry) => entry.file).join(", ")}`);
|
|
27
40
|
process.exit(0);
|
|
28
41
|
}
|
|
29
42
|
if (process.platform !== "darwin") fail("macOS signing must run on macOS");
|
|
30
43
|
let identity;
|
|
31
44
|
try { identity = resolveMacosDeveloperIdIdentity(); } catch (error) { fail(error.message); }
|
|
32
|
-
for (const
|
|
33
|
-
if (
|
|
45
|
+
for (const entry of entries) if (!existsSync(entry.file)) fail(`missing file: ${entry.file}`);
|
|
46
|
+
for (const entry of entries) if (entry.entitlements && !existsSync(entry.entitlements)) fail(`missing entitlements: ${entry.entitlements}`);
|
|
34
47
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const verification = run("codesign", ["--verify", "--strict", "--verbose=4", file]);
|
|
44
|
-
const details = run("codesign", ["-dvv", file]);
|
|
45
|
-
if (!/valid on disk/i.test(verification) || !/satisfies its designated requirement/i.test(verification)) {
|
|
46
|
-
fail(`strict codesign verification failed for ${file}`);
|
|
47
|
-
}
|
|
48
|
-
if (!new RegExp(`^Authority=${escapeRegExp(identity)}$`, "m").test(details) && !details.includes(`Authority=${identity}`)) {
|
|
49
|
-
fail(`codesign identity mismatch for ${file}: expected ${identity}`);
|
|
50
|
-
}
|
|
51
|
-
evidence.push({ file, before, after: fileEvidence(file), codesign: "strict-valid", identity, timestampPresent: /(?:Timestamp|Signed Time)[=:]/i.test(details) });
|
|
52
|
-
if (!evidence.at(-1).timestampPresent) fail(`Developer ID timestamp is missing for ${file}`);
|
|
53
|
-
}
|
|
48
|
+
let evidence;
|
|
49
|
+
try {
|
|
50
|
+
evidence = signMacosEntries(entries, {
|
|
51
|
+
identity,
|
|
52
|
+
signOne: (codesignArgs) => runMacCodesignCommands([codesignArgs])[0],
|
|
53
|
+
inspect: run,
|
|
54
|
+
});
|
|
55
|
+
} catch (error) { fail(`${error.code ? `[${error.code}] ` : ""}${error.message}`); }
|
|
54
56
|
|
|
55
57
|
const absoluteReceipt = path.resolve(receiptPath);
|
|
56
58
|
mkdirSync(path.dirname(absoluteReceipt), { recursive: true });
|
|
@@ -63,10 +65,6 @@ function run(cmd, runArgs) {
|
|
|
63
65
|
return output;
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
function escapeRegExp(value) {
|
|
67
|
-
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
68
|
-
}
|
|
69
|
-
|
|
70
68
|
function fail(message) {
|
|
71
69
|
console.error(`right-sign-macos: ${message}`);
|
|
72
70
|
process.exit(1);
|
package/sign-updater.mjs
CHANGED
|
@@ -8,8 +8,10 @@ import { refuseManagedEnvSecrets, MANAGED_HOST_ENV_SECRET_REFUSED } from "./mana
|
|
|
8
8
|
const args = process.argv.slice(2);
|
|
9
9
|
const files = [];
|
|
10
10
|
let dryRun = false;
|
|
11
|
+
let keyFile = process.env.RIGHT_UPDATER_KEY_FILE || "";
|
|
11
12
|
for (let i = 0; i < args.length; i++) {
|
|
12
13
|
if (args[i] === "--file") files.push(path.resolve(args[++i]));
|
|
14
|
+
else if (args[i] === "--key-file") keyFile = args[++i] ?? "";
|
|
13
15
|
else if (args[i] === "--dry-run") dryRun = true;
|
|
14
16
|
else fail(`unknown argument: ${args[i]}`);
|
|
15
17
|
}
|
|
@@ -22,8 +24,9 @@ if (!dryRun) {
|
|
|
22
24
|
}
|
|
23
25
|
if (!dryRun && !env.TAURI_SIGNING_PRIVATE_KEY) {
|
|
24
26
|
if (process.platform === "win32") {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
if (!keyFile) fail("updater key path required on win32: pass --key-file or set RIGHT_UPDATER_KEY_FILE (release config: targets.win.updater.keyFile), or set TAURI_SIGNING_PRIVATE_KEY");
|
|
28
|
+
const key = path.resolve(keyFile.replace(/%([^%]+)%/g, (m, name) => process.env[name] ?? m));
|
|
29
|
+
if (!existsSync(key)) fail(`updater key not found: ${key}`);
|
|
27
30
|
env.TAURI_SIGNING_PRIVATE_KEY = readFileSync(key, "utf8");
|
|
28
31
|
} else if (process.platform !== "darwin") {
|
|
29
32
|
fail("TAURI_SIGNING_PRIVATE_KEY is required on this platform");
|
package/swift-mac-dmg.mjs
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawnSync } from "./background-process.mjs";
|
|
6
6
|
import { buildDmg } from "./dmg-build.mjs";
|
|
7
|
+
import { readDmgSpec } from "./dmg-spec.mjs";
|
|
7
8
|
|
|
8
9
|
const fail = (code, message) => Object.assign(new Error(message), { code });
|
|
9
10
|
const DMG_HOOK = /dmg|hdiutil/i;
|
|
@@ -30,7 +31,9 @@ export function swiftMacDmgPlan({ target, projectType, appName = "app", resolve
|
|
|
30
31
|
return {
|
|
31
32
|
app: resolve(target.notarize.app),
|
|
32
33
|
out: resolve(d.out ?? target.notarize.file),
|
|
33
|
-
|
|
34
|
+
// `spec` is an appdmg-style JSON file (dmg-spec.mjs); explicit options here win over it, and its title is the volume name.
|
|
35
|
+
fallbackVolname: appName,
|
|
36
|
+
options: { volname: d.volname ?? (d.spec ? undefined : appName), spec: d.spec && resolve(d.spec), background: d.background && resolve(d.background), iconSize: d.iconSize, appPosition: d.appPosition,
|
|
34
37
|
applicationsPosition: d.applicationsPosition, window: d.window, volumeIcon: d.volumeIcon && resolve(d.volumeIcon) },
|
|
35
38
|
};
|
|
36
39
|
}
|
|
@@ -46,7 +49,9 @@ export async function buildStapledSwiftMacDmg(plan, { run = spawnSync, build = b
|
|
|
46
49
|
if (probe.error || probe.status !== 0) throw fail("SWIFT_DMG_APP_NOT_STAPLED", `refusing to build the DMG: ${plan.app} is not stapled (xcrun stapler validate failed). Notarize and staple the app before packaging.`);
|
|
47
50
|
remove(plan.out);
|
|
48
51
|
log(`right-release: building macOS DMG from stapled app ${plan.app}`);
|
|
49
|
-
|
|
52
|
+
let options = definedOnly(plan.options);
|
|
53
|
+
if (options.spec && !options.volname) options = { ...options, volname: readDmgSpec(options.spec, { app: plan.app }).volname ?? plan.fallbackVolname };
|
|
54
|
+
const built = build({ app: plan.app, out: plan.out, ...options });
|
|
50
55
|
if (signDmg) await signDmg(plan.out);
|
|
51
56
|
return built;
|
|
52
57
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// right-release swift-package <windows-stage|msix|mac-app
|
|
2
|
+
// right-release swift-package <windows-stage|msix|mac-app> --config <swift-package.json> [options]
|
|
3
3
|
//
|
|
4
4
|
// One product config (schema 1) drives all three steps; paths resolve against the config file.
|
|
5
5
|
// Build-only: nothing here uploads or publishes.
|
package/upload-release.mjs
CHANGED
|
@@ -2,13 +2,8 @@
|
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
|
-
import { spawnSync } from "./background-process.mjs";
|
|
6
|
-
import { pathToFileURL } from "node:url";
|
|
7
5
|
import { prepareGitHubRelease, publishGitHubRelease } from "./github-release.mjs";
|
|
8
|
-
import {
|
|
9
|
-
import { verifySealedRelease } from "./release-state.mjs";
|
|
10
|
-
import { assertCleanSource } from "./source-gate.mjs";
|
|
11
|
-
import { verifyOwnershipEvidence } from "./ownership-evidence.mjs";
|
|
6
|
+
import { authorizeGitHubUpload } from "./github-upload-gate.mjs";
|
|
12
7
|
import { recordPublishedVersion } from "./artifact-storage.mjs";
|
|
13
8
|
import { admitInstalledQa } from "./release-qa-audit.mjs";
|
|
14
9
|
|
|
@@ -32,33 +27,17 @@ for (let index = 0; index < args.length; index += 1) {
|
|
|
32
27
|
else if (arg === "-h" || arg === "--help") usage(0);
|
|
33
28
|
else fail(`unknown argument: ${arg}`);
|
|
34
29
|
}
|
|
35
|
-
if (tier !== "patch" && tier !== "update") fail("tier is required (patch|update)");
|
|
36
|
-
if (!releaseId) fail("--release is required");
|
|
37
|
-
if (platform !== "win" && platform !== "mac") fail("--platform must be win or mac");
|
|
38
30
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
const platformDir = platform === "win" ? "windows" : "mac";
|
|
47
|
-
const sealed = verifySealedRelease(path.join(repoRoot, ".right-release", "sealed", releaseId, platformDir));
|
|
48
|
-
if (sealed.manifest.commit !== candidateCommit) fail("sealed release was built from a different commit than the current checkout");
|
|
49
|
-
|
|
50
|
-
const configPath = path.resolve(configName);
|
|
51
|
-
if (!existsSync(configPath)) fail(`release config is required: ${configName}`);
|
|
52
|
-
const releaseConfig = (await import(`${pathToFileURL(configPath).href}?upload=${Date.now()}`)).default;
|
|
53
|
-
const ownership = verifyOwnershipEvidence(sealed.manifest.ownershipEvidence, repoRoot, path.dirname(configPath));
|
|
54
|
-
if (!ownership.ok) fail(`sealed ownership evidence rejected: ${ownership.reason}`);
|
|
55
|
-
if (releaseConfig?.distribution?.provider !== "github-releases") {
|
|
56
|
-
fail("release config distribution.provider must be github-releases");
|
|
31
|
+
// Tier, primary checkout, clean tree, sealed commit, ownership evidence, provider and repository are checked by
|
|
32
|
+
// the gate that `right-release github --release` also uses; nothing below runs until it passes.
|
|
33
|
+
let authority;
|
|
34
|
+
try {
|
|
35
|
+
authority = await authorizeGitHubUpload({ command: "right-release upload", tier, releaseId, platform, configName, repo });
|
|
36
|
+
} catch (error) {
|
|
37
|
+
fail(error.message);
|
|
57
38
|
}
|
|
58
|
-
const
|
|
59
|
-
|
|
60
|
-
if (repo && repo !== configuredRepo) fail(`--repo must match configured distribution.repository (${configuredRepo})`);
|
|
61
|
-
repo = configuredRepo;
|
|
39
|
+
const { repoRoot, platformDir, sealed, configPath } = authority;
|
|
40
|
+
repo = authority.repo;
|
|
62
41
|
|
|
63
42
|
// Hardening step 4: installed QA is recorded, not enforced; a missing receipt warns and the outcome is written to state.
|
|
64
43
|
if (!dryRun) {
|
|
@@ -87,12 +66,6 @@ if (!dryRun) writeJson(verifiedMarker, {
|
|
|
87
66
|
if (!dryRun) recordPublishedVersion({ app: sealed.manifest.app, version: sealed.manifest.version, platform });
|
|
88
67
|
console.log(`right-release upload: ${dryRun ? "dry-run " : ""}${result.status} ${releaseId} tier=${tier} repo=${repo}`);
|
|
89
68
|
|
|
90
|
-
function git(cwd, runArgs) {
|
|
91
|
-
const result = spawnSync("git", runArgs, { cwd, encoding: "utf8", windowsHide: true });
|
|
92
|
-
if (result.status !== 0) fail(`git ${runArgs.join(" ")} failed: ${result.stderr}`);
|
|
93
|
-
return result.stdout.trim();
|
|
94
|
-
}
|
|
95
|
-
|
|
96
69
|
function writeJson(file, value) {
|
|
97
70
|
mkdirSync(path.dirname(file), { recursive: true });
|
|
98
71
|
writeFileSync(file, `${JSON.stringify(value, null, 2)}\n`);
|