@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
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
// Ownership class rules for the app check (plan items 0.5 and 0.6). Code is placed by class, never by consumer count:
|
|
2
|
+
// foundation a row of ownership-mandatory-set.json for the app's profile: the app must use its owner;
|
|
3
|
+
// add-on any other catalogued capability: RightKit-owned from its first consumer;
|
|
4
|
+
// app-specific product domain logic and presentation only.
|
|
5
|
+
// Non-product code stays in an app only under an approved, expiring entry of .rightkit/ownership-exceptions.json that names the
|
|
6
|
+
// RightKit issue that moves it. These two rules run wherever the app check runs (`rightkit-gate check`, `right-release check`, the
|
|
7
|
+
// pre-push hook, the start of every checked right-release command and RightKit's own preflight):
|
|
8
|
+
//
|
|
9
|
+
// mandatory-foundation OW-05 an app whose profile requires a foundation owner the app does not declare as a dependency (missing,
|
|
10
|
+
// or dev-only), or that selects an unknown or unratified profile. An app is a repository whose root
|
|
11
|
+
// holds its declaration, rightkit-mandatory.json, or that ships a desktop app: a root
|
|
12
|
+
// right-release.config.(mjs|json), or one beside a src-tauri/tauri.conf.json; when that directory
|
|
13
|
+
// holds its own declaration (RightKit's desktop-app-template), its rulings are checked here and its
|
|
14
|
+
// owners when the directory is checked as its own root (no TOML parser is needed before install). An app without the
|
|
15
|
+
// declaration is a finding, so a missing declaration is never an exemption. A row the app declares not applicable is
|
|
16
|
+
// excused only by a ratified ruling: appAmendments.<app>.notApplicable.<row> for the `app` the
|
|
17
|
+
// declaration names and its profile, with the reason copied verbatim; any other not-applicable row is
|
|
18
|
+
// a finding and the row still applies (rightkit#1140). Owner-pending rows never fail. Below-required versions
|
|
19
|
+
// and path/git overrides stay with `right-release ownership-mandatory --enforce` (versions move with
|
|
20
|
+
// every publish). Findings carry an identity, so an app may ratchet them in its baseline.
|
|
21
|
+
// ownership-exceptions OW-06 every committed .rightkit/ownership-exceptions.json: { schema?: 1, generated?, exceptions: [...] };
|
|
22
|
+
// each entry holds only the known keys, with capability, package (or crate), reason, owner,
|
|
23
|
+
// approvedBy, a real `date` not later than today, a real `expires` not before `date`, and `issue`, a
|
|
24
|
+
// RightKit issue (`<owner>/rightkit#<n>` or https://github.com/<owner>/rightkit/issues/<n>). Schema
|
|
25
|
+
// findings carry an identity (an adoption ratchet, bounded by the baseline's 90-day sunset); an entry
|
|
26
|
+
// past its `expires` date is a finding with no identity, so no baseline can silence an expiry. Each
|
|
27
|
+
// entry must also match a ratified ruling, appAmendments.<app>.exceptions in ownership-mandatory-set.json
|
|
28
|
+
// for the `app` the root rightkit-mandatory.json names: same capability, package (or crate), path when
|
|
29
|
+
// the ruling names one, and RightKit issue, expiring no later than the ruling. `approvedBy` alone is
|
|
30
|
+
// text the app writes; an entry no ruling covers is a finding.
|
|
31
|
+
import fs from "node:fs";
|
|
32
|
+
import path from "node:path";
|
|
33
|
+
|
|
34
|
+
const readText = (root, rel) => { try { return fs.readFileSync(path.join(root, rel), "utf8"); } catch { return null; } };
|
|
35
|
+
const readJson = (root, rel) => { const text = readText(root, rel); try { return text === null ? null : JSON.parse(text); } catch { return null; } };
|
|
36
|
+
|
|
37
|
+
// The mandatory-set evaluator parses Cargo.toml (smol-toml). It is loaded only when a checked tree is an app, so a copy of the check
|
|
38
|
+
// without it still runs every other rule; an app whose check cannot load it fails the rule ("the rule itself failed").
|
|
39
|
+
let mandatorySet = null;
|
|
40
|
+
let mandatorySetError = null;
|
|
41
|
+
try { mandatorySet = await import("./ownership-mandatory-set.mjs"); } catch (error) { mandatorySetError = error; }
|
|
42
|
+
|
|
43
|
+
export const APP_MARKERS = Object.freeze(["rightkit-mandatory.json"]);
|
|
44
|
+
export const APP_DECLARATION_FILE = "rightkit-mandatory.json";
|
|
45
|
+
const RELEASE_CONFIG = /^(?:(.+)\/)?right-release\.config\.(?:mjs|json)$/;
|
|
46
|
+
const SKIPPED_TREE = /(^|\/)(node_modules|vendor|target)\//;
|
|
47
|
+
const MANDATORY_DATA = new URL("./ownership-mandatory-set.json", import.meta.url);
|
|
48
|
+
/** The mandatory-set data, read as JSON (no TOML parser needed), or null when it cannot be read. */
|
|
49
|
+
export function mandatoryData() { try { return JSON.parse(fs.readFileSync(MANDATORY_DATA, "utf8")); } catch { return null; } }
|
|
50
|
+
export const OWNERSHIP_LEDGER_FILE = ".rightkit/ownership-exceptions.json";
|
|
51
|
+
export const LEDGER_TOP_KEYS = Object.freeze(["schema", "generated", "exceptions"]);
|
|
52
|
+
export const LEDGER_ENTRY_KEYS = Object.freeze(["capability", "package", "crate", "path", "fingerprint", "reason", "owner", "approvedBy", "date", "expires", "issue"]);
|
|
53
|
+
const RIGHTKIT_ISSUE = /^(?:https:\/\/github\.com\/([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/rightkit\/issues\/([1-9]\d*)|([A-Za-z0-9](?:[A-Za-z0-9-]{0,38}))\/rightkit#([1-9]\d*))$/i;
|
|
54
|
+
const todayOf = (now) => (now instanceof Date ? now : new Date()).toISOString().slice(0, 10);
|
|
55
|
+
|
|
56
|
+
/** True when `text` is a real calendar date written YYYY-MM-DD (UTC). */
|
|
57
|
+
export function isRealDate(text) {
|
|
58
|
+
if (typeof text !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(text)) return false;
|
|
59
|
+
const parsed = new Date(`${text}T00:00:00Z`);
|
|
60
|
+
return !Number.isNaN(parsed.getTime()) && parsed.toISOString().slice(0, 10) === text;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** The RightKit issue an `issue` field names, as `<owner>/rightkit#<n>`, or null when it names no RightKit issue. */
|
|
64
|
+
export function rightkitIssue(text) {
|
|
65
|
+
const m = typeof text === "string" ? RIGHTKIT_ISSUE.exec(text.trim()) : null;
|
|
66
|
+
return m ? `${m[1] ?? m[3]}/rightkit#${m[2] ?? m[4]}` : null;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const nonEmpty = (value) => typeof value === "string" && value.trim().length > 0;
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Validate one ledger entry. Returns { problems: [string], expired: boolean }; `expired` is set only for an otherwise dated entry
|
|
73
|
+
* whose `expires` is before `today` (YYYY-MM-DD, UTC).
|
|
74
|
+
*/
|
|
75
|
+
export function validateLedgerEntry(entry, { today = todayOf() } = {}) {
|
|
76
|
+
const problems = [];
|
|
77
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry)) return { problems: ["is not an object"], expired: false };
|
|
78
|
+
const unknown = Object.keys(entry).filter((key) => !LEDGER_ENTRY_KEYS.includes(key));
|
|
79
|
+
if (unknown.length) problems.push(`unknown key(s) ${unknown.join(", ")} (allowed: ${LEDGER_ENTRY_KEYS.join(", ")})`);
|
|
80
|
+
for (const key of ["capability", "reason", "owner", "approvedBy"]) if (!nonEmpty(entry[key])) problems.push(`missing ${key}`);
|
|
81
|
+
if (entry.package !== undefined && entry.crate !== undefined) problems.push("names both package and crate; keep one");
|
|
82
|
+
else if (!nonEmpty(entry.package ?? entry.crate)) problems.push("missing package (or crate)");
|
|
83
|
+
for (const key of ["path", "fingerprint"]) if (entry[key] !== undefined && !nonEmpty(entry[key])) problems.push(`${key} must be a non-empty string when present`);
|
|
84
|
+
const dated = isRealDate(entry.date);
|
|
85
|
+
if (!dated) problems.push("date must be a real date, YYYY-MM-DD (UTC)");
|
|
86
|
+
else if (entry.date > today) problems.push(`date ${entry.date} is later than today (${today}, UTC)`);
|
|
87
|
+
const expiring = isRealDate(entry.expires);
|
|
88
|
+
if (!expiring) problems.push("expires must be a real date, YYYY-MM-DD (UTC): every exception expires");
|
|
89
|
+
else if (dated && entry.expires < entry.date) problems.push(`expires ${entry.expires} is before date ${entry.date}`);
|
|
90
|
+
if (entry.issue === undefined) problems.push("missing issue: the RightKit issue that moves this code into RightKit");
|
|
91
|
+
else if (!rightkitIssue(entry.issue)) problems.push(`issue ${JSON.stringify(entry.issue)} is not a RightKit issue (<owner>/rightkit#<n> or https://github.com/<owner>/rightkit/issues/<n>)`);
|
|
92
|
+
return { problems, expired: expiring && entry.expires < today };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Validate one ledger document (parsed JSON, or null when unparseable). Returns [{ problem, fix, subject?, expired? }]:
|
|
97
|
+
* a schema finding carries a `subject`; an expired entry carries `expired: true` and no subject.
|
|
98
|
+
*/
|
|
99
|
+
export function validateLedger(doc, { today = todayOf() } = {}) {
|
|
100
|
+
if (!doc || typeof doc !== "object" || Array.isArray(doc)) return [{ problem: "is not a JSON object { exceptions: [...] }", fix: "write { \"exceptions\": [] } and add approved, expiring entries", subject: "document" }];
|
|
101
|
+
const out = [];
|
|
102
|
+
const unknown = Object.keys(doc).filter((key) => !LEDGER_TOP_KEYS.includes(key));
|
|
103
|
+
if (unknown.length) out.push({ problem: `unknown top-level key(s) ${unknown.join(", ")} (allowed: ${LEDGER_TOP_KEYS.join(", ")})`, fix: "remove them", subject: "document" });
|
|
104
|
+
if (doc.schema !== undefined && doc.schema !== 1) out.push({ problem: `schema ${JSON.stringify(doc.schema)} is not 1`, fix: "set \"schema\": 1 or drop it", subject: "document" });
|
|
105
|
+
if (!Array.isArray(doc.exceptions)) { out.push({ problem: "exceptions must be an array", fix: "write \"exceptions\": []", subject: "document" }); return out; }
|
|
106
|
+
doc.exceptions.forEach((entry, index) => {
|
|
107
|
+
const label = entry && typeof entry === "object" && !Array.isArray(entry) ? `${entry.capability ?? "?"}/${entry.package ?? entry.crate ?? "?"}${entry.path ? ` ${entry.path}` : ""}` : "?";
|
|
108
|
+
const { problems, expired } = validateLedgerEntry(entry, { today });
|
|
109
|
+
if (problems.length) out.push({ problem: `entry #${index} (${label}): ${problems.join("; ")}`, fix: "every exception needs capability, package, reason, owner, approvedBy, date, expires and the RightKit issue that moves the code", subject: `#${index} ${label}` });
|
|
110
|
+
if (expired) out.push({ problem: `entry #${index} (${label}) expired on ${entry.expires} (today ${today}, UTC); an expired exception is a failure and no baseline lists it`, fix: `move the code into RightKit (${rightkitIssue(entry.issue) ?? "its RightKit issue"}) and remove the entry, or record a fresh approval with a new expires date`, expired: true });
|
|
111
|
+
});
|
|
112
|
+
return out;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* The ratified ruling that covers a ledger entry, or null. `rulings` is appAmendments.<app>.exceptions: [{ capability, package | crate,
|
|
117
|
+
* path?, issue, expires }]. A ruling with no `path` covers every path of its capability and package.
|
|
118
|
+
*/
|
|
119
|
+
export function rulingFor(entry, rulings) {
|
|
120
|
+
if (!entry || typeof entry !== "object" || !Array.isArray(rulings)) return null;
|
|
121
|
+
const issue = rightkitIssue(entry.issue);
|
|
122
|
+
return rulings.find((r) => r && r.capability === entry.capability && (r.package ?? r.crate) === (entry.package ?? entry.crate)
|
|
123
|
+
&& (r.path === undefined || r.path === entry.path) && issue !== null && rightkitIssue(r.issue)?.toLowerCase() === issue.toLowerCase()
|
|
124
|
+
&& isRealDate(r.expires) && isRealDate(entry.expires) && entry.expires <= r.expires) ?? null;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** The declaration as ownership-mandatory-set.mjs reads it (profile and not-applicable rows of 10+ characters), without its TOML parser. */
|
|
128
|
+
function declarationOf(appRoot, data) {
|
|
129
|
+
const doc = readJson(appRoot, data.appDeclarationFile) ?? {};
|
|
130
|
+
const notApplicable = {};
|
|
131
|
+
for (const [id, reason] of Object.entries(doc.notApplicable ?? {})) if (typeof reason === "string" && reason.trim().length >= 10) notApplicable[id] = reason.trim();
|
|
132
|
+
return { profile: doc.profile ?? data.defaultProfile, notApplicable, records: {}, problems: [] };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** The `app` a root declaration names, or null. */
|
|
136
|
+
export function declaredApp(root) {
|
|
137
|
+
const doc = readJson(root, APP_DECLARATION_FILE);
|
|
138
|
+
return doc && typeof doc === "object" && typeof doc.app === "string" && doc.app.trim() ? doc.app.trim() : null;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/** The ownership-exceptions rule: every committed ledger in the tree. */
|
|
142
|
+
export function ownershipExceptions({ root, files, now }, { data = mandatoryData() } = {}) {
|
|
143
|
+
const today = todayOf(now);
|
|
144
|
+
const app = declaredApp(root);
|
|
145
|
+
const amendment = app && data?.appAmendments ? data.appAmendments[app] : undefined;
|
|
146
|
+
const rulings = amendment && Array.isArray(amendment.exceptions) ? amendment.exceptions : [];
|
|
147
|
+
const ledgers = (files ?? []).filter((file) => file === OWNERSHIP_LEDGER_FILE || file.endsWith(`/${OWNERSHIP_LEDGER_FILE}`)).sort();
|
|
148
|
+
const out = [];
|
|
149
|
+
for (const file of ledgers) {
|
|
150
|
+
const text = readText(root, file);
|
|
151
|
+
let doc = null;
|
|
152
|
+
try { doc = JSON.parse(text); } catch (error) {
|
|
153
|
+
out.push({ file, subject: "document", problem: `${file} is not valid JSON (${error.message})`, fix: "repair the JSON" });
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
for (const finding of validateLedger(doc, { today })) {
|
|
157
|
+
const { subject, expired, ...rest } = finding;
|
|
158
|
+
out.push({ ...rest, problem: `${file}: ${rest.problem}`, ...(expired ? {} : { file, subject }) });
|
|
159
|
+
}
|
|
160
|
+
(Array.isArray(doc?.exceptions) ? doc.exceptions : []).forEach((entry, index) => {
|
|
161
|
+
if (!entry || typeof entry !== "object" || Array.isArray(entry) || rulingFor(entry, rulings)) return;
|
|
162
|
+
const label = `${entry.capability ?? "?"}/${entry.package ?? entry.crate ?? "?"}${entry.path ? ` ${entry.path}` : ""}`;
|
|
163
|
+
out.push({
|
|
164
|
+
file, subject: `#${index} ${label} ruling`,
|
|
165
|
+
problem: `${file}: entry #${index} (${label}) matches no ratified ruling: ${app ? `appAmendments.${app}.exceptions in ownership-mandatory-set.json has none with this capability, ${entry.crate !== undefined ? "crate" : "package"}, path, issue and an expiry no earlier than ${entry.expires}` : `${APP_DECLARATION_FILE} names no "app", so no ruling can apply`}`,
|
|
166
|
+
fix: `ratify the exception in RightKit (appAmendments.<app>.exceptions, with its issue and expiry) and name the app in ${APP_DECLARATION_FILE}, or move the code into RightKit and remove the entry`,
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
return out;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** The marker files that make the checked root an app, in APP_MARKERS order. */
|
|
174
|
+
export function appMarkers(root) {
|
|
175
|
+
return APP_MARKERS.filter((name) => readText(root, name) !== null);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* The release configs that make a tree an app even without its declaration: a root right-release.config.(mjs|json), or one whose
|
|
180
|
+
* directory also holds src-tauri/tauri.conf.json (a packaged desktop app).
|
|
181
|
+
*/
|
|
182
|
+
export function releaseAppMarkers(files) {
|
|
183
|
+
const tracked = new Set(files ?? []);
|
|
184
|
+
return [...tracked].filter((file) => {
|
|
185
|
+
const m = RELEASE_CONFIG.exec(file);
|
|
186
|
+
if (!m || SKIPPED_TREE.test(file)) return false;
|
|
187
|
+
return m[1] === undefined || tracked.has(`${m[1]}/src-tauri/tauri.conf.json`);
|
|
188
|
+
}).sort();
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/** Split declared not-applicable rows into those a ratified ruling covers and those it does not. */
|
|
192
|
+
export function ratifiedNotApplicable(declared, { app, profile, data }) {
|
|
193
|
+
const amendment = app && data?.appAmendments ? data.appAmendments[app] : undefined;
|
|
194
|
+
const rulings = amendment && amendment.profile === profile && amendment.notApplicable && typeof amendment.notApplicable === "object" ? amendment.notApplicable : {};
|
|
195
|
+
const ratified = {}, unratified = {};
|
|
196
|
+
for (const [id, reason] of Object.entries(declared ?? {})) {
|
|
197
|
+
if (typeof rulings[id] === "string" && rulings[id].trim() === reason.trim()) ratified[id] = reason;
|
|
198
|
+
else unratified[id] = reason;
|
|
199
|
+
}
|
|
200
|
+
return { ratified, unratified, ruled: Boolean(amendment) };
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* The findings of one app root (`dir` relative to the checked root, "" for the root itself). `owners: false` runs only the checks
|
|
205
|
+
* that need no TOML parser (the declaration parses; every not-applicable row is ratified): a nested app's foundation owners are
|
|
206
|
+
* checked when its own directory is checked, so RightKit's preflight, which runs before any install, can still see the rulings.
|
|
207
|
+
*/
|
|
208
|
+
function checkAppRoot({ root, dir, evaluator, data, owners = true }) {
|
|
209
|
+
const appRoot = dir ? path.join(root, dir) : root;
|
|
210
|
+
const declarationFile = data.appDeclarationFile;
|
|
211
|
+
const file = dir ? `${dir}/${declarationFile}` : declarationFile;
|
|
212
|
+
const out = [];
|
|
213
|
+
if (readJson(appRoot, declarationFile) === null) {
|
|
214
|
+
out.push({ file, subject: "declaration", problem: `${file} is not valid JSON, so the app's profile and not-applicable rows cannot be read`, fix: `repair ${file}` });
|
|
215
|
+
return out;
|
|
216
|
+
}
|
|
217
|
+
const declaration = (evaluator ?? { loadAppDeclaration: declarationOf }).loadAppDeclaration(appRoot, data);
|
|
218
|
+
const app = declaredApp(appRoot);
|
|
219
|
+
const { ratified, unratified, ruled } = ratifiedNotApplicable(declaration.notApplicable, { app, profile: declaration.profile, data });
|
|
220
|
+
for (const [row, reason] of Object.entries(unratified)) out.push({
|
|
221
|
+
file, subject: `notApplicable:${row}`,
|
|
222
|
+
problem: `notApplicable.${row} ("${reason.slice(0, 80)}${reason.length > 80 ? "..." : ""}") is not a ratified ruling: ${!app ? `${file} names no "app"` : !ruled ? `appAmendments has no entry for "${app}"` : `appAmendments.${app} (profile ${data.appAmendments[app].profile}) has no ruling for ${row} with this reason verbatim for profile ${declaration.profile}`}; the row still applies`,
|
|
223
|
+
fix: `ratify the ruling in RightKit (appAmendments.${app ?? "<app>"}.notApplicable.${row}, rightkit#1140) and copy its reason verbatim, or adopt the row's owner`,
|
|
224
|
+
});
|
|
225
|
+
if (!owners) return out;
|
|
226
|
+
const report = evaluator.checkApp(appRoot, { data, declaration: { ...declaration, notApplicable: ratified } });
|
|
227
|
+
for (const owner of report.owners) {
|
|
228
|
+
if (owner.status === "unknown-profile" || owner.status === "unratified") {
|
|
229
|
+
out.push({ file, subject: owner.owner, problem: `profile "${report.profile}" is ${owner.status === "unknown-profile" ? "not a profile of ownership-mandatory-set.json" : "not ratified (or has no rows)"}, so no foundation set can be checked`, fix: `select a ratified profile (${Object.entries(data.profiles).filter(([, p]) => p.ratified !== false).map(([name]) => name).join(", ")}) in ${file}` });
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
if (owner.status !== "missing") continue;
|
|
233
|
+
const devOnly = owner.evidence.some((line) => line.includes("dev-dependencies"));
|
|
234
|
+
out.push({
|
|
235
|
+
file, subject: owner.owner,
|
|
236
|
+
problem: `profile "${report.profile}" requires the foundation owner ${owner.owner} (${owner.ecosystem}, row${owner.rows.length > 1 ? "s" : ""} ${owner.rows.join(", ")}) and the app does not use it${devOnly ? " (declared only as a dev dependency)" : ""}`,
|
|
237
|
+
fix: `depend on ${owner.owner} >= ${owner.required} from the registry and use it, or, when the row truly does not apply, declare it under notApplicable in ${file} with the reason RightKit ratified for the app`,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
return out;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* The mandatory-foundation rule. The root declaration makes the whole tree one app. Without it, every release config that ships a
|
|
245
|
+
* desktop app (releaseAppMarkers) is an app: one whose own directory holds a declaration is checked from there; the others are one
|
|
246
|
+
* finding, the missing declaration (no profile is chosen for the app, so none of its rows can be excused or checked).
|
|
247
|
+
*/
|
|
248
|
+
export function mandatoryFoundation({ root, files }, { evaluator = mandatorySet } = {}) {
|
|
249
|
+
const markers = appMarkers(root);
|
|
250
|
+
const releaseMarkers = markers.length ? [] : releaseAppMarkers(files);
|
|
251
|
+
if (!markers.length && !releaseMarkers.length) return [];
|
|
252
|
+
const out = [];
|
|
253
|
+
const declared = [], undeclared = [];
|
|
254
|
+
for (const marker of releaseMarkers) {
|
|
255
|
+
const dir = marker.includes("/") ? marker.slice(0, marker.lastIndexOf("/")) : "";
|
|
256
|
+
(dir && readText(root, `${dir}/${APP_DECLARATION_FILE}`) !== null ? declared : undeclared).push(dir || marker);
|
|
257
|
+
}
|
|
258
|
+
if (undeclared.length) out.push({
|
|
259
|
+
file: APP_DECLARATION_FILE, subject: "declaration",
|
|
260
|
+
problem: `${undeclared.join(", ")} ships a desktop app, and the repository has no ${APP_DECLARATION_FILE}: an app without its declaration is never exempt from the mandatory foundation set`,
|
|
261
|
+
fix: `commit ${APP_DECLARATION_FILE} at the root (or beside the app's release config) with the app's "app" name, its "profile" and only the not-applicable rows ratified for it in appAmendments`,
|
|
262
|
+
});
|
|
263
|
+
if (!markers.length && !declared.length) return out;
|
|
264
|
+
if (markers.length) {
|
|
265
|
+
if (!evaluator) throw new Error(`ownership-mandatory-set.mjs could not be loaded: ${mandatorySetError?.message ?? "unknown error"}`);
|
|
266
|
+
return checkAppRoot({ root, dir: "", evaluator, data: evaluator.loadData() });
|
|
267
|
+
}
|
|
268
|
+
const data = mandatoryData();
|
|
269
|
+
if (!data) throw new Error("ownership-mandatory-set.json could not be read");
|
|
270
|
+
for (const dir of declared) out.push(...checkAppRoot({ root, dir, evaluator: null, data, owners: false }));
|
|
271
|
+
return out;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
export const OWNERSHIP_CLASS_RULES = Object.freeze([
|
|
275
|
+
{ id: "mandatory-foundation", gap: "OW-05", run: (ctx) => mandatoryFoundation(ctx) },
|
|
276
|
+
{ id: "ownership-exceptions", gap: "OW-06", run: ownershipExceptions },
|
|
277
|
+
]);
|
|
@@ -118,8 +118,10 @@ function persistCheckpoint(trust, bytes, file, audit) {
|
|
|
118
118
|
export function readCommittedKeyAuthority(trust) { return readAuthority(trust); }
|
|
119
119
|
function readAuthority(trust, { file, audit = auditPath } = {}) {
|
|
120
120
|
try {
|
|
121
|
-
|
|
121
|
+
// The authority's own shape is judged before anything host-specific: a malformed authority is "malformed" on every
|
|
122
|
+
// platform, not "unsupported platform" on the hosts that have no protected trust location (fixture scan-error-key-authority-malformed).
|
|
122
123
|
const expected = validateAuthority(trust?.keyAuthority);
|
|
124
|
+
file ??= trustedPath();
|
|
123
125
|
const top = String(git(expected.checkoutPath, ["rev-parse", "--show-toplevel"])).trim();
|
|
124
126
|
const normalizedTop = process.platform === "win32" ? path.resolve(top).toLowerCase() : path.resolve(top);
|
|
125
127
|
const normalizedExpected = process.platform === "win32" ? expected.checkoutPath.toLowerCase() : expected.checkoutPath;
|
|
@@ -301,10 +301,11 @@ export function pendingAppliesTo(row, active) {
|
|
|
301
301
|
return row.profile === "all" || (Array.isArray(row.profile) ? row.profile.includes(active) : row.profile === active);
|
|
302
302
|
}
|
|
303
303
|
|
|
304
|
-
|
|
304
|
+
// `declaration` replaces the one read from root (the app check passes the declaration with its unratified not-applicable rows removed).
|
|
305
|
+
export function checkApp(root, { data = loadData(), profile, amendments = [], declaration: override = null } = {}) {
|
|
305
306
|
const name = path.basename(root);
|
|
306
307
|
if (!existsSync(root) || !statSync(root).isDirectory()) return { app: name, root, scanned: false, profile: profile ?? data.defaultProfile, owners: [], pending: [], notApplicable: [], problems: ["app root missing"], violations: 0 };
|
|
307
|
-
const declaration = loadAppDeclaration(root, data);
|
|
308
|
+
const declaration = override ?? loadAppDeclaration(root, data);
|
|
308
309
|
const active = profile ?? declaration.profile;
|
|
309
310
|
const problems = [...declaration.problems];
|
|
310
311
|
if (!data.profiles[active]) problems.push(`unknown profile "${active}"`);
|
package/package.json
CHANGED
|
@@ -1,22 +1,63 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
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",
|
|
7
7
|
"bin": {
|
|
8
8
|
"right-release": "cli/right-release.mjs",
|
|
9
|
-
"rightkit-gate": "bin/rightkit-gate.mjs"
|
|
9
|
+
"rightkit-gate": "bin/rightkit-gate.mjs",
|
|
10
|
+
"rightkit-agent-hook": "bin/rightkit-agent-hook.mjs"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
"./bin/rightkit-agent-hook.mjs": "./bin/rightkit-agent-hook.mjs",
|
|
14
|
+
"./bin/rightkit-gate.mjs": "./bin/rightkit-gate.mjs",
|
|
15
|
+
"./cargo-contract.mjs": "./cargo-contract.mjs",
|
|
16
|
+
"./cargo-target.mjs": "./cargo-target.mjs",
|
|
17
|
+
"./cli/right-release.mjs": "./cli/right-release.mjs",
|
|
18
|
+
"./development.mjs": "./development.mjs",
|
|
19
|
+
"./direct-bootstrap.mjs": "./direct-bootstrap.mjs",
|
|
20
|
+
"./disk-policy.mjs": "./disk-policy.mjs",
|
|
21
|
+
"./dmg-build.mjs": "./dmg-build.mjs",
|
|
22
|
+
"./engine-locator.mjs": "./engine-locator.mjs",
|
|
23
|
+
"./github-release.mjs": "./github-release.mjs",
|
|
24
|
+
"./legal-contract.mjs": "./legal-contract.mjs",
|
|
25
|
+
"./macos-file-evidence.mjs": "./macos-file-evidence.mjs",
|
|
26
|
+
"./macos-notarization.mjs": "./macos-notarization.mjs",
|
|
27
|
+
"./macos-signing-identity.mjs": "./macos-signing-identity.mjs",
|
|
28
|
+
"./macos-signing-session.mjs": "./macos-signing-session.mjs",
|
|
29
|
+
"./managed-cargo.mjs": "./managed-cargo.mjs",
|
|
30
|
+
"./native-release-finalization.mjs": "./native-release-finalization.mjs",
|
|
31
|
+
"./notary-auth.mjs": "./notary-auth.mjs",
|
|
32
|
+
"./package.json": "./package.json",
|
|
33
|
+
"./packaged-app-identity.mjs": "./packaged-app-identity.mjs",
|
|
34
|
+
"./pe-content.mjs": "./pe-content.mjs",
|
|
35
|
+
"./publication-gate.mjs": "./publication-gate.mjs",
|
|
36
|
+
"./qa-build-features.mjs": "./qa-build-features.mjs",
|
|
37
|
+
"./release-dependencies.mjs": "./release-dependencies.mjs",
|
|
38
|
+
"./sign-windows.mjs": "./sign-windows.mjs",
|
|
39
|
+
"./signing-agent-client.mjs": "./signing-agent-client.mjs",
|
|
40
|
+
"./stage-binary.mjs": "./stage-binary.mjs",
|
|
41
|
+
"./stage-sidecar.mjs": "./stage-sidecar.mjs",
|
|
42
|
+
"./supply-chain-evidence.mjs": "./supply-chain-evidence.mjs",
|
|
43
|
+
"./tauri-bundle-marker.mjs": "./tauri-bundle-marker.mjs"
|
|
10
44
|
},
|
|
11
45
|
"rightkit": {
|
|
12
46
|
"publishCheck": {
|
|
47
|
+
"skipImport": [
|
|
48
|
+
"./sign-windows.mjs"
|
|
49
|
+
],
|
|
13
50
|
"binSmoke": {
|
|
14
51
|
"right-release": [
|
|
15
52
|
"--version"
|
|
16
53
|
],
|
|
17
54
|
"rightkit-gate": [
|
|
18
55
|
"--version"
|
|
19
|
-
]
|
|
56
|
+
],
|
|
57
|
+
"rightkit-agent-hook": {
|
|
58
|
+
"args": [],
|
|
59
|
+
"output": "{}"
|
|
60
|
+
}
|
|
20
61
|
}
|
|
21
62
|
}
|
|
22
63
|
},
|
|
@@ -26,6 +67,7 @@
|
|
|
26
67
|
"templates",
|
|
27
68
|
"*.mjs",
|
|
28
69
|
"*.json",
|
|
70
|
+
"deny-default.toml",
|
|
29
71
|
"*.sh",
|
|
30
72
|
"msix-repair-guard.ps1",
|
|
31
73
|
"bin/darwin/rightkit-swap",
|
|
@@ -48,6 +90,7 @@
|
|
|
48
90
|
"directory": "packages/release"
|
|
49
91
|
},
|
|
50
92
|
"dependencies": {
|
|
93
|
+
"@rightkit/outdated": "0.2.0",
|
|
51
94
|
"smol-toml": "1.9.1"
|
|
52
95
|
},
|
|
53
96
|
"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(" ")}`);
|