@rightkit/release 0.2.111 → 0.2.112

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/publish-swift.mjs CHANGED
@@ -1,25 +1,28 @@
1
- #!/usr/bin/env node
2
- // Publish RightKitSwift to a Swift package registry, gated exactly like `publish cargo`:
3
- // secret scan -> swift test -> release build -> deterministic source archive + hash ->
4
- // registry dry-run -> publish. Canonical source stays under tools/rightkit/swift; the
5
- // registry package is an output only. No Git distribution repo, no path/Git dependency.
6
- //
1
+ // Publish RightKitSwift by the ONE Swift distribution contract: a git repository pinned by URL +
2
+ // exact version tag (docs/RIGHTKIT-SHARED-PACKAGES.md, Swift section). Gates, in order:
3
+ // secret scan -> swift test -> release build -> mirror tree into a git checkout + tag ->
4
+ // FRESH CONSUMER RESOLUTION (a throwaway app pins the tag with `exact:` and runs
5
+ // `swift package resolve`) -> push. A dry-run performs everything except the push, so a green
6
+ // dry-run proves the mirror layout, the tag, and clean consumer resolution -- not just that tests passed.
7
7
  // swift-tools is macOS/Linux only, so the plan builder, arg parser, and version check are
8
8
  // pure and unit-tested on any OS (publish-swift.test.mjs); the swift steps run where swift exists.
9
9
  import fs from "node:fs";
10
+ import os from "node:os";
10
11
  import path from "node:path";
11
- import { createHash } from "node:crypto";
12
12
  import { spawnSync } from "./background-process.mjs";
13
13
  import { scanCrateForSecrets } from "./publish-cargo.mjs";
14
+ import { enforcePublicationGate, defaultEvidencePath } from "./publication-gate.mjs";
14
15
 
15
- const IDENTIFIER = /^[a-z0-9]+(?:[-_][a-z0-9]+)*$/i;
16
+ export const DEFAULT_SWIFT_GIT_URL = "https://github.com/bogusyogi/rightkit-swift.git";
17
+ const MIRROR_EXCLUDES = new Set([".build", ".swiftpm", ".git", "Package.resolved", ".DS_Store"]);
16
18
 
