@rightkit/release 0.6.3 → 0.6.4
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 +37 -5
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/rightkit-gate.mjs +4 -4
- package/cli/right-release.mjs +3 -2
- package/deterministic-scans.mjs +21 -10
- package/development.mjs +3 -2
- package/hosted-workflow-policy.mjs +111 -0
- package/ios-dev.mjs +2 -2
- package/ios-host.mjs +7 -7
- package/ios-package-recipe.mjs +2 -2
- package/local-publish.mjs +1 -1
- package/local-qualify.mjs +3 -3
- package/local-runtime.mjs +1 -1
- package/package.json +1 -1
- package/pipeline-admission.mjs +5 -4
- package/pipeline-receipts.mjs +302 -302
- package/pipeline-scan.mjs +3 -3
- package/pipeline-tools.mjs +175 -171
- package/prebuild-gates.mjs +187 -0
- package/progress-control.mjs +1 -1
- package/publish-update.mjs +4 -3
- package/qa-admission-policy.json +5 -0
- package/release-config.mjs +353 -330
- package/release-qa-audit.mjs +37 -11
- package/release-qa-command.mjs +46 -5
- package/release-qa-fixtures.mjs +127 -0
- package/release-qa-floor.json +6 -1
- package/release-qa-hardening.mjs +173 -0
- package/release-qa-policy.mjs +168 -0
- package/release-qa-receipt.mjs +21 -1
- package/release-switch-scan.mjs +137 -6
- package/release.mjs +244 -197
- package/rightapps-register.mjs +3 -1
- package/rightkit-drift.mjs +1 -1
- package/rightkit-ownership.json +15 -5
- package/rightkit-versions.json +43 -32
- package/stage-sidecar.mjs +17 -15
- package/suite-api-literal.mjs +109 -0
- package/suite-api.generated.mjs +78 -0
- package/swap-helper-bundle.mjs +2 -2
- package/win-package-recipe.mjs +7 -4
package/README.md
CHANGED
|
@@ -59,6 +59,18 @@ Every Right app has absolutely NO access to DevTools; the single exception is Vi
|
|
|
59
59
|
- S1-S4 need `compiledOut`: `feature:<name>`, `cfg:test`, `cfg:debug_assertions` or, for Swift, `swift:DEBUG` (the read sits inside `#if DEBUG`; the read exists only there). The source walk skips test directories, including Swift bundles named `Tests`, `UITests`, `<App>Tests` and `<App>UITests`. S5 needs `userGesture: true`. S6 needs `valueType` of `bool`, `int` or `enum`.
|
|
60
60
|
- `anchor` and `note` are informational. Fixtures: `fixtures/release-switches/` (`app/` is a clean example).
|
|
61
61
|
|
|
62
|
+
#### Non-shipped tooling: `releaseSwitches.nonShipped`
|
|
63
|
+
|
|
64
|
+
On desktop (non-iOS) targets the source walk covers the whole app root, so app tooling that never ships (a bakeoff harness) can produce `unregistered-read` findings that cannot truthfully be declared in `switches.json`. Declare such directories in the release config:
|
|
65
|
+
|
|
66
|
+
```json
|
|
67
|
+
"releaseSwitches": { "nonShipped": [ { "dir": "bakeoff", "reason": "bakeoff harness; not compiled or bundled" } ] }
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
- It is an exclusion list: any directory not listed is still scanned, so an omission fails safe. `dir` is relative to the app root; `reason` is required and non-empty. iOS is unchanged (it already walks only the Xcode project and the local crates in its cargo tree).
|
|
71
|
+
- Every entry is validated before the walk and a bad one fails the release with `inventory-invalid`. Refused: a dir that is missing, not a real directory (a symlink), absolute, escaping the root, or the root itself; an empty `reason`; a dir that contains or sits inside a local crate of the resolved release cargo tree (a crate at the app root protects `src/`), `src-tauri`, or the Tauri `frontendDist` bundle; a dir that contains a `package.json` or sits inside a directory below the root that has one; when the root has a `package.json`, `src/`, `public/` and every existing directory named by the root `index.html` (`src`/`href`), a quoted string in `vite.config.*` (root, publicDir, build input) or `include`/`files` of `tsconfig.json`, plus anything inside or containing them; and any dir holding a path named by `src-tauri/tauri.conf.json` `beforeBuildCommand`/`beforeDevCommand`/`beforeBundleCommand` a root `package.json` `build*` script, or the release recipe's own commands (`targets.<os>.prePackage`, `package`, `postSign`, `packageHook`, the finalizer; followed through `pnpm|npm|yarn run <script>`), because those run during the release build.
|
|
72
|
+
- Not protected: the release code does not otherwise know the frontend source layout. A frontend input that none of the rules above names (for example one referenced only from a nested config, or built by a script not reachable from those commands) is not refused, so list only directories you know never ship.
|
|
73
|
+
|
|
62
74
|
## Commands
|
|
63
75
|
|
|
64
76
|
`right-release` maps these subcommands to the modules in this package:
|
|
@@ -109,7 +121,7 @@ a JSON line before and after (`{"event":"e2e_suite_completed","suite":"tooling",
|
|
|
109
121
|
(repeatable) selects suites; a group name such as `journeys` selects all its chunks. `--runner vitest -- run --project unit`
|
|
110
122
|
runs a runner once with the arguments after `--` (watch mode, one project); `-- <args>` after `--suite` is allowed when exactly
|
|
111
123
|
one suite is selected. `--list [--json]` prints the resolved commands and runs nothing. Wrap it as the repository needs, for
|
|
112
|
-
example `
|
|
124
|
+
example `rustright storage run --app <app> -- right-release test`.
|
|
113
125
|
|
|
114
126
|
### `right-release build --web` (or `web-build`)
|
|
115
127
|
|
|
@@ -187,9 +199,27 @@ Only a target directory under a managed root (`resolveManagedRoots`: `RIGHTKIT_D
|
|
|
187
199
|
`postSign`: it packs the signed, stapled `.app` and signs the tarball. The default output is `<App>.app.tar.gz`, named after the
|
|
188
200
|
app in `notarize.app`. Declare the file in `updater.artifacts` as before. See `docs/MAC-PACKAGE-RECIPE.md`.
|
|
189
201
|
|
|
190
|
-
## QA receipts (warn
|
|
202
|
+
## QA receipts and admission (warn by default, refuse per app)
|
|
203
|
+
|
|
204
|
+
`right-release qa --stage source|installed --platform mac|win` runs `rightkit-qa` and writes a MAC'd receipt. Admission is a per-app policy in `qa-admission-policy.json`:
|
|
191
205
|
|
|
192
|
-
|
|
206
|
+
```json
|
|
207
|
+
{ "schema": 1,
|
|
208
|
+
"apps": { "postright": { "mode": "refuse", "since": "2026-11-02", "set_by": "adrian" } },
|
|
209
|
+
"approvedRunners": [ { "sha256": "<64 hex>", "version": "0.8.0" } ] }
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- **`warn` (default; an app with no entry).** A missing or invalid receipt does not block. `build`, `upload`, `publish-update` and `github` print `[right-release] QA warning: ...` to stderr, continue, and record the outcome (below).
|
|
213
|
+
- **`refuse`.** The same refusal throws `QA admission refused for <app> (policy refuse since ... by ...)`; nothing is sealed, uploaded or published. Switch an app on with `node release-qa-policy.mjs set --app <app> --mode refuse --set-by <name>`. Moving an app back to `warn` (or deleting its entry) is refused unless `set_by` is `adrian`; a hand edit that does so is caught on every load by comparing with the copy committed at `RIGHT_RELEASE_QA_POLICY_BASELINE_REF` (default `HEAD`; CI should use `origin/main`). `set_by` is a recorded claim, not an authenticated identity: the file must be owner-protected in the repository (CODEOWNERS and branch protection), and a downgrade that was already committed becomes the new baseline. An unreadable or malformed policy file is a hard error, never a fall back to warn.
|
|
214
|
+
|
|
215
|
+
Hardening, applied in both modes (a failure is a warning in `warn` and a refusal in `refuse`):
|
|
216
|
+
|
|
217
|
+
1. **Platform and release id.** The sealed receipt must be for the platform and the release id being sealed or published.
|
|
218
|
+
2. **Approved runner.** The `rightkit-qa` executable's sha256 recorded in the receipt must be listed in `approvedRunners`. An empty list approves nothing. The binary is digested before the run and again after it; a swap during the run is refused.
|
|
219
|
+
3. **Every release-critical journey.** With `qa.journeys` in the release config (`{ manifest?: "journeys.toml", receipt?: "<path>", args?: { source?, installed? } }`) `right-release qa` runs `rightkit-qa journeys run` (or reads `receipt`), validates the `rightkit.journeys.receipt/1` through `rightkit-qa journeys validate-receipt --manifest .. --tree ..` (exit code 0 and a final `ACCEPTED` line), and seals a `journeys` summary. `release-qa-floor.json` maps the `journey` floor id to "all release-critical journeys passed": at admission the required set is recomputed from the `journeys.toml` committed at the receipt's commit (every `release_critical` journey that lists the platform), and each must be `passed`. Zero critical journeys is never a pass. One matching check no longer satisfies the floor.
|
|
220
|
+
4. **Signing outside test execution.** Checked: the receipt records the test processes' pids and the window they ran in; admission refuses a receipt sealed before that window ended, signing that began before it ended (`signingStartedAt`), a sealing or admitting process that is a test process, `RIGHT_RELEASE_QA_STAGE` set at admission (admission or signing inside a QA run), and `right-release qa` itself refuses to seal inside a QA run. Not checkable here: that test processes cannot reach the signing key (OS credential store and signing agent ACL) and that CI runs QA and signing as separate jobs (the carried receipt, rightkit#183, is the hook).
|
|
221
|
+
|
|
222
|
+
`right-release qa` keeps the older behavior for evidence checks and verification of the receipt itself; the outcome is recorded:
|
|
193
223
|
|
|
194
224
|
- Source stage: sealed into the manifest as `qa: { status: "passed", sourceReceiptDigest, floorDigest }` or `qa: { status: "missing" | "rejected", reason }`.
|
|
195
225
|
- Installed stage: written to `.right-release/state/<releaseId>/<platformDir>/qa-installed.json` as `{ status, reason?, receiptDigest?, checkedAt, provider }`. It is written when publication starts, so a later upload failure still leaves a record.
|
|
@@ -212,7 +242,7 @@ right-release install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id d
|
|
|
212
242
|
|
|
213
243
|
It mounts the image read-only and hidden (`-nobrowse`), verifies the app (`codesign --verify --deep --strict`, `spctl --assess`, `stapler validate`, bundle id, and Team ID against the installed copy; a Team ID change is refused) before touching the installed copy, stages a `ditto` copy beside it in the destination and verifies that copy, quits the running instance with SIGTERM (15 s, then SIGKILL), swaps the staged copy in (one atomic exchange when the `rightkit-swap` helper is found, otherwise two journaled renames; the old bundle is restored on failure), re-verifies the installed copy, only then moves the old bundle to `~/.Trash/<App> <timestamp>.app`, and relaunches only if it was running or `--relaunch` is given, using `open -g -j -b` (background, hidden, never activated). `--json` emits one lifecycle event per line (`start`, `verified`, `quit`, `replaced`, `relaunched`, `done`/`failed`, among others). `right-release install-app --recover [--dest <dir>] [--app-name <App.app>] [--json]` rolls an interrupted install forward or back from its journal; every install runs that recovery first. Win32 exits with "not implemented on win32". Exit codes: 0 ok, 1 failed, 2 usage, 3 unsupported platform.
|
|
214
244
|
|
|
215
|
-
The shipped `bin/darwin/rightkit-swap` is Developer ID signed. Local publish adds it via `right-release publish npm --package @rightkit/release --local` on the Mac (managed `
|
|
245
|
+
The shipped `bin/darwin/rightkit-swap` is Developer ID signed. Local publish adds it via `right-release publish npm --package @rightkit/release --local` on the Mac (managed `rustright cargo build --release -p rightkit-fs --bin rightkit-swap`, `codesign --options runtime --timestamp`, strict verify). CI qualification adds it via `publish.yml` build-swap → sign-swap → pack (`docs/CI.md`); sign-swap needs `APPLE_CERTIFICATE_BASE64`, `APPLE_CERTIFICATE_PASSWORD`, `APPLE_KEYCHAIN_PASSWORD` & `APPLE_DEVELOPER_ID` in the `release` environment, & stays blocked until root provisions the certificate pair. Exporting that identity requires an Aqua login session: SSH export fails `errSecInteractionNotAllowed` (-25308) & the Aqua signing agent exposes only codesign/updater/notarize/staple, no key export — do not retry either route. `install-app` refuses a helper that exists but is not validly signed (`SWAP_HELPER_UNSIGNED`, naming the fix); with no helper at all it uses the journaled two-rename fallback and logs it (`swap-fallback`, `replaced {swap}`).
|
|
216
246
|
|
|
217
247
|
Development builds (from `@rightkit/ci` `devArtifact`, Developer ID-signed but not notarized) install with `--dev` and exactly one of `--dmg`, `--zip <path.zip>` (extracted with `ditto -x -k`), or `--app <path.app>`: `right-release install-app --dev --zip dev-mac.zip`. `--dev` still requires `codesign --verify --deep --strict`, a Team ID, no Team ID change, and a bundle-id match; it skips only `spctl` and `stapler`, removes `com.apple.quarantine` from the new bundle only, prints `DEV BUILD (not notarized)`, and sets `dev: true` on every JSON event.
|
|
218
248
|
|
|
@@ -384,7 +414,7 @@ Updater artifacts are declared as for R2 but need no `key`; `platform` must be a
|
|
|
384
414
|
```js
|
|
385
415
|
targets: {
|
|
386
416
|
win: {
|
|
387
|
-
updater: {
|
|
417
|
+
updater: { artifacts: [
|
|
388
418
|
{ file: "src-tauri/target/release/bundle/nsis/Ember_1.2.3_x64-setup.exe", signature: "src-tauri/target/release/bundle/nsis/Ember_1.2.3_x64-setup.exe.sig", platform: "windows-x86_64" },
|
|
389
419
|
] },
|
|
390
420
|
artifacts: ["dist/Ember-1.2.3-portable-win.zip"], // portable zips seal alongside; they are not in latest.json
|
|
@@ -395,6 +425,8 @@ targets: {
|
|
|
395
425
|
}
|
|
396
426
|
```
|
|
397
427
|
|
|
428
|
+
Updater signing keys are never named in the config: `sign-updater` reads the minisign key from the OS credential provider (`rightkit-credentials`, namespace `rightsuite-updater-key`; see `protected-credentials.mjs`). `targets.<platform>.updater.keyFile` is refused at config validation (`RELEASE_CONFIG_UPDATER_KEY_FILE`, listed by `right-release doctor` and the build with every other finding) and is never forwarded to the signer, so remove it from apps that still set it.
|
|
429
|
+
|
|
398
430
|
NSIS, MSI, `.app.tar.gz` and AppImage bundles all work the same way (any file signed by `sign-updater`). With no sealed installer the installer platform-trust check is skipped (reported in the output); installer-bearing seals still run it. The app side: `plugins.updater.endpoints` is the `latest.json` URL above and `plugins.updater.pubkey` is the suite updater public key (see `@rightkit/app-update` README).
|
|
399
431
|
|
|
400
432
|
## Android (AAB build, Play internal track, dev install)
|
package/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|
package/bin/rightkit-gate.mjs
CHANGED
|
@@ -750,13 +750,13 @@ export function quietLeaseLane(o, { tag = "rightkit-gate:", reason = "rightkit-g
|
|
|
750
750
|
|
|
751
751
|
// ---- command resolution -------------------------------------------------------------------------------------
|
|
752
752
|
|
|
753
|
-
// Cargo only through the broker: `
|
|
753
|
+
// Cargo only through the broker: `rustright cargo`, or a `cargo` on PATH that is the managed shim (it runs cargo-guard.mjs).
|
|
754
754
|
function resolveCargoCommand(o) {
|
|
755
755
|
const which = (cmd) => {
|
|
756
756
|
const r = o.spawn(o.platform === "win32" ? "where" : "which", [cmd]);
|
|
757
757
|
return failed(r) ? [] : text(r).split(/\r?\n/).map((l) => l.trim()).filter(Boolean);
|
|
758
758
|
};
|
|
759
|
-
if (which("
|
|
759
|
+
if (which("rustright").length) return ["rustright", "cargo"];
|
|
760
760
|
const first = which("cargo")[0];
|
|
761
761
|
if (first) { try { if (o.readFile(first).includes("cargo-guard.mjs")) return ["cargo"]; } catch { /* unreadable: not the shim */ } }
|
|
762
762
|
return null;
|
|
@@ -900,12 +900,12 @@ export async function main(argv, io = {}) {
|
|
|
900
900
|
const readLive = (f) => o.readFile(f);
|
|
901
901
|
|
|
902
902
|
// Units come from the live checkout (cargo reads it); a dirty checkout then blocks below.
|
|
903
|
-
let rust; let npm; let cargoCommand = ["
|
|
903
|
+
let rust; let npm; let cargoCommand = ["rustright", "cargo"];
|
|
904
904
|
try {
|
|
905
905
|
const manifests = tracked("Cargo.toml");
|
|
906
906
|
if (manifests.length) {
|
|
907
907
|
cargoCommand = resolveCargoCommand(o);
|
|
908
|
-
if (!cargoCommand) { o.err(`${tag} BLOCKED: neither \`
|
|
908
|
+
if (!cargoCommand) { o.err(`${tag} BLOCKED: neither \`rustright\` nor the managed cargo shim (cargo-guard.mjs) is first on PATH; plain cargo is never used`); return 1; }
|
|
909
909
|
if (!o.exists(path.join(root, GRANT_FILE))) { o.err(`${tag} BLOCKED: no local compile grant (${GRANT_FILE}); the broker refuses cargo, so the Rust units cannot be discovered or scanned`); return 1; }
|
|
910
910
|
}
|
|
911
911
|
// Untracked (not ignored) manifests only: git does not descend into ignored directories, so this stays cheap. A member of a
|
package/cli/right-release.mjs
CHANGED
|
@@ -270,7 +270,8 @@ Commands:
|
|
|
270
270
|
upload --platform ios --release <id> [--apply] Sealed IPA to TestFlight, then the exact build into the beta groups; dry run unless --apply (macOS)
|
|
271
271
|
ios-credentials store --key-id <KEYID> --issuer <uuid> | check
|
|
272
272
|
Put the App Store Connect API key (stdin), key id and issuer into the login Keychain once, or check them; never a file (macOS)
|
|
273
|
-
qa --stage source|installed --platform mac|win [--release <id>] Run rightkit-qa and write the sealed QA receipt
|
|
273
|
+
qa --stage source|installed --platform mac|win [--release <id>] Run rightkit-qa (and the journeys lane when the config sets qa.journeys) and write the sealed QA receipt. Refused inside a QA run. The receipt must match the platform and release id, come from an approved runner digest, and show every release-critical journey passed
|
|
274
|
+
Admission is per app in qa-admission-policy.json: warn (default) records a refusal and continues; refuse blocks build, upload and publish. Only adrian moves an app from refuse back to warn
|
|
274
275
|
qa audit [--app <dir>] [--json] [--since <YYYY-MM-DD>] List source/installed QA status per release; exit 1 if an uploaded build lacks passing QA
|
|
275
276
|
publish-update --release <id> --platform mac|win --tier patch|update
|
|
276
277
|
Upload sealed bytes to R2 & register RightApps
|
|
@@ -307,7 +308,7 @@ Commands:
|
|
|
307
308
|
apple profile renew [--config <file>] [--profile <file>] [--dry-run]
|
|
308
309
|
Renew macOS profile for current Keychain signer
|
|
309
310
|
signing-agent install|uninstall|status Manage invisible Aqua-session signing service
|
|
310
|
-
doctor [--platform mac|win]
|
|
311
|
+
doctor [--platform mac|win] [--gates] Inspect one app's release config; --gates also runs the build's pre-build source gates
|
|
311
312
|
doctor --all Verify all Right Suite app release contracts
|
|
312
313
|
suite-doctor Verify all local Right Suite repositories
|
|
313
314
|
ownership [--app <dir>] [--json] Fail on app copies of RightKit-owned capabilities
|
package/deterministic-scans.mjs
CHANGED
|
@@ -10,10 +10,10 @@
|
|
|
10
10
|
// repository has none. A deferred scan is listed by name (result.deferred, scanDeferrals), is never a
|
|
11
11
|
// pass (result.ok and result.complete are false while any scan is deferred) and cannot become a SCAN_B receipt.
|
|
12
12
|
// Content scans run on the exported HEAD (contentRoot). Only a scan that inherently needs the live
|
|
13
|
-
// checkout uses liveRoot: `
|
|
13
|
+
// checkout uses liveRoot: `rustright cargo` scans (the broker binds to the registered checkout), the
|
|
14
14
|
// tsc, eslint and package tests (need installed node_modules).
|
|
15
15
|
// Class B runs only when every selected class A scan passed (SCAN_B is distinct from SCAN_A and
|
|
16
|
-
// follows it). Rust commands go through `
|
|
16
|
+
// follows it). Rust commands go through `rustright cargo`, JavaScript tools through `pnpm exec`.
|
|
17
17
|
// The runner takes injectable spawn/exists/readFile/readdir so tests execute nothing real. The
|
|
18
18
|
// result is a plain object (per-scan status, tool versions, scanSetDigest) usable as receipt content
|
|
19
19
|
// (packages/release/pipeline-receipts.mjs); it carries no authority by itself.
|
|
@@ -34,7 +34,7 @@ export const SCAN_CLASS = Object.freeze({ A: "A", B: "B" });
|
|
|
34
34
|
export const CLASS_A_SCAN_PREFIXES = Object.freeze([
|
|
35
35
|
"rustfmt", "node-check", "json-parse", "toml-parse", "yaml-parse", "ownership-catalog",
|
|
36
36
|
"actionlint", "pnpm-lockfile", "pnpm-audit", "cargo-metadata", "cargo-deny", "cargo-audit", "cargo-package-list",
|
|
37
|
-
"changelog-version", "license", "secret-scan", "path-policy", "advisory-exceptions",
|
|
37
|
+
"changelog-version", "license", "secret-scan", "path-policy", "advisory-exceptions", "suite-api",
|
|
38
38
|
]);
|
|
39
39
|
export const CLASS_B_SCAN_PREFIXES = Object.freeze([
|
|
40
40
|
"clippy", "check", "check-all-features", "check-features", "check-dependent", "doc", "tsc", "eslint", "swift-build", "test", "generated-docs",
|
|
@@ -206,9 +206,11 @@ const ESLINT_FILES = ["eslint.config.js", "eslint.config.mjs", ".eslintrc.json"]
|
|
|
206
206
|
const ROOT_LINT_FILES = [...ESLINT_FILES, "biome.json"];
|
|
207
207
|
// Everything scripts/generate-index.mjs reads or writes (its header and loadInventory): crate and package manifests and
|
|
208
208
|
// READMEs, the root Cargo.toml (workspace version), the versions and ownership data, the ownership-availability helper it imports, itself, and its outputs.
|
|
209
|
+
// The Right Suite API contract and every projection scripts/gen-suite-api.mjs writes from it (the test-vector copy included).
|
|
210
|
+
const SUITE_API_INPUT = /^(contracts\/right-suite-api\.v1\.json|scripts\/gen-suite-api\.mjs|crates\/rightkit-license\/src\/suite_api_generated\.rs|packages\/app-update\/src\/suite-api\.generated\.ts|packages\/release\/suite-api\.generated\.mjs|crates\/rightkit-license\/test-vectors\/right-suite-api\.v1\.json)$/;
|
|
209
211
|
const GENERATOR_INPUT = /^(crates\/[^/]+\/(Cargo\.toml|README\.md)|packages\/[^/]+\/(package\.json|README\.md)|Cargo\.toml|packages\/release\/(rightkit-versions\.json|rightkit-ownership\.json|ownership-availability\.mjs)|scripts\/generate-index\.mjs|INDEX\.md|docs\/(ADOPTION|CAPABILITY-MAP)\.md)$/;
|
|
210
212
|
const PROBES = Object.freeze({
|
|
211
|
-
rustfmt: ["--version"], node: ["--version"],
|
|
213
|
+
rustfmt: ["--version"], node: ["--version"], rustright: ["cargo", "--version"], pnpm: ["--version"],
|
|
212
214
|
actionlint: ["-version"], "right-git": ["--version"], swift: ["--version"],
|
|
213
215
|
});
|
|
214
216
|
|
|
@@ -221,7 +223,7 @@ function commandText(d) {
|
|
|
221
223
|
/**
|
|
222
224
|
* The scan plan for a change: derived from the changed files plus the manifests they belong to.
|
|
223
225
|
* `contentRoot` is a clean export of the commit (pure content checks); `liveRoot` is the checkout the
|
|
224
|
-
* managed `
|
|
226
|
+
* managed `rustright cargo` and `pnpm` run in. A scan whose files did not change is not planned.
|
|
225
227
|
*/
|
|
226
228
|
export function planScans({
|
|
227
229
|
changed = [], contentRoot, liveRoot = contentRoot, exists = fs.existsSync, readFile = (f) => fs.readFileSync(f, "utf8"),
|
|
@@ -230,8 +232,8 @@ export function planScans({
|
|
|
230
232
|
// CI shards (one crate per runner, where the port, pipe, desktop and wall-clock conflicts that force the serial lane do not exist)
|
|
231
233
|
// and records a note naming them (Adrian 2026-10-22: compile locally on the warm cache, qualify the conflicting tests on VMs).
|
|
232
234
|
quietTests = "local",
|
|
233
|
-
// The command prefix every cargo scan runs through: `
|
|
234
|
-
cargoCommand = ["
|
|
235
|
+
// The command prefix every cargo scan runs through: `rustright cargo` on a managed host, plain `cargo` in CI.
|
|
236
|
+
cargoCommand = ["rustright", "cargo"],
|
|
235
237
|
// Physical containment: a changed path whose real path leaves the real content root (a symlink) is refused.
|
|
236
238
|
realpath = (p) => fs.realpathSync(p),
|
|
237
239
|
// Bytes of a changed file for the input digest (defaults to readFile; the gates pass the raw reader).
|
|
@@ -260,8 +262,8 @@ export function planScans({
|
|
|
260
262
|
rustLocks = null,
|
|
261
263
|
} = {}) {
|
|
262
264
|
if (!contentRoot) throw new TypeError("planScans: contentRoot is required");
|
|
263
|
-
if (!Array.isArray(cargoCommand) || cargoCommand.length === 0 || !["
|
|
264
|
-
throw new TypeError('planScans: cargoCommand must be ["
|
|
265
|
+
if (!Array.isArray(cargoCommand) || cargoCommand.length === 0 || !["rustright", "cargo"].includes(cargoCommand[0]) || cargoCommand.slice(1).some((a) => a !== "cargo")) {
|
|
266
|
+
throw new TypeError('planScans: cargoCommand must be ["rustright", "cargo"] or ["cargo"]');
|
|
265
267
|
}
|
|
266
268
|
const refusals = []; const accepted = [];
|
|
267
269
|
for (const raw of new Set(changed)) {
|
|
@@ -434,6 +436,9 @@ export function planScans({
|
|
|
434
436
|
|
|
435
437
|
// Exceptions expire by date, not by change: checked on every run, whatever changed.
|
|
436
438
|
if (exists(path.join(contentRoot, "ci/advisory-exceptions.json"))) add({ id: "advisory-exceptions", class: A, internal: "advisory-exceptions" });
|
|
439
|
+
// Like generated-docs, planned only when a changed file is one of the contract's inputs or projections. The generator runs from
|
|
440
|
+
// the trusted scanner checkout (the import below); the exported contract and projections are data it checks.
|
|
441
|
+
if (files.some((f) => SUITE_API_INPUT.test(f)) && exists(path.join(contentRoot, "contracts/right-suite-api.v1.json"))) add({ id: "suite-api", class: A, internal: "suite-api" });
|
|
437
442
|
if (rustTouched) {
|
|
438
443
|
for (const ws of touchedWs) add({ id: `cargo-metadata${wsIdSuffix(ws)}`, class: A, ...cargoIn(ws, ...metadataArgs()) });
|
|
439
444
|
for (const name of crateNames.filter((n) => rustMap?.get(n).publish !== false)) add({ id: rustScanId.packageList(name), class: A, ...cargoIn(wsOf(name), "package", "-p", crateOf(name), "--list", "--allow-dirty", "--locked") });
|
|
@@ -578,7 +583,7 @@ export function planScans({
|
|
|
578
583
|
const deferred = [];
|
|
579
584
|
if (!localCompile) {
|
|
580
585
|
scans = scans.map((x) => {
|
|
581
|
-
if (x.tool !== "
|
|
586
|
+
if (x.tool !== "rustright" && !(x.tool === "cargo" && cargoCommand.length === 1)) return x;
|
|
582
587
|
deferred.push(x.id);
|
|
583
588
|
return { ...x, deferred: "no local compile grant; the broker refuses this scan" };
|
|
584
589
|
});
|
|
@@ -758,6 +763,12 @@ const INTERNAL = {
|
|
|
758
763
|
// Every RustSec exception is dated, scoped to the locked versions and identical in deny.toml and
|
|
759
764
|
// .cargo/audit.toml (ci/advisory-exceptions.json; Astra 2026-10-22). Expiry fails every run.
|
|
760
765
|
"advisory-exceptions": (s, ctx) => checkAdvisoryExceptions({ root: ctx.contentRoot, readFile: ctx.read }),
|
|
766
|
+
// Every Right Suite API projection must equal what the generator writes from the contract. The pure check is imported from the
|
|
767
|
+
// scanner's own checkout (scripts/ sits beside packages/release), never from the content root.
|
|
768
|
+
"suite-api": async (s, ctx) => {
|
|
769
|
+
const { suiteApiProblems } = await import("../../scripts/gen-suite-api.mjs");
|
|
770
|
+
return suiteApiProblems((f) => ctx.read(f));
|
|
771
|
+
},
|
|
761
772
|
"ownership-catalog": (s, ctx) => (ctx.read(s.files[0]) === ctx.read(s.files[1]) ? [] : ["copy packages/release/rightkit-ownership.json to crates/rightkit-ownership/catalog/ownership-catalog.json"]),
|
|
762
773
|
"changelog-version": (s, ctx) => {
|
|
763
774
|
const version = (ctx.read(s.files[0]).match(/^version\s*=\s*"([^"]+)"/m) || [])[1];
|
package/development.mjs
CHANGED
|
@@ -50,7 +50,7 @@ export function developmentEnvironment(env = process.env) {
|
|
|
50
50
|
result[pathKey] = [agentBin, result[pathKey]].filter(Boolean).join(path.delimiter);
|
|
51
51
|
}
|
|
52
52
|
// Same default as the agent-bin `rightkit` shim: children that run the managed client directly
|
|
53
|
-
// (resolveCommand("
|
|
53
|
+
// (resolveCommand("rustright"), build scripts) must not lose the broker endpoint.
|
|
54
54
|
const socket = discoverBrokerSocket(result);
|
|
55
55
|
if (!result.RIGHTKIT_BUILD_BROKER_SOCKET && socket && existsSync(socket)) result.RIGHTKIT_BUILD_BROKER_SOCKET = socket;
|
|
56
56
|
}
|
|
@@ -298,7 +298,8 @@ function execute(command, { root, env, dryRun, run, label }) {
|
|
|
298
298
|
export function resolveCommand(command, args, { root, env = process.env } = {}) {
|
|
299
299
|
if (command === "node") return { command: process.execPath, args };
|
|
300
300
|
if (command === "pnpm") return { command: process.execPath, args: [resolvePnpmCli({ root, env }), ...args] };
|
|
301
|
-
if (command === "rightkit")
|
|
301
|
+
if (command === "rightkit") throw new Error("`rightkit` is the retired name of the RustRight client (rustright#91); use rustright (cmd: \"rustright\")");
|
|
302
|
+
if (command === "rustright") {
|
|
302
303
|
const client = managedRuntimeClient(env);
|
|
303
304
|
if (!client) throw new Error("installed RightKit runtime client is unavailable for development command");
|
|
304
305
|
const socket = discoverBrokerSocket(env);
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// hostedWorkflows: { policy: "declared-unrelated", files: { "<file>.yml": "<reason>" | { reason, idToken } } }
|
|
2
|
+
//
|
|
3
|
+
// For a repository that ships more than the released app (a monorepo whose web or server pipeline is a hand-written
|
|
4
|
+
// GitHub workflow). The released app still builds, signs and uploads only through right-release on its native host;
|
|
5
|
+
// every hosted workflow must be declared with the reason it exists, and none may be able to build, sign or upload a
|
|
6
|
+
// release. What makes that hold is credentials: an admitted workflow reaches no secret but GITHUB_TOKEN (no secrets
|
|
7
|
+
// context, no `secrets: inherit`, no reusable workflow, no escape that could spell one), mints no OIDC token unless the
|
|
8
|
+
// file declares why (`idToken`), and runs only commit-pinned actions. Without an Apple, Azure or store credential it
|
|
9
|
+
// cannot sign or upload as Orthic Labs. The command and artifact patterns are a denylist backstop, not the guarantee:
|
|
10
|
+
// a script file the workflow runs is not read.
|
|
11
|
+
export const DECLARED_UNRELATED_POLICY = "declared-unrelated";
|
|
12
|
+
|
|
13
|
+
const FILE_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,60}\.ya?ml$/;
|
|
14
|
+
const MIN_REASON = 12;
|
|
15
|
+
|
|
16
|
+
// Release machinery a declared-unrelated workflow must never run. Matched on the whole file, comments included: a comment cannot hide a command.
|
|
17
|
+
const RELEASE_MACHINERY = [
|
|
18
|
+
[/\bright-release\b/, "right-release"],
|
|
19
|
+
[/\bxcodebuild\b[^\n]*(?:\barchive\b|-exportArchive\b|-exportOptionsPlist\b)/, "xcodebuild archive/export"],
|
|
20
|
+
[/\bcodesign\b/, "codesign"],
|
|
21
|
+
[/\bnotarytool\b|\bstapler\b/, "notarization"],
|
|
22
|
+
[/\baltool\b|\biTMSTransporter\b|\bupload-testflight\b|\bapp-store-connect\b/i, "App Store upload"],
|
|
23
|
+
[/\bfastlane\b/, "fastlane"],
|
|
24
|
+
[/\bsecurity\s+import\b|\bapple-actions\//, "Apple signing keychain import"],
|
|
25
|
+
[/\bproductbuild\b|\bpkgbuild\b/, "installer package build"],
|
|
26
|
+
[/\bsigntool\b|\bAzureSignTool\b|\bazure\/(?:trusted|artifact)-signing/i, "Windows code signing"],
|
|
27
|
+
[/\btauri(?:-apps\/tauri-action|\s+build\b)/, "Tauri app build"],
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// A `uses:` must be a local action (./...), a docker image by digest, or owner/repo[/path]@<40-hex commit>.
|
|
31
|
+
const PINNED_USES = /^(?:\.\/[^\s]*|docker:\/\/[^\s@]+@sha256:[0-9a-f]{64}|[A-Za-z0-9_.-]+\/[A-Za-z0-9_./-]+@[0-9a-f]{40})$/;
|
|
32
|
+
|
|
33
|
+
const normalize = (source) => source.replace(/\r\n?/g, "\n");
|
|
34
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
35
|
+
const entryReason = (entry) => (isObject(entry) ? entry.reason : entry);
|
|
36
|
+
|
|
37
|
+
/** Shape problems of a declared-unrelated hostedWorkflows value (an empty list when it is well formed). */
|
|
38
|
+
export function declaredUnrelatedShapeProblems(value) {
|
|
39
|
+
if (!isObject(value)) return ["hostedWorkflows must be a policy string or an object"];
|
|
40
|
+
const problems = [];
|
|
41
|
+
for (const key of Object.keys(value)) if (key !== "policy" && key !== "files") problems.push(`hostedWorkflows.${key} is not a known field (policy, files)`);
|
|
42
|
+
if (value.policy !== DECLARED_UNRELATED_POLICY) problems.push(`hostedWorkflows.policy must be "${DECLARED_UNRELATED_POLICY}"`);
|
|
43
|
+
const files = value.files;
|
|
44
|
+
if (!isObject(files) || Object.keys(files).length === 0) {
|
|
45
|
+
problems.push("hostedWorkflows.files must map each workflow file name to the reason it exists");
|
|
46
|
+
return problems;
|
|
47
|
+
}
|
|
48
|
+
for (const [file, entry] of Object.entries(files)) {
|
|
49
|
+
if (!FILE_NAME.test(file)) problems.push(`hostedWorkflows.files has an invalid workflow file name ${JSON.stringify(file)}`);
|
|
50
|
+
if (isObject(entry)) {
|
|
51
|
+
for (const key of Object.keys(entry)) if (key !== "reason" && key !== "idToken") problems.push(`hostedWorkflows.files["${file}"].${key} is not a known field (reason, idToken)`);
|
|
52
|
+
if (entry.idToken !== undefined && (typeof entry.idToken !== "string" || entry.idToken.trim().length < MIN_REASON)) problems.push(`hostedWorkflows.files["${file}"].idToken must say why the workflow mints an OIDC token (at least ${MIN_REASON} characters)`);
|
|
53
|
+
}
|
|
54
|
+
const reason = entryReason(entry);
|
|
55
|
+
if (typeof reason !== "string" || reason.trim().length < MIN_REASON) problems.push(`hostedWorkflows.files["${file}"] must say why the workflow exists (at least ${MIN_REASON} characters)`);
|
|
56
|
+
}
|
|
57
|
+
return problems;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function credentialRefusal(file, source, entry) {
|
|
61
|
+
// GitHub decodes YAML escapes before it evaluates expressions, so an escape could spell `secrets` or a command.
|
|
62
|
+
if (/\\[xuU]/.test(source)) return `${file} contains a \\x, \\u or \\U escape; a declared-unrelated workflow must spell every name literally`;
|
|
63
|
+
// The only secret is GITHUB_TOKEN; any other use of the word (secrets.X, secrets['X'], secrets: inherit, toJSON(secrets),
|
|
64
|
+
// join(secrets)) is refused. A path segment such as /run/secrets/ is not the context.
|
|
65
|
+
const rest = source.replace(/\bsecrets\.GITHUB_TOKEN\b/g, "");
|
|
66
|
+
if (/(?<![\w/-])secrets\b(?![\w/-])/i.test(rest)) return `${file} references secrets other than GITHUB_TOKEN (or passes the secrets context on); a declared-unrelated workflow can hold no signing or store credential`;
|
|
67
|
+
for (const [, ref] of source.matchAll(/^\s*(?:-\s+)?uses:\s*["']?([^\s"'#]+)/gm)) {
|
|
68
|
+
if (/\.github\/workflows\//.test(ref)) return `${file} calls the reusable workflow ${ref}; a called workflow is not checked`;
|
|
69
|
+
if (!PINNED_USES.test(ref)) return `${file} uses ${ref}, which is not pinned to a full commit sha (or a local action, or a docker image by digest)`;
|
|
70
|
+
}
|
|
71
|
+
if (/^\s*id-token\s*:\s*write\b/m.test(source) && !(isObject(entry) && entry.idToken)) return `${file} mints an OIDC token (id-token: write) without declaring why; set hostedWorkflows.files["${file}"] to { reason, idToken: "<why>" }`;
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Why a declared-unrelated workflow set is refused, or null when it is admitted.
|
|
77
|
+
* workflows: the file names present in .github/workflows; read(file): its text; artifacts: release artifact paths
|
|
78
|
+
* from the config's targets (a workflow that names one could produce or ship it).
|
|
79
|
+
*/
|
|
80
|
+
export function declaredUnrelatedRefusal({ declaration, workflows, read, artifacts = [] }) {
|
|
81
|
+
const shape = declaredUnrelatedShapeProblems(declaration);
|
|
82
|
+
if (shape.length) return shape.join("; ");
|
|
83
|
+
const declared = Object.keys(declaration.files).sort();
|
|
84
|
+
const present = [...workflows].sort();
|
|
85
|
+
const undeclared = present.filter((file) => !declared.includes(file));
|
|
86
|
+
if (undeclared.length) return `undeclared hosted workflow file(s): ${undeclared.join(", ")} (declare each in hostedWorkflows.files with its reason, or remove it)`;
|
|
87
|
+
const missing = declared.filter((file) => !present.includes(file));
|
|
88
|
+
if (missing.length) return `hostedWorkflows.files declares workflow file(s) that do not exist: ${missing.join(", ")}`;
|
|
89
|
+
for (const file of present) {
|
|
90
|
+
const source = normalize(read(file));
|
|
91
|
+
const credential = credentialRefusal(file, source, declaration.files[file]);
|
|
92
|
+
if (credential) return credential;
|
|
93
|
+
for (const [pattern, label] of RELEASE_MACHINERY) {
|
|
94
|
+
if (pattern.test(source)) return `${file} runs release machinery (${label}); the released app builds, signs and uploads only through right-release on its native host`;
|
|
95
|
+
}
|
|
96
|
+
for (const artifact of artifacts) {
|
|
97
|
+
if (artifact && source.includes(artifact)) return `${file} names the release artifact ${artifact}`;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Release artifact paths a config's targets declare (artifacts, iosPackage archive and ipa). */
|
|
104
|
+
export function releaseArtifactPaths(config) {
|
|
105
|
+
const paths = new Set();
|
|
106
|
+
for (const target of Object.values(config?.targets ?? {})) {
|
|
107
|
+
for (const artifact of target?.artifacts ?? []) if (typeof artifact === "string") paths.add(artifact);
|
|
108
|
+
for (const key of ["archive", "ipa"]) if (typeof target?.iosPackage?.[key] === "string") paths.add(target.iosPackage[key]);
|
|
109
|
+
}
|
|
110
|
+
return [...paths];
|
|
111
|
+
}
|
package/ios-dev.mjs
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
// | { kind: "device", device: "<devicectl identifier>", bundleId, productName?, launch?: true, launchArgs?: [] },
|
|
14
14
|
// }
|
|
15
15
|
// The build commands are generated from the config (a hand-written `build` is refused). The device is chosen by id only, never
|
|
16
|
-
// "the only device": `install.device`, `--serial` or RIGHTKIT_IOS_DEVICE. DerivedData comes from `
|
|
16
|
+
// "the only device": `install.device`, `--serial` or RIGHTKIT_IOS_DEVICE. DerivedData comes from `rustright storage xcode-args`.
|
|
17
17
|
import * as nodeFs from "node:fs";
|
|
18
18
|
import os from "node:os";
|
|
19
19
|
import path from "node:path";
|
|
@@ -102,7 +102,7 @@ export function validateIosDevelopment(target, { root, env = process.env } = {})
|
|
|
102
102
|
install = { ...descriptor, productName: descriptor.productName ?? target.scheme, launchArgs: [...(descriptor.launchArgs ?? [])] };
|
|
103
103
|
}
|
|
104
104
|
const normalized = { project, projectSpec, scheme: target.scheme, configuration, teamId: target.teamId, extraBuildSettings, prepare, install };
|
|
105
|
-
return { ...normalized, build: iosDevelopmentBuildCommands(normalized, { root, storage: { derivedData: "<
|
|
105
|
+
return { ...normalized, build: iosDevelopmentBuildCommands(normalized, { root, storage: { derivedData: "<rustright storage xcode-args -derivedDataPath>" }, destinationId: "<device or simulator id>" }) };
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
const destinationFor = (install, id) => (install?.kind === "device" ? `id=${id}` : `platform=iOS Simulator,id=${id}`);
|
package/ios-host.mjs
CHANGED
|
@@ -34,29 +34,29 @@ export function assertGuiSigningSession({ operation, env = process.env, run = sp
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
/**
|
|
37
|
-
* DerivedData and result-bundle paths. With RIGHTKIT_RUN_ROOT set they come from `
|
|
37
|
+
* DerivedData and result-bundle paths. With RIGHTKIT_RUN_ROOT set they come from `rustright storage xcode-args --run-root <root>`
|
|
38
38
|
* (a JSON argv list holding -derivedDataPath and -resultBundlePath), the same source CodeRight and HeardRight use. Without a
|
|
39
39
|
* managed run the fallback is a directory under the right-release state root, never an in-repo DerivedData.
|
|
40
40
|
*/
|
|
41
41
|
export function resolveXcodeStorage({ env = process.env, run = spawnSync, cwd = process.cwd(), fallbackRoot }) {
|
|
42
42
|
const runRoot = typeof env.RIGHTKIT_RUN_ROOT === "string" ? env.RIGHTKIT_RUN_ROOT.trim() : "";
|
|
43
43
|
if (!runRoot) {
|
|
44
|
-
if (!fallbackRoot) throw new IosHostError("IOS_STORAGE_UNAVAILABLE", "no RIGHTKIT_RUN_ROOT and no fallback storage root; run via:
|
|
44
|
+
if (!fallbackRoot) throw new IosHostError("IOS_STORAGE_UNAVAILABLE", "no RIGHTKIT_RUN_ROOT and no fallback storage root; run via: rustright storage run --app <app> -- right-release ...");
|
|
45
45
|
return { derivedData: path.join(fallbackRoot, "DerivedData"), resultBundle: undefined, source: "fallback" };
|
|
46
46
|
}
|
|
47
|
-
const result = run("
|
|
48
|
-
if (result.error || result.status !== 0) throw new IosHostError("IOS_STORAGE_FAILED", `
|
|
47
|
+
const result = run("rustright", ["storage", "xcode-args", "--run-root", runRoot], { cwd, env, encoding: "utf8", windowsHide: true, shell: false, timeout: 30_000 });
|
|
48
|
+
if (result.error || result.status !== 0) throw new IosHostError("IOS_STORAGE_FAILED", `rustright storage xcode-args failed (${result.status ?? "unavailable"})`);
|
|
49
49
|
let args;
|
|
50
50
|
try { args = JSON.parse(String(result.stdout ?? "")); } catch { args = null; }
|
|
51
|
-
if (!Array.isArray(args) || !args.length || args.some((arg) => typeof arg !== "string")) throw new IosHostError("IOS_STORAGE_FAILED", "
|
|
51
|
+
if (!Array.isArray(args) || !args.length || args.some((arg) => typeof arg !== "string")) throw new IosHostError("IOS_STORAGE_FAILED", "rustright storage xcode-args returned no argv list");
|
|
52
52
|
const value = (flag, required) => {
|
|
53
53
|
const index = args.indexOf(flag);
|
|
54
54
|
if (index < 0 || index + 1 >= args.length) {
|
|
55
|
-
if (required) throw new IosHostError("IOS_STORAGE_FAILED", `
|
|
55
|
+
if (required) throw new IosHostError("IOS_STORAGE_FAILED", `rustright storage xcode-args is missing ${flag}`);
|
|
56
56
|
return undefined;
|
|
57
57
|
}
|
|
58
58
|
const found = args[index + 1];
|
|
59
|
-
if (!path.isAbsolute(found)) throw new IosHostError("IOS_STORAGE_FAILED", `
|
|
59
|
+
if (!path.isAbsolute(found)) throw new IosHostError("IOS_STORAGE_FAILED", `rustright storage xcode-args ${flag} is not an absolute path`);
|
|
60
60
|
return found;
|
|
61
61
|
};
|
|
62
62
|
return { derivedData: value("-derivedDataPath", true), resultBundle: value("-resultBundlePath", false), source: "rightkit-storage" };
|
package/ios-package-recipe.mjs
CHANGED
|
@@ -378,7 +378,7 @@ const plannedTrust = (spec) => {
|
|
|
378
378
|
};
|
|
379
379
|
|
|
380
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>", storage = { derivedData: "<
|
|
381
|
+
export function iosPackagePlan(spec, { root, buildNumber = "<YYYYMMDDNN>", storage = { derivedData: "<rustright storage xcode-args -derivedDataPath>" }, receiptDir }) {
|
|
382
382
|
const optionsFile = path.join(receiptDir, "ios-export-options.plist");
|
|
383
383
|
return {
|
|
384
384
|
steps: [
|
|
@@ -504,7 +504,7 @@ export function scanForbiddenStrings(dir, strings, fs = nodeFs) {
|
|
|
504
504
|
* scanFiles(label, files, o) release.mjs's assertNoQaControlBinaries (QA rules plus the release-switch binary layer)
|
|
505
505
|
* detectSession / route GUI signing session seams (ios-host.mjs)
|
|
506
506
|
* gitHead() the source commit; it must not change during the build
|
|
507
|
-
* storage() DerivedData and result bundle (
|
|
507
|
+
* storage() DerivedData and result bundle (rustright storage xcode-args)
|
|
508
508
|
* scanForbidden(dir, strings) forbidden-string scan
|
|
509
509
|
* counterDir where the per-app build number counter lives (the vault state root under right-release build)
|
|
510
510
|
*/
|
package/local-publish.mjs
CHANGED
|
@@ -529,7 +529,7 @@ async function publishUnit(parsed, deps, batch) {
|
|
|
529
529
|
// #258: the package ships a Developer-ID-signed bin/darwin/rightkit-swap; build, sign and verify it before packing (fails closed).
|
|
530
530
|
try { (deps.prepareSwapHelper ?? prepareSwapHelper)({ workspaceRoot, packageDir: unitPath, platform, run, env, log }); }
|
|
531
531
|
catch (error) { refuse(`${error.code ? `[${error.code}] ` : ""}${error.message}`); }
|
|
532
|
-
packCommands.push(`
|
|
532
|
+
packCommands.push(`rustright cargo ${SWAP_HELPER_CARGO_ARGS.join(" ")} && codesign --verify --strict bin/darwin/rightkit-swap`);
|
|
533
533
|
}
|
|
534
534
|
if (kind === "npm") {
|
|
535
535
|
// The feed a tarball ships must list the version it ships with (#319): stage it before packing.
|
package/local-qualify.mjs
CHANGED
|
@@ -370,16 +370,16 @@ function brokerIdentity(deps) {
|
|
|
370
370
|
return { name: "rustright", client: String(client).replaceAll("\\", "/"), sha256: sha256((deps.readFile ?? fs.readFileSync)(client)) };
|
|
371
371
|
}
|
|
372
372
|
|
|
373
|
-
// Cargo only through the broker: `
|
|
373
|
+
// Cargo only through the broker: `rustright cargo`, or a `cargo` on PATH that is the managed shim (scripts/local-gate.mjs).
|
|
374
374
|
function detectCargoCommand(deps) {
|
|
375
375
|
const which = (command) => {
|
|
376
376
|
const result = (deps.run ?? spawnSync)((deps.platform ?? process.platform) === "win32" ? "where" : "which", [command], { encoding: "utf8", windowsHide: true });
|
|
377
377
|
return result.status === 0 ? String(result.stdout).split(/\r?\n/).map((line) => line.trim()).filter(Boolean) : [];
|
|
378
378
|
};
|
|
379
|
-
if (which("
|
|
379
|
+
if (which("rustright").length) return ["rustright", "cargo"];
|
|
380
380
|
const first = which("cargo")[0];
|
|
381
381
|
if (first) { try { if (fs.readFileSync(first, "utf8").includes("cargo-guard.mjs")) return ["cargo"]; } catch { /* not the managed shim */ } }
|
|
382
|
-
return refuse("neither `
|
|
382
|
+
return refuse("neither `rustright` nor the managed cargo shim (cargo-guard.mjs) is first on PATH; plain cargo is never used");
|
|
383
383
|
}
|
|
384
384
|
|
|
385
385
|
const loadFromRoot = (root, file) => {
|
package/local-runtime.mjs
CHANGED
|
@@ -176,7 +176,7 @@ export async function measureRuntimeCrate(options, deps = {}) {
|
|
|
176
176
|
const run = deps.run ?? ((command, args, settings) => spawnCollect(command, args, { maxBuffer: 64 * 1024 * 1024, ...settings }));
|
|
177
177
|
const invoke = (args, settings) => run(cargoCommand[0], [...cargoCommand.slice(1), ...args], { cwd: root, windowsHide: true, ...settings });
|
|
178
178
|
const toolchainOf = async () => {
|
|
179
|
-
const rustc = await run(cargoCommand[0] === "
|
|
179
|
+
const rustc = await run(cargoCommand[0] === "rustright" ? "rustright" : "rustc", cargoCommand[0] === "rustright" ? ["rustc", "-Vv"] : ["-Vv"], { cwd: root, env: { ...baseEnv, ...brokerEnv }, windowsHide: true, timeout: 60_000 });
|
|
180
180
|
const cargo = await invoke(["-V"], { env: { ...baseEnv, ...brokerEnv }, timeout: 60_000 });
|
|
181
181
|
if (rustc.error || rustc.status !== 0 || cargo.error || cargo.status !== 0) throw new Error("cannot read the toolchain through the broker");
|
|
182
182
|
return { rustc: String(rustc.stdout).trim(), cargo: String(cargo.stdout).trim() };
|
package/package.json
CHANGED
package/pipeline-admission.mjs
CHANGED
|
@@ -352,11 +352,12 @@ export function admitInvocation(step, options = {}) {
|
|
|
352
352
|
if (name === "right-release") return admitRightRelease(args, label);
|
|
353
353
|
if (name === "pnpm") return admitPnpm(args, inner, label, chain);
|
|
354
354
|
if (name === "node") return admitNode(args, inner, label);
|
|
355
|
-
if (name === "rightkit") {
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
if (name === "rightkit") return refuse("R1-retired-client", `${label}: rightkit is the retired name of the RustRight client (rustright#91); use rustright ${args.slice(0, 2).join(" ")}`);
|
|
356
|
+
if (name === "rustright") {
|
|
357
|
+
if (args[0] !== "cargo" || !CARGO_SUBCOMMANDS.has(args[1])) return refuse("R1-managed-client", `${label}: only rustright cargo ${[...CARGO_SUBCOMMANDS].join("|")} is admitted`);
|
|
358
|
+
return shellSafeArgs(args, label) ?? admit(`rustright-cargo-${args[1]}`, "managed-cargo");
|
|
358
359
|
}
|
|
359
|
-
if (name === "cargo" || name === "rustc" || name === "rustdoc") return refuse("R1-bare-cargo", `${label}: run Cargo through the managed client (
|
|
360
|
+
if (name === "cargo" || name === "rustc" || name === "rustdoc") return refuse("R1-bare-cargo", `${label}: run Cargo through the managed client (rustright cargo ...), never bare ${name}`);
|
|
360
361
|
if (name === "tauri") return refuse("R1-direct-tauri", `${label}: run the Tauri CLI as pnpm exec tauri ..., or through right-release; a bare tauri is not an approved installed runtime`);
|
|
361
362
|
if (PACKAGE_MANAGERS.has(name)) return refuse("R1-package-manager", `${label}: ${name} is not allowed; the workspace uses pnpm`);
|
|
362
363
|
if (INTERPRETERS.has(name)) return refuse("R1-interpreter", `${label}: ${name} runs script text or a script file the app owns; only an admitted shared generator may do that`);
|