@rightkit/release 0.6.10 → 0.6.12

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.
Files changed (69) hide show
  1. package/README.md +7 -5
  2. package/admission-files.mjs +3 -0
  3. package/admission.mjs +2 -2
  4. package/advisory-exceptions.mjs +41 -5
  5. package/android-play-upload.mjs +16 -15
  6. package/app-check.mjs +13 -9
  7. package/bin/rightkit-gate.mjs +772 -355
  8. package/broker-pins.mjs +152 -0
  9. package/build-release.mjs +80 -27
  10. package/cargo-ledger.mjs +62 -0
  11. package/cargo-target-shim.mjs +98 -26
  12. package/cli/right-release.mjs +28 -4
  13. package/clippy-equivalence.mjs +202 -0
  14. package/clippy-route.mjs +543 -0
  15. package/committed-source.mjs +39 -5
  16. package/crate-workspaces.mjs +87 -0
  17. package/create-mac-updater.mjs +14 -2
  18. package/deterministic-scans.mjs +196 -21
  19. package/development.mjs +13 -4
  20. package/direct-receipt.mjs +135 -0
  21. package/door-policy.mjs +10 -0
  22. package/door.mjs +6 -6
  23. package/freshness-census.mjs +257 -0
  24. package/gate-stages.mjs +76 -10
  25. package/hook-retirement.mjs +65 -0
  26. package/init-command.mjs +1 -1
  27. package/inventory-check.mjs +11 -8
  28. package/ios-upload.mjs +20 -15
  29. package/local-publish.mjs +77 -41
  30. package/local-qualify.mjs +5 -3
  31. package/local-runtime.mjs +5 -4
  32. package/mac-package-recipe.mjs +4 -1
  33. package/mirror-root-artifact.mjs +18 -5
  34. package/ownership-class-rules.mjs +1 -1
  35. package/ownership-evidence.mjs +37 -13
  36. package/ownership-hooks.mjs +20 -59
  37. package/ownership-lockfiles.mjs +14 -1
  38. package/ownership-policy-bundle.mjs +3 -2
  39. package/ownership-staged.mjs +2 -1
  40. package/ownership.mjs +24 -0
  41. package/package-recipe-targets.mjs +35 -6
  42. package/package.json +1 -1
  43. package/pipeline-admission.mjs +4 -4
  44. package/preflight.mjs +27 -4
  45. package/publish-cargo.mjs +4 -3
  46. package/recipe-build.mjs +38 -14
  47. package/release-artifact-path.mjs +341 -0
  48. package/release-invocation.mjs +8 -1
  49. package/release-qa-command.mjs +25 -9
  50. package/release-source-brand.mjs +17 -0
  51. package/release-source-identity.mjs +171 -0
  52. package/release.mjs +44 -17
  53. package/rightkit-versions.json +63 -42
  54. package/runtime-budgets.mjs +6 -2
  55. package/scan-digest.mjs +71 -5
  56. package/sign-updater.mjs +6 -3
  57. package/snapshot-broker.mjs +172 -0
  58. package/snapshot-check.mjs +12 -6
  59. package/snapshot-request.mjs +207 -5
  60. package/snapshot.mjs +123 -10
  61. package/source-split.mjs +73 -0
  62. package/stage-commands.mjs +2 -2
  63. package/stage-ledger.mjs +204 -0
  64. package/stage-sidecar-command.mjs +5 -1
  65. package/stage-sidecar.mjs +100 -20
  66. package/swap-helper-bundle.mjs +5 -2
  67. package/unit-gate.mjs +45 -3
  68. package/web-build.mjs +6 -4
  69. package/win-package-recipe.mjs +20 -7
package/README.md CHANGED
@@ -213,14 +213,14 @@ Adrian, 2026-10-26: the apps lost every build route on 2026-10-25 and build agai
213
213
 
214
214
  | Tier | Rules | Owed by |
215
215
  |---|---|---|
