@rightkit/release 0.6.16 → 0.6.20
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 +7 -3
- package/admission-host.mjs +9 -5
- package/app-closure.mjs +9 -3
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +469 -102
- package/bin/rightkit-linux-setup.mjs +15 -0
- package/broker-admission.mjs +184 -0
- package/broker-wire.mjs +15 -3
- package/build-launch.mjs +4 -1
- package/build-ownership.mjs +125 -36
- package/build-release.mjs +39 -7
- package/cargo-contract.mjs +4 -1
- package/cargo-guard.mjs +25 -5
- package/cargo-target-shim.mjs +56 -7
- package/clippy-route.mjs +8 -7
- package/crate-graph.mjs +5 -1
- package/credential-provider.mjs +174 -0
- package/deterministic-scans.mjs +247 -40
- package/development.mjs +8 -0
- package/disk-policy.mjs +2 -2
- package/gate-executor.mjs +5 -3
- package/gate-stages.mjs +34 -3
- package/gradle-lock.mjs +206 -0
- package/heavy-command.mjs +33 -2
- package/linux-artifacts.mjs +2 -1
- package/linux-setup.mjs +106 -0
- package/local-publish.mjs +21 -10
- package/local-qualify.mjs +9 -3
- package/managed-cargo.mjs +54 -18
- package/ownership-lockfiles.mjs +14 -3
- package/ownership.mjs +5 -1
- package/package.json +17 -11
- package/protected-credentials.mjs +13 -5
- package/publication-gate.mjs +9 -4
- package/python-lock.mjs +209 -0
- package/qa-admission-policy.json +32 -0
- package/qa-layout.mjs +229 -0
- package/qa-runner.mjs +156 -0
- package/release-artifact-path.mjs +29 -3
- package/release-config.mjs +1 -1
- package/release-qa-command.mjs +58 -8
- package/release-qa-policy.mjs +5 -3
- package/release-source-identity.mjs +1 -0
- package/release.mjs +10 -2
- package/rightkit-ownership.json +1465 -79
- package/rightkit-registry-resolver.mjs +7 -3
- package/rightkit-versions.json +173 -36
- package/runtime-budget-classes.json +4 -0
- package/runtime-budgets.mjs +5 -1
- package/shell-contract.mjs +1 -1
- package/snapshot-broker.mjs +107 -9
- package/snapshot-check.mjs +2 -0
- package/snapshot-request.mjs +20 -3
- package/snapshot.mjs +11 -4
- package/stage-commands.mjs +2 -1
- package/stage-ledger.mjs +15 -0
- package/stage-sidecar.mjs +17 -3
- package/suite-signing.mjs +40 -4
- package/swift-lock.mjs +334 -0
- package/test-command.mjs +13 -3
- package/tool-build.mjs +156 -0
- package/unit-gate.mjs +9 -2
- package/windows-identity-plan.mjs +119 -0
- package/windows-identity.mjs +136 -0
package/managed-cargo.mjs
CHANGED
|
@@ -125,12 +125,38 @@ function fileHead(file) {
|
|
|
125
125
|
catch { return ""; } finally { if (fd !== null) try { closeSync(fd); } catch { /* closed */ } }
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
/** True when `file` is a gate's own runtime cargo wrapper (cargo-target-shim.mjs installCargoShim writes it), not a toolchain cargo. */
|
|
129
|
+
export function isGateCargoWrapper(file, readHead = fileHead) {
|
|
130
|
+
const head = readHead(file);
|
|
131
|
+
return head.startsWith("#!/bin/sh\n") && head.includes("cargo-target-shim.mjs") && head.includes(" --map ");
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The absolute cargo a gate wraps: the first of `candidates` (a PATH lookup, in order) that is not another gate's runtime wrapper, or the
|
|
136
|
+
* toolchain cargo such a wrapper's map records (a lookup that returns only the first match finds only the wrapper).
|
|
137
|
+
* A gate started inside a scan of an outer gate finds that gate's wrapper first on PATH; wrapping it would send every call through
|
|
138
|
+
* the outer map, which binds none of this gate's workspaces (0.6.18 Linux qualify: RK_CARGO_UNBOUND_WORKSPACE "resolved (none)").
|
|
139
|
+
*/
|
|
140
|
+
export function realCargoOf(candidates, readHead = fileHead, readMap = (file) => JSON.parse(readFileSync(file, "utf8"))) {
|
|
141
|
+
for (const c of candidates) {
|
|
142
|
+
if (!path.isAbsolute(c)) continue;
|
|
143
|
+
if (!isGateCargoWrapper(c, readHead)) return c;
|
|
144
|
+
// A PATH lookup (`which`) names only the first match: the wrapper's own map records the toolchain cargo it wraps.
|
|
145
|
+
try {
|
|
146
|
+
const at = /--map '([^']+)'/.exec(readHead(c));
|
|
147
|
+
const wrapped = at ? readMap(at[1]).realCargo : null;
|
|
148
|
+
if (typeof wrapped === "string" && path.isAbsolute(wrapped) && !isGateCargoWrapper(wrapped, readHead)) return wrapped;
|
|
149
|
+
} catch { /* an unreadable map names nothing: keep looking */ }
|
|
150
|
+
}
|
|
151
|
+
return undefined;
|
|
152
|
+
}
|
|
153
|
+
|
|
128
154
|
/**
|
|
129
155
|
* On a direct (Linux) host: PATH without the directories whose `cargo` is a RustRight shim (the managed agent-bin, a configured
|
|
130
156
|
* RIGHTKIT_MANAGED_AGENT_BIN, or a script that routes through the broker, read 4 KiB at most), and the first real cargo left on it.
|
|
131
157
|
* Plain `cargo` then can only be a real toolchain; with none, a cargo launch fails as missing, never through a shim.
|
|
132
158
|
*/
|
|
133
|
-
export function directCargoPath({ env = process.env, exists = existsSync, readHead = fileHead, platform = process.platform } = {}) {
|
|
159
|
+
export function directCargoPath({ env = process.env, exists = existsSync, readHead = fileHead, platform = process.platform, gateWrappers = false } = {}) {
|
|
134
160
|
// `platform` only matters for a forced direct run on Windows (a recorded broker exception): ";" separators, backslash paths, cargo.exe.
|
|
135
161
|
const win = platform === "win32";
|
|
136
162
|
const norm = (d) => (win ? String(d).replaceAll("\\", "/") : d);
|
|
@@ -139,7 +165,10 @@ export function directCargoPath({ env = process.env, exists = existsSync, readHe
|
|
|
139
165
|
for (const dir of String(env.PATH ?? "").split(win ? ";" : ":").filter(Boolean)) {
|
|
140
166
|
const candidates = win ? ["cargo.exe", "cargo.cmd", "cargo.bat", "cargo"].map((n) => path.win32.join(dir, n)) : [path.posix.join(dir, "cargo")];
|
|
141
167
|
const found = candidates.find((c) => exists(c));
|
|
142
|
-
|
|
168
|
+
// `gateWrappers`: a gate resolving its OWN executor also skips another gate's runtime cargo wrapper (a gate started inside an outer gate's scan,
|
|
169
|
+
// 0.6.18/0.6.19 Linux qualify). A gate's children never pass it: inside a stage the wrapper first on PATH is the binding they must go through. The
|
|
170
|
+
// one other caller is test-support/toolchain-cargo.mjs, for a test's throwaway fixtures under the temp directory, never anything in the snapshot.
|
|
171
|
+
const isShim = Boolean(found) && (MANAGED_AGENT_BIN.test(norm(dir)) || (configured && path.posix.resolve(norm(dir)) === configured) || /cargo-guard\.mjs|rustright/i.test(readHead(found)) || (gateWrappers && isGateCargoWrapper(found, readHead)));
|
|
143
172
|
if (isShim) { dropped.push(dir); continue; }
|
|
144
173
|
kept.push(dir);
|
|
145
174
|
if (!cargo && found) cargo = found;
|
|
@@ -201,28 +230,35 @@ function carriedSeam(args, options, { platform, spawn }) {
|
|
|
201
230
|
return runCarriedCargo(args, options, { map: carried.map, spawn });
|
|
202
231
|
}
|
|
203
232
|
|
|
233
|
+
/**
|
|
234
|
+
* The ONE route a call takes to the broker's client on a broker host (runCargoSync and cargo-guard both use it): the declared Linux route when the host declared one (only when it
|
|
235
|
+
* validates; an invalid one is `{ refusal }`, never a fallback to another client), else the installed runtime client with its discovered socket (on the Mac a missing socket is
|
|
236
|
+
* `{ refusal: "RightKit broker socket is unavailable" }`), else null. `{ client, env, mapUnavailable }` runs as `<client> cargo <args>`; `mapUnavailable` says a RK_BROKER_UNAVAILABLE
|
|
237
|
+
* answer of the client is to be reported as an unavailable broker (the declared route, and the Mac).
|
|
238
|
+
*/
|
|
239
|
+
export function brokerClientRoute({ env = process.env, platform = process.platform, exists = existsSync, fsOps, linuxRoute = resolveLinuxBrokerRoute, installedClient = managedRuntimeClient, socketOf = discoverBrokerSocket } = {}) {
|
|
240
|
+
const linux = linuxRoute({ env, platform, fsOps });
|
|
241
|
+
if (linux.declared) return linux.ok ? { client: linux.client, env: { ...env, RUSTRIGHT_BUILD_BROKER_SOCKET: linux.socket }, mapUnavailable: true } : { refusal: linux.problem };
|
|
242
|
+
const client = installedClient(env, platform, exists);
|
|
243
|
+
if (!client) return null;
|
|
244
|
+
const socket = socketOf(env, { platform, exists });
|
|
245
|
+
if (platform === 'darwin' && (!socket || !exists(socket))) return { refusal: 'RightKit broker socket is unavailable' };
|
|
246
|
+
return { client, env: socket ? { ...env, RUSTRIGHT_BUILD_BROKER_SOCKET: socket } : env, mapUnavailable: platform === 'darwin' };
|
|
247
|
+
}
|
|
248
|
+
|
|
204
249
|
export function runCargoSync(args, options = {}, { platform = process.platform, exists = existsSync, spawn = spawnSync, fsOps } = {}) {
|
|
205
250
|
const env = options.env ?? process.env;
|
|
206
251
|
const seam = carriedSeam(args, options, { platform, spawn });
|
|
207
252
|
if (seam) return seam;
|
|
208
253
|
// A gate run outside the broker by Adrian's recorded exception: the real toolchain, shims off PATH, on every platform (NO_BROKER_ENV).
|
|
209
254
|
if (gateNoBroker(env)) return spawn("cargo", args, { ...options, env: directCargoEnv(env, platform, exists) });
|
|
210
|
-
// A Linux host that declared a broker route: every call goes to the validated client at the declared endpoint, or is refused. Never direct.
|
|
211
|
-
const
|
|
212
|
-
if (
|
|
213
|
-
|
|
214
|
-
const
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
}
|
|
218
|
-
const client = managedRuntimeClient(env, platform, exists);
|
|
219
|
-
if (client) {
|
|
220
|
-
const socket = discoverBrokerSocket(env, { platform, exists });
|
|
221
|
-
if (platform === 'darwin' && (!socket || !exists(socket))) return unavailable('RightKit broker socket is unavailable');
|
|
222
|
-
const childEnv = socket ? { ...env, RUSTRIGHT_BUILD_BROKER_SOCKET: socket } : env;
|
|
223
|
-
const invocation = managedRuntimeCommand(client, ["cargo", ...args]);
|
|
224
|
-
const result = spawn(invocation.command, invocation.args, { ...options, env: childEnv });
|
|
225
|
-
if (platform === 'darwin' && result.status !== 0 && /RK_BROKER_UNAVAILABLE|RightKit broker (?:unavailable|endpoint is not configured)/i.test(String(result.stderr ?? ''))) {
|
|
255
|
+
// A Linux host that declared a broker route: every call goes to the validated client at the declared endpoint, or is refused. Never direct. Otherwise the managed client (brokerClientRoute).
|
|
256
|
+
const route = brokerClientRoute({ env, platform, exists, fsOps });
|
|
257
|
+
if (route?.refusal) return unavailable(route.refusal);
|
|
258
|
+
if (route) {
|
|
259
|
+
const invocation = managedRuntimeCommand(route.client, ["cargo", ...args]);
|
|
260
|
+
const result = spawn(invocation.command, invocation.args, { ...options, env: route.env });
|
|
261
|
+
if (route.mapUnavailable && result.status !== 0 && /RK_BROKER_UNAVAILABLE|RightKit broker (?:unavailable|endpoint is not configured)/i.test(String(result.stderr ?? ''))) {
|
|
226
262
|
return { ...result, error: unavailable(String(result.stderr).trim()).error };
|
|
227
263
|
}
|
|
228
264
|
return result;
|
package/ownership-lockfiles.mjs
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
// Stage 1: resolved lock graphs, without feature or production-only pruning.
|
|
2
|
-
import {
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
// smol-toml loads on first parse, not at import: CI jobs whose closure reaches this module without installing
|
|
4
|
+
// @rightkit/release dependencies (publish.yml crates pack, bind) never parse TOML here (rightkit#1341).
|
|
5
|
+
let smolTomlParse;
|
|
6
|
+
const parseToml = (text) => (smolTomlParse ??= createRequire(import.meta.url)("smol-toml").parse)(text);
|
|
3
7
|
import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
|
|
4
8
|
import path from "node:path";
|
|
5
9
|
import { sha256 } from "./ownership-approval.mjs";
|
|
@@ -281,8 +285,14 @@ function pnpmGraph(text, manifests, lockDir, workspaceBytes) {
|
|
|
281
285
|
if (typeof ref !== "string") fail("invalid pnpm dependency version");
|
|
282
286
|
if (/^(?:link:|workspace:)/.test(ref)) {
|
|
283
287
|
if (!ref.startsWith("link:")) throw new LockError("lockfile-unresolved-dependency", ref);
|
|
284
|
-
const importer = node.id.replace(/^importer:/, "");
|
|
285
|
-
|
|
288
|
+
const importer = node.id.replace(/^importer:/, ""), target = path.posix.normalize(path.posix.join(importer, ref.slice(5)));
|
|
289
|
+
const found = g.nodes.get(`importer:${target}`);
|
|
290
|
+
if (found) return found;
|
|
291
|
+
// A link to a directory of the scanned repository outside this lock's importers (RightKit's own sample apps link
|
|
292
|
+
// packages/*) is that repository's own source: a local node, never a gap. A link leaving the repository stays unresolved.
|
|
293
|
+
const repoRel = path.posix.normalize(path.posix.join(lockDir, target));
|
|
294
|
+
if (repoRel === ".." || repoRel.startsWith("../") || path.posix.isAbsolute(repoRel)) return undefined;
|
|
295
|
+
return g.nodes.get(`linked:${repoRel}`) ?? g.add(`linked:${repoRel}`, name, "0.0.0", `path:${repoRel}`, true);
|
|
286
296
|
}
|
|
287
297
|
const candidates = [ref, `${name}@${ref}`, `/${name}@${ref}`];
|
|
288
298
|
const exact = candidates.map((id) => g.nodes.get(id)).find(Boolean);
|
|
@@ -292,6 +302,7 @@ function pnpmGraph(text, manifests, lockDir, workspaceBytes) {
|
|
|
292
302
|
return g.nodes.get(normalized) ?? g.nodes.get(`/${normalized}`);
|
|
293
303
|
};
|
|
294
304
|
for (const node of g.nodes.values()) {
|
|
305
|
+
if (node.id.startsWith("linked:")) continue; // its own dependencies belong to its own manifest and lock
|
|
295
306
|
const isImporter = node.id.startsWith("importer:"), pkg = isImporter ? lock.importers[node.id.slice(9)] : snapshots[node.id];
|
|
296
307
|
for (const section of isImporter ? sections : ["dependencies", "optionalDependencies"]) {
|
|
297
308
|
if (pkg[section] !== undefined && !object(pkg[section])) fail("invalid pnpm dependency map");
|
package/ownership.mjs
CHANGED
|
@@ -2,7 +2,11 @@
|
|
|
2
2
|
import { isBlockingFinding } from "./ownership-verdict.mjs";
|
|
3
3
|
import { spawnSync } from "./background-process.mjs";
|
|
4
4
|
import { canonicalRepositoryIdentity, loadTrustedExternal, occurrenceHash, resolveKeyLogTrust, sha256, verifyProtectedProvenance } from "./ownership-approval.mjs";
|
|
5
|
-
import {
|
|
5
|
+
import { createRequire } from "node:module";
|
|
6
|
+
// smol-toml loads on first parse, not at import: CI jobs whose closure reaches this module without installing
|
|
7
|
+
// @rightkit/release dependencies (publish.yml crates pack, bind) never parse TOML here (rightkit#1341).
|
|
8
|
+
let smolTomlParse;
|
|
9
|
+
const parseToml = (text) => (smolTomlParse ??= createRequire(import.meta.url)("smol-toml").parse)(text);
|
|
6
10
|
import { sourceNonCode } from "./ownership-source.mjs";
|
|
7
11
|
import { OWNERSHIP_LOCKFILES, transitiveDependencies } from "./ownership-lockfiles.mjs";
|
|
8
12
|
import { ALLOWLIST_FILE, ALLOWLIST_FINDING_ID, evaluateDependency, sourceKind, validateAllowlist } from "./ownership-dependency-allowlist.mjs";
|
package/package.json
CHANGED
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rightkit/release",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.20",
|
|
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
9
|
"rightkit-gate": "bin/rightkit-gate.mjs",
|
|
10
|
+
"rightkit-linux-setup": "bin/rightkit-linux-setup.mjs",
|
|
10
11
|
"rightkit-agent-hook": "bin/rightkit-agent-hook.mjs"
|
|
11
12
|
},
|
|
12
13
|
"exports": {
|
|
13
14
|
"./bin/rightkit-agent-hook.mjs": "./bin/rightkit-agent-hook.mjs",
|
|
14
15
|
"./bin/rightkit-gate.mjs": "./bin/rightkit-gate.mjs",
|
|
16
|
+
"./bin/rightkit-linux-setup.mjs": "./bin/rightkit-linux-setup.mjs",
|
|
15
17
|
"./cargo-contract.mjs": "./cargo-contract.mjs",
|
|
16
18
|
"./cargo-target.mjs": "./cargo-target.mjs",
|
|
17
19
|
"./cli/right-release.mjs": "./cli/right-release.mjs",
|
|
@@ -23,6 +25,7 @@
|
|
|
23
25
|
"./engine-locator.mjs": "./engine-locator.mjs",
|
|
24
26
|
"./github-release.mjs": "./github-release.mjs",
|
|
25
27
|
"./legal-contract.mjs": "./legal-contract.mjs",
|
|
28
|
+
"./linux-setup.mjs": "./linux-setup.mjs",
|
|
26
29
|
"./macos-file-evidence.mjs": "./macos-file-evidence.mjs",
|
|
27
30
|
"./macos-notarization.mjs": "./macos-notarization.mjs",
|
|
28
31
|
"./macos-signing-identity.mjs": "./macos-signing-identity.mjs",
|
|
@@ -53,6 +56,9 @@
|
|
|
53
56
|
"rightkit-gate": [
|
|
54
57
|
"--version"
|
|
55
58
|
],
|
|
59
|
+
"rightkit-linux-setup": [
|
|
60
|
+
"--version"
|
|
61
|
+
],
|
|
56
62
|
"rightkit-agent-hook": {
|
|
57
63
|
"args": [],
|
|
58
64
|
"output": "{}"
|
|
@@ -80,14 +86,6 @@
|
|
|
80
86
|
"!provenance-real-receipt.fixture.json"
|
|
81
87
|
],
|
|
82
88
|
"sideEffects": false,
|
|
83
|
-
"scripts": {
|
|
84
|
-
"test": "node test-support/run-test-shards.mjs",
|
|
85
|
-
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
86
|
-
"test:signer:windows": "node sign-release-manifest.integration.mjs",
|
|
87
|
-
"prepublishOnly": "pnpm test && pnpm test:signer:windows && pnpm test:registry-parity",
|
|
88
|
-
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
89
|
-
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
90
|
-
},
|
|
91
89
|
"publishConfig": {
|
|
92
90
|
"registry": "https://registry.npmjs.org/",
|
|
93
91
|
"access": "public"
|
|
@@ -97,9 +95,17 @@
|
|
|
97
95
|
"url": "git+https://github.com/bogusyogi/rightkit.git",
|
|
98
96
|
"directory": "packages/release"
|
|
99
97
|
},
|
|
100
|
-
"packageManager": "pnpm@12.16.1",
|
|
101
98
|
"dependencies": {
|
|
102
99
|
"@rightkit/outdated": "0.3.0",
|
|
103
100
|
"smol-toml": "1.9.1"
|
|
101
|
+
},
|
|
102
|
+
"scripts": {
|
|
103
|
+
"test": "node test-support/run-test-shards.mjs --class=unit",
|
|
104
|
+
"test:broker": "node test-support/run-test-shards.mjs --class=broker",
|
|
105
|
+
"test:all": "node test-support/run-test-shards.mjs --class=all",
|
|
106
|
+
"test:registry-parity": "node registry-parity.mjs --allow-unpublished",
|
|
107
|
+
"test:signer:windows": "node sign-release-manifest.integration.mjs",
|
|
108
|
+
"doctor:all": "node --test right-suite-contract.test.mjs",
|
|
109
|
+
"verify:standalone": "node standalone-clone-verify.mjs"
|
|
104
110
|
}
|
|
105
|
-
}
|
|
111
|
+
}
|
|
@@ -3,6 +3,7 @@ import os from "node:os";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
5
|
import { isManagedHost, refuseManagedEnvSecrets } from "./managed-host-secrets.mjs";
|
|
6
|
+
import { CREDENTIAL_PROVIDER_INSTALL, updaterCredentialProvider } from "./credential-provider.mjs";
|
|
6
7
|
|
|
7
8
|
// Protected credential loaders for release sites that previously read only
|
|
8
9
|
// ambient environment variables. The storage pattern matches release-token.mjs:
|
|
@@ -35,13 +36,17 @@ export class ProtectedSecretMissingError extends Error {
|
|
|
35
36
|
|
|
36
37
|
// One-line command the operator runs once per host. macOS prompts for the value
|
|
37
38
|
// (-w with no argument); Windows prompts with Read-Host -AsSecureString.
|
|
38
|
-
|
|
39
|
+
// `provider` is the enrolled rightkit-credentials a Windows store command runs (execWithUpdaterCredential's file); unset, the bare executable name.
|
|
40
|
+
export function protectedSecretStoreCommand(name, platform = process.platform, { provider, env = process.env } = {}) {
|
|
39
41
|
const spec = PROTECTED_SECRETS[name];
|
|
40
42
|
if (!spec) throw new Error(`unknown protected secret: ${name}`);
|
|
41
43
|
if (platform === "darwin" && spec.darwinFile) return null;
|
|
42
44
|
if (platform === "darwin") return `security add-generic-password -a "$USER" -s ${spec.keychain} -U -w`;
|
|
43
45
|
if (platform === "win32") {
|
|
44
|
-
if (spec.providerExec
|
|
46
|
+
if (spec.providerExec && !provider && platform === process.platform) {
|
|
47
|
+
try { provider = updaterCredentialProvider({ env, hostPlatform: platform }).file; } catch { /* not enrolled: the hint names the bare executable; signing itself refuses with the install command */ }
|
|
48
|
+
}
|
|
49
|
+
if (spec.providerExec) return `$rightkitSecure = Read-Host -AsSecureString '${spec.label}'; $rightkitPointer = [Runtime.InteropServices.Marshal]::SecureStringToBSTR($rightkitSecure); try { [Runtime.InteropServices.Marshal]::PtrToStringBSTR($rightkitPointer) | & ${provider ? `'${String(provider).replaceAll("'", "''")}'` : "rightkit-credentials.exe"} set --namespace '${spec.keychain}' --account ([Environment]::UserName) --replace } finally { [Runtime.InteropServices.Marshal]::ZeroFreeBSTR($rightkitPointer) }`;
|
|
45
50
|
return `powershell -NoProfile -Command "$s = Read-Host -AsSecureString '${spec.label}'; $d = Join-Path $env:APPDATA 'RightKit'; New-Item -ItemType Directory -Force -Path $d | Out-Null; [IO.File]::WriteAllText((Join-Path $d '${spec.file}'), [Runtime.InteropServices.Marshal]::PtrToStringBSTR([Runtime.InteropServices.Marshal]::SecureStringToBSTR($s)))"`;
|
|
46
51
|
}
|
|
47
52
|
return null;
|
|
@@ -120,9 +125,11 @@ export function resolveEnvBackedSecret({ name, envName, env = process.env, crede
|
|
|
120
125
|
|
|
121
126
|
// M11 owns OS acquisition & secret lifetime. The release adapter supplies only
|
|
122
127
|
// non-secret identity plus an absolute native child executable and literal argv.
|
|
128
|
+
// The provider is the ENROLLED rightkit-credentials (credential-provider.mjs; installed by `rightkit-gate --to tool --unit cargo:rightkit-credentials`) on a Mac or
|
|
129
|
+
// Windows host, resolved and re-hashed here; the bare PATH name is used only where no managed data root exists (Linux, CI). `provider` names a file explicitly.
|
|
123
130
|
export function execWithUpdaterCredential(command, args, {
|
|
124
|
-
env = process.env, cwd = process.cwd(), run = spawnSync,
|
|
125
|
-
provider
|
|
131
|
+
env = process.env, cwd = process.cwd(), run = spawnSync, hostPlatform = process.platform, dataRoot,
|
|
132
|
+
provider,
|
|
126
133
|
account = os.userInfo().username,
|
|
127
134
|
} = {}) {
|
|
128
135
|
for (const key of Object.keys(env)) {
|
|
@@ -131,10 +138,11 @@ export function execWithUpdaterCredential(command, args, {
|
|
|
131
138
|
}
|
|
132
139
|
}
|
|
133
140
|
if (!path.isAbsolute(command)) throw new Error("credential provider child must be an absolute native executable");
|
|
141
|
+
provider ??= updaterCredentialProvider({ env, hostPlatform, dataRoot }).file;
|
|
134
142
|
const result = run(provider, ["exec", "--namespace", PROTECTED_SECRETS["updater-signing-key"].keychain,
|
|
135
143
|
"--account", account, "--env", "TAURI_SIGNING_PRIVATE_KEY", "--", command, ...args],
|
|
136
144
|
{ cwd, env: { ...env, TAURI_SIGNING_PRIVATE_KEY_PASSWORD: "" }, stdio: "inherit", shell: false, windowsHide: true });
|
|
137
|
-
if (result.error?.code === "ENOENT") throw new Error(
|
|
145
|
+
if (result.error?.code === "ENOENT") throw new Error(`OS credential provider unavailable: install rightkit-credentials >=0.2.2 on PATH before updater signing (a Mac or Windows host installs it with \`${CREDENTIAL_PROVIDER_INSTALL}\`)`);
|
|
138
146
|
return result;
|
|
139
147
|
}
|
|
140
148
|
|
package/publication-gate.mjs
CHANGED
|
@@ -177,7 +177,12 @@ export function validatePublicationEvidence({ expected = {}, evidence, artifactB
|
|
|
177
177
|
if (!SHA.test(String(commit ?? ""))) missing.push("expected commit is not a full 40-character SHA");
|
|
178
178
|
const defaults = requiredHosts(kind);
|
|
179
179
|
let hosts = defaults;
|
|
180
|
-
|
|
180
|
+
// GB-29 (rightkit#1341): a direct Linux qualification bundle proves its own Linux run; Mac and Windows are proved by the publish run's
|
|
181
|
+
// native verify jobs before upload. Only an explicit caller request with linux-direct evidence selects it; it never satisfies an upload.
|
|
182
|
+
if (expected.directLinux === true) {
|
|
183
|
+
if (evidence?.hostPolicy !== "linux-direct" || kind === "swift") missing.push("direct Linux qualification needs linux-direct cargo or npm evidence");
|
|
184
|
+
hosts = ["linux"];
|
|
185
|
+
} else if (expected.requiredHosts !== undefined) {
|
|
181
186
|
const requested = expected.requiredHosts;
|
|
182
187
|
if (!(Array.isArray(requested) && requested.length > 0 && new Set(requested).size === requested.length
|
|
183
188
|
&& Array.from(requested).every((host) => defaults.includes(host)))) {
|
|
@@ -197,7 +202,7 @@ export function validatePublicationEvidence({ expected = {}, evidence, artifactB
|
|
|
197
202
|
const runs = Array.isArray(evidence.runs) ? evidence.runs : [];
|
|
198
203
|
if (runs.some((run) => run?.batch !== undefined) && !SHA.test(String(tree ?? ""))) missing.push("expected tree is not a full 40-character SHA for batch evidence");
|
|
199
204
|
for (const host of hosts) {
|
|
200
|
-
const label = host === "darwin" ? "macOS" : "Windows";
|
|
205
|
+
const label = host === "darwin" ? "macOS" : host === "linux" ? "Linux" : "Windows";
|
|
201
206
|
const matching = runs.filter((run) => run?.host?.os === host);
|
|
202
207
|
if (!matching.length) { missing.push(`no ${label} host run receipt`); continue; }
|
|
203
208
|
// Every run on the host must qualify: a failing run is not masked by a passing run beside it.
|
|
@@ -213,7 +218,7 @@ export function validatePublicationEvidence({ expected = {}, evidence, artifactB
|
|
|
213
218
|
}
|
|
214
219
|
|
|
215
220
|
// Acquire local source identity and cleanliness, then use the same content validator as CI.
|
|
216
|
-
export function checkPublicationGate({ kind, name, version, packagePath, evidencePath, artifactBytes, git = defaultGit, readFile = (p) => fs.readFileSync(p, "utf8") }) {
|
|
221
|
+
export function checkPublicationGate({ kind, name, version, packagePath, evidencePath, artifactBytes, directLinux = false, git = defaultGit, readFile = (p) => fs.readFileSync(p, "utf8") }) {
|
|
217
222
|
const missing = [];
|
|
218
223
|
const head = git(["rev-parse", "HEAD"], packagePath);
|
|
219
224
|
if (!head || !SHA.test(head)) missing.push(`cannot read current commit SHA for ${packagePath}`);
|
|
@@ -230,7 +235,7 @@ export function checkPublicationGate({ kind, name, version, packagePath, evidenc
|
|
|
230
235
|
// Local publication retains its recorded single-host grant. CI callers own their policy separately.
|
|
231
236
|
const singleHost = (kind === "npm" || kind === "cargo") ? singleHostOfPolicy(evidence) : null;
|
|
232
237
|
const result = validatePublicationEvidence({ expected: { kind, name, version, commit: head, tree,
|
|
233
|
-
...(singleHost ? { requiredHosts: [singleHost] } : {}) }, evidence, artifactBytes });
|
|
238
|
+
...(directLinux ? { directLinux: true } : singleHost ? { requiredHosts: [singleHost] } : {}) }, evidence, artifactBytes });
|
|
234
239
|
for (const problem of result.missing) {
|
|
235
240
|
if (problem === `evidence commit ${evidence?.commit} != expected commit ${head}`) {
|
|
236
241
|
missing.push(`evidence commit ${evidence.commit} != current HEAD ${head} (${describeCommitMismatch(evidence.commit, head, packagePath, git)})`);
|
package/python-lock.mjs
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// File-only Python lock check (class A scan `python-lock`). Reads committed files in one directory and executes nothing (no pip, uv or poetry).
|
|
2
|
+
// * `*pins.txt` (and `requirements*.txt` compiled from a sibling `<stem>.in`): every entry must be an exact `name==version` pin; when the
|
|
3
|
+
// `<stem>.in` (or `<stem>.txt`) it was pinned from exists, each of its requirements must be satisfied by the pin of that package.
|
|
4
|
+
// * `requirements*.txt` or `pyproject.toml` beside a `uv.lock` / `poetry.lock`: each direct requirement must be satisfied by the locked
|
|
5
|
+
// version of that package, and a requirements file's unpinned entries are fine only where no lock exists (they are then the declaration).
|
|
6
|
+
// A requirement line the checker cannot read literally (URL, VCS, editable, unknown specifier) is a finding, never a silent pass. An entry with
|
|
7
|
+
// an environment marker is checked when its package is pinned and not required to be pinned (a lock may be for another platform).
|
|
8
|
+
|
|
9
|
+
export const PYTHON_LOCK_FILE = /(^|\/)(requirements[^/]*\.(txt|in)|[^/]*pins?\.txt|pyproject\.toml|uv\.lock|poetry\.lock)$/;
|
|
10
|
+
const join = (dir, f) => (dir === "" || dir === "." ? f : `${dir}/${f}`);
|
|
11
|
+
export const normalizeName = (n) => String(n).toLowerCase().replace(/[-_.]+/g, "-");
|
|
12
|
+
|
|
13
|
+
// ---- versions (PEP 440 subset) --------------------------------------------------------------------------------------------
|
|
14
|
+
const PEP440 = /^v?(\d+(?:\.\d+)*)(?:[-_.]?(a|b|rc|alpha|beta|c|pre|preview)[-_.]?(\d*))?(?:[-_.]?(?:post|rev|r)[-_.]?(\d*))?(?:[-_.]?dev[-_.]?(\d*))?(?:\+[A-Za-z0-9.]+)?$/i;
|
|
15
|
+
export function parsePyVersion(text) {
|
|
16
|
+
const m = PEP440.exec(String(text).trim());
|
|
17
|
+
if (!m) return null;
|
|
18
|
+
const rank = { a: 0, alpha: 0, b: 1, beta: 1, c: 2, rc: 2, pre: 2, preview: 2 };
|
|
19
|
+
return { rel: m[1].split(".").map(Number), pre: m[2] ? [rank[m[2].toLowerCase()], Number(m[3] || 0)] : null, post: m[4] !== undefined ? Number(m[4] || 0) : null, dev: m[5] !== undefined ? Number(m[5] || 0) : null, text: String(text).trim() };
|
|
20
|
+
}
|
|
21
|
+
export function comparePyVersions(a, b) {
|
|
22
|
+
for (let i = 0; i < Math.max(a.rel.length, b.rel.length); i++) { const x = a.rel[i] ?? 0; const y = b.rel[i] ?? 0; if (x !== y) return x < y ? -1 : 1; }
|
|
23
|
+
const key = (v) => [v.dev !== null && v.pre === null && v.post === null ? -1 : 0, v.pre ? 0 : 1, ...(v.pre ?? [0, 0]), v.post ?? -1, v.dev === null ? 1 : 0, v.dev ?? 0];
|
|
24
|
+
const ka = key(a); const kb = key(b);
|
|
25
|
+
for (let i = 0; i < ka.length; i++) if (ka[i] !== kb[i]) return ka[i] < kb[i] ? -1 : 1;
|
|
26
|
+
return 0;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Clauses of a specifier string ("" for any): [{ op, version, wildcard }] or null when not readable. */
|
|
30
|
+
export function parseSpecifier(spec) {
|
|
31
|
+
const text = String(spec).trim().replace(/^\(|\)$/g, "");
|
|
32
|
+
if (text === "") return [];
|
|
33
|
+
const out = [];
|
|
34
|
+
for (const part of text.split(",")) {
|
|
35
|
+
const m = /^\s*(===|==|!=|~=|>=|<=|>|<)\s*([^\s,]+?)(\.\*)?\s*$/.exec(part);
|
|
36
|
+
if (!m) return null;
|
|
37
|
+
if (m[3] && !["==", "!="].includes(m[1])) return null;
|
|
38
|
+
const version = parsePyVersion(m[2]);
|
|
39
|
+
if (!version) return null;
|
|
40
|
+
out.push({ op: m[1], version, wildcard: Boolean(m[3]), raw: part.trim() });
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** true when `version` (parsed) satisfies every clause. */
|
|
45
|
+
export function satisfies(version, clauses) {
|
|
46
|
+
return clauses.every(({ op, version: v, wildcard }) => {
|
|
47
|
+
const c = comparePyVersions(version, v);
|
|
48
|
+
switch (op) {
|
|
49
|
+
case "===": case "==": return wildcard ? v.rel.every((n, i) => (version.rel[i] ?? 0) === n) : c === 0;
|
|
50
|
+
case "!=": return wildcard ? !v.rel.every((n, i) => (version.rel[i] ?? 0) === n) : c !== 0;
|
|
51
|
+
case ">=": return c >= 0;
|
|
52
|
+
case "<=": return c <= 0;
|
|
53
|
+
case ">": return c > 0;
|
|
54
|
+
case "<": return c < 0;
|
|
55
|
+
case "~=": { if (c < 0 || v.rel.length < 2) return false; return v.rel.slice(0, -1).every((n, i) => (version.rel[i] ?? 0) === n); }
|
|
56
|
+
default: return false;
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ---- requirement lines ----------------------------------------------------------------------------------------------------
|
|
62
|
+
/** { reqs: [{ line, name, spec, clauses, marker, pinned }], problems: [{ line, text }] } from requirements-file text. */
|
|
63
|
+
export function parseRequirements(text) {
|
|
64
|
+
const reqs = []; const problems = [];
|
|
65
|
+
const lines = String(text).split(/\r?\n/);
|
|
66
|
+
for (let i = 0; i < lines.length; i++) {
|
|
67
|
+
const start = i + 1; let line = lines[i];
|
|
68
|
+
while (/\\\s*$/.test(line) && i + 1 < lines.length) { line = line.replace(/\\\s*$/, " ") + lines[++i]; }
|
|
69
|
+
line = line.replace(/(^|\s)#.*$/, "").trim();
|
|
70
|
+
if (!line) continue;
|
|
71
|
+
if (/^-(r|c|-requirement|-constraint)\b/.test(line) || /^--(index-url|extra-index-url|find-links|trusted-host|no-binary|only-binary|prefer-binary|pre|require-hashes|no-index|use-feature|prefer-binary)\b/.test(line) || /^-[ifz]\s/.test(line)) continue;
|
|
72
|
+
if (/^-e\b|^--editable\b|^-/.test(line)) { problems.push({ line: start, text: `option ${line.slice(0, 50)} cannot be read literally` }); continue; }
|
|
73
|
+
line = line.replace(/\s--hash[=\s]\S+/g, "").trim();
|
|
74
|
+
const [body, ...markerParts] = line.split(";");
|
|
75
|
+
const marker = markerParts.join(";").trim() || null;
|
|
76
|
+
if (/@|:\/\/|^\.{0,2}\//.test(body)) { problems.push({ line: start, text: `requirement ${body.trim().slice(0, 60)} is a URL, VCS or path reference, which cannot be read literally` }); continue; }
|
|
77
|
+
const m = /^([A-Za-z0-9][A-Za-z0-9._-]*)\s*(?:\[[^\]]*\])?\s*(.*)$/.exec(body.trim());
|
|
78
|
+
if (!m) { problems.push({ line: start, text: `requirement ${body.trim().slice(0, 60)} cannot be read literally` }); continue; }
|
|
79
|
+
const clauses = parseSpecifier(m[2]);
|
|
80
|
+
if (clauses === null) { problems.push({ line: start, text: `specifier ${m[2].slice(0, 60)} of ${m[1]} cannot be read literally` }); continue; }
|
|
81
|
+
const pinned = clauses.length === 1 && ["==", "==="].includes(clauses[0].op) && !clauses[0].wildcard;
|
|
82
|
+
reqs.push({ line: start, name: normalizeName(m[1]), display: m[1], spec: m[2].trim(), clauses, marker, pinned });
|
|
83
|
+
}
|
|
84
|
+
return { reqs, problems };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// ---- locks ----------------------------------------------------------------------------------------------------------------
|
|
88
|
+
/** Map name -> [versions] from a uv.lock / poetry.lock parsed as TOML (`[[package]]` name + version). */
|
|
89
|
+
export function lockVersions(doc) {
|
|
90
|
+
const map = new Map();
|
|
91
|
+
for (const p of Array.isArray(doc?.package) ? doc.package : []) {
|
|
92
|
+
if (typeof p?.name !== "string" || typeof p?.version !== "string") continue;
|
|
93
|
+
const k = normalizeName(p.name);
|
|
94
|
+
map.set(k, [...(map.get(k) ?? []), p.version]);
|
|
95
|
+
}
|
|
96
|
+
return map;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** PEP 508 strings or poetry specs from a parsed pyproject.toml -> [{ name, spec, marker, where }], plus problems. */
|
|
100
|
+
export function pyprojectRequirements(doc) {
|
|
101
|
+
const out = []; const problems = [];
|
|
102
|
+
const pep = (list, where) => { for (const s of Array.isArray(list) ? list : []) { if (typeof s !== "string") continue; const r = parseRequirements(s); r.reqs.forEach((q) => out.push({ name: q.name, display: q.display, clauses: q.clauses, spec: q.spec, marker: q.marker, where })); r.problems.forEach((p) => problems.push(`${where}: ${p.text}`)); } };
|
|
103
|
+
pep(doc?.project?.dependencies, "[project].dependencies");
|
|
104
|
+
for (const [k, v] of Object.entries(doc?.project?.["optional-dependencies"] ?? {})) pep(v, `[project.optional-dependencies].${k}`);
|
|
105
|
+
for (const [k, v] of Object.entries(doc?.["dependency-groups"] ?? {})) pep(Array.isArray(v) ? v.filter((x) => typeof x === "string") : [], `[dependency-groups].${k}`);
|
|
106
|
+
pep(doc?.tool?.uv?.["dev-dependencies"], "[tool.uv].dev-dependencies");
|
|
107
|
+
const poetry = (table, where) => {
|
|
108
|
+
for (const [name, raw] of Object.entries(table ?? {})) {
|
|
109
|
+
if (name.toLowerCase() === "python") continue;
|
|
110
|
+
const spec = typeof raw === "string" ? raw : typeof raw?.version === "string" ? raw.version : null;
|
|
111
|
+
if (spec === null) { if (!(raw && typeof raw === "object" && (raw.git || raw.path || raw.url))) problems.push(`${where}: ${name} has no literal version`); continue; }
|
|
112
|
+
const clauses = poetrySpec(spec);
|
|
113
|
+
if (clauses === null) problems.push(`${where}: ${name} specifier ${spec} cannot be read literally`);
|
|
114
|
+
else out.push({ name: normalizeName(name), display: name, clauses, spec, marker: raw?.markers ?? (raw?.python ? "python" : null), where });
|
|
115
|
+
}
|
|
116
|
+
};
|
|
117
|
+
poetry(doc?.tool?.poetry?.dependencies, "[tool.poetry.dependencies]");
|
|
118
|
+
poetry(doc?.tool?.poetry?.["dev-dependencies"], "[tool.poetry.dev-dependencies]");
|
|
119
|
+
for (const [g, v] of Object.entries(doc?.tool?.poetry?.group ?? {})) poetry(v?.dependencies, `[tool.poetry.group.${g}.dependencies]`);
|
|
120
|
+
return { reqs: out, problems };
|
|
121
|
+
}
|
|
122
|
+
/** Poetry version constraint -> clauses (caret and tilde expanded, bare version exact, `*` any); null when not readable. */
|
|
123
|
+
export function poetrySpec(spec) {
|
|
124
|
+
const t = String(spec).trim();
|
|
125
|
+
if (t === "*" || t === "") return [];
|
|
126
|
+
if (t.includes("||")) return null;
|
|
127
|
+
const clauses = [];
|
|
128
|
+
for (const part of t.split(",").map((x) => x.trim())) {
|
|
129
|
+
let m;
|
|
130
|
+
if ((m = /^\^\s*(\d+(?:\.\d+)*)$/.exec(part))) {
|
|
131
|
+
const r = m[1].split(".").map(Number); const at = r.findIndex((n) => n !== 0); const i = at < 0 ? r.length - 1 : at;
|
|
132
|
+
const upper = r.slice(0, i); upper.push(r[i] + 1);
|
|
133
|
+
clauses.push({ op: ">=", version: parsePyVersion(m[1]) }, { op: "<", version: parsePyVersion(upper.join(".")) });
|
|
134
|
+
} else if ((m = /^~=?\s*(\d+(?:\.\d+)*)$/.exec(part))) {
|
|
135
|
+
const r = m[1].split(".").map(Number); const upper = r.length === 1 ? [r[0] + 1] : [r[0], r[1] + 1];
|
|
136
|
+
clauses.push({ op: ">=", version: parsePyVersion(m[1]) }, { op: "<", version: parsePyVersion(upper.join(".")) });
|
|
137
|
+
} else if ((m = /^(\d+(?:\.\d+)*)$/.exec(part))) clauses.push({ op: "==", version: parsePyVersion(m[1]) });
|
|
138
|
+
else { const c = parseSpecifier(part); if (c === null || c.length !== 1) return null; clauses.push(c[0]); }
|
|
139
|
+
}
|
|
140
|
+
return clauses.every((c) => c.version) ? clauses : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// ---- the check ------------------------------------------------------------------------------------------------------------
|
|
144
|
+
const describe = (r) => `${r.display ?? r.name}${r.spec ? ` ${r.spec}` : ""}`;
|
|
145
|
+
function against(reqs, versionsOf, { file, source, kind }) {
|
|
146
|
+
const out = [];
|
|
147
|
+
for (const r of reqs) {
|
|
148
|
+
const have = versionsOf.get(r.name);
|
|
149
|
+
const at = `${file}${r.line ? `:${r.line}` : ""}${r.where ? ` ${r.where}` : ""}`;
|
|
150
|
+
if (!have || !have.length) { if (!r.marker) out.push(`${at}: ${describe(r)} has no ${kind} in ${source}`); continue; }
|
|
151
|
+
const parsed = have.map(parsePyVersion);
|
|
152
|
+
if (parsed.some((p) => p === null)) { out.push(`${source}: ${r.display ?? r.name} version ${have.join(", ")} is not a readable version`); continue; }
|
|
153
|
+
if (!parsed.some((p) => satisfies(p, r.clauses))) out.push(`${at}: ${describe(r)} is not satisfied by ${source}, which has ${have.join(", ")}`);
|
|
154
|
+
}
|
|
155
|
+
return out;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Findings for one directory. `names` = the directory's file names; `read(file)` takes repo-relative paths; `parseToml(text)` parses TOML.
|
|
160
|
+
*/
|
|
161
|
+
export function pythonLockProblems({ dir, names, read, parseToml }) {
|
|
162
|
+
const out = []; const has = (n) => names.includes(n);
|
|
163
|
+
const f = (n) => join(dir, n);
|
|
164
|
+
const isPins = (n) => /pins?\.txt$/.test(n);
|
|
165
|
+
const reqFiles = names.filter((n) => /^requirements[^/]*\.txt$/.test(n) && !isPins(n));
|
|
166
|
+
const pinsFiles = names.filter(isPins);
|
|
167
|
+
const toml = (file) => { try { return parseToml(read(file)); } catch (e) { out.push(`${file}: cannot be parsed as TOML (${String(e.message).split("\n")[0]})`); return null; } };
|
|
168
|
+
const parsedTxt = new Map();
|
|
169
|
+
const reqsOf = (n) => { if (!parsedTxt.has(n)) { const r = parseRequirements(read(f(n))); parsedTxt.set(n, r); } return parsedTxt.get(n); };
|
|
170
|
+
const needPins = (n, r) => { for (const q of r.reqs) if (!q.pinned) out.push(`${f(n)}:${q.line}: ${describe(q)} is not an exact == pin in a lock file`); };
|
|
171
|
+
const pinMap = (r) => { const m = new Map(); for (const q of r.reqs) if (q.pinned) m.set(q.name, [...(m.get(q.name) ?? []), q.clauses[0].version.text]); return m; };
|
|
172
|
+
const problems = (n, r) => r.problems.forEach((p) => out.push(`${f(n)}:${p.line}: ${p.text}`));
|
|
173
|
+
|
|
174
|
+
// pins files
|
|
175
|
+
for (const n of pinsFiles) {
|
|
176
|
+
const pins = reqsOf(n); problems(n, pins); needPins(n, pins);
|
|
177
|
+
const stem = n.replace(/[._-]?pins?\.txt$/, "");
|
|
178
|
+
for (const src of [`${stem}.in`, `${stem}.txt`]) {
|
|
179
|
+
if (src === n || !has(src) || isPins(src)) continue;
|
|
180
|
+
const direct = reqsOf(src); problems(src, direct);
|
|
181
|
+
out.push(...against(direct.reqs, pinMap(pins), { file: f(src), source: f(n), kind: "pin" }));
|
|
182
|
+
break;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// requirements*.txt
|
|
186
|
+
const locks = [["uv.lock"], ["poetry.lock"]].map(([n]) => n).filter(has);
|
|
187
|
+
const lockMaps = locks.map((n) => { const doc = toml(f(n)); return doc ? { n, map: lockVersions(doc) } : null; }).filter(Boolean);
|
|
188
|
+
for (const n of reqFiles) {
|
|
189
|
+
const stem = n.replace(/\.txt$/, ""); const r = reqsOf(n);
|
|
190
|
+
if (has(`${stem}.in`)) {
|
|
191
|
+
problems(n, r); needPins(n, r);
|
|
192
|
+
const direct = reqsOf(`${stem}.in`); problems(`${stem}.in`, direct);
|
|
193
|
+
out.push(...against(direct.reqs, pinMap(r), { file: f(`${stem}.in`), source: f(n), kind: "pin" }));
|
|
194
|
+
} else if (lockMaps.length) {
|
|
195
|
+
problems(n, r);
|
|
196
|
+
for (const l of lockMaps) out.push(...against(r.reqs, l.map, { file: f(n), source: f(l.n), kind: "locked version" }));
|
|
197
|
+
} else if (pinsFiles.some((p) => p.replace(/[._-]?pins?\.txt$/, "") === stem)) { /* its pins file is the lock (checked above) */ }
|
|
198
|
+
}
|
|
199
|
+
// pyproject.toml
|
|
200
|
+
if (has("pyproject.toml") && lockMaps.length) {
|
|
201
|
+
const doc = toml(f("pyproject.toml"));
|
|
202
|
+
if (doc) {
|
|
203
|
+
const p = pyprojectRequirements(doc);
|
|
204
|
+
out.push(...p.problems.map((t) => `${f("pyproject.toml")} ${t}`));
|
|
205
|
+
for (const l of lockMaps) out.push(...against(p.reqs, l.map, { file: f("pyproject.toml"), source: f(l.n), kind: "locked version" }));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return out;
|
|
209
|
+
}
|
package/qa-admission-policy.json
CHANGED
|
@@ -9,6 +9,38 @@
|
|
|
9
9
|
"set_by": "adrian",
|
|
10
10
|
"since": "2026-10-27",
|
|
11
11
|
"note": "Dell runner, Adrian's go 2026-10-28, sha256 3c11788e…2986"
|
|
12
|
+
},
|
|
13
|
+
{
|
|
14
|
+
"sha256": "569069fee0feb5f9d60e75ef8a5825fbff44edbffd5aa927c25d4032e9dca7a0",
|
|
15
|
+
"version": "0.10.1",
|
|
16
|
+
"platform": "win",
|
|
17
|
+
"set_by": "adrian",
|
|
18
|
+
"since": "2026-11-01",
|
|
19
|
+
"note": "Dell runner built by rightkit-gate --to tool from e2f2d2152 (world QA root fix) on BOGUS-DELL, rotated in; replaces 9cb78352 (rightkit#1309: pins/trust routine)"
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
"sha256": "974b646b50f4630ff829c96e3747f800d17cf5125667ead0fa43ee809de4781f",
|
|
23
|
+
"version": "0.10.0",
|
|
24
|
+
"platform": "win",
|
|
25
|
+
"set_by": "adrian",
|
|
26
|
+
"since": "2026-11-01",
|
|
27
|
+
"note": "Dell runner built and enrolled by rightkit-gate --to tool from 05eb0d689 on BOGUS-DELL (rightkit#1309: pins/trust routine)"
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"sha256": "a1e84a19d1b75d49a98fbff34e4ac1001014ff0aafcd129ce23721eb3297e4b3",
|
|
31
|
+
"version": "0.10.0",
|
|
32
|
+
"platform": "mac",
|
|
33
|
+
"set_by": "adrian",
|
|
34
|
+
"since": "2026-10-31",
|
|
35
|
+
"note": "Mac runner built by rightkit-gate --to tool from 24c5a9281 (rightkit#1309: pins/trust routine)"
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
"sha256": "14eba841b7af8f8b015d25be6dbf68ad3c96863a4c9c43adc104b1ab632f9f60",
|
|
39
|
+
"version": "0.10.1",
|
|
40
|
+
"platform": "mac",
|
|
41
|
+
"set_by": "adrian",
|
|
42
|
+
"since": "2026-11-01",
|
|
43
|
+
"note": "Mac runner built by rightkit-gate --to tool from e8da5a7b7 (world token + temp-length fixes), rotated in; replaces e7590e12 (rightkit#1309: pins/trust routine)"
|
|
12
44
|
}
|
|
13
45
|
]
|
|
14
46
|
}
|