@rightkit/release 0.2.151 → 0.2.154
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/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +117 -8
- package/build-release.mjs +7 -2
- package/cli/right-release.mjs +18 -5
- package/committed-source.mjs +19 -1
- package/crate-graph.mjs +121 -0
- package/deps.mjs +0 -0
- package/development-signing.mjs +8 -0
- package/development.mjs +46 -13
- package/devtools-policy.json +5 -0
- package/devtools-policy.mjs +119 -0
- package/direct-bootstrap.mjs +9 -3
- package/dmg-build.mjs +19 -5
- package/dmg-layout.mjs +2 -2
- package/dmg-spec.mjs +97 -0
- package/execution-policy.mjs +17 -1
- package/github-release.mjs +25 -7
- package/github-upload-gate.mjs +84 -0
- package/hardeningscan.mjs +3 -0
- package/inventory-check.mjs +4 -4
- package/lsclean.sh +0 -0
- package/mac-bundle-signing.mjs +1 -1
- package/mac-sign-core.mjs +97 -0
- package/mac-signing-matrix.mjs +186 -0
- package/msix-appinstaller.mjs +4 -1
- package/msix-build.mjs +3 -6
- package/msix-config.mjs +5 -1
- package/msix-repair-guard.mjs +24 -0
- package/msix-repair-guard.ps1 +356 -0
- package/msix-signer.mjs +211 -0
- package/ownership-catalog-config.json +6 -7
- package/ownership-command.mjs +69 -8
- package/ownership-evidence.mjs +1 -1
- package/ownership-key-authority.mjs +1 -1
- package/ownership-key-command.mjs +15 -3
- package/ownership-key-elevation.mjs +11 -2
- package/ownership-mandatory-set.json +420 -0
- package/ownership-mandatory-set.mjs +273 -0
- package/ownership.mjs +2 -2
- package/package.json +25 -11
- package/packaged-app-identity.mjs +88 -0
- package/preflight.mjs +25 -3
- package/prune-r2.mjs +28 -3
- package/publication-gate.mjs +49 -10
- package/publish-cargo.mjs +21 -1
- package/publish-swift.mjs +78 -141
- package/publish-update.mjs +24 -2
- package/qa-build-features.mjs +216 -0
- package/qa-control-scan.mjs +435 -0
- package/registry-parity.mjs +18 -1
- package/release-invocation.mjs +2 -1
- package/release-state.mjs +9 -0
- package/release-switch-registry.json +33 -0
- package/release-switch-scan.mjs +452 -0
- package/release.mjs +320 -26
- package/renamed-away.mjs +29 -0
- package/rightkit-ownership.json +682 -709
- package/rightkit-sync.mjs +8 -4
- package/rightkit-version-policy.mjs +18 -0
- package/rightkit-versions.json +211 -127
- package/runtime-budget-classes.json +4 -0
- package/runtime-budgets.mjs +870 -0
- package/runtime-bundle-scan.mjs +61 -0
- package/sign-macos.mjs +28 -30
- package/sign-updater.mjs +5 -2
- package/swift-mac-dmg.mjs +7 -2
- package/swift-package-command.mjs +1 -1
- package/upload-large.mjs +0 -0
- package/upload-release.mjs +10 -37
package/publication-gate.mjs
CHANGED
|
@@ -17,6 +17,9 @@ export const REQUIRED_HOSTS = Object.freeze(["darwin", "win32"]);
|
|
|
17
17
|
// Swift packages (RightKitSwift) are Apple-only; Windows cannot run them.
|
|
18
18
|
export const requiredHosts = (kind) => (kind === "swift" ? Object.freeze(["darwin"]) : REQUIRED_HOSTS);
|
|
19
19
|
const SHA = /^[0-9a-f]{40}$/;
|
|
20
|
+
// The one definition of a recorded darwin-only host policy (release evidence and the runtime budget gate share it).
|
|
21
|
+
export const isDarwinOnlyPolicy = (source) => source?.hostPolicy === "darwin-only"
|
|
22
|
+
&& typeof source.hostPolicyReason === "string" && source.hostPolicyReason.trim() !== "";
|
|
20
23
|
|
|
21
24
|
export function defaultEvidencePath(workspaceRoot, kind, name) {
|
|
22
25
|
return path.join(workspaceRoot, "release-evidence", "gate", `${kind}-${name.replace(/^@/, "").replace("/", "-")}.json`);
|
|
@@ -28,6 +31,40 @@ function defaultGit(args, cwd) {
|
|
|
28
31
|
return (result.stdout ?? "").trim();
|
|
29
32
|
}
|
|
30
33
|
|
|
34
|
+
// Explains a receipt commit that is not the publishing HEAD. The gate still refuses it (exact-commit rule,
|
|
35
|
+
// docs/RIGHTKIT-SHARED-PACKAGES.md "Publication gate" rule 1); this only says whether the receipt is an
|
|
36
|
+
// ancestor with an unchanged unit path (re-qualify by re-running on HEAD) or unrelated/changed.
|
|
37
|
+
function describeCommitMismatch(evidenceCommit, head, packagePath, git) {
|
|
38
|
+
const known = git(["cat-file", "-t", evidenceCommit], packagePath);
|
|
39
|
+
if (known !== "commit") return "receipt commit is not a commit object in this repository";
|
|
40
|
+
// `merge-base --is-ancestor` prints nothing: "" means ancestor, null (non-zero exit) means not.
|
|
41
|
+
const ancestor = git(["merge-base", "--is-ancestor", evidenceCommit, head], packagePath) !== null;
|
|
42
|
+
const relation = ancestor ? git(["rev-list", "--count", `${evidenceCommit}..${head}`], packagePath) : null;
|
|
43
|
+
const changed = git(["diff", "--name-only", evidenceCommit, head, "--", "."], packagePath);
|
|
44
|
+
const parts = [];
|
|
45
|
+
parts.push(ancestor ? `receipt commit is an ancestor, ${relation ?? "?"} commit(s) behind HEAD` : "receipt commit is not an ancestor of HEAD");
|
|
46
|
+
if (changed === null) parts.push("cannot diff the unit path");
|
|
47
|
+
else parts.push(changed === "" ? "unit path unchanged since the receipt (re-run the host runs on HEAD to re-qualify)" : `unit path changed since the receipt: ${changed.split("\n").slice(0, 5).join(", ")}`);
|
|
48
|
+
return parts.join("; ");
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Names of the fields of one host run that do not qualify (empty when the run qualifies). Every run binds to
|
|
52
|
+
// the same unit, commit and artifact: commit, host arch, feature set and artifactSha256 equal to the evidence.
|
|
53
|
+
function invalidRunFields(run, evidence) {
|
|
54
|
+
const bad = [];
|
|
55
|
+
const text = (value) => typeof value === "string" && value.trim() !== "";
|
|
56
|
+
if (run.commit !== evidence.commit) bad.push(`commit (must equal ${evidence.commit})`);
|
|
57
|
+
if (!text(run.host?.arch)) bad.push("host.arch");
|
|
58
|
+
if (!(Array.isArray(run.features) && run.features.length > 0 && run.features.every(text))) bad.push("features");
|
|
59
|
+
if (run.artifactSha256 !== evidence.artifactSha256) bad.push("artifactSha256 (must equal the evidence hash)");
|
|
60
|
+
if (!text(run.command)) bad.push("command");
|
|
61
|
+
if (!text(run.operator)) bad.push("operator");
|
|
62
|
+
if (!text(run.timestamp)) bad.push("timestamp");
|
|
63
|
+
if (!(Number.isInteger(run.passed) && run.passed > 0)) bad.push("passed (must be > 0)");
|
|
64
|
+
if (run.failed !== 0) bad.push("failed (must be 0)");
|
|
65
|
+
return bad;
|
|
66
|
+
}
|
|
67
|
+
|
|
31
68
|
// Returns { ok, missing[] }. Pure apart from the injected git/readFile.
|
|
32
69
|
export function checkPublicationGate({ kind, name, version, packagePath, evidencePath, artifactBytes, git = defaultGit, readFile = (p) => fs.readFileSync(p, "utf8") }) {
|
|
33
70
|
const missing = [];
|
|
@@ -43,25 +80,27 @@ export function checkPublicationGate({ kind, name, version, packagePath, evidenc
|
|
|
43
80
|
if (evidence) {
|
|
44
81
|
if (evidence.schema !== GATE_SCHEMA) missing.push(`evidence schema must be ${GATE_SCHEMA}`);
|
|
45
82
|
if (evidence.kind !== kind || evidence.name !== name) missing.push(`evidence is for ${evidence.kind}:${evidence.name}, expected ${kind}:${name}`);
|
|
46
|
-
|
|
83
|
+
// A missing publishing version must refuse: a skipped comparison would qualify any evidence version.
|
|
84
|
+
if (typeof version !== "string" || version.trim() === "") missing.push("publishing version is unknown (manifest has no literal version), cannot bind the receipt to it");
|
|
85
|
+
else if (evidence.version !== version) missing.push(`evidence version ${evidence.version} != ${version}`);
|
|
47
86
|
if (!SHA.test(String(evidence.commit ?? ""))) missing.push("evidence commit is not a full 40-character SHA");
|
|
48
|
-
else if (head && evidence.commit !== head)
|
|
87
|
+
else if (head && evidence.commit !== head) {
|
|
88
|
+
missing.push(`evidence commit ${evidence.commit} != current HEAD ${head} (${describeCommitMismatch(evidence.commit, head, packagePath, git)})`);
|
|
89
|
+
}
|
|
49
90
|
const runs = Array.isArray(evidence.runs) ? evidence.runs : [];
|
|
50
91
|
// npm packages and crates may ship on macOS evidence first when Adrian decides so; the decision must be
|
|
51
92
|
// recorded in the evidence (hostPolicy "darwin-only" + a non-empty hostPolicyReason).
|
|
52
93
|
// Crates may ship Mac-first too (Adrian 2026-10-07): the Windows run follows; the reason is recorded.
|
|
53
|
-
const darwinOnly = (kind === "npm" || kind === "cargo") && evidence
|
|
54
|
-
&& typeof evidence.hostPolicyReason === "string" && evidence.hostPolicyReason.trim() !== "";
|
|
94
|
+
const darwinOnly = (kind === "npm" || kind === "cargo") && isDarwinOnlyPolicy(evidence);
|
|
55
95
|
for (const host of darwinOnly ? ["darwin"] : requiredHosts(kind)) {
|
|
56
96
|
const label = host === "darwin" ? "macOS" : "Windows";
|
|
57
97
|
const matching = runs.filter((run) => run?.host?.os === host);
|
|
58
98
|
if (!matching.length) { missing.push(`no ${label} host run receipt`); continue; }
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
if (!good) missing.push(`${label} run receipt must name commit ${evidence.commit}, command, operator, timestamp, passed>0 and failed=0`);
|
|
99
|
+
// Every run on the host must qualify: a failing run is not masked by a passing run beside it.
|
|
100
|
+
matching.forEach((run, index) => {
|
|
101
|
+
const fields = invalidRunFields(run, evidence);
|
|
102
|
+
if (fields.length) missing.push(`${label} run receipt ${index} invalid: ${fields.join(", ")}`);
|
|
103
|
+
});
|
|
65
104
|
}
|
|
66
105
|
if (!/^[0-9a-f]{64}$/.test(String(evidence.artifactSha256 ?? ""))) missing.push("evidence artifactSha256 must be a 64-hex hash");
|
|
67
106
|
}
|
package/publish-cargo.mjs
CHANGED
|
@@ -7,6 +7,9 @@ import { runCargoSync } from "./managed-cargo.mjs";
|
|
|
7
7
|
import { readVerifiedArtifact, assertArtifactUnchanged } from "./publication-artifacts.mjs";
|
|
8
8
|
import { verifyCargoRegistry } from "./cargo-registry-readback.mjs";
|
|
9
9
|
import { recordPublished } from "./rightkit-sync.mjs";
|
|
10
|
+
import { readCargoPackage } from "./crate-graph.mjs";
|
|
11
|
+
import { runtimeBudgetCheck, RUNTIME_BUDGET_CODE } from "./runtime-budgets.mjs";
|
|
12
|
+
import { formatPreflight, PREFLIGHT_FAIL } from "./preflight.mjs";
|
|
10
13
|
|
|
11
14
|
const FORBIDDEN_PACKAGE_PATH = /(^|\/)(?:\.env(?:\.[^/]*)?|target(?:\/.*)?|node_modules(?:\/.*)?|[^/]+\.(?:p8|p12|pem|key))$/i;
|
|
12
15
|
const SECRET_PATTERNS = [
|
|
@@ -46,6 +49,9 @@ export function buildCargoPublishPlan({ crate, crateDir, dryRun, allowDirty = fa
|
|
|
46
49
|
if (allowDirty) packageArgs.push("--allow-dirty");
|
|
47
50
|
const plan = [
|
|
48
51
|
{ label: "secret scan", internal: "secret-scan", crateDir },
|
|
52
|
+
// Runtime-budget crates (runtime-budgets.mjs) must carry a measured runtime-budget.json for this
|
|
53
|
+
// version on every required host. Dry-run reports the check; a real publish refuses it.
|
|
54
|
+
{ label: "runtime budget", internal: "runtime-budget", crate, crateDir },
|
|
49
55
|
{ label: "format", command: "cargo", args: ["fmt", "-p", crate, "--check"] },
|
|
50
56
|
{ label: "tests", command: "cargo", args: ["test", "-p", crate, "--", "--test-threads=1"] },
|
|
51
57
|
{ label: "clippy", command: "cargo", args: ["clippy", "-p", crate, "--all-targets", "--", "-D", "warnings"] },
|
|
@@ -91,7 +97,11 @@ export async function runCargoPublish(options, execute = (_command, args, settin
|
|
|
91
97
|
const crateDir = path.join(options.workspaceRoot, "crates", options.crate);
|
|
92
98
|
const manifest = path.join(crateDir, "Cargo.toml");
|
|
93
99
|
if (!fs.existsSync(manifest)) throw new Error(`crate manifest not found: ${manifest}`);
|
|
94
|
-
|
|
100
|
+
// Parsed, not pattern-matched: `version.workspace = true` resolves through the workspace root, and a
|
|
101
|
+
// manifest whose [package].name is not the requested crate is refused before anything else runs.
|
|
102
|
+
const pkg = readCargoPackage(crateDir);
|
|
103
|
+
if (pkg.name !== options.crate) throw new Error(`Cargo.toml [package].name ${JSON.stringify(pkg.name)} differs from --crate ${options.crate}`);
|
|
104
|
+
const cargoVersion = pkg.version ?? undefined;
|
|
95
105
|
const gateOptions = {
|
|
96
106
|
kind: "cargo", name: options.crate, version: cargoVersion, packagePath: crateDir,
|
|
97
107
|
evidencePath: path.resolve(options.evidence ?? defaultEvidencePath(options.workspaceRoot, "cargo", options.crate)),
|
|
@@ -110,6 +120,16 @@ export async function runCargoPublish(options, execute = (_command, args, settin
|
|
|
110
120
|
if (violations.length) throw new Error(`secret scan rejected: ${violations.join(", ")}`);
|
|
111
121
|
continue;
|
|
112
122
|
}
|
|
123
|
+
if (step.internal === "runtime-budget") {
|
|
124
|
+
const check = runtimeBudgetCheck({ crate: options.crate, crateDir, version: cargoVersion, evidence: gate.evidence ?? undefined });
|
|
125
|
+
if (check) process.stdout.write(`${formatPreflight([check])}\n`);
|
|
126
|
+
if (check?.status === PREFLIGHT_FAIL && !options.dryRun) {
|
|
127
|
+
const error = new Error(`runtime budget refused: ${check.detail}. Fix: ${check.fix}`);
|
|
128
|
+
error.code = RUNTIME_BUDGET_CODE;
|
|
129
|
+
throw error;
|
|
130
|
+
}
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
113
133
|
if (step.internal === "verify-archive") {
|
|
114
134
|
// Revalidate source/evidence after packaging; preserve that exact evidence object.
|
|
115
135
|
const current = (deps.enforceGate ?? enforcePublicationGate)(gateOptions);
|
package/publish-swift.mjs
CHANGED
|
@@ -1,129 +1,73 @@
|
|
|
1
|
-
// Publish RightKitSwift by the ONE Swift distribution contract: a
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// `
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
1
|
+
// Publish RightKitSwift by the ONE Swift distribution contract: a plain semver git tag on this
|
|
2
|
+
// repository (github.com/Orthic-Labs/rightkit), resolved by apps through the root Package.swift
|
|
3
|
+
// (docs/RIGHTKIT-SHARED-PACKAGES.md, Swift section). There is no mirror and no second repository.
|
|
4
|
+
//
|
|
5
|
+
// `right-release publish swift [--dry-run] [--evidence <file>] [--workspace <dir>]` verifies, in order:
|
|
6
|
+
// 1. VERSION (swift/RightKitSwift/VERSION) is plain semver (X.Y.Z, no "v", no prerelease);
|
|
7
|
+
// 2. the checkout is on main, has a clean tree, and HEAD is reachable from origin/main;
|
|
8
|
+
// 3. the root Package.swift and the nested manifest agree (scripts/swift-root-manifest.test.mjs);
|
|
9
|
+
// 4. the tag <VERSION> exists neither locally nor on origin;
|
|
10
|
+
// 5. the publication gate evidence is bound to HEAD and to the package tree at HEAD;
|
|
11
|
+
// then creates the tag on HEAD and pushes it:
|
|
12
|
+
// git tag <VERSION> <HEAD sha>
|
|
13
|
+
// git push origin refs/tags/<VERSION>:refs/tags/<VERSION>
|
|
14
|
+
// A dry-run performs every check and prints those commands without running them.
|
|
9
15
|
import fs from "node:fs";
|
|
10
|
-
import os from "node:os";
|
|
11
16
|
import path from "node:path";
|
|
12
17
|
import { fileURLToPath } from "node:url";
|
|
13
18
|
import { spawnSync } from "./background-process.mjs";
|
|
14
19
|
import { digestBytes } from "./publication-artifacts.mjs";
|
|
15
|
-
import { scanCrateForSecrets } from "./publish-cargo.mjs";
|
|
16
20
|
import { enforcePublicationGate, defaultEvidencePath } from "./publication-gate.mjs";
|
|
17
21
|
|
|
18
|
-
export const
|
|
19
|
-
|
|
20
|
-
// compared), so only untracked build state is excluded. Tracked Xcode scheme
|
|
21
|
-
// metadata under .swiftpm ships with the package; SwiftPM consumers ignore it.
|
|
22
|
-
const MIRROR_EXCLUDES = new Set([".build", ".git", "Package.resolved", ".DS_Store"]);
|
|
22
|
+
export const SWIFT_DISTRIBUTION_URL = "https://github.com/Orthic-Labs/rightkit";
|
|
23
|
+
const DRIFT_TEST = path.join("scripts", "swift-root-manifest.test.mjs");
|
|
23
24
|
|
|
24
|
-
export function parseSwiftPublishArgs(argv
|
|
25
|
-
const allowed = new Set(["--
|
|
26
|
-
let repo = env.RIGHTKIT_SWIFT_GIT_URL ?? DEFAULT_SWIFT_GIT_URL;
|
|
25
|
+
export function parseSwiftPublishArgs(argv) {
|
|
26
|
+
const allowed = new Set(["--dry-run", "--evidence", "--workspace"]);
|
|
27
27
|
let dryRun = false;
|
|
28
|
-
let allowDirty = false;
|
|
29
28
|
let evidence;
|
|
30
29
|
let workspace;
|
|
31
30
|
for (let index = 0; index < argv.length; index += 1) {
|
|
32
31
|
const arg = argv[index];
|
|
33
32
|
if (!allowed.has(arg)) throw new Error(`unexpected argument: ${arg}`);
|
|
34
|
-
if (arg === "--dry-run") dryRun = true;
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
if (arg === "--evidence") evidence = value;
|
|
40
|
-
else if (arg === "--workspace") workspace = value;
|
|
41
|
-
else repo = value;
|
|
42
|
-
}
|
|
33
|
+
if (arg === "--dry-run") { dryRun = true; continue; }
|
|
34
|
+
const value = argv[(index += 1)];
|
|
35
|
+
if (value === undefined) throw new Error(`${arg} needs a value`);
|
|
36
|
+
if (arg === "--evidence") evidence = value;
|
|
37
|
+
else workspace = value;
|
|
43
38
|
}
|
|
44
|
-
|
|
45
|
-
if (allowDirty && !dryRun) throw new Error("--allow-dirty is only valid with --dry-run");
|
|
46
|
-
return { repo, dryRun, allowDirty, evidence, workspace };
|
|
39
|
+
return { dryRun, evidence, workspace };
|
|
47
40
|
}
|
|
48
41
|
|
|
49
|
-
//
|
|
50
|
-
// version must be a clean semver so the registry archive is reproducible.
|
|
42
|
+
// VERSION is the single source of truth; the tag is exactly its text.
|
|
51
43
|
export function readPackageIdentity(packageDir) {
|
|
52
44
|
const manifest = path.join(packageDir, "Package.swift");
|
|
53
45
|
if (!fs.existsSync(manifest)) throw new Error(`Package.swift not found: ${manifest}`);
|
|
54
46
|
const versionFile = path.join(packageDir, "VERSION");
|
|
55
47
|
if (!fs.existsSync(versionFile)) throw new Error(`VERSION not found: ${versionFile}`);
|
|
56
48
|
const version = fs.readFileSync(versionFile, "utf8").trim();
|
|
57
|
-
if (!/^\d+\.\d+\.\d
|
|
58
|
-
throw new Error(`VERSION is not clean semver: "${version}"`);
|
|
59
|
-
}
|
|
49
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) throw new Error(`VERSION is not plain semver (X.Y.Z): "${version}"`);
|
|
60
50
|
const nameMatch = fs.readFileSync(manifest, "utf8").match(/name:\s*"([^"]+)"/);
|
|
61
51
|
if (!nameMatch) throw new Error("could not read package name from Package.swift");
|
|
62
52
|
return { name: nameMatch[1], version };
|
|
63
53
|
}
|
|
64
54
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
// Dry-run mirrors into a fresh local repo; a real publish clones the distribution repo first.
|
|
71
|
-
{ label: "mirror and tag", internal: "mirror", tag: version, repo: dryRun ? null : repo },
|
|
72
|
-
{ label: "fresh consumer resolution (exact pin)", internal: "consume", tag: version, name },
|
|
55
|
+
// The exact commands a real publish runs after every check passes.
|
|
56
|
+
export function swiftTagCommands(version, sha) {
|
|
57
|
+
return [
|
|
58
|
+
["git", ["tag", version, sha]],
|
|
59
|
+
["git", ["push", "origin", `refs/tags/${version}:refs/tags/${version}`]],
|
|
73
60
|
];
|
|
74
|
-
if (!dryRun) plan.push({ label: "git push (branch and tag)", internal: "push", tag: version, repo });
|
|
75
|
-
return plan;
|
|
76
61
|
}
|
|
77
62
|
|
|
78
|
-
function git(args, cwd, execute
|
|
63
|
+
function git(args, cwd, execute) {
|
|
79
64
|
const result = execute("git", args, { cwd, encoding: "utf8", windowsHide: true });
|
|
80
65
|
if (result.error) throw result.error;
|
|
81
66
|
if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${(result.stderr ?? "").trim()}`);
|
|
82
67
|
return (result.stdout ?? "").trim();
|
|
83
68
|
}
|
|
84
69
|
|
|
85
|
-
|
|
86
|
-
fs.mkdirSync(to, { recursive: true });
|
|
87
|
-
for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
|
|
88
|
-
if (MIRROR_EXCLUDES.has(entry.name)) continue;
|
|
89
|
-
const source = path.join(from, entry.name);
|
|
90
|
-
const target = path.join(to, entry.name);
|
|
91
|
-
if (entry.isDirectory()) copyTree(source, target);
|
|
92
|
-
else if (entry.isFile()) { fs.copyFileSync(source, target); fs.chmodSync(target, fs.statSync(source).mode & 0o777); }
|
|
93
|
-
else throw new Error(`unsupported Swift source entry: ${entry.name}`);
|
|
94
|
-
}
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
function mirrorAndTag(step, packageDir, work, execute) {
|
|
98
|
-
const mirror = path.join(work, "rightkit-swift"); // SwiftPM names the package by repo directory
|
|
99
|
-
if (step.repo) git(["clone", step.repo, mirror], work, execute);
|
|
100
|
-
else { fs.mkdirSync(mirror, { recursive: true }); git(["init", "-q", "-b", "main"], mirror, execute); }
|
|
101
|
-
for (const entry of fs.readdirSync(mirror)) if (entry !== ".git") fs.rmSync(path.join(mirror, entry), { recursive: true, force: true });
|
|
102
|
-
copyTree(packageDir, mirror);
|
|
103
|
-
git(["add", "-A"], mirror, execute);
|
|
104
|
-
git(["-c", "user.email=release@rightsuite.invalid", "-c", "user.name=right-release", "commit", "-q", "--allow-empty", "-m", `RightKitSwift ${step.tag}`], mirror, execute);
|
|
105
|
-
if (git(["tag", "-l", step.tag], mirror, execute) !== "") throw new Error(`tag ${step.tag} already exists: bump VERSION`);
|
|
106
|
-
git(["tag", step.tag], mirror, execute);
|
|
107
|
-
return mirror;
|
|
108
|
-
}
|
|
109
|
-
|
|
110
|
-
function consumeFresh(step, mirror, work, execute) {
|
|
111
|
-
const app = path.join(work, "consumer");
|
|
112
|
-
fs.mkdirSync(path.join(app, "Sources", "App"), { recursive: true });
|
|
113
|
-
fs.writeFileSync(path.join(app, "Package.swift"), [
|
|
114
|
-
"// swift-tools-version: 6.0", "import PackageDescription", "let package = Package(",
|
|
115
|
-
' name: "App", platforms: [.iOS(.v17), .macOS(.v14)],',
|
|
116
|
-
` dependencies: [.package(url: "file://${mirror}", exact: "${step.tag}")],`,
|
|
117
|
-
' targets: [.target(name: "App", dependencies: [.product(name: "RightKitStore", package: "rightkit-swift")])]', ")", "",
|
|
118
|
-
].join("\n"));
|
|
119
|
-
fs.writeFileSync(path.join(app, "Sources", "App", "App.swift"), "import RightKitStore\npublic let ok = true\n");
|
|
120
|
-
const result = execute("swift", ["package", "resolve"], { cwd: app, encoding: "utf8", windowsHide: true, stdio: "inherit" });
|
|
121
|
-
if (result.error || result.status !== 0) throw new Error("fresh consumer resolution failed");
|
|
122
|
-
const pins = fs.readFileSync(path.join(app, "Package.resolved"), "utf8");
|
|
123
|
-
if (!pins.includes(`"version" : "${step.tag}"`)) throw new Error(`consumer did not resolve exact tag ${step.tag}`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
// Evidence binds this exact git-archive command, rooted at the package tree.
|
|
70
|
+
// Evidence binds this exact git-archive of the package tree at HEAD.
|
|
127
71
|
export function verifySwiftSourceArchive(bytes, evidence) {
|
|
128
72
|
if (!/^[a-f0-9]{64}$/.test(evidence?.artifactSha256 ?? "")) throw new Error("Swift source archive hash evidence is required");
|
|
129
73
|
if (digestBytes(bytes) !== evidence.artifactSha256) throw new Error("Swift source archive sha256 does not match evidence artifactSha256");
|
|
@@ -133,70 +77,63 @@ export function verifySwiftSourceArchive(bytes, evidence) {
|
|
|
133
77
|
export function runSwiftPublish(options, execute = spawnSync) {
|
|
134
78
|
const packageDir = options.packageDir;
|
|
135
79
|
const { name, version } = readPackageIdentity(packageDir);
|
|
80
|
+
const toplevel = git(["rev-parse", "--show-toplevel"], packageDir, execute);
|
|
81
|
+
const out = (message) => process.stdout.write(`[right-release] swift ${message}\n`);
|
|
82
|
+
|
|
83
|
+
const branch = git(["symbolic-ref", "--short", "-q", "HEAD"], toplevel, execute);
|
|
84
|
+
if (branch !== "main") throw new Error(`publish swift runs only on main (current: ${branch || "detached HEAD"})`);
|
|
85
|
+
if (git(["status", "--porcelain", "--untracked-files=normal"], toplevel, execute) !== "") throw new Error("working tree is not clean: commit or remove changes before tagging");
|
|
86
|
+
git(["fetch", "--quiet", "origin", "main"], toplevel, execute);
|
|
87
|
+
const sha = git(["rev-parse", "HEAD"], toplevel, execute);
|
|
88
|
+
const ancestor = execute("git", ["merge-base", "--is-ancestor", sha, "origin/main"], { cwd: toplevel, encoding: "utf8", windowsHide: true });
|
|
89
|
+
if (ancestor.error) throw ancestor.error;
|
|
90
|
+
if (ancestor.status !== 0) throw new Error(`HEAD ${sha} is not on origin/main: push it and let CI pass first`);
|
|
91
|
+
out("branch, clean tree, HEAD on origin/main: ok");
|
|
92
|
+
|
|
93
|
+
const drift = execute(process.execPath, ["--test", DRIFT_TEST], { cwd: toplevel, encoding: "utf8", windowsHide: true });
|
|
94
|
+
if (drift.error) throw drift.error;
|
|
95
|
+
if (drift.status !== 0) throw new Error(`root Package.swift drifted from swift/RightKitSwift/Package.swift:\n${drift.stdout ?? ""}${drift.stderr ?? ""}`);
|
|
96
|
+
out("root manifest matches nested manifest: ok");
|
|
97
|
+
|
|
98
|
+
if (git(["tag", "-l", version], toplevel, execute) !== "") throw new Error(`tag ${version} already exists locally: bump VERSION`);
|
|
99
|
+
if (git(["ls-remote", "--tags", "origin", `refs/tags/${version}`], toplevel, execute) !== "") throw new Error(`tag ${version} already exists on origin: bump VERSION`);
|
|
100
|
+
out(`tag ${version} is free: ok`);
|
|
101
|
+
|
|
136
102
|
const workspaceRoot = path.resolve(options.workspace ?? path.join(packageDir, "..", ".."));
|
|
137
103
|
const gate = enforcePublicationGate({
|
|
138
104
|
kind: "swift", name, version, packagePath: packageDir,
|
|
139
105
|
evidencePath: path.resolve(options.evidence ?? defaultEvidencePath(workspaceRoot, "swift", name)),
|
|
140
106
|
}, { dryRun: options.dryRun });
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
const snapshot = path.join(work, "source");
|
|
156
|
-
fs.mkdirSync(snapshot);
|
|
157
|
-
try {
|
|
158
|
-
const extracted = execute("tar", ["-xf", "-", "-C", snapshot], { input: bytes, windowsHide: true });
|
|
159
|
-
if (extracted.error || extracted.status !== 0) throw new Error("Swift source archive extraction failed");
|
|
160
|
-
const identity = readPackageIdentity(snapshot);
|
|
161
|
-
if (identity.name !== name || identity.version !== version) throw new Error("Swift source archive identity mismatch");
|
|
162
|
-
for (const step of plan) {
|
|
163
|
-
process.stdout.write(`[right-release] swift ${step.label}\n`);
|
|
164
|
-
if (step.internal === "secret-scan") {
|
|
165
|
-
const violations = scanCrateForSecrets(snapshot);
|
|
166
|
-
if (violations.length) throw new Error(`secret scan rejected: ${violations.join(", ")}`);
|
|
167
|
-
continue;
|
|
168
|
-
}
|
|
169
|
-
if (step.internal === "mirror") { mirror = mirrorAndTag(step, snapshot, work, execute);
|
|
170
|
-
if (git(["rev-parse", `${step.tag}^{tree}`], mirror, execute) !== tree) throw new Error("Swift mirror differs from verified source archive");
|
|
171
|
-
sealedCommit = git(["rev-parse", step.tag], mirror, execute);
|
|
172
|
-
continue; }
|
|
173
|
-
if (step.internal === "consume") { consumeFresh(step, mirror, work, execute); continue; }
|
|
174
|
-
if (step.internal === "push") {
|
|
175
|
-
if (git(["rev-parse", `${sealedCommit}^{tree}`], mirror, execute) !== tree || git(["rev-parse", step.tag], mirror, execute) !== sealedCommit) throw new Error("Swift publication tag changed after verification");
|
|
176
|
-
const branch = git(["symbolic-ref", "--short", "HEAD"], mirror, execute);
|
|
177
|
-
git(["push", "--atomic", "origin", `${sealedCommit}:refs/heads/${branch}`, `${sealedCommit}:refs/tags/${step.tag}`], mirror, execute);
|
|
178
|
-
continue;
|
|
179
|
-
}
|
|
180
|
-
const result = execute(step.command, step.args.map((arg) => arg === packageDir ? snapshot : arg), { cwd: snapshot, encoding: "utf8", windowsHide: true, stdio: "inherit" });
|
|
181
|
-
if (result.error) {
|
|
182
|
-
if (result.error.code === "ENOENT") {
|
|
183
|
-
throw new Error(`${step.command} not found -- run publish swift on macOS/Linux with the Swift toolchain installed`);
|
|
184
|
-
}
|
|
185
|
-
throw result.error;
|
|
186
|
-
}
|
|
187
|
-
if (result.status !== 0) throw new Error(`${step.label} failed with exit code ${result.status}`);
|
|
188
|
-
}
|
|
189
|
-
} finally {
|
|
190
|
-
fs.rmSync(work, { recursive: true, force: true });
|
|
107
|
+
if (gate.ok) {
|
|
108
|
+
if (gate.evidence?.commit !== sha) throw new Error(`publication evidence commit ${gate.evidence?.commit} is not HEAD ${sha}`);
|
|
109
|
+
const prefix = git(["rev-parse", "--show-prefix"], packageDir, execute).replace(/\/$/, "");
|
|
110
|
+
const tree = git(["rev-parse", prefix ? `${sha}:${prefix}` : `${sha}^{tree}`], packageDir, execute);
|
|
111
|
+
// `git archive` filters by the current subdirectory, so archive the tree id from the toplevel.
|
|
112
|
+
const archive = execute("git", ["archive", "--format=tar", "--mtime=1970-01-01T00:00:00Z", tree], { cwd: toplevel, windowsHide: true, maxBuffer: 128 * 1024 * 1024 });
|
|
113
|
+
if (archive.error || archive.status !== 0 || !archive.stdout?.length) throw new Error("Swift package tree archive creation failed");
|
|
114
|
+
verifySwiftSourceArchive(Buffer.from(archive.stdout), gate.evidence);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const commands = swiftTagCommands(version, sha);
|
|
118
|
+
if (options.dryRun) {
|
|
119
|
+
for (const [command, args] of commands) out(`dry-run would run: ${command} ${args.join(" ")}`);
|
|
120
|
+
return { name, version, commit: sha, published: false };
|
|
191
121
|
}
|
|
192
|
-
|
|
122
|
+
const [tagCommand, pushCommand] = commands;
|
|
123
|
+
git(tagCommand[1], toplevel, execute);
|
|
124
|
+
try { git(pushCommand[1], toplevel, execute); }
|
|
125
|
+
catch (error) { git(["tag", "-d", version], toplevel, execute); throw error; }
|
|
126
|
+
const remote = git(["ls-remote", "--tags", "origin", `refs/tags/${version}`], toplevel, execute);
|
|
127
|
+
if (!remote.startsWith(sha)) throw new Error(`origin does not show tag ${version} at ${sha}`);
|
|
128
|
+
out(`tagged ${version} at ${sha} on ${SWIFT_DISTRIBUTION_URL}`);
|
|
129
|
+
return { name, version, commit: sha, published: true };
|
|
193
130
|
}
|
|
194
131
|
|
|
195
132
|
const realOrSelf = (file) => { try { return fs.realpathSync(file); } catch { return path.resolve(file); } };
|
|
196
133
|
if (process.argv[1] && realOrSelf(process.argv[1]) === realOrSelf(fileURLToPath(import.meta.url))) {
|
|
197
134
|
try {
|
|
198
135
|
const options = parseSwiftPublishArgs(process.argv.slice(2));
|
|
199
|
-
const packageDir = path.resolve(path.dirname(
|
|
136
|
+
const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../swift/RightKitSwift");
|
|
200
137
|
runSwiftPublish({ ...options, packageDir });
|
|
201
138
|
} catch (error) {
|
|
202
139
|
process.stderr.write(`${error.message}\n`);
|
package/publish-update.mjs
CHANGED
|
@@ -114,6 +114,7 @@ if (!dryRun && (tier === "update" || artifacts.length > 0)) {
|
|
|
114
114
|
if (tier === "patch") {
|
|
115
115
|
for (const installer of installers) {
|
|
116
116
|
assertCurrentKey(installer.key, "installer");
|
|
117
|
+
assertVersionedKey(installer.versionedKey);
|
|
117
118
|
artifactFile(installer.file);
|
|
118
119
|
}
|
|
119
120
|
const platforms = {};
|
|
@@ -161,6 +162,13 @@ if (tier === "patch") {
|
|
|
161
162
|
...metadata,
|
|
162
163
|
updaterSignature: null,
|
|
163
164
|
});
|
|
165
|
+
// The versioned copy is the App Installer install source: Windows repairs by re-adding from the URI the
|
|
166
|
+
// package came from, so it must outlive the version that replaces it.
|
|
167
|
+
if (installer.versionedKey && !uploadedKeys.has(installer.versionedKey)) {
|
|
168
|
+
console.log(`${dryRun ? "[dry-run] " : ""}UPLOAD public/${installer.versionedKey}`);
|
|
169
|
+
if (!dryRun) uploadVerifiedR2Artifact(verifiedArtifact(file), installer.versionedKey, "public", { cwd: root });
|
|
170
|
+
uploadedKeys.add(installer.versionedKey);
|
|
171
|
+
}
|
|
164
172
|
}
|
|
165
173
|
if (!artifacts.length) {
|
|
166
174
|
console.log(`${dryRun ? "[dry-run] " : ""}NO patch updater artifacts; skipping patch manifest registration`);
|
|
@@ -169,9 +177,10 @@ if (tier === "patch") {
|
|
|
169
177
|
platform: platform === "mac" ? "mac" : "windows",
|
|
170
178
|
dryRun,
|
|
171
179
|
keep: {
|
|
172
|
-
public:
|
|
180
|
+
public: installerKeepKeys(),
|
|
173
181
|
private: target.updater.artifacts.map((artifact) => artifact.key),
|
|
174
182
|
},
|
|
183
|
+
retainPreviousVersions: installers.some((artifact) => artifact.versionedKey) ? 1 : 0,
|
|
175
184
|
});
|
|
176
185
|
if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform, configPath, releaseId });
|
|
177
186
|
process.exit(0);
|
|
@@ -207,9 +216,10 @@ if (tier === "patch") {
|
|
|
207
216
|
platform: platform === "mac" ? "mac" : "windows",
|
|
208
217
|
dryRun,
|
|
209
218
|
keep: {
|
|
210
|
-
public: [...
|
|
219
|
+
public: [...installerKeepKeys(), ...registrations.keys()],
|
|
211
220
|
private: [],
|
|
212
221
|
},
|
|
222
|
+
retainPreviousVersions: installers.some((artifact) => artifact.versionedKey) ? 1 : 0,
|
|
213
223
|
});
|
|
214
224
|
if (!dryRun) recordPublishedVersion({ app: config.app, version: versionForStorage, platform, configPath, releaseId });
|
|
215
225
|
process.exit(0);
|
|
@@ -311,6 +321,18 @@ function assertCurrentKey(key, kind) {
|
|
|
311
321
|
}
|
|
312
322
|
}
|
|
313
323
|
|
|
324
|
+
// Current + versioned installer keys: both stay public so the old install source is never deleted.
|
|
325
|
+
function installerKeepKeys() {
|
|
326
|
+
return installers.flatMap((artifact) => [artifact.key, ...(artifact.versionedKey ? [artifact.versionedKey] : [])]);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function assertVersionedKey(key) {
|
|
330
|
+
if (key === undefined) return;
|
|
331
|
+
if (!/\/installers\/(?:mac|windows)\/versions\/[^/]+\/[^/]+$/.test(key)) {
|
|
332
|
+
fail(`installer versionedKey must be <app>/installers/<os>/versions/<version>/<file>, got: ${key}`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
314
336
|
function patchInstallerKey(artifact) {
|
|
315
337
|
if (artifact.patchKey) return artifact.patchKey;
|
|
316
338
|
if (!artifact.key.includes("/updates/")) fail(`patch updater key must be convertible to installers/current, got: ${artifact.key}`);
|