@rightkit/release 0.2.173 → 0.3.0
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 +188 -5
- package/bin/darwin/rightkit-swap +0 -0
- package/bin/darwin/rightkit-swap.sign.json +1 -1
- package/cli/right-release.mjs +34 -6
- package/command-spec.mjs +80 -0
- package/deps-cargo.mjs +55 -0
- package/deps.mjs +7 -0
- package/development-signing.mjs +135 -4
- package/development.mjs +12 -11
- package/dist-scan.mjs +102 -0
- package/dmg-build.mjs +53 -4
- package/icon-pack.mjs +117 -0
- package/in-place-install.mjs +32 -7
- package/init-command.mjs +116 -0
- package/install-app-windows.mjs +108 -17
- package/install-app.mjs +68 -11
- package/install-tauri-devid.mjs +106 -47
- package/local-publish.mjs +317 -29
- package/mac-bundle-signing.mjs +143 -1
- package/mac-signing-matrix.mjs +73 -6
- package/macho-approval-hash.mjs +181 -0
- package/macos-signing-session.mjs +12 -2
- package/managed-cargo.mjs +49 -1
- package/media-sidecar.mjs +20 -4
- package/mirror-root-artifact.mjs +69 -21
- package/npm-license-notices.mjs +159 -0
- package/ownership-dependency-allowlist.json +1 -0
- package/ownership.mjs +37 -4
- package/package-recipe-targets.mjs +180 -16
- package/package.json +1 -1
- package/pe-content.mjs +133 -0
- package/portable-development.mjs +9 -9
- package/publication-gate.mjs +70 -3
- package/publish-cargo.mjs +26 -7
- package/qa-build-features.mjs +7 -1
- package/recipe-build.mjs +346 -0
- package/release-dependencies-command.mjs +45 -0
- package/release-dependencies.mjs +60 -0
- package/release-switch-scan.mjs +17 -7
- package/release.mjs +98 -12
- package/rightkit-ownership.json +46 -2
- package/rightkit-signing-agent.mjs +10 -4
- package/rightkit-versions.json +11 -6
- package/runtime-budgets.mjs +28 -12
- package/stage-binary.mjs +1 -1
- package/stage-sidecar-command.mjs +36 -4
- package/stage-sidecar.mjs +926 -74
- package/test-command.mjs +190 -0
- package/toolchain-swift.mjs +239 -0
- package/web-build.mjs +67 -0
package/README.md
CHANGED
|
@@ -65,7 +65,7 @@ Every Right app has absolutely NO access to DevTools; the single exception is Vi
|
|
|
65
65
|
|
|
66
66
|
| Group | Subcommands |
|
|
67
67
|
|---|---|
|
|
68
|
-
| Build | `release`, `build`, `package` |
|
|
68
|
+
| Build | `release`, `build`, `package`, `build --web` / `web-build` (web frontend only) |
|
|
69
69
|
| Build (SwiftPM apps) | `swift-package` (`windows-stage`, `msix`, `mac-app`) (config schema 1: `windows.{exe,out,searchDirs,include,dependencies,publisher,assets}`, `mac.{executable,bundleId,out,frameworks,resources,entitlements}`) |
|
|
70
70
|
| Install | `install-app` |
|
|
71
71
|
| Publish | `publish`, `upload`, `publish-update`, `github`, `mirror-root-artifact` |
|
|
@@ -75,8 +75,86 @@ Every Right app has absolutely NO access to DevTools; the single exception is Vi
|
|
|
75
75
|
| Dev artifact signing | `sign-dev-artifact --artifact <dir> --manifest <sign.json> [--metadata <payload.json>] [--evidence <path>] [--dry-run]`: Developer ID signs an existing CI dev artifact per file with fixed identifiers (macOS). It verifies the recorded hashes in the payload metadata (default `payload.json`, `{ schema: 1, files: [{ path, sha256 }] }`) first, signs each sign-manifest entry (`file` relative to the artifact, `identifier` required, nested code before its bundle) through the dev-signing identity (missing Developer ID: `DEV_MAC_SIGNING_DEVELOPER_ID_REQUIRED`, no ad hoc fallback), verifies with `codesign --verify --strict`, reads back Identifier and TeamIdentifier, refreshes the metadata hashes, and writes evidence JSON. Never builds, notarizes or publishes. The payload metadata must sit outside every signed entry (`SIGN_DEV_ARTIFACT_METADATA_IN_SIGNED_ENTRY`). An optional declarative `refresh` in the sign manifest recomputes digests that other JSON files record about signed bytes: `{ "refreshRoot": "<dir inside the artifact>", "refresh": [{ "file", "pointer" (JSON pointer to an existing string), "compute": "sha256-file" \| "sha256-tree-v1" \| "verify-sha256-file", "of", "format"?, "currentMatches"? }] }` (`file` and `of` relative to `refreshRoot`; `sha256-tree-v1` = sha256 over the sorted relative paths, each as path, NUL, 8-byte big-endian length, bytes; `verify-sha256-file` writes nothing and refuses a digest that changed; `format` embeds the digest in a locator string through one `{value}`; `currentMatches` refuses to overwrite a value of another kind, such as a signed attestation locator). Order is enforced: operations run after the entries they read are signed and before the first entry that contains a refreshed file, so that bundle (the wrapper) signs last over the final bytes; an operation may not read what a later one writes. JSON is rewritten in place with key order kept, and a file the writer would have to reformat is refused. Evidence gains a `refresh` list |
|
|
76
76
|
| Checks | `deps`, `hardening`, `harden`, `hardeningscan`, `cargo-target` |
|
|
77
77
|
| Ownership | `ownership`, `ownership-approval`, `ownership-report` |
|
|
78
|
+
| Test | `test` (the app's declared suites) |
|
|
79
|
+
| Legal | `license-notices npm` |
|
|
78
80
|
| Other | `dev`, `doctor`, `suite-doctor`, `qa`, `dmg`, `cache`, `apple`, `inventory`, `model`, `addon`, `hooks`, `lsclean`, `generate-dmg-background`, `publish cargo`, `publish npm`, `publish swift` |
|
|
79
81
|
|
|
82
|
+
## Declared test suites, web build, npm notices and artifact mirroring
|
|
83
|
+
|
|
84
|
+
These replace per-app wrapper scripts. Each is declared in `right-release.config.mjs` and runs without a shell from the app
|
|
85
|
+
root (the config file's directory). A step is `{ cmd | node, args?, cwd?, env?, nodeFlags?, nodeOptions? }`: `node` is an
|
|
86
|
+
app-relative script run by the Node that runs `right-release`, `cmd: "node"` means the same Node, `nodeFlags` go before the
|
|
87
|
+
script, `nodeOptions` are merged once into `NODE_OPTIONS`. `cwd` and `node` must stay inside the app; unknown keys are refused.
|
|
88
|
+
|
|
89
|
+
### `right-release test`
|
|
90
|
+
|
|
91
|
+
```js
|
|
92
|
+
tests: {
|
|
93
|
+
eventPrefix: "e2e", // optional, default "test": events are <prefix>_suite_started / _completed
|
|
94
|
+
runners: { // optional named bases
|
|
95
|
+
vitest: { node: "node_modules/vitest/vitest.mjs", nodeFlags: ["--no-experimental-webstorage"], nodeOptions: ["--no-experimental-webstorage"] },
|
|
96
|
+
},
|
|
97
|
+
suites: [
|
|
98
|
+
{ name: "tooling", runner: "vitest", args: ["run", "scripts/tooling.test.mjs", "--no-file-parallelism", "--maxWorkers=1"] },
|
|
99
|
+
{ name: "runtime-config", node: "scripts/runtime-config.test.mjs", nodeFlags: ["--test"] },
|
|
100
|
+
// Expands to journeys-1, journeys-2, ...: the names in the JSON list run `chunk` at a time, one process per chunk.
|
|
101
|
+
{ name: "journeys", node: "scripts/qa-browser-functional.mjs", args: ["--keep-going"],
|
|
102
|
+
journeys: { file: "scripts/qa-e2e-journeys.json", key: "journeys", chunk: 12, flag: "--journey" } },
|
|
103
|
+
],
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
`right-release test` runs every suite in order and stops at the first failure, exiting with that suite's code; each suite prints
|
|
108
|
+
a JSON line before and after (`{"event":"e2e_suite_completed","suite":"tooling","exitCode":0,"durationMs":1234}`). `--suite a,b`
|
|
109
|
+
(repeatable) selects suites; a group name such as `journeys` selects all its chunks. `--runner vitest -- run --project unit`
|
|
110
|
+
runs a runner once with the arguments after `--` (watch mode, one project); `-- <args>` after `--suite` is allowed when exactly
|
|
111
|
+
one suite is selected. `--list [--json]` prints the resolved commands and runs nothing. Wrap it as the repository needs, for
|
|
112
|
+
example `rightkit storage run --app <app> -- right-release test`.
|
|
113
|
+
|
|
114
|
+
### `right-release build --web` (or `web-build`)
|
|
115
|
+
|
|
116
|
+
```js
|
|
117
|
+
web: { build: [
|
|
118
|
+
{ node: "node_modules/typescript/bin/tsc", nodeFlags: ["--disable-warning=DEP0205"] },
|
|
119
|
+
{ node: "node_modules/vite/bin/vite.js", args: ["build"], nodeFlags: ["--disable-warning=DEP0205"] },
|
|
120
|
+
] }
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Runs only the `web.build` steps: no Tauri, no signing, no packaging. `--dry-run` prints the commands. It is guarded by the same
|
|
124
|
+
GitHub-Actions-only execution policy as the other build commands.
|
|
125
|
+
|
|
126
|
+
### `right-release license-notices npm`
|
|
127
|
+
|
|
128
|
+
```js
|
|
129
|
+
legal: { npmNotices: { output: "legal/THIRD_PARTY_NPM_LICENSES.html", productName: "ViewRight", exclude: ["@rightkit/"] } }
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
Writes the third-party npm license appendix for the production closure of a pnpm app (`pnpm licenses list --prod --json`),
|
|
133
|
+
with each package's own LICENSE / COPYING / NOTICE text. A package with no license file gets the standard published text for
|
|
134
|
+
its declared MIT or ISC license, labelled as a fallback; any other license without a file fails the run. The page records the
|
|
135
|
+
app version, the `pnpm-lock.yaml` sha256 and `generated-by=right-release license-notices npm`, so equal inputs give equal bytes.
|
|
136
|
+
`--check` writes nothing and exits 1 when the file differs. `--output` and `--product-name` override the config.
|
|
137
|
+
|
|
138
|
+
### Mirroring an artifact (`mirror-root-artifact`)
|
|
139
|
+
|
|
140
|
+
```js
|
|
141
|
+
targets: { win: {
|
|
142
|
+
mirror: [{ file: "src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/App_1.0.0_x64-setup.exe", pruneSuffix: "-setup.exe" }],
|
|
143
|
+
postSign: { cmd: "right-release", args: ["mirror-root-artifact", "--config", "right-release.config.mjs", "--platform", "win"] },
|
|
144
|
+
} }
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
Copies each `targets.<platform>.mirror` entry (an app-relative path, or `{ file, name?, pruneSuffix? }`) atomically into
|
|
148
|
+
`RIGHT_RELEASE_ARTIFACT_DIR`. The directory must be outside the repository and the command refuses to run without it, so it
|
|
149
|
+
never writes into the repository root. `pruneSuffix` then removes the other files with that suffix from that directory, so it
|
|
150
|
+
holds only the current installer. The one-file form (`--file <path> --package-root <dir> [--name <name>]`) is unchanged.
|
|
151
|
+
|
|
152
|
+
### macOS updater tarball from `macPackage`
|
|
153
|
+
|
|
154
|
+
`targets.mac.macPackage.updater: true` (or an app-relative `.tar.gz` path) adds a `create-mac-updater` command at the front of
|
|
155
|
+
`postSign`: it packs the signed, stapled `.app` and signs the tarball. The default output is `<App>.app.tar.gz`, named after the
|
|
156
|
+
app in `notarize.app`. Declare the file in `updater.artifacts` as before. See `docs/MAC-PACKAGE-RECIPE.md`.
|
|
157
|
+
|
|
80
158
|
## QA receipts (warn-and-record)
|
|
81
159
|
|
|
82
160
|
`right-release qa --stage source|installed --platform mac|win` still runs `rightkit-qa` and writes a signed receipt, and receipt verification is unchanged. A missing or invalid receipt no longer blocks a release. `build`, `upload`, `publish-update` and `github` print `[right-release] QA warning: ...` to stderr, continue, and record the outcome:
|
|
@@ -100,13 +178,13 @@ The shipped `bin/darwin/rightkit-swap` is Developer ID signed. It is added by `r
|
|
|
100
178
|
|
|
101
179
|
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.
|
|
102
180
|
|
|
103
|
-
## One fixed path, one identity
|
|
181
|
+
## One fixed path, one identity; release keeps one `.previous`, dev keeps none (every app)
|
|
104
182
|
|
|
105
|
-
macOS and Windows tie privacy grants (mic, screen, Files and Folders, Removable Volumes, Full Disk Access) to the path an app runs from and to its code identity. So every app, helper and dev or QA binary installs and updates IN PLACE at ONE fixed path under ONE identity; there is no per-version, per-digest, per-pid or per-build install folder or bundle id.
|
|
183
|
+
macOS and Windows tie privacy grants (mic, screen, Files and Folders, Removable Volumes, Full Disk Access) to the path an app runs from and to its code identity. So every app, helper and dev or QA binary installs and updates IN PLACE at ONE fixed path under ONE identity; there is no per-version, per-digest, per-pid or per-build install folder or bundle id. A RELEASE install may keep one rollback `<name>.previous` beside the fixed path (rename it back). A DEV or QA install keeps NO old copy of any kind (Adrian, bogusyogi/rightkit#348: "when installing dev, old copy must never be backedup"; reinstall from an older DMG): it replaces the path in place through a short-lived staging name and removes any stale `.previous`, and a failed dev install leaves the path absent or partial with a clear error. Nothing ever runs from `.previous` or from a staging copy.
|
|
106
184
|
|
|
107
|
-
- `install-app` (macOS) renames the replaced app to `<App>.app.previous` (never the Trash); `install-app --installer <Setup.exe> --dest <folder> --exe <App.exe
|
|
185
|
+
- `install-app` (macOS) renames the replaced app to `<App>.app.previous` (never the Trash) for a release install only; `install-app --dev` removes the old app and keeps nothing. `install-app --installer <Setup.exe> --dest <folder> --exe <App.exe> [--payload <file>]... [--payload-manifest <json>]` runs the NSIS installer in place. It NEVER copies or restores the whole install folder, because that folder can hold user data (CodeRight keeps `coderight.db`, its WAL, state and models there). Its backup and rollback are payload-only, from an explicit allowlist: the `--exe` file plus `--payload` entries and a `--payload-manifest` JSON of relative file paths (files, never directories); with no `--payload` it backs up and rolls back nothing. `right-release dev` (mac) installs to `install.destination` (required) with no `.previous` and fails if its designated requirement differs from the installed copy; Windows tauri dev runs the silent installer without copying `<exe>.previous`; a portable dev payload installs as `<installRoot>/current` with no `current.previous` (data beside `current` is never touched); the Windows direct installer installs to `current` with `current.previous`.
|
|
108
186
|
- `right-release dev` (mac) signs with the same Developer ID Application identity as release (an app that needs a provisioning profile uses a Developer ID profile); Apple Development certificates are refused with `DEV_MAC_SIGNING_DEVELOPER_ID_REQUIRED`.
|
|
109
|
-
- **Cargo target folders are not stable paths.** The broker's target tree is `<managed>/rightkit-build-control/ws/<hash>/g/<generation>/target/...` and the generation changes; `cargo-guard` uses a hashed `dev-targets/<project>-<hash>` folder. They are build output only, so nothing may run from them. Dev and QA binaries run from a fixed staged path: list them in `development.targets.<platform>.stage` (`[{ source } | { manifestPath, bin, targetTriple?, profile?, name? }]`) and `right-release dev` copies each in place to `<app root>/.rightkit-dev-bin/<triple-or-host>/<name>` (
|
|
187
|
+
- **Cargo target folders are not stable paths.** The broker's target tree is `<managed>/rightkit-build-control/ws/<hash>/g/<generation>/target/...` and the generation changes; `cargo-guard` uses a hashed `dev-targets/<project>-<hash>` folder. They are build output only, so nothing may run from them. Dev and QA binaries run from a fixed staged path: list them in `development.targets.<platform>.stage` (`[{ source } | { manifestPath, bin, targetTriple?, profile?, name? }]`) and `right-release dev` copies each in place to `<app root>/.rightkit-dev-bin/<triple-or-host>/<name>` (no backup of the old copy); `rightkit-qa` launches from its `.rightkit-qa-bin` stash on every path.
|
|
110
188
|
|
|
111
189
|
## Runtime budget gate (crates)
|
|
112
190
|
|
|
@@ -203,6 +281,10 @@ sign: {
|
|
|
203
281
|
|
|
204
282
|
Nested code that the map does not name (frameworks, other helpers) is signed first with the defaults; the appex's own `Contents/MacOS` executable is signed before the appex, and the appex signature then carries the extension entitlements. The Finder extension's entitlements never reach the app and the app's Apple Events entitlement never reaches the extension.
|
|
205
283
|
|
|
284
|
+
### Signing the built DMG (`dmg build --sign`)
|
|
285
|
+
|
|
286
|
+
`right-release dmg build ... --sign [--identifier <id>] [--receipt <path>]` Developer ID signs the finished image with the shared Developer ID Application identity (secure timestamp, no hardened runtime, as for any disk image; optional stable codesign `--identifier`; strict verification and identity/timestamp readback), so an app whose `package` step builds its own DMG needs no direct `sign-macos.mjs` call before `notarize`. `--receipt` writes the same signing receipt `sign-macos.mjs` writes. A signing failure removes the unsigned image (`DMG_SIGN_FAILED`). It never notarizes or uploads; there is no ad hoc or Apple Development fallback. `buildDmg({ ..., sign: true, identifier, signReceipt })` is the SDK form. `targets.mac.macPackage` and SwiftPM apps already sign the DMG themselves.
|
|
287
|
+
|
|
206
288
|
### DMG from an appdmg-style spec
|
|
207
289
|
|
|
208
290
|
`right-release dmg build --app <.app> --out <dmg> --spec <appdmg.json>` (and `targets.mac.dmg.spec` for SwiftPM apps that right-release packs itself) renders an appdmg-style JSON file through the same headless hdiutil pipeline; explicit flags and `targets.mac.dmg` options win over the spec, and the spec's `title` is the volume name (on the command line, `--volname` is required when the spec has no `title`). Relative `background` and `icon` paths resolve against the spec file's directory. Pulse's `release/appdmg.json` works unchanged:
|
|
@@ -292,6 +374,22 @@ Linux has catalog and planner support only: the planner maps `linux` to the `*-u
|
|
|
292
374
|
|
|
293
375
|
Any app that ships more than the ffmpeg sidecar (MailRight: `mailright-engine`, `mailright-core`) sets `targets.mac.externalBin` / `targets.win.externalBin`: `{ names: [...], arch?, stageDir? ("src-tauri/binaries"), bundleDir? (mac: "Contents/MacOS" | "Contents/Resources"), stage?: { cmd, args? } }`. It is the generic form of the ffmpeg staging above and reuses its target-triple naming (`<name>-<triple>[.exe]`), staged-file checks and signing evidence checks (`external-bin.mjs` over `media-sidecar.mjs`); it has no catalog, notices or manifest seal. `stage` is the app's own build of the files. Windows signs the staged files in the raw-EXE step; macOS signs `<bundleDir>/<name>` as nested code (hardened runtime) and checks the nested receipt. `external-bin.json` (state root) records pre-sign and signed digests. Names that `mediaSidecar` stages (`ffmpeg`, `ffprobe`) are refused here. Tests: `external-bin.test.mjs`.
|
|
294
376
|
|
|
377
|
+
## Sidecar staging: source builds, prebuilt checks, archives, mixed variants, universal macOS
|
|
378
|
+
|
|
379
|
+
`stage-sidecar` (the `sidecars` list in `right-release.config.mjs`, receipt `stage-sidecar.json`) covers these cases without an app script. Every download is https and digest-pinned, nothing runs through a shell, and a failed check stages nothing.
|
|
380
|
+
|
|
381
|
+
- **`cmake` (Orthic-Labs/rightkit#489):** `{ kind: "cmake", name: "whisper-cli", repo: "https://github.com/ggml-org/whisper.cpp", commit: "<40 hex>", target?: "whisper-cli", output?: "bin/whisper-cli", cmakeArgs?: ["-DBUILD_SHARED_LIBS=OFF"], platformArgs?: { mac: ["-DGGML_METAL=ON"], win: [...] }, generator?, smoke?: ["--help"], sha256?, dest?, clean?, platforms? }`. It fetches exactly that commit into the fixed `.right-release/cmake/<name>/src` (`git init`, `fetch --depth 1 origin <commit>`, detach; a checkout already at the commit is reused, one with local edits or at another commit is refused with `STAGE_SIDECAR_SOURCE_MISMATCH`), configures and builds `target` in `.right-release/cmake/<name>/build` (`CMAKE_OSX_ARCHITECTURES` is set from the target; `universal-apple-darwin` builds `arm64;x86_64` and checks `lipo -archs`), finds the executable (`output`, then `Release/` layouts, `.exe` on Windows), optionally pins its sha256, stages it as `<name>-<triple>[.exe]` and smoke-runs it when the target can run on this host. `cmakeArgs` and `platformArgs` accept only `-DNAME=value` definitions.
|
|
382
|
+
- **`prebuilt` (#490):** verifies a host-built binary already staged at `<stageDir>/<name>-<triple>` (or copied from `from`) against an app manifest, never building or downloading: `manifest: { file, entry: "binaries.{platform}", sha256?: "sha256", approvalSha256?: "approval_sha256", approvalAlgorithm?: "approval_hash_algorithm" }` (`{platform}` is `macos-aarch64`, `windows-x86_64`, ...). The raw sha256 must match; when an approval hash is named it must match too, with `raw-sha256` or `macho-codesign-strip-linkedit-v1` (`macho-approval-hash.mjs`: the sha256 of a thin Mach-O with its code signature removed and `__LINKEDIT` normalised, so a signed and an unsigned copy of one build agree; macOS only, `codesign --remove-signature` on a private copy). A missing file, entry or digest fails closed.
|
|
383
|
+
- **`remote` archives (#491):** `extract: "<member file name>"` (with `memberSha256?`, `executable?`) unpacks one member of a zip or tgz after the archive digest passes, so a Windows zip needs no `command` entry.
|
|
384
|
+
- **`ffmpeg` mixed variants, fetch and probes (#491):** `variants: { ffmpeg: "export-ass" }` lets one entry take the export-ass ffmpeg with the decode ffprobe (a tool not named uses `variant`). `sources: { ffmpeg: { url, sha256, member }, ffprobe: {...} }` downloads the bytes the catalog only pins: the archive is digest-checked, the member is unpacked and its sha256 must equal the APPROVED catalog row's content digest, so a swapped archive can never stage. `probe: true` runs `-version` and `-encoders` on a staged binary this host can run: the configuration must say `--disable-gpl` and `--disable-nonfree` and must not enable GPL, nonfree, libx264, libx265, libxvid or libfdk-aac, and ffmpeg may not list those encoders (`STAGE_SIDECAR_FFMPEG_PROBE_FAILED`; another architecture or OS is recorded as `skipped-cross-target`). The catalog has no Windows `export-ass` row yet (only `export-ass-zimg`); that row belongs to the rightkit-ffmpeg crate.
|
|
385
|
+
- **`universal-apple-darwin` for `ffmpeg` and `onnxruntime` (#492):** each arch row is staged thin under `.right-release/slices/<triple>/` and the files are lipo-merged into `<stageDir>/<tool>-universal-apple-darwin` (onnxruntime: into `resourceDir`), requiring both archs and every row approved (onnxruntime also accepts `pinned`). For `ffmpeg`, `{arch}`, `{triple}` and `{stageDir}` in the `stage` command's arguments name the slice (it runs once per slice), `sources` need the per-arch form `{ ffmpeg: { aarch64: {...}, x86_64: {...} } }`, one source offer lists both slices, and the receipt rows carry `slices`. A merged file identical to the staged one is not rewritten.
|
|
386
|
+
|
|
387
|
+
## Declared Rust audit, dist-scan and the Windows Swift toolchain
|
|
388
|
+
|
|
389
|
+
- `right-release deps --audit` also runs `cargo audit` in every folder named by `deps.cargoWorkdirs` (app-relative, each must hold a `Cargo.lock`), through the managed cargo client (the RightKit broker on managed hosts; there is no bare-cargo fallback on a managed Mac), and fails when any workdir fails or cannot start. An app with no `deps.cargoWorkdirs` audits no Rust. Orthic-Labs/rightkit#674.
|
|
390
|
+
- `right-release dist-scan --dist <dir> --forbid <string>... [--forbid-file <path>] [--forbid-name <regex>]... [--ext js,css,...]` fails a built web `dist/` that carries a forbidden marker string in a text file, or a chunk named `devSource-*`, `mockDataSource-*`, `fixtures-*` or matching `--forbid-name` (exit 1; exit 2 on a usage error, a missing `dist/` or a run with no markers). It replaces per-app copies of a release-bundle checker. Orthic-Labs/rightkit#484.
|
|
391
|
+
- `right-release toolchain swift --version 6.4.0 --sha256 <pin> [--github-env] [--json]` (Windows) downloads the swift.org installer, refuses unless its sha256 equals the pin (before anything runs; a bad download is deleted), skips the install when that Swift version is already on PATH, otherwise runs it silently (`/quiet /norestart`, exit 0 and 3010 pass), checks `swift --version`, finds the runtime bin (`Runtimes\<ver>\usr\bin` with `swiftCore.dll`) and, with `--github-env`, appends only the variables the installer added or changed (names printed, values never) to `GITHUB_ENV`, and the new PATH entries, the swift directory and the runtime bin to `GITHUB_PATH`. Orthic-Labs/rightkit#436 (the `@rightkit/ci` `swift-toolchain` step calls this and lives in the ci package).
|
|
392
|
+
|
|
295
393
|
## Keeping agent writes off the internal drive (`disk agent-env`, `disk probe`)
|
|
296
394
|
|
|
297
395
|
No agent may write to the internal drive (C: on Windows, the internal disk on macOS); only installed apps go there.
|
|
@@ -320,3 +418,88 @@ No agent may write to the internal drive (C: on Windows, the internal disk on ma
|
|
|
320
418
|
- `smol-toml` is pinned exactly. A consumer that depends on a different `smol-toml` version gets a second copy.
|
|
321
419
|
- Apps pin this package at different versions; `docs/ADOPTION.md` lists each pin and marks the stale ones.
|
|
322
420
|
- Prepublish runs the test suite, the Windows signer integration test and the registry parity check.
|
|
421
|
+
|
|
422
|
+
## Post-sign hook chains
|
|
423
|
+
|
|
424
|
+
`target.postSign` accepts either one command object or an array of command objects. An array runs in order, for example `create-mac-updater` followed by `lsclean`, and stops at the first failing command, so the release fails closed. Every command in the array is scanned for forbidden build features and release switches, exactly like a single command. A single object behaves as before.
|
|
425
|
+
|
|
426
|
+
Each command may set `timeoutMs`. When it expires the release kills the whole process tree (`taskkill /T /F` on Windows, the process group elsewhere) and exits non-zero, so a hung child such as Tauri leaves no orphan.
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
## App-specific forbidden release crates (G14)
|
|
430
|
+
|
|
431
|
+
Declare exact Cargo package names in `right-release.config.mjs`:
|
|
432
|
+
|
|
433
|
+
```js
|
|
434
|
+
export default {
|
|
435
|
+
// Existing app, version & distribution fields...
|
|
436
|
+
releaseDependencies: {
|
|
437
|
+
manifest: "src-tauri/Cargo.toml",
|
|
438
|
+
forbiddenCrates: [
|
|
439
|
+
"rightkit-record", "rightkit-automation", "rightkit-hotkeys",
|
|
440
|
+
"rightkit-overlay", "rightkit-agent-tools", "rightkit-qa", "rightkit-qa-driver",
|
|
441
|
+
],
|
|
442
|
+
// Optional extra features for build flags hidden inside opaque scripts:
|
|
443
|
+
features: [],
|
|
444
|
+
// Set true only when every release command disables default features:
|
|
445
|
+
noDefaultFeatures: false,
|
|
446
|
+
},
|
|
447
|
+
targets: {
|
|
448
|
+
mac: { cargoTarget: "aarch64-apple-darwin", /* existing packaging config */ },
|
|
449
|
+
win: { cargoTarget: "x86_64-pc-windows-msvc", /* existing packaging config */ },
|
|
450
|
+
},
|
|
451
|
+
};
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
`right-release build` enforces this policy before packaging. `--skip-checks` skips only app scripts;
|
|
455
|
+
this gate also blocks dry runs when Cargo cannot resolve dependencies. Development builds do not run it.
|
|
456
|
+
Without `releaseDependencies`, existing apps retain their current behavior. An explicit empty
|
|
457
|
+
`forbiddenCrates: []` still verifies that Cargo can resolve its graph.
|
|
458
|
+
|
|
459
|
+
Standalone checks, run from app root:
|
|
460
|
+
|
|
461
|
+
```sh
|
|
462
|
+
pnpm exec right-release check-release-dependencies
|
|
463
|
+
pnpm exec right-release check-release-dependencies --platform win
|
|
464
|
+
pnpm exec right-release check-release-dependencies --all-targets
|
|
465
|
+
pnpm exec right-release check-release-dependencies --config path/to/right-release.config.mjs
|
|
466
|
+
```
|
|
467
|
+
|
|
468
|
+
Default selection uses host platform's release commands & host architecture's triple. `--all-targets` checks every configured
|
|
469
|
+
release target; non-host targets need `cargoTarget`, `targetTriple` or `rustTarget`. Each target must be
|
|
470
|
+
registered with managed Cargo. Unknown hosts fail unless an explicit platform or all targets is selected.
|
|
471
|
+
|
|
472
|
+
Graphs use `cargo tree --locked -e normal --prefix none --target <triple>` through normal Cargo dispatch.
|
|
473
|
+
Normal direct & transitive dependencies count; build-only & dev-only edges do not. Names match exactly,
|
|
474
|
+
including renamed dependencies' actual package names; versions, sources & duplicate tree entries do not
|
|
475
|
+
change matching. Checks never read Cargo.lock as evidence of reachability.
|
|
476
|
+
|
|
477
|
+
Each distinct command feature set is resolved separately. Commands, recipe steps & package scripts
|
|
478
|
+
(up to three nested script calls) contribute `--features`, `-F`, `-f`, `--all-features` &
|
|
479
|
+
`--no-default-features`; `releaseSwitches.features` & `releaseDependencies.features` add explicit features.
|
|
480
|
+
Declare flags hidden inside shell/Node script files in config. Manifest precedence is
|
|
481
|
+
`releaseDependencies.manifest`, target `cargoManifest`, `releaseSwitches.manifest`, native assembly
|
|
482
|
+
manifest, then `src-tauri/Cargo.toml` or `Cargo.toml`. Declared missing manifests fail.
|
|
483
|
+
|
|
484
|
+
SDK usage:
|
|
485
|
+
|
|
486
|
+
```js
|
|
487
|
+
import { assertReleaseDependencies, forbiddenCrateFindings } from "@rightkit/release/release-dependencies.mjs";
|
|
488
|
+
|
|
489
|
+
const evidence = assertReleaseDependencies({ root: appRoot, config, target: config.targets.mac });
|
|
490
|
+
// { manifest, target, trees, forbiddenCrates }; throws with crate & target on refusal.
|
|
491
|
+
const names = forbiddenCrateFindings(treeText, config.releaseDependencies.forbiddenCrates);
|
|
492
|
+
```
|
|
493
|
+
|
|
494
|
+
`run(command, args, options)` can inject Cargo execution for tests. Standalone CLI rejects missing policy,
|
|
495
|
+
invalid arguments, failed resolution & forbidden dependencies with nonzero exit status. Demo capability
|
|
496
|
+
files, base Tauri config content & app-specific default-feature restrictions remain separate guards.
|
|
497
|
+
|
|
498
|
+
|
|
499
|
+
## Template rename, PE digest, icon packing
|
|
500
|
+
|
|
501
|
+
`right-release init --name <Product Name> --id <reverse.dns.bundle.id> [--slug <lowercase>] [--dir <root>] [--dry-run]` renames the desktop app template identity in place (see `init-command.mjs`); it runs no gate or build.
|
|
502
|
+
|
|
503
|
+
`right-release pe digest <file> [--algorithm sha256|sha384|sha512] [--json]` prints the signature-independent PE content digest (Authenticode image hash: CheckSum, certificate table entry and certificate data excluded, so signing, re-signing and timestamping do not change it). `right-release pe imports <file> [--allow dll,...] [--allow-file <path>] [--json]` lists regular and delay-load imports and exits 1 when any is outside the allow list (case-insensitive; a trailing `*` allows a prefix such as `api-ms-win-*`; no allow list refuses every import). SDK: `peContentDigest(bytes)` and `verifyImportClosure(bytes, allowedDlls)` in `pe-content.mjs`.
|
|
504
|
+
|
|
505
|
+
`right-release icons ico|icns --out <file> --frame <PNG>... [--dir <folder>]` packs per-size PNG frames, hand-tuned small ones included, into an ICO (sizes 1..256, PNG-compressed entries) or an ICNS (16, 32, 64, 128, 256, 512, 1024). Frame sizes come from each PNG's IHDR; non-square, duplicate or unsupported sizes are refused. Rendering the PNG frames from a design master stays with the app's design source; the DMG background generator is `generate-dmg-background.py`.
|
package/bin/darwin/rightkit-swap
CHANGED
|
Binary file
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
|
-
"sha256": "
|
|
3
|
+
"sha256": "cbd04b2fa9af4c416c9d4983e2c63097dc0ca5f987d15eb272123df2de3d22cd",
|
|
4
4
|
"identity": "Developer ID Application: Adrian D'souza (6KLGD3LLKF)",
|
|
5
5
|
"codesign": "strict-valid"
|
|
6
6
|
}
|
package/cli/right-release.mjs
CHANGED
|
@@ -22,9 +22,17 @@ const commands = new Map([
|
|
|
22
22
|
["sign-candidate", "sign-candidate.mjs"],
|
|
23
23
|
["engine-locate", "engine-locator.mjs"],
|
|
24
24
|
["stage-sidecar", "stage-sidecar-command.mjs"],
|
|
25
|
+
["init", "init-command.mjs"],
|
|
26
|
+
["pe", "pe-content.mjs"],
|
|
27
|
+
["icons", "icon-pack.mjs"],
|
|
28
|
+
["recipe-build", "recipe-build.mjs"],
|
|
25
29
|
["signing-agent", "signing-agent-command.mjs"],
|
|
26
30
|
["create-mac-updater", "create-mac-updater.mjs"],
|
|
27
31
|
["mirror-root-artifact", "mirror-root-artifact.mjs"],
|
|
32
|
+
["test", "test-command.mjs"],
|
|
33
|
+
["web-build", "web-build.mjs"],
|
|
34
|
+
["license-notices", "npm-license-notices.mjs"],
|
|
35
|
+
["toolchain", "toolchain-swift.mjs"],
|
|
28
36
|
["github", "github-release.mjs"],
|
|
29
37
|
["cargo-target", "cargo-target.mjs"],
|
|
30
38
|
["qa", "release-qa-command.mjs"],
|
|
@@ -34,6 +42,8 @@ const commands = new Map([
|
|
|
34
42
|
["ownership-mandatory", "ownership-mandatory-set.mjs"],
|
|
35
43
|
["swift-package", "swift-package-command.mjs"],
|
|
36
44
|
["bundle-scan", "runtime-bundle-scan.mjs"],
|
|
45
|
+
["check-release-dependencies", "release-dependencies-command.mjs"],
|
|
46
|
+
["dist-scan", "dist-scan.mjs"],
|
|
37
47
|
["identity-scan", "packaged-app-identity.mjs"],
|
|
38
48
|
["apple-profile-create", "apple-profile-create.mjs"],
|
|
39
49
|
]);
|
|
@@ -46,7 +56,7 @@ if (first === "dev") {
|
|
|
46
56
|
process.exitCode = 1;
|
|
47
57
|
});
|
|
48
58
|
} else {
|
|
49
|
-
const githubBuildCommands = new Set(["release", "build", "package", "swift-package", "sign-windows", "sign-updater", "create-mac-updater", "cargo-target"]);
|
|
59
|
+
const githubBuildCommands = new Set(["release", "build", "package", "swift-package", "sign-windows", "sign-updater", "create-mac-updater", "cargo-target", "web-build", "test"]);
|
|
50
60
|
|
|
51
61
|
try {
|
|
52
62
|
if (githubBuildCommands.has(first)
|
|
@@ -123,6 +133,8 @@ if (first === "--version" || first === "-v") {
|
|
|
123
133
|
runBinary("bash", [path.join(packageRoot, "lsclean.sh"), ...args.slice(1)]);
|
|
124
134
|
} else if (first === "generate-dmg-background") {
|
|
125
135
|
runBinary("python3", [path.join(packageRoot, "generate-dmg-background.py"), ...args.slice(1)]);
|
|
136
|
+
} else if (first === "build" && args.includes("--web")) {
|
|
137
|
+
run("web-build.mjs", args.slice(1).filter((arg) => arg !== "--web"));
|
|
126
138
|
} else if (commands.has(first)) {
|
|
127
139
|
run(commands.get(first), args.slice(1));
|
|
128
140
|
} else if (first.startsWith("-")) {
|
|
@@ -182,24 +194,38 @@ function printHelp() {
|
|
|
182
194
|
console.log(`right-release <command> [options]
|
|
183
195
|
|
|
184
196
|
Commands:
|
|
197
|
+
pe digest <file> [--algorithm sha256|sha384|sha512] [--json] | pe imports <file> [--allow dll,...] [--allow-file <path>] Signature-independent PE content digest; import-closure check
|
|
198
|
+
icons ico|icns --out <file> --frame <PNG>... [--dir <folder>] Pack hand-tuned per-size PNG frames into an ICO or ICNS
|
|
199
|
+
init --name <Product Name> --id <reverse.dns.bundle.id> [--slug <lowercase>] [--dir <root>] [--dry-run] Rename the desktop app template into a real app, in place
|
|
185
200
|
stage-sidecar [--config <file>] [--target <triple>] [--json] Stage declarative sidecars into src-tauri/binaries (Tauri externalBin naming)
|
|
201
|
+
recipe-build --plan <json> | --plan-file <path> [--app-root <dir>] Run a package recipe: prepare steps, the Tauri build, postBuild steps (argv, no shell, fail closed)
|
|
186
202
|
dmg build --app <.app> --out <dmg> --volname <name> [--background <png>] [--spec <appdmg.json>]
|
|
187
203
|
[--icon-size N] [--app-position x,y] [--applications-position x,y] [--window w,h] [--window-position x,y]
|
|
188
|
-
|
|
204
|
+
[--sign [--identifier <codesign-identifier>] [--receipt <path>]]
|
|
205
|
+
Build branded UDZO image without Finder; --sign then Developer ID signs the image (secure timestamp, no hardened runtime; never notarizes)
|
|
189
206
|
install-app --dmg <path.dmg> [--app-name Pulse.app] [--bundle-id <id>] [--dest /Applications]
|
|
190
207
|
[--relaunch|--no-relaunch] [--json]
|
|
191
208
|
Verify a signed DMG, quit, replace & relaunch hidden; no UI, no osascript (macOS)
|
|
192
|
-
|
|
193
|
-
install-app --installer <Setup.exe> --dest <fixed install folder> --exe <App.exe> [--json]
|
|
194
|
-
Windows: run the installer in place at the fixed folder,
|
|
209
|
+
A release install keeps the replaced app beside the fixed path as <App>.app.previous; --dev keeps NO old copy
|
|
210
|
+
install-app --installer <Setup.exe> --dest <fixed install folder> --exe <App.exe> [--payload <file>]... [--payload-manifest <json>] [--json]
|
|
211
|
+
Windows: run the installer in place at the fixed folder; payload-only rollback (never the whole folder, user data beside the app is never copied)
|
|
195
212
|
build [--platform mac|win] [--skip-checks] [--resume-from <phase>]
|
|
196
213
|
Resume macOS at prepackage, sign, package, post-sign, or harden
|
|
214
|
+
build --web [--config <file>] [--dry-run] Web frontend build only (config web.build steps); no Tauri, signing or packaging (alias: web-build)
|
|
215
|
+
test [--config <file>] [--suite <name>[,<name>]]... [--runner <name>] [--list] [--json] [-- <args>]
|
|
216
|
+
Run the app's declared test suites (config tests.suites / tests.runners) in order; stops at the first failure
|
|
217
|
+
license-notices npm [--config <file>] [--output <file>] [--product-name <name>] [--check]
|
|
218
|
+
Write the third-party npm license appendix HTML from pnpm licenses (config legal.npmNotices); --check only compares
|
|
219
|
+
toolchain swift --version <x.y.z> --sha256 <pin> [--url <https url>] [--github-env] [--json]
|
|
220
|
+
Windows: install the pinned swift.org Swift toolchain (sha256 verified before it runs), check swift --version, find the runtime bin; --github-env exports what the installer changed to GITHUB_ENV / GITHUB_PATH
|
|
197
221
|
dev --platform win|mac [--config <path>] [--dry-run] [--build-only]
|
|
198
222
|
Incremental unsigned build & optional local install
|
|
199
223
|
swift-package <windows-stage|msix|mac-app> --config <swift-package.json> [--dry-run] [--no-sign] [--sign] [--notarize] [--profile <name>]
|
|
200
224
|
Stage SwiftPM app; build+sign MSIX (Windows) or assemble+sign+notarize .app (macOS); never uploads
|
|
201
225
|
bundle-scan [--allow-name <basename>]... <bundle-root>...
|
|
202
226
|
Fail when a bundle carries Node/Python runtimes or script sources
|
|
227
|
+
dist-scan --dist <dir> --forbid <string>... [--forbid-file <path>] [--forbid-name <regex>]... [--ext js,css,...]
|
|
228
|
+
Fail when a built web dist/ carries a forbidden marker string or mock/fixture chunk name (dev, mock or fixture code in release)
|
|
203
229
|
sign-dev-artifact --artifact <dir> --manifest <sign.json> [--metadata <payload.json>] [--evidence <path>] [--dry-run]
|
|
204
230
|
Developer ID sign an existing CI dev artifact per file with fixed identifiers; never builds, notarizes or publishes (macOS)
|
|
205
231
|
sign-candidate --platform win|mac --artifact <dir> --candidate-receipt <receipt.json> --out <binding.json> [--manifest <sign.json>] [--publisher <subject>] [--dry-run]
|
|
@@ -216,6 +242,7 @@ Commands:
|
|
|
216
242
|
Upload sealed bytes to R2 & register RightApps
|
|
217
243
|
release Compatibility alias for build
|
|
218
244
|
publish Compatibility alias for upload
|
|
245
|
+
check-release-dependencies [--platform mac|win|linux | --all-targets] Check app-forbidden release crates
|
|
219
246
|
publish npm --package @rightkit/<name> [--dry-run] Gate preflight for npm publish
|
|
220
247
|
publish npm --package @rightkit/<name> --publish --tarball <gated.tgz>
|
|
221
248
|
Publish the gated tarball with the Keychain token
|
|
@@ -266,7 +293,8 @@ Commands:
|
|
|
266
293
|
lsclean <AppName.app> Clear macOS LaunchServices duplicates
|
|
267
294
|
generate-dmg-background <options> Generate the branded multi-resolution DMG background
|
|
268
295
|
mirror-root-artifact --file <path> --package-root <dir>
|
|
269
|
-
|
|
296
|
+
mirror-root-artifact --config <file> --platform mac|win [--package-root <dir>]
|
|
297
|
+
Persist artifact(s) in RIGHT_RELEASE_ARTIFACT_DIR (config form: targets.<platform>.mirror entries, optional pruneSuffix)
|
|
270
298
|
github --release <id> --tier patch|update --platform mac|win [--config <file>] [--repo owner/repo] [--dry-run]
|
|
271
299
|
Same checks and publisher as upload (explicit tier, github-releases provider + configured repository)
|
|
272
300
|
github --addon <config> --platform mac|win [--repo owner/repo] [--dry-run]
|
package/command-spec.mjs
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// Shared argv command shape for the declarative `tests` and `web` config sections (`right-release test`, `web-build`).
|
|
2
|
+
// A step is one command run without a shell:
|
|
3
|
+
// { cmd, args?, cwd?, env? } any executable ("node" means this Node binary)
|
|
4
|
+
// { node, args?, nodeFlags?, nodeOptions? } a Node script path inside the app, run by this Node binary
|
|
5
|
+
// `cwd` and `node` are app-relative and may not leave the app. `nodeFlags` go before the script, `nodeOptions` are merged
|
|
6
|
+
// (de-duplicated) into NODE_OPTIONS for the child. Unknown keys are refused so a typo cannot silently change a run.
|
|
7
|
+
import path from "node:path";
|
|
8
|
+
import { pathToFileURL } from "node:url";
|
|
9
|
+
|
|
10
|
+
/** Load an app's `right-release.config.mjs`; the app root is the config file's directory. */
|
|
11
|
+
export async function loadAppConfig(configFile = "right-release.config.mjs") {
|
|
12
|
+
const configPath = path.resolve(configFile);
|
|
13
|
+
const loaded = await import(pathToFileURL(configPath).href);
|
|
14
|
+
return { config: loaded.default ?? loaded.config ?? loaded, appRoot: path.dirname(configPath), configPath };
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export class CommandSpecError extends Error {
|
|
18
|
+
constructor(message) { super(message); this.name = "CommandSpecError"; this.code = "RK_COMMAND_SPEC"; }
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const fail = (message) => { throw new CommandSpecError(message); };
|
|
22
|
+
const isObject = (value) => value !== null && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
export const STEP_KEYS = ["cmd", "node", "args", "cwd", "env", "nodeFlags", "nodeOptions"];
|
|
24
|
+
|
|
25
|
+
export function relativeInside(value, label) {
|
|
26
|
+
if (typeof value !== "string" || !value.trim()) fail(`${label} must be a non-empty relative path`);
|
|
27
|
+
const normalized = value.replaceAll("\\", "/");
|
|
28
|
+
if (path.isAbsolute(value) || /^[A-Za-z]:/.test(value) || normalized.split("/").includes("..")) fail(`${label} must be relative and stay inside the app: ${value}`);
|
|
29
|
+
return normalized;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const stringList = (value, label) => {
|
|
33
|
+
if (!Array.isArray(value) || value.some((item) => typeof item !== "string")) fail(`${label} must be a list of strings`);
|
|
34
|
+
return [...value];
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/** Validate one step; `extraKeys` are accepted and left to the caller. Returns the step. */
|
|
38
|
+
export function validateStep(step, label, extraKeys = []) {
|
|
39
|
+
if (!isObject(step)) fail(`${label} must be an object`);
|
|
40
|
+
const allowed = new Set([...STEP_KEYS, ...extraKeys]);
|
|
41
|
+
const unknown = Object.keys(step).filter((key) => !allowed.has(key));
|
|
42
|
+
if (unknown.length) fail(`${label} has unknown key(s): ${unknown.join(", ")} (allowed: ${[...allowed].join(", ")})`);
|
|
43
|
+
if (step.cmd !== undefined && step.node !== undefined) fail(`${label} sets both cmd and node; use one`);
|
|
44
|
+
if (step.cmd !== undefined && (typeof step.cmd !== "string" || !step.cmd.trim())) fail(`${label}.cmd must be a non-empty string`);
|
|
45
|
+
if (step.node !== undefined) relativeInside(step.node, `${label}.node`);
|
|
46
|
+
if (step.cwd !== undefined) relativeInside(step.cwd, `${label}.cwd`);
|
|
47
|
+
if (step.args !== undefined) stringList(step.args, `${label}.args`);
|
|
48
|
+
if (step.nodeFlags !== undefined) stringList(step.nodeFlags, `${label}.nodeFlags`);
|
|
49
|
+
if (step.nodeOptions !== undefined) stringList(step.nodeOptions, `${label}.nodeOptions`);
|
|
50
|
+
if (step.env !== undefined && (!isObject(step.env) || Object.values(step.env).some((value) => typeof value !== "string"))) fail(`${label}.env must be an object of strings`);
|
|
51
|
+
return step;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Add flags to a NODE_OPTIONS string without repeating one already present. */
|
|
55
|
+
export function mergeNodeOptions(existing, flags) {
|
|
56
|
+
const present = (existing ?? "").trim().split(/\s+/).filter(Boolean);
|
|
57
|
+
for (const flag of flags) if (!present.includes(flag)) present.push(flag);
|
|
58
|
+
return present.join(" ");
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Resolve a validated step to what to spawn: `{ cmd, args, cwd, env }`. `extraArgs` are appended after the step's own
|
|
63
|
+
* arguments (the `--` passthrough of `right-release test`). `cmd: "node"` and `node:` use `process.execPath`.
|
|
64
|
+
*/
|
|
65
|
+
export function resolveInvocation(step, { appRoot, baseEnv = process.env, extraArgs = [] }) {
|
|
66
|
+
const args = [...(step.args ?? []), ...extraArgs];
|
|
67
|
+
let cmd;
|
|
68
|
+
let argv;
|
|
69
|
+
if (step.node !== undefined) {
|
|
70
|
+
cmd = process.execPath;
|
|
71
|
+
argv = [...(step.nodeFlags ?? []), path.resolve(appRoot, step.node), ...args];
|
|
72
|
+
} else {
|
|
73
|
+
if (typeof step.cmd !== "string" || !step.cmd.trim()) fail("a step needs cmd or node");
|
|
74
|
+
cmd = step.cmd === "node" ? process.execPath : step.cmd;
|
|
75
|
+
argv = [...(step.cmd === "node" ? (step.nodeFlags ?? []) : []), ...args];
|
|
76
|
+
}
|
|
77
|
+
const env = { ...baseEnv, ...(step.env ?? {}) };
|
|
78
|
+
if (step.nodeOptions?.length) env.NODE_OPTIONS = mergeNodeOptions(env.NODE_OPTIONS, step.nodeOptions);
|
|
79
|
+
return { cmd, args: argv, cwd: path.resolve(appRoot, step.cwd ?? "."), env };
|
|
80
|
+
}
|
package/deps-cargo.mjs
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
// `right-release deps --audit` for Cargo workdirs (Orthic-Labs/rightkit#674): replaces an app's own audit-rust script.
|
|
2
|
+
//
|
|
3
|
+
// deps: { workdirs: ["."], cargoWorkdirs: ["src-tauri"] } // app-relative folders that hold a Cargo.lock
|
|
4
|
+
//
|
|
5
|
+
// Declared only: an app with no `deps.cargoWorkdirs` audits no Rust, exactly as before. Each folder must be inside the app and
|
|
6
|
+
// hold a `Cargo.lock` (an unlocked workspace cannot be audited, so it is refused, never skipped). `cargo audit` runs through
|
|
7
|
+
// the managed cargo client (`runCargoSync`): on a managed host it goes through the RightKit broker, and a host with no
|
|
8
|
+
// broker or no `cargo-audit` fails closed with the reason; there is never a bare-cargo fallback on a managed Mac.
|
|
9
|
+
import { existsSync } from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import { runCargoSync } from "./managed-cargo.mjs";
|
|
12
|
+
|
|
13
|
+
const isInside = (root, target) => {
|
|
14
|
+
const relative = path.relative(root, target);
|
|
15
|
+
return relative === "" || (!relative.startsWith("..") && !path.isAbsolute(relative));
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** The validated Cargo workdirs `[{ rel, cwd }]` of a release config (empty when none are declared). Throws on a bad declaration. */
|
|
19
|
+
export function cargoAuditWorkdirs(config, root, { exists = existsSync } = {}) {
|
|
20
|
+
const declared = config?.deps?.cargoWorkdirs;
|
|
21
|
+
if (declared === undefined) return [];
|
|
22
|
+
if (!Array.isArray(declared) || declared.some((item) => typeof item !== "string" || !item.trim())) {
|
|
23
|
+
throw new Error("deps.cargoWorkdirs must be a list of app-relative folders");
|
|
24
|
+
}
|
|
25
|
+
const seen = new Set();
|
|
26
|
+
const workdirs = [];
|
|
27
|
+
for (const rel of declared) {
|
|
28
|
+
const normalized = rel.replaceAll("\\", "/");
|
|
29
|
+
if (path.isAbsolute(rel) || /^[A-Za-z]:/.test(rel) || normalized.split("/").includes("..")) {
|
|
30
|
+
throw new Error(`deps.cargoWorkdirs entry must be relative and stay inside the app: ${rel}`);
|
|
31
|
+
}
|
|
32
|
+
const cwd = path.resolve(root, normalized);
|
|
33
|
+
if (!isInside(root, cwd)) throw new Error(`deps.cargoWorkdirs entry must stay inside the app: ${rel}`);
|
|
34
|
+
if (seen.has(cwd)) continue;
|
|
35
|
+
seen.add(cwd);
|
|
36
|
+
if (!exists(path.join(cwd, "Cargo.lock"))) throw new Error(`deps.cargoWorkdirs entry ${rel} has no Cargo.lock (${path.join(cwd, "Cargo.lock")}); an unlocked workspace cannot be audited`);
|
|
37
|
+
workdirs.push({ rel, cwd });
|
|
38
|
+
}
|
|
39
|
+
return workdirs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Run `cargo audit` in each workdir and collect failures so every workdir reports. `run(args, options)` is the managed
|
|
44
|
+
* cargo runner (injectable). Returns `{ failures: string[] }`: one entry per workdir that could not start or exited non-zero.
|
|
45
|
+
*/
|
|
46
|
+
export function runCargoAudits(workdirs, { run = runCargoSync, log = console.log, env = process.env } = {}) {
|
|
47
|
+
const failures = [];
|
|
48
|
+
for (const { rel, cwd } of workdirs) {
|
|
49
|
+
log(`\n> cargo audit (${cwd})`);
|
|
50
|
+
const result = run(["audit"], { cwd, env, stdio: "inherit" });
|
|
51
|
+
if (result?.error) failures.push(`${rel}: cargo audit could not start (${result.error.message})`);
|
|
52
|
+
else if (result?.status !== 0) failures.push(`${rel}: cargo audit exited ${result?.status ?? "without status"}`);
|
|
53
|
+
}
|
|
54
|
+
return { failures };
|
|
55
|
+
}
|
package/deps.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import path from "node:path";
|
|
|
3
3
|
import { pathToFileURL } from "node:url";
|
|
4
4
|
import { spawnSync } from "./background-process.mjs";
|
|
5
5
|
import { existsSync } from "node:fs";
|
|
6
|
+
import { cargoAuditWorkdirs, runCargoAudits } from "./deps-cargo.mjs";
|
|
6
7
|
|
|
7
8
|
const args = process.argv.slice(2);
|
|
8
9
|
const opts = {
|
|
@@ -55,6 +56,12 @@ for (const { rel, cwd } of resolvedWorkdirs) {
|
|
|
55
56
|
run("pnpm", ["install", ...workspaceArgs, "--lockfile-only", "--ignore-scripts"], cwd, { ignoreScripts: true });
|
|
56
57
|
}
|
|
57
58
|
}
|
|
59
|
+
if (opts.mode === "audit") {
|
|
60
|
+
// Rust advisories (#674): every deps.cargoWorkdirs folder, through the managed cargo client; declared only.
|
|
61
|
+
let cargoWorkdirs;
|
|
62
|
+
try { cargoWorkdirs = cargoAuditWorkdirs(config, root); } catch (error) { fail(error.message); }
|
|
63
|
+
failures.push(...runCargoAudits(cargoWorkdirs).failures);
|
|
64
|
+
}
|
|
58
65
|
if (failures.length > 0) {
|
|
59
66
|
for (const failure of failures) console.error(`right-deps: ${failure}`);
|
|
60
67
|
fail(`${failures.length} dependency workdir(s) failed`);
|