216
- | `compile` (default) | the 13 rules @rightkit/release 0.6.6 published (`COMPILE_TIER_IDS`) | compile admission: the RustRight broker, the pre-push hook, `rightkit-gate check`, `--to compile\|package`, `right-release build\|dev\|test\|doctor` |
216
+ | `compile` (default) | the 13 rules @rightkit/release 0.6.6 published (`COMPILE_TIER_IDS`) | compile admission: the RustRight broker, `rightkit-gate check`, `--to compile\|package`, `right-release build\|dev\|test\|doctor` |
217
217
  | `release` | every rule, the ones added since 0.6.6 included (C3, SC, CA, LG, SF-01, OW-05, OW-06), plus the `shellcheck` scan and actionlint's shellcheck requirement | `rightkit-gate --to qualify` (it runs `check --rule-tier release` before installed QA and records `ruleTier` in the receipt) and `right-release release\|qa\|upload\|publish\|publish-update\|github\|sign-*\|create-mac-updater\|sign-candidate` (`RELEASE_TIER_COMMANDS`) |
218
218
 
219
219
  `rightkit-gate check --rule-tier compile|release` selects the tier (`--tier patch|update` is the upload tier); `--list` names each rule's tier; `--only` is a view over the whole set. A rule is promoted to the compile tier by moving its id into `COMPILE_TIER_IDS` in RightKit, never per app and never through a baseline. RightKit's own preflight runs every rule.
220
220
 
221
- ## The pre-push gate on a host without the broker: the Rust gate is deferred, the HOST RECEIPT gates the merge
221
+ ## The gate on a host without the broker, and tests that reach it
222
222
 
223
- The gate chooses its executor itself (`gate-executor.mjs` `resolveExecutor`), never the caller: the RustRight broker on a managed Mac or Dell (chosen by host identity, not by `rustright` on PATH), a refusal on a broker-managed machine whose broker cannot be reached, and direct `cargo` on every Linux host and any other host (a cloud session, a CI runner). Every host runs the same gates in the same order (app rules, class A, then the compile scans); an unmanaged host is never sent to a broker and nothing is deferred to one.
223
+ The gate chooses its executor itself (`gate-executor.mjs` `resolveExecutor`), never the caller: the RustRight broker on a managed Mac or Dell (chosen by host identity, not by `rustright` on PATH), a refusal on a broker-managed machine whose broker cannot be reached, and direct `cargo` on every Linux host and any other host (a cloud session, a CI runner). Every host runs the same gates in the same order (app rules, class A, then the compile scans); an unmanaged host is never sent to a broker and nothing is deferred to one. Commit and push run no gate and no local hook check, and `install-hook` writes no git hook (below); when git's ref lines are nevertheless given on stdin (the form a pre-push hook receives), every ref is judged in any position (split on single ASCII spaces, so a Unicode space in a ref name cannot hide one), and a line that is not four fields is refused, never dropped, so the pushed commit is always compared with HEAD.
224
224
 
225
225
  `scriptBrokerCall` reads `package.json` scripts only: it follows `pnpm <script>`, `pnpm run`, `npm run`, `yarn` and the wrapper words, not `node -e`, a node script, `sh -c`, or a run in another package (`pnpm --dir`, `--filter`, `-r`). A unit whose test reaches the broker that way says so in the app's own `ci/gate.config.json`, in the `npm` entry the unit's `quietTests` already uses (the key is the unit name: the package name, or the directory with `/` as `-` for a unit of a pnpm root below the repository root):
226
226
 
@@ -231,9 +231,11 @@ The gate chooses its executor itself (`gate-executor.mjs` `resolveExecutor`), ne
231
231
 
232
232
  The `reason` is required and is printed. The file is read from the gated commit, so the declaration is a reviewed change in the diff that adds it. Where the Rust gate is deferred, the unit's `test:npm:<unit>` scan is deferred with it and named `declared in ci/gate.config.json npm.<unit>.requiresBroker: <reason>`, never reported as passed; on a host with the broker, in CI, on a protected ref and with the opt-out it runs as it did. A declaration of any other shape (`true`, a string, an empty or blank `reason`, an array) is ignored with a note and the test runs where it can: a malformed entry never skips a test. `false` or `null` is no declaration, and a call the scripts do show is reported from the scripts. The gate does not read node scripts for a literal `rustright` or `cargo`: a literal cannot tell a spawn from a command table, a mention or a test fixture, so the declaration is the way.
233
233
 
234
- ## The pre-push hook finds the gate in a repository whose root installs nothing
234
+ ## `install-hook` writes no git hook, and retires the ones an earlier release wrote
235
235
 
