@rightkit/release 0.6.5 → 0.6.7
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 +196 -11
- package/admission-files.mjs +3 -2
- package/advisory-db.json +4 -0
- package/advisory-db.mjs +124 -0
- package/advisory-exceptions.mjs +13 -10
- package/android-play-upload.mjs +4 -4
- package/app-check.mjs +702 -26
- package/apple-profile-create.mjs +4 -3
- package/apple-profile-renew.mjs +5 -4
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-agent-hook.mjs +9 -0
- package/bin/rightkit-gate.mjs +671 -133
- package/binary-license-notices.mjs +2 -1
- package/build-release.mjs +20 -12
- 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 +37 -7
- package/command-spec.mjs +4 -5
- package/compile-inputs.mjs +66 -0
- package/consumer-contracts.json +1044 -0
- package/consumer-contracts.mjs +627 -0
- package/consumer-input-contracts.mjs +422 -0
- package/consumer-policy.mjs +588 -0
- package/consumer-source-contracts.mjs +469 -0
- package/deny-default.toml +42 -0
- package/deps.mjs +5 -4
- package/deterministic-scans.mjs +513 -54
- package/development.mjs +5 -5
- package/disk-command.mjs +270 -30
- package/disk-policy.json +50 -10
- package/disk-policy.mjs +482 -35
- package/disk-report.mjs +129 -0
- package/door-policy.mjs +228 -0
- package/gate-executor.mjs +30 -0
- package/gate-stages.mjs +110 -0
- package/github-release.mjs +5 -3
- package/github-updater-release.mjs +48 -8
- package/github-upload-gate.mjs +6 -6
- package/init-command.mjs +175 -8
- package/ios-upload.mjs +4 -4
- package/legion-semantic-contracts.mjs +428 -0
- package/mirror-root-artifact.mjs +3 -4
- package/npm-license-notices.mjs +2 -1
- package/outdated-command.mjs +91 -0
- package/ownership-class-rules.mjs +277 -0
- package/ownership-key-authority.mjs +3 -1
- package/ownership-mandatory-set.mjs +3 -2
- package/package.json +46 -3
- package/pipeline-admission.mjs +12 -0
- package/publish-cargo.mjs +219 -4
- package/publish-update.mjs +19 -14
- package/release-config.mjs +87 -11
- 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 +178 -170
- package/shell-contract-bridge.json +251 -0
- package/shell-contract.mjs +1069 -0
- package/sign-updater.mjs +24 -4
- package/source-closure-rules.mjs +204 -0
- package/stage-sidecar-command.mjs +7 -6
- package/stage-sidecar.mjs +437 -114
- package/suite-api.generated.mjs +96 -6
- package/suite-trust-roots.mjs +198 -0
- package/test-command.mjs +3 -2
- package/upload-release.mjs +3 -2
- package/web-build.mjs +2 -1
package/github-upload-gate.mjs
CHANGED
|
@@ -8,12 +8,12 @@
|
|
|
8
8
|
// - sealed ownership evidence that still matches the committed source;
|
|
9
9
|
// - an explicit distribution.provider of github-releases and a distribution.repository, with any --repo
|
|
10
10
|
// argument required to equal it (the origin remote is never consulted).
|
|
11
|
-
// Order matters:
|
|
12
|
-
// proven clean and the sealed commit equals HEAD.
|
|
11
|
+
// Order matters: a legacy release config is a module that runs on import (a JSON config is data), so the config is
|
|
12
|
+
// loaded only after the tree is proven clean and the sealed commit equals HEAD.
|
|
13
13
|
import { existsSync } from "node:fs";
|
|
14
14
|
import path from "node:path";
|
|
15
|
-
import { pathToFileURL } from "node:url";
|
|
16
15
|
import { spawnSync } from "./background-process.mjs";
|
|
16
|
+
import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
|
|
17
17
|
import { verifyOwnershipEvidence } from "./ownership-evidence.mjs";
|
|
18
18
|
import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
|
|
19
19
|
import { verifySealedRelease } from "./release-state.mjs";
|
|
@@ -48,9 +48,9 @@ export async function authorizeGitHubUpload({
|
|
|
48
48
|
tier,
|
|
49
49
|
releaseId,
|
|
50
50
|
platform,
|
|
51
|
-
configName = "right-release.config.mjs",
|
|
52
|
-
repo = "",
|
|
53
51
|
cwd = process.cwd(),
|
|
52
|
+
configName = defaultReleaseConfigFile(cwd),
|
|
53
|
+
repo = "",
|
|
54
54
|
}) {
|
|
55
55
|
assertUploadTier(tier);
|
|
56
56
|
if (!releaseId) throw new Error("--release is required");
|
|
@@ -69,7 +69,7 @@ export async function authorizeGitHubUpload({
|
|
|
69
69
|
|
|
70
70
|
const configPath = path.resolve(cwd, configName);
|
|
71
71
|
if (!existsSync(configPath)) throw new Error(`release config is required: ${configName}`);
|
|
72
|
-
const releaseConfig =
|
|
72
|
+
const releaseConfig = await importReleaseConfig(configPath, { query: `upload=${Date.now()}` });
|
|
73
73
|
const ownership = verifyOwnershipEvidence(sealed.manifest.ownershipEvidence, repoRoot, path.dirname(configPath));
|
|
74
74
|
if (!ownership.ok) throw new Error(`sealed ownership evidence rejected: ${ownership.reason}`);
|
|
75
75
|
const configuredRepo = resolveGitHubDistribution(releaseConfig, repo);
|
package/init-command.mjs
CHANGED
|
@@ -3,9 +3,11 @@
|
|
|
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";
|
|
10
|
+
import { isJudgedNotice } from "./consumer-contracts.mjs";
|
|
9
11
|
|
|
10
12
|
export const SKIP_DIRS = new Set(["node_modules", "target", ".git", "dist", "gen"]);
|
|
11
13
|
export const TEXT_EXT = new Set(["json", "toml", "mjs", "ts", "tsx", "rs", "md", "html", "css", "yaml", "yml", "sh"]);
|
|
@@ -50,6 +52,60 @@ export function resolveIdentity({ name, id, slug }) {
|
|
|
50
52
|
return { name, id, slug: resolvedSlug, warnings };
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
// Cargo writes Cargo.lock with its [[package]] tables sorted by name (then version) and each `dependencies` list sorted by name, and
|
|
56
|
+
// `cargo --locked` refuses a lock that differs from what it would write. Renaming the template's own crates moves them in that order,
|
|
57
|
+
// so the lock is re-sorted after the text rename; without it every `--locked` script fails on the first build of the new app.
|
|
58
|
+
const LOCK_FILE = "Cargo.lock";
|
|
59
|
+
const byteOrder = (a, b) => (a < b ? -1 : a > b ? 1 : 0);
|
|
60
|
+
const semverParts = (version) => {
|
|
61
|
+
const [core, pre] = String(version ?? "").split("+")[0].split(/-(.*)/s);
|
|
62
|
+
return { nums: core.split(".").map((part) => Number.parseInt(part, 10) || 0), pre: pre ?? null };
|
|
63
|
+
};
|
|
64
|
+
function compareSemver(a, b) {
|
|
65
|
+
const x = semverParts(a);
|
|
66
|
+
const y = semverParts(b);
|
|
67
|
+
for (let i = 0; i < 3; i += 1) if (x.nums[i] !== y.nums[i]) return (x.nums[i] ?? 0) - (y.nums[i] ?? 0);
|
|
68
|
+
if (x.pre === y.pre) return 0;
|
|
69
|
+
if (x.pre === null) return 1;
|
|
70
|
+
if (y.pre === null) return -1;
|
|
71
|
+
return byteOrder(x.pre, y.pre);
|
|
72
|
+
}
|
|
73
|
+
const lockField = (block, key) => new RegExp(`^${key} = "([^"]*)"$`, "m").exec(block)?.[1] ?? "";
|
|
74
|
+
/** `"name"`, `"name version"` or `"name version (source)"`: the name and version of one `dependencies` entry. */
|
|
75
|
+
const lockDependency = (line) => {
|
|
76
|
+
const [name = "", version = ""] = line.trim().replace(/^"|",?$/g, "").split(" ");
|
|
77
|
+
return { name, version };
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
/** Sort one lock's [[package]] tables and their `dependencies` lists the way cargo writes them; everything else is kept byte for byte. */
|
|
81
|
+
export function sortCargoLock(text) {
|
|
82
|
+
const crlf = text.includes("\r\n");
|
|
83
|
+
const lf = crlf ? text.replace(/\r\n/g, "\n") : text;
|
|
84
|
+
const first = /^\[\[package\]\]$/m.exec(lf);
|
|
85
|
+
if (!first) return text;
|
|
86
|
+
const head = lf.slice(0, first.index);
|
|
87
|
+
let body = lf.slice(first.index);
|
|
88
|
+
let tail = "";
|
|
89
|
+
const other = /^\[(?!\[package\]\]$)/m.exec(body);
|
|
90
|
+
if (other) {
|
|
91
|
+
tail = body.slice(other.index);
|
|
92
|
+
body = body.slice(0, other.index);
|
|
93
|
+
}
|
|
94
|
+
const blocks = body.replace(/\n+$/, "").split(/\n\n(?=\[\[package\]\]$)/m).map((block) => block.replace(
|
|
95
|
+
/^dependencies = \[\n([\s\S]*?)\n\]$/m,
|
|
96
|
+
(_, list) => `dependencies = [\n${list.split("\n").map((line) => ({ line, ...lockDependency(line) }))
|
|
97
|
+
.sort((a, b) => byteOrder(a.name, b.name) || compareSemver(a.version, b.version)).map((entry) => entry.line).join("\n")}\n]`,
|
|
98
|
+
));
|
|
99
|
+
blocks.sort((a, b) => byteOrder(lockField(a, "name"), lockField(b, "name")) || compareSemver(lockField(a, "version"), lockField(b, "version")));
|
|
100
|
+
const out = `${head}${blocks.join("\n\n")}\n${tail ? `\n${tail}` : ""}`;
|
|
101
|
+
return crlf ? out.replace(/\n/g, "\r\n") : out;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** Rename the template identity in a Cargo.lock and restore cargo's order, so `cargo --locked` accepts the renamed lock. */
|
|
105
|
+
export function reidentifyCargoLock(text, identity) {
|
|
106
|
+
return sortCargoLock(renameText(text, identity));
|
|
107
|
+
}
|
|
108
|
+
|
|
53
109
|
export function walkTextFiles(dir, out = []) {
|
|
54
110
|
let entries;
|
|
55
111
|
try {
|
|
@@ -61,26 +117,77 @@ export function walkTextFiles(dir, out = []) {
|
|
|
61
117
|
const full = path.join(dir, entry.name);
|
|
62
118
|
if (entry.isDirectory()) {
|
|
63
119
|
if (!SKIP_DIRS.has(entry.name)) walkTextFiles(full, out);
|
|
64
|
-
} else if (entry.isFile() && TEXT_EXT.has(path.extname(entry.name).slice(1))) {
|
|
120
|
+
} else if (entry.isFile() && (TEXT_EXT.has(path.extname(entry.name).slice(1)) || entry.name === LOCK_FILE)) {
|
|
65
121
|
out.push(full);
|
|
66
122
|
}
|
|
67
123
|
}
|
|
68
124
|
return out;
|
|
69
125
|
}
|
|
70
126
|
|
|
127
|
+
// `rightapps` is not the template identity: it is the RightApps distribution service (the `r2-rightapps` provider), which every app
|
|
128
|
+
// keeps. A spelling followed by a whole-word `s` is therefore left alone; `rightapp-engine`, `rightapp_protocol`, `RIGHTAPP_HOME` and
|
|
129
|
+
// the rest of the identity still rename.
|
|
130
|
+
const spelling = (word) => new RegExp(`${word}(?)`, "g");
|
|
131
|
+
const UPPER_WORD = spelling(UPPER);
|
|
132
|
+
const TITLE_WORD = spelling(TITLE);
|
|
133
|
+
const LOWER_WORD = spelling(LOWER);
|
|
134
|
+
|
|
71
135
|
/** Rewrite the template identity in one string; the bundle id first, then the three spellings. */
|
|
72
136
|
export function renameText(text, { name, id, slug }) {
|
|
73
137
|
return text
|
|
74
138
|
.replaceAll(TEMPLATE_ID, id)
|
|
75
|
-
.
|
|
76
|
-
.
|
|
77
|
-
.
|
|
139
|
+
.replace(UPPER_WORD, () => slug.toUpperCase())
|
|
140
|
+
.replace(TITLE_WORD, () => name)
|
|
141
|
+
.replace(LOWER_WORD, () => slug);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export const LEGAL_MANIFEST = "legal/legal-manifest.json";
|
|
145
|
+
const sha256Hex = (bytes) => createHash("sha256").update(bytes).digest("hex");
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Recompute the `sha256` of every document and appendix of a legal manifest from the bytes `read(relativePath)` returns (paths are
|
|
149
|
+
* relative to the manifest's directory; `read` returns Buffer, string or null for a missing file). Returns `{ text, changed, stale,
|
|
150
|
+
* material }`: the manifest text with the new digests (2-space JSON, as `legal-snapshot` writes it), the entries whose digest moved,
|
|
151
|
+
* and which of those are material agreements. Material documents feed the acceptance digest in src/generated/legal-contract.json, which
|
|
152
|
+
* only `legal-snapshot` can recompute, so the caller warns about them.
|
|
153
|
+
*/
|
|
154
|
+
export function rehashLegalManifest(text, read) {
|
|
155
|
+
const manifest = JSON.parse(text);
|
|
156
|
+
const changed = [];
|
|
157
|
+
const material = [];
|
|
158
|
+
for (const entry of [...(manifest.documents ?? []), ...(manifest.appendices ?? [])]) {
|
|
159
|
+
const bytes = typeof entry?.path === "string" ? read(entry.path) : null;
|
|
160
|
+
if (bytes === null || bytes === undefined) continue;
|
|
161
|
+
const digest = sha256Hex(typeof bytes === "string" ? Buffer.from(bytes) : bytes);
|
|
162
|
+
if (entry.sha256 === digest) continue;
|
|
163
|
+
entry.sha256 = digest;
|
|
164
|
+
changed.push(entry.path);
|
|
165
|
+
if (entry.material === true) material.push(entry.path);
|
|
166
|
+
}
|
|
167
|
+
return { text: changed.length ? `${JSON.stringify(manifest, null, 2)}\n` : text, changed, material };
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Rename the template under `root`. Returns the identity, the changed file paths (relative, sorted) and `warnings`.
|
|
172
|
+
* The rename changes the bytes of text the legal manifest hashes (the privacy notice and the npm appendix carry the product name), so the
|
|
173
|
+
* manifest's digests are recomputed from the renamed bytes in the same pass.
|
|
174
|
+
*/
|
|
175
|
+
/** The lock a notice field names (`prefix` holds the name or path), resolved from `dir` upward to `root` as CA-14 resolves it. */
|
|
176
|
+
function resolveLockUpward(root, dir, prefix) {
|
|
177
|
+
const ref = /^`?([^`\s(]+)/.exec(prefix)[1];
|
|
178
|
+
for (let at = path.resolve(dir); ; at = path.dirname(at)) {
|
|
179
|
+
const candidate = path.resolve(at, ...ref.split("/"));
|
|
180
|
+
if (existsSync(candidate)) return candidate;
|
|
181
|
+
if (at === path.resolve(root) || at === path.dirname(at)) return null;
|
|
182
|
+
}
|
|
78
183
|
}
|
|
79
184
|
|
|
80
|
-
/** Rename the template under `root`. Returns the identity and the changed file paths (relative, sorted). */
|
|
81
185
|
export function initTemplate(root, options) {
|
|
82
186
|
const identity = resolveIdentity(options);
|
|
83
187
|
const changed = [];
|
|
188
|
+
const warnings = [...identity.warnings];
|
|
189
|
+
const renamed = new Map();
|
|
190
|
+
const lockDigestsBefore = new Map();
|
|
84
191
|
for (const file of walkTextFiles(root)) {
|
|
85
192
|
if (path.basename(file) === SELF) continue;
|
|
86
193
|
let before;
|
|
@@ -89,20 +196,80 @@ export function initTemplate(root, options) {
|
|
|
89
196
|
} catch {
|
|
90
197
|
continue;
|
|
91
198
|
}
|
|
92
|
-
const after = renameText(before, identity);
|
|
199
|
+
const after = path.basename(file) === LOCK_FILE ? reidentifyCargoLock(before, identity) : renameText(before, identity);
|
|
93
200
|
if (after === before) continue;
|
|
201
|
+
if (path.basename(file) === LOCK_FILE) lockDigestsBefore.set(path.resolve(file), createHash("sha256").update(before).digest("hex"));
|
|
202
|
+
renamed.set(path.resolve(file), after);
|
|
94
203
|
if (!options.dryRun) writeFileSync(file, after);
|
|
95
204
|
changed.push(path.relative(root, file).split(path.sep).join("/"));
|
|
96
205
|
}
|
|
97
|
-
|
|
206
|
+
// The rename changes Cargo.lock's bytes (the app's own crates), so every notice that records the lock's digest
|
|
207
|
+
// (`Cargo.lock sha256 <hex>` / `sha256=<hex>`, checked by notice-lock-digest CA-14) would go stale: carry the new digest.
|
|
208
|
+
// Only the provenance field moves: `<lock> sha256 <hex>` or `<lock> sha256=<hex>` (optionally backticked, optionally with a
|
|
209
|
+
// parenthesised label) in a notice CA-14 judges (isJudgedNotice), where <lock> resolves from the notice's directory upward to
|
|
210
|
+
// this lock, as CA-14 resolves it, and <hex> is its old digest. The same digest, or the same formatted text, anywhere else
|
|
211
|
+
// (another input's field, archived reports and evidence, a source literal, a notice in a history tree) is left alone.
|
|
212
|
+
const escapeRe = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
213
|
+
const pairs = [...lockDigestsBefore].map(([file, old]) => {
|
|
214
|
+
const names = [...new Set([path.basename(file), path.relative(root, file).split(path.sep).join("/")])].map(escapeRe).join("|");
|
|
215
|
+
const field = new RegExp(`(\`?(?:${names})\`?(?:[ \\t]*\\([^)\\n]*\\))?[ \\t]+sha256(?:=|[ \\t]+)\`?)${old}(?![0-9a-f])`, "g");
|
|
216
|
+
return { lock: file, field, now: createHash("sha256").update(renamed.get(file)).digest("hex") };
|
|
217
|
+
});
|
|
218
|
+
if (pairs.length) {
|
|
219
|
+
for (const file of walkTextFiles(root)) {
|
|
220
|
+
if (path.basename(file) === SELF || path.basename(file) === LOCK_FILE) continue;
|
|
221
|
+
if (!isJudgedNotice(path.relative(root, file).split(path.sep).join("/"))) continue;
|
|
222
|
+
const before = renamed.get(path.resolve(file)) ?? readFileSync(file, "utf8");
|
|
223
|
+
let after = before;
|
|
224
|
+
for (const { lock, field, now } of pairs) {
|
|
225
|
+
after = after.replace(field, (whole, prefix) => (resolveLockUpward(root, path.dirname(file), prefix) === lock ? `${prefix}${now}` : whole));
|
|
226
|
+
}
|
|
227
|
+
if (after === before) continue;
|
|
228
|
+
renamed.set(path.resolve(file), after);
|
|
229
|
+
if (!options.dryRun) writeFileSync(file, after);
|
|
230
|
+
const relative = path.relative(root, file).split(path.sep).join("/");
|
|
231
|
+
if (!changed.includes(relative)) changed.push(relative);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
const manifestFile = path.join(root, ...LEGAL_MANIFEST.split("/"));
|
|
235
|
+
if (existsSync(manifestFile)) {
|
|
236
|
+
const legalDir = path.dirname(manifestFile);
|
|
237
|
+
const read = (relative) => {
|
|
238
|
+
const target = path.resolve(legalDir, relative);
|
|
239
|
+
if (renamed.has(target)) return renamed.get(target);
|
|
240
|
+
try { return readFileSync(target); } catch { return null; }
|
|
241
|
+
};
|
|
242
|
+
const rehashed = rehashLegalManifest(renamed.get(path.resolve(manifestFile)) ?? readFileSync(manifestFile, "utf8"), read);
|
|
243
|
+
if (rehashed.changed.length) {
|
|
244
|
+
if (!options.dryRun) writeFileSync(manifestFile, rehashed.text);
|
|
245
|
+
if (!changed.includes(LEGAL_MANIFEST)) changed.push(LEGAL_MANIFEST);
|
|
246
|
+
}
|
|
247
|
+
for (const material of rehashed.material) {
|
|
248
|
+
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`);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return { ...identity, changed: changed.sort(), warnings };
|
|
98
252
|
}
|
|
99
253
|
|
|
254
|
+
/** What a person does after init: the steps no command can do for them, in order. */
|
|
255
|
+
export const NEXT_STEPS = Object.freeze([
|
|
256
|
+
"pnpm install (installs the pinned packages and, through `prepare`, the pre-push hook)",
|
|
257
|
+
"right-release doctor (the build's own checks for this app, as rows)",
|
|
258
|
+
"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`",
|
|
259
|
+
"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)",
|
|
260
|
+
"README.md in the template: \"Create a new app (checklist)\"",
|
|
261
|
+
]);
|
|
262
|
+
|
|
100
263
|
export function runInitCommand(argv, io = { stdout: console.log, stderr: console.error }) {
|
|
101
264
|
const opts = parseInitArgs(argv);
|
|
102
265
|
const result = initTemplate(path.resolve(opts.dir), opts);
|
|
103
266
|
for (const warning of result.warnings) io.stderr(`warning: ${warning}`);
|
|
104
267
|
io.stdout(`right-release init${opts.dryRun ? " --dry-run" : ""}: ${result.changed.length} file(s) ${opts.dryRun ? "would change" : "changed"}`);
|
|
105
268
|
for (const file of result.changed) io.stdout(` ${file}`);
|
|
269
|
+
if (!opts.dryRun) {
|
|
270
|
+
io.stdout("next:");
|
|
271
|
+
for (const step of NEXT_STEPS) io.stdout(` ${step}`);
|
|
272
|
+
}
|
|
106
273
|
return result;
|
|
107
274
|
}
|
|
108
275
|
|
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);
|