@rightkit/release 0.4.0 → 0.5.0
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 +59 -7
- package/adhoc-entitlements.mjs +8 -0
- package/adoption-evidence.mjs +650 -0
- package/advisory-exceptions.mjs +371 -0
- package/apple-profile-create.mjs +113 -10
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +1079 -0
- package/binary-license-notices.mjs +161 -0
- package/cargo-license-notices.mjs +111 -0
- package/cli/right-release.mjs +39 -0
- package/deterministic-scans.mjs +966 -0
- package/development-signing.mjs +2 -1
- package/host-upload-refusal.mjs +90 -0
- package/local-publish.mjs +142 -19
- package/local-qualify.mjs +728 -0
- package/local-runtime.mjs +319 -0
- package/lock-graph.mjs +51 -0
- package/managed-cargo.mjs +4 -1
- package/ownership-approval.mjs +23 -6
- package/ownership-dependency-allowlist.json +2 -0
- package/ownership-mandatory-set.json +158 -68
- package/ownership-mandatory-set.mjs +82 -6
- package/package.json +6 -2
- package/pipeline-admission.mjs +375 -0
- package/pipeline-config.mjs +100 -0
- package/pipeline-generators.json +4 -0
- package/pipeline-generators.mjs +137 -0
- package/pipeline-receipts.mjs +302 -0
- package/pipeline-scan.mjs +285 -0
- package/pipeline-tools.mjs +171 -0
- package/production-mode.mjs +42 -0
- package/publication-gate.mjs +94 -32
- package/publish-cargo.mjs +24 -7
- package/publish-swift.mjs +12 -3
- package/qa-control-scan.mjs +4 -0
- package/release-config.mjs +330 -0
- package/release-switch-scan.mjs +44 -5
- package/release.mjs +14 -4
- package/rightkit-ownership.json +57 -0
- package/rightkit-sync.mjs +8 -0
- package/rightkit-versions.json +191 -131
- package/runtime-budget-classes.json +8 -0
- package/runtime-budget-crates.mjs +14 -0
- package/runtime-budgets.mjs +128 -31
- package/scan-digest.mjs +107 -0
- package/sign-qa-bundle.mjs +210 -0
- package/stage-sidecar.mjs +99 -15
- package/swap-helper-bundle.mjs +7 -3
- package/workflow-yaml.mjs +177 -0
- package/bin/darwin/rightkit-swap.sign.json +0 -6
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
// M24 R3: the build, sign, notarize, package and upload tool inventory, and the text scanner that finds those tools in a pipeline
|
|
2
|
+
// context (an app script, a package hook, a workflow step, a framework hook, a build.rs). Pure functions: no file access.
|
|
3
|
+
//
|
|
4
|
+
// Scope (Astra, M24 R3): the tools below are tracked when a pipeline context or a helper reachable from one runs them. Read-only
|
|
5
|
+
// forms (`gh release view`, `codesign --verify`, `signtool verify`, `stapler validate`, `aws s3 ls`, ...) are not an upload or a
|
|
6
|
+
// signature and are never reported. This is a static text scan: it proves nothing about what a native tool does, and ordinary
|
|
7
|
+
// product runtime code (src/, crates) is never scanned by it, so there is no blanket ban on a product that happens to sign
|
|
8
|
+
// or package something at runtime.
|
|
9
|
+
|
|
10
|
+
/** The tool ids R3 inventories, with the pipeline stage each one acts in. */
|
|
11
|
+
export const PIPELINE_TOOL_IDS = Object.freeze([
|
|
12
|
+
"codesign", "notarytool", "altool", "stapler", "signtool", "azuresigntool", "productbuild", "pkgbuild", "hdiutil", "create-dmg",
|
|
13
|
+
"makensis", "iscc", "wix", "wrangler", "rclone", "aws-s3", "gh-release", "cargo-publish", "npm-publish", "pnpm-publish",
|
|
14
|
+
"tauri-build", "xcode-archive",
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
const READ_ONLY = (list) => (rest) => list.includes(String(rest[0] ?? "").toLowerCase());
|
|
18
|
+
|
|
19
|
+
/** name (lowercased basename without a script or exe extension) -> classifier of the remaining argument tokens. */
|
|
20
|
+
const RULES = [
|
|
21
|
+
{ id: "codesign", stage: "sign", names: ["codesign"], flagged: (rest) => {
|
|
22
|
+
const flags = rest.filter((arg) => arg.startsWith("-"));
|
|
23
|
+
const signs = flags.some((arg) => /^(?:-s|--sign|-f|--force|--remove-signature)$/.test(arg));
|
|
24
|
+
const readsOnly = flags.some((arg) => /^(?:--verify|-v+|-d+v*|--display|--test-requirement|-r+-?|-h|--help)$/.test(arg));
|
|
25
|
+
return signs || !readsOnly;
|
|
26
|
+
} },
|
|
27
|
+
{ id: "notarytool", stage: "notarize", names: ["notarytool"], flagged: (rest) => !READ_ONLY(["info", "log", "history", "--help", "--version", "help"])(rest) },
|
|
28
|
+
{ id: "altool", stage: "notarize", names: ["altool"], flagged: () => true },
|
|
29
|
+
{ id: "stapler", stage: "notarize", names: ["stapler"], flagged: (rest) => !READ_ONLY(["validate", "--help", "help"])(rest) },
|
|
30
|
+
{ id: "signtool", stage: "sign", names: ["signtool"], flagged: (rest) => !READ_ONLY(["verify", "/?", "-?", "--help"])(rest) },
|
|
31
|
+
{ id: "azuresigntool", stage: "sign", names: ["azuresigntool", "trusted-signing-cli"], flagged: () => true },
|
|
32
|
+
{ id: "productbuild", stage: "package", names: ["productbuild", "productsign"], flagged: () => true },
|
|
33
|
+
{ id: "pkgbuild", stage: "package", names: ["pkgbuild"], flagged: () => true },
|
|
34
|
+
{ id: "hdiutil", stage: "package", names: ["hdiutil"], flagged: (rest) => READ_ONLY(["create", "convert", "makehybrid", "resize", "compact", "burn"])(rest) },
|
|
35
|
+
{ id: "create-dmg", stage: "package", names: ["create-dmg"], flagged: () => true },
|
|
36
|
+
{ id: "makensis", stage: "package", names: ["makensis"], flagged: () => true },
|
|
37
|
+
{ id: "iscc", stage: "package", names: ["iscc"], flagged: () => true },
|
|
38
|
+
{ id: "wix", stage: "package", names: ["candle"], flagged: () => true },
|
|
39
|
+
{ id: "wix", stage: "package", names: ["wix"], flagged: (rest) => READ_ONLY(["build", "msi", "burn", "convert", "extension"])(rest) },
|
|
40
|
+
{ id: "wix", stage: "package", names: ["light"], flagged: (rest) => rest.some((arg) => /\.(?:wixobj|wxs|wixlib)$/i.test(arg) || /^-out$/i.test(arg)) },
|
|
41
|
+
{ id: "wrangler", stage: "upload", names: ["wrangler"], flagged: (rest) => !READ_ONLY(["--version", "-v", "whoami", "--help", "-h"])(rest) },
|
|
42
|
+
{ id: "rclone", stage: "upload", names: ["rclone"], flagged: (rest) => !READ_ONLY(["ls", "lsf", "lsd", "lsl", "lsjson", "size", "check", "version", "listremotes", "about", "cat", "--version", "--help"])(rest) },
|
|
43
|
+
{ id: "aws-s3", stage: "upload", names: ["aws"], flagged: (rest) => (rest[0] === "s3" && !["ls", "help"].includes(String(rest[1] ?? "")))
|
|
44
|
+
|| (rest[0] === "s3api" && /^(?:put-|create-multipart-upload|upload-part|complete-multipart-upload|copy-object|delete-)/.test(String(rest[1] ?? ""))) },
|
|
45
|
+
{ id: "gh-release", stage: "upload", names: ["gh"], flagged: (rest) => rest[0] === "release" && ["create", "upload", "edit", "delete", "delete-asset"].includes(String(rest[1] ?? "")) },
|
|
46
|
+
{ id: "cargo-publish", stage: "upload", names: ["cargo"], flagged: (rest) => rest[0] === "publish" || (rest[0] === "tauri" && ["build", "bundle", "signer"].includes(String(rest[1] ?? ""))) },
|
|
47
|
+
{ id: "tauri-build", stage: "build", names: ["cargo-tauri"], flagged: (rest) => ["build", "bundle", "signer"].includes(String(rest[0] ?? "")) },
|
|
48
|
+
{ id: "npm-publish", stage: "upload", names: ["npm"], flagged: (rest) => rest[0] === "publish" },
|
|
49
|
+
{ id: "pnpm-publish", stage: "upload", names: ["pnpm"], flagged: (rest) => rest[0] === "publish" },
|
|
50
|
+
{ id: "npm-publish", stage: "upload", names: ["yarn"], flagged: (rest) => rest[0] === "publish" || (rest[0] === "npm" && rest[1] === "publish") },
|
|
51
|
+
{ id: "tauri-build", stage: "build", names: ["tauri"], flagged: (rest) => ["build", "bundle", "signer"].includes(String(rest[0] ?? "")) },
|
|
52
|
+
{ id: "xcode-archive", stage: "package", names: ["xcodebuild"], flagged: (rest) => rest.some((arg) => /^(?:archive|-exportArchive|-archivePath|-exportOptionsPlist)$/.test(arg)) },
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
/** `rightkit cargo publish` reaches the same crates.io upload through the managed client. */
|
|
56
|
+
const MANAGED_CLIENT_RULE = { id: "cargo-publish", stage: "upload" };
|
|
57
|
+
|
|
58
|
+
/** Wrapper words that run another command: skipped so the tool behind them is the one classified. */
|
|
59
|
+
const WRAPPERS = new Set(["sudo", "env", "time", "command", "exec", "call", "start", "nohup", "xcrun", "npx", "corepack", "pwsh", "powershell", "bash", "sh", "zsh", "cmd", "node", "python", "python3", "py"]);
|
|
60
|
+
|
|
61
|
+
const stripQuotes = (token) => String(token).replace(/^[\s"'`@&]+|[\s"'`,;]+$/g, "");
|
|
62
|
+
export const commandBaseName = (token) => stripQuotes(token).split(/[\\/]/).pop().toLowerCase().replace(/\.(?:exe|cmd|bat|ps1|sh|com|command)$/i, "");
|
|
63
|
+
|
|
64
|
+
/** Split one script line into command segments at the shell operators. */
|
|
65
|
+
export function splitSegments(line) {
|
|
66
|
+
return String(line).split(/&&|\|\||\$\(|[;|&`()]/).map((part) => part.trim()).filter(Boolean);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Tokens of one segment after the leading VAR=value assignments and wrapper words. */
|
|
70
|
+
export function segmentTokens(segment) {
|
|
71
|
+
let tokens = segment.match(/"[^"]*"|'[^']*'|\S+/g) ?? [];
|
|
72
|
+
tokens = tokens.map((token) => token.replace(/^["']|["']$/g, ""));
|
|
73
|
+
while (tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[0])) tokens = tokens.slice(1);
|
|
74
|
+
for (let guard = 0; guard < 8 && tokens.length; guard += 1) {
|
|
75
|
+
const name = commandBaseName(tokens[0]);
|
|
76
|
+
if (name === "pnpm" && (tokens[1] === "exec" || tokens[1] === "dlx")) { tokens = tokens.slice(2); continue; }
|
|
77
|
+
if (name === "npm" && tokens[1] === "exec") { const dash = tokens.indexOf("--"); tokens = dash >= 0 ? tokens.slice(dash + 1) : tokens.slice(2); continue; }
|
|
78
|
+
if (WRAPPERS.has(name) && tokens.length > 1) {
|
|
79
|
+
// Interpreters and shells run a script, not another tool, unless they take -c/-Command text: keep scanning the rest.
|
|
80
|
+
tokens = tokens.slice(1).filter((token, index) => !(index === 0 && /^-+(?:c|command|noprofile|nologo|noninteractive|executionpolicy|file|f|e|s)$/i.test(token)));
|
|
81
|
+
// `bash -c "tool args"` and `powershell -Command "tool args"`: the quoted text is the command line.
|
|
82
|
+
if (tokens.length && /\s/.test(tokens[0])) tokens = [...tokens[0].trim().split(/\s+/), ...tokens.slice(1)];
|
|
83
|
+
continue;
|
|
84
|
+
}
|
|
85
|
+
break;
|
|
86
|
+
}
|
|
87
|
+
return tokens;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** Classify one command's tokens against the R3 inventory: `{ id, stage, tool }` for a pipeline tool run in a write form, else null. */
|
|
91
|
+
export function classifyTokens(tokens) {
|
|
92
|
+
if (!tokens.length) return null;
|
|
93
|
+
const name = commandBaseName(tokens[0]);
|
|
94
|
+
const rest = tokens.slice(1);
|
|
95
|
+
if (name === "rightkit" && rest[0] === "cargo" && rest[1] === "publish") return { ...MANAGED_CLIENT_RULE, tool: "rightkit cargo publish" };
|
|
96
|
+
for (const rule of RULES) {
|
|
97
|
+
if (rule.names.includes(name) && rule.flagged(rest)) return { id: rule.id, stage: rule.stage, tool: [name, ...rest.slice(0, 2)].join(" ").trim() };
|
|
98
|
+
}
|
|
99
|
+
return null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** True when the tokens run RightKit's own pipeline CLI or managed client (a generator must never dispatch another pipeline). */
|
|
103
|
+
export function dispatchesRightKit(tokens) {
|
|
104
|
+
if (!tokens.length) return false;
|
|
105
|
+
const name = commandBaseName(tokens[0]);
|
|
106
|
+
if (name === "rightkit" || name === "right-release") return true;
|
|
107
|
+
return tokens.some((token) => /(?:^|[\\/])(?:@rightkit[\\/]release[\\/]cli[\\/]right-release|right-release)(?:\.mjs)?$/i.test(token));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const YAML_RUN_PREFIX = /^\s*(?:-\s+)?(?:run|script|command)\s*:\s*[|>]?[-+]?\s*/;
|
|
111
|
+
const COMMENT_LINE = /^\s*(?:#|\/\/|::|rem\s|@rem\s)/i;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Pipeline tool matches in the text of a shell-like source (a script, a hook, a workflow). `kind`:
|
|
115
|
+
* - "shell": every line is read as a command line (sh, ps1, bat, cmd, command, yml run steps, a package.json script value);
|
|
116
|
+
* - "code": quoted tool names in a js/mjs/py/rs source (`spawnSync("codesign", ...)`), qualified by the next literals.
|
|
117
|
+
* Returns `[{ line, id, stage, tool, text }]` (1-based lines).
|
|
118
|
+
*/
|
|
119
|
+
export function scanTextForPipelineTools(text, { kind = "shell" } = {}) {
|
|
120
|
+
const found = [];
|
|
121
|
+
const lines = String(text).split(/\r?\n/);
|
|
122
|
+
if (kind === "code") {
|
|
123
|
+
const names = new Map();
|
|
124
|
+
for (const rule of RULES) for (const name of rule.names) names.set(name, rule);
|
|
125
|
+
lines.forEach((line, index) => {
|
|
126
|
+
if (COMMENT_LINE.test(line)) return;
|
|
127
|
+
for (const literal of line.matchAll(/["'`]([^"'`\n]{1,200})["'`]/g)) {
|
|
128
|
+
const tokens = literal[1].trim().split(/\s+/);
|
|
129
|
+
const first = commandBaseName(tokens[0]);
|
|
130
|
+
const rule = names.get(first) ?? (first === "rightkit" ? MANAGED_CLIENT_RULE : null);
|
|
131
|
+
if (!rule) continue;
|
|
132
|
+
// The arguments are the rest of this literal, then the next few literals of the same call (an argv array).
|
|
133
|
+
const tail = line.slice(literal.index + literal[0].length, literal.index + literal[0].length + 240);
|
|
134
|
+
const following = [...tail.matchAll(/["'`]([^"'`\n]{0,120})["'`]/g)].map((match) => match[1]);
|
|
135
|
+
const rest = [...tokens.slice(1), ...following].map((arg) => arg.trim()).filter(Boolean);
|
|
136
|
+
const hit = first === "rightkit" ? classifyTokens([first, ...rest]) : rule.flagged(rest) ? { id: rule.id, stage: rule.stage, tool: first } : null;
|
|
137
|
+
if (hit) found.push({ line: index + 1, ...hit, text: line.trim().slice(0, 160) });
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
return found;
|
|
141
|
+
}
|
|
142
|
+
lines.forEach((line, index) => {
|
|
143
|
+
if (COMMENT_LINE.test(line)) return;
|
|
144
|
+
const body = line.replace(YAML_RUN_PREFIX, "");
|
|
145
|
+
for (const segment of splitSegments(body)) {
|
|
146
|
+
const hit = classifyTokens(segmentTokens(segment));
|
|
147
|
+
if (hit) found.push({ line: index + 1, ...hit, text: line.trim().slice(0, 160) });
|
|
148
|
+
}
|
|
149
|
+
});
|
|
150
|
+
return found;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** GitHub Actions that build, sign or upload on their own (R3: the same tools behind an action). */
|
|
154
|
+
export const PIPELINE_ACTIONS = Object.freeze([
|
|
155
|
+
{ pattern: /^tauri-apps\/tauri-action(?:@|$)/i, id: "tauri-build", stage: "build" },
|
|
156
|
+
{ pattern: /^apple-actions\//i, id: "codesign", stage: "sign" },
|
|
157
|
+
{ pattern: /^azure\/(?:trusted-signing-action|artifact-signing-action)(?:@|$)/i, id: "azuresigntool", stage: "sign" },
|
|
158
|
+
{ pattern: /^cloudflare\/wrangler-action(?:@|$)/i, id: "wrangler", stage: "upload" },
|
|
159
|
+
{ pattern: /^softprops\/action-gh-release(?:@|$)/i, id: "gh-release", stage: "upload" },
|
|
160
|
+
{ pattern: /^ncipollo\/release-action(?:@|$)/i, id: "gh-release", stage: "upload" },
|
|
161
|
+
{ pattern: /^svenstaro\/upload-release-action(?:@|$)/i, id: "gh-release", stage: "upload" },
|
|
162
|
+
{ pattern: /^actions\/upload-release-asset(?:@|$)/i, id: "gh-release", stage: "upload" },
|
|
163
|
+
{ pattern: /^shogo82148\/actions-upload-release-asset(?:@|$)/i, id: "gh-release", stage: "upload" },
|
|
164
|
+
{ pattern: /^jakejarvis\/s3-sync-action(?:@|$)/i, id: "aws-s3", stage: "upload" },
|
|
165
|
+
]);
|
|
166
|
+
|
|
167
|
+
export function classifyAction(uses) {
|
|
168
|
+
const value = String(uses).trim().replace(/^["']|["']$/g, "");
|
|
169
|
+
for (const entry of PIPELINE_ACTIONS) if (entry.pattern.test(value)) return { id: entry.id, stage: entry.stage, tool: value };
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
// The release frontend build always resolves in production mode (M12: `@rightkit/license/testing` and
|
|
2
|
+
// other development-only conditional exports must not resolve into a release bundle). Release build
|
|
3
|
+
// children get NODE_ENV=production, which Vite's build honours ahead of a `.env` NODE_ENV and of any
|
|
4
|
+
// custom `--mode` (vite.dev/guide/env-and-mode, "NODE_ENV and Modes"). What the pin cannot override is
|
|
5
|
+
// a config that lists the `development` resolve condition explicitly, so the preflight refuses that.
|
|
6
|
+
// Package installs keep the caller's NODE_ENV (production would drop devDependencies).
|
|
7
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
8
|
+
import path from "node:path";
|
|
9
|
+
|
|
10
|
+
const SKIP_DIRS = new Set(["node_modules", "target", "dist", ".git", "src-tauri", "build", "out"]);
|
|
11
|
+
const VITE_CONFIG = /^vite\.config\.(?:[cm]?[jt]s)$/;
|
|
12
|
+
|
|
13
|
+
export function productionBuildEnv(env) {
|
|
14
|
+
return { ...env, NODE_ENV: "production" };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function walk(dir, depth, visit) {
|
|
18
|
+
let names;
|
|
19
|
+
try { names = readdirSync(dir); } catch { return; }
|
|
20
|
+
for (const name of names) {
|
|
21
|
+
const full = path.join(dir, name);
|
|
22
|
+
let stat;
|
|
23
|
+
try { stat = statSync(full); } catch { continue; }
|
|
24
|
+
if (stat.isDirectory()) {
|
|
25
|
+
if (depth > 0 && !SKIP_DIRS.has(name) && !name.startsWith(".")) walk(full, depth - 1, visit);
|
|
26
|
+
} else visit(name, full);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Vite configs that would resolve development-only exports even in a production build. A text scan:
|
|
31
|
+
* a computed conditions array is not seen (the post-build web-bundle marker scan is the backstop). */
|
|
32
|
+
export function productionModeProblems({ appRoot, depth = 3 }) {
|
|
33
|
+
const problems = [];
|
|
34
|
+
if (!appRoot || !existsSync(appRoot)) return problems;
|
|
35
|
+
walk(appRoot, depth, (name, full) => {
|
|
36
|
+
if (!VITE_CONFIG.test(name)) return;
|
|
37
|
+
if (/conditions\s*:\s*\[[^\]]*["'`]development["'`]/.test(readFileSync(full, "utf8"))) {
|
|
38
|
+
problems.push(`${path.relative(appRoot, full)} lists a "development" resolve condition; a release build must not resolve development-only exports`);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
return problems;
|
|
42
|
+
}
|
package/publication-gate.mjs
CHANGED
|
@@ -8,7 +8,8 @@ import fs from "node:fs";
|
|
|
8
8
|
import os from "node:os";
|
|
9
9
|
import path from "node:path";
|
|
10
10
|
import { spawnSync } from "./background-process.mjs";
|
|
11
|
-
import {
|
|
11
|
+
import { assertPublishCapability, refuseHostUpload } from "./host-upload-refusal.mjs";
|
|
12
|
+
import { recordPublished, workspaceFeedOptions } from "./rightkit-sync.mjs";
|
|
12
13
|
import { fileURLToPath } from "node:url";
|
|
13
14
|
import { digestBytes, readVerifiedArtifact } from "./publication-artifacts.mjs";
|
|
14
15
|
|
|
@@ -93,6 +94,27 @@ export function batchReceiptProblems(batch, evidence, tree) {
|
|
|
93
94
|
return problems;
|
|
94
95
|
}
|
|
95
96
|
|
|
97
|
+
/**
|
|
98
|
+
* Feature sets of this crate that a batch receipt did NOT lint (0.4.1 review A2). Batch mode lints each crate's tested
|
|
99
|
+
* feature sets in shared `-p a -p b` runs, so a receipt from before every set was linted (0.4.0: first set only)
|
|
100
|
+
* still satisfies `batchReceiptProblems`, which accepts any ONE clippy row covering the crate. Replay therefore
|
|
101
|
+
* needs one passing row per feature set: bound to the evidence commit and the batch tree, naming the crate (`-p <crate>`
|
|
102
|
+
* and `crates`), and ending `--no-deps --all-targets <features> -- -D warnings`. `featureSets` is the crate's
|
|
103
|
+
* `ci/gate.config.json` list (`[[]]` when unset). Returns the missing sets as argument arrays.
|
|
104
|
+
*/
|
|
105
|
+
export function batchClippyMissingFeatureSets(batch, evidence, featureSets) {
|
|
106
|
+
const rows = (Array.isArray(batch?.checks) ? batch.checks : []).filter((row) => row?.name === "clippy" && row.ok === true
|
|
107
|
+
&& row.commit === evidence.commit && row.tree === batch.tree && Array.isArray(row.crates) && row.crates.includes(evidence.name) && text(row.command));
|
|
108
|
+
const sets = (Array.isArray(featureSets) && featureSets.length ? featureSets : [[]]).map((set) => (Array.isArray(set) ? set : []));
|
|
109
|
+
return sets.filter((features) => {
|
|
110
|
+
const tail = ` --no-deps --all-targets${features.length ? ` ${features.join(" ")}` : ""} -- -D warnings`;
|
|
111
|
+
return !rows.some((row) => {
|
|
112
|
+
const words = row.command.split(" ");
|
|
113
|
+
return row.command.endsWith(tail) && words.some((word, index) => word === "-p" && words[index + 1] === evidence.name);
|
|
114
|
+
});
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
96
118
|
/**
|
|
97
119
|
* Which per-crate publication steps the evidence ALREADY proves for this exact commit, so the publisher may
|
|
98
120
|
* skip repeating them: `tests` (a passing `cargo test -p <name>` step that executed tests), `clippy` (a passing
|
|
@@ -137,40 +159,44 @@ function invalidRunFields(run, evidence, tree) {
|
|
|
137
159
|
return bad;
|
|
138
160
|
}
|
|
139
161
|
|
|
140
|
-
|
|
141
|
-
|
|
162
|
+
function gateResult(missing, evidence) {
|
|
163
|
+
const result = { ok: missing.length === 0, missing };
|
|
164
|
+
Object.defineProperty(result, "evidence", { value: evidence });
|
|
165
|
+
return Object.freeze(result);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Validate supplied evidence against caller-authenticated source identity, without reading Git or files.
|
|
169
|
+
* The caller owns authentication of expected.commit/tree and requiredHosts (for example, committed CI policy).
|
|
170
|
+
* Evidence cannot relax host requirements: only an explicit, nonempty subset supplied by the caller can.
|
|
171
|
+
* This proves receipt contents only; it makes no claim about working-tree cleanliness or receipt provenance. */
|
|
172
|
+
export function validatePublicationEvidence({ expected = {}, evidence, artifactBytes }) {
|
|
173
|
+
const { kind, name, version, commit, tree } = expected;
|
|
142
174
|
const missing = [];
|
|
143
|
-
|
|
144
|
-
if (!
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
175
|
+
if (!["npm", "cargo", "swift"].includes(kind)) missing.push("expected kind must be npm, cargo or swift");
|
|
176
|
+
if (!text(name)) missing.push("expected name must be nonempty");
|
|
177
|
+
if (!SHA.test(String(commit ?? ""))) missing.push("expected commit is not a full 40-character SHA");
|
|
178
|
+
const defaults = requiredHosts(kind);
|
|
179
|
+
let hosts = defaults;
|
|
180
|
+
if (expected.requiredHosts !== undefined) {
|
|
181
|
+
const requested = expected.requiredHosts;
|
|
182
|
+
if (!(Array.isArray(requested) && requested.length > 0 && new Set(requested).size === requested.length
|
|
183
|
+
&& Array.from(requested).every((host) => defaults.includes(host)))) {
|
|
184
|
+
missing.push(`expected requiredHosts must be a nonempty unique subset of ${defaults.join(", ")}`);
|
|
185
|
+
} else hosts = requested;
|
|
151
186
|
}
|
|
152
|
-
if (evidence) {
|
|
187
|
+
if (!evidence || typeof evidence !== "object" || Array.isArray(evidence)) {
|
|
188
|
+
missing.push("gate evidence must be an object");
|
|
189
|
+
} else {
|
|
153
190
|
if (evidence.schema !== GATE_SCHEMA) missing.push(`evidence schema must be ${GATE_SCHEMA}`);
|
|
154
191
|
if (evidence.kind !== kind || evidence.name !== name) missing.push(`evidence is for ${evidence.kind}:${evidence.name}, expected ${kind}:${name}`);
|
|
155
192
|
// A missing publishing version must refuse: a skipped comparison would qualify any evidence version.
|
|
156
193
|
if (typeof version !== "string" || version.trim() === "") missing.push("publishing version is unknown (manifest has no literal version), cannot bind the receipt to it");
|
|
157
194
|
else if (evidence.version !== version) missing.push(`evidence version ${evidence.version} != ${version}`);
|
|
158
195
|
if (!SHA.test(String(evidence.commit ?? ""))) missing.push("evidence commit is not a full 40-character SHA");
|
|
159
|
-
else if (
|
|
160
|
-
missing.push(`evidence commit ${evidence.commit} != current HEAD ${head} (${describeCommitMismatch(evidence.commit, head, packagePath, git)})`);
|
|
161
|
-
}
|
|
196
|
+
else if (SHA.test(String(commit ?? "")) && evidence.commit !== commit) missing.push(`evidence commit ${evidence.commit} != expected commit ${commit}`);
|
|
162
197
|
const runs = Array.isArray(evidence.runs) ? evidence.runs : [];
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
const tree = runs.some((run) => run?.batch !== undefined) ? git(["rev-parse", "HEAD^{tree}"], packagePath) : undefined;
|
|
166
|
-
if (tree === null) missing.push(`cannot read the HEAD tree for ${packagePath}`);
|
|
167
|
-
// npm packages and crates may ship on macOS evidence first when Adrian decides so; the decision must be
|
|
168
|
-
// recorded in the evidence (hostPolicy "darwin-only" + a non-empty hostPolicyReason).
|
|
169
|
-
// Crates may ship Mac-first too (Adrian 2026-10-07): the Windows run follows; the reason is recorded.
|
|
170
|
-
// A crate published from the Windows host under the local-build grant records the symmetric
|
|
171
|
-
// hostPolicy "win32-only" + reason, and then the one required receipt is the Windows run.
|
|
172
|
-
const singleHost = (kind === "npm" || kind === "cargo") ? singleHostOfPolicy(evidence) : null;
|
|
173
|
-
for (const host of singleHost ? [singleHost] : requiredHosts(kind)) {
|
|
198
|
+
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
|
+
for (const host of hosts) {
|
|
174
200
|
const label = host === "darwin" ? "macOS" : "Windows";
|
|
175
201
|
const matching = runs.filter((run) => run?.host?.os === host);
|
|
176
202
|
if (!matching.length) { missing.push(`no ${label} host run receipt`); continue; }
|
|
@@ -183,9 +209,34 @@ export function checkPublicationGate({ kind, name, version, packagePath, evidenc
|
|
|
183
209
|
if (!/^[0-9a-f]{64}$/.test(String(evidence.artifactSha256 ?? ""))) missing.push("evidence artifactSha256 must be a 64-hex hash");
|
|
184
210
|
}
|
|
185
211
|
if (artifactBytes !== undefined && evidence && digestBytes(artifactBytes) !== evidence.artifactSha256) missing.push("artifact sha256 does not match evidence artifactSha256");
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
212
|
+
return gateResult(missing, evidence);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// 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") }) {
|
|
217
|
+
const missing = [];
|
|
218
|
+
const head = git(["rev-parse", "HEAD"], packagePath);
|
|
219
|
+
if (!head || !SHA.test(head)) missing.push(`cannot read current commit SHA for ${packagePath}`);
|
|
220
|
+
const dirty = git(["status", "--porcelain", "--", "."], packagePath);
|
|
221
|
+
if (dirty === null) missing.push(`cannot read git status for ${packagePath}`);
|
|
222
|
+
else if (dirty !== "") missing.push(`tree is not clean for ${packagePath}`);
|
|
223
|
+
let evidence = null;
|
|
224
|
+
try { evidence = JSON.parse(readFile(evidencePath)); } catch {
|
|
225
|
+
missing.push(`gate evidence file missing or unreadable: ${evidencePath}`);
|
|
226
|
+
}
|
|
227
|
+
const runs = Array.isArray(evidence?.runs) ? evidence.runs : [];
|
|
228
|
+
const tree = runs.some((run) => run?.batch !== undefined) ? git(["rev-parse", "HEAD^{tree}"], packagePath) : undefined;
|
|
229
|
+
if (tree === null) missing.push(`cannot read the HEAD tree for ${packagePath}`);
|
|
230
|
+
// Local publication retains its recorded single-host grant. CI callers own their policy separately.
|
|
231
|
+
const singleHost = (kind === "npm" || kind === "cargo") ? singleHostOfPolicy(evidence) : null;
|
|
232
|
+
const result = validatePublicationEvidence({ expected: { kind, name, version, commit: head, tree,
|
|
233
|
+
...(singleHost ? { requiredHosts: [singleHost] } : {}) }, evidence, artifactBytes });
|
|
234
|
+
for (const problem of result.missing) {
|
|
235
|
+
if (problem === `evidence commit ${evidence?.commit} != expected commit ${head}`) {
|
|
236
|
+
missing.push(`evidence commit ${evidence.commit} != current HEAD ${head} (${describeCommitMismatch(evidence.commit, head, packagePath, git)})`);
|
|
237
|
+
} else missing.push(problem);
|
|
238
|
+
}
|
|
239
|
+
return gateResult(missing, evidence);
|
|
189
240
|
}
|
|
190
241
|
|
|
191
242
|
// Enforce: throws on missing evidence unless dryRun (then reports and continues).
|
|
@@ -251,17 +302,27 @@ export function runNpmPreflight(argv, workspaceRoot = process.cwd(), deps = {})
|
|
|
251
302
|
return;
|
|
252
303
|
}
|
|
253
304
|
if (dryRun) throw new Error("--publish and --dry-run are mutually exclusive");
|
|
305
|
+
// Host uploads are retired on every host with no environment override; publishGatedTarball stays for direct calls only.
|
|
306
|
+
refuseHostUpload("right-release publish npm --publish");
|
|
254
307
|
if (!tarball) throw new Error("--publish requires --tarball <gated .tgz>");
|
|
255
308
|
enforcePublicationGate(options);
|
|
256
|
-
publishGatedTarball({ name: pkg, version: manifest.version, tarball, evidencePath }, deps);
|
|
309
|
+
publishGatedTarball({ name: pkg, version: manifest.version, tarball, evidencePath, workspaceRoot }, deps);
|
|
257
310
|
}
|
|
258
311
|
|
|
259
|
-
|
|
312
|
+
/**
|
|
313
|
+
* Publish one gated tarball. Exported for the reviewed publisher and its tests only: it reads registry credentials, so it refuses
|
|
314
|
+
* (HOST_UPLOAD_REFUSED) before reading anything unless `capability` is a publish capability (host-upload-refusal.mjs
|
|
315
|
+
* createPublishCapability) created from a validated admission that names this npm archive, and the bytes about to be sent must
|
|
316
|
+
* be the admitted archive's.
|
|
317
|
+
*/
|
|
318
|
+
export function publishGatedTarball({ name, version, tarball, evidencePath, workspaceRoot, capability }, deps = {}) {
|
|
319
|
+
assertPublishCapability(capability, { what: "publishGatedTarball", registry: "npm", name, version });
|
|
260
320
|
const run = deps.spawnSync ?? spawnSync;
|
|
261
321
|
const bytes = fs.readFileSync(tarball);
|
|
262
322
|
const evidence = JSON.parse(fs.readFileSync(evidencePath, "utf8"));
|
|
263
323
|
const sha256 = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
264
324
|
if (sha256 !== evidence.artifactSha256) throw new Error(`tarball sha256 ${sha256} != evidence artifactSha256 ${evidence.artifactSha256}`);
|
|
325
|
+
assertPublishCapability(capability, { what: "publishGatedTarball", registry: "npm", name, version, sha256 });
|
|
265
326
|
const token = deps.readToken ? deps.readToken() : readKeychainToken(run);
|
|
266
327
|
const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rk-npm-publish-"));
|
|
267
328
|
const userconfig = path.join(dir, "npmrc");
|
|
@@ -278,7 +339,8 @@ export function publishGatedTarball({ name, version, tarball, evidencePath }, de
|
|
|
278
339
|
}
|
|
279
340
|
const sha1 = crypto.createHash("sha1").update(bytes).digest("hex");
|
|
280
341
|
verifyNpmRegistryBytes({ name, version, sha1 }, deps);
|
|
281
|
-
|
|
342
|
+
// Feed and consumer copies belong to the requested workspace, never to this module's own (possibly installed) location.
|
|
343
|
+
(deps.recordPublished ?? recordPublished)({ registry: "npm", name, version }, workspaceRoot ? workspaceFeedOptions(workspaceRoot) : undefined);
|
|
282
344
|
}
|
|
283
345
|
|
|
284
346
|
// CI and the authenticated helper share mandatory, fail-closed registry equality.
|
package/publish-cargo.mjs
CHANGED
|
@@ -2,12 +2,13 @@
|
|
|
2
2
|
import fs from "node:fs";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
-
import { enforcePublicationGate, defaultEvidencePath, provenGateSteps, singleHostOfPolicy } from "./publication-gate.mjs";
|
|
5
|
+
import { enforcePublicationGate, defaultEvidencePath, provenGateSteps, singleHostOfPolicy, batchClippyMissingFeatureSets } from "./publication-gate.mjs";
|
|
6
6
|
import { runCargoSync } from "./managed-cargo.mjs";
|
|
7
|
+
import { assertPublishCapability, refuseHostUpload } from "./host-upload-refusal.mjs";
|
|
7
8
|
import { spawnSync } from "./background-process.mjs";
|
|
8
9
|
import { readVerifiedArtifact, assertArtifactUnchanged } from "./publication-artifacts.mjs";
|
|
9
10
|
import { verifyCargoRegistry } from "./cargo-registry-readback.mjs";
|
|
10
|
-
import { recordPublished } from "./rightkit-sync.mjs";
|
|
11
|
+
import { recordPublished, workspaceFeedOptions } from "./rightkit-sync.mjs";
|
|
11
12
|
import { readCargoPackage } from "./crate-graph.mjs";
|
|
12
13
|
import { runtimeBudgetCheck, RUNTIME_BUDGET_CODE } from "./runtime-budgets.mjs";
|
|
13
14
|
import { formatPreflight, PREFLIGHT_FAIL } from "./preflight.mjs";
|
|
@@ -147,14 +148,19 @@ export function scanCrateForSecrets(crateDir) {
|
|
|
147
148
|
continue;
|
|
148
149
|
}
|
|
149
150
|
const bytes = fs.readFileSync(file);
|
|
150
|
-
if (bytes.includes(0)) continue;
|
|
151
151
|
const text = bytes.toString("utf8");
|
|
152
152
|
if (SECRET_PATTERNS.some((pattern) => pattern.test(text))) violations.push(relative);
|
|
153
153
|
}
|
|
154
154
|
return [...new Set(violations)].sort();
|
|
155
155
|
}
|
|
156
156
|
|
|
157
|
+
/**
|
|
158
|
+
* Package, qualify and (unless `options.dryRun`) publish one crate. A real publish is refused (HOST_UPLOAD_REFUSED) before anything
|
|
159
|
+
* runs unless `options.capability` is a publish capability (host-upload-refusal.mjs createPublishCapability) created from a
|
|
160
|
+
* validated admission that names this crate, and again before the registry upload unless the verified archive is the admitted one.
|
|
161
|
+
*/
|
|
157
162
|
export async function runCargoPublish(options, execute = (_command, args, settings) => runCargoSync(args, settings), deps = {}) {
|
|
163
|
+
if (!options.dryRun) assertPublishCapability(options.capability, { what: "runCargoPublish", registry: "crates", name: options.crate });
|
|
158
164
|
const crateDir = path.join(options.workspaceRoot, "crates", options.crate);
|
|
159
165
|
const manifest = path.join(crateDir, "Cargo.toml");
|
|
160
166
|
if (!fs.existsSync(manifest)) throw new Error(`crate manifest not found: ${manifest}`);
|
|
@@ -176,8 +182,9 @@ export async function runCargoPublish(options, execute = (_command, args, settin
|
|
|
176
182
|
const proven = (reuseEvidence || options.trustEvidenceSteps === true) && !options.dryRun ? provenGateSteps(gate.evidence) : {};
|
|
177
183
|
if (reuseEvidence && (proven.tests !== true || proven.clippy !== true)) throw new Error("--from-evidence requires gate-proven tests and clippy for this crate and commit");
|
|
178
184
|
if (reuseEvidence) {
|
|
179
|
-
const status = (deps.workspaceStatus ?? ((workspaceRoot) => spawnSync("git", ["status", "--porcelain", "--", "Cargo.toml", "Cargo.lock"], { cwd: workspaceRoot, encoding: "utf8", windowsHide: true })))(options.workspaceRoot);
|
|
180
|
-
|
|
185
|
+
const status = (deps.workspaceStatus ?? ((workspaceRoot) => spawnSync("git", ["status", "--porcelain", "--", "Cargo.toml", "Cargo.lock", "ci/gate.config.json", "ci/asset-gated.json"], { cwd: workspaceRoot, encoding: "utf8", windowsHide: true })))(options.workspaceRoot);
|
|
186
|
+
// The gate config decides the steps replayed below, so an uncommitted edit would change what is demanded (0.4.1 review A3).
|
|
187
|
+
if (status.error || status.status !== 0 || String(status.stdout ?? "").trim()) throw new Error("--from-evidence requires clean workspace Cargo.toml, Cargo.lock and ci gate config (ci/gate.config.json, ci/asset-gated.json)");
|
|
181
188
|
const proofHost = singleHostOfPolicy(gate.evidence) ?? "darwin";
|
|
182
189
|
const runs = gate.evidence.runs.filter((run) => run?.host?.os === proofHost);
|
|
183
190
|
const command = `cargo package --locked -p ${options.crate}`;
|
|
@@ -201,6 +208,11 @@ export async function runCargoPublish(options, execute = (_command, args, settin
|
|
|
201
208
|
throw new Error(`--from-evidence missing configured qualification step: ${text}`);
|
|
202
209
|
}
|
|
203
210
|
}
|
|
211
|
+
// A batch run's clippy lives in its batch receipt, which `expected` leaves out: demand one row per feature set (0.4.1 review A2).
|
|
212
|
+
if (run.batch) {
|
|
213
|
+
const missing = batchClippyMissingFeatureSets(run.batch, gate.evidence, config?.rust?.[options.crate]?.featureSets ?? [[]]);
|
|
214
|
+
if (missing.length) throw new Error(`--from-evidence missing batch clippy for feature set${missing.length === 1 ? "" : "s"}: ${missing.map((features) => features.join(" ") || "default").join("; ")}`);
|
|
215
|
+
}
|
|
204
216
|
}
|
|
205
217
|
}
|
|
206
218
|
const plan = buildCargoPublishPlan({ ...options, crateDir, proven,
|
|
@@ -246,7 +258,10 @@ export async function runCargoPublish(options, execute = (_command, args, settin
|
|
|
246
258
|
publication = await verifyCargoRegistry({ name: options.crate, version: cargoVersion, sha256: artifact.sha256 }, deps);
|
|
247
259
|
continue;
|
|
248
260
|
}
|
|
249
|
-
if (step.label === "registry publish")
|
|
261
|
+
if (step.label === "registry publish") {
|
|
262
|
+
assertArtifactUnchanged(artifact);
|
|
263
|
+
assertPublishCapability(options.capability, { what: "runCargoPublish", registry: "crates", name: options.crate, version: cargoVersion, sha256: artifact.sha256 });
|
|
264
|
+
}
|
|
250
265
|
// `execute` may be synchronous (the default) or async (the batch publisher overlaps uploads).
|
|
251
266
|
const isReadOnlyInspection = ["package contents", "package location"].includes(step.label);
|
|
252
267
|
const settings = {
|
|
@@ -313,10 +328,12 @@ function valueAfter(argv, flag) {
|
|
|
313
328
|
|
|
314
329
|
async function main() {
|
|
315
330
|
const options = parseCargoPublishArgs(process.argv.slice(2));
|
|
331
|
+
// Every non---dry-run call (including --from-evidence) is an upload: refused on every host, with no environment override.
|
|
332
|
+
if (!options.dryRun) refuseHostUpload("right-release publish cargo");
|
|
316
333
|
const result = await runCargoPublish(options);
|
|
317
334
|
process.stdout.write(`[right-release] ${options.dryRun ? "validated" : "published"} ${options.crate}\n`);
|
|
318
335
|
if (!options.dryRun) {
|
|
319
|
-
recordPublished({ registry: "cargo", name: result.name, version: result.version });
|
|
336
|
+
recordPublished({ registry: "cargo", name: result.name, version: result.version }, workspaceFeedOptions(options.workspaceRoot));
|
|
320
337
|
}
|
|
321
338
|
}
|
|
322
339
|
|
package/publish-swift.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Publish RightKitSwift by the ONE Swift distribution contract: a plain semver git tag on this
|
|
2
|
-
// repository (github.com/
|
|
2
|
+
// repository (the checkout's own origin, github.com/bogusyogi/rightkit), resolved by apps through the root Package.swift
|
|
3
3
|
// (docs/RIGHTKIT-SHARED-PACKAGES.md, Swift section). There is no mirror and no second repository.
|
|
4
4
|
//
|
|
5
5
|
// `right-release publish swift [--dry-run] [--evidence <file>] [--workspace <dir>]` verifies, in order:
|
|
@@ -19,7 +19,14 @@ import { spawnSync } from "./background-process.mjs";
|
|
|
19
19
|
import { digestBytes } from "./publication-artifacts.mjs";
|
|
20
20
|
import { enforcePublicationGate, defaultEvidencePath } from "./publication-gate.mjs";
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
// The distribution URL is whatever repository this checkout's origin names (GITHUB_REPOSITORY when origin does not
|
|
23
|
+
// parse); it is never a constant, so a repository move cannot leave a stale name in the success message.
|
|
24
|
+
export function swiftDistributionUrl(origin, environment = process.env) {
|
|
25
|
+
const fromOrigin = /github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?\/?$/i.exec(String(origin ?? "").trim())?.[1];
|
|
26
|
+
const fromEnvironment = /^[\w.-]+\/[\w.-]+$/.test(environment?.GITHUB_REPOSITORY ?? "") ? environment.GITHUB_REPOSITORY : null;
|
|
27
|
+
const repository = fromOrigin ?? fromEnvironment;
|
|
28
|
+
return repository ? `https://github.com/${repository}` : String(origin ?? "").trim();
|
|
29
|
+
}
|
|
23
30
|
const DRIFT_TEST = path.join("scripts", "swift-root-manifest.test.mjs");
|
|
24
31
|
|
|
25
32
|
export function parseSwiftPublishArgs(argv) {
|
|
@@ -125,7 +132,9 @@ export function runSwiftPublish(options, execute = spawnSync) {
|
|
|
125
132
|
catch (error) { git(["tag", "-d", version], toplevel, execute); throw error; }
|
|
126
133
|
const remote = git(["ls-remote", "--tags", "origin", `refs/tags/${version}`], toplevel, execute);
|
|
127
134
|
if (!remote.startsWith(sha)) throw new Error(`origin does not show tag ${version} at ${sha}`);
|
|
128
|
-
|
|
135
|
+
let origin = "";
|
|
136
|
+
try { origin = git(["config", "--get", "remote.origin.url"], toplevel, execute); } catch { /* the tag is already verified on origin; the name is cosmetic */ }
|
|
137
|
+
out(`tagged ${version} at ${sha} on ${swiftDistributionUrl(origin, options.environment)}`);
|
|
129
138
|
return { name, version, commit: sha, published: true };
|
|
130
139
|
}
|
|
131
140
|
|
package/qa-control-scan.mjs
CHANGED
|
@@ -391,6 +391,10 @@ export const WEB_MOCK_MARKERS = Object.freeze([
|
|
|
391
391
|
{ id: "isBrowserQaMode", pattern: /isBrowserQaMode/ },
|
|
392
392
|
{ id: "MockDataSource", pattern: /MockDataSource/ },
|
|
393
393
|
{ id: "qa-build-flag", pattern: /\bVITE_[A-Z0-9_]*_QA\b/ },
|
|
394
|
+
|
|
395
|
+
// @rightkit/license/testing (M12): its sentinel value, and the subpath a bundle that kept the import still names.
|
|
396
|
+
{ id: "license-testing-sentinel", pattern: /\bRIGHTKIT_LICENSE_TESTING_DO_NOT_SHIP\b/ },
|
|
397
|
+
{ id: "license-testing-entry", pattern: /@rightkit\/license\/testing/ },
|
|
394
398
|
]);
|
|
395
399
|
const WEB_EXTENSIONS = /\.(?:m?js|cjs|html?|css|map)$/i;
|
|
396
400
|
|