@rightkit/release 0.6.4 → 0.6.5
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 +30 -0
- package/admission-files.mjs +279 -0
- package/admission-host.mjs +403 -0
- package/admission.mjs +146 -0
- package/app-check.mjs +869 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +64 -25
- package/build-release.mjs +81 -21
- package/cli/right-release.mjs +37 -4
- package/deterministic-scans.mjs +21 -5
- package/inventory-check.mjs +12 -1
- package/ownership-dependency-allowlist.json +2 -0
- package/package.json +5 -3
- package/prebuild-gates.mjs +7 -3
- package/release.mjs +113 -58
- package/rightkit-versions.json +10 -8
package/README.md
CHANGED
|
@@ -91,6 +91,36 @@ On desktop (non-iOS) targets the source walk covers the whole app root, so app t
|
|
|
91
91
|
| Legal | `license-notices npm`, `license-notices cargo`, `license-notices binaries` |
|
|
92
92
|
| Other | `dev`, `doctor`, `suite-doctor`, `qa`, `dmg`, `cache`, `apple`, `inventory`, `model`, `addon`, `hooks`, `lsclean`, `generate-dmg-background`, `publish cargo`, `publish npm`, `publish swift` |
|
|
93
93
|
|
|
94
|
+
## The app check: deterministic first, no flag skips it
|
|
95
|
+
|
|
96
|
+
`right-release check [--repo <dir>] [--config <file>] [--commit <rev> | --worktree] [--only <rule,...>] [--list] [--print-baseline] [--baseline <file>]`
|
|
97
|
+
is the offline app check, the same as `rightkit-gate check`: it reads files only (the committed tree of `--commit`, default HEAD, or with
|
|
98
|
+
`--worktree` the tree on disk, which is what a compile would see), runs every rule and prints every finding in one run. Exit 0 passes,
|
|
99
|
+
1 has findings, 2 could not run (a repository with no commit yet says `the repository has no commit yet; commit first`). The repository
|
|
100
|
+
is the git top-level of `--config`'s folder, else of `--app-root` (`recipe-build`), else of the working directory.
|
|
101
|
+
|
|
102
|
+
The commands that compile, package, sign, ship or inspect a release run it first, in the same process, and start nothing when it is
|
|
103
|
+
red or cannot run (outside a git repository it is red). There is no flag that skips it: `--help`, `--dry-run`, `--check` and `--list`
|
|
104
|
+
only change what a command does after the check.
|
|
105
|
+
|
|
106
|
+
| Mode | Commands |
|
|
107
|
+
|---|---|
|
|
108
|
+
| `commit` (HEAD: what is built, packaged, signed and uploaded) | `release`, `build` (also `build --web` and the direct-flag form `right-release --platform ...`), `package`, `web-build`, `swift-package`, `recipe-build`, `qa`, `upload`, `publish`, `publish-update`, `github`, `sign-windows`, `sign-updater`, `create-mac-updater`, `sign-candidate`, `addon build` |
|
|
109
|
+
| `worktree` (the tree on disk, as a compile sees it) | `dev`, `doctor`, `test`, `stage-sidecar`, `sign-dev-artifact` |
|
|
110
|
+
|
|
111
|
+
Not guarded: the report forms of guarded commands (`publish cargo|npm|swift`, `qa audit`, `doctor --all`) and every other command.
|
|
112
|
+
Each of those is listed with its reason in `UNCHECKED_COMMANDS` (`app-check.mjs`), and `app-check.test.mjs` fails a command that is in
|
|
113
|
+
neither list. `dmg` takes `--app`/`--out` paths, compiles nothing and has no repository; `addon` is checked only as `addon build` (it
|
|
114
|
+
runs the add-on's own build command), its `doctor`, `upload` and `adopt` forms compile nothing.
|
|
115
|
+
|
|
116
|
+
Known findings are tolerated only through a baseline: `--baseline <file>`, else the checked tree's `.rightkit/app-check-baseline.json`,
|
|
117
|
+
else (from a RightKit checkout only) `ci/app-check-baseline.json`. The app's own file is always `{ "schema": 1, "known": [{ "id", "issue",
|
|
118
|
+
"added", "sunset" }] }`; the central `{ "known": { "<repo>": [...] } }` shape in an app's file is refused (exit 2). Every app entry needs
|
|
119
|
+
an issue, an `added` date (YYYY-MM-DD, UTC, not later than today) and a `sunset` that is not before `added` and at most 90 days after
|
|
120
|
+
it. Past its sunset an entry stops tolerating its finding (the finding fails with `its baseline entry expired`) and is reported as
|
|
121
|
+
`EXPIRED`; a listed identity that no finding has any more is `STALE` and fails, so the list only shrinks. A rule the build refuses
|
|
122
|
+
whatever a baseline says (`switches-inventory`) fails even when listed.
|
|
123
|
+
|
|
94
124
|
## Declared test suites, web build, npm notices and artifact mirroring
|
|
95
125
|
|
|
96
126
|
These replace per-app wrapper scripts. Each is declared in `right-release.config.mjs` and runs without a shell from the app
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
// The files tier of the admission (admission.mjs): refusals decided by the release config and the committed files alone.
|
|
2
|
+
// Each row is the predicate of a refusal the build used to raise later, after the lock, the install or the compile; the
|
|
3
|
+
// message is the build's own, so the doctor, the build and the worker say the same thing. A row names where its twin lives.
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { assertGitHubRepository } from "./github-updater-release.mjs";
|
|
6
|
+
import { validateAndroidTarget } from "./android-release.mjs";
|
|
7
|
+
import { assertNsisInPlaceUpgradeContract } from "./nsis-upgrade-contract.mjs";
|
|
8
|
+
import { dirtyBuildInputs } from "./release-invocation.mjs";
|
|
9
|
+
import { packageCommandOptional } from "./project-type.mjs";
|
|
10
|
+
import { planMediaSidecar, validateMediaSidecarConfig } from "./media-sidecar.mjs";
|
|
11
|
+
import { loadSidecarCatalog } from "./media-sidecar-catalog.mjs";
|
|
12
|
+
|
|
13
|
+
export const WINDOWS_NSIS_SIGNING_CONTRACT = "windows-raw-exe-authenticode-before-nsis-v1";
|
|
14
|
+
export const DISTRIBUTION_PROVIDERS = Object.freeze(["r2-rightapps", "github-releases"]);
|
|
15
|
+
const SAFE_APP = /^[A-Za-z0-9][A-Za-z0-9._-]*$/;
|
|
16
|
+
const SAFE_VERSION = /^[A-Za-z0-9][A-Za-z0-9._+-]*$/;
|
|
17
|
+
// publish-update.mjs assertCurrentKey / assertVersionedKey: where the R2 objects of a private release live.
|
|
18
|
+
const CURRENT_UPDATER_KEY = /\/updates\/(?:mac|windows)\/current\//;
|
|
19
|
+
const CURRENT_INSTALLER_KEY = /\/installers\/(?:mac|windows)\/current\//;
|
|
20
|
+
const VERSIONED_INSTALLER_KEY = /\/installers\/(?:mac|windows)\/versions\/[^/]+\/[^/]+$/;
|
|
21
|
+
|
|
22
|
+
const isNsis = (ctx) => ctx.platform === "win" && ctx.target?.signingContract === WINDOWS_NSIS_SIGNING_CONTRACT;
|
|
23
|
+
const asList = (value) => (Array.isArray(value) ? value : []);
|
|
24
|
+
const app = (ctx) => ctx.config?.app ?? "app";
|
|
25
|
+
|
|
26
|
+
// build-release.mjs (the controller) and release.mjs (the worker) read the same "which file is the app's package.json" answer.
|
|
27
|
+
const workdirOf = (ctx) => path.resolve(ctx.root, ctx.config?.workdir ?? ".");
|
|
28
|
+
|
|
29
|
+
export const FILES_ROWS = Object.freeze([
|
|
30
|
+
{
|
|
31
|
+
// build-release.mjs: the config must expose a release identity that is safe to put in paths and ids.
|
|
32
|
+
id: "config-identity",
|
|
33
|
+
tier: "files",
|
|
34
|
+
needsTarget: false,
|
|
35
|
+
run({ config }) {
|
|
36
|
+
const found = [];
|
|
37
|
+
if (config?.schema !== 1) found.push(`unsupported config schema: ${config?.schema ?? "<missing>"} (expected 1)`);
|
|
38
|
+
if (!config?.app || !config?.version) found.push("release config must expose app and version");
|
|
39
|
+
else if (!SAFE_APP.test(config.app) || !SAFE_VERSION.test(config.version)) found.push("app and version must be safe release-id components");
|
|
40
|
+
return found;
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
// release.mjs `has no <platform> package command`; build-release.mjs said the same after the lock.
|
|
45
|
+
id: "package-command",
|
|
46
|
+
tier: "files",
|
|
47
|
+
run(ctx) {
|
|
48
|
+
if (ctx.platform === "android" || ctx.platform === "ios") return { na: "android and ios build without a package command" };
|
|
49
|
+
if (ctx.target.package || packageCommandOptional(ctx.target, ctx.projectType())) return undefined;
|
|
50
|
+
return [`${app(ctx)} has no ${ctx.platform} package command`];
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
// release-invocation.mjs resolveReleaseBuildInputs: the doctor reports it as its own check, the build as this row.
|
|
55
|
+
id: "build-inputs",
|
|
56
|
+
tier: "files",
|
|
57
|
+
doctorOwned: true,
|
|
58
|
+
run(ctx) {
|
|
59
|
+
const inputs = ctx.buildInputs();
|
|
60
|
+
return inputs.ok ? undefined : [inputs.error.message];
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
// The files that can change the packaged app are committed: the doctor reports it as its own check, the build as this row.
|
|
65
|
+
id: "dirty-inputs",
|
|
66
|
+
tier: "files",
|
|
67
|
+
doctorOwned: true,
|
|
68
|
+
after: ["build-inputs"],
|
|
69
|
+
run(ctx) {
|
|
70
|
+
const inputs = ctx.buildInputs();
|
|
71
|
+
if (!inputs.ok) return { skipped: "build-inputs is reported on its own" };
|
|
72
|
+
const dirty = dirtyBuildInputs(ctx.repoRoot, inputs.value.buildInputs, ctx.commit());
|
|
73
|
+
return dirty.length > 0 ? [`dirty files can change the packaged app; resolve them before release:\n${dirty.map((file) => `- ${file}`).join("\n")}`] : undefined;
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
// build-release.mjs hashInputs: two inputs may not share a sealed hash key, or one silently replaces the other.
|
|
78
|
+
id: "input-key-collision",
|
|
79
|
+
tier: "files",
|
|
80
|
+
after: ["build-inputs"],
|
|
81
|
+
run(ctx) {
|
|
82
|
+
const inputs = ctx.buildInputs();
|
|
83
|
+
if (!inputs.ok) return { skipped: "build-inputs is reported on its own" };
|
|
84
|
+
const seen = new Set();
|
|
85
|
+
const collided = new Set();
|
|
86
|
+
for (const file of inputs.value.requiredInputs) {
|
|
87
|
+
const key = path.relative(path.dirname(path.dirname(file)), file);
|
|
88
|
+
if (seen.has(key)) collided.add(key);
|
|
89
|
+
seen.add(key);
|
|
90
|
+
}
|
|
91
|
+
return [...collided].map((key) => `release inputs share the hash key ${key}; rename one so every input is sealed`);
|
|
92
|
+
},
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
// build-release.mjs assertNsisInPlaceUpgradeContract: the NSIS template that upgrades in place. The build judges it on the target
|
|
96
|
+
// as the config declares it, so a winPackage recipe (which declares no signingContract) is not held to it today.
|
|
97
|
+
id: "nsis-upgrade-contract",
|
|
98
|
+
tier: "files",
|
|
99
|
+
run(ctx) {
|
|
100
|
+
if (ctx.platform !== "win" || ctx.rawTarget?.signingContract !== WINDOWS_NSIS_SIGNING_CONTRACT) return { na: "not a Windows NSIS target declared in the config" };
|
|
101
|
+
try { assertNsisInPlaceUpgradeContract(ctx.root, ctx.rawTarget.nsisUpgradeContract); } catch (error) { return [error.message]; }
|
|
102
|
+
return undefined;
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
{
|
|
106
|
+
// release.mjs, after the installer is signed: the post-code-signing minisign needs updater.artifacts to sign.
|
|
107
|
+
id: "win-updater-artifacts",
|
|
108
|
+
tier: "files",
|
|
109
|
+
run(ctx) {
|
|
110
|
+
if (!isNsis(ctx)) return { na: "not a Windows NSIS target" };
|
|
111
|
+
return asList(ctx.target.updater?.artifacts).length > 0 ? undefined : [`${app(ctx)} win must declare updater.artifacts for post-code-signing minisign`];
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
// build-release.mjs sealRelease: what the seal needs from the config, evaluated before anything is compiled or signed.
|
|
116
|
+
id: "seal-routes",
|
|
117
|
+
tier: "files",
|
|
118
|
+
run(ctx) {
|
|
119
|
+
const { target, platform, config } = ctx;
|
|
120
|
+
const found = [];
|
|
121
|
+
const declared = asList(target.artifacts);
|
|
122
|
+
for (const entry of declared) {
|
|
123
|
+
const file = typeof entry === "string" ? entry : entry?.file;
|
|
124
|
+
if (typeof file !== "string" || !file) { found.push("target.artifacts entries must be paths or { file }"); break; }
|
|
125
|
+
}
|
|
126
|
+
const installers = asList(target.installer?.artifacts);
|
|
127
|
+
const updaters = asList(target.updater?.artifacts);
|
|
128
|
+
if (installers.length + updaters.length + declared.length === 0) found.push(`nothing to seal: declare installer.artifacts, updater.artifacts or artifacts for the ${platform} target`);
|
|
129
|
+
for (const entry of [...installers, ...updaters]) {
|
|
130
|
+
if (typeof entry?.file !== "string" || !entry.file) { found.push("installer and updater artifacts must each declare a file"); break; }
|
|
131
|
+
}
|
|
132
|
+
const minisign = updaters.length > 0 && target.updater?.kind !== "appinstaller";
|
|
133
|
+
if (minisign) {
|
|
134
|
+
const publicKey = config.distribution?.updaterPublicKey ?? config.distribution?.updaterPubkey ?? target.updater?.pubkey ?? target.updater?.publicKey;
|
|
135
|
+
if (typeof publicKey !== "string" || !publicKey.trim()) found.push("updater public key is required to verify updater artifacts before sealing");
|
|
136
|
+
for (const entry of updaters) {
|
|
137
|
+
if (typeof entry?.signature !== "string" || !entry.signature) found.push(`updater artifact ${entry?.file} must declare the signature file sign-updater writes next to it`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
const github = config.distribution?.provider === "github-releases";
|
|
141
|
+
for (const entry of updaters) {
|
|
142
|
+
if (entry?.key === undefined && !github) found.push(`updater artifact ${entry?.file} must declare the R2 route key (key); only github-releases names the route from the file`);
|
|
143
|
+
}
|
|
144
|
+
// The R2 keys publish-update.mjs checks at upload, tier by tier. Only a private release (r2-rightapps) is held to them. Both
|
|
145
|
+
// tiers check each updater's current key; only the patch tier checks the installer keys and where the patch updater lands, and
|
|
146
|
+
// it cannot run without installer.artifacts ("patch has no installer.artifacts"). The build is tier-neutral, so the patch
|
|
147
|
+
// checks apply exactly when the config makes the patch tier possible.
|
|
148
|
+
if (config.distribution?.provider === "r2-rightapps") {
|
|
149
|
+
const patchTier = installers.length > 0;
|
|
150
|
+
if (patchTier) {
|
|
151
|
+
for (const entry of installers) {
|
|
152
|
+
// publish-update.mjs assertCurrentKey tests the key as it is, so an installer with no key is refused with "got: undefined".
|
|
153
|
+
if (!CURRENT_INSTALLER_KEY.test(entry?.key)) found.push(`installer key must target the replaceable current R2 object, got: ${entry?.key}`);
|
|
154
|
+
if (entry?.versionedKey !== undefined && !VERSIONED_INSTALLER_KEY.test(entry.versionedKey)) found.push(`installer versionedKey must be <app>/installers/<os>/versions/<version>/<file>, got: ${entry.versionedKey}`);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const entry of updaters) {
|
|
158
|
+
if (typeof entry.key !== "string") continue;
|
|
159
|
+
if (!CURRENT_UPDATER_KEY.test(entry.key)) found.push(`updater key must target the replaceable current R2 object, got: ${entry.key}`);
|
|
160
|
+
if (!patchTier) continue;
|
|
161
|
+
// publish-update.mjs patchInstallerKey: the patch updater is uploaded as an installer, under patchKey or the updater key's installers twin.
|
|
162
|
+
const patchKey = entry.patchKey ? entry.patchKey : entry.key.includes("/updates/") ? entry.key.replace("/updates/", "/installers/") : null;
|
|
163
|
+
if (patchKey === null) found.push(`patch updater key must be convertible to installers/current, got: ${entry.key}`);
|
|
164
|
+
else if (!CURRENT_INSTALLER_KEY.test(patchKey)) found.push(`installer key must target the replaceable current R2 object, got: ${patchKey}`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
// The Android and iOS seals pin one file of target.artifacts (the AAB, the IPA).
|
|
168
|
+
const listed = declared.map((entry) => path.posix.normalize(String(typeof entry === "string" ? entry : entry?.file).replaceAll("\\", "/")));
|
|
169
|
+
if (platform === "android") {
|
|
170
|
+
let aab = null;
|
|
171
|
+
try { aab = validateAndroidTarget(target, { root: ctx.root }).aab?.file ?? null; } catch { /* the android-target check reports a bad target */ }
|
|
172
|
+
if (aab && !listed.includes(path.posix.normalize(aab.replaceAll("\\", "/")))) found.push("android seal requires targets.android.artifacts to list aab.file");
|
|
173
|
+
}
|
|
174
|
+
if (platform === "ios") {
|
|
175
|
+
const ipa = target.recipe?.spec?.ipa;
|
|
176
|
+
if (typeof ipa === "string" && !listed.includes(path.posix.normalize(ipa.replaceAll("\\", "/")))) found.push("ios seal requires targets.ios.artifacts to list iosPackage.ipa");
|
|
177
|
+
}
|
|
178
|
+
return found;
|
|
179
|
+
},
|
|
180
|
+
},
|
|
181
|
+
{
|
|
182
|
+
// publish-update.mjs: the provider the upload takes, for the win and mac releases it uploads (iOS and Android go through their own
|
|
183
|
+
// lanes, whose providers are app-store-connect and google-play). A config that declares no distribution is refused there too, as
|
|
184
|
+
// is a provider it does not know; preflight G16 refuses an unknown provider in the config text.
|
|
185
|
+
id: "distribution-provider",
|
|
186
|
+
tier: "files",
|
|
187
|
+
needsTarget: false,
|
|
188
|
+
run({ config, platform }) {
|
|
189
|
+
if (platform !== "win" && platform !== "mac") return { na: "publish-update.mjs uploads win and mac releases; other platforms upload through their own lane" };
|
|
190
|
+
const distribution = config.distribution;
|
|
191
|
+
const provider = distribution?.provider;
|
|
192
|
+
if (!DISTRIBUTION_PROVIDERS.includes(provider)) return [`release config distribution.provider must be ${DISTRIBUTION_PROVIDERS.join(" or ")}${provider === undefined ? "" : ` (got ${JSON.stringify(provider)})`}`];
|
|
193
|
+
if (provider === "github-releases") {
|
|
194
|
+
try { assertGitHubRepository(distribution.repository); } catch (error) { return [error.message]; }
|
|
195
|
+
}
|
|
196
|
+
return undefined;
|
|
197
|
+
},
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
// release.mjs frontendBundleDirs: the mock scan locates the frontend bundle from src-tauri/tauri.conf.json.
|
|
201
|
+
id: "frontend-config-readable",
|
|
202
|
+
tier: "files",
|
|
203
|
+
run(ctx) {
|
|
204
|
+
if (Array.isArray(ctx.target.webBundleScan)) return { na: "target.webBundleScan names the bundle directories" };
|
|
205
|
+
const confPath = path.join(ctx.root, "src-tauri", "tauri.conf.json");
|
|
206
|
+
if (!ctx.exists(confPath)) return { na: "no src-tauri/tauri.conf.json" };
|
|
207
|
+
try { JSON.parse(ctx.fs.readFileSync(confPath, "utf8")); } catch (error) {
|
|
208
|
+
return [`src-tauri/tauri.conf.json is unreadable, so the frontend bundle cannot be located for the mock scan: ${error.message}`];
|
|
209
|
+
}
|
|
210
|
+
return undefined;
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
{
|
|
214
|
+
// release-checks.mjs: every config.checks entry runs `<packageManager> run <entry>` after the install; a script that is not
|
|
215
|
+
// in package.json fails there, minutes in.
|
|
216
|
+
id: "declared-checks-exist",
|
|
217
|
+
tier: "files",
|
|
218
|
+
needsTarget: false,
|
|
219
|
+
run(ctx) {
|
|
220
|
+
const checks = asList(ctx.config.checks).filter((script) => script !== false);
|
|
221
|
+
if (checks.length === 0) return { na: "the config declares no checks" };
|
|
222
|
+
if (ctx.skipChecks) return { na: "the build was asked to skip the declared checks" };
|
|
223
|
+
const file = path.join(workdirOf(ctx), "package.json");
|
|
224
|
+
let scripts;
|
|
225
|
+
try { scripts = JSON.parse(ctx.fs.readFileSync(file, "utf8")).scripts ?? {}; } catch {
|
|
226
|
+
return [`config.checks declares ${checks.map((script) => JSON.stringify(script)).join(", ")} but ${file} is missing or unreadable`];
|
|
227
|
+
}
|
|
228
|
+
return checks.filter((script) => typeof script !== "string" || !Object.hasOwn(scripts, script))
|
|
229
|
+
.map((script) => `config.checks names ${JSON.stringify(script)}, which is not a script in ${path.relative(ctx.root, file).replaceAll("\\", "/") || "package.json"}`);
|
|
230
|
+
},
|
|
231
|
+
},
|
|
232
|
+
{
|
|
233
|
+
// release.mjs: a mediaSidecar or externalBin on macOS is verified inside the built .app, so the signing set must name one.
|
|
234
|
+
id: "sidecar-app-entry",
|
|
235
|
+
tier: "files",
|
|
236
|
+
run(ctx) {
|
|
237
|
+
if (ctx.platform !== "mac" || (ctx.target.mediaSidecar === undefined && ctx.target.externalBin === undefined)) return { na: "no mac sidecar" };
|
|
238
|
+
const hasApp = asList(ctx.target.sign?.prePackageFiles).some((entry) => /\.app\/?$/.test(typeof entry === "string" ? entry : String(entry?.file ?? "")));
|
|
239
|
+
if (hasApp) return undefined;
|
|
240
|
+
return [ctx.target.mediaSidecar !== undefined
|
|
241
|
+
? "mediaSidecar on macOS needs an .app entry in sign.prePackageFiles"
|
|
242
|
+
: "externalBin on macOS needs an .app entry in sign.prePackageFiles"];
|
|
243
|
+
},
|
|
244
|
+
},
|
|
245
|
+
{
|
|
246
|
+
// release.mjs prepareMediaSidecar: the sidecar plan is made from the catalog before anything is staged or compiled. The two parts
|
|
247
|
+
// the committed files decide: the app has a native Cargo manifest to resolve the catalog from, and the catalog the config names
|
|
248
|
+
// plans (a row for each tool, approved, with its digest and notices). A catalog read through `cargo metadata` is the host row
|
|
249
|
+
// media-sidecar-catalog. An invalid mediaSidecar config is the media-sidecar rule's finding, not this row's.
|
|
250
|
+
id: "media-sidecar-plan",
|
|
251
|
+
tier: "files",
|
|
252
|
+
after: ["build-inputs"],
|
|
253
|
+
run(ctx) {
|
|
254
|
+
if (ctx.target.mediaSidecar === undefined || (ctx.platform !== "mac" && ctx.platform !== "win")) return { na: "the target declares no mediaSidecar" };
|
|
255
|
+
let sidecar;
|
|
256
|
+
try { sidecar = validateMediaSidecarConfig(ctx.target.mediaSidecar, { platform: ctx.platform }); } catch { return { skipped: "an invalid mediaSidecar configuration is reported by the media-sidecar rule" }; }
|
|
257
|
+
const inputs = ctx.buildInputs();
|
|
258
|
+
if (!inputs.ok) return { skipped: "build-inputs is reported on its own" };
|
|
259
|
+
const layout = inputs.value.nativeLayout;
|
|
260
|
+
// Unreachable for iOS (mediaSidecar is mac and win only); a Cargo-less layout must still never be dereferenced.
|
|
261
|
+
if (!layout) return [`${app(ctx)} ${ctx.platform} mediaSidecar needs a native Cargo manifest to read the sidecar catalog, and this app has none`];
|
|
262
|
+
if (!sidecar.catalog) return { skipped: "the catalog is resolved through cargo metadata, which the media-sidecar-catalog host row asks" };
|
|
263
|
+
try {
|
|
264
|
+
const loaded = loadSidecarCatalog({ config: sidecar, root: ctx.root, manifestPath: layout.manifestPath, read: (file) => ctx.fs.readFileSync(file) });
|
|
265
|
+
planMediaSidecar({ config: sidecar, ...loaded, root: ctx.root });
|
|
266
|
+
} catch (error) { return [`${app(ctx)} ${ctx.platform} mediaSidecar: [${error.code ?? "ERROR"}] ${error.message}`]; }
|
|
267
|
+
return undefined;
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
{
|
|
271
|
+
// build-release.mjs runConfiguredReleaseCheck: the preSeal check runs a command.
|
|
272
|
+
id: "pre-seal-command",
|
|
273
|
+
tier: "files",
|
|
274
|
+
run(ctx) {
|
|
275
|
+
if (!ctx.target.preSeal) return { na: "no preSeal check" };
|
|
276
|
+
return typeof ctx.target.preSeal.cmd === "string" && ctx.target.preSeal.cmd.trim() ? undefined : ["release preSeal check must declare a command"];
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
]);
|