@stim-cli/ci 1.35.0 → 1.37.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 +98 -63
- package/dist/{cli-BujO5iDm.mjs → cli-BCFk6LoD.mjs} +29 -13
- package/dist/index.d.mts +19 -14
- package/dist/index.mjs +1 -1
- package/dist/{src-q52NuL5J.mjs → src-CFd6F73i.mjs} +45 -30
- package/dist/stim-ci.mjs +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
# @stim-cli/ci
|
|
2
2
|
|
|
3
|
-
Build and launch
|
|
4
|
-
and stop the workspace. The library imports the public API from `stim`;
|
|
5
|
-
not depend on GitHub Actions, EAS, or internal Stim modules.
|
|
3
|
+
Build an app once, then install and launch it through Stim, run your test
|
|
4
|
+
command and stop the workspace. The library imports the public API from `stim`;
|
|
5
|
+
it does not depend on GitHub Actions, EAS, or internal Stim modules.
|
|
6
6
|
|
|
7
7
|
Requires Node.js 22.12 or newer and the native tools for the selected platform.
|
|
8
8
|
Install the app's dependencies before running it. Each concurrent CI run needs
|
|
@@ -10,50 +10,78 @@ its own checkout. The run stops that checkout's workspace, including resources
|
|
|
10
10
|
started before a build failure; an explicit API `run.slot` limits cleanup to
|
|
11
11
|
that slot. Do not point CI at a developer's active checkout.
|
|
12
12
|
|
|
13
|
-
## Build
|
|
13
|
+
## Build, then run
|
|
14
|
+
|
|
15
|
+
`stim-ci build` is the only step that compiles. `stim-ci run` never compiles:
|
|
14
16
|
|
|
15
17
|
```sh
|
|
16
|
-
npx --yes --package @stim-cli/ci stim-ci build --platform ios --project ./app
|
|
18
|
+
npx --yes --package @stim-cli/ci stim-ci build --platform ios --project ./app
|
|
19
|
+
npx --yes --package @stim-cli/ci stim-ci run --platform ios --project ./app -- pnpm test:e2e
|
|
17
20
|
```
|
|
18
21
|
|
|
19
|
-
|
|
20
|
-
Metro server, or app
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
22
|
+
`build` compiles or restores the app from the native artifact cache, without
|
|
23
|
+
starting a simulator, emulator, Metro server, or app, and without stopping an
|
|
24
|
+
existing workspace session. It records the resulting app in the checkout's Stim
|
|
25
|
+
state (the workspace state under the Stim home) for the platform and selectors:
|
|
26
|
+
iOS scheme and configuration, Android variant. A later `build` with the same
|
|
27
|
+
selectors replaces the record. Architecture and ABI are build-only; the last build
|
|
28
|
+
for the selectors wins whatever its architecture.
|
|
29
|
+
|
|
30
|
+
`run` installs and launches the recorded app exactly as built, then runs the
|
|
31
|
+
test. It does not fingerprint, consult the cache, build or replace the app's JS:
|
|
32
|
+
it trusts the recorded build, so rebuild after changing the project. A Debug app
|
|
33
|
+
loads its JS from Metro, which `run` starts as before. With no recorded build for
|
|
34
|
+
the selectors (or after its app was removed), `run` refuses before touching a
|
|
35
|
+
device with `STIM_NO_RECORDED_BUILD`, and the failure's `remedy` names the command
|
|
36
|
+
to run first, such as `stim-ci build --platform ios --scheme App`. `build` and
|
|
37
|
+
`run` must use the same Stim home: pass the same `--home`, or keep the default.
|
|
38
|
+
|
|
39
|
+
Every iOS and Android `run` on this machine starts the app from clean state on
|
|
40
|
+
the workspace's own simulator or emulator, including when the device already
|
|
41
|
+
has this build.
|
|
42
|
+
Before it installs, `run` uninstalls every other user app except XCUITest
|
|
43
|
+
runner apps, such as the ones agent-device installs. On iOS it also clears the
|
|
44
|
+
app's defaults and data container and resets the simulator keychain; on Android
|
|
45
|
+
it runs `pm clear` on the app. Simulator-wide settings such as privacy grants
|
|
46
|
+
are kept. A local simulator or emulator Stim does not own refuses with
|
|
47
|
+
`STIM_BAD_ARG` instead of skipping the cleanup. A run that `ios.remote` or
|
|
48
|
+
`android.remote` places on another machine or on EAS is not cleaned, and a
|
|
49
|
+
physical device is refused. `build` always compiles on this machine and ignores
|
|
50
|
+
`remote.build`. By default it compiles an iOS app, and an Android Debug app,
|
|
51
|
+
for this machine's architecture only, so when `run` can land on another
|
|
52
|
+
machine or on EAS, build
|
|
53
|
+
for that device too with `--arch all` (iOS) or `--abi all` (Android).
|
|
54
|
+
|
|
55
|
+
`--eas-profile <name>` (iOS and Android, build only) downloads a completed EAS
|
|
56
|
+
development build instead of compiling, as `stim ios --eas-profile` does; `run`
|
|
57
|
+
then installs it. Stim never starts an EAS build.
|
|
58
|
+
|
|
59
|
+
macOS and web are unchanged: `run --platform macos` builds its app itself and web
|
|
60
|
+
has no native build. iOS builds target the simulator; archives, device
|
|
61
|
+
distribution and web compilation are not supported.
|
|
62
|
+
|
|
63
|
+
`build` exports nothing by default. `--export <path>` also writes the app there:
|
|
64
|
+
`app.apk` for Android, a tar.gz for iOS and macOS that preserves executable
|
|
65
|
+
permissions and symlinks (extract with `tar -xzf`). On a cache hit the archive is
|
|
66
|
+
the slowest part of `build`, so export only when something consumes the file.
|
|
67
|
+
|
|
68
|
+
The build result has `stage: "build"`, `build`, `buildPath`, and `artifactPath`
|
|
69
|
+
(the export, or null). With `--artifacts` the directory also contains
|
|
70
|
+
`build.json`, `result.json` and `diagnostics.json`.
|
|
71
|
+
|
|
72
|
+
The library exports `buildCI({ projectRoot, build: { platform: "ios" }, exportPath, ... })`
|
|
73
|
+
and `runCI`. `runCI`'s `run` option takes the selectors of the build to run; build-only
|
|
74
|
+
options such as `easProfile` belong to `buildCI`. Build cancellation waits for the
|
|
75
|
+
owned build operation to finish cleanup and retains diagnostics; it never calls
|
|
76
|
+
workspace `stop()`. A cancel or timeout after the export is written does not fail
|
|
77
|
+
the build stage.
|
|
45
78
|
|
|
46
79
|
## Command line
|
|
47
80
|
|
|
48
|
-
Without installing:
|
|
49
|
-
|
|
50
|
-
```sh
|
|
51
|
-
npx --yes --package @stim-cli/ci stim-ci run --platform ios --project ./app -- pnpm test:e2e
|
|
52
|
-
```
|
|
53
|
-
|
|
54
81
|
Or install `npm install --global @stim-cli/ci`, then:
|
|
55
82
|
|
|
56
83
|
```sh
|
|
84
|
+
stim-ci build --platform android --project ./app
|
|
57
85
|
stim-ci run --platform android --project ./app --artifacts ./test-results --timeout 1800 -- pnpm test:e2e
|
|
58
86
|
```
|
|
59
87
|
|
|
@@ -62,20 +90,21 @@ to the current directory. `--home` and `--build-cache` explicitly select the
|
|
|
62
90
|
Stim home and native artifact cache; leaving them unset preserves normal Stim
|
|
63
91
|
configuration.
|
|
64
92
|
|
|
65
|
-
Without selectors, the CLI uses each project's normal build settings. `
|
|
66
|
-
|
|
67
|
-
build:
|
|
93
|
+
Without selectors, the CLI uses each project's normal build settings. `run`
|
|
94
|
+
must use the selectors its `build` used to find the recorded app:
|
|
68
95
|
|
|
69
|
-
| Option | Platform
|
|
70
|
-
| ------------------------ |
|
|
71
|
-
| `--scheme <name>` | iOS
|
|
72
|
-
| `--configuration <name>` | iOS
|
|
73
|
-
| `--variant <name>` | Android
|
|
74
|
-
| `--arch <name>` | iOS
|
|
75
|
-
| `--abi <name>` | Android
|
|
96
|
+
| Option | Platform | Stage | Selects |
|
|
97
|
+
| ------------------------ | ------------ | ---------- | ------------------------------------------------------- |
|
|
98
|
+
| `--scheme <name>` | iOS | build, run | Xcode scheme |
|
|
99
|
+
| `--configuration <name>` | iOS | build, run | Xcode configuration |
|
|
100
|
+
| `--variant <name>` | Android | build, run | Gradle variant |
|
|
101
|
+
| `--arch <name>` | iOS | build | Architecture: `arm64`, `x86_64`, `all` |
|
|
102
|
+
| `--abi <name>` | Android | build | ABI: `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64`, `all` |
|
|
103
|
+
| `--eas-profile <name>` | iOS, Android | build | Download this EAS profile's development build |
|
|
104
|
+
| `--export <path>` | all native | build | Also write the app (APK or tar.gz) to this path |
|
|
76
105
|
|
|
77
|
-
The library also accepts the public API's other platform-specific build and
|
|
78
|
-
options.
|
|
106
|
+
The library also accepts the public API's other platform-specific build options, and
|
|
107
|
+
run options other than those that build.
|
|
79
108
|
|
|
80
109
|
Everything after `--` is an argument vector. No shell is inferred. To use shell
|
|
81
110
|
syntax, pass a shell explicitly: `-- bash -e -o pipefail -c 'pnpm test:e2e'`.
|
|
@@ -83,25 +112,28 @@ syntax, pass a shell explicitly: `-- bash -e -o pipefail -c 'pnpm test:e2e'`.
|
|
|
83
112
|
The test process runs in the project directory. It receives the caller's
|
|
84
113
|
environment plus these values:
|
|
85
114
|
|
|
86
|
-
| Variable | Value
|
|
87
|
-
| ----------------------- |
|
|
88
|
-
| `STIM_CI_PLATFORM` | Selected platform
|
|
89
|
-
| `STIM_CI_DEVICE_ID` | Exact simulator UDID or emulator serial; empty for macOS/web
|
|
90
|
-
| `STIM_CI_APP_ID` | App bundle/package identifier when available
|
|
91
|
-
| `STIM_CI_METRO_PORT` | Metro port when available
|
|
92
|
-
| `STIM_CI_ARTIFACTS_DIR` | Absolute results directory
|
|
93
|
-
| `STIM_CI_RUN_RESULT` | Path to `run.json
|
|
115
|
+
| Variable | Value |
|
|
116
|
+
| ----------------------- | ------------------------------------------------------------------------- |
|
|
117
|
+
| `STIM_CI_PLATFORM` | Selected platform |
|
|
118
|
+
| `STIM_CI_DEVICE_ID` | Exact simulator UDID or emulator serial; empty for macOS/web |
|
|
119
|
+
| `STIM_CI_APP_ID` | App bundle/package identifier when available |
|
|
120
|
+
| `STIM_CI_METRO_PORT` | Metro port when available |
|
|
121
|
+
| `STIM_CI_ARTIFACTS_DIR` | Absolute results directory; empty without `--artifacts` |
|
|
122
|
+
| `STIM_CI_RUN_RESULT` | Path to `run.json` before the command starts; empty without `--artifacts` |
|
|
94
123
|
|
|
95
124
|
`run.json` contains the public Stim result `{ platform, facts }`, including
|
|
96
|
-
platform-specific process, target,
|
|
125
|
+
platform-specific process, target, launch, and log facts; `appPath` is the stable
|
|
126
|
+
path of the installed app. Use the exact
|
|
97
127
|
reported target; do not select an arbitrary booted device. Tests own
|
|
98
128
|
app-specific readiness checks. A launch can still be `bundling` or `unverified`.
|
|
99
129
|
|
|
100
|
-
Progress and test output go to stderr. Stdout contains one JSON result.
|
|
101
|
-
result is saved as `result.json` beside `run.json`,
|
|
102
|
-
`test.stderr.log`, and `diagnostics.json`. Without
|
|
103
|
-
|
|
104
|
-
|
|
130
|
+
Progress and test output go to stderr. Stdout contains one JSON result. With
|
|
131
|
+
`--artifacts <dir>` the same result is saved as `result.json` beside `run.json`,
|
|
132
|
+
`test.stdout.log`, `test.stderr.log`, and `diagnostics.json`. Without it no result,
|
|
133
|
+
test or diagnostic file is written, no logs are copied, and the result's
|
|
134
|
+
`artifactsDir`, `resultPath` and `runPath` are null. An explicit results directory
|
|
135
|
+
must be empty; a reused directory is refused before setup so stale evidence cannot
|
|
136
|
+
describe a new run.
|
|
105
137
|
Keep results outside fingerprinted project inputs.
|
|
106
138
|
|
|
107
139
|
The result includes `version: 1`, `projectRoot`, `platform`, `artifactsDir`,
|
|
@@ -188,6 +220,9 @@ For another disposable runner dedicated to one job, explicitly provide a fresh
|
|
|
188
220
|
job home and a job-local artifact cache:
|
|
189
221
|
|
|
190
222
|
```sh
|
|
223
|
+
stim-ci build --platform ios --project ./app \
|
|
224
|
+
--home "$RUNNER_TEMP/stim-home" \
|
|
225
|
+
--build-cache "$RUNNER_TEMP/stim-build-cache"
|
|
191
226
|
stim-ci run --platform ios --project ./app \
|
|
192
227
|
--home "$RUNNER_TEMP/stim-home" \
|
|
193
228
|
--build-cache "$RUNNER_TEMP/stim-build-cache" \
|
|
@@ -1,16 +1,19 @@
|
|
|
1
|
-
import { n as buildCI, t as runCI } from "./src-
|
|
1
|
+
import { n as buildCI, t as runCI } from "./src-CFd6F73i.mjs";
|
|
2
2
|
import { parseArgs } from "node:util";
|
|
3
3
|
//#region src/cli.ts
|
|
4
4
|
const HELP = `Usage: stim-ci build --platform <ios|android|macos> [options]
|
|
5
5
|
stim-ci run --platform <ios|android|macos|web> [options] -- <command> [args...]
|
|
6
6
|
|
|
7
|
-
Build
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
Build is the only step that compiles. It records the app for this checkout, platform and
|
|
8
|
+
selectors, and exports it only with --export.
|
|
9
|
+
Run installs and launches the app the last matching build recorded, runs a test command, and
|
|
10
|
+
stops the app. It never builds; with no recorded build it refuses and names the build to run.
|
|
11
|
+
The final result is JSON on stdout; progress and test output go to stderr. No result, test or
|
|
12
|
+
diagnostic file is written unless --artifacts is given.
|
|
10
13
|
|
|
11
14
|
Options:
|
|
12
15
|
--project <path> Project directory (default: current directory)
|
|
13
|
-
--artifacts <path>
|
|
16
|
+
--artifacts <path> Empty directory for result, test and diagnostic files (default: write none)
|
|
14
17
|
--home <path> Explicit Stim home (default: job-local on GitHub-hosted runners)
|
|
15
18
|
--build-cache <path> Native artifact cache directory
|
|
16
19
|
--timeout <seconds> Setup and test timeout (cleanup has its own deadline)
|
|
@@ -22,7 +25,9 @@ Build selectors (build and run):
|
|
|
22
25
|
--configuration <name> iOS configuration
|
|
23
26
|
--variant <name> Android variant
|
|
24
27
|
|
|
25
|
-
Build-only
|
|
28
|
+
Build-only options:
|
|
29
|
+
--eas-profile <name> Download this EAS profile's completed development build instead of compiling
|
|
30
|
+
--export <path> Also write the app here: an APK for Android, a tar.gz for iOS and macOS
|
|
26
31
|
--arch <name> iOS architecture: arm64, x86_64, all
|
|
27
32
|
--abi <name> Android ABI: arm64-v8a, armeabi-v7a, x86, x86_64, all
|
|
28
33
|
`;
|
|
@@ -43,7 +48,9 @@ function parse(argv) {
|
|
|
43
48
|
configuration: { type: "string" },
|
|
44
49
|
variant: { type: "string" },
|
|
45
50
|
arch: { type: "string" },
|
|
46
|
-
abi: { type: "string" }
|
|
51
|
+
abi: { type: "string" },
|
|
52
|
+
"eas-profile": { type: "string" },
|
|
53
|
+
export: { type: "string" }
|
|
47
54
|
}
|
|
48
55
|
});
|
|
49
56
|
const stage = positionals[0];
|
|
@@ -58,11 +65,17 @@ function parse(argv) {
|
|
|
58
65
|
buildCache: values["build-cache"],
|
|
59
66
|
timeoutMs
|
|
60
67
|
};
|
|
61
|
-
|
|
68
|
+
for (const flag of [
|
|
69
|
+
"arch",
|
|
70
|
+
"abi",
|
|
71
|
+
"eas-profile",
|
|
72
|
+
"export"
|
|
73
|
+
]) if (stage === "run" && values[flag] !== void 0) throw new Error(`--${flag} only applies to build.`);
|
|
74
|
+
if (values["eas-profile"] !== void 0 && platform !== "ios" && platform !== "android") throw new Error("--eas-profile only applies to iOS and Android.");
|
|
62
75
|
if (platform !== "ios" && (values.scheme !== void 0 || values.configuration !== void 0 || values.arch !== void 0)) throw new Error("--scheme, --configuration and --arch only apply to iOS.");
|
|
63
76
|
if (platform !== "android" && (values.variant !== void 0 || values.abi !== void 0)) throw new Error("--variant and --abi only apply to Android.");
|
|
64
77
|
if (stage === "build") {
|
|
65
|
-
if (separator >= 0) throw new Error("Build
|
|
78
|
+
if (separator >= 0) throw new Error("Build does not take a test command. Use stim-ci run to launch and test.");
|
|
66
79
|
if (platform === "web") throw new Error("Web projects have no native build artifact. Use stim-ci run --platform web.");
|
|
67
80
|
if (values.arch !== void 0 && ![
|
|
68
81
|
"arm64",
|
|
@@ -80,17 +93,20 @@ function parse(argv) {
|
|
|
80
93
|
platform,
|
|
81
94
|
scheme: values.scheme,
|
|
82
95
|
configuration: values.configuration,
|
|
83
|
-
arch: values.arch
|
|
96
|
+
arch: values.arch,
|
|
97
|
+
easProfile: values["eas-profile"]
|
|
84
98
|
} : platform === "android" ? {
|
|
85
99
|
platform,
|
|
86
100
|
variant: values.variant,
|
|
87
|
-
abi: values.abi
|
|
101
|
+
abi: values.abi,
|
|
102
|
+
easProfile: values["eas-profile"]
|
|
88
103
|
} : { platform };
|
|
89
104
|
return {
|
|
90
105
|
stage,
|
|
91
106
|
options: {
|
|
92
107
|
...common,
|
|
93
|
-
build
|
|
108
|
+
build,
|
|
109
|
+
exportPath: values.export
|
|
94
110
|
}
|
|
95
111
|
};
|
|
96
112
|
}
|
|
@@ -115,7 +131,7 @@ function parse(argv) {
|
|
|
115
131
|
}
|
|
116
132
|
async function main(argv = process.argv.slice(2)) {
|
|
117
133
|
if (argv.length === 1 && argv[0] === "--version") {
|
|
118
|
-
process.stdout.write(`1.
|
|
134
|
+
process.stdout.write(`1.37.0\n`);
|
|
119
135
|
return;
|
|
120
136
|
}
|
|
121
137
|
if (argv.length === 0 || argv[0] === "--help" || (argv[0] === "run" || argv[0] === "build") && argv[1] === "--help") {
|
package/dist/index.d.mts
CHANGED
|
@@ -2,6 +2,7 @@ import { StimBuildOptions, StimBuildResult, StimOptions, StimRunOptions, StimRun
|
|
|
2
2
|
//#region src/context.d.ts
|
|
3
3
|
interface CIContextOptions {
|
|
4
4
|
projectRoot: string;
|
|
5
|
+
/** Writes result, test and diagnostic files here; without it nothing is written. */
|
|
5
6
|
artifactsDir?: string;
|
|
6
7
|
home?: string;
|
|
7
8
|
buildCache?: string;
|
|
@@ -15,25 +16,28 @@ interface CommandResult {
|
|
|
15
16
|
exitCode: number | null;
|
|
16
17
|
signal: NodeJS.Signals | null;
|
|
17
18
|
durationMs: number;
|
|
18
|
-
stdout: string;
|
|
19
|
-
stderr: string;
|
|
19
|
+
stdout: string | null;
|
|
20
|
+
stderr: string | null;
|
|
20
21
|
error?: string;
|
|
21
22
|
}
|
|
22
23
|
//#endregion
|
|
23
24
|
//#region src/build.d.ts
|
|
24
|
-
type
|
|
25
|
+
type WithoutKeys$1<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
25
26
|
interface CIBuildOptions extends CIContextOptions {
|
|
26
|
-
|
|
27
|
+
/** Always compiles on this machine; `remoteBuild` is not accepted. */
|
|
28
|
+
build: WithoutKeys$1<StimBuildOptions, "signal" | "remoteBuild">;
|
|
29
|
+
/** Also writes the app here as a portable APK (Android) or tar.gz (iOS, macOS); without it the build is only recorded for `run`. */
|
|
30
|
+
exportPath?: string;
|
|
27
31
|
}
|
|
28
32
|
interface CIBuildResult {
|
|
29
33
|
version: 1;
|
|
30
34
|
stage: "build";
|
|
31
35
|
projectRoot: string;
|
|
32
36
|
platform: StimBuildOptions["platform"];
|
|
33
|
-
artifactsDir: string;
|
|
34
|
-
resultPath: string;
|
|
35
|
-
buildPath: string;
|
|
36
|
-
/**
|
|
37
|
+
artifactsDir: string | null;
|
|
38
|
+
resultPath: string | null;
|
|
39
|
+
buildPath: string | null;
|
|
40
|
+
/** The exported APK or tar.gz bundle preserving executable permissions and symlinks; null unless an export was requested. */
|
|
37
41
|
artifactPath: string | null;
|
|
38
42
|
startedAt: string;
|
|
39
43
|
durationMs: number;
|
|
@@ -46,13 +50,14 @@ interface CIBuildResult {
|
|
|
46
50
|
error?: CIFailure;
|
|
47
51
|
};
|
|
48
52
|
}
|
|
49
|
-
/** Builds and
|
|
53
|
+
/** Builds the app and records it for `runCI` without starting or stopping a workspace runtime. */
|
|
50
54
|
declare function buildCI(input: CIBuildOptions): Promise<CIBuildResult>;
|
|
51
55
|
//#endregion
|
|
52
56
|
//#region src/index.d.ts
|
|
53
|
-
type
|
|
57
|
+
type WithoutKeys<T, K extends PropertyKey> = T extends unknown ? Omit<T, K> : never;
|
|
54
58
|
interface CIOptions extends CIContextOptions {
|
|
55
|
-
run
|
|
59
|
+
/** Selectors of the recorded build to run. `run` never builds, so build-time options such as `easProfile` belong to `buildCI`. */
|
|
60
|
+
run: WithoutKeys<StimRunOptions, "signal" | "recordedBuild" | "cleanAppState" | "easProfile">;
|
|
56
61
|
command: readonly [string, ...string[]];
|
|
57
62
|
}
|
|
58
63
|
interface CIFailure {
|
|
@@ -64,9 +69,9 @@ interface CIResult {
|
|
|
64
69
|
version: 1;
|
|
65
70
|
projectRoot: string;
|
|
66
71
|
platform: StimRunOptions["platform"];
|
|
67
|
-
artifactsDir: string;
|
|
68
|
-
resultPath: string;
|
|
69
|
-
runPath: string;
|
|
72
|
+
artifactsDir: string | null;
|
|
73
|
+
resultPath: string | null;
|
|
74
|
+
runPath: string | null;
|
|
70
75
|
startedAt: string;
|
|
71
76
|
durationMs: number;
|
|
72
77
|
exitCode: number;
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { n as buildCI, t as runCI } from "./src-
|
|
1
|
+
import { n as buildCI, t as runCI } from "./src-CFd6F73i.mjs";
|
|
2
2
|
export { buildCI, runCI };
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { copyDiagnosticLogs } from "./artifacts.mjs";
|
|
2
|
-
import { closeSync, copyFileSync, mkdirSync,
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
2
|
+
import { closeSync, copyFileSync, mkdirSync, openSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
|
|
4
3
|
import { basename, dirname, join, resolve } from "node:path";
|
|
5
4
|
import { createStim } from "stim";
|
|
6
5
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
@@ -30,9 +29,11 @@ function prepareCI(options) {
|
|
|
30
29
|
};
|
|
31
30
|
}
|
|
32
31
|
const projectRoot = realpathSync(options.projectRoot);
|
|
33
|
-
const artifactsDir = options.artifactsDir ? resolve(options.artifactsDir) :
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
const artifactsDir = options.artifactsDir ? resolve(options.artifactsDir) : null;
|
|
33
|
+
if (artifactsDir) {
|
|
34
|
+
mkdirSync(artifactsDir, { recursive: true });
|
|
35
|
+
if (readdirSync(artifactsDir).length > 0) throw new Error(`Artifacts directory must be empty: ${artifactsDir}. Choose a new directory for this run.`);
|
|
36
|
+
}
|
|
36
37
|
const timeout = options.timeoutMs === void 0 ? void 0 : AbortSignal.timeout(options.timeoutMs);
|
|
37
38
|
const signals = [options.signal, timeout].filter((value) => value !== void 0);
|
|
38
39
|
const signal = signals.length ? AbortSignal.any(signals) : void 0;
|
|
@@ -49,12 +50,12 @@ function prepareCI(options) {
|
|
|
49
50
|
async function runCommand({ command, cwd, env, artifactsDir, logName = "test", signal, onOutput }) {
|
|
50
51
|
signal?.throwIfAborted();
|
|
51
52
|
const started = Date.now();
|
|
52
|
-
const stdout = join(artifactsDir, `${logName}.stdout.log`);
|
|
53
|
-
const stderr = join(artifactsDir, `${logName}.stderr.log`);
|
|
54
|
-
const out = openSync(stdout, "w");
|
|
53
|
+
const stdout = artifactsDir ? join(artifactsDir, `${logName}.stdout.log`) : null;
|
|
54
|
+
const stderr = artifactsDir ? join(artifactsDir, `${logName}.stderr.log`) : null;
|
|
55
|
+
const out = stdout ? openSync(stdout, "w") : void 0;
|
|
55
56
|
let err;
|
|
56
57
|
try {
|
|
57
|
-
err = openSync(stderr, "w");
|
|
58
|
+
if (stderr) err = openSync(stderr, "w");
|
|
58
59
|
const child = spawn(command[0], command.slice(1), {
|
|
59
60
|
cwd,
|
|
60
61
|
env,
|
|
@@ -93,7 +94,7 @@ async function runCommand({ command, cwd, env, artifactsDir, logName = "test", s
|
|
|
93
94
|
};
|
|
94
95
|
const output = (stream, fd, chunk) => {
|
|
95
96
|
try {
|
|
96
|
-
writeSync(fd, chunk);
|
|
97
|
+
if (fd !== void 0) writeSync(fd, chunk);
|
|
97
98
|
onOutput?.({
|
|
98
99
|
stream,
|
|
99
100
|
message: chunk.toString("utf8")
|
|
@@ -153,13 +154,13 @@ async function runCommand({ command, cwd, env, artifactsDir, logName = "test", s
|
|
|
153
154
|
clearTimeout(killTimer);
|
|
154
155
|
}
|
|
155
156
|
} finally {
|
|
156
|
-
closeSync(out);
|
|
157
|
+
if (out !== void 0) closeSync(out);
|
|
157
158
|
if (err !== void 0) closeSync(err);
|
|
158
159
|
}
|
|
159
160
|
}
|
|
160
161
|
//#endregion
|
|
161
162
|
//#region src/build.ts
|
|
162
|
-
/** Builds and
|
|
163
|
+
/** Builds the app and records it for `runCI` without starting or stopping a workspace runtime. */
|
|
163
164
|
async function buildCI(input) {
|
|
164
165
|
const { options, projectRoot, artifactsDir, timeout, signal } = prepareCI(input);
|
|
165
166
|
const started = Date.now();
|
|
@@ -169,8 +170,8 @@ async function buildCI(input) {
|
|
|
169
170
|
projectRoot,
|
|
170
171
|
platform: options.build.platform,
|
|
171
172
|
artifactsDir,
|
|
172
|
-
resultPath: join(artifactsDir, "result.json"),
|
|
173
|
-
buildPath: join(artifactsDir, "build.json"),
|
|
173
|
+
resultPath: artifactsDir ? join(artifactsDir, "result.json") : null,
|
|
174
|
+
buildPath: artifactsDir ? join(artifactsDir, "build.json") : null,
|
|
174
175
|
artifactPath: null,
|
|
175
176
|
startedAt: new Date(started).toISOString(),
|
|
176
177
|
durationMs: 0,
|
|
@@ -200,16 +201,16 @@ async function buildCI(input) {
|
|
|
200
201
|
attempted = true;
|
|
201
202
|
result.build = await stim.build({
|
|
202
203
|
...options.build,
|
|
204
|
+
remoteBuild: "local",
|
|
203
205
|
signal
|
|
204
206
|
});
|
|
205
207
|
signal?.throwIfAborted();
|
|
206
|
-
writeJson(result.buildPath, result.build);
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
208
|
+
if (result.buildPath) writeJson(result.buildPath, result.build);
|
|
209
|
+
if (input.exportPath) exporting = resolve(input.exportPath);
|
|
210
|
+
if (exporting) mkdirSync(dirname(exporting), { recursive: true });
|
|
211
|
+
if (exporting && result.build.platform === "android") copyFileSync(result.build.facts.apkPath, exporting);
|
|
212
|
+
else if (exporting && result.build.platform !== "android") {
|
|
211
213
|
const bundle = result.build.platform === "ios" ? result.build.facts.appPath : result.build.facts.bundle;
|
|
212
|
-
exporting = join(artifactsDir, "app.tar.gz");
|
|
213
214
|
const packed = await runCommand({
|
|
214
215
|
command: [
|
|
215
216
|
"tar",
|
|
@@ -233,7 +234,7 @@ async function buildCI(input) {
|
|
|
233
234
|
if (packed.exitCode !== 0 || packed.error) throw Object.assign(new Error(packed.error ?? `Artifact export exited with ${packed.exitCode}.`), { code: "STIM_CI_ARTIFACT_FAILED" });
|
|
234
235
|
}
|
|
235
236
|
signal?.throwIfAborted();
|
|
236
|
-
result.artifactPath = exporting;
|
|
237
|
+
result.artifactPath = exporting ?? null;
|
|
237
238
|
result.exitCode = 0;
|
|
238
239
|
} catch (error) {
|
|
239
240
|
result.failure = failure(error, result.build ? "STIM_CI_ARTIFACT_FAILED" : "STIM_CI_BUILD_FAILED");
|
|
@@ -253,7 +254,7 @@ async function buildCI(input) {
|
|
|
253
254
|
} catch (error) {
|
|
254
255
|
result.reportingError ??= failure(error, "STIM_CI_REPORT_FAILED");
|
|
255
256
|
}
|
|
256
|
-
if (attempted) try {
|
|
257
|
+
if (attempted && artifactsDir) try {
|
|
257
258
|
const diagnostics = await stim.diagnostics({
|
|
258
259
|
tail: 1e3,
|
|
259
260
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -268,7 +269,7 @@ async function buildCI(input) {
|
|
|
268
269
|
result.failure = result.reportingError;
|
|
269
270
|
}
|
|
270
271
|
result.durationMs = Date.now() - started;
|
|
271
|
-
try {
|
|
272
|
+
if (result.resultPath) try {
|
|
272
273
|
writeJson(result.resultPath, result);
|
|
273
274
|
} catch (error) {
|
|
274
275
|
result.reportingError = failure(error, "STIM_CI_REPORT_FAILED");
|
|
@@ -292,8 +293,8 @@ function commandEnvironment(result, options) {
|
|
|
292
293
|
STIM_CI_DEVICE_ID: String("udid" in facts ? facts.udid : "serial" in facts ? facts.serial ?? "" : ""),
|
|
293
294
|
STIM_CI_APP_ID: String("bundleId" in facts ? facts.bundleId ?? "" : ""),
|
|
294
295
|
STIM_CI_METRO_PORT: String("metroPort" in facts ? facts.metroPort ?? "" : ""),
|
|
295
|
-
STIM_CI_ARTIFACTS_DIR: result.artifactsDir,
|
|
296
|
-
STIM_CI_RUN_RESULT: result.runPath
|
|
296
|
+
STIM_CI_ARTIFACTS_DIR: result.artifactsDir ?? "",
|
|
297
|
+
STIM_CI_RUN_RESULT: result.runPath ?? ""
|
|
297
298
|
};
|
|
298
299
|
}
|
|
299
300
|
/** Runs an app and argv-based test command, stops its workspace or explicit slot, and retains diagnostics. */
|
|
@@ -309,8 +310,8 @@ async function runCI(options) {
|
|
|
309
310
|
projectRoot,
|
|
310
311
|
platform: options.run.platform,
|
|
311
312
|
artifactsDir,
|
|
312
|
-
resultPath: join(artifactsDir, "result.json"),
|
|
313
|
-
runPath: join(artifactsDir, "run.json"),
|
|
313
|
+
resultPath: artifactsDir ? join(artifactsDir, "result.json") : null,
|
|
314
|
+
runPath: artifactsDir ? join(artifactsDir, "run.json") : null,
|
|
314
315
|
startedAt: new Date(started).toISOString(),
|
|
315
316
|
durationMs: 0,
|
|
316
317
|
exitCode: 1,
|
|
@@ -340,10 +341,12 @@ async function runCI(options) {
|
|
|
340
341
|
attempted = true;
|
|
341
342
|
result.run = await stim.run({
|
|
342
343
|
...options.run,
|
|
344
|
+
recordedBuild: true,
|
|
345
|
+
cleanAppState: true,
|
|
343
346
|
signal
|
|
344
347
|
});
|
|
345
348
|
signal?.throwIfAborted();
|
|
346
|
-
writeJson(result.runPath, result.run);
|
|
349
|
+
if (result.runPath) writeJson(result.runPath, result.run);
|
|
347
350
|
result.test = await runCommand({
|
|
348
351
|
command: options.command,
|
|
349
352
|
cwd: projectRoot,
|
|
@@ -360,6 +363,7 @@ async function runCI(options) {
|
|
|
360
363
|
};
|
|
361
364
|
} catch (error) {
|
|
362
365
|
result.failure = failure(error, result.run ? "STIM_CI_TEST_FAILED" : "STIM_CI_SETUP_FAILED");
|
|
366
|
+
if (result.failure.code === "STIM_NO_RECORDED_BUILD") result.failure.remedy = `Run ${buildCommand(options, projectRoot)} first.`;
|
|
363
367
|
} finally {
|
|
364
368
|
cleaningUp = true;
|
|
365
369
|
if (signal?.aborted) {
|
|
@@ -383,7 +387,7 @@ async function runCI(options) {
|
|
|
383
387
|
} catch (error) {
|
|
384
388
|
result.cleanup.error = failure(error, "STIM_CI_CLEANUP_FAILED");
|
|
385
389
|
}
|
|
386
|
-
try {
|
|
390
|
+
if (artifactsDir) try {
|
|
387
391
|
const diagnostics = await stim.diagnostics({
|
|
388
392
|
tail: 1e3,
|
|
389
393
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -412,7 +416,7 @@ async function runCI(options) {
|
|
|
412
416
|
result.failure = result.reportingError;
|
|
413
417
|
}
|
|
414
418
|
result.durationMs = Date.now() - started;
|
|
415
|
-
try {
|
|
419
|
+
if (result.resultPath) try {
|
|
416
420
|
writeJson(result.resultPath, result);
|
|
417
421
|
} catch (error) {
|
|
418
422
|
result.reportingError = failure(error, "STIM_CI_REPORT_FAILED");
|
|
@@ -424,5 +428,16 @@ async function runCI(options) {
|
|
|
424
428
|
}
|
|
425
429
|
return result;
|
|
426
430
|
}
|
|
431
|
+
function buildCommand({ run, home, buildCache }, projectRoot) {
|
|
432
|
+
const flags = [`--platform ${run.platform}`];
|
|
433
|
+
if (projectRoot !== realpathSync(process.cwd())) flags.push(`--project ${JSON.stringify(projectRoot)}`);
|
|
434
|
+
if (home) flags.push(`--home ${JSON.stringify(resolve(home))}`);
|
|
435
|
+
if (buildCache) flags.push(`--build-cache ${JSON.stringify(resolve(buildCache))}`);
|
|
436
|
+
if (run.platform === "ios") {
|
|
437
|
+
if (run.scheme !== void 0) flags.push(`--scheme ${JSON.stringify(run.scheme)}`);
|
|
438
|
+
if (run.configuration !== void 0) flags.push(`--configuration ${JSON.stringify(run.configuration)}`);
|
|
439
|
+
} else if (run.platform === "android" && run.variant !== void 0) flags.push(`--variant ${JSON.stringify(run.variant)}`);
|
|
440
|
+
return `stim-ci build ${flags.join(" ")}`;
|
|
441
|
+
}
|
|
427
442
|
//#endregion
|
|
428
443
|
export { buildCI as n, runCI as t };
|
package/dist/stim-ci.mjs
CHANGED
|
@@ -6,6 +6,6 @@ const minor = parts[1] ?? 0;
|
|
|
6
6
|
if (major < 22 || major === 22 && minor < 12) {
|
|
7
7
|
process.stderr.write(`stim-ci requires Node.js 22.12.0 or later; this is ${process.versions.node}.\n`);
|
|
8
8
|
process.exitCode = 1;
|
|
9
|
-
} else import("./cli-
|
|
9
|
+
} else import("./cli-BCFk6LoD.mjs").then(({ main }) => main());
|
|
10
10
|
//#endregion
|
|
11
11
|
export {};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stim-cli/ci",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.37.0",
|
|
4
4
|
"description": "Build Stim app artifacts or run Stim apps and test commands in CI with diagnostics and scoped cleanup.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"cross-spawn": "^7.0.6",
|
|
36
|
-
"stim": "1.
|
|
36
|
+
"stim": "1.37.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/cross-spawn": "^6.0.6"
|