236
- `rightkit-gate install-hook` writes `.githooks/pre-push` (and sets `core.hooksPath`). The hook runs the repository's own `.git/hooks/pre-push`, then `rightkit-gate check --commit HEAD`, then the gate, and finds the gate binary in `node_modules/.bin/rightkit-gate` of the repository root, else of any workspace package up to three levels down, else through `pnpm exec`, else with `node` at a path written into the hook. That path is always relative to the repository, the same for every version and a plain name, never absolute, never `../` out of the repository, never a `node_modules/.pnpm/@rightkit+release@x.y.z/` store path, and it is decided by what the tracked `package.json` files declare, never by what is installed: the same repository gets the same hook on a fresh clone, from an exports checkout, after `pnpm install` and after a version bump, so `install-hook` as a `prepare` script never meets a hook it wrote earlier as one that "already exists and differs". When the root `package.json` declares `@rightkit/release` (alone, or together with packages: Pulse declares it at the root and in `hub/`), the path is the root's `node_modules/@rightkit/release/bin/rightkit-gate.mjs` and the hook is the single-root form. When only packages declare it (CutRight's `apps/cutright`, HeardRight's `tauri-app-next`), the path is the same link in the first of them in sorted order, whichever is installed, the hook also tries each declaring package's `node_modules/.bin/rightkit-gate` directly (deeper than the glob reaches) and it runs the `pnpm` fallback as `pnpm --dir <package> exec rightkit-gate`, because `pnpm exec` at a root that does not declare the package has no `rightkit-gate` to run. When no `package.json` declares it, the gate's own path inside the repository if it runs from there and not from a `node_modules` (a checkout of the package itself), else the root's link. A declaring package whose directory is not made of letters, digits and `_ - . @ +` is named in a warning and left out. A root-install repository gets, byte for byte, the hook that 0.6.4 and 0.6.5 wrote (`fixtures/published-gate-hooks.json`); a multi-root repository whose committed hook still has a baked path regenerates it once with `rightkit-gate install-hook --force`, and the result no longer changes with the version or the install.
236
+ Commit and push run no gate and no local hook check (`docs/GATE-CONTRACT.md`: the gate is for building; CI runs preflight and the full check on every push). `rightkit-gate install-hook` (a `prepare` script; `right-release hooks install` is an alias) registers the one-door agent hook in `.claude/settings.json` and writes no git hook: no `.githooks/pre-push`, no forwarder for the hooks in `.git/hooks`, no pre-commit ownership step, and it no longer sets `core.hooksPath`.
237
+
238
+ It removes what an earlier release wrote, each file matched by its own text only: `.githooks/pre-push` by its header (line 2: `# Mandatory local [check and] gate before every push (rightkit-gate install-hook). Never bypass with --no-verify.`), a `.githooks/<name>` forwarder by its header naming that same hook, and the ownership pre-commit hook by the marker `rightkit-ownership-precommit v1` in its first three lines (in `.githooks`, in `.git/hooks`, or in the directory `core.hooksPath` names); the foreign pre-commit the ownership step kept as `pre-commit.rightkit-foreign` goes back to `pre-commit`. A foreign hook, a symbolic link, a directory and any file whose header differs are never read for change, moved or deleted. It unsets the repository's own `core.hooksPath` only when its value is exactly `.githooks` and nothing foreign remains in that directory (an emptied `.githooks` is removed, and the hooks in `.git/hooks` run again); otherwise it leaves the setting and says so. The global and system git configuration is never written. `--force` is accepted and ignored: there is nothing to overwrite. `right-release ownership --staged` stays as a manual command; broker admission and the release gate remain the authoritative ownership checks. An app deletes its committed `.githooks/pre-push` after the release carrying this change is published.
237
239
 
238
240
  ## The rustfmt scan reads each file's edition from its crate
239
241
 
