@rightkit/release 0.6.4 → 0.6.6
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/README.md +109 -7
- package/admission-files.mjs +279 -0
- package/admission-host.mjs +403 -0
- package/admission.mjs +146 -0
- package/android-play-upload.mjs +4 -4
- package/app-check.mjs +1326 -0
- package/apple-profile-create.mjs +4 -3
- package/apple-profile-renew.mjs +5 -4
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +495 -69
- package/binary-license-notices.mjs +2 -1
- package/build-release.mjs +87 -26
- package/cache-command.mjs +4 -3
- package/cargo-license-notices.mjs +2 -1
- package/cargo-manifest-text.mjs +25 -0
- package/cargo-profile-contract.mjs +26 -13
- package/cli/right-release.mjs +45 -5
- package/command-spec.mjs +4 -5
- package/deps.mjs +5 -4
- package/deterministic-scans.mjs +301 -37
- package/development.mjs +5 -5
- package/github-release.mjs +5 -3
- package/github-upload-gate.mjs +6 -6
- package/init-command.mjs +134 -8
- package/inventory-check.mjs +12 -1
- package/ios-upload.mjs +4 -4
- package/mirror-root-artifact.mjs +3 -4
- package/npm-license-notices.mjs +2 -1
- package/outdated-command.mjs +91 -0
- package/ownership-dependency-allowlist.json +2 -0
- package/package.json +41 -3
- package/pipeline-admission.mjs +12 -0
- package/prebuild-gates.mjs +7 -3
- package/publish-cargo.mjs +215 -3
- package/publish-update.mjs +5 -3
- package/release-config.mjs +36 -5
- package/release-dependencies-command.mjs +4 -3
- package/release-qa-audit.mjs +12 -10
- package/release-qa-command.mjs +42 -7
- package/release-qa-hardening.mjs +10 -3
- package/release-qa-policy.mjs +214 -47
- package/release.mjs +117 -62
- package/rightkit-registry-resolver.mjs +5 -2
- package/rightkit-versions.json +18 -13
- package/stage-sidecar-command.mjs +5 -5
- package/test-command.mjs +3 -2
- package/upload-release.mjs +3 -2
- package/web-build.mjs +2 -1
package/init-command.mjs
CHANGED
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// (bogusyogi/rightkit#385): renames the desktop app template into a real app, in place. Ports PostRight's
|
|
4
4
|
// crates/tools/src/init.rs. The template identity (`RightApp`, `rightapp`, `RIGHTAPP`, `app.rightsuite.rightapp`)
|
|
5
5
|
// is rewritten in text files; nothing else is touched and no gate or build is run.
|
|
6
|
-
import {
|
|
6
|
+
import { createHash } from "node:crypto";
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
7
8
|
import path from "node:path";
|
|
8
9
|
import { fileURLToPath } from "node:url";
|
|
9
10
|
|
|
@@ -50,6 +51,60 @@ export function resolveIdentity({ name, id, slug }) {
|
|
|
50
51
|
return { name, id, slug: resolvedSlug, warnings };
|
|
51
52
|
}
|
|
52
53
|
|
|
54
|
+
// Cargo writes Cargo.lock with its [[package]] tables sorted by name (then version) and each `dependencies` list sorted by name, and
|
|
55
|
+
// `cargo --locked` refuses a lock that differs from what it would write. Renaming the template's own crates moves them in that order,
|
|
56
|
+
// so the lock is re-sorted after the text rename; without it every `--locked` script fails on the first build of the new app.
|
|
57
|
+
const LOCK_FILE = "Cargo.lock";
|
|
58
|
+
const byteOrder = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
59
|
+
const semverParts = (version) => {
|
|
60
|
+
const [core, pre] = String(version ?? "").split("+")[0].split(/-(.*)/s);
|
|
61
|
+
return { nums: core.split(".").map((part) => Number.parseInt(part, 10) || 0), pre: pre ?? null };
|
|
62
|
+
};
|
|
63
|
+
function compareSemver(a, b) {
|
|
64
|
+
const x = semverParts(a);
|
|
65
|
+
const y = semverParts(b);
|
|
66
|
+
for (let i = 0; i < 3; i += 1) if (x.nums[i] !== y.nums[i]) return (x.nums[i] ?? 0) - (y.nums[i] ?? 0);
|
|
67
|
+
if (x.pre === y.pre) return 0;
|
|
68
|
+
if (x.pre === null) return 1;
|
|
69
|
+
if (y.pre === null) return -1;
|
|
70
|
+
return byteOrder(x.pre, y.pre);
|
|
71
|
+
}
|
|
72
|
+
const lockField = (block, key) => new RegExp(`^${key} = "([^"]*)"$`, "m").exec(block)?.[1] ?? "";
|
|
73
|
+
/** `"name"`, `"name version"` or `"name version (source)"`: the name and version of one `dependencies` entry. */
|
|
74
|
+
const lockDependency = (line) => {
|
|
75
|
+
const [name = "", version = ""] = line.trim().replace(/^"|",?$/g, "").split(" ");
|
|
76
|
+
return { name, version };
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
/** Sort one lock's [[package]] tables and their `dependencies` lists the way cargo writes them; everything else is kept byte for byte. */
|
|
80
|
+
export function sortCargoLock(text) {
|
|
81
|
+
const crlf = text.includes("\r\n");
|
|
82
|
+
const lf = crlf ? text.replace(/\r\n/g, "\n") : text;
|
|
83
|
+
const first = /^\[\[package\]\]$/m.exec(lf);
|
|
84
|
+
if (!first) return text;
|
|
85
|
+
const head = lf.slice(0, first.index);
|
|
86
|
+
let body = lf.slice(first.index);
|
|
87
|
+
let tail = "";
|
|
88
|
+
const other = /^\[(?!\[package\]\]$)/m.exec(body);
|
|
89
|
+
if (other) {
|
|
90
|
+
tail = body.slice(other.index);
|
|
91
|
+
body = body.slice(0, other.index);
|
|
92
|
+
}
|
|
93
|
+
const blocks = body.replace(/\n+$/, "").split(/\n\n(?=\[\[package\]\]$)/m).map((block) => block.replace(
|
|
94
|
+
/^dependencies = \[\n([\s\S]*?)\n\]$/m,
|
|
95
|
+
(_, list) => `dependencies = [\n${list.split("\n").map((line) => ({ line, ...lockDependency(line) }))
|
|
96
|
+
.sort((a, b) => byteOrder(a.name, b.name) || compareSemver(a.version, b.version)).map((entry) => entry.line).join("\n")}\n]`,
|
|
97
|
+
));
|
|
98
|
+
blocks.sort((a, b) => byteOrder(lockField(a, "name"), lockField(b, "name")) || compareSemver(lockField(a, "version"), lockField(b, "version")));
|
|
99
|
+
const out = `${head}${blocks.join("\n\n")}\n${tail ? `\n${tail}` : ""}`;
|
|
100
|
+
return crlf ? out.replace(/\n/g, "\r\n") : out;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Rename the template identity in a Cargo.lock and restore cargo's order, so `cargo --locked` accepts the renamed lock. */
|
|
104
|
+
export function reidentifyCargoLock(text, identity) {
|
|
105
|
+
return sortCargoLock(renameText(text, identity));
|
|
106
|
+
}
|
|
107
|
+
|
|
53
108
|
export function walkTextFiles(dir, out = []) {
|
|
54
109
|
let entries;
|
|
55
110
|
try {
|
|
@@ -61,26 +116,66 @@ export function walkTextFiles(dir, out = []) {
|
|
|
61
116
|
const full = path.join(dir, entry.name);
|
|
62
117
|
if (entry.isDirectory()) {
|
|
63
118
|
if (!SKIP_DIRS.has(entry.name)) walkTextFiles(full, out);
|
|
64
|
-
} else if (entry.isFile() && TEXT_EXT.has(path.extname(entry.name).slice(1))) {
|
|
119
|
+
} else if (entry.isFile() && (TEXT_EXT.has(path.extname(entry.name).slice(1)) || entry.name === LOCK_FILE)) {
|
|
65
120
|
out.push(full);
|
|
66
121
|
}
|
|
67
122
|
}
|
|
68
123
|
return out;
|
|
69
124
|
}
|
|
70
125
|
|
|
126
|
+
// `rightapps` is not the template identity: it is the RightApps distribution service (the `r2-rightapps` provider), which every app
|
|
127
|
+
// keeps. A spelling followed by a whole-word `s` is therefore left alone; `rightapp-engine`, `rightapp_protocol`, `RIGHTAPP_HOME` and
|
|
128
|
+
// the rest of the identity still rename.
|
|
129
|
+
const spelling = (word) => new RegExp(`${word}(?)`, "g");
|
|
130
|
+
const UPPER_WORD = spelling(UPPER);
|
|
131
|
+
const TITLE_WORD = spelling(TITLE);
|
|
132
|
+
const LOWER_WORD = spelling(LOWER);
|
|
133
|
+
|
|
71
134
|
/** Rewrite the template identity in one string; the bundle id first, then the three spellings. */
|
|
72
135
|
export function renameText(text, { name, id, slug }) {
|
|
73
136
|
return text
|
|
74
137
|
.replaceAll(TEMPLATE_ID, id)
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
.
|
|
138
|
+
.replace(UPPER_WORD, () => slug.toUpperCase())
|
|
139
|
+
.replace(TITLE_WORD, () => name)
|
|
140
|
+
.replace(LOWER_WORD, () => slug);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export const LEGAL_MANIFEST = "legal/legal-manifest.json";
|
|
144
|
+
const sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Recompute the `sha256` of every document and appendix of a legal manifest from the bytes `read(relativePath)` returns (paths are
|
|
148
|
+
* relative to the manifest's directory; `read` returns Buffer, string or null for a missing file). Returns `{ text, changed, stale,
|
|
149
|
+
* material }`: the manifest text with the new digests (2-space JSON, as `legal-snapshot` writes it), the entries whose digest moved,
|
|
150
|
+
* and which of those are material agreements. Material documents feed the acceptance digest in src/generated/legal-contract.json, which
|
|
151
|
+
* only `legal-snapshot` can recompute, so the caller warns about them.
|
|
152
|
+
*/
|
|
153
|
+
export function rehashLegalManifest(text, read) {
|
|
154
|
+
const manifest = JSON.parse(text);
|
|
155
|
+
const changed = [];
|
|
156
|
+
const material = [];
|
|
157
|
+
for (const entry of [...(manifest.documents ?? []), ...(manifest.appendices ?? [])]) {
|
|
158
|
+
const bytes = typeof entry?.path === "string" ? read(entry.path) : null;
|
|
159
|
+
if (bytes === null || bytes === undefined) continue;
|
|
160
|
+
const digest = sha256Hex(typeof bytes === "string" ? Buffer.from(bytes) : bytes);
|
|
161
|
+
if (entry.sha256 === digest) continue;
|
|
162
|
+
entry.sha256 = digest;
|
|
163
|
+
changed.push(entry.path);
|
|
164
|
+
if (entry.material === true) material.push(entry.path);
|
|
165
|
+
}
|
|
166
|
+
return { text: changed.length ? `${JSON.stringify(manifest, null, 2)}\n` : text, changed, material };
|
|
78
167
|
}
|
|
79
168
|
|
|
80
|
-
/**
|
|
169
|
+
/**
|
|
170
|
+
* Rename the template under `root`. Returns the identity, the changed file paths (relative, sorted) and `warnings`.
|
|
171
|
+
* The rename changes the bytes of text the legal manifest hashes (the privacy notice and the npm appendix carry the product name), so the
|
|
172
|
+
* manifest's digests are recomputed from the renamed bytes in the same pass.
|
|
173
|
+
*/
|
|
81
174
|
export function initTemplate(root, options) {
|
|
82
175
|
const identity = resolveIdentity(options);
|
|
83
176
|
const changed = [];
|
|
177
|
+
const warnings = [...identity.warnings];
|
|
178
|
+
const renamed = new Map();
|
|
84
179
|
for (const file of walkTextFiles(root)) {
|
|
85
180
|
if (path.basename(file) === SELF) continue;
|
|
86
181
|
let before;
|
|
@@ -89,20 +184,51 @@ export function initTemplate(root, options) {
|
|
|
89
184
|
} catch {
|
|
90
185
|
continue;
|
|
91
186
|
}
|
|
92
|
-
const after = renameText(before, identity);
|
|
187
|
+
const after = path.basename(file) === LOCK_FILE ? reidentifyCargoLock(before, identity) : renameText(before, identity);
|
|
93
188
|
if (after === before) continue;
|
|
189
|
+
renamed.set(path.resolve(file), after);
|
|
94
190
|
if (!options.dryRun) writeFileSync(file, after);
|
|
95
191
|
changed.push(path.relative(root, file).split(path.sep).join("/"));
|
|
96
192
|
}
|
|
97
|
-
|
|
193
|
+
const manifestFile = path.join(root, ...LEGAL_MANIFEST.split("/"));
|
|
194
|
+
if (existsSync(manifestFile)) {
|
|
195
|
+
const legalDir = path.dirname(manifestFile);
|
|
196
|
+
const read = (relative) => {
|
|
197
|
+
const target = path.resolve(legalDir, relative);
|
|
198
|
+
if (renamed.has(target)) return renamed.get(target);
|
|
199
|
+
try { return readFileSync(target); } catch { return null; }
|
|
200
|
+
};
|
|
201
|
+
const rehashed = rehashLegalManifest(renamed.get(path.resolve(manifestFile)) ?? readFileSync(manifestFile, "utf8"), read);
|
|
202
|
+
if (rehashed.changed.length) {
|
|
203
|
+
if (!options.dryRun) writeFileSync(manifestFile, rehashed.text);
|
|
204
|
+
if (!changed.includes(LEGAL_MANIFEST)) changed.push(LEGAL_MANIFEST);
|
|
205
|
+
}
|
|
206
|
+
for (const material of rehashed.material) {
|
|
207
|
+
warnings.push(`the rename changed the material legal document legal/${material}, which feeds src/generated/legal-contract.json; run \`pnpm exec legal-snapshot\` to regenerate the acceptance contract`);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
return { ...identity, changed: changed.sort(), warnings };
|
|
98
211
|
}
|
|
99
212
|
|
|
213
|
+
/** What a person does after init: the steps no command can do for them, in order. */
|
|
214
|
+
export const NEXT_STEPS = Object.freeze([
|
|
215
|
+
"pnpm install (installs the pinned packages and, through `prepare`, the pre-push hook)",
|
|
216
|
+
"right-release doctor (the build's own checks for this app, as rows)",
|
|
217
|
+
"set the app's public legal host in legal-snapshot.config.json, replace legal/PRODUCT_SCHEDULE.md and legal/PRIVACY_NOTICE.md with reviewed text, then `pnpm exec legal-snapshot`",
|
|
218
|
+
"register the repository on each host: add it to packages/release/ownership-repositories.json in RightKit (and its byte copy under packages/build-control-policy), then run `right-release ownership trust-refresh` on the Mac and the Dell (a person with administrator rights; an agent cannot)",
|
|
219
|
+
"README.md in the template: \"Create a new app (checklist)\"",
|
|
220
|
+
]);
|
|
221
|
+
|
|
100
222
|
export function runInitCommand(argv, io = { stdout: console.log, stderr: console.error }) {
|
|
101
223
|
const opts = parseInitArgs(argv);
|
|
102
224
|
const result = initTemplate(path.resolve(opts.dir), opts);
|
|
103
225
|
for (const warning of result.warnings) io.stderr(`warning: ${warning}`);
|
|
104
226
|
io.stdout(`right-release init${opts.dryRun ? " --dry-run" : ""}: ${result.changed.length} file(s) ${opts.dryRun ? "would change" : "changed"}`);
|
|
105
227
|
for (const file of result.changed) io.stdout(` ${file}`);
|
|
228
|
+
if (!opts.dryRun) {
|
|
229
|
+
io.stdout("next:");
|
|
230
|
+
for (const step of NEXT_STEPS) io.stdout(` ${step}`);
|
|
231
|
+
}
|
|
106
232
|
return result;
|
|
107
233
|
}
|
|
108
234
|
|
package/inventory-check.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
generateAdoption,
|
|
17
17
|
generateLocal,
|
|
18
18
|
} from "../../scripts/generate-index.mjs";
|
|
19
|
+
import { PORT_MATRIX_OUTPUTS, generatePortMatrix } from "../../scripts/generate-port-matrix.mjs";
|
|
19
20
|
|
|
20
21
|
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
22
|
export const DEFAULT_REPO_ROOT = resolve(HERE, "..", "..");
|
|
@@ -781,6 +782,16 @@ function checkGenerated({ repoRoot, appsRoot, apps, findings }) {
|
|
|
781
782
|
for (const rel of driftOf(repoRoot, adoption)) {
|
|
782
783
|
findings.push(issue("generated", rel, "generated-doc-drift", "committed adoption report differs from the app repos; run generate-index.mjs"));
|
|
783
784
|
}
|
|
785
|
+
// The port matrix reads the app repos like the adoption report. Its stamp, app directories and HEADs, and per-app check counts
|
|
786
|
+
// are carried over from the committed docs/port-matrix.json, so only a change in the data itself reads as drift.
|
|
787
|
+
try {
|
|
788
|
+
const matrix = generatePortMatrix({ root: repoRoot, workspace: appsRoot, carry: true });
|
|
789
|
+
for (const rel of driftOf(repoRoot, matrix.outputs)) {
|
|
790
|
+
findings.push(issue("generated", rel, "generated-doc-drift", "committed port matrix differs from the app repos; run generate-index.mjs"));
|
|
791
|
+
}
|
|
792
|
+
} catch (error) {
|
|
793
|
+
findings.push(issue("generated", PORT_MATRIX_OUTPUTS[2], "generated-doc-drift", `the port matrix could not be regenerated: ${error.message}`));
|
|
794
|
+
}
|
|
784
795
|
return { local, adoption: Object.values(adoption)[0] };
|
|
785
796
|
}
|
|
786
797
|
|
|
@@ -842,7 +853,7 @@ export async function runInventoryCheck(options = {}) {
|
|
|
842
853
|
account: { crates: account.crates, npm: account.npm },
|
|
843
854
|
thirdParty: { declarations: thirdParty.declarations, uniquePackages: thirdParty.uniquePackages, lockDuplicates: thirdParty.lockDuplicates },
|
|
844
855
|
apps: { rows: appRows },
|
|
845
|
-
generated: { checked: ["INDEX.md", "docs/CAPABILITY-MAP.md", "docs/ADOPTION.md"] },
|
|
856
|
+
generated: { checked: ["INDEX.md", "docs/CAPABILITY-MAP.md", "docs/ADOPTION.md", ...PORT_MATRIX_OUTPUTS] },
|
|
846
857
|
hygiene: { checked: records.length },
|
|
847
858
|
},
|
|
848
859
|
};
|
package/ios-upload.mjs
CHANGED
|
@@ -21,8 +21,8 @@
|
|
|
21
21
|
// TestFlight steps use the same in-memory ES256 JWT. No file, temp or not, ever holds the key; no altool, no Transporter.
|
|
22
22
|
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
|
|
23
23
|
import path from "node:path";
|
|
24
|
-
import { pathToFileURL } from "node:url";
|
|
25
24
|
import { spawnSync } from "./background-process.mjs";
|
|
25
|
+
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
26
26
|
import { assertGuiSigningSession, assertIosHost, IosHostError } from "./ios-host.mjs";
|
|
27
27
|
import { acquireFileLock } from "./ios-lock.mjs";
|
|
28
28
|
import {
|
|
@@ -136,7 +136,7 @@ const git = (cwd, args) => {
|
|
|
136
136
|
};
|
|
137
137
|
|
|
138
138
|
/** Local checks only; no network, no secret. The sealed release is the only input: build-free by construction. */
|
|
139
|
-
export async function authorizeIosUpload({ releaseId,
|
|
139
|
+
export async function authorizeIosUpload({ releaseId, cwd = process.cwd(), configName = defaultReleaseConfigFile(cwd) }) {
|
|
140
140
|
if (!RELEASE_ID.test(releaseId ?? "")) refuse("IOS_UPLOAD_RELEASE_REQUIRED", "--release <sealed-id> is required");
|
|
141
141
|
const repoRoot = git(cwd, ["rev-parse", "--show-toplevel"]);
|
|
142
142
|
assertPrimaryReleaseCheckout(repoRoot);
|
|
@@ -144,8 +144,8 @@ export async function authorizeIosUpload({ releaseId, configName = "right-releas
|
|
|
144
144
|
assertCleanSource({ status: git(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]), commandId: "right-release upload" });
|
|
145
145
|
const configPath = path.resolve(cwd, configName);
|
|
146
146
|
if (!existsSync(configPath)) refuse("IOS_UPLOAD_CONFIG_INVALID", `release config is required: ${configName}`);
|
|
147
|
-
//
|
|
148
|
-
const config =
|
|
147
|
+
// A legacy config is a module that runs on import (a JSON config is data): loaded only after the tree is proven clean.
|
|
148
|
+
const config = await importReleaseConfig(configPath, { query: `upload=${Date.now()}` });
|
|
149
149
|
const distribution = validateIosDistribution(targetDistribution(config, "ios"));
|
|
150
150
|
const sealedDir = path.join(repoRoot, ".right-release", "sealed", releaseId, "ios");
|
|
151
151
|
const appRoot = path.dirname(configPath);
|
package/mirror-root-artifact.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// Two forms:
|
|
3
3
|
// right-release mirror-root-artifact --file <artifact> --package-root <dir> [--name <filename>]
|
|
4
|
-
// right-release mirror-root-artifact --config <right-release.config.
|
|
4
|
+
// right-release mirror-root-artifact --config <right-release.config.json> --platform mac|win [--package-root <dir>]
|
|
5
5
|
// The config form mirrors every entry of `targets.<platform>.mirror`, a list of `"<app-relative file>"` or
|
|
6
6
|
// `{ file, name?, pruneSuffix? }`. `pruneSuffix` (for example "-setup.exe") then removes the other files with that suffix
|
|
7
7
|
// from the managed directory, so it holds exactly the current artifact. Run it as `targets.<platform>.postSign`.
|
|
@@ -10,7 +10,7 @@ import { configuredManagedRootSpellings } from "./artifact-storage.mjs";
|
|
|
10
10
|
import { copyFileSync, lstatSync, mkdirSync, readdirSync, realpathSync, renameSync, rmSync, statSync } from "node:fs";
|
|
11
11
|
import { execFileSync } from "./background-process.mjs";
|
|
12
12
|
import path from "node:path";
|
|
13
|
-
import {
|
|
13
|
+
import { importReleaseConfig } from "./release-config.mjs";
|
|
14
14
|
|
|
15
15
|
const args = parseArgs(process.argv.slice(2));
|
|
16
16
|
const usage = "Usage: right-release mirror-root-artifact (--file <artifact> --package-root <dir> [--name <filename>] | --config <file> --platform mac|win [--package-root <dir>])";
|
|
@@ -31,8 +31,7 @@ let entries;
|
|
|
31
31
|
let rootArg = args.packageRoot;
|
|
32
32
|
if (args.config) {
|
|
33
33
|
const configPath = path.resolve(args.config);
|
|
34
|
-
const
|
|
35
|
-
const config = loaded.default ?? loaded.config ?? loaded;
|
|
34
|
+
const config = await importReleaseConfig(configPath, { lenient: true });
|
|
36
35
|
const declared = config?.targets?.[args.platform]?.mirror;
|
|
37
36
|
if (!Array.isArray(declared) || declared.length === 0) {
|
|
38
37
|
console.error(`right-release mirror-root-artifact: targets.${args.platform}.mirror declares no files in ${args.config}`);
|
package/npm-license-notices.mjs
CHANGED
|
@@ -16,6 +16,7 @@ import path from "node:path";
|
|
|
16
16
|
import { pathToFileURL } from "node:url";
|
|
17
17
|
import { spawnSync } from "./background-process.mjs";
|
|
18
18
|
import { CommandSpecError, loadAppConfig, relativeInside } from "./command-spec.mjs";
|
|
19
|
+
import { defaultReleaseConfigFile } from "./release-config.mjs";
|
|
19
20
|
|
|
20
21
|
const fail = (message) => { throw new CommandSpecError(message); };
|
|
21
22
|
|
|
@@ -119,7 +120,7 @@ export function generateNpmNotices({ appRoot, config, flags = {}, check = false,
|
|
|
119
120
|
}
|
|
120
121
|
|
|
121
122
|
export function parseNoticeArgs(argv) {
|
|
122
|
-
const opts = { config:
|
|
123
|
+
const opts = { config: defaultReleaseConfigFile(), output: undefined, productName: undefined, check: false };
|
|
123
124
|
const rest = argv[0] === "npm" ? argv.slice(1) : argv;
|
|
124
125
|
if (argv[0] !== "npm") throw new Error("expected: license-notices npm");
|
|
125
126
|
for (let i = 0; i < rest.length; i += 1) {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// `right-release update <args>`: run the @rightkit/outdated updater (its `right-update` bin) that this package pins.
|
|
3
|
+
//
|
|
4
|
+
// Why a command and not only the dependency: pnpm links just the bins of a project's direct dependencies into
|
|
5
|
+
// node_modules/.bin, so an app that depends on @rightkit/release alone has no `right-update` on its path even though
|
|
6
|
+
// @rightkit/outdated is installed beneath release. This module finds the updater installed for @rightkit/release and
|
|
7
|
+
// runs its bin, so `pnpm exec right-release update run` works in every app that can run `right-release`.
|
|
8
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
11
|
+
import { spawn } from "./background-process.mjs";
|
|
12
|
+
|
|
13
|
+
export const OUTDATED_PACKAGE = "@rightkit/outdated";
|
|
14
|
+
export const OUTDATED_BIN = "right-update";
|
|
15
|
+
|
|
16
|
+
/** The exact command a refusal gives an app that must move a stale RightKit pin. */
|
|
17
|
+
export const UPDATE_COMMAND = "pnpm exec right-release update run";
|
|
18
|
+
|
|
19
|
+
export class OutdatedUnavailableError extends Error {
|
|
20
|
+
constructor(message) {
|
|
21
|
+
super(message);
|
|
22
|
+
this.name = "OutdatedUnavailableError";
|
|
23
|
+
this.code = "OUTDATED_UNAVAILABLE";
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const here = () => path.dirname(fileURLToPath(import.meta.url));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Directory of the @rightkit/outdated installed for the package at `fromDir`, or null. This is Node's lookup
|
|
31
|
+
* (each ancestor's node_modules, never one named node_modules itself) done on the filesystem, because
|
|
32
|
+
* @rightkit/outdated exports only "." under an `import` condition and a caller must not depend on that shape.
|
|
33
|
+
*/
|
|
34
|
+
export function findOutdatedPackage(fromDir = here()) {
|
|
35
|
+
let directory = realpathSync(fromDir);
|
|
36
|
+
for (;;) {
|
|
37
|
+
if (path.basename(directory) !== "node_modules") {
|
|
38
|
+
const candidate = path.join(directory, "node_modules", ...OUTDATED_PACKAGE.split("/"));
|
|
39
|
+
if (existsSync(path.join(candidate, "package.json"))) return realpathSync(candidate);
|
|
40
|
+
}
|
|
41
|
+
const parent = path.dirname(directory);
|
|
42
|
+
if (parent === directory) return null;
|
|
43
|
+
directory = parent;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Absolute path of the `right-update` script of the @rightkit/outdated installed for the package at `fromDir`. */
|
|
48
|
+
export function resolveOutdatedBin(fromDir = here()) {
|
|
49
|
+
const root = findOutdatedPackage(fromDir);
|
|
50
|
+
if (root === null) {
|
|
51
|
+
throw new OutdatedUnavailableError(`${OUTDATED_PACKAGE} is not installed for @rightkit/release; reinstall @rightkit/release so its pinned dependency is present`);
|
|
52
|
+
}
|
|
53
|
+
let manifest;
|
|
54
|
+
try {
|
|
55
|
+
manifest = JSON.parse(readFileSync(path.join(root, "package.json"), "utf8"));
|
|
56
|
+
} catch {
|
|
57
|
+
throw new OutdatedUnavailableError(`${OUTDATED_PACKAGE} has an unreadable package.json at ${root}`);
|
|
58
|
+
}
|
|
59
|
+
const named = typeof manifest.bin === "string" ? manifest.bin : manifest.bin?.[OUTDATED_BIN];
|
|
60
|
+
if (manifest.name !== OUTDATED_PACKAGE || typeof named !== "string" || named === "") {
|
|
61
|
+
throw new OutdatedUnavailableError(`${root} is not an ${OUTDATED_PACKAGE} that provides ${OUTDATED_BIN}`);
|
|
62
|
+
}
|
|
63
|
+
const file = path.resolve(root, named);
|
|
64
|
+
const relative = path.relative(root, file);
|
|
65
|
+
if (relative === "" || relative.startsWith("..") || path.isAbsolute(relative) || !existsSync(file)) {
|
|
66
|
+
throw new OutdatedUnavailableError(`${OUTDATED_PACKAGE} ${manifest.version ?? ""} lists ${OUTDATED_BIN} at ${named}, which is not installed`);
|
|
67
|
+
}
|
|
68
|
+
return file;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Run the updater with `argv` and resolve to its exit status. */
|
|
72
|
+
export function runOutdated(argv, { fromDir = here(), cwd = process.cwd(), env = process.env, stdio = "inherit" } = {}) {
|
|
73
|
+
const bin = resolveOutdatedBin(fromDir);
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const child = spawn(process.execPath, [bin, ...argv], { cwd, env, stdio, windowsHide: true });
|
|
76
|
+
child.on("error", reject);
|
|
77
|
+
child.on("exit", (code) => resolve(code ?? 1));
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export async function main(argv = process.argv.slice(2)) {
|
|
82
|
+
try {
|
|
83
|
+
process.exitCode = await runOutdated(argv);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
console.error(`right-release update: ${error.message}`);
|
|
86
|
+
process.exitCode = error instanceof OutdatedUnavailableError ? 2 : 1;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const realOrSelf = (file) => { try { return realpathSync(file); } catch { return path.resolve(file); } };
|
|
91
|
+
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) await main();
|
|
@@ -399,10 +399,12 @@
|
|
|
399
399
|
"cargo:base64",
|
|
400
400
|
"cargo:block2",
|
|
401
401
|
"cargo:chrono",
|
|
402
|
+
"cargo:clap",
|
|
402
403
|
"cargo:dirs",
|
|
403
404
|
"cargo:futures",
|
|
404
405
|
"cargo:getrandom",
|
|
405
406
|
"cargo:image",
|
|
407
|
+
"cargo:libc",
|
|
406
408
|
"cargo:objc2",
|
|
407
409
|
"cargo:objc2-foundation",
|
|
408
410
|
"cargo:reqwest",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.6",
|
|
4
4
|
"description": "Portable release CLI/SDK with fail-closed public GitHub Releases and private R2 + RightApps distribution.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"type": "module",
|
|
@@ -8,8 +8,43 @@
|
|
|
8
8
|
"right-release": "cli/right-release.mjs",
|
|
9
9
|
"rightkit-gate": "bin/rightkit-gate.mjs"
|
|
10
10
|
},
|
|
11
|
+
"exports": {
|
|
12
|
+
"./bin/rightkit-gate.mjs": "./bin/rightkit-gate.mjs",
|
|
13
|
+
"./cargo-contract.mjs": "./cargo-contract.mjs",
|
|
14
|
+
"./cargo-target.mjs": "./cargo-target.mjs",
|
|
15
|
+
"./cli/right-release.mjs": "./cli/right-release.mjs",
|
|
16
|
+
"./development.mjs": "./development.mjs",
|
|
17
|
+
"./direct-bootstrap.mjs": "./direct-bootstrap.mjs",
|
|
18
|
+
"./disk-policy.mjs": "./disk-policy.mjs",
|
|
19
|
+
"./dmg-build.mjs": "./dmg-build.mjs",
|
|
20
|
+
"./engine-locator.mjs": "./engine-locator.mjs",
|
|
21
|
+
"./github-release.mjs": "./github-release.mjs",
|
|
22
|
+
"./legal-contract.mjs": "./legal-contract.mjs",
|
|
23
|
+
"./macos-file-evidence.mjs": "./macos-file-evidence.mjs",
|
|
24
|
+
"./macos-notarization.mjs": "./macos-notarization.mjs",
|
|
25
|
+
"./macos-signing-identity.mjs": "./macos-signing-identity.mjs",
|
|
26
|
+
"./macos-signing-session.mjs": "./macos-signing-session.mjs",
|
|
27
|
+
"./managed-cargo.mjs": "./managed-cargo.mjs",
|
|
28
|
+
"./native-release-finalization.mjs": "./native-release-finalization.mjs",
|
|
29
|
+
"./notary-auth.mjs": "./notary-auth.mjs",
|
|
30
|
+
"./package.json": "./package.json",
|
|
31
|
+
"./packaged-app-identity.mjs": "./packaged-app-identity.mjs",
|
|
32
|
+
"./pe-content.mjs": "./pe-content.mjs",
|
|
33
|
+
"./publication-gate.mjs": "./publication-gate.mjs",
|
|
34
|
+
"./qa-build-features.mjs": "./qa-build-features.mjs",
|
|
35
|
+
"./release-dependencies.mjs": "./release-dependencies.mjs",
|
|
36
|
+
"./sign-windows.mjs": "./sign-windows.mjs",
|
|
37
|
+
"./signing-agent-client.mjs": "./signing-agent-client.mjs",
|
|
38
|
+
"./stage-binary.mjs": "./stage-binary.mjs",
|
|
39
|
+
"./stage-sidecar.mjs": "./stage-sidecar.mjs",
|
|
40
|
+
"./supply-chain-evidence.mjs": "./supply-chain-evidence.mjs",
|
|
41
|
+
"./tauri-bundle-marker.mjs": "./tauri-bundle-marker.mjs"
|
|
42
|
+
},
|
|
11
43
|
"rightkit": {
|
|
12
44
|
"publishCheck": {
|
|
45
|
+
"skipImport": [
|
|
46
|
+
"./sign-windows.mjs"
|
|
47
|
+
],
|
|
13
48
|
"binSmoke": {
|
|
14
49
|
"right-release": [
|
|
15
50
|
"--version"
|
|
@@ -33,7 +68,9 @@
|
|
|
33
68
|
"LICENSE-MIT",
|
|
34
69
|
"LICENSE-APACHE",
|
|
35
70
|
"!*.test.mjs",
|
|
36
|
-
"!
|
|
71
|
+
"!cli/*.test.mjs",
|
|
72
|
+
"!standalone-clone-evidence.json",
|
|
73
|
+
"!refusals.json"
|
|
37
74
|
],
|
|
38
75
|
"sideEffects": false,
|
|
39
76
|
"publishConfig": {
|
|
@@ -46,10 +83,11 @@
|
|
|
46
83
|
"directory": "packages/release"
|
|
47
84
|
},
|
|
48
85
|
"dependencies": {
|
|
86
|
+
"@rightkit/outdated": "0.2.0",
|
|
49
87
|
"smol-toml": "1.9.1"
|
|
50
88
|
},
|
|
51
89
|
"scripts": {
|
|
52
|
-
"test": "node --import ./test-support/broker-guard.mjs --test --test-concurrency=1 --test-force-exit *.test.mjs",
|
|
90
|
+
"test": "node --import ./test-support/broker-guard.mjs --test --test-concurrency=1 --test-force-exit *.test.mjs cli/*.test.mjs",
|
|
53
91
|
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
54
92
|
"test:signer:windows": "node sign-release-manifest.integration.mjs",
|
|
55
93
|
"doctor:all": "node --test right-suite-contract.test.mjs",
|
package/pipeline-admission.mjs
CHANGED
|
@@ -230,6 +230,7 @@ function admitRuntime(bin, rest, options, label) {
|
|
|
230
230
|
case "right-release": return admitRightRelease(rest, label);
|
|
231
231
|
case "tauri": return admitTauri(rest, options, label);
|
|
232
232
|
case "vite": return rest[0] === "build" ? (VITE_OPTIONS(rest.slice(1), label) ?? admit("vite-build", "runtime")) : rest.length === 1 && rest[0] === "--version" ? admit("vite-version", "runtime") : refuse("R1-option", `${label}: only vite build and vite --version are admitted`);
|
|
233
|
+
case "rightkit-gate": return admitRightKitGate(rest, label);
|
|
233
234
|
case "tsc": return shellSafeArgs(rest, label) ?? admit("tsc", "runtime");
|
|
234
235
|
case "vitest": return rest[0] === "run" || (rest.length === 1 && rest[0] === "--version") ? (shellSafeArgs(rest, label) ?? admit("vitest", "runtime")) : refuse("R1-option", `${label}: only vitest run is admitted`);
|
|
235
236
|
case "eslint": return shellSafeArgs(rest, label) ?? admit("eslint", "runtime");
|
|
@@ -237,6 +238,16 @@ function admitRuntime(bin, rest, options, label) {
|
|
|
237
238
|
}
|
|
238
239
|
}
|
|
239
240
|
|
|
241
|
+
/**
|
|
242
|
+
* `rightkit-gate install-hook`: writes the committed pre-push hook and sets core.hooksPath, so an app's `prepare` script may run it.
|
|
243
|
+
* It builds, signs, packages and uploads nothing. The gate itself (`rightkit-gate`, `--base`) compiles and is run by the hook, never
|
|
244
|
+
* by a release config; only the installer and the version probe are admitted.
|
|
245
|
+
*/
|
|
246
|
+
function admitRightKitGate(args, label) {
|
|
247
|
+
if (args.length === 1 && (args[0] === "install-hook" || args[0] === "--version")) return admit(`rightkit-gate-${args[0].replace(/^--/, "")}`, "owner");
|
|
248
|
+
return refuse("R1-owner-subcommand", `${label}: only rightkit-gate install-hook (and --version) is an admitted pipeline command; the gate itself runs from the pre-push hook`);
|
|
249
|
+
}
|
|
250
|
+
|
|
240
251
|
function admitRightRelease(args, label) {
|
|
241
252
|
const sub = args[0];
|
|
242
253
|
if (!OWNER_SUBCOMMANDS.has(sub)) return refuse("R1-owner-subcommand", `${label}: right-release ${JSON.stringify(sub ?? "")} is not a known RightKit command`);
|
|
@@ -350,6 +361,7 @@ export function admitInvocation(step, options = {}) {
|
|
|
350
361
|
const tool = classifyTokens([name, ...args]);
|
|
351
362
|
if (tool) return refuse("R3-pipeline-tool", `${label}: ${tool.tool} is a ${tool.stage} tool; build, sign, notarize, package and upload run only inside RightKit`);
|
|
352
363
|
if (name === "right-release") return admitRightRelease(args, label);
|
|
364
|
+
if (name === "rightkit-gate") return admitRightKitGate(args, label);
|
|
353
365
|
if (name === "pnpm") return admitPnpm(args, inner, label, chain);
|
|
354
366
|
if (name === "node") return admitNode(args, inner, label);
|
|
355
367
|
if (name === "rightkit") return refuse("R1-retired-client", `${label}: rightkit is the retired name of the RustRight client (rustright#91); use rustright ${args.slice(0, 2).join(" ")}`);
|
package/prebuild-gates.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Source-level release gates that run before a build starts. The build (release.mjs) and `doctor --gates` call the
|
|
2
2
|
// same function, so the doctor can never pass a config the build refuses. Gate order is part of the contract.
|
|
3
|
-
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { spawnSync } from "./background-process.mjs";
|
|
6
6
|
import { assertReleaseDependencies } from "./release-dependencies.mjs";
|
|
@@ -145,11 +145,15 @@ export function evaluatePrebuildGates({ root, config, target, platform, iosSpec,
|
|
|
145
145
|
let sourceRoots;
|
|
146
146
|
let skipRoots;
|
|
147
147
|
let repoTop;
|
|
148
|
+
let appRootReal;
|
|
149
|
+
const nativeReal = (file) => { try { return realpathSync.native(file); } catch { return file; } };
|
|
148
150
|
if (releaseSwitchInventoryText != null) {
|
|
149
151
|
gate("source-switch-roots", ["closure-commands", ...(strict ? ["feature-closure"] : [])], () => {
|
|
150
152
|
if (platform === "ios") {
|
|
151
153
|
repoTop = git(root, ["rev-parse", "--show-toplevel"]);
|
|
152
|
-
|
|
154
|
+
// Git reports the canonical path; a caller's root may be a symlink or a Windows short name (hosted temp roots).
|
|
155
|
+
appRootReal = nativeReal(root);
|
|
156
|
+
const swiftSide = (iosOwnershipScope({ repoRoot: repoTop, appRoot: appRootReal, target: config.targets?.ios }) ?? []).filter((dir) => dir !== ".rightkit").map((dir) => path.join(repoTop, dir));
|
|
153
157
|
sourceRoots = [...new Set([...swiftSide, ...localCrateDirsFromTree(treeText, repoTop)])].sort();
|
|
154
158
|
console.log(`right-release: source switch scan (ios): ${sourceRoots.map((dir) => path.relative(repoTop, dir) || ".").join(", ")}`);
|
|
155
159
|
} else {
|
|
@@ -162,7 +166,7 @@ export function evaluatePrebuildGates({ root, config, target, platform, iosSpec,
|
|
|
162
166
|
if (platform === "ios") {
|
|
163
167
|
gate("source-switch-prepare", ["source-switch-roots"], () => {
|
|
164
168
|
for (const step of config.targets?.ios?.iosPackage?.prepare ?? []) {
|
|
165
|
-
const cwd = path.resolve(
|
|
169
|
+
const cwd = path.resolve(appRootReal, step?.cwd ?? ".");
|
|
166
170
|
if (!sourceRoots.some((dir) => cwd === dir || cwd.startsWith(dir + path.sep))) throw new Error(`source switch inventory: targets.ios.iosPackage.prepare runs in ${path.relative(repoTop, cwd) || "."}, outside the iOS scan roots (${sourceRoots.map((dir) => path.relative(repoTop, dir) || ".").join(", ")}); list that directory in targets.ios.ownershipScope`);
|
|
167
171
|
}
|
|
168
172
|
});
|