snapdrift 0.8.1 → 0.9.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/CHANGELOG.md +61 -0
- package/README.md +12 -4
- package/action.yml +16 -9
- package/actions/baseline/action.yml +68 -47
- package/actions/pr-diff/action.yml +208 -107
- package/actions/resolve-baseline/action.yml +36 -50
- package/actions/scope/action.yml +24 -1
- package/bin/snapdrift.mjs +6 -3
- package/docs/contracts.md +141 -9
- package/docs/integration-guide.md +54 -13
- package/docs/local-cli.md +6 -5
- package/docs/workflow-templates/refresh-baseline-on-merge.yml +25 -13
- package/lib/cli.mjs +243 -168
- package/lib/init-from-action.mjs +24 -13
- package/lib/outage-policy.mjs +278 -0
- package/lib/provider.mjs +20 -2
- package/lib/resolve-baseline-artifact.mjs +147 -0
- package/lib/resolve-baseline-paths.mjs +40 -0
- package/lib/snap-provider.mjs +1246 -303
- package/package.json +15 -9
- package/types/visual-diff-types.d.ts +28 -0
package/actions/scope/action.yml
CHANGED
|
@@ -105,9 +105,32 @@ runs:
|
|
|
105
105
|
per_page: 100
|
|
106
106
|
});
|
|
107
107
|
|
|
108
|
+
if (!Array.isArray(files)) {
|
|
109
|
+
throw new Error('Malformed GitHub changed-file response: expected an array of file records.');
|
|
110
|
+
}
|
|
111
|
+
for (const [index, file] of files.entries()) {
|
|
112
|
+
if (!file || typeof file !== 'object' || typeof file.filename !== 'string' || file.filename.length === 0) {
|
|
113
|
+
throw new Error(`Malformed GitHub changed-file response: file record ${index} must include a non-empty string filename.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
if (files.length >= 3000) {
|
|
117
|
+
core.warning('GitHub returned the maximum 3000 changed-file records; the list may be truncated, so running all configured captures.');
|
|
118
|
+
setOutputs({
|
|
119
|
+
shouldRun: true,
|
|
120
|
+
reason: 'changed_files_truncated',
|
|
121
|
+
selectedRouteIds: allRouteIds
|
|
122
|
+
});
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const changedFiles = [...new Set(files.flatMap((file) => [
|
|
126
|
+
file.filename,
|
|
127
|
+
...(file.status === 'renamed' && typeof file.previous_filename === 'string' && file.previous_filename
|
|
128
|
+
? [file.previous_filename]
|
|
129
|
+
: [])
|
|
130
|
+
]))];
|
|
108
131
|
const scope = selectRoutesForChangedFiles(
|
|
109
132
|
config,
|
|
110
|
-
|
|
133
|
+
changedFiles
|
|
111
134
|
);
|
|
112
135
|
setOutputs(scope);
|
|
113
136
|
} catch (error) {
|
package/bin/snapdrift.mjs
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
// @ts-check
|
|
3
3
|
|
|
4
|
-
import { pathToFileURL } from 'node:url';
|
|
4
|
+
import { fileURLToPath, pathToFileURL } from 'node:url';
|
|
5
5
|
|
|
6
6
|
// Resolve cli.mjs relative to this file so the bin entry works regardless of
|
|
7
|
-
// where the consumer installs the package.
|
|
8
|
-
|
|
7
|
+
// where the consumer installs the package. Use fileURLToPath(new URL(...)) rather
|
|
8
|
+
// than URL.pathname so paths containing spaces or Windows drive letters survive
|
|
9
|
+
// the conversion (URL.pathname percent-encodes them, then pathToFileURL re-encodes
|
|
10
|
+
// the result, producing a path that does not exist).
|
|
11
|
+
const cliPath = fileURLToPath(new URL('../lib/cli.mjs', import.meta.url));
|
|
9
12
|
const { main } = await import(pathToFileURL(cliPath).href);
|
|
10
13
|
|
|
11
14
|
main(process.argv).catch((error) => {
|
package/docs/contracts.md
CHANGED
|
@@ -22,7 +22,7 @@ SnapDrift reads runtime behavior from `.github/snapdrift.json` by default.
|
|
|
22
22
|
|
|
23
23
|
| Field | Type | Description |
|
|
24
24
|
|:------|:-----|:------------|
|
|
25
|
-
| `id` | `string` | Unique route identifier across runs |
|
|
25
|
+
| `id` | `string` | Unique route identifier across runs. Its sanitized screenshot filename must also be unique across the full configured route set. |
|
|
26
26
|
| `path` | `string` | URL path appended to `baseUrl` |
|
|
27
27
|
| `viewport` | `string` or `object` | Preset name (`"desktop"`, `"mobile"`) or a custom object `{ "width": number, "height": number }` |
|
|
28
28
|
| `changePaths` | `string[]` | Optional prefixes used for changed-file scoping |
|
|
@@ -43,6 +43,24 @@ SnapDrift reads runtime behavior from `.github/snapdrift.json` by default.
|
|
|
43
43
|
|
|
44
44
|
When `provider: "snap"` is set, the `snap` block is required. Exactly one of `snap.apiKeyEnv` or `snap.apiKey` must be present. `snap.apiKey` accepts `${VAR}` interpolation (for example `"${SNAP_API_KEY}"`); the referenced environment variable must be set at runtime or the config loader throws.
|
|
45
45
|
|
|
46
|
+
Changed-file scoping treats a GitHub `renamed` record as both the current
|
|
47
|
+
`filename` and its nonempty `previous_filename`. This keeps a route selected
|
|
48
|
+
when a watched file moves out of its configured `changePaths`, and also detects
|
|
49
|
+
renames into watched or shared paths. Duplicate paths are ignored; other change
|
|
50
|
+
statuses use only their current filename. The `pr-diff` wrapper's explicit
|
|
51
|
+
`route-ids` and `force-run` inputs take precedence over this lookup; the
|
|
52
|
+
standalone `scope` action supports `force-run` and otherwise derives its
|
|
53
|
+
selection from the changed files. If GitHub returns the maximum 3,000 file
|
|
54
|
+
records, both actions run all configured routes with reason
|
|
55
|
+
`changed_files_truncated` because the list may be incomplete.
|
|
56
|
+
|
|
57
|
+
Route ids are sanitized before local captures and Snap baseline exports write
|
|
58
|
+
`screenshots/<route-id>.png`: `..` becomes `_`, path separators become `_`, and
|
|
59
|
+
control characters are removed. Distinct route ids that produce the same
|
|
60
|
+
sanitized filename are rejected before capture starts. Rename one of the route
|
|
61
|
+
ids and recapture the affected baseline; existing screenshot filenames remain
|
|
62
|
+
unchanged for noncolliding ids.
|
|
63
|
+
|
|
46
64
|
### Example (local provider)
|
|
47
65
|
|
|
48
66
|
```json
|
|
@@ -101,6 +119,27 @@ The published baseline bundle contains:
|
|
|
101
119
|
| `manifest.json` | Screenshot manifest with ids, paths, and dimensions |
|
|
102
120
|
| `screenshots/*.png` | Captured screenshot images |
|
|
103
121
|
|
|
122
|
+
Every manifest entry must use a unique staged screenshot filename (the basename
|
|
123
|
+
of `imagePath`). Duplicate paths or paths that flatten to the same filename are
|
|
124
|
+
rejected before comparison, even when the run selects only one of the affected
|
|
125
|
+
route ids, because they can point two logical screenshots at the same pixels.
|
|
126
|
+
Rename the conflicting route ids and recapture the baseline and current bundle.
|
|
127
|
+
|
|
128
|
+
### Baseline resolution
|
|
129
|
+
|
|
130
|
+
The `actions/resolve-baseline` action and the `actions/pr-diff` wrapper distinguish three lookup
|
|
131
|
+
outcomes: `found` means a successful search selected a non-expired artifact, `missing` means a
|
|
132
|
+
successful search found no usable artifact, and `error` means GitHub returned an API, network, or
|
|
133
|
+
malformed-response failure. The boolean `found` output remains available for compatibility; use
|
|
134
|
+
`resolution-status` on `resolve-baseline` or `baseline-resolution-status` on `pr-diff` when the
|
|
135
|
+
reason matters. A missing baseline may produce the intentional first-run skipped summary. A lookup
|
|
136
|
+
error fails local comparisons and local Snap fallbacks, so infrastructure failures cannot be
|
|
137
|
+
reported as a clean or intentional missing-baseline result. Healthy hosted Snap diffs may continue
|
|
138
|
+
using their hosted baseline path after a GitHub artifact lookup error.
|
|
139
|
+
The standalone `actions/resolve-baseline` action fails its step for `error`; custom workflows
|
|
140
|
+
should branch on `resolution-status` rather than treating `found: false` as proof that no baseline
|
|
141
|
+
exists.
|
|
142
|
+
|
|
104
143
|
## Drift artifact
|
|
105
144
|
|
|
106
145
|
The pull request drift bundle contains:
|
|
@@ -210,6 +249,12 @@ Additional missing-baseline fields: `baselineAvailable`, `currentResultsPath`.
|
|
|
210
249
|
| `fail-on-incomplete` | Errors, dimension shifts, or missing captures occur |
|
|
211
250
|
| `strict` | Any drift or incomplete comparison appears |
|
|
212
251
|
|
|
252
|
+
A summary with `status: "skipped"` — or any summary carrying no `diff.mode` —
|
|
253
|
+
never stops the run, whichever mode is configured. A skipped run has no diff
|
|
254
|
+
counters to enforce against, so `shouldFailDriftCheck` returns `false` for it
|
|
255
|
+
before any mode branch is reached. This holds for `actions/enforce` invoked
|
|
256
|
+
directly, not just for the guarded enforcement step inside `actions/pr-diff`.
|
|
257
|
+
|
|
213
258
|
## Viewport presets
|
|
214
259
|
|
|
215
260
|
| Preset | Width | Height | Scale factor | Mobile | Touch |
|
|
@@ -316,6 +361,16 @@ Two files are written: `.github/snapdrift.json` and `.github/MIGRATION_NOTES.md`
|
|
|
316
361
|
|
|
317
362
|
When `provider: "snap"`, every `capture` / `diff` / `publishBaseline` call goes through `SnapProvider`, which talks to Snap's hosted `/v1/visual/*` API instead of writing to the runner filesystem. The local provider keeps working exactly as before.
|
|
318
363
|
|
|
364
|
+
### Complete hosted baseline publication
|
|
365
|
+
|
|
366
|
+
A hosted baseline is an authoritative complete snapshot. For `purpose: "baseline"`, `SnapProvider.capture()` requires the selected route ids to equal the complete configured route set; `route-ids`, `--routes`, and `SNAPDRIFT_ROUTE_IDS` may not reduce it. It requires a resolved branch, 40-character commit SHA, publication workflow ref, and positive publication sequence. GitHub Actions provides these as `GITHUB_REF_NAME`, `GITHUB_SHA`, `GITHUB_WORKFLOW_REF`, and `GITHUB_RUN_NUMBER`; other CI systems may set `SNAPDRIFT_PUBLICATION_WORKFLOW_REF` and `SNAPDRIFT_PUBLICATION_SEQUENCE`. Local-provider baselines, non-publishing hosted captures, and hosted PR-diff runs retain scoped behavior.
|
|
367
|
+
|
|
368
|
+
The capture `results.json` records `configuredRouteIds`, `selectedRouteIds`, and `expectedCaptures` (route id, path, and viewport descriptor), plus one resolved `refBranch` / `refSha`, publication workflow ref, and publication sequence. The run-creation request sends the same immutable set and sequence as `capturePlan` and sends the ref as `branch` / `prHeadSha`. Before publication, SnapDrift requires the source run to belong to the configured project, settle to terminal `new`, and contain exactly one successful `new` capture with a non-empty `currentObjectKey` for every expected route/viewport identity. It keeps polling while any expected capture remains non-terminal, even when an older server reports the run itself as terminal, but fails fast after a stable incomplete capture set. Failed, missing, duplicate, extra, path-mismatched, or malformed captures abort before the baseline request. If another default-branch baseline wins first, Snap returns `409 baseline_stale_source`; rerun the current baseline job.
|
|
369
|
+
|
|
370
|
+
Hosted PR diffs use the same persisted `expectedCaptures` identities. `SnapProvider.diff()` requires `purpose: "diff"` plus a nonempty, unique expected set consistent with `selectedRouteIds`; results written by older SnapDrift versions without that set must be recaptured. It polls until the expected capture count is available, verifies the returned run id, and reconciles every returned capture by route id, path, and normalized viewport descriptor. Missing expected captures, duplicate or unexpected identities, pending or unknown statuses, run or capture errors, missing current objects, and invalid `diffPct` values produce an `incomplete` summary and are never counted as clean matches. A valid comparison requires a current object, a baseline object, and a finite numeric mismatch ratio from 0 through 1. The `diff.mode` setting then controls whether that incomplete summary fails the check.
|
|
371
|
+
|
|
372
|
+
Publication sends `publicationMode: "complete"` and the source run id both as top-level `sourceRunId` and as `manifest.sourceRunId`. The baseline id is deterministically derived from the run id, and manifest routes are sorted, so a retry sends the same identity and payload. Reusing that id with different persisted fields is rejected; rerun the baseline job to obtain a new source run. `refBranch` / `refSha` come from the persisted capture metadata rather than resolving git a second time. Snap accepts complete publication only from the project's default branch (effective default `main`).
|
|
373
|
+
|
|
319
374
|
### Local-capture hybrid
|
|
320
375
|
|
|
321
376
|
If `baseUrl` resolves to a local address (see [`isLocalBaseUrl`](#islocalbaseurl-detection) below), SnapDrift runs Playwright on the runner to render the page and uploads the resulting screenshots to Snap. This is necessary whenever your `baseUrl` is a server only the runner can reach (the common case) — Snap's render worker cannot reach a `127.0.0.1` or `localhost` server.
|
|
@@ -328,10 +383,10 @@ SnapDrift uses a small, stable subset of the Snap API:
|
|
|
328
383
|
|
|
329
384
|
| Endpoint | Used by |
|
|
330
385
|
|:---------|:--------|
|
|
331
|
-
| `POST /v1/visual/projects/:id/runs` | Create a run; client passes `baseUrl`, `trigger`, optional `baselineId` and `branch` |
|
|
386
|
+
| `POST /v1/visual/projects/:id/runs` | Create a run; client passes `baseUrl`, `trigger`, optional `baselineId` and `branch`, plus the immutable configured/selected route and expected-capture plan; baseline runs also pass the resolved commit as `prHeadSha` |
|
|
332
387
|
| `POST /v1/visual/runs/:run_id/captures` | Submit each route as a capture; client passes `routeId`, `routePath`, `viewportDescriptorJson` |
|
|
333
388
|
| `POST /v1/visual/captures/:capture_id/local-result` | Hybrid path: upload the locally rendered PNG and its dimensions |
|
|
334
|
-
| `POST /v1/visual/projects/:id/baselines` | Publish a baseline from a run's
|
|
389
|
+
| `POST /v1/visual/projects/:id/baselines` | Publish a complete default-branch baseline from a source run's validated captures |
|
|
335
390
|
| `GET /v1/visual/projects/:id/baselines/latest` | Resolve the latest accepted baseline for diff runs |
|
|
336
391
|
| `GET /v1/visual/runs/:run_id` | Poll a run until it reaches a terminal state (`pass`, `fail`, `error`, or `new`) |
|
|
337
392
|
|
|
@@ -342,9 +397,21 @@ SnapDrift uses a small, stable subset of the Snap API:
|
|
|
342
397
|
The Snap HTTP client classifies responses and applies the following rules:
|
|
343
398
|
|
|
344
399
|
- **2xx** — success, return the parsed body.
|
|
345
|
-
- **4xx** — non-retryable. The client throws `SnapApiError(status, message, path)` immediately. `onUnavailable` is **not** consulted for 4xx — a 404 from `/baselines/latest` is a "no baseline yet" signal, but a 4xx from `/runs` is a configuration error that retrying won't fix.
|
|
346
|
-
- **5xx** — retryable up to 3 attempts with exponential backoff (`1 s` → `2 s` → `4 s`, capped at `30 s`
|
|
347
|
-
- **Network errors** — same retry/backoff behavior as 5xx. After exhaustion, falls through to the `onUnavailable` handler.
|
|
400
|
+
- **4xx** — non-retryable. The client throws `SnapApiError(status, message, path)` immediately. `onUnavailable` is **not** consulted for 4xx — a 404 from `/baselines/latest` is a "no baseline yet" signal, but a 4xx from `/runs` is a configuration error that retrying won't fix. If the diagnostic body itself stalls or is unavailable, the same `SnapApiError` retains the HTTP status and includes that diagnostic failure.
|
|
401
|
+
- **5xx** — retryable up to 3 attempts with exponential backoff (`1 s` → `2 s` → `4 s`, with each delay capped at `30 s`). If the final attempt still returns 5xx, the client falls through to the `onUnavailable` handler.
|
|
402
|
+
- **Network errors and transport timeouts** — same retry/backoff behavior as 5xx. After exhaustion, falls through to the `onUnavailable` handler.
|
|
403
|
+
|
|
404
|
+
Every JSON request attempt has a 30-second limit; binary export attempts have a
|
|
405
|
+
120-second limit. Headers and response bodies share the applicable attempt
|
|
406
|
+
limit, and each public Snap operation has one 10-minute deadline covering all
|
|
407
|
+
of its requests, retries, backoff, and polling. The client passes an
|
|
408
|
+
`AbortSignal` to fetch and aborts/cancels stalled bodies; injected transports
|
|
409
|
+
that ignore cancellation are still released by the client-side deadline.
|
|
410
|
+
Retry and poll waits are clipped to the remaining operation time, so no new
|
|
411
|
+
request starts after the operation deadline. Deadline exhaustion is treated as
|
|
412
|
+
Snap unavailability and follows `onUnavailable`; received 4xx responses keep
|
|
413
|
+
their immediate non-retryable behavior, and a stalled 4xx diagnostic body
|
|
414
|
+
retains its HTTP status with a timeout diagnostic.
|
|
348
415
|
|
|
349
416
|
`onUnavailable` is consulted once retries are exhausted:
|
|
350
417
|
|
|
@@ -352,6 +419,25 @@ The Snap HTTP client classifies responses and applies the following rules:
|
|
|
352
419
|
- `"warn-and-skip"` — log a warning, throw `SnapSkipError`. The wrapper actions catch this and write a skipped summary, exiting 0.
|
|
353
420
|
- `"fallback-local"` — log a warning, throw `SnapFallbackError`. The wrapper actions catch this and continue the pipeline with `LocalProvider`.
|
|
354
421
|
|
|
422
|
+
### Outage policy
|
|
423
|
+
|
|
424
|
+
Snap can go down at any phase of a run, and the configured `onUnavailable`
|
|
425
|
+
behavior has to hold for all of them. `lib/outage-policy.mjs` is the single
|
|
426
|
+
implementation, used by `lib/cli.mjs`, `actions/baseline` and `actions/pr-diff`
|
|
427
|
+
alike:
|
|
428
|
+
|
|
429
|
+
| Phase | `warn-and-skip` | `fallback-local` |
|
|
430
|
+
|:------|:----------------|:-----------------|
|
|
431
|
+
| Capture | Write a skipped `summary.json`/`summary.md` with reason `snap_unavailable`, expose the summary outputs, stage the report, exit 0. | Capture with `LocalProvider` and report the **effective** provider (`local`) so the rest of the pipeline uses local artifacts and the local pixel engine. |
|
|
432
|
+
| Diff | Same skipped summary, exit 0. | Diff with `LocalProvider`. A capture that Snap rendered server-side has no PNGs on the runner, so the routes are **recaptured locally first**; the recaptured paths replace the Snap ones in the staged bundle. If baseline resolution succeeded but no artifact was found, the run reports `missing_main_baseline_artifact`; a lookup error fails instead of entering the missing-baseline path. |
|
|
433
|
+
| Baseline publish | Skip the publish and exit 0 without an artifact. | Capture locally and stage/upload that bundle, so the run still leaves a usable baseline. |
|
|
434
|
+
|
|
435
|
+
Enforcement of `diff.mode` never runs against a skipped summary — a skipped run
|
|
436
|
+
carries no diff counters, and `warn-and-skip` means "do not fail my build".
|
|
437
|
+
|
|
438
|
+
A `fallback-local` capture leaves nothing on Snap: no run is published and no
|
|
439
|
+
baseline is created. The fallback is a local run, not a deferred hosted one.
|
|
440
|
+
|
|
355
441
|
### `isLocalBaseUrl` detection
|
|
356
442
|
|
|
357
443
|
`isLocalBaseUrl(baseUrl)` returns `true` when the URL hostname is:
|
|
@@ -369,12 +455,16 @@ All four error classes are exported from `lib/provider.mjs` and `lib/snap-provid
|
|
|
369
455
|
|
|
370
456
|
| Class | Thrown when | Typical handler |
|
|
371
457
|
|:------|:------------|:----------------|
|
|
372
|
-
| `SnapApiError` | A 4xx response was received,
|
|
373
|
-
| `SnapUnavailableError` | A network error or
|
|
458
|
+
| `SnapApiError` | A 4xx response was received, including a response whose diagnostic body stalled. Carries `status` and `path` properties. A final 5xx response also retains its status when it reaches the outage handler. | Surface the message; do not retry. |
|
|
459
|
+
| `SnapUnavailableError` | A retryable network error, transport timeout, or operation deadline was exhausted. In fail mode this may be surfaced directly; skip and fallback modes wrap it in their policy error. | Treat as a temporary outage. |
|
|
374
460
|
| `SnapFallbackError` | `onUnavailable: "fallback-local"` is set and Snap could not be reached. | Catch and switch to `LocalProvider` for the rest of the pipeline. |
|
|
375
461
|
| `SnapSkipError` | `onUnavailable: "warn-and-skip"` is set and Snap could not be reached. | Catch and exit cleanly with a skipped summary. |
|
|
376
462
|
|
|
377
|
-
The wrapper actions (`actions/baseline`, `actions/pr-diff`)
|
|
463
|
+
The wrapper actions (`actions/baseline`, `actions/pr-diff`) and the CLI both handle `SnapSkipError` and `SnapFallbackError` through `lib/outage-policy.mjs` (`captureWithPolicy`, `diffWithPolicy`, `publishBaselineWithPolicy`). Custom orchestrations that call `provider.capture()`, `provider.diff()` or `provider.publishBaseline()` directly should use those helpers rather than re-implementing the matrix.
|
|
464
|
+
|
|
465
|
+
`SnapTransportTimeoutError` is an internal diagnostic name used while a
|
|
466
|
+
request or response body is being bounded. It is not an additional public
|
|
467
|
+
error class; callers should handle the exported error classes above.
|
|
378
468
|
|
|
379
469
|
## Primary entrypoints
|
|
380
470
|
|
|
@@ -472,3 +562,45 @@ The `VisualProvider` interface requires `buildCommentBody(summary, meta?)`:
|
|
|
472
562
|
|
|
473
563
|
- **`LocalProvider`** — delegates to `buildReportCommentBody` without a `dashboardUrl`.
|
|
474
564
|
- **`SnapProvider`** — constructs `dashboardUrl` from `{apiUrl}/projects/{projectId}/runs/{lastRunId}` and passes it through.
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
## Workspace TypeScript contracts
|
|
568
|
+
|
|
569
|
+
The four `@snapdrift/*` workspace packages expose their declarations through a
|
|
570
|
+
`types` export condition before the existing JavaScript entrypoint. ESM consumers
|
|
571
|
+
can use Bundler, Node16, or NodeNext module resolution with `strict: true` and
|
|
572
|
+
`skipLibCheck: false`; Node16/NodeNext consumers should use an ESM package or an
|
|
573
|
+
`.mts` entrypoint. The top-level `types` field remains for older tooling.
|
|
574
|
+
|
|
575
|
+
Install `typescript` and `@types/node` in the consuming development environment
|
|
576
|
+
for APIs that use Node buffers. Cross-package declaration dependencies are
|
|
577
|
+
declared by the package that needs them. `npm run check:package-types` packs the
|
|
578
|
+
workspace packages and checks isolated consumers in all three resolution modes,
|
|
579
|
+
including rejected invalid calls. `npm run typecheck` also checks declaration
|
|
580
|
+
bodies without skipping library checks.
|
|
581
|
+
|
|
582
|
+
These guarantees cover the workspace package entrypoints. They do not add type
|
|
583
|
+
entrypoints for the root `snapdrift` package's JavaScript subpaths. Package release
|
|
584
|
+
publication is required before existing registry consumers receive these fixes.
|
|
585
|
+
|
|
586
|
+
Comment renderers accept `VisualReportSummary`: a complete comparison summary or
|
|
587
|
+
a `VisualDriftStatusSummary` with a required status and reason. `buildDriftSummary`
|
|
588
|
+
and `writeDriftSummary` return the latter, preserving their supported non-skipped
|
|
589
|
+
statuses as well as intentional skips. Unrelated object literals are rejected.
|
|
590
|
+
Known viewport presets have complete descriptors; arbitrary string lookups may
|
|
591
|
+
return `undefined` and must be checked by consumers.
|
|
592
|
+
|
|
593
|
+
The packed type gate runs in PR CI and before release publication. It validates
|
|
594
|
+
the candidate workspace tarballs together, not already-published registry
|
|
595
|
+
versions. Release preparation must bump every changed published package and
|
|
596
|
+
raise sibling dependency floors to the versions that contain these type fixes
|
|
597
|
+
before publication; the gate alone does not validate that release-version step.
|
|
598
|
+
|
|
599
|
+
Each workspace package also declares every runtime package it imports. In
|
|
600
|
+
particular, `@snapdrift/adapter-fs` declares `@snapdrift/adapter-report-md`, so
|
|
601
|
+
it can be installed and imported without the SnapDrift repository or a sibling
|
|
602
|
+
workspace masking a missing dependency. `npm run check:package-runtime` packs
|
|
603
|
+
each workspace package, installs it in an isolated temporary consumer together
|
|
604
|
+
with only its declared local workspace dependency closure, and imports its
|
|
605
|
+
public entrypoint. The gate runs in pull-request and release workflows; publish
|
|
606
|
+
the updated package before registry consumers can receive a dependency fix.
|
|
@@ -46,14 +46,14 @@ SnapDrift owns route selection, capture, comparison, skipped-report generation,
|
|
|
46
46
|
|
|
47
47
|
```yaml
|
|
48
48
|
- name: SnapDrift Baseline
|
|
49
|
-
uses: ranacseruet/snapdrift@v0.
|
|
49
|
+
uses: ranacseruet/snapdrift@v0.9.0
|
|
50
50
|
with:
|
|
51
51
|
mode: baseline
|
|
52
52
|
repo-config-path: .github/snapdrift.json
|
|
53
53
|
artifact-retention-days: '30'
|
|
54
54
|
```
|
|
55
55
|
|
|
56
|
-
|
|
56
|
+
For a local-provider baseline, `route-ids` may select part of the route set. A hosted Snap baseline is authoritative and must run in GitHub Actions on the project default branch with every configured route, so a partial `route-ids` value or unresolved CI commit fails before a run is created. Route scoping remains supported in `pr-diff` mode. If you don't want to upload the artifact (for example when using the Snap provider, which uploads via its own API), set `upload-artifact: 'false'`.
|
|
57
57
|
|
|
58
58
|
## Step 3: Run the pull request report
|
|
59
59
|
|
|
@@ -71,7 +71,7 @@ Then add SnapDrift after the app is running:
|
|
|
71
71
|
|
|
72
72
|
```yaml
|
|
73
73
|
- name: SnapDrift Report
|
|
74
|
-
uses: ranacseruet/snapdrift@v0.
|
|
74
|
+
uses: ranacseruet/snapdrift@v0.9.0
|
|
75
75
|
with:
|
|
76
76
|
mode: pr-diff
|
|
77
77
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -86,8 +86,8 @@ The wrappers remain published in their own right, and are equivalent to the disp
|
|
|
86
86
|
|
|
87
87
|
| Entry point | Equivalent to |
|
|
88
88
|
|-------------|---------------|
|
|
89
|
-
| `ranacseruet/snapdrift@v0.
|
|
90
|
-
| `ranacseruet/snapdrift@v0.
|
|
89
|
+
| `ranacseruet/snapdrift@v0.9.0` with `mode: baseline` | `ranacseruet/snapdrift/actions/baseline@v0.9.0` |
|
|
90
|
+
| `ranacseruet/snapdrift@v0.9.0` with `mode: pr-diff` | `ranacseruet/snapdrift/actions/pr-diff@v0.9.0` |
|
|
91
91
|
|
|
92
92
|
Use the wrappers directly when you want to skip the dispatcher's input indirection, and the lower-level `capture`, `compare`, `scope`, `resolve-baseline`, `stage`, `comment`, and `enforce` actions when you need to orchestrate the stages yourself.
|
|
93
93
|
|
|
@@ -130,7 +130,7 @@ jobs:
|
|
|
130
130
|
done
|
|
131
131
|
|
|
132
132
|
- name: SnapDrift Report
|
|
133
|
-
uses: ranacseruet/snapdrift@v0.
|
|
133
|
+
uses: ranacseruet/snapdrift@v0.9.0
|
|
134
134
|
with:
|
|
135
135
|
mode: pr-diff
|
|
136
136
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -170,7 +170,7 @@ jobs:
|
|
|
170
170
|
done
|
|
171
171
|
|
|
172
172
|
- name: SnapDrift Report
|
|
173
|
-
uses: ranacseruet/snapdrift@v0.
|
|
173
|
+
uses: ranacseruet/snapdrift@v0.9.0
|
|
174
174
|
with:
|
|
175
175
|
mode: pr-diff
|
|
176
176
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -188,6 +188,15 @@ jobs:
|
|
|
188
188
|
- Upserts the PR report
|
|
189
189
|
- Enforces `diff.mode` after publication completes
|
|
190
190
|
|
|
191
|
+
When the wrapper scopes a pull request from GitHub's changed-file list, a
|
|
192
|
+
renamed file contributes both its new and previous paths. A move out of a
|
|
193
|
+
watched directory therefore still selects the route that owned the old path,
|
|
194
|
+
while a move into a watched or shared path selects the new scope as well.
|
|
195
|
+
Repeated paths are deduplicated, and `route-ids` or `force-run` keep their
|
|
196
|
+
existing precedence. GitHub caps this file listing at 3,000 records; if that
|
|
197
|
+
limit is reached, the actions run all configured routes with scope reason
|
|
198
|
+
`changed_files_truncated` instead of trusting a potentially incomplete list.
|
|
199
|
+
|
|
191
200
|
## Useful overrides
|
|
192
201
|
|
|
193
202
|
| Input | Default | Purpose |
|
|
@@ -205,7 +214,7 @@ jobs:
|
|
|
205
214
|
| `max-changed-rows` | `20` | Max changed-route rows shown in the PR comment before truncation |
|
|
206
215
|
| `max-error-rows` | `10` | Max error rows shown in the PR comment before truncation |
|
|
207
216
|
|
|
208
|
-
The `pr-diff` action also exposes outputs you can use in subsequent steps: `should-run`, `scope-reason`, `selected-route-ids`, `baseline-found`, `status`, `summary-path`, `markdown-path`, `artifact-name`, `bundle-dir`. See the wrapper action's `outputs:` block for the canonical list.
|
|
217
|
+
The `pr-diff` action also exposes outputs you can use in subsequent steps: `should-run`, `scope-reason`, `selected-route-ids`, `baseline-found`, `baseline-resolution-status`, `status`, `summary-path`, `markdown-path`, `artifact-name`, `bundle-dir`. `baseline-resolution-status` is `found`, `missing`, or `error`; `missing` is the intentional first-baseline case, while `error` means the GitHub lookup failed and causes local comparisons or local Snap fallbacks to fail. Healthy hosted Snap diffs can continue without the downloaded GitHub artifact. See the wrapper action's `outputs:` block for the canonical list.
|
|
209
218
|
|
|
210
219
|
## Hosted Snap provider
|
|
211
220
|
|
|
@@ -230,7 +239,11 @@ By default SnapDrift writes baselines and reports to the runner filesystem (`pro
|
|
|
230
239
|
- `"warn-and-skip"` — log a warning, write a skipped summary, and exit 0.
|
|
231
240
|
- `"fallback-local"` — log a warning and run the rest of the pipeline with `LocalProvider`.
|
|
232
241
|
|
|
233
|
-
|
|
242
|
+
Both behaviors apply at every phase of a run — capture, diff, and baseline publish. `fallback-local` switches the whole remainder of the pipeline to local artifacts and the local pixel engine; if Snap had already rendered the current capture server-side, the routes are recaptured on the runner first, so `actions/pr-diff` needs Playwright available (the wrapper installs it whenever `onUnavailable` is `fallback-local`). Nothing is published to Snap on a fallback run. See [Outage policy](contracts.md#outage-policy) for the full matrix.
|
|
243
|
+
|
|
244
|
+
Hosted baseline publication is complete and default-branch-only. SnapDrift rejects scoped baseline capture before creating the run, records the configured/selected route ids and exact route/viewport identities, and publishes only when every expected capture finishes successfully with a stored image. The run records one resolved branch and commit (`branch` / `prHeadSha`) plus a publication workflow ref and monotonic sequence. GitHub Actions provides these automatically; other CI systems may set `SNAPDRIFT_PUBLICATION_WORKFLOW_REF` and `SNAPDRIFT_PUBLICATION_SEQUENCE`. Snap uses the sequence for same-workflow ordering and source-run timestamps for cross-workflow freshness. Missing, failed, duplicate, or unexpected captures abort publication without replacing the accepted baseline. If a source run loses the race to a newer candidate, the API returns `409 baseline_stale_source`; an inconsistent manifest/source workflow identity returns `409 baseline_source_run_mismatch` with `details.reason = "workflow_mismatch"`. Workflow-ref rotation itself is supported. This restriction does not apply to non-publishing hosted captures, hosted PR-diff runs, or local-provider baselines.
|
|
245
|
+
|
|
246
|
+
The Snap API client retries 5xx, network errors, and transport timeouts with exponential backoff (3 attempts, 1 s → 2 s → 4 s, with each delay capped at 30 s). JSON request attempts are limited to 30 seconds and binary baseline exports to 120 seconds. Each Snap operation has a 10-minute deadline covering its requests, retries, backoff, and polling; stalled headers and response bodies are aborted and cannot keep the workflow waiting indefinitely. Retry and poll waits are clipped to the remaining operation time. Received 4xx errors never retry or fall back; if a 4xx diagnostic body stalls, the response remains a non-retryable error with its HTTP status and timeout diagnostic. When a deadline or other retryable transport failure is exhausted, `onUnavailable` controls fail, warn-and-skip, or fallback-local behavior.
|
|
234
247
|
|
|
235
248
|
When `provider: "snap"` and `baseUrl` points to a local address (localhost, `127.0.0.0/8`, `::1`, or `0.0.0.0`), SnapDrift uses a **local-capture hybrid**: Playwright runs on the runner to render the page, then SnapDrift uploads the resulting screenshots to Snap. This makes it possible to point SnapDrift at a server that only the runner can reach (a typical case) without exposing the server to Snap's render worker.
|
|
236
249
|
|
|
@@ -263,7 +276,9 @@ The `pr-diff` wrapper composes the following low-level steps. They're still avai
|
|
|
263
276
|
- `actions/capture` — capture routes and emit `results.json` + `manifest.json`
|
|
264
277
|
- `actions/compare` — diff current capture against a baseline
|
|
265
278
|
- `actions/scope` — decide whether to run and which routes to select from changed files
|
|
266
|
-
- `actions/resolve-baseline` — find and download the latest successful baseline artifact
|
|
279
|
+
- `actions/resolve-baseline` — find and download the latest successful baseline artifact; its
|
|
280
|
+
`resolution-status` output distinguishes `found`, `missing`, and `error`; the action fails for
|
|
281
|
+
`error`, while `missing` remains the intentional first-baseline result
|
|
267
282
|
- `actions/stage` — assemble the baseline or diff bundle for upload
|
|
268
283
|
- `actions/enforce` — evaluate the summary against `diff.mode` and fail when required
|
|
269
284
|
- `actions/comment` — upsert a PR comment from a summary (provider-aware)
|
|
@@ -302,15 +317,19 @@ Use the provided workflow template to refresh the baseline automatically on ever
|
|
|
302
317
|
|
|
303
318
|
Drop a copy into your repo at `.github/workflows/snapdrift-refresh-baseline.yml`, then fill in the `TODO` blocks with your app's build and start steps — the same steps you use in your PR workflow. The template uses a `push` trigger on the default branch so published artifacts are discoverable by the baseline resolver in `actions/pr-diff`.
|
|
304
319
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
If you don't want to republish the baseline on every push, the template includes a commented `if` condition that gates on a label (e.g. `snapdrift:refresh-baseline`). A preceding job can check the most recent merged PR for the label and set an output to control whether the baseline refresh runs.
|
|
320
|
+
Run this job after every successful default-branch build. Hosted publication is the only operation that may intentionally remove a configured route from the canonical baseline, so skipping merges can leave later PRs comparing against stale route configuration.
|
|
308
321
|
|
|
309
322
|
## Troubleshooting
|
|
310
323
|
|
|
311
324
|
**"No non-expired SnapDrift baseline artifact was found"**
|
|
312
325
|
The baseline workflow has not completed successfully on `main`, or the artifact expired. With the Snap provider, the equivalent situation is a 404 from `/v1/visual/projects/:id/baselines/latest`; `SnapProvider` swallows that 404 and proceeds without a baseline — `onUnavailable` is **not** consulted for this case (a 404 is the legitimate "no baseline yet" signal, not a Snap outage). If you want the PR pipeline to tolerate the first-run case, set `diff.mode: "report-only"`; `onUnavailable: "warn-and-skip"` will not help here.
|
|
313
326
|
|
|
327
|
+
**"Unable to resolve the SnapDrift baseline artifact"**
|
|
328
|
+
GitHub failed while listing workflow runs or artifacts. Check the job's `actions: read` permission,
|
|
329
|
+
repository/workflow/branch inputs, and GitHub API availability. Snap hosted diffs can still use the
|
|
330
|
+
hosted comparison path, but local comparisons and `fallback-local` require a successful GitHub
|
|
331
|
+
artifact lookup.
|
|
332
|
+
|
|
314
333
|
**403 when posting the PR report**
|
|
315
334
|
Grant `issues: write` and `pull-requests: write` to the job.
|
|
316
335
|
|
|
@@ -325,3 +344,25 @@ Capture failed before comparison. Confirm the app is fully ready and reachable b
|
|
|
325
344
|
|
|
326
345
|
**Playwright install runs even though `provider: "snap"`**
|
|
327
346
|
That's expected for the Snap local-capture hybrid. Playwright runs locally to render the page, then the resulting screenshot is uploaded to Snap. The hybrid kicks in only when `baseUrl` points to a local address; for remote `baseUrl` Snap's render worker captures directly.
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
## TypeScript package consumers
|
|
350
|
+
|
|
351
|
+
Import APIs and types from the public `@snapdrift/*` package names. Their export
|
|
352
|
+
maps support Bundler, Node16, and NodeNext resolution; enable `strict: true` and
|
|
353
|
+
`skipLibCheck: false` to validate the contracts in your project. For Node16 or
|
|
354
|
+
NodeNext, use `"type": "module"` or an `.mts` entrypoint. Install `typescript` and
|
|
355
|
+
`@types/node` as development dependencies when using the Node buffer APIs.
|
|
356
|
+
|
|
357
|
+
No TypeScript path aliases or imports into workspace source directories are
|
|
358
|
+
needed. Root `snapdrift/lib/*` subpath typing is separate from these workspace
|
|
359
|
+
contracts. The fixes become available to registry consumers after the packages
|
|
360
|
+
are released and their installed versions are updated.
|
|
361
|
+
|
|
362
|
+
The workspace packages are independently installable runtime packages. Install
|
|
363
|
+
the package you use from the registry and let npm resolve its declared runtime
|
|
364
|
+
dependencies; do not rely on a repository checkout or workspace links. The
|
|
365
|
+
filesystem adapter includes its reporting dependency, so `@snapdrift/adapter-fs`
|
|
366
|
+
can load config, capture, comparison, and report entrypoints from an isolated
|
|
367
|
+
consumer. Release a new adapter version before expecting an existing registry
|
|
368
|
+
installation to include a dependency correction.
|
package/docs/local-cli.md
CHANGED
|
@@ -31,11 +31,11 @@ snapdrift baseline [options]
|
|
|
31
31
|
```
|
|
32
32
|
|
|
33
33
|
- **`provider: "local"`** — identical to `snapdrift capture`: the screenshots written to the baseline directory *are* the baseline.
|
|
34
|
-
- **`provider: "snap"`** —
|
|
34
|
+
- **`provider: "snap"`** — canonical baseline publication is CI-only. Run it on the Snap project's default branch; GitHub Actions supplies the publication metadata automatically, while another CI system may set `SNAPDRIFT_PUBLICATION_WORKFLOW_REF` and `SNAPDRIFT_PUBLICATION_SEQUENCE`. It captures every configured route, waits for the complete hosted run, then publishes a baseline referencing the stored objects. A local checkout without those metadata values fails before creating a hosted run, and partial `--routes` selection is rejected. `snapdrift capture` may still be used locally for non-publishing diagnostics.
|
|
35
35
|
|
|
36
36
|
`snap.onUnavailable` is honoured across both phases — the capture *and* the publish that follows it. `warn-and-skip` exits 0 without a baseline; `fallback-local` captures locally so you still end up with one.
|
|
37
37
|
|
|
38
|
-
**Baseline attribution.**
|
|
38
|
+
**Baseline attribution.** A hosted publication records a 40-character commit and branch from GitHub Actions or the current git checkout, plus a publication workflow identity and sequence. Outside GitHub Actions, set `SNAPDRIFT_PUBLICATION_WORKFLOW_REF` and `SNAPDRIFT_PUBLICATION_SEQUENCE`; without them SnapDrift refuses to create a canonical hosted baseline.
|
|
39
39
|
|
|
40
40
|
**`snap.projectId` must be an explicit `prj_...` id** for local runs. `"auto"` derives the id from `GITHUB_REPOSITORY`, which is not set outside Actions — the command fails early telling you to set an explicit id.
|
|
41
41
|
|
|
@@ -44,13 +44,14 @@ snapdrift baseline [options]
|
|
|
44
44
|
| Flag | Default | Description |
|
|
45
45
|
|:-----|:--------|:------------|
|
|
46
46
|
| `--config <path>` | `.github/snapdrift.json` | Path to the config file |
|
|
47
|
-
| `--routes <ids>` | all routes | Comma-separated route IDs
|
|
47
|
+
| `--routes <ids>` | all routes | Comma-separated route IDs for a local-provider baseline; hosted baselines require all configured routes |
|
|
48
48
|
| `--baseline-dir <path>` | `.snapdrift/baseline` | Directory to write baseline screenshots and metadata (local provider) |
|
|
49
49
|
|
|
50
50
|
**Example**
|
|
51
51
|
|
|
52
52
|
```bash
|
|
53
53
|
snapdrift baseline
|
|
54
|
+
# Scoped baseline capture remains available with provider: "local"
|
|
54
55
|
snapdrift baseline --routes home-desktop,home-mobile
|
|
55
56
|
```
|
|
56
57
|
|
|
@@ -296,7 +297,7 @@ The `report.html` path is printed whenever the status is anything other than `cl
|
|
|
296
297
|
|:---------|:-----------|:------------|
|
|
297
298
|
| `SNAPDRIFT_CAPTURE_CONCURRENCY` | `capture`, `diff` | Max concurrent route captures per viewport context (positive integer, default `5`). Set to `1` to restore serial behavior for apps with shared session or auth state. |
|
|
298
299
|
| `SNAPDRIFT_CONFIG_PATH` | `capture`, `diff`, `migrate-baselines` | Override the config file path. Equivalent to `--config`. |
|
|
299
|
-
| `SNAPDRIFT_ROUTE_IDS` | `capture`, `diff` | Comma-separated route ids to scope to. Equivalent to `--routes
|
|
300
|
+
| `SNAPDRIFT_ROUTE_IDS` | `capture`, `baseline`, `diff` | Comma-separated route ids to scope to. Equivalent to `--routes`; it may not reduce a hosted baseline's complete route set. |
|
|
300
301
|
|
|
301
302
|
`SNAPDRIFT_CAPTURE_CONCURRENCY` is the same env var consumed by the GitHub Actions wrapper; tweak it the same way for both environments.
|
|
302
303
|
|
|
@@ -304,7 +305,7 @@ The `report.html` path is printed whenever the status is anything other than `cl
|
|
|
304
305
|
|
|
305
306
|
## Tips
|
|
306
307
|
|
|
307
|
-
- **Partial runs**: use `--routes` to capture or compare only the routes you are actively changing.
|
|
308
|
+
- **Partial runs**: use `--routes` to capture or compare only the routes you are actively changing. A hosted baseline is the exception: its GitHub Actions run always requires the complete configured route set.
|
|
308
309
|
- **Multiple baselines**: use `--baseline-dir` to maintain separate baselines per branch or feature.
|
|
309
310
|
- **CI parity**: the CLI uses the same capture and comparison engine as the GitHub Actions workflow, so results are comparable.
|
|
310
311
|
- **Self-contained HTML report**: `report.html` embeds baseline, current, and diff images as base64. Open it from any machine — no server required, and no relative-path resolution surprises. (The HTML report is a local-CLI feature; the GitHub Actions wrapper ships only `summary.json` + `summary.md` in its artifact bundle.)
|
|
@@ -17,12 +17,6 @@
|
|
|
17
17
|
# resolver in actions/pr-diff (which filters on event: 'push'). A merge into
|
|
18
18
|
# the default branch produces a push event, so this fires exactly when needed.
|
|
19
19
|
#
|
|
20
|
-
# Optional — refresh only on a specific label:
|
|
21
|
-
# If you don't want to refresh on every push, you can gate on a label applied
|
|
22
|
-
# before merging. Use the pull_request: labeled event in a separate workflow to
|
|
23
|
-
# set an output, or simply restrict this workflow to specific paths.
|
|
24
|
-
# See the commented block below for a label-based gate using a preceding job.
|
|
25
|
-
|
|
26
20
|
name: SnapDrift Refresh Baseline
|
|
27
21
|
|
|
28
22
|
on:
|
|
@@ -35,12 +29,12 @@ permissions:
|
|
|
35
29
|
|
|
36
30
|
jobs:
|
|
37
31
|
refresh-baseline:
|
|
38
|
-
#
|
|
39
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
32
|
+
# Preserve the in-flight publisher and queue the next merge behind it. This
|
|
33
|
+
# keeps older SnapDrift clients safe during a rolling release; SnapDrift's
|
|
34
|
+
# workflow-scoped sequence is an additional stale-source guard.
|
|
35
|
+
concurrency:
|
|
36
|
+
group: snapdrift-baseline-${{ github.repository }}-${{ github.ref }}
|
|
37
|
+
cancel-in-progress: false
|
|
44
38
|
runs-on: ubuntu-latest
|
|
45
39
|
|
|
46
40
|
steps:
|
|
@@ -69,8 +63,26 @@ jobs:
|
|
|
69
63
|
# done
|
|
70
64
|
# ────────────────────────────────────────────────────────────────────────
|
|
71
65
|
|
|
66
|
+
- name: Verify current default-branch candidate
|
|
67
|
+
id: baseline_candidate
|
|
68
|
+
shell: bash
|
|
69
|
+
run: |
|
|
70
|
+
set -euo pipefail
|
|
71
|
+
latest_sha="$(git ls-remote origin "refs/heads/${GITHUB_REF_NAME}" | cut -f1)"
|
|
72
|
+
if [ -z "$latest_sha" ]; then
|
|
73
|
+
echo "::error::Could not resolve the current default-branch commit."
|
|
74
|
+
exit 1
|
|
75
|
+
fi
|
|
76
|
+
if [ "$GITHUB_SHA" = "$latest_sha" ]; then
|
|
77
|
+
echo "current=true" >> "$GITHUB_OUTPUT"
|
|
78
|
+
else
|
|
79
|
+
echo "current=false" >> "$GITHUB_OUTPUT"
|
|
80
|
+
echo "Skipping stale baseline candidate $GITHUB_SHA; current branch head is $latest_sha."
|
|
81
|
+
fi
|
|
82
|
+
|
|
72
83
|
- name: Publish SnapDrift baseline
|
|
73
|
-
|
|
84
|
+
if: steps.baseline_candidate.outputs.current == 'true'
|
|
85
|
+
uses: ranacseruet/snapdrift@v0.9.0
|
|
74
86
|
with:
|
|
75
87
|
mode: baseline
|
|
76
88
|
repo-config-path: .github/snapdrift.json
|