@rightkit/release 0.6.5 → 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 +79 -7
- package/android-play-upload.mjs +4 -4
- package/app-check.mjs +474 -17
- 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 +431 -44
- package/binary-license-notices.mjs +2 -1
- package/build-release.mjs +7 -6
- 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 +8 -1
- package/command-spec.mjs +4 -5
- package/deps.mjs +5 -4
- package/deterministic-scans.mjs +280 -32
- 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/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/package.json +37 -1
- package/pipeline-admission.mjs +12 -0
- 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 +4 -4
- package/rightkit-registry-resolver.mjs +5 -2
- package/rightkit-versions.json +11 -8
- 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/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();
|
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"
|
|
@@ -48,6 +83,7 @@
|
|
|
48
83
|
"directory": "packages/release"
|
|
49
84
|
},
|
|
50
85
|
"dependencies": {
|
|
86
|
+
"@rightkit/outdated": "0.2.0",
|
|
51
87
|
"smol-toml": "1.9.1"
|
|
52
88
|
},
|
|
53
89
|
"scripts": {
|
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(" ")}`);
|