shippilot 0.0.1
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/CHANGELOG.md +15 -0
- package/LICENSE +21 -0
- package/README.md +241 -0
- package/dist/auth/prepareCodexHome.d.ts +7 -0
- package/dist/auth/prepareCodexHome.js +52 -0
- package/dist/auth/prepareCodexHome.js.map +1 -0
- package/dist/auth/validateAuth.d.ts +2 -0
- package/dist/auth/validateAuth.js +19 -0
- package/dist/auth/validateAuth.js.map +1 -0
- package/dist/cases/parseCase.d.ts +13 -0
- package/dist/cases/parseCase.js +21 -0
- package/dist/cases/parseCase.js.map +1 -0
- package/dist/cases/resolveEnv.d.ts +6 -0
- package/dist/cases/resolveEnv.js +19 -0
- package/dist/cases/resolveEnv.js.map +1 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +219 -0
- package/dist/cli.js.map +1 -0
- package/dist/codex/outputSchema.d.ts +72 -0
- package/dist/codex/outputSchema.js +48 -0
- package/dist/codex/outputSchema.js.map +1 -0
- package/dist/codex/promptBuilder.d.ts +8 -0
- package/dist/codex/promptBuilder.js +48 -0
- package/dist/codex/promptBuilder.js.map +1 -0
- package/dist/codex/runWithSdk.d.ts +11 -0
- package/dist/codex/runWithSdk.js +396 -0
- package/dist/codex/runWithSdk.js.map +1 -0
- package/dist/config/loadConfig.d.ts +2 -0
- package/dist/config/loadConfig.js +13 -0
- package/dist/config/loadConfig.js.map +1 -0
- package/dist/config/schema.d.ts +43 -0
- package/dist/config/schema.js +71 -0
- package/dist/config/schema.js.map +1 -0
- package/dist/exitCodes.d.ts +7 -0
- package/dist/exitCodes.js +7 -0
- package/dist/exitCodes.js.map +1 -0
- package/dist/ios/doctorXcode.d.ts +3 -0
- package/dist/ios/doctorXcode.js +33 -0
- package/dist/ios/doctorXcode.js.map +1 -0
- package/dist/ios/xcodebuildmcp.d.ts +15 -0
- package/dist/ios/xcodebuildmcp.js +95 -0
- package/dist/ios/xcodebuildmcp.js.map +1 -0
- package/dist/reports/jsonReport.d.ts +11 -0
- package/dist/reports/jsonReport.js +27 -0
- package/dist/reports/jsonReport.js.map +1 -0
- package/dist/reports/junitReport.d.ts +4 -0
- package/dist/reports/junitReport.js +39 -0
- package/dist/reports/junitReport.js.map +1 -0
- package/dist/reports/markdownReport.d.ts +4 -0
- package/dist/reports/markdownReport.js +46 -0
- package/dist/reports/markdownReport.js.map +1 -0
- package/dist/security/redact.d.ts +5 -0
- package/dist/security/redact.js +12 -0
- package/dist/security/redact.js.map +1 -0
- package/docs/auth.md +47 -0
- package/docs/bitrise.md +22 -0
- package/docs/github-actions.md +47 -0
- package/docs/personal-chatgpt-subscription.md +31 -0
- package/docs/plan.md +55 -0
- package/docs/release.md +87 -0
- package/docs/security.md +39 -0
- package/examples/bitrise/shippilot.sh +6 -0
- package/examples/github-actions/shippilot.yml +39 -0
- package/examples/qa/login.md +15 -0
- package/examples/shippilot.yml +24 -0
- package/package.json +51 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to ShipPilot will be documented in this file.
|
|
4
|
+
|
|
5
|
+
## 0.0.1 - 2026-05-31
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
|
|
9
|
+
- Initial public npm release of the `shippilot` CLI.
|
|
10
|
+
- Markdown QA case parsing with required environment variable checks.
|
|
11
|
+
- Codex SDK execution path for agentic iOS QA runs.
|
|
12
|
+
- XcodeBuildMCP-backed iOS simulator doctor and run support.
|
|
13
|
+
- JSON, Markdown, and JUnit report output.
|
|
14
|
+
- GitHub Actions and Bitrise usage examples.
|
|
15
|
+
- npm release workflow for future trusted publishing releases.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Mahmoud Ashraf
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="assets/shippilot-icon.png" alt="ShipPilot icon" width="180">
|
|
3
|
+
</p>
|
|
4
|
+
|
|
5
|
+
# ShipPilot
|
|
6
|
+
|
|
7
|
+
[](https://github.com/mahmoudashraf93/ShipPilot/actions/workflows/ci.yml)
|
|
8
|
+
[](https://www.npmjs.com/package/shippilot)
|
|
9
|
+
[](LICENSE)
|
|
10
|
+
|
|
11
|
+
ShipPilot is an open-source agentic QA runner for mobile apps. v1 focuses on iOS simulator testing: teams write Markdown QA cases, run them from GitHub Actions, Bitrise, or local CI, and fail the pipeline when Codex cannot verify expected app behavior.
|
|
12
|
+
|
|
13
|
+
ShipPilot is intentionally test-and-report only. It does not edit source files, create patches, commit, push, or open pull requests.
|
|
14
|
+
|
|
15
|
+
## Quick Start
|
|
16
|
+
|
|
17
|
+
Prerequisites:
|
|
18
|
+
|
|
19
|
+
- macOS with Xcode for iOS simulator runs.
|
|
20
|
+
- Node.js 20+.
|
|
21
|
+
- One supported Codex auth mode.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx shippilot init
|
|
25
|
+
npx shippilot doctor
|
|
26
|
+
npx shippilot run --case qa/login.md --verbose
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## Usage Guide
|
|
30
|
+
|
|
31
|
+
ShipPilot reads `shippilot.yml` by default.
|
|
32
|
+
|
|
33
|
+
```yaml
|
|
34
|
+
codex:
|
|
35
|
+
engine: sdk
|
|
36
|
+
auth: api_key # api_key | access_token | chatgpt_hosted_experimental
|
|
37
|
+
model: default
|
|
38
|
+
sandbox: danger-full-access
|
|
39
|
+
fail_on: failed_or_blocked
|
|
40
|
+
verbose: false
|
|
41
|
+
allow_experimental_personal_hosted_auth: false
|
|
42
|
+
|
|
43
|
+
ios:
|
|
44
|
+
project: MyApp.xcodeproj
|
|
45
|
+
bundle_id:
|
|
46
|
+
scheme: MyApp
|
|
47
|
+
simulator: iPhone 17 Pro
|
|
48
|
+
backend: xcodebuildmcp
|
|
49
|
+
configuration: Debug
|
|
50
|
+
|
|
51
|
+
reports:
|
|
52
|
+
output_dir: .shippilot
|
|
53
|
+
markdown: true
|
|
54
|
+
json: true
|
|
55
|
+
junit: true
|
|
56
|
+
screenshots: true
|
|
57
|
+
logs: true
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Use either `ios.project` or `ios.workspace`, not both. Add `ios.bundle_id` when available; it avoids a separate bundle-id discovery step during CI.
|
|
61
|
+
|
|
62
|
+
### QA Cases
|
|
63
|
+
|
|
64
|
+
QA cases are Markdown files with YAML front matter:
|
|
65
|
+
|
|
66
|
+
```md
|
|
67
|
+
---
|
|
68
|
+
id: login-happy-path
|
|
69
|
+
title: Login happy path
|
|
70
|
+
required_env:
|
|
71
|
+
- TEST_EMAIL
|
|
72
|
+
- TEST_PASSWORD
|
|
73
|
+
tags:
|
|
74
|
+
- release
|
|
75
|
+
- smoke
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
Launch the app.
|
|
79
|
+
Dismiss onboarding, permission prompts, or paywalls if they appear.
|
|
80
|
+
Enter `${TEST_EMAIL}` and `${TEST_PASSWORD}`.
|
|
81
|
+
Tap Log In.
|
|
82
|
+
Expect the Home screen to be visible.
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Environment placeholders are resolved at runtime and redacted from prompts, logs, reports, and artifacts. Declare every secret placeholder in `required_env`; missing required variables fail setup before the agent runs.
|
|
86
|
+
|
|
87
|
+
### Running Cases
|
|
88
|
+
|
|
89
|
+
```bash
|
|
90
|
+
npx shippilot doctor
|
|
91
|
+
npx shippilot run --case qa/login.md
|
|
92
|
+
npx shippilot run --cases qa/
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
Use verbose mode while developing or debugging CI:
|
|
96
|
+
|
|
97
|
+
```bash
|
|
98
|
+
npx shippilot run --case qa/login.md --verbose
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Verbose mode streams XcodeBuildMCP output and Codex SDK events, including progress, tool calls, command executions, reasoning summaries, errors, and token usage. It does not expose private model chain-of-thought.
|
|
102
|
+
|
|
103
|
+
## Auth Modes
|
|
104
|
+
|
|
105
|
+
Choose one auth mode:
|
|
106
|
+
|
|
107
|
+
- `api_key`: uses `OPENAI_API_KEY`. Recommended for hosted CI and open-source projects.
|
|
108
|
+
- `access_token`: uses `CODEX_ACCESS_TOKEN`. Recommended for trusted Business/Enterprise automation.
|
|
109
|
+
- `chatgpt_hosted_experimental`: restores a pre-authenticated Codex home from `CODEX_HOME_TGZ_BASE64`. This is only for experimental personal ChatGPT subscription use on trusted runners.
|
|
110
|
+
|
|
111
|
+
The personal ChatGPT hosted-runner path is sensitive and fragile. Do not cache or upload restored auth directories, and do not run it on arbitrary fork PRs.
|
|
112
|
+
|
|
113
|
+
## Simulator Access And Security
|
|
114
|
+
|
|
115
|
+
For iOS simulator UI automation, use:
|
|
116
|
+
|
|
117
|
+
```yaml
|
|
118
|
+
codex:
|
|
119
|
+
sandbox: danger-full-access
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
XcodeBuildMCP needs access to CoreSimulator services outside the repository workspace. More restrictive sandbox modes can build the app but fail once the agent tries to inspect UI, tap controls, or capture screenshots.
|
|
123
|
+
|
|
124
|
+
Run ShipPilot only in trusted workflows when secrets are present. For open-source repositories, prefer `workflow_dispatch`, releases, schedules, or maintainer-approved workflows. Use `actions/checkout` with `persist-credentials: false`.
|
|
125
|
+
|
|
126
|
+
## GitHub Actions
|
|
127
|
+
|
|
128
|
+
```yaml
|
|
129
|
+
name: ShipPilot QA
|
|
130
|
+
|
|
131
|
+
on:
|
|
132
|
+
workflow_dispatch:
|
|
133
|
+
release:
|
|
134
|
+
types: [published]
|
|
135
|
+
|
|
136
|
+
jobs:
|
|
137
|
+
shippilot:
|
|
138
|
+
runs-on: macos-15
|
|
139
|
+
permissions:
|
|
140
|
+
contents: read
|
|
141
|
+
steps:
|
|
142
|
+
- uses: actions/checkout@v5
|
|
143
|
+
with:
|
|
144
|
+
persist-credentials: false
|
|
145
|
+
|
|
146
|
+
- uses: actions/setup-node@v4
|
|
147
|
+
with:
|
|
148
|
+
node-version: 22
|
|
149
|
+
|
|
150
|
+
- name: Run ShipPilot
|
|
151
|
+
env:
|
|
152
|
+
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
|
|
153
|
+
CODEX_ACCESS_TOKEN: ${{ secrets.CODEX_ACCESS_TOKEN }}
|
|
154
|
+
CODEX_HOME_TGZ_BASE64: ${{ secrets.CODEX_HOME_TGZ_BASE64 }}
|
|
155
|
+
TEST_EMAIL: ${{ secrets.TEST_EMAIL }}
|
|
156
|
+
TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }}
|
|
157
|
+
run: |
|
|
158
|
+
npx shippilot doctor
|
|
159
|
+
npx shippilot run --case qa/login.md --verbose
|
|
160
|
+
|
|
161
|
+
- name: Upload ShipPilot report
|
|
162
|
+
if: always()
|
|
163
|
+
uses: actions/upload-artifact@v4
|
|
164
|
+
with:
|
|
165
|
+
name: shippilot-report
|
|
166
|
+
path: .shippilot/
|
|
167
|
+
include-hidden-files: true
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## Bitrise
|
|
171
|
+
|
|
172
|
+
Use a macOS stack with Xcode and add a Script Step:
|
|
173
|
+
|
|
174
|
+
```bash
|
|
175
|
+
#!/usr/bin/env bash
|
|
176
|
+
set -euo pipefail
|
|
177
|
+
|
|
178
|
+
npm install -g shippilot
|
|
179
|
+
shippilot doctor
|
|
180
|
+
shippilot run --case qa/login.md --verbose
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Upload `.shippilot/` as build artifacts so failed QA runs still leave reports and screenshots.
|
|
184
|
+
|
|
185
|
+
## Reports And Exit Codes
|
|
186
|
+
|
|
187
|
+
Reports are written to `.shippilot/`:
|
|
188
|
+
|
|
189
|
+
```text
|
|
190
|
+
.shippilot/
|
|
191
|
+
run.json
|
|
192
|
+
report.md
|
|
193
|
+
junit.xml
|
|
194
|
+
logs/
|
|
195
|
+
screenshots/
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
ShipPilot exits like a test runner:
|
|
199
|
+
|
|
200
|
+
- `0`: all cases passed
|
|
201
|
+
- `1`: at least one case failed
|
|
202
|
+
- `2`: setup/auth/project/simulator/config error
|
|
203
|
+
- `3`: at least one case was blocked or inconclusive
|
|
204
|
+
|
|
205
|
+
Set `codex.fail_on: never` for report-only mode.
|
|
206
|
+
|
|
207
|
+
## Contribution Guide
|
|
208
|
+
|
|
209
|
+
Contributions should keep ShipPilot test-and-report focused. Avoid features that let the CI agent edit source, commit, push, or open PRs unless that behavior is explicitly designed behind a separate mode.
|
|
210
|
+
|
|
211
|
+
Before opening a PR:
|
|
212
|
+
|
|
213
|
+
```bash
|
|
214
|
+
npm run build
|
|
215
|
+
npm test
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
Useful areas to improve:
|
|
219
|
+
|
|
220
|
+
- config validation and clearer doctor checks
|
|
221
|
+
- QA case parsing and secret redaction
|
|
222
|
+
- report quality and artifact collection
|
|
223
|
+
- XcodeBuildMCP integration robustness
|
|
224
|
+
- CI examples for common hosted runners
|
|
225
|
+
|
|
226
|
+
## Roadmap
|
|
227
|
+
|
|
228
|
+
- Add richer screenshot and log attachments to reports.
|
|
229
|
+
- Add sample app integration tests.
|
|
230
|
+
- Add Bitrise Step packaging.
|
|
231
|
+
- Add Android support.
|
|
232
|
+
|
|
233
|
+
## Documentation
|
|
234
|
+
|
|
235
|
+
- [Full plan](docs/plan.md)
|
|
236
|
+
- [Auth modes](docs/auth.md)
|
|
237
|
+
- [GitHub Actions](docs/github-actions.md)
|
|
238
|
+
- [Bitrise](docs/bitrise.md)
|
|
239
|
+
- [Release process](docs/release.md)
|
|
240
|
+
- [Personal ChatGPT subscription](docs/personal-chatgpt-subscription.md)
|
|
241
|
+
- [Security](docs/security.md)
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { ShipPilotConfig } from "../config/schema.js";
|
|
2
|
+
export type PreparedAuth = {
|
|
3
|
+
apiKey?: string;
|
|
4
|
+
env: Record<string, string>;
|
|
5
|
+
cleanup: () => void;
|
|
6
|
+
};
|
|
7
|
+
export declare function prepareCodexAuth(config: ShipPilotConfig, env?: NodeJS.ProcessEnv): PreparedAuth;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { spawnSync } from "node:child_process";
|
|
5
|
+
function stringEnv(env = process.env) {
|
|
6
|
+
return Object.fromEntries(Object.entries(env).filter((entry) => typeof entry[1] === "string"));
|
|
7
|
+
}
|
|
8
|
+
function makeTempCodexHome() {
|
|
9
|
+
return mkdtempSync(path.join(os.tmpdir(), "shippilot-codex-home-"));
|
|
10
|
+
}
|
|
11
|
+
export function prepareCodexAuth(config, env = process.env) {
|
|
12
|
+
const baseEnv = stringEnv(env);
|
|
13
|
+
if (config.codex.auth === "api_key") {
|
|
14
|
+
return {
|
|
15
|
+
apiKey: env.OPENAI_API_KEY,
|
|
16
|
+
env: baseEnv,
|
|
17
|
+
cleanup: () => undefined,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (config.codex.auth === "access_token") {
|
|
21
|
+
const codexHome = makeTempCodexHome();
|
|
22
|
+
const result = spawnSync("codex", ["login", "--with-access-token"], {
|
|
23
|
+
input: env.CODEX_ACCESS_TOKEN,
|
|
24
|
+
encoding: "utf8",
|
|
25
|
+
env: { ...baseEnv, CODEX_HOME: codexHome },
|
|
26
|
+
});
|
|
27
|
+
if (result.status !== 0) {
|
|
28
|
+
rmSync(codexHome, { recursive: true, force: true });
|
|
29
|
+
throw new Error(`codex login --with-access-token failed: ${result.stderr || result.stdout}`);
|
|
30
|
+
}
|
|
31
|
+
return {
|
|
32
|
+
env: { ...baseEnv, CODEX_HOME: codexHome },
|
|
33
|
+
cleanup: () => rmSync(codexHome, { recursive: true, force: true }),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
const codexHome = makeTempCodexHome();
|
|
37
|
+
const archivePath = path.join(os.tmpdir(), `shippilot-codex-home-${Date.now()}.tgz`);
|
|
38
|
+
writeFileSync(archivePath, Buffer.from(String(env.CODEX_HOME_TGZ_BASE64), "base64"));
|
|
39
|
+
const result = spawnSync("tar", ["-xzf", archivePath, "-C", codexHome, "--strip-components=1"], {
|
|
40
|
+
encoding: "utf8",
|
|
41
|
+
});
|
|
42
|
+
rmSync(archivePath, { force: true });
|
|
43
|
+
if (result.status !== 0) {
|
|
44
|
+
rmSync(codexHome, { recursive: true, force: true });
|
|
45
|
+
throw new Error(`Failed to restore CODEX_HOME_TGZ_BASE64: ${result.stderr || result.stdout}`);
|
|
46
|
+
}
|
|
47
|
+
return {
|
|
48
|
+
env: { ...baseEnv, CODEX_HOME: codexHome },
|
|
49
|
+
cleanup: () => rmSync(codexHome, { recursive: true, force: true }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
//# sourceMappingURL=prepareCodexHome.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prepareCodexHome.js","sourceRoot":"","sources":["../../src/auth/prepareCodexHome.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function validateAuthConfig(config, env = process.env) {
|
|
2
|
+
const issues = [];
|
|
3
|
+
if (config.codex.auth === "api_key" && !env.OPENAI_API_KEY) {
|
|
4
|
+
issues.push("OPENAI_API_KEY is required for codex.auth: api_key.");
|
|
5
|
+
}
|
|
6
|
+
if (config.codex.auth === "access_token" && !env.CODEX_ACCESS_TOKEN) {
|
|
7
|
+
issues.push("CODEX_ACCESS_TOKEN is required for codex.auth: access_token.");
|
|
8
|
+
}
|
|
9
|
+
if (config.codex.auth === "chatgpt_hosted_experimental") {
|
|
10
|
+
if (!config.codex.allow_experimental_personal_hosted_auth) {
|
|
11
|
+
issues.push("chatgpt_hosted_experimental requires allow_experimental_personal_hosted_auth: true.");
|
|
12
|
+
}
|
|
13
|
+
if (!env.CODEX_HOME_TGZ_BASE64) {
|
|
14
|
+
issues.push("CODEX_HOME_TGZ_BASE64 is required for chatgpt_hosted_experimental.");
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return issues;
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=validateAuth.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"validateAuth.js","sourceRoot":"","sources":["../../src/auth/validateAuth.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,kBAAkB,CAAC,MAAuB,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG;IAC3E,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,GAAG,CAAC,cAAc,EAAE,CAAC;QAC3D,MAAM,CAAC,IAAI,CAAC,qDAAqD,CAAC,CAAC;IACrE,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,cAAc,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,CAAC;QACpE,MAAM,CAAC,IAAI,CAAC,8DAA8D,CAAC,CAAC;IAC9E,CAAC;IAED,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,KAAK,6BAA6B,EAAE,CAAC;QACxD,IAAI,CAAC,MAAM,CAAC,KAAK,CAAC,uCAAuC,EAAE,CAAC;YAC1D,MAAM,CAAC,IAAI,CACT,qFAAqF,CACtF,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,GAAG,CAAC,qBAAqB,EAAE,CAAC;YAC/B,MAAM,CAAC,IAAI,CAAC,oEAAoE,CAAC,CAAC;QACpF,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
declare const qaCaseFrontMatterSchema: z.ZodObject<{
|
|
3
|
+
id: z.ZodString;
|
|
4
|
+
title: z.ZodString;
|
|
5
|
+
required_env: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
6
|
+
tags: z.ZodDefault<z.ZodArray<z.ZodString>>;
|
|
7
|
+
}, z.core.$strip>;
|
|
8
|
+
export type QaCase = z.infer<typeof qaCaseFrontMatterSchema> & {
|
|
9
|
+
body: string;
|
|
10
|
+
path: string;
|
|
11
|
+
};
|
|
12
|
+
export declare function parseCase(casePath: string, cwd?: string): QaCase;
|
|
13
|
+
export {};
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import matter from "gray-matter";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
const qaCaseFrontMatterSchema = z.object({
|
|
6
|
+
id: z.string().min(1),
|
|
7
|
+
title: z.string().min(1),
|
|
8
|
+
required_env: z.array(z.string().min(1)).default([]),
|
|
9
|
+
tags: z.array(z.string()).default([]),
|
|
10
|
+
});
|
|
11
|
+
export function parseCase(casePath, cwd = process.cwd()) {
|
|
12
|
+
const absolutePath = path.resolve(cwd, casePath);
|
|
13
|
+
const parsed = matter(readFileSync(absolutePath, "utf8"));
|
|
14
|
+
const frontMatter = qaCaseFrontMatterSchema.parse(parsed.data);
|
|
15
|
+
return {
|
|
16
|
+
...frontMatter,
|
|
17
|
+
body: parsed.content.trim(),
|
|
18
|
+
path: absolutePath,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=parseCase.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"parseCase.js","sourceRoot":"","sources":["../../src/cases/parseCase.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AACvC,OAAO,IAAI,MAAM,WAAW,CAAC;AAC7B,OAAO,MAAM,MAAM,aAAa,CAAC;AACjC,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAExB,MAAM,uBAAuB,GAAG,CAAC,CAAC,MAAM,CAAC;IACvC,EAAE,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACrB,KAAK,EAAE,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC;IACxB,YAAY,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;IACpD,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC;CACtC,CAAC,CAAC;AAOH,MAAM,UAAU,SAAS,CAAC,QAAgB,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG,EAAE;IAC7D,MAAM,YAAY,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;IACjD,MAAM,MAAM,GAAG,MAAM,CAAC,YAAY,CAAC,YAAY,EAAE,MAAM,CAAC,CAAC,CAAC;IAC1D,MAAM,WAAW,GAAG,uBAAuB,CAAC,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;IAE/D,OAAO;QACL,GAAG,WAAW;QACd,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,IAAI,EAAE;QAC3B,IAAI,EAAE,YAAY;KACnB,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
export function resolveCaseEnv(qaCase, env = process.env) {
|
|
2
|
+
const missing = qaCase.required_env.filter((key) => !env[key]);
|
|
3
|
+
if (missing.length > 0) {
|
|
4
|
+
throw new Error(`Missing required env vars for ${qaCase.id}: ${missing.join(", ")}`);
|
|
5
|
+
}
|
|
6
|
+
const envValues = Object.fromEntries(qaCase.required_env.map((key) => [key, String(env[key] ?? "")]));
|
|
7
|
+
const resolvedBody = qaCase.body.replace(/\$\{([A-Z0-9_]+)\}/g, (match, key) => {
|
|
8
|
+
if (Object.prototype.hasOwnProperty.call(envValues, key)) {
|
|
9
|
+
return envValues[key];
|
|
10
|
+
}
|
|
11
|
+
return match;
|
|
12
|
+
});
|
|
13
|
+
return {
|
|
14
|
+
...qaCase,
|
|
15
|
+
resolvedBody,
|
|
16
|
+
envValues,
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=resolveEnv.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolveEnv.js","sourceRoot":"","sources":["../../src/cases/resolveEnv.ts"],"names":[],"mappings":"AAOA,MAAM,UAAU,cAAc,CAAC,MAAc,EAAE,GAAG,GAAG,OAAO,CAAC,GAAG;IAC9D,MAAM,OAAO,GAAG,MAAM,CAAC,YAAY,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAC/D,IAAI,OAAO,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACvB,MAAM,IAAI,KAAK,CAAC,iCAAiC,MAAM,CAAC,EAAE,KAAK,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;IACvF,CAAC;IAED,MAAM,SAAS,GAAG,MAAM,CAAC,WAAW,CAClC,MAAM,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,EAAE,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,CAChE,CAAC;IAEF,MAAM,YAAY,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,qBAAqB,EAAE,CAAC,KAAK,EAAE,GAAW,EAAE,EAAE;QACrF,IAAI,MAAM,CAAC,SAAS,CAAC,cAAc,CAAC,IAAI,CAAC,SAAS,EAAE,GAAG,CAAC,EAAE,CAAC;YACzD,OAAO,SAAS,CAAC,GAAG,CAAC,CAAC;QACxB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC,CAAC,CAAC;IAEH,OAAO;QACL,GAAG,MAAM;QACT,YAAY;QACZ,SAAS;KACV,CAAC;AACJ,CAAC"}
|
package/dist/cli.d.ts
ADDED
package/dist/cli.js
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { Command } from "commander";
|
|
4
|
+
import fg from "fast-glob";
|
|
5
|
+
import { loadConfig } from "./config/loadConfig.js";
|
|
6
|
+
import { validateAuthConfig } from "./auth/validateAuth.js";
|
|
7
|
+
import { parseCase } from "./cases/parseCase.js";
|
|
8
|
+
import { resolveCaseEnv } from "./cases/resolveEnv.js";
|
|
9
|
+
import { createRedactor } from "./security/redact.js";
|
|
10
|
+
import { doctorXcode } from "./ios/doctorXcode.js";
|
|
11
|
+
import { runCaseWithSdk } from "./codex/runWithSdk.js";
|
|
12
|
+
import { writeJsonReport, readJsonReport } from "./reports/jsonReport.js";
|
|
13
|
+
import { writeMarkdownReport } from "./reports/markdownReport.js";
|
|
14
|
+
import { writeJunitReport } from "./reports/junitReport.js";
|
|
15
|
+
import { ExitCodes } from "./exitCodes.js";
|
|
16
|
+
function writeReports(configPath, report) {
|
|
17
|
+
const config = loadConfig(configPath);
|
|
18
|
+
if (config.reports.markdown)
|
|
19
|
+
writeMarkdownReport(config, report);
|
|
20
|
+
if (config.reports.junit)
|
|
21
|
+
writeJunitReport(config, report);
|
|
22
|
+
}
|
|
23
|
+
function exitCodeFor(report, failOn) {
|
|
24
|
+
if (failOn === "never")
|
|
25
|
+
return ExitCodes.success;
|
|
26
|
+
if (report.status === "failed")
|
|
27
|
+
return ExitCodes.failed;
|
|
28
|
+
if (report.status === "blocked")
|
|
29
|
+
return ExitCodes.blocked;
|
|
30
|
+
return ExitCodes.success;
|
|
31
|
+
}
|
|
32
|
+
function printCheck(name, ok, detail) {
|
|
33
|
+
const marker = ok ? "PASS" : "FAIL";
|
|
34
|
+
console.log(`${marker} ${name}${detail ? `: ${detail.split("\n")[0]}` : ""}`);
|
|
35
|
+
}
|
|
36
|
+
async function main() {
|
|
37
|
+
const program = new Command();
|
|
38
|
+
program
|
|
39
|
+
.name("shippilot")
|
|
40
|
+
.description("ShipPilot: agentic iOS QA runner for Codex")
|
|
41
|
+
.version("0.0.1")
|
|
42
|
+
.option("-c, --config <path>", "config file path", "shippilot.yml");
|
|
43
|
+
program.command("init").description("Scaffold ShipPilot config, sample case, and CI templates").action(() => {
|
|
44
|
+
mkdirSync("qa", { recursive: true });
|
|
45
|
+
mkdirSync(".github/workflows", { recursive: true });
|
|
46
|
+
mkdirSync("bitrise", { recursive: true });
|
|
47
|
+
writeFileSync("shippilot.yml", `codex:
|
|
48
|
+
engine: sdk
|
|
49
|
+
auth: api_key
|
|
50
|
+
model: default
|
|
51
|
+
sandbox: danger-full-access
|
|
52
|
+
fail_on: failed_or_blocked
|
|
53
|
+
allow_experimental_personal_hosted_auth: false
|
|
54
|
+
verbose: false
|
|
55
|
+
|
|
56
|
+
ios:
|
|
57
|
+
project: MyApp.xcodeproj
|
|
58
|
+
bundle_id:
|
|
59
|
+
scheme: MyApp
|
|
60
|
+
simulator: iPhone 17 Pro
|
|
61
|
+
backend: xcodebuildmcp
|
|
62
|
+
configuration: Debug
|
|
63
|
+
|
|
64
|
+
reports:
|
|
65
|
+
output_dir: .shippilot
|
|
66
|
+
markdown: true
|
|
67
|
+
json: true
|
|
68
|
+
junit: true
|
|
69
|
+
screenshots: true
|
|
70
|
+
logs: true
|
|
71
|
+
`);
|
|
72
|
+
writeFileSync("qa/login.md", `---
|
|
73
|
+
id: login-happy-path
|
|
74
|
+
title: Login happy path
|
|
75
|
+
required_env:
|
|
76
|
+
- TEST_EMAIL
|
|
77
|
+
- TEST_PASSWORD
|
|
78
|
+
tags:
|
|
79
|
+
- release
|
|
80
|
+
- smoke
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
Launch the app.
|
|
84
|
+
Enter \${TEST_EMAIL} and \${TEST_PASSWORD}.
|
|
85
|
+
Tap Log In.
|
|
86
|
+
Expect the Home screen to be visible.
|
|
87
|
+
`);
|
|
88
|
+
writeFileSync(".github/workflows/shippilot.yml", `name: ShipPilot QA
|
|
89
|
+
|
|
90
|
+
on:
|
|
91
|
+
workflow_dispatch:
|
|
92
|
+
release:
|
|
93
|
+
types: [published]
|
|
94
|
+
|
|
95
|
+
jobs:
|
|
96
|
+
shippilot:
|
|
97
|
+
runs-on: macos-15
|
|
98
|
+
permissions:
|
|
99
|
+
contents: read
|
|
100
|
+
steps:
|
|
101
|
+
- uses: actions/checkout@v5
|
|
102
|
+
with:
|
|
103
|
+
persist-credentials: false
|
|
104
|
+
|
|
105
|
+
- uses: actions/setup-node@v4
|
|
106
|
+
with:
|
|
107
|
+
node-version: 22
|
|
108
|
+
|
|
109
|
+
- name: Run ShipPilot
|
|
110
|
+
env:
|
|
111
|
+
OPENAI_API_KEY: \${{ secrets.OPENAI_API_KEY }}
|
|
112
|
+
CODEX_ACCESS_TOKEN: \${{ secrets.CODEX_ACCESS_TOKEN }}
|
|
113
|
+
CODEX_HOME_TGZ_BASE64: \${{ secrets.CODEX_HOME_TGZ_BASE64 }}
|
|
114
|
+
TEST_EMAIL: \${{ secrets.TEST_EMAIL }}
|
|
115
|
+
TEST_PASSWORD: \${{ secrets.TEST_PASSWORD }}
|
|
116
|
+
run: |
|
|
117
|
+
npx shippilot doctor
|
|
118
|
+
npx shippilot run --case qa/login.md
|
|
119
|
+
|
|
120
|
+
- name: Upload ShipPilot report
|
|
121
|
+
if: always()
|
|
122
|
+
uses: actions/upload-artifact@v4
|
|
123
|
+
with:
|
|
124
|
+
name: shippilot-report
|
|
125
|
+
path: .shippilot/
|
|
126
|
+
`);
|
|
127
|
+
writeFileSync("bitrise/shippilot.sh", `#!/usr/bin/env bash
|
|
128
|
+
set -euo pipefail
|
|
129
|
+
|
|
130
|
+
npm install -g shippilot
|
|
131
|
+
shippilot doctor
|
|
132
|
+
shippilot run --case qa/login.md
|
|
133
|
+
`, { mode: 0o755 });
|
|
134
|
+
console.log("Created shippilot.yml, qa/login.md, and CI templates.");
|
|
135
|
+
});
|
|
136
|
+
program.command("doctor").description("Validate config, auth, Xcode, XcodeBuildMCP, and project inputs").action(() => {
|
|
137
|
+
const options = program.opts();
|
|
138
|
+
try {
|
|
139
|
+
const config = loadConfig(options.config);
|
|
140
|
+
const authIssues = validateAuthConfig(config);
|
|
141
|
+
for (const issue of authIssues)
|
|
142
|
+
printCheck("auth", false, issue);
|
|
143
|
+
const checks = doctorXcode(config);
|
|
144
|
+
for (const check of checks)
|
|
145
|
+
printCheck(check.name, check.ok, check.detail);
|
|
146
|
+
if (authIssues.length > 0 || checks.some((check) => !check.ok)) {
|
|
147
|
+
process.exitCode = ExitCodes.setupError;
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
printCheck("ShipPilot doctor", true, "ready");
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
154
|
+
process.exitCode = ExitCodes.setupError;
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
program
|
|
158
|
+
.command("run")
|
|
159
|
+
.description("Run one or more ShipPilot QA cases")
|
|
160
|
+
.option("--case <path>", "single QA case Markdown file")
|
|
161
|
+
.option("--cases <glob>", "QA case glob or directory")
|
|
162
|
+
.option("--verbose", "stream XcodeBuildMCP and Codex SDK events")
|
|
163
|
+
.action(async (runOptions) => {
|
|
164
|
+
const options = program.opts();
|
|
165
|
+
const startedAt = new Date().toISOString();
|
|
166
|
+
try {
|
|
167
|
+
const config = loadConfig(options.config);
|
|
168
|
+
const authIssues = validateAuthConfig(config);
|
|
169
|
+
if (authIssues.length > 0)
|
|
170
|
+
throw new Error(authIssues.join("\n"));
|
|
171
|
+
const casePaths = runOptions.case
|
|
172
|
+
? [runOptions.case]
|
|
173
|
+
: await fg(runOptions.cases ? `${runOptions.cases.replace(/\/$/, "")}/**/*.md` : "qa/**/*.md");
|
|
174
|
+
if (casePaths.length === 0)
|
|
175
|
+
throw new Error("No QA cases found.");
|
|
176
|
+
const records = [];
|
|
177
|
+
for (const casePath of casePaths) {
|
|
178
|
+
const qaCase = parseCase(casePath);
|
|
179
|
+
const resolved = resolveCaseEnv(qaCase);
|
|
180
|
+
const redactor = createRedactor(Object.values(resolved.envValues));
|
|
181
|
+
console.log(`Running ${qaCase.id}: ${qaCase.title}`);
|
|
182
|
+
records.push(await runCaseWithSdk(config, resolved, redactor, process.cwd(), runOptions.verbose ?? config.codex.verbose));
|
|
183
|
+
}
|
|
184
|
+
const report = writeJsonReport(config, records, startedAt);
|
|
185
|
+
if (config.reports.markdown)
|
|
186
|
+
writeMarkdownReport(config, report);
|
|
187
|
+
if (config.reports.junit)
|
|
188
|
+
writeJunitReport(config, report);
|
|
189
|
+
console.log(`ShipPilot completed with status: ${report.status}`);
|
|
190
|
+
process.exitCode = exitCodeFor(report, config.codex.fail_on);
|
|
191
|
+
}
|
|
192
|
+
catch (error) {
|
|
193
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
194
|
+
process.exitCode = ExitCodes.setupError;
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
program
|
|
198
|
+
.command("report")
|
|
199
|
+
.description("Regenerate reports from a saved run.json")
|
|
200
|
+
.requiredOption("--run <path>", "path to run.json")
|
|
201
|
+
.action((reportOptions) => {
|
|
202
|
+
const options = program.opts();
|
|
203
|
+
try {
|
|
204
|
+
const report = readJsonReport(reportOptions.run);
|
|
205
|
+
writeReports(options.config, report);
|
|
206
|
+
console.log("Regenerated ShipPilot reports.");
|
|
207
|
+
}
|
|
208
|
+
catch (error) {
|
|
209
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
210
|
+
process.exitCode = ExitCodes.setupError;
|
|
211
|
+
}
|
|
212
|
+
});
|
|
213
|
+
await program.parseAsync(process.argv);
|
|
214
|
+
}
|
|
215
|
+
main().catch((error) => {
|
|
216
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
217
|
+
process.exitCode = ExitCodes.setupError;
|
|
218
|
+
});
|
|
219
|
+
//# sourceMappingURL=cli.js.map
|