@@ -69,6 +69,9 @@ export const FILES_ROWS = Object.freeze([
69
69
  run(ctx) {
70
70
  const inputs = ctx.buildInputs();
71
71
  if (!inputs.ok) return { skipped: "build-inputs is reported on its own" };
72
+ // A door source split (row 7.04): `repoRoot` is then the snapshot, the commit's bytes verified by the door at the start and end of the
73
+ // stage, and there is no checkout cleanliness to ask. Every input resolved under it by construction.
74
+ if (ctx.releaseSource?.mode === "snapshot") return undefined;
72
75
  const dirty = dirtyBuildInputs(ctx.repoRoot, inputs.value.buildInputs, ctx.commit());
73
76
  return dirty.length > 0 ? [`dirty files can change the packaged app; resolve them before release:\n${dirty.map((file) => `- ${file}`).join("\n")}`] : undefined;
74
77
  },
package/admission.mjs CHANGED
@@ -60,7 +60,7 @@ function gitHead(repoRoot, spawn) {
60
60
  export async function runAdmission({
61
61
  root, repoRoot = root, configPath, config, platform, target, tiers = TIERS, env = process.env, spawn = spawnSync, registry,
62
62
  log = () => {}, hostPlatform = process.platform, mode = "build", skipChecks = false, skipDoctorOwned = false, commit,
63
- fs = { existsSync, readdirSync, readFileSync, statSync }, feed, rows = ADMISSION_ROWS, resolveInputs = resolveBuildInputs,
63
+ fs = { existsSync, readdirSync, readFileSync, statSync }, feed, rows = ADMISSION_ROWS, resolveInputs = resolveBuildInputs, releaseSource = null,
64
64
  } = {}) {
65
65
  const findings = [];
66
66
  const results = [];
@@ -78,7 +78,7 @@ export async function runAdmission({
78
78
  const exists = fs.existsSync;
79
79
  const projectType = memo(() => resolveProjectType({ appRoot: root, config, target }));
80
80
  const ctx = {
81
- root, repoRoot, configPath, config, platform, target, env, spawn, registry, log, hostPlatform, mode, skipChecks, fs, exists, feed, out,
81
+ root, repoRoot, releaseSource, configPath, config, platform, target, env, spawn, registry, log, hostPlatform, mode, skipChecks, fs, exists, feed, out,
82
82
  // The target as the config declares it: the build controller judges a few rules on it, the worker on the expanded one.
83
83
  rawTarget: config?.targets?.[platform],
84
84
  projectType,
@@ -1,9 +1,11 @@
1
- // Advisory exception gate. ci/advisory-exceptions.json is the single source; deny.toml
1
+ // Advisory exception gate. Every crate is its own Cargo workspace (rightkit#1285), and a crate that ignores an advisory keeps its
2
+ // own single source, crates/<crate>/.rightkit/advisory-exceptions.json (CRATE_EXCEPTIONS_FILE): the crate's deny.toml
2
3
  // [advisories].ignore and .cargo/audit.toml must match it exactly, every entry needs an owner,
3
4
  // evidence and an unexpired absolute reviewBy date, and the entry's versions must match what
4
- // Cargo.lock actually holds, `dependents` must equal the exact set of direct reverse
5
- // dependencies of the affected versions in Cargo.lock, and every hop of `path` must be a real
6
- // dependency edge (scope drift fails in both directions). Policy: Astra 2026-10-22.
5
+ // the crate's Cargo.lock actually holds, `dependents` must equal the exact set of direct reverse
6
+ // dependencies of the affected versions in that Cargo.lock, and every hop of `path` must be a real
7
+ // dependency edge (scope drift fails in both directions). A crate with no ignore needs no ledger.
8
+ // There is no repository-level ledger. Policy: Astra 2026-10-22.
7
9
  import fs from 'node:fs';
8
10
  import path from 'node:path';
9
11
  import { fileURLToPath } from 'node:url';
@@ -12,6 +14,8 @@ export const EXCEPTIONS_FILE = 'ci/advisory-exceptions.json';
12
14
  export const DENY_FILE = 'deny.toml';
13
15
  export const AUDIT_FILE = '.cargo/audit.toml';
14
16
  export const LOCK_FILE = 'Cargo.lock';
17
+ /** The per-workspace ledger, relative to the workspace root (a crate directory). */
18
+ export const CRATE_EXCEPTIONS_FILE = '.rightkit/advisory-exceptions.json';
15
19
 
16
20
  const REQUIRED = ['id', 'crate', 'path', 'owner', 'rationale', 'evidence', 'reviewBy', 'approvedBy'];
17
21
  const ID_RE = /^RUSTSEC-\d{4}-\d{4}$/;
@@ -359,12 +363,44 @@ export function checkAdvisoryExceptions({ root = process.cwd(), now = new Date()
359
363
  return problems;
360
364
  }
361
365
 
366
+ // ---- per crate ----
367
+
368
+ // A non-empty `ignore = [...]` list (an empty list followed by a later `]` is not one).
369
+ const IGNORE_LIST_RE = /\bignore\s*=\s*\[[^\]]*[^\]\s][^\]]*\]/s;
370
+
371
+ /**
372
+ * Every crate workspace of the repository (crates/<x> holding a Cargo.toml) checked against its own ledger. A crate whose deny.toml or
373
+ * .cargo/audit.toml ignores an advisory must have its ledger; a crate with the ledger is checked in full against its own deny.toml,
374
+ * .cargo/audit.toml and Cargo.lock; a crate with neither is fine. Problems are prefixed with the crate directory.
375
+ * `io` = { readFile(absPath), readdir(absDir), exists(absPath) }, for tests.
376
+ */
377
+ export function checkCrateAdvisoryExceptions({ root = process.cwd(), now = new Date(), allowPending = false, io = {} } = {}) {
378
+ const readFile = io.readFile ?? (p => fs.readFileSync(p, 'utf8'));
379
+ const readdir = io.readdir ?? (d => fs.readdirSync(d));
380
+ const exists = io.exists ?? fs.existsSync;
381
+ const cratesDir = path.join(root, 'crates');
382
+ if (!exists(cratesDir)) return [];
383
+ const problems = [];
384
+ for (const entry of [...readdir(cratesDir)].map(String).sort()) {
385
+ const dir = `crates/${entry}`;
386
+ if (entry.startsWith('.') || !exists(path.join(root, dir, 'Cargo.toml'))) continue;
387
+ const here = path.join(root, dir);
388
+ const ignores = [DENY_FILE, AUDIT_FILE].some(f => { try { return IGNORE_LIST_RE.test(readFile(path.join(here, f))); } catch { return false; } });
389
+ if (!exists(path.join(here, CRATE_EXCEPTIONS_FILE))) {
390
+ if (ignores) problems.push(`${dir}: ${DENY_FILE} or ${AUDIT_FILE} ignores an advisory but ${CRATE_EXCEPTIONS_FILE} does not exist`);
391
+ continue;
392
+ }
393
+ for (const p of checkAdvisoryExceptions({ root: here, now, allowPending, exceptionsFile: CRATE_EXCEPTIONS_FILE, readFile: f => readFile(path.join(here, f)) })) problems.push(`${dir}: ${p}`);
394
+ }
395
+ return problems;
396
+ }
397
+
362
398
  // ---- CLI ----
363
399
 
364
400
  if (process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
365
401
  const args = process.argv.slice(2);
366
402
  const root = path.resolve(import.meta.dirname, '..', '..');
367
- const problems = checkAdvisoryExceptions({ root, allowPending: args.includes('--allow-pending') });
403
+ const problems = checkCrateAdvisoryExceptions({ root, allowPending: args.includes('--allow-pending') });
368
404
  if (problems.length) {
369
405
  for (const p of problems) console.error(`advisory-exceptions: ${p}`);
370
406
  console.error(`advisory-exceptions: ${problems.length} problem(s)`);
@@ -23,15 +23,14 @@
23
23
  import { createSign } from "node:crypto";
24
24
  import { existsSync, mkdirSync, writeFileSync } from "node:fs";
25
25
  import path from "node:path";
26
- import { spawnSync } from "./background-process.mjs";
27
26
  import { defaultReleaseConfigFile, importReleaseConfig } from "./release-config.mjs";
28
27
  import { describeAabProblems, scanAabBuffer } from "./android-aab-scan.mjs";
29
28
  import { sealedManifestAllowsContentDevtools } from "./devtools-policy.mjs";
30
29
  import { targetDistribution } from "./target-distribution.mjs";
31
30
  import { AndroidReleaseError, createAndroidCredentialStore, resolveAndroidSecret, validateSecretSource } from "./android-release.mjs";
32
31
  import { assertArtifactUnchanged, digestBytes, verifyPublicationRelease } from "./publication-artifacts.mjs";
33
- import { assertPrimaryReleaseCheckout } from "./release-invocation.mjs";
34
- import { assertCleanSource } from "./source-gate.mjs";
32
+ import { assertBindsSource, assertInsideSource, assertReleaseSource, gitRootOf, isSnapshotSource, resolveReleaseSource } from "./release-source-identity.mjs";
33
+ import { verifyOwnershipEvidence } from "./ownership-evidence.mjs";
35
34
 
36
35
  export const PLAY_PROVIDER = "google-play";
37
36
  export const PLAY_TRACK = "internal";
@@ -183,26 +182,28 @@ export async function uploadToPlay({ distribution, aabBytes, accessToken, fetchI
183
182
  }
184
183
 
185
184
  // ---- authorisation and the command --------------------------------------------------------------------------------
186
- const git = (cwd, args) => {
187
- const result = spawnSync("git", args, { cwd, encoding: "utf8", windowsHide: true });
188
- if (result.status !== 0) throw new Error(`git ${args.join(" ")} failed: ${result.stderr}`);
189
- return result.stdout.trim();
190
- };
191
185
 
192
186
  // Local checks only; no network, no secret. The sealed release is the only input: build-free by construction.
193
- export async function authorizePlayUpload({ releaseId, cwd = process.cwd(), configName = defaultReleaseConfigFile(cwd) }) {
187
+ export async function authorizePlayUpload({ releaseId, cwd = process.cwd(), configName = defaultReleaseConfigFile(cwd), env = process.env }) {
194
188
  if (!RELEASE_ID.test(releaseId ?? "")) refuse("PLAY_RELEASE_REQUIRED", "--release <sealed-id> is required");
195
- const repoRoot = git(cwd, ["rev-parse", "--show-toplevel"]);
196
- assertPrimaryReleaseCheckout(repoRoot);
197
- const commit = git(repoRoot, ["rev-parse", "HEAD"]);
198
- assertCleanSource({ status: git(repoRoot, ["status", "--porcelain=v1", "-z", "--untracked-files=all"]), commandId: "right-release upload" });
189
+ // Source identity (release-source-identity.mjs, item 7 row 7.04): the checkout's clean HEAD outside a door run, the selected commit and
190
+ // its verified snapshot inside one (HEAD, status and branch of the checkout are then never read).
191
+ const release = resolveReleaseSource({ cwd, env });
192
+ assertReleaseSource(release, { commandId: "right-release upload", env });
193
+ const split = isSnapshotSource(release);
194
+ const repoRoot = gitRootOf(release);
195
+ const commit = release.commit;
199
196
  const configPath = path.resolve(cwd, configName);
197
+ assertInsideSource(release, configPath, "the release config");
200
198
  if (!existsSync(configPath)) refuse("PLAY_CONFIG_INVALID", `release config is required: ${configName}`);
201
199
  // A legacy config is a module that runs on import (a JSON config is data): loaded only after the tree is proven clean.
202
200
  const config = await importReleaseConfig(configPath, { query: `upload=${Date.now()}` });
203
201
  const distribution = validatePlayDistribution(targetDistribution(config, "android"));
204
202
  const sealedDir = path.join(repoRoot, ".right-release", "sealed", releaseId, "android");
205
- const sealed = verifyPublicationRelease({ sealedDir, repoRoot, appRoot: path.dirname(configPath), commit, app: config.app, version: config.version, platform: "android", configPath, releaseId });
203
+ // The sealed release and its signing receipts are where the build wrote them (the checkout's `.right-release`).
204
+ const sealed = verifyPublicationRelease({ sealedDir, repoRoot, appRoot: split ? release.checkoutAppRoot : path.dirname(configPath), commit, app: config.app, version: config.version, platform: "android", configPath, releaseId },
205
+ split ? { verifyOwnership: (expected, root, scanRoot, scope) => verifyOwnershipEvidence(expected, root, scanRoot, scope, release) } : {});
206
+ if (split) assertBindsSource(release, sealed.manifest, "the sealed release");
206
207
  const entries = sealed.manifest.files.filter((entry) => entry.role === "aab");
207
208
  if (entries.length !== 1) refuse("PLAY_AAB_MISSING", "the sealed release must hold exactly one aab entry");
208
209
  const aab = sealed.artifacts.get(entries[0].name);
@@ -226,7 +227,7 @@ export async function runPlayUpload({
226
227
  fetchImpl = fetch, credentialStore, sign, nowSeconds, authorize = authorizePlayUpload,
227
228
  }) {
228
229
  refusePlayEnvSecrets(env);
229
- const auth = await authorize({ releaseId, configName, cwd });
230
+ const auth = await authorize({ releaseId, configName, cwd, env });
230
231
  const aabEvidence = { name: auth.entry.name, sha256: auth.entry.sha256, sizeBytes: auth.entry.sizeBytes };
231
232
  const plan = playUploadPlan({ distribution: auth.distribution, aab: aabEvidence });
232
233
  const stateRoot = path.join(auth.repoRoot, ".right-release", "state", releaseId, "android");
package/app-check.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  // The app check: step 1 of docs/PIPELINE-ARCHITECTURE.md for any repository that uses RightKit, shipped in @rightkit/release so an
2
- // app runs it without a RightKit checkout (`rightkit-gate check`, `right-release check`, the pre-push hook, and the start of every
2
+ // app runs it without a RightKit checkout (`rightkit-gate check`, `right-release check`, and the start of every
3
3
  // right-release command that builds or ships; see CHECKED_COMMANDS). RightKit's own scripts/preflight.mjs imports these rules and
4
4
  // adds the ones that police RightKit itself.
5
5
  //
@@ -1131,7 +1131,7 @@ export const RULES = Object.freeze([
1131
1131
  // asked of them. The compile tier is the rule set every app was green on when it last built through the gate: the rules @rightkit/release
1132
1132
  // 0.6.6 published. Every rule added since (consumer, shell, Legion, ownership and source-closure contracts) is release-tier until the
1133
1133
  // synthesis waves make it true in every app, and is promoted here, in RightKit, never per app and never through a baseline.
1134
- // compile gates compile admission: the RustRight broker, the pre-push hook, rightkit-gate check, --to compile|package, right-release build|dev|test.
1134
+ // compile gates compile admission: the RustRight broker, rightkit-gate check, --to compile|package, right-release build|dev|test.
1135
1135
  // release the whole set: --to qualify|release and every right-release command that qualifies, signs, ships or uploads.
1136
1136
  export const TIERS = Object.freeze(["compile", "release"]);
1137
1137
  export const COMPILE_TIER_IDS = Object.freeze(new Set([
@@ -1513,7 +1513,7 @@ export const UNCHECKED_COMMANDS = new Map([
1513
1513
  ["identity-scan", "asserts the signature of a path named on the command line"],
1514
1514
  ["apple-profile-create", "provisioning-profile credentials; no app source"],
1515
1515
  ["ios-credentials", "stores or checks App Store Connect credentials; no app source"],
1516
- ["hooks", "installs the git hooks, and the pre-push hook runs the check"],
1516
+ ["hooks", "retires the git hooks an earlier release wrote (install-hook); writes none"],
1517
1517
  ["suite-doctor", "verifies the Right Suite contract tests; a report"],
1518
1518
  ["cache", "host build-cache maintenance"],
1519
1519
  ["disk", "host disk maintenance"],
@@ -1578,14 +1578,18 @@ export const appDirectory = (args, cwd) => {
1578
1578
  * check is green (nothing is printed then), else { exitCode: 1, lines } for the caller to print: the findings and why nothing
1579
1579
  * was started. A check that cannot run is red too.
1580
1580
  */
1581
- export function guardCommand({ args, cwd = process.cwd(), run = runAppCheck } = {}) {
1582
- const mode = checkModeFor(args);
1583
- if (!mode) return null;
1581
+ export function guardCommand({ args, cwd = process.cwd(), run = runAppCheck, source = null } = {}) {
1582
+ const checkMode = checkModeFor(args);
1583
+ if (!checkMode) return null;
1584
1584
  const name = args[0].startsWith("-") ? "build" : checkedCommandOf(args) ?? args[0];
1585
- const dir = appDirectory(args, cwd);
1586
- const repo = repoRoot(dir);
1585
+ // A source split (source-split.mjs): the process runs in the door's snapshot of `source.commit`, which has no git directory. The
1586
+ // rules read that commit from the checkout `source.repo`, whatever the command's usual mode (the snapshot IS the commit, so a
1587
+ // "worktree" command checks it as a commit; the checkout's own working tree is never what is built).
1588
+ const mode = source ? "commit" : checkMode;
1589
+ const dir = source ? source.repo : appDirectory(args, cwd);
1590
+ const repo = source ? source.repo : repoRoot(dir);
1587
1591
  if (!repo) return { exitCode: 1, lines: [`right-release ${name}: the app check needs a git repository and ${dir} is not inside one; nothing was started`] };
1588
- const result = run({ repo, mode, tier: tierForCommand(name, args), label: "check", refuse: BUILD_REFUSES });
1592
+ const result = run({ repo, mode, ...(source ? { commit: source.commit } : {}), tier: tierForCommand(name, args), label: "check", refuse: BUILD_REFUSES });
1589
1593
  if (result.exitCode === 0) return null;
1590
1594
  const why = result.exitCode === 2 ? "could not run" : "found problems";
1591
1595
  return { exitCode: 1, lines: [...result.lines, ...result.errLines, `right-release ${name}: the app check ${why} in ${repo}; nothing was started. Fix the above and commit, then run it again.`] };