@stim-cli/ci 1.19.0 → 1.20.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 +49 -2
- package/dist/cli-oSziKUiL.mjs +155 -0
- package/dist/index.d.mts +43 -10
- package/dist/index.mjs +2 -2
- package/dist/{src-BdobdbxB.mjs → src-q52NuL5J.mjs} +172 -35
- package/dist/stim-ci.mjs +1 -1
- package/package.json +3 -3
- package/dist/cli-RvPAkat7.mjs +0 -80
package/README.md
CHANGED
|
@@ -10,6 +10,39 @@ 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 only
|
|
14
|
+
|
|
15
|
+
```sh
|
|
16
|
+
npx --yes --package @stim-cli/ci stim-ci build --platform ios --project ./app --artifacts ./build-results
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
This compiles or restores an artifact without starting a simulator, emulator,
|
|
20
|
+
Metro server, or app. It does not stop an existing workspace session. iOS builds
|
|
21
|
+
target the simulator; archives, device distribution and web compilation are
|
|
22
|
+
not supported. By default, iOS and Android builds use the same native artifact
|
|
23
|
+
cache as `run`. A later `run` in the same job validates that cache normally; its
|
|
24
|
+
`run.json` reports `cacheKey` and `cacheHit`, which show whether it reused the
|
|
25
|
+
build. Android reuse requires the run's emulator system image to have the host's
|
|
26
|
+
ABI; a configured `android.systemImage` with another ABI, or a physical device,
|
|
27
|
+
misses. An iOS Debug build with `--arch all` is keyed without an architecture,
|
|
28
|
+
so a later Debug `run` misses. macOS builds are not cached, so a later macOS
|
|
29
|
+
`run` builds again.
|
|
30
|
+
|
|
31
|
+
The result has `stage: "build"`, `build`, `buildPath`, and `artifactPath`.
|
|
32
|
+
The results directory contains `build.json`, `result.json`, `diagnostics.json`,
|
|
33
|
+
and `app.apk` for Android or `app.tar.gz` for iOS/macOS. iOS and macOS also
|
|
34
|
+
keep the archive command's output in `artifact.stdout.log` and
|
|
35
|
+
`artifact.stderr.log`. The archive preserves app
|
|
36
|
+
executable permissions and symlinks when uploaded through an artifact service.
|
|
37
|
+
Extract it with `tar -xzf app.tar.gz`. Importing a downloaded artifact into a
|
|
38
|
+
Stim run is not part of this command.
|
|
39
|
+
|
|
40
|
+
The library exports `buildCI({ projectRoot, build: { platform: "ios" }, ... })`.
|
|
41
|
+
It accepts the same home/cache, timeout, cancellation and progress options as
|
|
42
|
+
`runCI`. Build cancellation waits for the owned build operation to finish
|
|
43
|
+
cleanup and retains diagnostics; it never calls workspace `stop()`. A cancel or timeout after the artifact is exported does not fail the build
|
|
44
|
+
stage.
|
|
45
|
+
|
|
13
46
|
## Command line
|
|
14
47
|
|
|
15
48
|
Without installing:
|
|
@@ -27,8 +60,22 @@ stim-ci run --platform android --project ./app --artifacts ./test-results --time
|
|
|
27
60
|
`--platform` accepts `ios`, `android`, `macos`, or `web`. `--project` defaults
|
|
28
61
|
to the current directory. `--home` and `--build-cache` explicitly select the
|
|
29
62
|
Stim home and native artifact cache; leaving them unset preserves normal Stim
|
|
30
|
-
configuration.
|
|
31
|
-
|
|
63
|
+
configuration.
|
|
64
|
+
|
|
65
|
+
Without selectors, the CLI uses each project's normal build settings. `build`
|
|
66
|
+
and `run` accept the same selectors as the public API, so a run can match a
|
|
67
|
+
build:
|
|
68
|
+
|
|
69
|
+
| Option | Platform | Stage | Selects |
|
|
70
|
+
| ------------------------ | -------- | ---------- | ------------------------------------------------------- |
|
|
71
|
+
| `--scheme <name>` | iOS | build, run | Xcode scheme |
|
|
72
|
+
| `--configuration <name>` | iOS | build, run | Xcode configuration |
|
|
73
|
+
| `--variant <name>` | Android | build, run | Gradle variant |
|
|
74
|
+
| `--arch <name>` | iOS | build | Architecture: `arm64`, `x86_64`, `all` |
|
|
75
|
+
| `--abi <name>` | Android | build | ABI: `arm64-v8a`, `armeabi-v7a`, `x86`, `x86_64`, `all` |
|
|
76
|
+
|
|
77
|
+
The library also accepts the public API's other platform-specific build and run
|
|
78
|
+
options.
|
|
32
79
|
|
|
33
80
|
Everything after `--` is an argument vector. No shell is inferred. To use shell
|
|
34
81
|
syntax, pass a shell explicitly: `-- bash -e -o pipefail -c 'pnpm test:e2e'`.
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
import { n as buildCI, t as runCI } from "./src-q52NuL5J.mjs";
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
//#region src/cli.ts
|
|
4
|
+
const HELP = `Usage: stim-ci build --platform <ios|android|macos> [options]
|
|
5
|
+
stim-ci run --platform <ios|android|macos|web> [options] -- <command> [args...]
|
|
6
|
+
|
|
7
|
+
Build exports an artifact without starting or stopping an app.
|
|
8
|
+
Run builds and launches an app, runs a test command, collects diagnostics, and stops it.
|
|
9
|
+
The final result is JSON on stdout; progress and test output go to stderr.
|
|
10
|
+
|
|
11
|
+
Options:
|
|
12
|
+
--project <path> Project directory (default: current directory)
|
|
13
|
+
--artifacts <path> Results directory (default: a fresh temporary directory)
|
|
14
|
+
--home <path> Explicit Stim home (default: job-local on GitHub-hosted runners)
|
|
15
|
+
--build-cache <path> Native artifact cache directory
|
|
16
|
+
--timeout <seconds> Setup and test timeout (cleanup has its own deadline)
|
|
17
|
+
--help Show this help
|
|
18
|
+
--version Show the package version
|
|
19
|
+
|
|
20
|
+
Build selectors (build and run):
|
|
21
|
+
--scheme <name> iOS scheme
|
|
22
|
+
--configuration <name> iOS configuration
|
|
23
|
+
--variant <name> Android variant
|
|
24
|
+
|
|
25
|
+
Build-only selectors:
|
|
26
|
+
--arch <name> iOS architecture: arm64, x86_64, all
|
|
27
|
+
--abi <name> Android ABI: arm64-v8a, armeabi-v7a, x86, x86_64, all
|
|
28
|
+
`;
|
|
29
|
+
function parse(argv) {
|
|
30
|
+
const separator = argv.indexOf("--");
|
|
31
|
+
const command = separator < 0 ? [] : argv.slice(separator + 1);
|
|
32
|
+
const { values, positionals } = parseArgs({
|
|
33
|
+
args: separator < 0 ? argv : argv.slice(0, separator),
|
|
34
|
+
allowPositionals: true,
|
|
35
|
+
options: {
|
|
36
|
+
platform: { type: "string" },
|
|
37
|
+
project: { type: "string" },
|
|
38
|
+
artifacts: { type: "string" },
|
|
39
|
+
home: { type: "string" },
|
|
40
|
+
"build-cache": { type: "string" },
|
|
41
|
+
timeout: { type: "string" },
|
|
42
|
+
scheme: { type: "string" },
|
|
43
|
+
configuration: { type: "string" },
|
|
44
|
+
variant: { type: "string" },
|
|
45
|
+
arch: { type: "string" },
|
|
46
|
+
abi: { type: "string" }
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
const stage = positionals[0];
|
|
50
|
+
if (positionals.length !== 1 || stage !== "run" && stage !== "build") throw new Error("Expected stim-ci build or run.");
|
|
51
|
+
const platform = values.platform;
|
|
52
|
+
if (platform !== "ios" && platform !== "android" && platform !== "macos" && platform !== "web") throw new Error("--platform must be ios, android, macos or web.");
|
|
53
|
+
const timeoutMs = values.timeout === void 0 ? void 0 : Number(values.timeout) * 1e3;
|
|
54
|
+
const common = {
|
|
55
|
+
projectRoot: values.project ?? process.cwd(),
|
|
56
|
+
artifactsDir: values.artifacts,
|
|
57
|
+
home: values.home,
|
|
58
|
+
buildCache: values["build-cache"],
|
|
59
|
+
timeoutMs
|
|
60
|
+
};
|
|
61
|
+
if (stage === "run" && (values.arch !== void 0 || values.abi !== void 0)) throw new Error("--arch and --abi only apply to build-only.");
|
|
62
|
+
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
|
+
if (platform !== "android" && (values.variant !== void 0 || values.abi !== void 0)) throw new Error("--variant and --abi only apply to Android.");
|
|
64
|
+
if (stage === "build") {
|
|
65
|
+
if (separator >= 0) throw new Error("Build-only does not take a test command. Use stim-ci run to launch and test.");
|
|
66
|
+
if (platform === "web") throw new Error("Web projects have no native build artifact. Use stim-ci run --platform web.");
|
|
67
|
+
if (values.arch !== void 0 && ![
|
|
68
|
+
"arm64",
|
|
69
|
+
"x86_64",
|
|
70
|
+
"all"
|
|
71
|
+
].includes(values.arch)) throw new Error("--arch must be arm64, x86_64 or all.");
|
|
72
|
+
if (values.abi !== void 0 && ![
|
|
73
|
+
"arm64-v8a",
|
|
74
|
+
"armeabi-v7a",
|
|
75
|
+
"x86",
|
|
76
|
+
"x86_64",
|
|
77
|
+
"all"
|
|
78
|
+
].includes(values.abi)) throw new Error("--abi must be arm64-v8a, armeabi-v7a, x86, x86_64 or all.");
|
|
79
|
+
const build = platform === "ios" ? {
|
|
80
|
+
platform,
|
|
81
|
+
scheme: values.scheme,
|
|
82
|
+
configuration: values.configuration,
|
|
83
|
+
arch: values.arch
|
|
84
|
+
} : platform === "android" ? {
|
|
85
|
+
platform,
|
|
86
|
+
variant: values.variant,
|
|
87
|
+
abi: values.abi
|
|
88
|
+
} : { platform };
|
|
89
|
+
return {
|
|
90
|
+
stage,
|
|
91
|
+
options: {
|
|
92
|
+
...common,
|
|
93
|
+
build
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
if (separator < 0) throw new Error("Separate the test command with --.");
|
|
98
|
+
if (!command[0]) throw new Error("A test command is required after --.");
|
|
99
|
+
const run = platform === "ios" ? {
|
|
100
|
+
platform,
|
|
101
|
+
scheme: values.scheme,
|
|
102
|
+
configuration: values.configuration
|
|
103
|
+
} : platform === "android" ? {
|
|
104
|
+
platform,
|
|
105
|
+
variant: values.variant
|
|
106
|
+
} : { platform };
|
|
107
|
+
return {
|
|
108
|
+
stage,
|
|
109
|
+
options: {
|
|
110
|
+
...common,
|
|
111
|
+
run,
|
|
112
|
+
command
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
async function main(argv = process.argv.slice(2)) {
|
|
117
|
+
if (argv.length === 1 && argv[0] === "--version") {
|
|
118
|
+
process.stdout.write(`1.20.0\n`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (argv.length === 0 || argv[0] === "--help" || (argv[0] === "run" || argv[0] === "build") && argv[1] === "--help") {
|
|
122
|
+
process.stdout.write(HELP);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const controller = new AbortController();
|
|
126
|
+
const interrupt = () => controller.abort();
|
|
127
|
+
try {
|
|
128
|
+
const invocation = parse(argv);
|
|
129
|
+
process.on("SIGINT", interrupt);
|
|
130
|
+
process.on("SIGTERM", interrupt);
|
|
131
|
+
const callbacks = {
|
|
132
|
+
signal: controller.signal,
|
|
133
|
+
onProgress: ({ message }) => {
|
|
134
|
+
process.stderr.write(message);
|
|
135
|
+
}
|
|
136
|
+
};
|
|
137
|
+
const result = invocation.stage === "build" ? await buildCI({
|
|
138
|
+
...invocation.options,
|
|
139
|
+
...callbacks
|
|
140
|
+
}) : await runCI({
|
|
141
|
+
...invocation.options,
|
|
142
|
+
...callbacks
|
|
143
|
+
});
|
|
144
|
+
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
145
|
+
process.exitCode = result.exitCode;
|
|
146
|
+
} catch (error) {
|
|
147
|
+
process.stderr.write(`stim-ci: ${error.message}\n`);
|
|
148
|
+
process.exitCode = 1;
|
|
149
|
+
} finally {
|
|
150
|
+
process.off("SIGINT", interrupt);
|
|
151
|
+
process.off("SIGTERM", interrupt);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
//#endregion
|
|
155
|
+
export { main };
|
package/dist/index.d.mts
CHANGED
|
@@ -1,4 +1,15 @@
|
|
|
1
|
-
import { StimOptions, StimRunOptions, StimRunResult, StimStopResult } from "stim";
|
|
1
|
+
import { StimBuildOptions, StimBuildResult, StimOptions, StimRunOptions, StimRunResult, StimStopResult } from "stim";
|
|
2
|
+
//#region src/context.d.ts
|
|
3
|
+
interface CIContextOptions {
|
|
4
|
+
projectRoot: string;
|
|
5
|
+
artifactsDir?: string;
|
|
6
|
+
home?: string;
|
|
7
|
+
buildCache?: string;
|
|
8
|
+
timeoutMs?: number;
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
onProgress?: StimOptions["onProgress"];
|
|
11
|
+
}
|
|
12
|
+
//#endregion
|
|
2
13
|
//#region src/command.d.ts
|
|
3
14
|
interface CommandResult {
|
|
4
15
|
exitCode: number | null;
|
|
@@ -9,18 +20,40 @@ interface CommandResult {
|
|
|
9
20
|
error?: string;
|
|
10
21
|
}
|
|
11
22
|
//#endregion
|
|
23
|
+
//#region src/build.d.ts
|
|
24
|
+
type WithoutSignal$1<T> = T extends unknown ? Omit<T, "signal"> : never;
|
|
25
|
+
interface CIBuildOptions extends CIContextOptions {
|
|
26
|
+
build: WithoutSignal$1<StimBuildOptions>;
|
|
27
|
+
}
|
|
28
|
+
interface CIBuildResult {
|
|
29
|
+
version: 1;
|
|
30
|
+
stage: "build";
|
|
31
|
+
projectRoot: string;
|
|
32
|
+
platform: StimBuildOptions["platform"];
|
|
33
|
+
artifactsDir: string;
|
|
34
|
+
resultPath: string;
|
|
35
|
+
buildPath: string;
|
|
36
|
+
/** Portable APK or tar.gz bundle preserving executable permissions and symlinks. */
|
|
37
|
+
artifactPath: string | null;
|
|
38
|
+
startedAt: string;
|
|
39
|
+
durationMs: number;
|
|
40
|
+
exitCode: number;
|
|
41
|
+
build: StimBuildResult | null;
|
|
42
|
+
failure?: CIFailure;
|
|
43
|
+
reportingError?: CIFailure;
|
|
44
|
+
diagnostics: {
|
|
45
|
+
path: string | null;
|
|
46
|
+
error?: CIFailure;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Builds and exports an artifact without starting or stopping a workspace runtime. */
|
|
50
|
+
declare function buildCI(input: CIBuildOptions): Promise<CIBuildResult>;
|
|
51
|
+
//#endregion
|
|
12
52
|
//#region src/index.d.ts
|
|
13
53
|
type WithoutSignal<T> = T extends unknown ? Omit<T, "signal"> : never;
|
|
14
|
-
interface CIOptions {
|
|
15
|
-
projectRoot: string;
|
|
54
|
+
interface CIOptions extends CIContextOptions {
|
|
16
55
|
run: WithoutSignal<StimRunOptions>;
|
|
17
56
|
command: readonly [string, ...string[]];
|
|
18
|
-
artifactsDir?: string;
|
|
19
|
-
home?: string;
|
|
20
|
-
buildCache?: string;
|
|
21
|
-
timeoutMs?: number;
|
|
22
|
-
signal?: AbortSignal;
|
|
23
|
-
onProgress?: StimOptions["onProgress"];
|
|
24
57
|
}
|
|
25
58
|
interface CIFailure {
|
|
26
59
|
code: string;
|
|
@@ -54,4 +87,4 @@ interface CIResult {
|
|
|
54
87
|
/** Runs an app and argv-based test command, stops its workspace or explicit slot, and retains diagnostics. */
|
|
55
88
|
declare function runCI(options: CIOptions): Promise<CIResult>;
|
|
56
89
|
//#endregion
|
|
57
|
-
export { CIFailure, CIOptions, CIResult, type CommandResult, runCI };
|
|
90
|
+
export { type CIBuildOptions, type CIBuildResult, CIFailure, CIOptions, CIResult, type CommandResult, buildCI, runCI };
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { t as runCI } from "./src-
|
|
2
|
-
export { runCI };
|
|
1
|
+
import { n as buildCI, t as runCI } from "./src-q52NuL5J.mjs";
|
|
2
|
+
export { buildCI, runCI };
|
|
@@ -1,16 +1,56 @@
|
|
|
1
1
|
import { copyDiagnosticLogs } from "./artifacts.mjs";
|
|
2
|
-
import { closeSync, mkdirSync, mkdtempSync, openSync, readdirSync, realpathSync, renameSync, writeFileSync, writeSync } from "node:fs";
|
|
2
|
+
import { closeSync, copyFileSync, mkdirSync, mkdtempSync, openSync, readdirSync, realpathSync, renameSync, rmSync, writeFileSync, writeSync } from "node:fs";
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
|
-
import { join, resolve } from "node:path";
|
|
4
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
5
5
|
import { createStim } from "stim";
|
|
6
6
|
import { setTimeout as setTimeout$1 } from "node:timers/promises";
|
|
7
7
|
import spawn from "cross-spawn";
|
|
8
|
+
//#region src/context.ts
|
|
9
|
+
function failure(error, code) {
|
|
10
|
+
const caught = error;
|
|
11
|
+
return {
|
|
12
|
+
code: typeof caught?.code === "string" ? caught.code : code,
|
|
13
|
+
message: typeof caught?.message === "string" ? caught.message : String(error),
|
|
14
|
+
...typeof caught?.remedy === "string" ? { remedy: caught.remedy } : {}
|
|
15
|
+
};
|
|
16
|
+
}
|
|
17
|
+
function writeJson(path, value) {
|
|
18
|
+
const pending = `${path}.tmp`;
|
|
19
|
+
writeFileSync(pending, `${JSON.stringify(value, null, 2)}\n`);
|
|
20
|
+
renameSync(pending, path);
|
|
21
|
+
}
|
|
22
|
+
function prepareCI(options) {
|
|
23
|
+
if (options.timeoutMs !== void 0 && (!Number.isSafeInteger(options.timeoutMs) || options.timeoutMs <= 0)) throw new Error("timeoutMs must be a positive integer.");
|
|
24
|
+
if (process.env.GITHUB_ACTIONS === "true" && process.env.RUNNER_ENVIRONMENT === "github-hosted" && process.env.RUNNER_TEMP && !options.home && !process.env.STIM_HOME) {
|
|
25
|
+
const directory = join(resolve(process.env.RUNNER_TEMP), "stim-ci");
|
|
26
|
+
options = {
|
|
27
|
+
...options,
|
|
28
|
+
home: join(directory, "home"),
|
|
29
|
+
buildCache: options.buildCache || (process.env.STIM_BUILD_CACHE ? void 0 : join(directory, "build-cache"))
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
const projectRoot = realpathSync(options.projectRoot);
|
|
33
|
+
const artifactsDir = options.artifactsDir ? resolve(options.artifactsDir) : mkdtempSync(join(tmpdir(), "stim-ci-"));
|
|
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
|
+
const timeout = options.timeoutMs === void 0 ? void 0 : AbortSignal.timeout(options.timeoutMs);
|
|
37
|
+
const signals = [options.signal, timeout].filter((value) => value !== void 0);
|
|
38
|
+
const signal = signals.length ? AbortSignal.any(signals) : void 0;
|
|
39
|
+
return {
|
|
40
|
+
options,
|
|
41
|
+
projectRoot,
|
|
42
|
+
artifactsDir,
|
|
43
|
+
timeout,
|
|
44
|
+
signal
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//#endregion
|
|
8
48
|
//#region src/command.ts
|
|
9
|
-
async function runCommand({ command, cwd, env, artifactsDir, signal, onOutput }) {
|
|
49
|
+
async function runCommand({ command, cwd, env, artifactsDir, logName = "test", signal, onOutput }) {
|
|
10
50
|
signal?.throwIfAborted();
|
|
11
51
|
const started = Date.now();
|
|
12
|
-
const stdout = join(artifactsDir,
|
|
13
|
-
const stderr = join(artifactsDir,
|
|
52
|
+
const stdout = join(artifactsDir, `${logName}.stdout.log`);
|
|
53
|
+
const stderr = join(artifactsDir, `${logName}.stderr.log`);
|
|
14
54
|
const out = openSync(stdout, "w");
|
|
15
55
|
let err;
|
|
16
56
|
try {
|
|
@@ -95,7 +135,7 @@ async function runCommand({ command, cwd, env, artifactsDir, signal, onOutput })
|
|
|
95
135
|
groupError ??= String(caught);
|
|
96
136
|
}
|
|
97
137
|
if (Date.now() >= deadline) {
|
|
98
|
-
error ??= groupError ??
|
|
138
|
+
error ??= groupError ?? `${logName === "artifact" ? "Artifact export" : "Test"} process group ${child.pid} did not exit after termination`;
|
|
99
139
|
break;
|
|
100
140
|
}
|
|
101
141
|
await setTimeout$1(20);
|
|
@@ -118,20 +158,130 @@ async function runCommand({ command, cwd, env, artifactsDir, signal, onOutput })
|
|
|
118
158
|
}
|
|
119
159
|
}
|
|
120
160
|
//#endregion
|
|
121
|
-
//#region src/
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
161
|
+
//#region src/build.ts
|
|
162
|
+
/** Builds and exports an artifact without starting or stopping a workspace runtime. */
|
|
163
|
+
async function buildCI(input) {
|
|
164
|
+
const { options, projectRoot, artifactsDir, timeout, signal } = prepareCI(input);
|
|
165
|
+
const started = Date.now();
|
|
166
|
+
const result = {
|
|
167
|
+
version: 1,
|
|
168
|
+
stage: "build",
|
|
169
|
+
projectRoot,
|
|
170
|
+
platform: options.build.platform,
|
|
171
|
+
artifactsDir,
|
|
172
|
+
resultPath: join(artifactsDir, "result.json"),
|
|
173
|
+
buildPath: join(artifactsDir, "build.json"),
|
|
174
|
+
artifactPath: null,
|
|
175
|
+
startedAt: new Date(started).toISOString(),
|
|
176
|
+
durationMs: 0,
|
|
177
|
+
exitCode: 1,
|
|
178
|
+
build: null,
|
|
179
|
+
diagnostics: { path: null }
|
|
128
180
|
};
|
|
181
|
+
let reporting = false;
|
|
182
|
+
const onProgress = options.onProgress ? (event) => {
|
|
183
|
+
try {
|
|
184
|
+
options.onProgress(event);
|
|
185
|
+
} catch (error) {
|
|
186
|
+
result.reportingError ??= failure(error, "STIM_CI_REPORT_FAILED");
|
|
187
|
+
if (!reporting) throw error;
|
|
188
|
+
}
|
|
189
|
+
} : void 0;
|
|
190
|
+
const stim = createStim({
|
|
191
|
+
projectRoot,
|
|
192
|
+
home: options.home ? resolve(options.home) : void 0,
|
|
193
|
+
buildCache: options.buildCache ? resolve(options.buildCache) : void 0,
|
|
194
|
+
onProgress
|
|
195
|
+
});
|
|
196
|
+
let attempted = false;
|
|
197
|
+
let exporting;
|
|
198
|
+
try {
|
|
199
|
+
signal?.throwIfAborted();
|
|
200
|
+
attempted = true;
|
|
201
|
+
result.build = await stim.build({
|
|
202
|
+
...options.build,
|
|
203
|
+
signal
|
|
204
|
+
});
|
|
205
|
+
signal?.throwIfAborted();
|
|
206
|
+
writeJson(result.buildPath, result.build);
|
|
207
|
+
if (result.build.platform === "android") {
|
|
208
|
+
exporting = join(artifactsDir, "app.apk");
|
|
209
|
+
copyFileSync(result.build.facts.apkPath, exporting);
|
|
210
|
+
} else {
|
|
211
|
+
const bundle = result.build.platform === "ios" ? result.build.facts.appPath : result.build.facts.bundle;
|
|
212
|
+
exporting = join(artifactsDir, "app.tar.gz");
|
|
213
|
+
const packed = await runCommand({
|
|
214
|
+
command: [
|
|
215
|
+
"tar",
|
|
216
|
+
"-czf",
|
|
217
|
+
exporting,
|
|
218
|
+
"-C",
|
|
219
|
+
dirname(bundle),
|
|
220
|
+
"--",
|
|
221
|
+
basename(bundle)
|
|
222
|
+
],
|
|
223
|
+
cwd: projectRoot,
|
|
224
|
+
env: {
|
|
225
|
+
...process.env,
|
|
226
|
+
COPYFILE_DISABLE: "1"
|
|
227
|
+
},
|
|
228
|
+
artifactsDir,
|
|
229
|
+
logName: "artifact",
|
|
230
|
+
signal,
|
|
231
|
+
onOutput: onProgress
|
|
232
|
+
});
|
|
233
|
+
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
|
+
signal?.throwIfAborted();
|
|
236
|
+
result.artifactPath = exporting;
|
|
237
|
+
result.exitCode = 0;
|
|
238
|
+
} catch (error) {
|
|
239
|
+
result.failure = failure(error, result.build ? "STIM_CI_ARTIFACT_FAILED" : "STIM_CI_BUILD_FAILED");
|
|
240
|
+
if (result.build && !result.failure.code.startsWith("STIM_")) result.failure.code = "STIM_CI_ARTIFACT_FAILED";
|
|
241
|
+
} finally {
|
|
242
|
+
reporting = true;
|
|
243
|
+
if (signal?.aborted) {
|
|
244
|
+
const timedOut = timeout?.aborted && !options.signal?.aborted;
|
|
245
|
+
result.exitCode = timedOut ? 124 : 130;
|
|
246
|
+
result.failure = {
|
|
247
|
+
code: timedOut ? "STIM_CI_TIMEOUT" : "STIM_CI_CANCELLED",
|
|
248
|
+
message: timedOut ? `CI build exceeded ${options.timeoutMs}ms.` : "CI build was cancelled."
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (exporting && !result.artifactPath) try {
|
|
252
|
+
rmSync(exporting, { force: true });
|
|
253
|
+
} catch (error) {
|
|
254
|
+
result.reportingError ??= failure(error, "STIM_CI_REPORT_FAILED");
|
|
255
|
+
}
|
|
256
|
+
if (attempted) try {
|
|
257
|
+
const diagnostics = await stim.diagnostics({
|
|
258
|
+
tail: 1e3,
|
|
259
|
+
signal: AbortSignal.timeout(1e4)
|
|
260
|
+
});
|
|
261
|
+
result.diagnostics.path = join(artifactsDir, "diagnostics.json");
|
|
262
|
+
writeJson(result.diagnostics.path, diagnostics);
|
|
263
|
+
} catch (error) {
|
|
264
|
+
result.diagnostics.error = failure(error, "STIM_CI_DIAGNOSTICS_FAILED");
|
|
265
|
+
}
|
|
266
|
+
if (result.reportingError && result.exitCode === 0) {
|
|
267
|
+
result.exitCode = 1;
|
|
268
|
+
result.failure = result.reportingError;
|
|
269
|
+
}
|
|
270
|
+
result.durationMs = Date.now() - started;
|
|
271
|
+
try {
|
|
272
|
+
writeJson(result.resultPath, result);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
result.reportingError = failure(error, "STIM_CI_REPORT_FAILED");
|
|
275
|
+
if (result.exitCode === 0) {
|
|
276
|
+
result.exitCode = 1;
|
|
277
|
+
result.failure = result.reportingError;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return result;
|
|
129
282
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
writeFileSync(pending, `${JSON.stringify(value, null, 2)}\n`);
|
|
133
|
-
renameSync(pending, path);
|
|
134
|
-
}
|
|
283
|
+
//#endregion
|
|
284
|
+
//#region src/index.ts
|
|
135
285
|
function commandEnvironment(result, options) {
|
|
136
286
|
const facts = result.run.facts;
|
|
137
287
|
return {
|
|
@@ -150,22 +300,9 @@ function commandEnvironment(result, options) {
|
|
|
150
300
|
async function runCI(options) {
|
|
151
301
|
if (!options.command[0]) throw new Error("A non-empty test command is required.");
|
|
152
302
|
for (const argument of options.command) if (typeof argument !== "string" || argument.includes("\0")) throw new Error("Test command arguments must be strings without null bytes.");
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
options = {
|
|
157
|
-
...options,
|
|
158
|
-
home: join(directory, "home"),
|
|
159
|
-
buildCache: options.buildCache || (process.env.STIM_BUILD_CACHE ? void 0 : join(directory, "build-cache"))
|
|
160
|
-
};
|
|
161
|
-
}
|
|
162
|
-
const projectRoot = realpathSync(options.projectRoot);
|
|
163
|
-
const artifactsDir = options.artifactsDir ? resolve(options.artifactsDir) : mkdtempSync(join(tmpdir(), "stim-ci-"));
|
|
164
|
-
mkdirSync(artifactsDir, { recursive: true });
|
|
165
|
-
if (readdirSync(artifactsDir).length > 0) throw new Error(`Artifacts directory must be empty: ${artifactsDir}. Choose a new directory for this run.`);
|
|
166
|
-
const timeout = options.timeoutMs === void 0 ? void 0 : AbortSignal.timeout(options.timeoutMs);
|
|
167
|
-
const signals = [options.signal, timeout].filter((value) => value !== void 0);
|
|
168
|
-
const signal = signals.length ? AbortSignal.any(signals) : void 0;
|
|
303
|
+
const prepared = prepareCI(options);
|
|
304
|
+
options = prepared.options;
|
|
305
|
+
const { projectRoot, artifactsDir, timeout, signal } = prepared;
|
|
169
306
|
const started = Date.now();
|
|
170
307
|
const result = {
|
|
171
308
|
version: 1,
|
|
@@ -288,4 +425,4 @@ async function runCI(options) {
|
|
|
288
425
|
return result;
|
|
289
426
|
}
|
|
290
427
|
//#endregion
|
|
291
|
-
export { runCI as t };
|
|
428
|
+
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-oSziKUiL.mjs").then(({ main }) => main());
|
|
10
10
|
//#endregion
|
|
11
11
|
export {};
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@stim-cli/ci",
|
|
3
|
-
"version": "1.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "1.20.0",
|
|
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": {
|
|
7
7
|
"type": "git",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
},
|
|
34
34
|
"dependencies": {
|
|
35
35
|
"cross-spawn": "^7.0.6",
|
|
36
|
-
"stim": "^1.
|
|
36
|
+
"stim": "^1.20.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@types/cross-spawn": "^6.0.6"
|
package/dist/cli-RvPAkat7.mjs
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { t as runCI } from "./src-BdobdbxB.mjs";
|
|
2
|
-
import { parseArgs } from "node:util";
|
|
3
|
-
//#region src/cli.ts
|
|
4
|
-
const HELP = `Usage: stim-ci run --platform <ios|android|macos|web> [options] -- <command> [args...]
|
|
5
|
-
|
|
6
|
-
Build and launch an app, run a test command, collect diagnostics, and stop it.
|
|
7
|
-
The final result is JSON on stdout; progress and test output go to stderr.
|
|
8
|
-
|
|
9
|
-
Options:
|
|
10
|
-
--project <path> Project directory (default: current directory)
|
|
11
|
-
--artifacts <path> Results directory (default: a fresh temporary directory)
|
|
12
|
-
--home <path> Explicit Stim home (default: job-local on GitHub-hosted runners)
|
|
13
|
-
--build-cache <path> Native artifact cache directory
|
|
14
|
-
--timeout <seconds> Setup and test timeout (cleanup has its own deadline)
|
|
15
|
-
--help Show this help
|
|
16
|
-
--version Show the package version
|
|
17
|
-
`;
|
|
18
|
-
function parse(argv) {
|
|
19
|
-
const separator = argv.indexOf("--");
|
|
20
|
-
if (separator < 0) throw new Error("Separate the test command with --.");
|
|
21
|
-
const command = argv.slice(separator + 1);
|
|
22
|
-
if (!command[0]) throw new Error("A test command is required after --.");
|
|
23
|
-
const { values, positionals } = parseArgs({
|
|
24
|
-
args: argv.slice(0, separator),
|
|
25
|
-
allowPositionals: true,
|
|
26
|
-
options: {
|
|
27
|
-
platform: { type: "string" },
|
|
28
|
-
project: { type: "string" },
|
|
29
|
-
artifacts: { type: "string" },
|
|
30
|
-
home: { type: "string" },
|
|
31
|
-
"build-cache": { type: "string" },
|
|
32
|
-
timeout: { type: "string" }
|
|
33
|
-
}
|
|
34
|
-
});
|
|
35
|
-
if (positionals.length !== 1 || positionals[0] !== "run") throw new Error("Expected stim-ci run.");
|
|
36
|
-
const platform = values.platform;
|
|
37
|
-
if (platform !== "ios" && platform !== "android" && platform !== "macos" && platform !== "web") throw new Error("--platform must be ios, android, macos or web.");
|
|
38
|
-
const timeoutMs = values.timeout === void 0 ? void 0 : Number(values.timeout) * 1e3;
|
|
39
|
-
return {
|
|
40
|
-
projectRoot: values.project ?? process.cwd(),
|
|
41
|
-
run: { platform },
|
|
42
|
-
command,
|
|
43
|
-
artifactsDir: values.artifacts,
|
|
44
|
-
home: values.home,
|
|
45
|
-
buildCache: values["build-cache"],
|
|
46
|
-
timeoutMs
|
|
47
|
-
};
|
|
48
|
-
}
|
|
49
|
-
async function main(argv = process.argv.slice(2)) {
|
|
50
|
-
if (argv.length === 1 && argv[0] === "--version") {
|
|
51
|
-
process.stdout.write(`1.19.0\n`);
|
|
52
|
-
return;
|
|
53
|
-
}
|
|
54
|
-
if (argv.length === 0 || argv[0] === "--help" || argv[0] === "run" && argv[1] === "--help") {
|
|
55
|
-
process.stdout.write(HELP);
|
|
56
|
-
return;
|
|
57
|
-
}
|
|
58
|
-
const controller = new AbortController();
|
|
59
|
-
const interrupt = () => controller.abort();
|
|
60
|
-
try {
|
|
61
|
-
const options = parse(argv);
|
|
62
|
-
process.on("SIGINT", interrupt);
|
|
63
|
-
process.on("SIGTERM", interrupt);
|
|
64
|
-
const result = await runCI({
|
|
65
|
-
...options,
|
|
66
|
-
signal: controller.signal,
|
|
67
|
-
onProgress: ({ message }) => process.stderr.write(message)
|
|
68
|
-
});
|
|
69
|
-
process.stdout.write(`${JSON.stringify(result)}\n`);
|
|
70
|
-
process.exitCode = result.exitCode;
|
|
71
|
-
} catch (error) {
|
|
72
|
-
process.stderr.write(`stim-ci: ${error.message}\n`);
|
|
73
|
-
process.exitCode = 1;
|
|
74
|
-
} finally {
|
|
75
|
-
process.off("SIGINT", interrupt);
|
|
76
|
-
process.off("SIGTERM", interrupt);
|
|
77
|
-
}
|
|
78
|
-
}
|
|
79
|
-
//#endregion
|
|
80
|
-
export { main };
|