17
19
  export function parseSwiftPublishArgs(argv, env = process.env) {
18
- const allowed = new Set(["--scope", "--url", "--dry-run", "--allow-dirty"]);
19
- let scope = env.RIGHTKIT_SWIFT_REGISTRY_SCOPE ?? "rightsuite";
20
- let url = env.RIGHTKIT_SWIFT_REGISTRY_URL ?? "";
20
+ const allowed = new Set(["--repo", "--dry-run", "--allow-dirty", "--evidence", "--workspace"]);
21
+ let repo = env.RIGHTKIT_SWIFT_GIT_URL ?? DEFAULT_SWIFT_GIT_URL;
21
22
  let dryRun = false;
22
23
  let allowDirty = false;
24
+ let evidence;
25
+ let workspace;
23
26
  for (let index = 0; index < argv.length; index += 1) {
24
27
  const arg = argv[index];
25
28
  if (!allowed.has(arg)) throw new Error(`unexpected argument: ${arg}`);
@@ -28,14 +31,14 @@ export function parseSwiftPublishArgs(argv, env = process.env) {
28
31
  else {
29
32
  const value = argv[(index += 1)];
30
33
  if (value === undefined) throw new Error(`${arg} needs a value`);
31
- if (arg === "--scope") scope = value;
32
- if (arg === "--url") url = value;
34
+ if (arg === "--evidence") evidence = value;
35
+ else if (arg === "--workspace") workspace = value;
36
+ else repo = value;
33
37
  }
34
38
  }
35
- if (!IDENTIFIER.test(scope)) throw new Error(`invalid registry scope: ${scope}`);
39
+ if (!/^(https:\/\/|git@|ssh:\/\/|file:\/\/)/.test(repo)) throw new Error(`invalid swift git url: ${repo}`);
36
40
  if (allowDirty && !dryRun) throw new Error("--allow-dirty is only valid with --dry-run");
37
- if (!dryRun && !url) throw new Error("registry publish needs --url or RIGHTKIT_SWIFT_REGISTRY_URL");
38
- return { scope, url, dryRun, allowDirty };
41
+ return { repo, dryRun, allowDirty, evidence, workspace };
39
42
  }
40
43
 
41
44
  // The VERSION file is the single source of truth; Package.swift must name the package and the
@@ -54,58 +57,103 @@ export function readPackageIdentity(packageDir) {
54
57
  return { name: nameMatch[1], version };
55
58
  }
56
59
 
57
- export function buildSwiftPublishPlan({ packageDir, scope, name, version, url, dryRun, allowDirty = false }) {
58
- const packageId = `${scope}.${name}`;
59
- const archive = path.join(packageDir, ".build", `${name}-${version}.zip`);
60
+ export function buildSwiftPublishPlan({ packageDir, name, version, repo, dryRun }) {
60
61
  const plan = [
61
62
  { label: "secret scan", internal: "secret-scan" },
62
63
  { label: "tests", command: "swift", args: ["test", "--package-path", packageDir] },
63
64
  { label: "release build", command: "swift", args: ["build", "-c", "release", "--package-path", packageDir] },
64
- // Deterministic source archive; its bytes are what the registry stores, so hash it for the log.
65
- { label: "source archive", command: "swift", args: ["package", "--package-path", packageDir, "archive-source", "-o", archive] },
66
- { label: "archive hash", internal: "archive-hash", archive },
65
+ // Dry-run mirrors into a fresh local repo; a real publish clones the distribution repo first.
66
+ { label: "mirror and tag", internal: "mirror", tag: version, repo: dryRun ? null : repo },
67
+ { label: "fresh consumer resolution (exact pin)", internal: "consume", tag: version, name },
67
68
  ];
68
- if (dryRun) {
69
- plan.push({ label: "registry dry-run (validate only)", internal: "dry-run-note", packageId, version });
70
- return plan;
71
- }
72
- plan.push({
73
- label: "registry publish",
74
- command: "swift",
75
- args: ["package-registry", "publish", packageId, version, "--url", url, "--package-path", packageDir],
76
- });
69
+ if (!dryRun) plan.push({ label: "git push (branch and tag)", internal: "push", tag: version, repo });
77
70
  return plan;
78
71
  }
79
72
 
73
+ function git(args, cwd) {
74
+ const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true });
75
+ if (result.error) throw result.error;
76
+ if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${(result.stderr ?? "").trim()}`);
77
+ return (result.stdout ?? "").trim();
78
+ }
79
+
80
+ function copyTree(from, to) {
81
+ fs.mkdirSync(to, { recursive: true });
82
+ for (const entry of fs.readdirSync(from, { withFileTypes: true })) {
83
+ if (MIRROR_EXCLUDES.has(entry.name)) continue;
84
+ const source = path.join(from, entry.name);
85
+ const target = path.join(to, entry.name);
86
+ if (entry.isDirectory()) copyTree(source, target);
87
+ else if (entry.isFile()) fs.copyFileSync(source, target);
88
+ }
89
+ }
90
+
91
+ function mirrorAndTag(step, packageDir, work) {
92
+ const mirror = path.join(work, "rightkit-swift"); // SwiftPM names the package by repo directory
93
+ if (step.repo) git(["clone", step.repo, mirror], work);
94
+ else { fs.mkdirSync(mirror, { recursive: true }); git(["init", "-q", "-b", "main"], mirror); }
95
+ for (const entry of fs.readdirSync(mirror)) if (entry !== ".git") fs.rmSync(path.join(mirror, entry), { recursive: true, force: true });
96
+ copyTree(packageDir, mirror);
97
+ git(["add", "-A"], mirror);
98
+ git(["-c", "user.email=release@rightsuite.invalid", "-c", "user.name=right-release", "commit", "-q", "--allow-empty", "-m", `RightKitSwift ${step.tag}`], mirror);
99
+ if (git(["tag", "-l", step.tag], mirror) !== "") throw new Error(`tag ${step.tag} already exists: bump VERSION`);
100
+ git(["tag", step.tag], mirror);
101
+ return mirror;
102
+ }
103
+
104
+ function consumeFresh(step, mirror, work, execute) {
105
+ const app = path.join(work, "consumer");
106
+ fs.mkdirSync(path.join(app, "Sources", "App"), { recursive: true });
107
+ fs.writeFileSync(path.join(app, "Package.swift"), [
108
+ "// swift-tools-version: 6.0", "import PackageDescription", "let package = Package(",
109
+ ' name: "App", platforms: [.iOS(.v17), .macOS(.v14)],',
110
+ ` dependencies: [.package(url: "file://${mirror}", exact: "${step.tag}")],`,
111
+ ' targets: [.target(name: "App", dependencies: [.product(name: "RightKitStore", package: "rightkit-swift")])]', ")", "",
112
+ ].join("\n"));
113
+ fs.writeFileSync(path.join(app, "Sources", "App", "App.swift"), "import RightKitStore\npublic let ok = true\n");
114
+ const result = execute("swift", ["package", "resolve"], { cwd: app, encoding: "utf8", windowsHide: true, stdio: "inherit" });
115
+ if (result.error || result.status !== 0) throw new Error("fresh consumer resolution failed");
116
+ const pins = fs.readFileSync(path.join(app, "Package.resolved"), "utf8");
117
+ if (!pins.includes(`"version" : "${step.tag}"`)) throw new Error(`consumer did not resolve exact tag ${step.tag}`);
118
+ }
119
+
80
120
  export function runSwiftPublish(options, execute = spawnSync) {
81
121
  const packageDir = options.packageDir;
82
122
  const { name, version } = readPackageIdentity(packageDir);
123
+ const workspaceRoot = path.resolve(options.workspace ?? path.join(packageDir, "..", ".."));
124
+ enforcePublicationGate({
125
+ kind: "swift", name, version, packagePath: packageDir,
126
+ evidencePath: path.resolve(options.evidence ?? defaultEvidencePath(workspaceRoot, "swift", name)),
127
+ }, { dryRun: options.dryRun });
83
128
  const plan = buildSwiftPublishPlan({ ...options, packageDir, name, version });
84
-
85
- for (const step of plan) {
86
- process.stdout.write(`[right-release] swift ${step.label}\n`);
87
- if (step.internal === "secret-scan") {
88
- const violations = scanCrateForSecrets(packageDir);
89
- if (violations.length) throw new Error(`secret scan rejected: ${violations.join(", ")}`);
90
- continue;
91
- }
92
- if (step.internal === "dry-run-note") {
93
- process.stdout.write(`[right-release] dry-run OK: ${step.packageId} ${step.version} (not published)\n`);
94
- continue;
95
- }
96
- if (step.internal === "archive-hash") {
97
- const bytes = fs.readFileSync(step.archive);
98
- process.stdout.write(`sha256 ${createHash("sha256").update(bytes).digest("hex")} ${path.basename(step.archive)}\n`);
99
- continue;
100
- }
101
- const result = execute(step.command, step.args, { cwd: packageDir, encoding: "utf8", windowsHide: true, stdio: "inherit" });
102
- if (result.error) {
103
- if (result.error.code === "ENOENT") {
104
- throw new Error(`${step.command} not found — run publish swift on macOS/Linux with the Swift toolchain installed`);
129
+ const work = fs.mkdtempSync(path.join(os.tmpdir(), "rightkit-swift-publish-"));
130
+ let mirror = null;
131
+ try {
132
+ for (const step of plan) {
133
+ process.stdout.write(`[right-release] swift ${step.label}\n`);
134
+ if (step.internal === "secret-scan") {
135
+ const violations = scanCrateForSecrets(packageDir);
136
+ if (violations.length) throw new Error(`secret scan rejected: ${violations.join(", ")}`);
137
+ continue;
138
+ }
139
+ if (step.internal === "mirror") { mirror = mirrorAndTag(step, packageDir, work); continue; }
140
+ if (step.internal === "consume") { consumeFresh(step, mirror, work, execute); continue; }
141
+ if (step.internal === "push") {
142
+ git(["push", "origin", "HEAD"], mirror);
143
+ git(["push", "origin", step.tag], mirror);
144
+ continue;
145
+ }
146
+ const result = execute(step.command, step.args, { cwd: packageDir, encoding: "utf8", windowsHide: true, stdio: "inherit" });
147
+ if (result.error) {
148
+ if (result.error.code === "ENOENT") {
149
+ throw new Error(`${step.command} not found -- run publish swift on macOS/Linux with the Swift toolchain installed`);
150
+ }
151
+ throw result.error;
105
152
  }
106
- throw result.error;
153
+ if (result.status !== 0) throw new Error(`${step.label} failed with exit code ${result.status}`);
107
154
  }
108
- if (result.status !== 0) throw new Error(`${step.label} failed with exit code ${result.status}`);
155
+ } finally {
156
+ fs.rmSync(work, { recursive: true, force: true });
109
157
  }
110
158
  return { name, version, published: !options.dryRun };
111
159
  }
@@ -65,8 +65,24 @@ async function extract(archive, destination) {
65
65
  return join(destination, 'package');
66
66
  }
67
67
 
68
- export async function verifyRegistryParity({ allowUnpublished = false, fetchImpl = fetch } = {}) {
69
- const manifest = JSON.parse(await readFile(join(packageRoot, 'package.json'), 'utf8'));
68
+ // Every npm package RightKit publishes: the names in rightkit-versions.json (adopted, staged, legacy).
69
+ export async function publishedPackageNames() {
70
+ const versions = JSON.parse(await readFile(join(packageRoot, 'rightkit-versions.json'), 'utf8'));
71
+ return [...new Set([...Object.keys(versions.npm ?? {}), ...Object.keys(versions.stagedNpm ?? {}), ...Object.keys(versions.legacyNpm ?? {})])].sort();
72
+ }
73
+
74
+ export async function verifyAllRegistryParity({ allowUnpublished = false, fetchImpl = fetch, names } = {}) {
75
+ const results = [];
76
+ for (const name of names ?? await publishedPackageNames()) {
77
+ results.push(await verifyRegistryParity({ allowUnpublished, fetchImpl, packageName: name }));
78
+ }
79
+ return Object.freeze(results);
80
+ }
81
+
82
+ export async function verifyRegistryParity({ allowUnpublished = false, fetchImpl = fetch, packageName = '@rightkit/release' } = {}) {
83
+ const sourceRoot = packageName === '@rightkit/release' ? packageRoot : join(packageRoot, '..', packageName.replace('@rightkit/', ''));
84
+ const manifest = JSON.parse(await readFile(join(sourceRoot, 'package.json'), 'utf8'));
85
+ if (manifest.name !== packageName) throw new Error(`source ${sourceRoot} is ${manifest.name}, expected ${packageName}`);
70
86
  const encodedName = encodeURIComponent(manifest.name);
71
87
  const metadata = await fetchImpl(`https://registry.npmjs.org/${encodedName}/${manifest.version}`);
72
88
  if (metadata.status === 404 && allowUnpublished) return Object.freeze({ status: 'UNPUBLISHED', name: manifest.name, version: manifest.version });
@@ -77,7 +93,7 @@ export async function verifyRegistryParity({ allowUnpublished = false, fetchImpl
77
93
  try {
78
94
  const localDir = join(root, 'local'); const publishedArchive = join(root, 'published.tgz');
79
95
  await mkdir(localDir, { recursive: true });
80
- const packed = runPnpm(['pack', '--pack-destination', localDir, '--json'], { cwd: packageRoot, encoding: 'utf8' });
96
+ const packed = runPnpm(['pack', '--pack-destination', localDir, '--json'], { cwd: sourceRoot, encoding: 'utf8' });
81
97
  const localArchive = join(localDir, basename(packedArchivePath(packed)));
82
98
  const response = await fetchImpl(record.dist.tarball);
83
99
  if (!response.ok) throw new Error(`registry tarball failed: HTTP ${response.status}`);
@@ -93,7 +109,9 @@ export async function verifyRegistryParity({ allowUnpublished = false, fetchImpl
93
109
  }
94
110
 
95
111
  if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
96
- verifyRegistryParity({ allowUnpublished: process.argv.includes('--allow-unpublished') })
112
+ const flag = process.argv.indexOf('--package');
113
+ const allowUnpublished = process.argv.includes('--allow-unpublished');
114
+ (flag > 0 ? verifyRegistryParity({ allowUnpublished, packageName: process.argv[flag + 1] }) : verifyAllRegistryParity({ allowUnpublished }))
97
115
  .then((result) => process.stdout.write(`${JSON.stringify(result)}\n`))
98
116
  .catch((error) => { process.stderr.write(`${error.message}\n`); process.exitCode = 1; });
99
117
  }
@@ -13,17 +13,26 @@
13
13
  "@rightkit/updates": "0.2.3"
14
14
  },
15
15
  "stagedNpm": {
16
+ "@rightkit/app-shell": "0.2.0",
16
17
  "@rightkit/ax": "0.2.4",
18
+ "@rightkit/doc-render": "0.1.0",
17
19
  "@rightkit/git": "0.2.25",
18
20
  "@rightkit/hooks": "0.1.1",
21
+ "@rightkit/i18n": "0.1.0",
19
22
  "@rightkit/legal": "0.3.1",
20
- "@rightkit/legal-ui": "0.1.1",
23
+ "@rightkit/legal-ui": "0.1.2",
21
24
  "@rightkit/license": "0.1.8",
22
25
  "@rightkit/logs": "0.1.4",
26
+ "@rightkit/media-ui": "0.2.0",
27
+ "@rightkit/onboarding": "0.2.0",
28
+ "@rightkit/pdf-ui": "0.1.0",
23
29
  "@rightkit/platform-ui": "0.1.1",
24
30
  "@rightkit/qa": "0.2.1",
25
- "@rightkit/release": "0.2.111",
31
+ "@rightkit/release": "0.2.112",
32
+ "@rightkit/settings-ui": "0.2.0",
33
+ "@rightkit/shell": "0.1.0",
26
34
  "@rightkit/tauri": "0.1.1",
35
+ "@rightkit/theme": "0.2.0",
27
36
  "@rightkit/updates": "0.2.4"
28
37
  },
29
38
  "legacyNpm": {
@@ -59,7 +68,8 @@
59
68
  "0.2.24"
60
69
  ],
61
70
  "@rightkit/legal-ui": [
62
- "0.1.0"
71
+ "0.1.0",
72
+ "0.1.1"
63
73
  ],
64
74
  "@rightkit/hooks": [
65
75
  "0.1.0"
@@ -121,11 +131,27 @@
121
131
  "0.2.107",
122
132
  "0.2.108",
123
133
  "0.2.109",
124
- "0.2.110"
134
+ "0.2.110",
135
+ "0.2.111"
125
136
  ],
126
137
  "@rightkit/qa": [
127
138
  "0.1.0",
128
139
  "0.2.0"
140
+ ],
141
+ "@rightkit/app-shell": [
142
+ "0.1.0"
143
+ ],
144
+ "@rightkit/media-ui": [
145
+ "0.1.0"
146
+ ],
147
+ "@rightkit/theme": [
148
+ "0.1.0"
149
+ ],
150
+ "@rightkit/settings-ui": [
151
+ "0.1.0"
152
+ ],
153
+ "@rightkit/onboarding": [
154
+ "0.1.0"
129
155
  ]
130
156
  },
131
157
  "cargo": {
@@ -135,6 +161,30 @@
135
161
  "rightkit-tauri": "0.1.0"
136
162
  },
137
163
  "stagedCargo": {
164
+ "rightkit-automation": "0.1.0",
165
+ "rightkit-audio": "0.1.0",
166
+ "rightkit-browser": "0.1.0",
167
+ "rightkit-cache": "0.1.0",
168
+ "rightkit-control": "0.1.0",
169
+ "rightkit-crash": "0.1.0",
170
+ "rightkit-db": "0.1.0",
171
+ "rightkit-framed-sidecar": "0.1.0",
172
+ "rightkit-fs": "0.1.0",
173
+ "rightkit-jobs": "0.1.0",
174
+ "rightkit-logs": "0.2.0",
175
+ "rightkit-media": "0.1.0",
176
+ "rightkit-models": "0.1.0",
177
+ "rightkit-pdf": "0.1.0",
178
+ "rightkit-playback": "0.1.0",
179
+ "rightkit-process": "0.2.0",
180
+ "rightkit-qa": "0.1.0",
181
+ "rightkit-sandbox": "0.1.0",
182
+ "rightkit-search": "0.1.0",
183
+ "rightkit-service": "0.1.0",
184
+ "rightkit-settings": "0.1.0",
185
+ "rightkit-shell": "0.1.0",
186
+ "rightkit-speech-client": "0.1.0",
187
+ "rightkit-sync": "0.1.0",
138
188
  "rightkit-license": "0.1.3",
139
189
  "rightkit-tauri": "0.1.1"
140
190
  },
@@ -143,5 +193,11 @@
143
193
  "url": "https://github.com/bogusyogi/rightkit-swift.git",
144
194
  "version": "0.1.1"
145
195
  }
196
+ },
197
+ "stagedSwift": {
198
+ "rightkit-swift": {
199
+ "url": "https://github.com/bogusyogi/rightkit-swift.git",
200
+ "version": "0.3.1"
201
+ }
146
202
  }
147
203
  }