@reshotdev/screenshot 0.0.1-beta.1 → 0.0.1-beta.11
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 +65 -7
- package/package.json +9 -2
- package/src/commands/auth.js +108 -26
- package/src/commands/certify.js +62 -0
- package/src/commands/ci-run.js +57 -2
- package/src/commands/ci-setup.js +5 -5
- package/src/commands/doctor-release.js +67 -0
- package/src/commands/doctor-target.js +49 -0
- package/src/commands/drifts.js +5 -70
- package/src/commands/import-tests.js +13 -13
- package/src/commands/ingest.js +10 -10
- package/src/commands/init.js +16 -277
- package/src/commands/publish.js +204 -237
- package/src/commands/pull.js +253 -23
- package/src/commands/run.js +292 -12
- package/src/commands/setup-wizard.js +277 -499
- package/src/commands/setup.js +41 -13
- package/src/commands/status.js +313 -125
- package/src/commands/sync.js +28 -236
- package/src/commands/ui.js +1 -1
- package/src/commands/verify-publish.js +46 -0
- package/src/index.js +194 -94
- package/src/lib/api-client.js +121 -35
- package/src/lib/capture-engine.js +103 -7
- package/src/lib/capture-script-runner.js +359 -58
- package/src/lib/certification.js +865 -0
- package/src/lib/config.js +181 -76
- package/src/lib/record-cdp.js +288 -16
- package/src/lib/record-config.js +1 -1
- package/src/lib/release-doctor.js +313 -0
- package/src/lib/run-manifest.js +103 -0
- package/src/lib/standalone-mode.js +1 -1
- package/src/lib/storage-providers.js +4 -4
- package/src/lib/target-contract.js +292 -0
- package/src/lib/ui-api.js +6 -7
- package/web/manager/dist/assets/{index--ZgioErz.js → index-D2qqcFNN.js} +1 -1
- package/web/manager/dist/index.html +1 -1
- package/src/commands/validate-docs.js +0 -529
package/README.md
CHANGED
|
@@ -21,20 +21,47 @@ Requires Node.js >= 18. Playwright browsers are installed automatically on first
|
|
|
21
21
|
# 1. Interactive setup wizard
|
|
22
22
|
reshot setup
|
|
23
23
|
|
|
24
|
-
# 2.
|
|
24
|
+
# 2. Start your app with a production-like local server
|
|
25
|
+
npm run build
|
|
26
|
+
npm run start
|
|
27
|
+
|
|
28
|
+
# 3. Capture screenshots from your config
|
|
25
29
|
reshot run
|
|
26
30
|
|
|
27
|
-
#
|
|
31
|
+
# 4. Review captures in the web UI
|
|
28
32
|
reshot studio
|
|
33
|
+
|
|
34
|
+
# 5. Publish when you want hosted assets
|
|
35
|
+
reshot publish
|
|
29
36
|
```
|
|
30
37
|
|
|
38
|
+
For launch-grade reliability, do not treat `next dev` as the supported capture
|
|
39
|
+
runtime. Use a production-like local server and see the
|
|
40
|
+
[Supported Environments guide](https://reshot.dev/docs/cli/getting-started/supported-environments).
|
|
41
|
+
|
|
42
|
+
## Certified Targets
|
|
43
|
+
|
|
44
|
+
This release adds a **Certified Targets** contract for apps that need stronger guarantees than ad hoc capture. Certified targets declare their readiness selectors, auth mode, required routes, and expected published assets in `reshot.config.json`, then pass the full doctor/capture/publish/delivery pipeline before release.
|
|
45
|
+
|
|
31
46
|
## Configuration
|
|
32
47
|
|
|
33
|
-
Create `
|
|
48
|
+
Create `reshot.config.json` in your project root:
|
|
34
49
|
|
|
35
50
|
```json
|
|
36
51
|
{
|
|
37
52
|
"baseUrl": "http://localhost:3000",
|
|
53
|
+
"target": {
|
|
54
|
+
"key": "docs-app",
|
|
55
|
+
"displayName": "Docs App",
|
|
56
|
+
"tier": "certified",
|
|
57
|
+
"owner": "Docs Team",
|
|
58
|
+
"baseUrl": "http://localhost:3000",
|
|
59
|
+
"captureSafe": false,
|
|
60
|
+
"supportedLocalCommand": "npm run build && npm run start",
|
|
61
|
+
"defaultAuthMode": "fixture",
|
|
62
|
+
"requiredEnv": ["PROJECT_ID"],
|
|
63
|
+
"certificationScenarioKeys": ["dashboard"]
|
|
64
|
+
},
|
|
38
65
|
"assetDir": ".reshot/output",
|
|
39
66
|
"concurrency": 2,
|
|
40
67
|
"viewport": { "width": 1280, "height": 720 },
|
|
@@ -55,6 +82,15 @@ Create `docsync.config.json` in your project root:
|
|
|
55
82
|
"name": "Dashboard",
|
|
56
83
|
"url": "/dashboard",
|
|
57
84
|
"requiresAuth": true,
|
|
85
|
+
"captureClass": "fixture-auth",
|
|
86
|
+
"ready": {
|
|
87
|
+
"selector": "[data-loaded='true']",
|
|
88
|
+
"expression": "window.__APP_READY__ === true"
|
|
89
|
+
},
|
|
90
|
+
"requiredRoutes": ["/dashboard"],
|
|
91
|
+
"requiredSelectors": ["[data-testid='dashboard-content']"],
|
|
92
|
+
"expectedArtifacts": ["overview", "analytics"],
|
|
93
|
+
"publishPolicy": "required",
|
|
58
94
|
"readySelector": "[data-loaded='true']",
|
|
59
95
|
"steps": [
|
|
60
96
|
{ "action": "screenshot", "key": "overview", "description": "Dashboard overview" },
|
|
@@ -76,15 +112,30 @@ Create `docsync.config.json` in your project root:
|
|
|
76
112
|
| `reshot record [title]` | Interactive recording via Chrome DevTools | `--browser`, `--url`, `--port` |
|
|
77
113
|
| `reshot sync` | Upload traces/docs to Reshot platform | `--trace-dir`, `--dry-run` |
|
|
78
114
|
| `reshot studio` | Launch web management UI | `--port`, `--no-open` |
|
|
79
|
-
| `reshot validate` | Check config and bindings | `--strict`, `--fix` |
|
|
80
115
|
| `reshot status` | View project status and sync history | `--jobs`, `--drifts`, `--json` |
|
|
81
116
|
| `reshot publish` | Upload assets with versioning | `--tag`, `--message`, `--dry-run` |
|
|
82
117
|
| `reshot pull` | Generate asset map for builds | `--format json\|ts\|csv`, `--output`, `--status` |
|
|
118
|
+
| `reshot doctor target` | Audit target routes, readiness, and auth contract | `--scenarios`, `--json` |
|
|
119
|
+
| `reshot verify publish` | Validate publish, pull/export, and hosted delivery | `--scenarios`, `--tag`, `--json` |
|
|
120
|
+
| `reshot certify` | Run the full certified-target pipeline | `--scenarios`, `--tag`, `--json` |
|
|
83
121
|
| `reshot drifts` | Manage visual drift notifications | `approve`, `reject`, `ignore`, `approve-all` |
|
|
84
122
|
| `reshot import-tests` | Import Playwright tests as scenarios | `--dry-run`, `--no-interactive` |
|
|
85
123
|
| `reshot ci setup` | Generate CI/CD workflow files | — |
|
|
86
124
|
| `reshot ci run` | Capture + publish in one step (CI) | `--tag`, `--no-publish`, `--dry-run` |
|
|
87
125
|
|
|
126
|
+
## Certification Workflow
|
|
127
|
+
|
|
128
|
+
Use these commands when a target app needs release-grade verification:
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
reshot doctor target
|
|
132
|
+
reshot run --scenarios dashboard
|
|
133
|
+
reshot verify publish --tag v1.0.0
|
|
134
|
+
reshot certify --tag v1.0.0
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Certification reports are written to `.reshot/reports/certification.json`.
|
|
138
|
+
|
|
88
139
|
## Step Types
|
|
89
140
|
|
|
90
141
|
Steps define a sequence of browser actions within a scenario:
|
|
@@ -221,16 +272,23 @@ During recording:
|
|
|
221
272
|
- Press **C** to start/stop a video clip
|
|
222
273
|
- Press **Q** to quit and save
|
|
223
274
|
|
|
224
|
-
The recorded scenario is appended to `
|
|
275
|
+
The recorded scenario is appended to `reshot.config.json` automatically.
|
|
225
276
|
|
|
226
277
|
## Authentication
|
|
227
278
|
|
|
228
|
-
### Storage State
|
|
279
|
+
### Storage State
|
|
280
|
+
|
|
281
|
+
The CLI stores browser session state at `~/.reshot/session-state.json` (global).
|
|
282
|
+
This is automatically captured when you run `reshot record`.
|
|
283
|
+
|
|
284
|
+
To manually generate it:
|
|
229
285
|
|
|
230
286
|
```bash
|
|
231
|
-
npx playwright codegen http://localhost:3000 --save-storage
|
|
287
|
+
npx playwright codegen http://localhost:3000 --save-storage=$HOME/.reshot/session-state.json
|
|
232
288
|
```
|
|
233
289
|
|
|
290
|
+
Or reference a project-local path in your config:
|
|
291
|
+
|
|
234
292
|
```json
|
|
235
293
|
{
|
|
236
294
|
"storageStatePath": ".reshot/auth-state.json"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@reshotdev/screenshot",
|
|
3
|
-
"version": "0.0.1-beta.
|
|
3
|
+
"version": "0.0.1-beta.11",
|
|
4
4
|
"description": "CI/CD screenshot and video capture CLI",
|
|
5
5
|
"author": "Reshot <hello@reshot.dev>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -13,7 +13,14 @@
|
|
|
13
13
|
"publishConfig": {
|
|
14
14
|
"access": "public"
|
|
15
15
|
},
|
|
16
|
-
"keywords": [
|
|
16
|
+
"keywords": [
|
|
17
|
+
"screenshots",
|
|
18
|
+
"ci-cd",
|
|
19
|
+
"documentation",
|
|
20
|
+
"visual-testing",
|
|
21
|
+
"automation",
|
|
22
|
+
"playwright"
|
|
23
|
+
],
|
|
17
24
|
"bin": {
|
|
18
25
|
"reshot": "./src/index.js"
|
|
19
26
|
},
|
package/src/commands/auth.js
CHANGED
|
@@ -14,6 +14,7 @@ const pkg = require("../../package.json");
|
|
|
14
14
|
|
|
15
15
|
const DEFAULT_CALLBACK_PORT = 3721;
|
|
16
16
|
const POLL_INTERVAL_MS = 2000;
|
|
17
|
+
const DEFAULT_AUTH_TIMEOUT_MS = 120000;
|
|
17
18
|
|
|
18
19
|
const unwrapResponse = (payload) => {
|
|
19
20
|
if (!payload) {
|
|
@@ -95,15 +96,27 @@ function startLocalStatusServer(requestedPort, options = {}) {
|
|
|
95
96
|
});
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
async function waitForCompletion(
|
|
99
|
+
async function waitForCompletion(
|
|
100
|
+
apiBaseUrl,
|
|
101
|
+
authToken,
|
|
102
|
+
expiresAtIso,
|
|
103
|
+
options = {},
|
|
104
|
+
) {
|
|
105
|
+
const httpClient = options.httpClient || axios;
|
|
106
|
+
const spinnerFactory = options.spinnerFactory || ora;
|
|
99
107
|
const expiresAt = expiresAtIso
|
|
100
108
|
? Date.parse(expiresAtIso)
|
|
101
109
|
: Date.now() + 5 * 60 * 1000;
|
|
102
|
-
const
|
|
110
|
+
const timeoutMs = Math.max(
|
|
111
|
+
1,
|
|
112
|
+
Number(options.timeoutMs || DEFAULT_AUTH_TIMEOUT_MS),
|
|
113
|
+
);
|
|
114
|
+
const deadline = Math.min(expiresAt, Date.now() + timeoutMs);
|
|
115
|
+
const statusSpinner = spinnerFactory("Waiting for browser authentication…").start();
|
|
103
116
|
|
|
104
117
|
try {
|
|
105
|
-
while (Date.now() <
|
|
106
|
-
const statusResponse = await
|
|
118
|
+
while (Date.now() < deadline) {
|
|
119
|
+
const statusResponse = await httpClient.get(`${apiBaseUrl}/auth/cli/status`, {
|
|
107
120
|
params: { token: authToken },
|
|
108
121
|
});
|
|
109
122
|
const payload = unwrapResponse(statusResponse.data);
|
|
@@ -127,39 +140,70 @@ async function waitForCompletion(apiBaseUrl, authToken, expiresAtIso) {
|
|
|
127
140
|
await wait(POLL_INTERVAL_MS);
|
|
128
141
|
}
|
|
129
142
|
|
|
130
|
-
throw new Error(
|
|
143
|
+
throw new Error(
|
|
144
|
+
"Authentication timed out before completion. Re-run `reshot auth` and use the printed auth URL if the browser handoff stalls.",
|
|
145
|
+
);
|
|
131
146
|
} catch (error) {
|
|
132
147
|
statusSpinner.fail("Browser authentication failed");
|
|
133
148
|
throw error;
|
|
134
149
|
}
|
|
135
150
|
}
|
|
136
151
|
|
|
137
|
-
async function verifyApiKey(apiBaseUrl, apiKey) {
|
|
138
|
-
await
|
|
152
|
+
async function verifyApiKey(apiBaseUrl, apiKey, httpClient = axios) {
|
|
153
|
+
await httpClient.get(`${apiBaseUrl}/auth/cli/verify`, {
|
|
139
154
|
headers: {
|
|
140
155
|
Authorization: `Bearer ${apiKey}`,
|
|
141
156
|
},
|
|
142
157
|
});
|
|
143
158
|
}
|
|
144
159
|
|
|
145
|
-
async function authCommand() {
|
|
146
|
-
|
|
160
|
+
async function authCommand(options = {}) {
|
|
161
|
+
// Support non-interactive auth via environment variables (for CI/CD)
|
|
162
|
+
const envApiKey = process.env.RESHOT_API_KEY;
|
|
163
|
+
const envProjectId = process.env.RESHOT_PROJECT_ID;
|
|
164
|
+
const httpClient = options.httpClient || axios;
|
|
165
|
+
const openFn = options.openFn || open;
|
|
166
|
+
const writeSettingsFn = options.writeSettingsFn || writeSettings;
|
|
167
|
+
const startLocalStatusServerFn =
|
|
168
|
+
options.startLocalStatusServerFn || startLocalStatusServer;
|
|
169
|
+
const waitForCompletionFn = options.waitForCompletionFn || waitForCompletion;
|
|
170
|
+
const verifyApiKeyFn = options.verifyApiKeyFn || verifyApiKey;
|
|
171
|
+
const spinnerFactory = options.spinnerFactory || ora;
|
|
172
|
+
const timeoutMs = Number(options.timeoutMs || DEFAULT_AUTH_TIMEOUT_MS);
|
|
173
|
+
if (envApiKey && envProjectId) {
|
|
174
|
+
const platformUrl = process.env.RESHOT_PLATFORM_URL || "https://reshot.dev";
|
|
175
|
+
writeSettingsFn({
|
|
176
|
+
projectId: envProjectId,
|
|
177
|
+
apiKey: envApiKey,
|
|
178
|
+
platformUrl,
|
|
179
|
+
linkedAt: new Date().toISOString(),
|
|
180
|
+
cliVersion: pkg.version,
|
|
181
|
+
});
|
|
182
|
+
console.log(chalk.green("✔ Authenticated via environment variables"));
|
|
183
|
+
console.log(chalk.gray(` Project: ${envProjectId}`));
|
|
184
|
+
console.log(chalk.gray(` Platform: ${platformUrl}`));
|
|
185
|
+
return {
|
|
186
|
+
mode: "cloud-connected",
|
|
187
|
+
projectId: envProjectId,
|
|
188
|
+
platformUrl,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const apiBaseUrl = options.apiBaseUrl || getApiBaseUrl();
|
|
147
193
|
const explicitPortEnv =
|
|
148
|
-
process.env.RESHOT_CLI_CALLBACK_PORT ||
|
|
149
|
-
process.env.DOCSYNC_CLI_CALLBACK_PORT ||
|
|
150
|
-
"";
|
|
194
|
+
process.env.RESHOT_CLI_CALLBACK_PORT || "";
|
|
151
195
|
const basePort = parseInt(explicitPortEnv || `${DEFAULT_CALLBACK_PORT}`, 10);
|
|
152
196
|
const hasExplicitPort = Boolean(explicitPortEnv);
|
|
153
197
|
|
|
154
198
|
let localServer;
|
|
155
199
|
let callbackPort;
|
|
156
|
-
const spinner =
|
|
200
|
+
const spinner = spinnerFactory("Requesting authentication session…").start();
|
|
157
201
|
|
|
158
202
|
try {
|
|
159
203
|
if (hasExplicitPort) {
|
|
160
204
|
// Respect an explicitly configured port and fail fast with a clear error
|
|
161
205
|
// if it is not available.
|
|
162
|
-
const { server, port } = await
|
|
206
|
+
const { server, port } = await startLocalStatusServerFn(basePort, {
|
|
163
207
|
explicit: true,
|
|
164
208
|
});
|
|
165
209
|
localServer = server;
|
|
@@ -169,12 +213,12 @@ async function authCommand() {
|
|
|
169
213
|
// but automatically fall back to any available port so users never have
|
|
170
214
|
// to think about port conflicts.
|
|
171
215
|
try {
|
|
172
|
-
const { server, port } = await
|
|
216
|
+
const { server, port } = await startLocalStatusServerFn(basePort);
|
|
173
217
|
localServer = server;
|
|
174
218
|
callbackPort = port;
|
|
175
219
|
} catch (error) {
|
|
176
220
|
if (error && error.code === "EADDRINUSE") {
|
|
177
|
-
const { server, port } = await
|
|
221
|
+
const { server, port } = await startLocalStatusServerFn(0);
|
|
178
222
|
localServer = server;
|
|
179
223
|
callbackPort = port;
|
|
180
224
|
console.log(
|
|
@@ -188,7 +232,7 @@ async function authCommand() {
|
|
|
188
232
|
}
|
|
189
233
|
}
|
|
190
234
|
|
|
191
|
-
const initiateResponse = await
|
|
235
|
+
const initiateResponse = await httpClient.post(
|
|
192
236
|
`${apiBaseUrl}/auth/cli/initiate`,
|
|
193
237
|
{
|
|
194
238
|
callbackPort,
|
|
@@ -207,21 +251,47 @@ async function authCommand() {
|
|
|
207
251
|
spinner.succeed("Authentication session created");
|
|
208
252
|
console.log(chalk.gray(`Token expires at ${expiresAt || "unknown time"}`));
|
|
209
253
|
console.log(chalk.gray(`Settings will be stored in ${SETTINGS_PATH}`));
|
|
210
|
-
|
|
211
|
-
|
|
254
|
+
console.log(chalk.gray("Auth URL:"));
|
|
255
|
+
console.log(chalk.cyan(authUrl));
|
|
212
256
|
console.log(
|
|
213
|
-
chalk.
|
|
214
|
-
"
|
|
215
|
-
)
|
|
257
|
+
chalk.gray(
|
|
258
|
+
"If the browser did not open, copy the URL above into a browser and complete the approval flow there.",
|
|
259
|
+
),
|
|
216
260
|
);
|
|
217
261
|
|
|
218
|
-
|
|
219
|
-
|
|
262
|
+
let browserOpened = false;
|
|
263
|
+
try {
|
|
264
|
+
await openFn(authUrl, { wait: false });
|
|
265
|
+
browserOpened = true;
|
|
266
|
+
console.log(
|
|
267
|
+
chalk.blue(
|
|
268
|
+
"A browser window has been opened. Approve the session there to continue.",
|
|
269
|
+
)
|
|
270
|
+
);
|
|
271
|
+
} catch (error) {
|
|
272
|
+
console.log(
|
|
273
|
+
chalk.yellow(
|
|
274
|
+
`Could not open a browser automatically: ${error.message}`,
|
|
275
|
+
),
|
|
276
|
+
);
|
|
277
|
+
console.log(
|
|
278
|
+
chalk.gray(
|
|
279
|
+
"Continue by opening the auth URL manually. The CLI will keep waiting for approval.",
|
|
280
|
+
),
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const status = await waitForCompletionFn(apiBaseUrl, authToken, expiresAt, {
|
|
285
|
+
httpClient,
|
|
286
|
+
spinnerFactory,
|
|
287
|
+
timeoutMs,
|
|
288
|
+
});
|
|
289
|
+
await verifyApiKeyFn(apiBaseUrl, status.project.apiKey, httpClient);
|
|
220
290
|
|
|
221
291
|
// Derive platformUrl from apiBaseUrl (remove /api suffix)
|
|
222
|
-
const platformUrl = apiBaseUrl.replace(/\/api\/?$/, '') || '
|
|
292
|
+
const platformUrl = apiBaseUrl.replace(/\/api\/?$/, '') || 'https://reshot.dev';
|
|
223
293
|
|
|
224
|
-
|
|
294
|
+
writeSettingsFn({
|
|
225
295
|
projectId: status.project.id,
|
|
226
296
|
projectName: status.project.name,
|
|
227
297
|
apiKey: status.project.apiKey,
|
|
@@ -249,6 +319,15 @@ async function authCommand() {
|
|
|
249
319
|
)
|
|
250
320
|
);
|
|
251
321
|
console.log(chalk.gray(`Settings saved to ${SETTINGS_PATH}`));
|
|
322
|
+
console.log(chalk.gray("Mode: cloud-connected"));
|
|
323
|
+
return {
|
|
324
|
+
mode: "cloud-connected",
|
|
325
|
+
browserOpened,
|
|
326
|
+
authUrl,
|
|
327
|
+
projectId: status.project.id,
|
|
328
|
+
projectName: status.project.name,
|
|
329
|
+
platformUrl,
|
|
330
|
+
};
|
|
252
331
|
} finally {
|
|
253
332
|
if (localServer) {
|
|
254
333
|
localServer.close();
|
|
@@ -257,3 +336,6 @@ async function authCommand() {
|
|
|
257
336
|
}
|
|
258
337
|
|
|
259
338
|
module.exports = authCommand;
|
|
339
|
+
module.exports.waitForCompletion = waitForCompletion;
|
|
340
|
+
module.exports.verifyApiKey = verifyApiKey;
|
|
341
|
+
module.exports.startLocalStatusServer = startLocalStatusServer;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const { runCertification } = require("../lib/certification");
|
|
5
|
+
|
|
6
|
+
async function certifyCommand(options = {}) {
|
|
7
|
+
const scenarioKeys = options.scenarios
|
|
8
|
+
? String(options.scenarios)
|
|
9
|
+
.split(",")
|
|
10
|
+
.map((value) => value.trim())
|
|
11
|
+
.filter(Boolean)
|
|
12
|
+
: null;
|
|
13
|
+
|
|
14
|
+
const report = await runCertification({
|
|
15
|
+
scenarioKeys,
|
|
16
|
+
tag: options.tag,
|
|
17
|
+
message: options.message,
|
|
18
|
+
skipReleaseDoctor: options.skipReleaseDoctor,
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
if (options.json) {
|
|
22
|
+
console.log(JSON.stringify(report, null, 2));
|
|
23
|
+
} else {
|
|
24
|
+
console.log(chalk.cyan("\n🏅 Certified Target Verification\n"));
|
|
25
|
+
console.log(chalk.gray(`Target: ${report.target.displayName}`));
|
|
26
|
+
console.log(
|
|
27
|
+
report.ok
|
|
28
|
+
? chalk.green(` ✔ Final status: ${report.finalStatus}`)
|
|
29
|
+
: chalk.red(` ✖ Final status: ${report.finalStatus}`),
|
|
30
|
+
);
|
|
31
|
+
console.log(
|
|
32
|
+
report.releaseDoctor?.skipped
|
|
33
|
+
? chalk.gray(" • Release doctor skipped")
|
|
34
|
+
: report.releaseDoctor?.ok
|
|
35
|
+
? chalk.green(" ✔ Release doctor passed")
|
|
36
|
+
: chalk.red(" ✖ Release doctor failed"),
|
|
37
|
+
);
|
|
38
|
+
console.log(
|
|
39
|
+
report.doctor.ok
|
|
40
|
+
? chalk.green(" ✔ Doctor passed")
|
|
41
|
+
: chalk.red(" ✖ Doctor failed"),
|
|
42
|
+
);
|
|
43
|
+
console.log(
|
|
44
|
+
report.capture.success
|
|
45
|
+
? chalk.green(" ✔ Capture passed")
|
|
46
|
+
: chalk.red(" ✖ Capture failed"),
|
|
47
|
+
);
|
|
48
|
+
console.log(
|
|
49
|
+
report.publishVerification.ok
|
|
50
|
+
? chalk.green(" ✔ Publish verification passed")
|
|
51
|
+
: chalk.red(" ✖ Publish verification failed"),
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!report.ok) {
|
|
56
|
+
process.exitCode = 1;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return report;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = certifyCommand;
|
package/src/commands/ci-run.js
CHANGED
|
@@ -5,7 +5,14 @@ const path = require("path");
|
|
|
5
5
|
const { detectCI, getCIMetadata } = require("../lib/ci-detect");
|
|
6
6
|
|
|
7
7
|
async function ciRunCommand(options = {}) {
|
|
8
|
-
const {
|
|
8
|
+
const {
|
|
9
|
+
config: configPath,
|
|
10
|
+
tag,
|
|
11
|
+
message,
|
|
12
|
+
dryRun,
|
|
13
|
+
publish: shouldPublish = true,
|
|
14
|
+
skipReleaseDoctor = false,
|
|
15
|
+
} = options;
|
|
9
16
|
|
|
10
17
|
// Disable colors in CI for cleaner logs
|
|
11
18
|
const { isCI, provider } = detectCI();
|
|
@@ -33,12 +40,47 @@ async function ciRunCommand(options = {}) {
|
|
|
33
40
|
}
|
|
34
41
|
|
|
35
42
|
const ciResult = {
|
|
43
|
+
releaseDoctor: null,
|
|
36
44
|
run: { success: false, scenariosRun: 0, scenariosFailed: 0 },
|
|
37
45
|
publish: null,
|
|
38
46
|
ci: ciMeta,
|
|
39
47
|
timestamp: new Date().toISOString(),
|
|
40
48
|
};
|
|
41
49
|
|
|
50
|
+
if (!skipReleaseDoctor) {
|
|
51
|
+
console.log(chalk.cyan("━━━ Step 0: Release Doctor ━━━\n"));
|
|
52
|
+
try {
|
|
53
|
+
const { runReleaseDoctor } = require("../lib/release-doctor");
|
|
54
|
+
const releaseDoctor = await runReleaseDoctor({});
|
|
55
|
+
ciResult.releaseDoctor = {
|
|
56
|
+
success: releaseDoctor.ok,
|
|
57
|
+
reportPath: releaseDoctor.reportPath || null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
if (!releaseDoctor.ok) {
|
|
61
|
+
console.error(chalk.red("Release doctor failed. Skipping capture and publish."));
|
|
62
|
+
const outputDir = path.join(process.cwd(), ".reshot", "output");
|
|
63
|
+
fs.ensureDirSync(outputDir);
|
|
64
|
+
const resultPath = path.join(outputDir, "ci-result.json");
|
|
65
|
+
fs.writeJsonSync(resultPath, ciResult, { spaces: 2 });
|
|
66
|
+
process.exitCode = 1;
|
|
67
|
+
return ciResult;
|
|
68
|
+
}
|
|
69
|
+
} catch (error) {
|
|
70
|
+
console.error(chalk.red(`Release doctor failed: ${error.message}`));
|
|
71
|
+
ciResult.releaseDoctor = { success: false, error: error.message };
|
|
72
|
+
const outputDir = path.join(process.cwd(), ".reshot", "output");
|
|
73
|
+
fs.ensureDirSync(outputDir);
|
|
74
|
+
const resultPath = path.join(outputDir, "ci-result.json");
|
|
75
|
+
fs.writeJsonSync(resultPath, ciResult, { spaces: 2 });
|
|
76
|
+
process.exitCode = 1;
|
|
77
|
+
return ciResult;
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
ciResult.releaseDoctor = { skipped: true, success: true };
|
|
81
|
+
console.log(chalk.gray("Skipping release doctor (--skip-release-doctor)\n"));
|
|
82
|
+
}
|
|
83
|
+
|
|
42
84
|
// Step 1: Run capture scenarios
|
|
43
85
|
console.log(chalk.cyan("━━━ Step 1: Capture ━━━\n"));
|
|
44
86
|
try {
|
|
@@ -70,6 +112,7 @@ async function ciRunCommand(options = {}) {
|
|
|
70
112
|
dryRun,
|
|
71
113
|
force: true, // Skip prompts in CI
|
|
72
114
|
outputJson: true,
|
|
115
|
+
skipReleaseDoctor: true,
|
|
73
116
|
});
|
|
74
117
|
|
|
75
118
|
ciResult.publish = {
|
|
@@ -98,6 +141,15 @@ async function ciRunCommand(options = {}) {
|
|
|
98
141
|
|
|
99
142
|
// Summary
|
|
100
143
|
console.log(chalk.cyan("\n━━━ CI Summary ━━━\n"));
|
|
144
|
+
if (ciResult.releaseDoctor) {
|
|
145
|
+
const doctorIcon = ciResult.releaseDoctor.success ? chalk.green("✔") : chalk.red("✖");
|
|
146
|
+
const doctorLabel = ciResult.releaseDoctor.skipped
|
|
147
|
+
? "skipped"
|
|
148
|
+
: ciResult.releaseDoctor.success
|
|
149
|
+
? "passed"
|
|
150
|
+
: "failed";
|
|
151
|
+
console.log(` ${doctorIcon} Release doctor: ${doctorLabel}`);
|
|
152
|
+
}
|
|
101
153
|
const runIcon = ciResult.run.success ? chalk.green("✔") : chalk.red("✖");
|
|
102
154
|
console.log(` ${runIcon} Capture: ${ciResult.run.scenariosRun} scenario(s), ${ciResult.run.scenariosFailed} failed`);
|
|
103
155
|
|
|
@@ -112,7 +164,10 @@ async function ciRunCommand(options = {}) {
|
|
|
112
164
|
console.log();
|
|
113
165
|
|
|
114
166
|
// Exit with appropriate code
|
|
115
|
-
const overallSuccess =
|
|
167
|
+
const overallSuccess =
|
|
168
|
+
(ciResult.releaseDoctor?.success ?? true) &&
|
|
169
|
+
ciResult.run.success &&
|
|
170
|
+
(!ciResult.publish || ciResult.publish.success);
|
|
116
171
|
if (!overallSuccess) {
|
|
117
172
|
process.exitCode = 1;
|
|
118
173
|
}
|
package/src/commands/ci-setup.js
CHANGED
|
@@ -29,7 +29,7 @@ jobs:
|
|
|
29
29
|
run: npm install
|
|
30
30
|
|
|
31
31
|
- name: Install Reshot CLI
|
|
32
|
-
run: npm install -g @
|
|
32
|
+
run: npm install -g @reshotdev/screenshot
|
|
33
33
|
|
|
34
34
|
- name: Install Playwright browsers
|
|
35
35
|
run: npx playwright install chromium
|
|
@@ -37,7 +37,7 @@ jobs:
|
|
|
37
37
|
- name: Install ffmpeg
|
|
38
38
|
run: sudo apt-get update && sudo apt-get install -y ffmpeg
|
|
39
39
|
|
|
40
|
-
# Generate visual assets from
|
|
40
|
+
# Generate visual assets from reshot.config.json blueprint
|
|
41
41
|
# Only runs if features.visuals is enabled for the project
|
|
42
42
|
- name: Run Reshot scenarios
|
|
43
43
|
env:
|
|
@@ -85,7 +85,7 @@ jobs:
|
|
|
85
85
|
name: Install dependencies
|
|
86
86
|
command: |
|
|
87
87
|
npm install
|
|
88
|
-
npm install -g @
|
|
88
|
+
npm install -g @reshotdev/screenshot
|
|
89
89
|
|
|
90
90
|
- run:
|
|
91
91
|
name: Install Playwright browsers
|
|
@@ -130,7 +130,7 @@ docs:
|
|
|
130
130
|
- apt-get update && apt-get install -y ffmpeg
|
|
131
131
|
- npm install
|
|
132
132
|
- npx playwright install chromium
|
|
133
|
-
- npm install -g @
|
|
133
|
+
- npm install -g @reshotdev/screenshot
|
|
134
134
|
|
|
135
135
|
script:
|
|
136
136
|
- reshot run
|
|
@@ -205,7 +205,7 @@ async function ciSetupCommand() {
|
|
|
205
205
|
|
|
206
206
|
switch (provider) {
|
|
207
207
|
case 'github':
|
|
208
|
-
workflowPath = path.join(process.cwd(), '.github', 'workflows', '
|
|
208
|
+
workflowPath = path.join(process.cwd(), '.github', 'workflows', 'reshot.yml');
|
|
209
209
|
workflowContent = GITHUB_ACTIONS_WORKFLOW(secretNames);
|
|
210
210
|
break;
|
|
211
211
|
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const chalk = require("chalk");
|
|
4
|
+
const { runReleaseDoctor } = require("../lib/release-doctor");
|
|
5
|
+
|
|
6
|
+
async function doctorReleaseCommand(options = {}) {
|
|
7
|
+
const report = await runReleaseDoctor(options);
|
|
8
|
+
|
|
9
|
+
if (options.json) {
|
|
10
|
+
console.log(JSON.stringify(report, null, 2));
|
|
11
|
+
} else {
|
|
12
|
+
console.log(chalk.cyan("\n🧪 Release Doctor\n"));
|
|
13
|
+
console.log(
|
|
14
|
+
report.ok
|
|
15
|
+
? chalk.green(" ✔ Release gate checks passed")
|
|
16
|
+
: chalk.red(" ✖ Release gate checks failed"),
|
|
17
|
+
);
|
|
18
|
+
|
|
19
|
+
console.log(
|
|
20
|
+
report.runPreflight.ok
|
|
21
|
+
? chalk.green(" ✔ Run preflight healthy")
|
|
22
|
+
: chalk.red(" ✖ Run preflight failed"),
|
|
23
|
+
);
|
|
24
|
+
|
|
25
|
+
if (report.targetDoctor.skipped) {
|
|
26
|
+
console.log(chalk.gray(" • Target doctor skipped (non-certified target)"));
|
|
27
|
+
} else {
|
|
28
|
+
console.log(
|
|
29
|
+
report.targetDoctor.ok
|
|
30
|
+
? chalk.green(" ✔ Target doctor healthy")
|
|
31
|
+
: chalk.red(" ✖ Target doctor failed"),
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (report.docsAssetMap.skipped) {
|
|
36
|
+
console.log(chalk.gray(" • Docs asset map skipped"));
|
|
37
|
+
} else {
|
|
38
|
+
console.log(
|
|
39
|
+
report.docsAssetMap.ok
|
|
40
|
+
? chalk.green(" ✔ Docs asset map healthy")
|
|
41
|
+
: chalk.red(" ✖ Docs asset map failed"),
|
|
42
|
+
);
|
|
43
|
+
if (report.docsAssetMap.path) {
|
|
44
|
+
console.log(chalk.gray(` ${report.docsAssetMap.path}`));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const blockingIssues = report.summary?.blockingIssues || [];
|
|
49
|
+
if (blockingIssues.length > 0) {
|
|
50
|
+
for (const issue of blockingIssues.slice(0, 10)) {
|
|
51
|
+
console.log(chalk.red(` ✖ ${issue.scope}: ${issue.message}`));
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (report.reportPath) {
|
|
56
|
+
console.log(chalk.gray(`\n Report: ${report.reportPath}`));
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if (!report.ok) {
|
|
61
|
+
process.exitCode = 1;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return report;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = doctorReleaseCommand;
|