@rightkit/release 0.6.8 → 0.6.11
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 +30 -12
- package/admission-files.mjs +4 -1
- package/admission.mjs +2 -2
- package/advisory-exceptions.mjs +41 -5
- package/android-play-upload.mjs +16 -15
- package/app-check.mjs +105 -27
- package/assemble-app-command.mjs +151 -0
- package/assembled-app.mjs +445 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +6 -0
- package/bin/rightkit-gate.mjs +1608 -345
- package/broker-pins.mjs +152 -0
- package/build-launch.mjs +88 -0
- package/build-ownership.mjs +1231 -0
- package/build-release.mjs +82 -28
- package/cargo-contract.mjs +3 -1
- package/cargo-ledger.mjs +62 -0
- package/cargo-target-shim.mjs +238 -0
- package/class-a-launch-guard.mjs +126 -0
- package/cli/right-release.mjs +31 -5
- package/clippy-equivalence.mjs +202 -0
- package/clippy-route.mjs +543 -0
- package/codesign-shim.mjs +11 -1
- package/committed-source.mjs +39 -5
- package/crate-workspaces.mjs +87 -0
- package/create-mac-updater.mjs +14 -2
- package/deterministic-scans.mjs +997 -71
- package/development-signing.mjs +4 -1
- package/development.mjs +120 -21
- package/direct-receipt.mjs +135 -0
- package/door-policy.mjs +71 -6
- package/door.mjs +6 -6
- package/freshness-census.mjs +257 -0
- package/gate-executor.mjs +14 -2
- package/gate-stages.mjs +91 -15
- package/heavy-command.mjs +3 -2
- package/hook-retirement.mjs +65 -0
- package/init-command.mjs +1 -1
- package/install-app.mjs +11 -2
- package/install-assembled-app.mjs +423 -0
- package/inventory-check.mjs +11 -8
- package/ios-dev.mjs +70 -3
- package/ios-upload.mjs +20 -15
- package/linux-artifacts.mjs +93 -0
- package/local-publish.mjs +224 -53
- package/local-qualify.mjs +5 -3
- package/local-runtime.mjs +5 -4
- package/mac-package-recipe.mjs +4 -1
- package/managed-cargo.mjs +47 -5
- package/managed-root-guard.mjs +34 -0
- package/mirror-root-artifact.mjs +18 -5
- package/native-helpers.mjs +355 -0
- package/ownership-class-rules.mjs +1 -1
- package/ownership-evidence.mjs +37 -13
- package/ownership-hooks.mjs +20 -59
- package/ownership-lockfiles.mjs +14 -1
- package/ownership-policy-bundle.mjs +3 -2
- package/ownership-staged.mjs +2 -1
- package/ownership.mjs +24 -0
- package/package-recipe-targets.mjs +84 -10
- package/package.json +4 -3
- package/pipeline-admission.mjs +4 -4
- package/pipeline-receipts.mjs +302 -302
- package/preflight.mjs +27 -4
- package/project-type.mjs +2 -2
- package/provenance-verify.mjs +115 -0
- package/publish-cargo.mjs +12 -9
- package/qa-admission-policy.json +10 -1
- package/recipe-build.mjs +106 -14
- package/release-artifact-path.mjs +341 -0
- package/release-config.mjs +1 -1
- package/release-invocation.mjs +8 -1
- package/release-qa-command.mjs +25 -9
- package/release-source-brand.mjs +17 -0
- package/release-source-identity.mjs +171 -0
- package/release.mjs +59 -22
- package/rightkit-versions.json +73 -47
- package/runtime-budgets.mjs +6 -2
- package/scan-digest.mjs +71 -5
- package/sign-updater.mjs +6 -3
- package/snapshot-broker.mjs +172 -0
- package/snapshot-check.mjs +312 -0
- package/snapshot-git.mjs +166 -0
- package/snapshot-request.mjs +363 -0
- package/snapshot-root-config.mjs +76 -0
- package/snapshot.mjs +969 -0
- package/source-split.mjs +73 -0
- package/stage-commands.mjs +60 -0
- package/stage-ledger.mjs +204 -0
- package/stage-sidecar-command.mjs +5 -1
- package/stage-sidecar.mjs +157 -27
- package/swap-helper-bundle.mjs +5 -2
- package/test-command.mjs +17 -4
- package/unit-gate.mjs +301 -0
- package/web-build.mjs +40 -6
- package/win-package-recipe.mjs +20 -7
package/ios-upload.mjs
CHANGED
|
@@ -33,8 +33,8 @@ import { IOS_SIGNING_CONTRACT } from "./ios-package-recipe.mjs";
|
|
|
33
33
|
import { ASC_API, createAscClient, createTokenProvider, distributeToTestFlight, normalizeGroupSelection, TestFlightError, testflightPlan } from "./ios-testflight.mjs";
|
|
34
34
|
import { iosOwnershipScope } from "./ownership-scope.mjs";
|
|
35
35
|
import { assertArtifactUnchanged, digestBytes, verifyPublicationRelease } from "./publication-artifacts.mjs";
|
|
36
|
-
import {
|
|
37
|
-
import {
|
|
36
|
+
import { assertBindsSource, assertInsideSource, assertReleaseSource, gitRootOf, isSnapshotSource, resolveReleaseSource } from "./release-source-identity.mjs";
|
|
37
|
+
import { verifyOwnershipEvidence } from "./ownership-evidence.mjs";
|
|
38
38
|
import { targetDistribution } from "./target-distribution.mjs";
|
|
39
39
|
|
|
40
40
|
export const IOS_PROVIDER = "app-store-connect";
|
|
@@ -129,28 +129,33 @@ export function iosUploadPlan({ distribution, ipa, receipt }) {
|
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
// ---- authorisation -----------------------------------------------------------------------------------------------
|
|
132
|
-
const git = (cwd, args) => {
|
|
133
|
-
const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true });
|
|
134
|
-
if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`);
|
|
135
|
-
return result.stdout.trim();
|
|
136
|
-
};
|
|
137
132
|
|
|
138
133
|
/** Local checks only; no network, no secret. The sealed release is the only input: build-free by construction. */
|
|
139
|
-
export async function authorizeIosUpload({ releaseId, cwd = process.cwd(), configName = defaultReleaseConfigFile(cwd) }) {
|
|
134
|
+
export async function authorizeIosUpload({ releaseId, cwd = process.cwd(), configName = defaultReleaseConfigFile(cwd), env = process.env }) {
|
|
140
135
|
if (!RELEASE_ID.test(releaseId ?? "")) refuse("IOS_UPLOAD_RELEASE_REQUIRED", "--release <sealed-id> is required");
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
136
|
+
// Source identity (release-source-identity.mjs, item 7 row 7.04). Outside a door run: the checkout's HEAD, primary worktree and a clean
|
|
137
|
+
// status, as always. Inside one: the selected commit and its verified snapshot, with HEAD, status and branch of the checkout unread.
|
|
138
|
+
const release = resolveReleaseSource({ cwd, env });
|
|
139
|
+
assertReleaseSource(release, { commandId: "right-release upload", env });
|
|
140
|
+
const split = isSnapshotSource(release);
|
|
141
|
+
const repoRoot = gitRootOf(release);
|
|
142
|
+
const commit = release.commit;
|
|
145
143
|
const configPath = path.resolve(cwd, configName);
|
|
144
|
+
assertInsideSource(release, configPath, "the release config");
|
|
146
145
|
if (!existsSync(configPath)) refuse("IOS_UPLOAD_CONFIG_INVALID", `release config is required: ${configName}`);
|
|
147
146
|
// A legacy config is a module that runs on import (a JSON config is data): loaded only after the tree is proven clean.
|
|
148
147
|
const config = await importReleaseConfig(configPath, { query: `upload=${Date.now()}` });
|
|
149
148
|
const distribution = validateIosDistribution(targetDistribution(config, "ios"));
|
|
150
149
|
const sealedDir = path.join(repoRoot, ".right-release", "sealed", releaseId, "ios");
|
|
151
|
-
|
|
150
|
+
// The sealed release, its signing receipts and the state live where the build wrote them (the checkout's `.right-release`); the scope of
|
|
151
|
+
// the ownership scan is read from the snapshot's own config and project, which are the commit's.
|
|
152
|
+
const appRoot = split ? release.checkoutAppRoot : path.dirname(configPath);
|
|
153
|
+
const scopeRepoRoot = split ? release.snapshot : repoRoot;
|
|
154
|
+
const scopeAppRoot = path.dirname(configPath);
|
|
152
155
|
// The IPA is verified by a streamed hash and never held in memory (streamRoles); the parts are read as byte ranges at upload time.
|
|
153
|
-
const sealed = verifyPublicationRelease({ sealedDir, repoRoot, appRoot, commit, app: config.app, version: config.version, platform: "ios", configPath, releaseId, streamRoles: ["ipa"], ownershipScope: iosOwnershipScope({ repoRoot, appRoot, target: config.targets?.ios, nativeAssembly: config.nativeAssembly }) }
|
|
156
|
+
const sealed = verifyPublicationRelease({ sealedDir, repoRoot, appRoot, commit, app: config.app, version: config.version, platform: "ios", configPath, releaseId, streamRoles: ["ipa"], ownershipScope: iosOwnershipScope({ repoRoot: scopeRepoRoot, appRoot: scopeAppRoot, target: config.targets?.ios, nativeAssembly: config.nativeAssembly }) },
|
|
157
|
+
split ? { verifyOwnership: (expected, root, scanRoot, scope) => verifyOwnershipEvidence(expected, root, scanRoot, scope, release) } : {});
|
|
158
|
+
if (split) assertBindsSource(release, sealed.manifest, "the sealed release");
|
|
154
159
|
const entries = sealed.manifest.files.filter((entry) => entry.role === "ipa");
|
|
155
160
|
if (entries.length !== 1) refuse("IOS_IPA_MISSING", "the sealed release must hold exactly one ipa entry");
|
|
156
161
|
const ipa = sealed.artifacts.get(entries[0].name);
|
|
@@ -221,7 +226,7 @@ export async function runIosUpload({
|
|
|
221
226
|
acquireLock = acquireFileLock, journalDir, sleep, monotonic, now = () => new Date(),
|
|
222
227
|
}) {
|
|
223
228
|
refuseIosAscEnv(env);
|
|
224
|
-
const auth = await authorize({ releaseId, configName, cwd });
|
|
229
|
+
const auth = await authorize({ releaseId, configName, cwd, env });
|
|
225
230
|
const ipaEvidence = { name: auth.entry.name, sha256: auth.entry.sha256, sizeBytes: auth.entry.sizeBytes };
|
|
226
231
|
const plan = iosUploadPlan({ distribution: auth.distribution, ipa: ipaEvidence, receipt: auth.receipt });
|
|
227
232
|
const stateRoot = path.join(auth.repoRoot, ".right-release", "state", releaseId, "ios");
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// Linux build artifacts and their digest manifest (rightkit#1230): a native Linux dev/build run packs the declared files into one
|
|
2
|
+
// tarball per architecture and records it in linux-artifacts.json beside it. A Pulse guest link carries the sha256 and size from
|
|
3
|
+
// this manifest and its bootstrap refuses any download that does not match (ADR-0004 I1: pinned digest, no signature scheme).
|
|
4
|
+
// Nothing here uploads: publication is a separate, explicitly approved step.
|
|
5
|
+
import { createHash } from "node:crypto";
|
|
6
|
+
import nodeFs from "node:fs";
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { spawnSync } from "./background-process.mjs";
|
|
9
|
+
|
|
10
|
+
export const LINUX_ARTIFACTS_SCHEMA = "rightkit.linux-artifacts/1";
|
|
11
|
+
export const LINUX_ARCHES = Object.freeze(["x86_64", "aarch64"]);
|
|
12
|
+
const NAME = /^[a-z0-9][a-z0-9._-]{0,60}$/;
|
|
13
|
+
|
|
14
|
+
/** `development.targets.linux.archive`: `{ name, include: [relative paths] }`. */
|
|
15
|
+
export function validateLinuxArchive(archive, label = "development.targets.linux.archive") {
|
|
16
|
+
if (archive === undefined) return null;
|
|
17
|
+
if (!archive || typeof archive !== "object" || Array.isArray(archive)) throw new Error(`${label} must be an object { name, include }`);
|
|
18
|
+
const unknown = Object.keys(archive).filter((key) => !["name", "include"].includes(key));
|
|
19
|
+
if (unknown.length) throw new Error(`${label} has unknown key(s): ${unknown.join(", ")} (allowed: name, include)`);
|
|
20
|
+
if (typeof archive.name !== "string" || !NAME.test(archive.name)) throw new Error(`${label}.name must be a lower-case file name stem`);
|
|
21
|
+
if (!Array.isArray(archive.include) || !archive.include.length) throw new Error(`${label}.include must list the files or folders to pack`);
|
|
22
|
+
for (const item of archive.include) {
|
|
23
|
+
if (typeof item !== "string" || !item || path.isAbsolute(item) || item.split(/[\\/]/).includes("..")) throw new Error(`${label}.include entries must be relative paths inside the app root: ${JSON.stringify(item)}`);
|
|
24
|
+
}
|
|
25
|
+
return { name: archive.name, include: [...archive.include] };
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** The host architecture as `uname -m` names it; only the two native targets are accepted. */
|
|
29
|
+
export function linuxArch(run = spawnSync) {
|
|
30
|
+
const r = run("uname", ["-m"], { encoding: "utf8" });
|
|
31
|
+
const arch = String(r.stdout ?? "").trim();
|
|
32
|
+
if (r.status !== 0 || !LINUX_ARCHES.includes(arch)) throw new Error(`linux artifacts: unsupported or unknown architecture ${JSON.stringify(arch)} (supported: ${LINUX_ARCHES.join(", ")})`);
|
|
33
|
+
return arch;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const versionKey = (v) => v.split(".").map((n) => Number(n));
|
|
37
|
+
const newer = (a, b) => { const x = versionKey(a), y = versionKey(b); for (let i = 0; i < Math.max(x.length, y.length); i++) { if ((x[i] ?? 0) !== (y[i] ?? 0)) return (x[i] ?? 0) > (y[i] ?? 0); } return false; };
|
|
38
|
+
|
|
39
|
+
/** The highest GLIBC_x.y symbol version any listed ELF file needs; fails closed when a file cannot be read by objdump. */
|
|
40
|
+
export function glibcFloor(files, { run = spawnSync, fs = nodeFs } = {}) {
|
|
41
|
+
let floor = null;
|
|
42
|
+
for (const file of files) {
|
|
43
|
+
const head = Buffer.alloc(4);
|
|
44
|
+
const fd = fs.openSync(file, "r"); try { fs.readSync(fd, head, 0, 4, 0); } finally { fs.closeSync(fd); }
|
|
45
|
+
if (!head.equals(Buffer.from([0x7f, 0x45, 0x4c, 0x46]))) continue; // not ELF
|
|
46
|
+
const r = run("objdump", ["-T", file], { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 });
|
|
47
|
+
if (r.status !== 0) throw new Error(`linux artifacts: objdump -T ${file} failed (${r.status}); the glibc floor cannot be stated`);
|
|
48
|
+
for (const m of String(r.stdout).matchAll(/GLIBC_(\d+(?:\.\d+)+)/g)) if (!floor || newer(m[1], floor)) floor = m[1];
|
|
49
|
+
}
|
|
50
|
+
return floor;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function listFiles(fs, root, rel) {
|
|
54
|
+
const abs = path.join(root, rel);
|
|
55
|
+
const st = fs.statSync(abs);
|
|
56
|
+
if (!st.isDirectory()) return [abs];
|
|
57
|
+
return fs.readdirSync(abs).flatMap((entry) => listFiles(fs, root, path.join(rel, entry)));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Pack `archive.include` (relative to `root`) into `<outDir>/<name>-<version>-linux-<arch>.tar.gz`, then write or merge
|
|
62
|
+
* `<outDir>/linux-artifacts.json`. A manifest of another version or commit is replaced, never mixed.
|
|
63
|
+
*/
|
|
64
|
+
export function packLinuxArtifacts({ root, archive, app, version, commit, outDir = path.join(root, ".right-release", "linux"), run = spawnSync, fs = nodeFs, log = console.log }) {
|
|
65
|
+
if (!/^\d+\.\d+\.\d+([-+][0-9A-Za-z.-]+)?$/.test(String(version ?? ""))) throw new Error(`linux artifacts: version ${JSON.stringify(version)} is not semver`);
|
|
66
|
+
if (!/^[0-9a-f]{40}$/.test(String(commit ?? ""))) throw new Error("linux artifacts: commit must be the 40-hex source commit");
|
|
67
|
+
const arch = linuxArch(run);
|
|
68
|
+
for (const item of archive.include) if (!fs.existsSync(path.join(root, item))) throw new Error(`linux artifacts: ${item} does not exist; build it before packing`);
|
|
69
|
+
const glibc = glibcFloor(archive.include.flatMap((item) => listFiles(fs, root, item)), { run, fs });
|
|
70
|
+
fs.mkdirSync(outDir, { recursive: true });
|
|
71
|
+
const file = `${archive.name}-${version}-linux-${arch}.tar.gz`;
|
|
72
|
+
const target = path.join(outDir, file);
|
|
73
|
+
fs.rmSync(target, { force: true });
|
|
74
|
+
// Flat archive: each listed path is stored under its base name (dist/stage/pulse -> pulse), which is what a guest bootstrap
|
|
75
|
+
// extracts and runs; two entries with the same base name are refused rather than one silently overwriting the other.
|
|
76
|
+
const names = archive.include.map((item) => path.basename(item));
|
|
77
|
+
const dup = names.find((n, i) => names.indexOf(n) !== i);
|
|
78
|
+
if (dup) throw new Error(`linux artifacts: two include entries share the base name ${dup}; the archive is flat`);
|
|
79
|
+
const tar = run("tar", ["-czf", target, ...archive.include.flatMap((item) => ["-C", path.join(root, path.dirname(item)), path.basename(item)])], { stdio: "inherit" });
|
|
80
|
+
if (tar.status !== 0) throw new Error(`linux artifacts: tar failed (${tar.status})`);
|
|
81
|
+
const bytes = fs.readFileSync(target);
|
|
82
|
+
const entry = { file, sha256: createHash("sha256").update(bytes).digest("hex"), size: bytes.length, glibc };
|
|
83
|
+
const manifestPath = path.join(outDir, "linux-artifacts.json");
|
|
84
|
+
let manifest = null;
|
|
85
|
+
try { manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); } catch { manifest = null; }
|
|
86
|
+
if (!manifest || manifest.schema !== LINUX_ARTIFACTS_SCHEMA || manifest.app !== app || manifest.version !== version || manifest.commit !== commit) {
|
|
87
|
+
manifest = { schema: LINUX_ARTIFACTS_SCHEMA, app, version, commit, artifacts: {} };
|
|
88
|
+
}
|
|
89
|
+
manifest.artifacts[arch] = entry;
|
|
90
|
+
fs.writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
|
|
91
|
+
log(`right-release: linux artifact ${file} sha256 ${entry.sha256} size ${entry.size} glibc ${glibc ?? "none"}; manifest ${manifestPath}`);
|
|
92
|
+
return { manifestPath, arch, ...entry };
|
|
93
|
+
}
|