@rightkit/release 0.6.13 → 0.6.14
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 +49 -1
- package/adapter-env.mjs +165 -0
- package/android-ndk.mjs +5 -1
- package/android-release.mjs +73 -8
- package/app-check.mjs +22 -8
- package/app-closure.mjs +214 -0
- package/assemble-app-command.mjs +24 -7
- package/atomic-write.mjs +57 -0
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +1190 -337
- package/broker-client.mjs +106 -0
- package/broker-exception.mjs +211 -0
- package/broker-handshake.mjs +160 -0
- package/broker-pins.mjs +27 -8
- package/broker-wire.mjs +262 -0
- package/build-launch.mjs +364 -12
- package/build-ownership.mjs +117 -49
- package/build-release.mjs +3 -2
- package/cache-command.mjs +7 -2
- package/candidate-build-receipt.mjs +96 -0
- package/candidate-receipt-command.mjs +29 -0
- package/cargo-contract.mjs +4 -3
- package/cargo-ledger.mjs +1 -1
- package/cargo-license-notices.mjs +2 -2
- package/cargo-shapes.mjs +332 -0
- package/cargo-target-shim.mjs +44 -5
- package/cargo-target.mjs +5 -0
- package/carried-adapter-run.mjs +31 -0
- package/carried-cargo-run.mjs +9 -0
- package/carried-cargo.mjs +237 -0
- package/carrier-census.mjs +124 -0
- package/class-a-launch-guard.mjs +266 -33
- package/cli/right-release.mjs +2 -0
- package/clippy-route.mjs +23 -11
- package/compile-inputs.mjs +9 -12
- package/control-plane.mjs +18 -0
- package/deps-cargo-lock.mjs +208 -0
- package/deps.mjs +5 -0
- package/deterministic-scans.mjs +273 -44
- package/development-signing.mjs +14 -1
- package/development.mjs +109 -24
- package/direct-origin.mjs +107 -0
- package/disk-command.mjs +1 -0
- package/disk-policy.mjs +18 -3
- package/freshness-census.mjs +206 -58
- package/gate-executor.mjs +30 -12
- package/gate-stages.mjs +93 -16
- package/github-release.mjs +2 -1
- package/github-upload-gate.mjs +2 -1
- package/install-tauri-authenticode.mjs +346 -0
- package/ios-dev.mjs +77 -23
- package/ios-package-recipe.mjs +100 -41
- package/linux-artifacts.mjs +17 -2
- package/linux-qualifier.mjs +83 -0
- package/local-publish.mjs +36 -14
- package/local-runtime.mjs +2 -2
- package/lock-identity.mjs +138 -0
- package/lsclean.sh +0 -0
- package/managed-cargo.mjs +151 -44
- package/managed-roots.mjs +130 -12
- package/media-sidecar-catalog.mjs +1 -1
- package/native-adapters.mjs +411 -0
- package/native-helpers.mjs +26 -18
- package/native-tool-planning.mjs +94 -0
- package/ownership-mark.mjs +182 -0
- package/pack-digest.mjs +122 -0
- package/package.json +12 -12
- package/platform-dir.mjs +14 -0
- package/pnpm-adapter-grammar.mjs +226 -0
- package/progress-control.mjs +7 -2
- package/provenance-real-receipt.pins.json +7 -0
- package/provenance-verify.mjs +262 -1
- package/publication-outcomes.mjs +21 -0
- package/publish-update.mjs +2 -1
- package/qa-journeys-prebuilt-run.mjs +18 -0
- package/qa-journeys-prebuilt.mjs +148 -0
- package/qa-snapshot-run.mjs +53 -0
- package/qa-snapshot.mjs +164 -0
- package/qualification-verify.mjs +144 -0
- package/qualify-reuse.mjs +38 -0
- package/recipe-build.mjs +16 -3
- package/release-qa-audit.mjs +2 -2
- package/release-qa-command.mjs +2 -1
- package/release-qa-receipt.mjs +3 -2
- package/release-state.mjs +3 -2
- package/release.mjs +14 -3
- package/rightkit-drift.mjs +27 -5
- package/rightkit-versions.json +14 -10
- package/rustright-names.mjs +32 -0
- package/scan-tool-enrolment.mjs +167 -0
- package/sign-windows.mjs +212 -112
- package/signing-files.mjs +35 -0
- package/signing-identity.mjs +7 -0
- package/snapshot-broker.mjs +528 -57
- package/snapshot-check.mjs +95 -11
- package/snapshot-metadata.mjs +123 -0
- package/snapshot-request.mjs +389 -96
- package/stage-binary.mjs +22 -4
- package/stage-commands.mjs +56 -13
- package/stage-sidecar.mjs +93 -21
- package/suite-signing-policy.mjs +657 -0
- package/suite-signing.mjs +223 -0
- package/swift-scratch.mjs +25 -0
- package/tauri-locked.mjs +25 -0
- package/test-command.mjs +164 -29
- package/unit-gate.mjs +201 -31
- package/upload-large.mjs +0 -0
- package/web-build.mjs +2 -2
- package/win-package-recipe.mjs +26 -2
package/ios-package-recipe.mjs
CHANGED
|
@@ -13,14 +13,24 @@
|
|
|
13
13
|
//
|
|
14
14
|
// Upload is a separate command (ios-upload.mjs): `build` never uploads, `upload` never builds. Nothing here reads an App Store
|
|
15
15
|
// Connect credential: archive and export sign through the Xcode account of the GUI session. Fails closed: every problem
|
|
16
|
-
// throws IosPackageError, and once an
|
|
17
|
-
// seal cannot pick up a half-verified artifact. Every external effect is injectable through `deps`.
|
|
16
|
+
// throws IosPackageError, and once an IPA exists a failure deletes it (and any stale receipt) so a later
|
|
17
|
+
// seal cannot pick up a half-verified artifact (the broker-owned archive and export directory are the broker's to keep and reap). Every external effect is injectable through `deps`.
|
|
18
|
+
//
|
|
19
|
+
// RA-09 / R3-M02: every compiler-capable phase (xcodegen generate, xcodebuild archive, xcodebuild -exportArchive and any prepare step that names a native build
|
|
20
|
+
// tool) is a typed native adapter job of the production runner (`deps.native`, build-launch.mjs createNativeRunner: the carrier or the resolved executor's client),
|
|
21
|
+
// never a spawn of this recipe's own. The broker owns the output root, so the call names neither -derivedDataPath, -archivePath, -exportPath nor -resultBundlePath:
|
|
22
|
+
// the archive and the export directory are READ BACK from the verified receipt's managed output roles (`outputs.byRole.archive` / `.export`), and the export must read
|
|
23
|
+
// the very archive that was verified. The ExportOptions.plist is a regular XML file inside the step directory (the snapshot), not under the receipt directory. Signing stays
|
|
24
|
+
// the provisioned GUI session's (assertGuiSigningSession) with automatic signing and -allowProvisioningUpdates; no alternate signer exists. A missing runner, an unadvertised
|
|
25
|
+
// capability or an argv the broker's grammar refuses is refused BEFORE the first tool is launched. The non-compiling queries (`xcodebuild -version`, `xcodegen --version`,
|
|
26
|
+
// plutil, codesign, security, ditto, git) are this recipe's own direct steps through `deps.run`.
|
|
18
27
|
import { createHash } from "node:crypto";
|
|
19
28
|
import * as nodeFs from "node:fs";
|
|
20
29
|
import os from "node:os";
|
|
21
30
|
import path from "node:path";
|
|
22
31
|
import { spawnSync } from "./background-process.mjs";
|
|
23
|
-
import { assertGuiSigningSession, IosHostError, outputTail
|
|
32
|
+
import { assertGuiSigningSession, IosHostError, outputTail } from "./ios-host.mjs";
|
|
33
|
+
import { nativeToolOf } from "./native-adapters.mjs";
|
|
24
34
|
|
|
25
35
|
export const IOS_SIGNING_CONTRACT = "ios-xcodebuild-archive-automatic-v1";
|
|
26
36
|
export const IOS_RECEIPT_NAME = "ios-archive.json";
|
|
@@ -30,7 +40,8 @@ export const IOS_DEFAULT_TIMEOUT_MS = 60 * 60 * 1000;
|
|
|
30
40
|
/**
|
|
31
41
|
* Codes: IOS_PACKAGE_CONFIG_INVALID, IOS_PACKAGE_HOST_UNSUPPORTED, IOS_PACKAGE_TOOL_MISSING, IOS_PACKAGE_GENERATE_FAILED,
|
|
32
42
|
* IOS_PACKAGE_PREPARE_FAILED, IOS_PACKAGE_ARCHIVE_FAILED, IOS_PACKAGE_SOURCE_CHANGED, IOS_PACKAGE_VERIFY_FAILED,
|
|
33
|
-
* IOS_PACKAGE_EXPORT_FAILED, IOS_PACKAGE_BUILD_NUMBER, IOS_PACKAGE_RECEIPT_FAILED
|
|
43
|
+
* IOS_PACKAGE_EXPORT_FAILED, IOS_PACKAGE_BUILD_NUMBER, IOS_PACKAGE_RECEIPT_FAILED, IOS_PACKAGE_NATIVE_REQUIRED (no production native runner was supplied),
|
|
44
|
+
* IOS_PACKAGE_NATIVE_REFUSED (the door or the broker's grammar refused a native call before any launch; the message names the capability).
|
|
34
45
|
*/
|
|
35
46
|
export class IosPackageError extends Error {
|
|
36
47
|
constructor(code, message, options) { super(message, options); this.name = "IosPackageError"; this.code = code; }
|
|
@@ -39,6 +50,8 @@ const invalid = (message) => new IosPackageError("IOS_PACKAGE_CONFIG_INVALID", `
|
|
|
39
50
|
const isPlainObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
40
51
|
|
|
41
52
|
// ---- config ------------------------------------------------------------------------------------------------------
|
|
53
|
+
/** The ExportOptions.plist the export reads: a regular XML file inside the step directory (the snapshot), relative to the app root. It is never under the receipt directory. */
|
|
54
|
+
export const IOS_EXPORT_OPTIONS_RELATIVE = ".right-release/ios-export-options.plist";
|
|
42
55
|
const IOS_KEYS = ["project", "projectSpec", "scheme", "configuration", "teamId", "bundleId", "exportMethod", "signing", "buildNumber", "extraBuildSettings", "packageResolved", "trustedSwiftMacros", "trustedSwiftPlugins", "prepare", "cleanup", "checks", "archive", "ipa", "timeoutMs"];
|
|
43
56
|
const CLEANUP_KEYS = ["derivedData"];
|
|
44
57
|
const CHECK_KEYS = ["versionIdentity", "forbidStrings", "plist"];
|
|
@@ -138,7 +151,9 @@ export function validateIosPackageConfig(spec) {
|
|
|
138
151
|
const bad = Object.keys(spec.cleanup).filter((key) => !CLEANUP_KEYS.includes(key));
|
|
139
152
|
if (bad.length) throw invalid(`cleanup has unknown key(s): ${bad.join(", ")} (allowed: ${CLEANUP_KEYS.join(", ")})`);
|
|
140
153
|
if (spec.cleanup.derivedData !== undefined && typeof spec.cleanup.derivedData !== "boolean") throw invalid("cleanup.derivedData must be a boolean");
|
|
141
|
-
|
|
154
|
+
// The broker owns DerivedData and its lifetime (the xcodebuild adapter's managed output root): a recipe cannot delete it, and silently ignoring the request would be a lie.
|
|
155
|
+
if (spec.cleanup.derivedData === true) throw invalid("cleanup.derivedData cannot be honoured: the broker owns the DerivedData root and its lifetime (RA-09); remove it");
|
|
156
|
+
cleanup.derivedData = false;
|
|
142
157
|
}
|
|
143
158
|
|
|
144
159
|
const checks = { versionIdentity: true, forbidStrings: [], plist: {} };
|
|
@@ -183,6 +198,7 @@ export function validateIosPackageConfig(spec) {
|
|
|
183
198
|
prepare: spec.prepare === undefined ? [] : commandList(spec.prepare, "prepare"),
|
|
184
199
|
cleanup,
|
|
185
200
|
checks,
|
|
201
|
+
// Kept for config compatibility only: the broker owns where the archive is written (a caller names no -archivePath), so this path is validated but never used to build or read.
|
|
186
202
|
archive: relativeInside(spec.archive, "archive", /\.xcarchive$/i),
|
|
187
203
|
ipa: relativeInside(spec.ipa, "ipa", /\.ipa$/i),
|
|
188
204
|
timeoutMs,
|
|
@@ -343,16 +359,17 @@ export function installSwiftTrustSource({ trust, root, fs = nodeFs }) {
|
|
|
343
359
|
assertSwiftTrustUnchanged({ trust, root, fs });
|
|
344
360
|
}
|
|
345
361
|
|
|
346
|
-
|
|
362
|
+
/**
|
|
363
|
+
* The archive call as the broker's xcodebuild adapter admits it: no -archivePath, -derivedDataPath or -resultBundlePath (the broker owns the output root and names the archive by
|
|
364
|
+
* its managed `archive` role in the signed receipt). Signing is automatic through the provisioned session.
|
|
365
|
+
*/
|
|
366
|
+
export function archiveArgv(spec, { root, buildNumber, trusted }) {
|
|
347
367
|
const trustFlags = swiftTrustFlags(spec, trusted);
|
|
348
368
|
return [
|
|
349
369
|
"-project", path.resolve(root, spec.project),
|
|
350
370
|
"-scheme", spec.scheme,
|
|
351
371
|
"-configuration", spec.configuration,
|
|
352
372
|
"-destination", "generic/platform=iOS",
|
|
353
|
-
"-archivePath", path.resolve(root, spec.archive),
|
|
354
|
-
"-derivedDataPath", storage.derivedData,
|
|
355
|
-
...(storage.resultBundle ? ["-resultBundlePath", storage.resultBundle] : []),
|
|
356
373
|
`DEVELOPMENT_TEAM=${spec.teamId}`,
|
|
357
374
|
`CURRENT_PROJECT_VERSION=${buildNumber}`,
|
|
358
375
|
...Object.entries(spec.extraBuildSettings).map(([name, value]) => `${name}=${value}`),
|
|
@@ -362,11 +379,9 @@ export function archiveArgv(spec, { root, buildNumber, storage, trusted }) {
|
|
|
362
379
|
];
|
|
363
380
|
}
|
|
364
381
|
|
|
365
|
-
/**
|
|
366
|
-
export
|
|
367
|
-
|
|
368
|
-
export function exportArgv(spec, { root, optionsFile }) {
|
|
369
|
-
return ["-exportArchive", "-archivePath", path.resolve(root, spec.archive), "-exportPath", exportDirectory(spec, root), "-exportOptionsPlist", optionsFile, "-allowProvisioningUpdates"];
|
|
382
|
+
/** The export call: `-exportArchive` stands alone with its options file; the broker reads the managed archive the archive call wrote and names the export directory (role `export`). */
|
|
383
|
+
export function exportArgv(spec, { optionsFile }) {
|
|
384
|
+
return ["-exportArchive", "-exportOptionsPlist", optionsFile, "-allowProvisioningUpdates"];
|
|
370
385
|
}
|
|
371
386
|
|
|
372
387
|
/** Dry-run display only: the flags the run would add once its own check passes. The real run never uses this. */
|
|
@@ -377,27 +392,26 @@ const plannedTrust = (spec) => {
|
|
|
377
392
|
return planned;
|
|
378
393
|
};
|
|
379
394
|
|
|
380
|
-
/** The ordered plan (pure; no secret is part of any argument). Used by --dry-run and by the run itself. */
|
|
381
|
-
export function iosPackagePlan(spec, { root, buildNumber = "<YYYYMMDDNN>",
|
|
382
|
-
const optionsFile = path.join(
|
|
395
|
+
/** The ordered plan (pure; no secret is part of any argument). Used by --dry-run and by the run itself. `native` names the broker adapter that runs the step. */
|
|
396
|
+
export function iosPackagePlan(spec, { root, buildNumber = "<YYYYMMDDNN>", receiptDir }) {
|
|
397
|
+
const optionsFile = path.join(root, IOS_EXPORT_OPTIONS_RELATIVE);
|
|
383
398
|
return {
|
|
384
399
|
steps: [
|
|
385
|
-
{ id: "preflight", description: "macOS host, GUI signing session, xcodebuild" + (spec.projectSpec ? ", xcodegen (required)" : "") },
|
|
386
|
-
...(spec.projectSpec ? [{ id: "generate", cmd: "xcodegen", args: xcodegenArgv(spec, root) }] : []),
|
|
387
|
-
...spec.prepare.map((step, index) => ({ id: "prepare", index, cmd: step.cmd, args: step.args, cwd: step.cwd ?? "." })),
|
|
400
|
+
{ id: "preflight", description: "macOS host, GUI signing session, xcodebuild" + (spec.projectSpec ? ", xcodegen (required)" : "") + "; every compiler-capable call is planned against the broker's adapter grammar before the first launch" },
|
|
401
|
+
...(spec.projectSpec ? [{ id: "generate", cmd: "xcodegen", args: xcodegenArgv(spec, root), native: "rightkit-xcodegen" }] : []),
|
|
402
|
+
...spec.prepare.map((step, index) => ({ id: "prepare", index, cmd: step.cmd, args: step.args, cwd: step.cwd ?? ".", ...(nativeToolOf(step.cmd) ? { native: true } : {}) })),
|
|
388
403
|
...(spec.trustedSwiftMacros.length || spec.trustedSwiftPlugins.length
|
|
389
404
|
? [{ id: "trust-check", description: `Package.resolved (${spec.packageResolved ?? defaultPackageResolvedPath(spec.project)}) tracked and equal to HEAD; ${[...spec.trustedSwiftMacros, ...spec.trustedSwiftPlugins].map((entry) => entry.package).join(", ")} pinned to the reviewed revisions; the skip flags below are added only after this passes` }]
|
|
390
405
|
: []),
|
|
391
|
-
{ id: "archive", cmd: "xcodebuild", args: archiveArgv(spec, { root, buildNumber,
|
|
406
|
+
{ id: "archive", cmd: "xcodebuild", args: archiveArgv(spec, { root, buildNumber, trusted: plannedTrust(spec) }), native: "rightkit-xcodebuild", outputs: "managed role archive (read back from the signed receipt)" },
|
|
392
407
|
{ id: "verify-archive", description: "version identity across app, appex and Watch plists; bundle id; build number; QA scan; forbidden strings" },
|
|
393
|
-
{ id: "export", cmd: "xcodebuild", args: exportArgv(spec, {
|
|
408
|
+
{ id: "export", cmd: "xcodebuild", args: exportArgv(spec, { optionsFile }), optionsFile, native: "rightkit-xcodebuild", outputs: "managed roles archive (must equal the verified one) and export (read back from the signed receipt)" },
|
|
394
409
|
{ id: "verify-ipa", description: "extracted IPA: version identity, entitlements, embedded.mobileprovision, code signature" },
|
|
395
410
|
{ id: "receipt", file: path.join(receiptDir, IOS_RECEIPT_NAME) },
|
|
396
411
|
],
|
|
397
412
|
teamId: spec.teamId,
|
|
398
413
|
bundleId: spec.bundleId,
|
|
399
414
|
ipa: path.resolve(root, spec.ipa),
|
|
400
|
-
archive: path.resolve(root, spec.archive),
|
|
401
415
|
};
|
|
402
416
|
}
|
|
403
417
|
|
|
@@ -499,24 +513,23 @@ export function scanForbiddenStrings(dir, strings, fs = nodeFs) {
|
|
|
499
513
|
// ---- the recipe --------------------------------------------------------------------------------------------------
|
|
500
514
|
/**
|
|
501
515
|
* Runs the steps. `root` is the app root. `deps` (all optional, defaults are the real host):
|
|
502
|
-
*
|
|
516
|
+
* native REQUIRED: the production native runner ({ plan, run }, build-launch.mjs createNativeRunner). Every compiler-capable call goes through it; there is no spawn fallback
|
|
517
|
+
* run(cmd, args, options) spawnSync-shaped, for the NON-compiling direct steps only (xcodebuild -version, xcodegen --version, plutil, codesign, security, ditto, git)
|
|
503
518
|
* fs node:fs-shaped
|
|
504
519
|
* scanFiles(label, files, o) release.mjs's assertNoQaControlBinaries (QA rules plus the release-switch binary layer)
|
|
505
520
|
* detectSession / route GUI signing session seams (ios-host.mjs)
|
|
506
521
|
* gitHead() the source commit; it must not change during the build
|
|
507
|
-
* storage() DerivedData and result bundle (rustright storage xcode-args)
|
|
508
522
|
* scanForbidden(dir, strings) forbidden-string scan
|
|
509
523
|
* counterDir where the per-app build number counter lives (the vault state root under right-release build)
|
|
510
524
|
*/
|
|
511
525
|
export async function runIosPackageRecipe({
|
|
512
526
|
root, app = "app", spec, dryRun = false, env = process.env, platform = process.platform,
|
|
513
527
|
receiptDir = path.join(root, ".right-release", "receipts"), stateDir = path.join(root, ".right-release"), counterDir = stateDir,
|
|
514
|
-
run = spawnSync, fs = nodeFs, scanFiles, detectSession, route, gitHead,
|
|
528
|
+
native, run = spawnSync, fs = nodeFs, scanFiles, detectSession, route, gitHead, scanForbidden = scanForbiddenStrings,
|
|
515
529
|
now = () => new Date(), tmpRoot = os.tmpdir(), log = console.log,
|
|
516
530
|
}) {
|
|
517
531
|
if (!path.isAbsolute(root)) throw invalid("root must be an absolute path");
|
|
518
532
|
const ipa = path.resolve(root, spec.ipa);
|
|
519
|
-
const archive = path.resolve(root, spec.archive);
|
|
520
533
|
const receiptPath = path.join(receiptDir, IOS_RECEIPT_NAME);
|
|
521
534
|
|
|
522
535
|
if (dryRun) {
|
|
@@ -527,6 +540,9 @@ export async function runIosPackageRecipe({
|
|
|
527
540
|
}
|
|
528
541
|
if (platform !== "darwin") throw new IosPackageError("IOS_PACKAGE_HOST_UNSUPPORTED", `the iOS package recipe needs macOS with Xcode; this host is ${platform} (only config validation and --dry-run run elsewhere)`);
|
|
529
542
|
|
|
543
|
+
// ---- the production native runner: there is no spawn route for a compiler-capable phase ----
|
|
544
|
+
if (!native || typeof native.run !== "function" || typeof native.plan !== "function") throw new IosPackageError("IOS_PACKAGE_NATIVE_REQUIRED", "the iOS package recipe needs the production native runner (build-launch.mjs createNativeRunner): xcodegen and xcodebuild archive/export run only as typed broker adapter jobs, never as a spawn of this recipe's own");
|
|
545
|
+
|
|
530
546
|
// ---- preflight: before any effect ----
|
|
531
547
|
let session;
|
|
532
548
|
try { session = assertGuiSigningSession({ operation: "ios package", env, run, detect: detectSession, route }); }
|
|
@@ -541,18 +557,17 @@ export async function runIosPackageRecipe({
|
|
|
541
557
|
if (gen.error || gen.status !== 0) throw new IosPackageError("IOS_PACKAGE_TOOL_MISSING", `xcodegen is required because iosPackage.projectSpec names ${spec.projectSpec}; install it (brew install xcodegen). A committed xcodeproj is never used instead`);
|
|
542
558
|
}
|
|
543
559
|
if (!fs.existsSync(path.resolve(root, spec.projectSpec ?? spec.project))) throw new IosPackageError("IOS_PACKAGE_CONFIG_INVALID", `${spec.projectSpec ?? spec.project} does not exist`);
|
|
544
|
-
const resolvedStorage = (storage ?? (() => resolveXcodeStorage({ env, run, cwd: root, fallbackRoot: path.join(stateDir, "ios") })))();
|
|
545
560
|
const head = gitHead ?? (() => { const r = run("git", ["rev-parse", "HEAD"], { cwd: root, encoding: "utf8", windowsHide: true }); if (r.status !== 0) throw new IosPackageError("IOS_PACKAGE_SOURCE_CHANGED", "git rev-parse HEAD failed"); return String(r.stdout).trim(); });
|
|
546
561
|
const sourceCommit = head();
|
|
547
562
|
|
|
548
563
|
// ---- stale outputs go first, so an old IPA is never mistaken for this build's ----
|
|
549
|
-
const
|
|
550
|
-
const stale = [
|
|
564
|
+
const optionsFile = path.join(root, IOS_EXPORT_OPTIONS_RELATIVE);
|
|
565
|
+
const stale = [ipa, receiptPath, optionsFile];
|
|
551
566
|
for (const file of stale) fs.rmSync(file, { recursive: true, force: true });
|
|
552
567
|
fs.mkdirSync(receiptDir, { recursive: true });
|
|
553
568
|
fs.mkdirSync(path.dirname(ipa), { recursive: true });
|
|
554
569
|
|
|
555
|
-
const cleanupOutputs = () => { for (const file of [ipa,
|
|
570
|
+
const cleanupOutputs = () => { for (const file of [ipa, receiptPath]) { try { fs.rmSync(file, { recursive: true, force: true }); } catch { /* best effort */ } } };
|
|
556
571
|
const step = (id, code, cmd, args, options = {}) => {
|
|
557
572
|
log(`right-release: ios ${id}: ${cmd}`);
|
|
558
573
|
const { logFile, ...runOptions } = options;
|
|
@@ -561,6 +576,34 @@ export async function runIosPackageRecipe({
|
|
|
561
576
|
if (result.error || result.status !== 0) throw new IosPackageError(code, `${cmd} ${id} failed (${result.status ?? "unavailable"})${result.error ? `: ${result.error.message}` : ""}\n${outputTail(result)}`);
|
|
562
577
|
return result;
|
|
563
578
|
};
|
|
579
|
+
// A compiler-capable call: the production runner only. A refusal (no runner route, an unadvertised capability, an argv the broker's grammar refuses) happened BEFORE any launch and names the capability.
|
|
580
|
+
const refusedNative = (cmd, refusal) => new IosPackageError("IOS_PACKAGE_NATIVE_REFUSED", `${cmd} was refused before any launch (${refusal.code ?? "refused"}): ${refusal.message}`);
|
|
581
|
+
const planNative = (cmd, args, options = {}) => {
|
|
582
|
+
try { native.plan(cmd, args, { cwd: root, env, ...options }); } catch (error) {
|
|
583
|
+
if (error?.code === "RK_BUILD_LAUNCH_REFUSED") throw refusedNative(cmd, { code: error.reason, message: error.message });
|
|
584
|
+
throw error;
|
|
585
|
+
}
|
|
586
|
+
};
|
|
587
|
+
const nativeStep = (id, code, cmd, args, options = {}) => {
|
|
588
|
+
log(`right-release: ios ${id}: ${cmd} (broker adapter job)`);
|
|
589
|
+
const { logFile, cwd = root } = options;
|
|
590
|
+
const result = native.run(cmd, args, { cwd, env, timeout: spec.timeoutMs });
|
|
591
|
+
if (logFile) { try { fs.writeFileSync(logFile, `${outputTail(result, 4000)}\n`); } catch { /* diagnostics only */ } }
|
|
592
|
+
if (result.refusal) throw refusedNative(cmd, result.refusal);
|
|
593
|
+
if (!result.ok) throw new IosPackageError(code, `${cmd} ${id} failed (${result.status ?? "unavailable"})${result.error ? `: ${result.error.message}` : ""}\n${outputTail(result)}`);
|
|
594
|
+
return result;
|
|
595
|
+
};
|
|
596
|
+
// The managed location of a role, read back from the verified receipt's output mapping: never a path this recipe composed.
|
|
597
|
+
const located = (result, role, code) => {
|
|
598
|
+
const outputs = result.outputs;
|
|
599
|
+
const found = outputs?.ok === true ? outputs.byRole?.[role] : undefined;
|
|
600
|
+
if (typeof found !== "string" || !path.isAbsolute(found)) throw new IosPackageError(code, `the broker did not report the managed ${role} location of ${result.tool} (${outputs && outputs.ok === false ? outputs.problem : "no verified output directory"}); a broker-owned build's output is never guessed`);
|
|
601
|
+
return found;
|
|
602
|
+
};
|
|
603
|
+
const nativeEvidence = (result) => ({
|
|
604
|
+
tool: result.tool, adapter: result.adapter, capability: result.capability, carried: result.carried, relativeCwd: result.relativeCwd, requestId: result.requestId, outputDir: result.outputDir,
|
|
605
|
+
outputsSource: result.outputs?.source ?? null, argv: result.argv, outputs: result.outputs?.outputs ?? [],
|
|
606
|
+
});
|
|
564
607
|
const plist = (file) => {
|
|
565
608
|
const result = sh("plutil", ["-convert", "json", "-o", "-", "--", file], { timeout: 60_000 });
|
|
566
609
|
try { if (result.status === 0) return JSON.parse(String(result.stdout)); } catch { /* reported below */ }
|
|
@@ -582,23 +625,35 @@ export async function runIosPackageRecipe({
|
|
|
582
625
|
|
|
583
626
|
let tempDir;
|
|
584
627
|
try {
|
|
628
|
+
// ---- plan every compiler-capable call against the broker's grammar BEFORE the first launch: a missing capability or a refused argv stops here, with nothing sent ----
|
|
629
|
+
// The export reads an options file of the step directory, so it is written first; the build number is a placeholder here (the real one is drawn from the counter just before the archive).
|
|
630
|
+
const optionsText = exportOptionsPlist(spec);
|
|
631
|
+
fs.mkdirSync(path.dirname(optionsFile), { recursive: true });
|
|
632
|
+
fs.writeFileSync(optionsFile, optionsText);
|
|
633
|
+
const plannedBuild = `${now().toISOString().slice(0, 10).replaceAll("-", "")}01`;
|
|
634
|
+
if (spec.projectSpec) planNative("xcodegen", xcodegenArgv(spec, root));
|
|
635
|
+
planNative("xcodebuild", archiveArgv(spec, { root, buildNumber: plannedBuild, trusted: plannedTrust(spec) }));
|
|
636
|
+
planNative("xcodebuild", exportArgv(spec, { optionsFile }));
|
|
585
637
|
// ---- trust-check: reviewed Swift macro/plugin pins, before anything is generated or built ----
|
|
586
638
|
const trust = verifySwiftTrustSource({ spec, root, sh, fs });
|
|
587
639
|
// ---- generate ----
|
|
588
640
|
if (spec.projectSpec) {
|
|
589
|
-
|
|
641
|
+
nativeStep("generate", "IOS_PACKAGE_GENERATE_FAILED", "xcodegen", xcodegenArgv(spec, root));
|
|
590
642
|
if (!fs.existsSync(path.resolve(root, spec.project))) throw new IosPackageError("IOS_PACKAGE_GENERATE_FAILED", `xcodegen did not produce ${spec.project}`);
|
|
591
643
|
}
|
|
592
644
|
// ---- prepare ----
|
|
593
645
|
for (const [index, command] of spec.prepare.entries()) {
|
|
594
646
|
const cwd = path.resolve(root, command.cwd ?? ".");
|
|
595
|
-
|
|
647
|
+
// A prepare step that names a native build tool is a broker adapter job like any other; the app's own non-native commands stay direct steps.
|
|
648
|
+
if (nativeToolOf(command.cmd) !== null) nativeStep(`prepare ${index + 1}`, "IOS_PACKAGE_PREPARE_FAILED", command.cmd, command.args, { cwd });
|
|
649
|
+
else step(`prepare ${index + 1}`, "IOS_PACKAGE_PREPARE_FAILED", command.cmd, command.args, { cwd });
|
|
596
650
|
}
|
|
597
651
|
if (trust) installSwiftTrustSource({ trust, root, fs });
|
|
598
652
|
// ---- archive ----
|
|
599
653
|
const buildNumber = nextDateBuildNumber({ now: now(), stateFile: path.join(counterDir, `ios-build-number.${spec.bundleId}.json`), fs });
|
|
600
|
-
|
|
654
|
+
const archived = nativeStep("archive", "IOS_PACKAGE_ARCHIVE_FAILED", "xcodebuild", archiveArgv(spec, { root, buildNumber, trusted: trust?.evidence }), { logFile: path.join(receiptDir, "ios-archive.log") });
|
|
601
655
|
if (trust) assertSwiftTrustUnchanged({ trust, root, fs });
|
|
656
|
+
const archive = located(archived, "archive", "IOS_PACKAGE_ARCHIVE_FAILED");
|
|
602
657
|
const appsDir = path.join(archive, "Products", "Applications");
|
|
603
658
|
const apps = fs.existsSync(appsDir) ? fs.readdirSync(appsDir).filter((name) => name.endsWith(".app")) : [];
|
|
604
659
|
if (apps.length !== 1) throw new IosPackageError("IOS_PACKAGE_ARCHIVE_FAILED", `the archive must hold exactly one app under Products/Applications, found ${apps.length}`);
|
|
@@ -620,17 +675,20 @@ export async function runIosPackageRecipe({
|
|
|
620
675
|
if (head() !== sourceCommit) throw new IosPackageError("IOS_PACKAGE_SOURCE_CHANGED", "the repository HEAD changed during the archive; refusing to export a mixed-source build");
|
|
621
676
|
|
|
622
677
|
// ---- export ----
|
|
623
|
-
const optionsFile = path.join(receiptDir, "ios-export-options.plist");
|
|
624
|
-
const optionsText = exportOptionsPlist(spec);
|
|
625
|
-
fs.writeFileSync(optionsFile, optionsText);
|
|
626
678
|
// Taken just before the export: xcodebuild registers a build upload for this version and build during it (see ios-build-upload.mjs).
|
|
627
679
|
const exportStartedAt = now().toISOString();
|
|
628
|
-
|
|
680
|
+
const exported = nativeStep("export", "IOS_PACKAGE_EXPORT_FAILED", "xcodebuild", exportArgv(spec, { optionsFile }), { logFile: path.join(receiptDir, "ios-export.log") });
|
|
629
681
|
{
|
|
682
|
+
// The export must have read the very archive that was verified, and its directory is the broker's managed `export` role.
|
|
683
|
+
const exportedArchive = located(exported, "archive", "IOS_PACKAGE_EXPORT_FAILED");
|
|
684
|
+
if (path.normalize(exportedArchive) !== path.normalize(archive)) throw new IosPackageError("IOS_PACKAGE_EXPORT_FAILED", `the export read ${exportedArchive}, not the verified archive ${archive}`);
|
|
685
|
+
const exportDir = located(exported, "export", "IOS_PACKAGE_EXPORT_FAILED");
|
|
630
686
|
const produced = fs.existsSync(exportDir) ? fs.readdirSync(exportDir).filter((name) => name.endsWith(".ipa")) : [];
|
|
631
|
-
if (produced.length !== 1) throw new IosPackageError("IOS_PACKAGE_EXPORT_FAILED", `the export must produce exactly one .ipa in
|
|
687
|
+
if (produced.length !== 1) throw new IosPackageError("IOS_PACKAGE_EXPORT_FAILED", `the export must produce exactly one .ipa in the managed export directory, found ${produced.length}`);
|
|
688
|
+
const source = path.join(exportDir, produced[0]);
|
|
632
689
|
fs.mkdirSync(path.dirname(ipa), { recursive: true });
|
|
633
|
-
fs.
|
|
690
|
+
fs.copyFileSync(source, ipa);
|
|
691
|
+
if (sha256Bytes(fs.readFileSync(source)) !== sha256Bytes(fs.readFileSync(ipa))) throw new IosPackageError("IOS_PACKAGE_EXPORT_FAILED", "the IPA copied out of the managed export directory differs from the exported bytes");
|
|
634
692
|
}
|
|
635
693
|
if (!fs.statSync(ipa).size) throw new IosPackageError("IOS_PACKAGE_EXPORT_FAILED", "the exported IPA is empty");
|
|
636
694
|
|
|
@@ -690,6 +748,8 @@ export async function runIosPackageRecipe({
|
|
|
690
748
|
profile,
|
|
691
749
|
bundles: ipaInfos.map((bundle) => ({ role: bundle.role, name: bundle.label, bundleId: String(bundle.info.CFBundleIdentifier ?? "") })),
|
|
692
750
|
archiveVersion: identity,
|
|
751
|
+
// RA-09: the broker's output readback. The archive and the export directory are the managed roles of the signed receipts of the archive and export calls (never paths this recipe chose).
|
|
752
|
+
native: { archivePath: archive, archive: nativeEvidence(archived), export: nativeEvidence(exported) },
|
|
693
753
|
// Present only when the app trusts reviewed Swift macros/plugins: what was trusted and the exact pins file it was checked against.
|
|
694
754
|
...(trust ? { swiftTrust: { packageResolved: { path: trust.sourceRel, sha256: trust.sha256 }, macros: trust.evidence.macros, plugins: trust.evidence.plugins } } : {}),
|
|
695
755
|
};
|
|
@@ -701,7 +761,6 @@ export async function runIosPackageRecipe({
|
|
|
701
761
|
cleanupOutputs();
|
|
702
762
|
throw error instanceof IosPackageError ? error : new IosPackageError("IOS_PACKAGE_VERIFY_FAILED", `iOS package failed: ${error.message}`, { cause: error });
|
|
703
763
|
} finally {
|
|
704
|
-
try { fs.rmSync(exportDir, { recursive: true, force: true }); } catch { /* best effort */ }
|
|
705
764
|
if (tempDir) { try { fs.rmSync(tempDir, { recursive: true, force: true }); } catch { /* best effort */ } }
|
|
706
765
|
}
|
|
707
766
|
}
|
package/linux-artifacts.mjs
CHANGED
|
@@ -6,6 +6,7 @@ import { createHash } from "node:crypto";
|
|
|
6
6
|
import nodeFs from "node:fs";
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { spawnSync } from "./background-process.mjs";
|
|
9
|
+
import { readSourceSplit } from "./source-split.mjs";
|
|
9
10
|
|
|
10
11
|
export const LINUX_ARTIFACTS_SCHEMA = "rightkit.linux-artifacts/1";
|
|
11
12
|
export const LINUX_ARCHES = Object.freeze(["x86_64", "aarch64"]);
|
|
@@ -57,11 +58,25 @@ function listFiles(fs, root, rel) {
|
|
|
57
58
|
return fs.readdirSync(abs).flatMap((entry) => listFiles(fs, root, path.join(rel, entry)));
|
|
58
59
|
}
|
|
59
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Where the archive and its manifest go: `<app root>/.right-release/linux`. Inside the door's source split (docs/GATE-CONTRACT.md "Commit-only build") `root`
|
|
63
|
+
* is the SNAPSHOT of the commit, and `.right-release` is a reserved path segment of the stage-output ledger: a file there is never an admissible ledger
|
|
64
|
+
* entry, so the stage would end "result covers no known bytes" (exit 2) after a good build. Every output right-release owns stays with the CHECKOUT
|
|
65
|
+
* (source-split.mjs; build-release.mjs puts the vault, state and sealed there), so a split packs into the checkout's same app-relative
|
|
66
|
+
* `.right-release/linux`, which is also where an unsplit run writes it. The split is the live door run's (readSourceSplit: proven, or a refusal), never
|
|
67
|
+
* a path the caller names. `split` overrides the lookup (a caller that has already read it).
|
|
68
|
+
*/
|
|
69
|
+
export function linuxArtifactsOutDir({ root, split, env = process.env, run = spawnSync }) {
|
|
70
|
+
const found = split !== undefined ? split : readSourceSplit({ env, cwd: root, run });
|
|
71
|
+
return path.join(found ? found.checkoutAppRoot : root, ".right-release", "linux");
|
|
72
|
+
}
|
|
73
|
+
|
|
60
74
|
/**
|
|
61
75
|
* 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.
|
|
76
|
+
* `<outDir>/linux-artifacts.json`. A manifest of another version or commit is replaced, never mixed. `outDir` defaults to
|
|
77
|
+
* linuxArtifactsOutDir: under `root` itself, except inside a source split, where it is the checkout's (never the snapshot's).
|
|
63
78
|
*/
|
|
64
|
-
export function packLinuxArtifacts({ root, archive, app, version, commit, outDir =
|
|
79
|
+
export function packLinuxArtifacts({ root, archive, app, version, commit, split, env = process.env, outDir = linuxArtifactsOutDir({ root, split, env }), run = spawnSync, fs = nodeFs, log = console.log }) {
|
|
65
80
|
if (!/^\d+\.\d+\.\d+([-+][0-9A-Za-z.-]+)?$/.test(String(version ?? ""))) throw new Error(`linux artifacts: version ${JSON.stringify(version)} is not semver`);
|
|
66
81
|
if (!/^[0-9a-f]{40}$/.test(String(commit ?? ""))) throw new Error("linux artifacts: commit must be the 40-hex source commit");
|
|
67
82
|
const arch = linuxArch(run);
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// GB-29 (GATE-CONTRACT GC-L02; P-L, A-L; E4): the private Linux qualifier, only where a manifest DECLARES a Linux target, and the pending candidate that waits for it.
|
|
2
|
+
//
|
|
3
|
+
// A private app qualifies on Mac or Dell, never on a mandatory GitHub CI path. When its manifest declares a Linux deliverable (`targets.linux`), the exact sealed Linux bytes the Linux origin
|
|
4
|
+
// produced are qualified in a COMPATIBLE Linux runtime (OS, architecture, libc) hosted by the Mac or the Dell: they are not rebuilt there. With no compatible runtime the candidate WAITS:
|
|
5
|
+
// a pending record keeps the candidate identity, the exact sealed digest and the pinned policy, nothing is discarded, and a later qualification resumes it only when it binds the same
|
|
6
|
+
// candidate, the same bytes, the same policy digest and every expected check passed. Nothing declared, nothing created: an app that declares no Linux target has no Linux artifact or qualification.
|
|
7
|
+
import { createHash } from "node:crypto";
|
|
8
|
+
|
|
9
|
+
export const PENDING_SCHEMA = "rightkit.pending-linux-candidate/1";
|
|
10
|
+
export const LINUX_QUALIFIER_REFUSED = "RK_LINUX_QUALIFIER_REFUSED";
|
|
11
|
+
const SHA256 = /^[0-9a-f]{64}$/;
|
|
12
|
+
const ARCHES = new Set(["x86_64", "aarch64"]);
|
|
13
|
+
const HOSTS = ["dell", "mac"];
|
|
14
|
+
const refuse = (message) => { throw Object.assign(new Error(`${LINUX_QUALIFIER_REFUSED}: ${message}`), { code: LINUX_QUALIFIER_REFUSED }); };
|
|
15
|
+
const isObject = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
16
|
+
|
|
17
|
+
/** The Linux targets a product manifest declares (`targets.linux`: one object or a list, each `{ arch, libc? }`); none when it declares none. Never inferred from another target. */
|
|
18
|
+
export function declaredLinuxTargets(config) {
|
|
19
|
+
const declared = config?.targets?.linux;
|
|
20
|
+
if (declared === undefined || declared === null) return [];
|
|
21
|
+
const list = Array.isArray(declared) ? declared : [declared];
|
|
22
|
+
return list.map((target, i) => {
|
|
23
|
+
if (!isObject(target) || !ARCHES.has(target.arch) || (target.libc !== undefined && !["glibc", "musl"].includes(target.libc))) refuse(`targets.linux[${i}] must be { arch: x86_64|aarch64, libc?: glibc|musl }`);
|
|
24
|
+
return { os: "linux", arch: target.arch, ...(target.libc ? { libc: target.libc } : {}) };
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Whether one hosted runtime can qualify one declared target: Linux, the same architecture, the same libc when the target names one, and available now. */
|
|
29
|
+
export function runtimeCompatible(runtime, target) {
|
|
30
|
+
return isObject(runtime) && runtime.os === "linux" && runtime.available === true && runtime.arch === target.arch && (target.libc === undefined || runtime.libc === target.libc);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* The qualifier for each declared target, or the reason it waits. `hosts` is the Mac and Dell's hosted Linux runtimes `[{ host: "dell"|"mac", runtime: { kind, os, arch, libc?, available } }]`.
|
|
35
|
+
* `{ state: "NOT_APPLICABLE" }` when nothing is declared; `{ state: "QUALIFIER", assignments }` when every declared target has a compatible hosted runtime (the Dell first, then the Mac, so the
|
|
36
|
+
* choice is stable); `{ state: "WAITING", assignments, missing }` otherwise. Never CI, never a rebuild.
|
|
37
|
+
*/
|
|
38
|
+
export function selectLinuxQualifier({ config, hosts }) {
|
|
39
|
+
const targets = declaredLinuxTargets(config);
|
|
40
|
+
if (targets.length === 0) return { state: "NOT_APPLICABLE", reason: "the manifest declares no Linux target: no Linux artifact or qualification is created" };
|
|
41
|
+
const ordered = [...(hosts ?? [])].filter((h) => HOSTS.includes(h?.host)).sort((a, b) => HOSTS.indexOf(a.host) - HOSTS.indexOf(b.host));
|
|
42
|
+
const assignments = []; const missing = [];
|
|
43
|
+
for (const target of targets) {
|
|
44
|
+
const hit = ordered.find((h) => runtimeCompatible(h.runtime, target));
|
|
45
|
+
if (hit) assignments.push({ target, host: hit.host, runtime: { kind: hit.runtime.kind, os: "linux", arch: hit.runtime.arch, ...(hit.runtime.libc ? { libc: hit.runtime.libc } : {}) } });
|
|
46
|
+
else missing.push({ target, reason: ordered.length ? `no hosted Linux runtime of ${ordered.map((h) => `${h.host}:${h.runtime?.arch ?? "?"}${h.runtime?.available === true ? "" : " (unavailable)"}`).join(", ")} is compatible with ${target.arch}${target.libc ? `/${target.libc}` : ""}` : "no Mac or Dell hosted Linux runtime is registered" });
|
|
47
|
+
}
|
|
48
|
+
return missing.length ? { state: "WAITING", assignments, missing } : { state: "QUALIFIER", assignments };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const digest = (text) => createHash("sha256").update(text).digest("hex");
|
|
52
|
+
|
|
53
|
+
/** The pending record of a Linux candidate that has no qualifier yet: it keeps everything a later qualification must bind to, and discards nothing. */
|
|
54
|
+
export function pendingCandidate({ candidate, artifactSha256, target, policyDigest, missing }) {
|
|
55
|
+
if (!SHA256.test(candidate ?? "") || !SHA256.test(artifactSha256 ?? "") || !SHA256.test(policyDigest ?? "")) refuse("a pending candidate names its candidate identity, sealed artifact digest and pinned policy digest");
|
|
56
|
+
return { schema: PENDING_SCHEMA, state: "WAITING", candidate, artifactSha256, target, policyDigest, missing: missing ?? [] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Resume a pending candidate from a qualification run. Refuses (the pending record stays WAITING, unchanged) unless the evidence binds the same candidate and the exact sealed bytes, was
|
|
61
|
+
* produced by a runtime compatible with the declared target on a Mac or Dell host, ran under the policy digest pinned before the run, rebuilt nothing, and executed every expected check with a
|
|
62
|
+
* pass: a missing, skipped or failed check, an empty suite or a changed policy fails.
|
|
63
|
+
*/
|
|
64
|
+
export function resumeCandidate(pending, evidence) {
|
|
65
|
+
if (!isObject(pending) || pending.schema !== PENDING_SCHEMA || pending.state !== "WAITING") refuse("only a pending (WAITING) Linux candidate can be resumed");
|
|
66
|
+
if (!isObject(evidence)) refuse("the qualification evidence is not an object");
|
|
67
|
+
if (evidence.candidate !== pending.candidate) refuse("the evidence is for another candidate");
|
|
68
|
+
if (evidence.artifactSha256 !== pending.artifactSha256) refuse(`the evidence qualified ${String(evidence.artifactSha256).slice(0, 12)}, not the sealed ${pending.artifactSha256.slice(0, 12)}: the exact sealed bytes are qualified, never a rebuild`);
|
|
69
|
+
if (evidence.rebuilt === true) refuse("the qualifier rebuilt the bytes: sealed Linux bytes are qualified unchanged");
|
|
70
|
+
if (!HOSTS.includes(evidence.host)) refuse(`the qualifier host ${JSON.stringify(evidence.host)} is not the Mac or the Dell`);
|
|
71
|
+
if (!runtimeCompatible({ ...evidence.runtime, available: true }, pending.target)) refuse("the qualifier's runtime is not compatible with the declared target");
|
|
72
|
+
if (evidence.policyDigest !== pending.policyDigest) refuse("the qualification ran under another policy than the one pinned before the run");
|
|
73
|
+
const expected = evidence.expectedChecks; const executed = evidence.checks;
|
|
74
|
+
if (!Array.isArray(expected) || expected.length === 0) refuse("an empty mandatory suite is never a qualification");
|
|
75
|
+
if (!Array.isArray(executed)) refuse("the evidence carries no executed checks");
|
|
76
|
+
for (const id of expected) {
|
|
77
|
+
const run = executed.filter((c) => c?.id === id);
|
|
78
|
+
if (run.length !== 1) refuse(`expected check ${id} was ${run.length === 0 ? "not executed" : "executed more than once"}`);
|
|
79
|
+
if (run[0].result !== "pass") refuse(`check ${id} is ${run[0].result}`);
|
|
80
|
+
}
|
|
81
|
+
if (executed.some((c) => !expected.includes(c?.id))) refuse("the evidence carries a check the pinned policy does not expect");
|
|
82
|
+
return { state: "QUALIFIED", candidate: pending.candidate, artifactSha256: pending.artifactSha256, host: evidence.host, evidenceDigest: digest(JSON.stringify(evidence)) };
|
|
83
|
+
}
|
package/local-publish.mjs
CHANGED
|
@@ -29,6 +29,7 @@ import { refuseHostUpload } from "./host-upload-refusal.mjs";
|
|
|
29
29
|
import { pathDependencyDirs, readCargoPackage } from "./crate-graph.mjs";
|
|
30
30
|
import { recordPublished, stageSourceVersions, workspaceFeedOptions } from "./rightkit-sync.mjs";
|
|
31
31
|
import { prepareSwapHelper, SWAP_HELPER_CARGO_ARGS, SWAP_HELPER_PACKAGE } from "./swap-helper-bundle.mjs";
|
|
32
|
+
import { exclusionsFor, npmContentDigest } from "./pack-digest.mjs";
|
|
32
33
|
import { requireDoorRun, gateStateDir } from "./gate-stages.mjs";
|
|
33
34
|
|
|
34
35
|
export const LOCAL_OPERATOR = "agent (local-build grant)";
|
|
@@ -546,6 +547,16 @@ async function publishUnit(parsed, deps, batch) {
|
|
|
546
547
|
return result.value;
|
|
547
548
|
}
|
|
548
549
|
|
|
550
|
+
|
|
551
|
+
/**
|
|
552
|
+
* The host's scratch root for an npm pack: the disk policy's root (`/Volumes/D/scratch`, `D:\scratch`) where it exists, the system temp directory where it does not
|
|
553
|
+
* (a hosted runner, a cloud container, any host the disk policy does not govern). The pack destination is always a fresh child of the root that is returned.
|
|
554
|
+
*/
|
|
555
|
+
export function hostScratchRoot(platform, exists = fs.existsSync) {
|
|
556
|
+
const root = platform === "win32" ? "D:\\scratch" : "/Volumes/D/scratch";
|
|
557
|
+
return exists(root) ? root : os.tmpdir();
|
|
558
|
+
}
|
|
559
|
+
|
|
549
560
|
async function* publishUnitStages(parsed, deps, batch) {
|
|
550
561
|
const log = deps.log ?? ((line) => process.stdout.write(`${line}\n`));
|
|
551
562
|
const { kind, unit, allowIgnored, workspaceRoot } = parsed;
|
|
@@ -559,6 +570,10 @@ async function* publishUnitStages(parsed, deps, batch) {
|
|
|
559
570
|
// Every pnpm step goes through the one door for build and test launches (build-launch.mjs): a rightkit-pnpm broker job on a
|
|
560
571
|
// managed host (refused, never run directly, when the broker is unreachable), direct on a host without one. A run headed to release
|
|
561
572
|
// then also has the signed receipt each stage owes (stage-commands.mjs). Other commands (cargo is already a broker job) are unchanged.
|
|
573
|
+
// A release-bound stage carries its nonce in RIGHTKIT_ATTEST_STAGE_NONCE (unit-gate.mjs stageEnv), which is no carrier variable. Every pnpm
|
|
574
|
+
// and Cargo step of a snapshot stage is a carried request of its own (build-launch.mjs routes pnpm to the carried adapter runner, carried-cargo.mjs
|
|
575
|
+
// runCarriedAdapter), and each derives its per-call nonce from the stage nonce (snapshot-broker.mjs carryBrokerAdapter and carryBrokerCargo,
|
|
576
|
+
// rightkit#1301 B8), so nothing is given the stage nonce itself and no step sets RUSTRIGHT_ATTEST_NONCE.
|
|
562
577
|
const run = (command, args, options) => {
|
|
563
578
|
const target = command === "pnpm" ? buildLaunch(command, args, { env: options?.env ?? process.env, platform: deps.platform ?? process.platform, exists: fs.existsSync }) : { command, args };
|
|
564
579
|
const result = launch(target.command, target.args, options);
|
|
@@ -668,22 +683,20 @@ async function* publishUnitStages(parsed, deps, batch) {
|
|
|
668
683
|
if (packageEnv) Object.assign(env, testEnv({ env: packageEnv }, isAssetGated));
|
|
669
684
|
|
|
670
685
|
// 3. Pack the artifact and hash it.
|
|
671
|
-
// An npm pack is a broker job whose --
|
|
672
|
-
//
|
|
673
|
-
|
|
686
|
+
// An npm pack is a broker job whose --pack-destination the adapter accepts only as an empty child of the host's scratch root, so the
|
|
687
|
+
// tarball goes there (the workspace rule for run output) rather than in the OS temp directory; the package itself is packed from the
|
|
688
|
+
// snapshot (relative --dir).
|
|
689
|
+
const scratchRoot = deps.tmpdir ?? (kind === "npm" ? hostScratchRoot(platform, deps.exists ?? fs.existsSync) : os.tmpdir());
|
|
674
690
|
const scratch = fs.mkdtempSync(path.join(scratchRoot, "rk-local-publish-"));
|
|
675
691
|
const cleanup = () => fs.rmSync(scratch, { recursive: true, force: true });
|
|
676
692
|
try {
|
|
677
693
|
let artifactFile; const packCommands = [];
|
|
678
|
-
let packDir = dir;
|
|
679
694
|
if (kind === "npm" && unit === SWAP_HELPER_PACKAGE) {
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
}
|
|
685
|
-
// #258: the package ships a Developer-ID-signed bin/darwin/rightkit-swap; build, sign and verify it before packing (fails closed).
|
|
686
|
-
try { (deps.prepareSwapHelper ?? prepareSwapHelper)({ workspaceRoot, packageDir: parsed.qualifyOnly ? packDir : unitPath, platform, run, env, log }); }
|
|
695
|
+
// #258: the package ships a Developer-ID-signed bin/darwin/rightkit-swap; build, sign and verify it before packing (fails closed). The
|
|
696
|
+
// helper and its signing receipt are written into the unit's own directory of the snapshot (packages/release/bin/darwin, both ignored by
|
|
697
|
+
// packages/release/.gitignore): declared stage outputs of this stage, recorded by the gate's stage-output ledger (stage-ledger.mjs) like
|
|
698
|
+
// any ignored generated file, never a copy of the package kept outside the snapshot, so the pack below runs from the snapshot.
|
|
699
|
+
try { (deps.prepareSwapHelper ?? prepareSwapHelper)({ workspaceRoot, packageDir: unitPath, platform, run, env, log }); }
|
|
687
700
|
catch (error) { refuse(`${error.code ? `[${error.code}] ` : ""}${error.message}`); }
|
|
688
701
|
packCommands.push(`rustright cargo ${SWAP_HELPER_CARGO_ARGS.join(" ")} && codesign --verify --strict bin/darwin/rightkit-swap`);
|
|
689
702
|
}
|
|
@@ -695,7 +708,9 @@ async function* publishUnitStages(parsed, deps, batch) {
|
|
|
695
708
|
// child of a scratch directory (never the scratch directory itself) and refuses one that already holds a tarball.
|
|
696
709
|
const packOut = path.join(scratch, "pack");
|
|
697
710
|
fs.mkdirSync(packOut);
|
|
698
|
-
|
|
711
|
+
// `--dir` is the unit's directory RELATIVE to the repository root, where the step runs: a snapshot-bound adapter job refuses an absolute --dir; only
|
|
712
|
+
// --pack-destination is outside the snapshot (an empty child of the scratch directory).
|
|
713
|
+
const args = npmPackArgs(dir, packOut);
|
|
699
714
|
packCommands.push(`pnpm ${args.join(" ")}`);
|
|
700
715
|
const packed = await run("pnpm", args, { cwd: workspaceRoot, env });
|
|
701
716
|
if (packed.error || packed.status !== 0) refuse(`pnpm pack failed for ${unit}\n${tail(`${packed.stdout ?? ""}\n${packed.stderr ?? ""}`)}`);
|
|
@@ -725,6 +740,7 @@ async function* publishUnitStages(parsed, deps, batch) {
|
|
|
725
740
|
if (!fs.existsSync(artifactFile)) refuse(`cargo package did not produce ${artifactFile}`);
|
|
726
741
|
}
|
|
727
742
|
const artifactSha256 = sha256(fs.readFileSync(artifactFile));
|
|
743
|
+
let contentSha256 = null, contentExcluded = [];
|
|
728
744
|
if (parsed.qualifyOnly && (git(["rev-parse", "HEAD"], workspaceRoot) !== head || git(["status", "--porcelain", "--", ...guardedPaths], workspaceRoot) !== "")) refuse("qualification inputs changed while preparing the artifact; no bundle was created");
|
|
729
745
|
if (parsed.qualifyOnly) {
|
|
730
746
|
fs.mkdirSync(parsed.bundleDir, { recursive: false });
|
|
@@ -732,7 +748,13 @@ async function* publishUnitStages(parsed, deps, batch) {
|
|
|
732
748
|
fs.copyFileSync(artifactFile, bundledArtifact, fs.constants.COPYFILE_EXCL);
|
|
733
749
|
if (sha256(fs.readFileSync(bundledArtifact)) !== artifactSha256) refuse("qualification archive changed while copying");
|
|
734
750
|
artifactFile = bundledArtifact;
|
|
735
|
-
|
|
751
|
+
// An npm archive is bound hosted by what it ships (pack-digest.mjs), not by its bytes; the swap helper and its signing receipt are
|
|
752
|
+
// the only entries left out, for @rightkit/release alone, and their presence is recorded.
|
|
753
|
+
if (kind === "npm") {
|
|
754
|
+
try { const digest = npmContentDigest(artifactFile, { exclude: exclusionsFor(unit) }); contentSha256 = digest.contentSha256; contentExcluded = Object.keys(digest.excluded).sort(); }
|
|
755
|
+
catch (error) { refuse(`${unit}: cannot read the packed archive for its content digest (${error.message})`); }
|
|
756
|
+
}
|
|
757
|
+
yield { stage: "package", kind, name: unit, version, commit: head, artifactFile, artifactSha256, ...(contentSha256 ? { contentSha256, contentExcluded } : {}), brokerRequests: brokerRequests.splice(0) };
|
|
736
758
|
if (sha256(fs.readFileSync(artifactFile)) !== artifactSha256) refuse("qualification archive changed after packaging");
|
|
737
759
|
}
|
|
738
760
|
|
|
@@ -767,7 +789,7 @@ async function* publishUnitStages(parsed, deps, batch) {
|
|
|
767
789
|
// Bytes and evidence stay together outside the checkout.
|
|
768
790
|
const bundledEvidence = evidencePath;
|
|
769
791
|
log(`[right-release] qualified ${unit}@${version}; bundle ${parsed.bundleDir}${kind === "cargo" ? `; publish cargo --crate ${unit} --from-evidence ${bundledEvidence}` : ""}`);
|
|
770
|
-
return { kind, name: unit, version, commit: head, artifactSha256, evidencePath: bundledEvidence, artifactFile, qualifiedOnly: true };
|
|
792
|
+
return { kind, name: unit, version, commit: head, artifactSha256, ...(contentSha256 ? { contentSha256, contentExcluded } : {}), evidencePath: bundledEvidence, artifactFile, qualifiedOnly: true };
|
|
771
793
|
}
|
|
772
794
|
|
|
773
795
|
// 5. The EXISTING publish path.
|
package/local-runtime.mjs
CHANGED
|
@@ -33,7 +33,7 @@ const round3 = (value) => Math.round(value * 1000) / 1000;
|
|
|
33
33
|
const tail = (text, lines = 30) => String(text ?? "").split(/\r?\n/).slice(-lines).join("\n");
|
|
34
34
|
const loadFromRoot = (root, ...segments) => import(pathToFileURL(path.join(root, ...segments)).href);
|
|
35
35
|
const CREDENTIAL_LIKE = /TOKEN|PASSWORD|SECRET|CREDENTIAL|^ACTIONS_ID_TOKEN/i;
|
|
36
|
-
const BROKER_ENV = ["RIGHTKIT_BUILD_BROKER_SOCKET", "RIGHTKIT_CONTROL_ROOT", "RIGHTKIT_DATA_ROOT", "RIGHTKIT_MANAGED_ROOT"];
|
|
36
|
+
const BROKER_ENV = ["RUSTRIGHT_BUILD_BROKER_SOCKET", "RIGHTKIT_BUILD_BROKER_SOCKET", "RIGHTKIT_CONTROL_ROOT", "RIGHTKIT_DATA_ROOT", "RIGHTKIT_MANAGED_ROOT"];
|
|
37
37
|
const BENCH_TIMEOUT_MS = 40 * 60 * 1000;
|
|
38
38
|
|
|
39
39
|
// ---- host evidence -------------------------------------------------------------------------------
|
|
@@ -173,7 +173,7 @@ export async function measureRuntimeCrate(options, deps = {}) {
|
|
|
173
173
|
const baseEnv = Object.fromEntries(Object.entries(deps.env ?? process.env).filter(([name]) => !CREDENTIAL_LIKE.test(name)));
|
|
174
174
|
const brokerEnv = Object.fromEntries(BROKER_ENV.filter((name) => baseEnv[name]).map((name) => [name, baseEnv[name]]));
|
|
175
175
|
const socket = discoverBrokerSocket(deps.env ?? process.env, { platform });
|
|
176
|
-
if (socket) brokerEnv.
|
|
176
|
+
if (socket) brokerEnv.RUSTRIGHT_BUILD_BROKER_SOCKET = socket;
|
|
177
177
|
const run = deps.run ?? ((command, args, settings) => spawnCollect(command, args, { maxBuffer: 64 * 1024 * 1024, ...settings }));
|
|
178
178
|
const invoke = (args, settings) => run(cargoCommand[0], [...cargoCommand.slice(1), ...args], { cwd: crateDir, windowsHide: true, ...settings });
|
|
179
179
|
const toolchainOf = async () => {
|