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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,66 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.9.0 - 2026-09-09
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **Workspace packages now declare their runtime dependency boundaries** (#148).
|
|
8
|
+
`@snapdrift/adapter-fs` declares `@snapdrift/adapter-report-md`, and an isolated
|
|
9
|
+
packed-consumer gate imports every workspace package with only its declared
|
|
10
|
+
local dependency closure. Repository workspaces can no longer mask a missing
|
|
11
|
+
runtime dependency.
|
|
12
|
+
|
|
13
|
+
- **Published workspace TypeScript contracts resolve under modern module resolution** (#135, #129). All four `@snapdrift/*` packages expose declarations through their export maps, include signatures for every public runtime export, and correct stale declaration references. Strict declaration checks and isolated packed-package consumer fixtures now run in CI.
|
|
14
|
+
|
|
15
|
+
- **Snap transport now has enforceable request and operation deadlines** (#150). JSON requests
|
|
16
|
+
settle stalled headers or bodies within 30 seconds, binary exports within 120 seconds, and each
|
|
17
|
+
Snap operation shares a 10-minute budget across requests, retries, backoff, and polling. Aborted
|
|
18
|
+
or otherwise exhausted transport operations now reach the configured `onUnavailable` policy;
|
|
19
|
+
received 4xx responses remain immediate and non-retryable, while stalled 4xx diagnostics retain
|
|
20
|
+
their HTTP status with a timeout diagnostic.
|
|
21
|
+
- **Renamed files now retain both paths during route scoping** (#149). The
|
|
22
|
+
`scope` and `pr-diff` actions include a nonempty `previous_filename` for
|
|
23
|
+
GitHub rename records, deduplicate the combined paths, and preserve the
|
|
24
|
+
existing force-run and lookup-fallback behavior (with explicit route ids
|
|
25
|
+
still taking precedence in `pr-diff`). If GitHub returns its 3,000-record
|
|
26
|
+
cap, both actions run all configured routes with reason
|
|
27
|
+
`changed_files_truncated` rather than trust an incomplete list. A file moved
|
|
28
|
+
out of a watched directory can no longer silently skip its route.
|
|
29
|
+
- **Sanitized route-id collisions are rejected before writing screenshots** (#126). Local capture now fails before browser work, while Snap baseline export validates every source route after downloading the archive and before returning or writing screenshot files. Both paths share the same route-id sanitizer when distinct ids would map to one filename. Existing manifests with duplicate or flattening `imagePath` values also fail closed, including scoped comparisons; rename the conflicting ids and recapture the affected baseline. The published package floors move `@snapdrift/manifest` to 1.3.0 and `@snapdrift/adapter-fs` to 1.1.0 so consumers receive the shared helper.
|
|
30
|
+
- **Baseline lookup failures no longer masquerade as missing artifacts** (#147). The baseline
|
|
31
|
+
resolver now reports `found`, `missing`, or `error`, fails standalone resolution on API/network/
|
|
32
|
+
malformed responses, and keeps the PR wrapper's hosted Snap path available while failing local
|
|
33
|
+
comparisons and local fallbacks when GitHub cannot provide the baseline. The existing intentional
|
|
34
|
+
first-run skipped summary remains limited to a successful lookup with no non-expired artifact.
|
|
35
|
+
- **Hosted Snap diffs now fail closed on incomplete results** (#146). Hosted comparison now validates `purpose: "diff"`, the persisted run metadata, and expected route/viewport identities, polls until the expected capture set is available, verifies the returned run id and paths, and rejects duplicate, missing, pending, malformed, or invalid captures as `incomplete`. A diffed capture is counted only when both image objects and a finite numeric `diffPct` are present; older results without expected capture identities must be recaptured instead of being treated as clean.
|
|
36
|
+
|
|
37
|
+
### Changed
|
|
38
|
+
|
|
39
|
+
- **Standalone baseline resolution now fails on lookup errors** (#147). Consumers orchestrating
|
|
40
|
+
`actions/resolve-baseline` should handle its `resolution-status` output: `missing` is the
|
|
41
|
+
intentional first-baseline case, while `error` means the GitHub lookup could not be trusted and
|
|
42
|
+
fails the action. The PR wrapper keeps hosted Snap comparisons available and reports the status
|
|
43
|
+
through `baseline-resolution-status`.
|
|
44
|
+
|
|
45
|
+
### Chore
|
|
46
|
+
|
|
47
|
+
- **The release workflow now fails when the root dispatcher's immutable pin predates wrapper changes** (#133). The Marketplace `action.yml` pins its inner `baseline` / `pr-diff` refs to an immutable commit SHA, but nothing detected when that pin went stale: between releases every change to `actions/baseline` or `actions/pr-diff` stayed invisible to consumers of the root action, so a release could ship bumped wrapper code while the dispatcher still executed old wrapper code at the pinned SHA. `npm run check:pin-stale` (new `scripts/check-pin-stale.mjs`) fails when the pinned SHA is older than the newest commit touching `actions/`. It runs in `publish.yml` (gated on a published release) rather than on every PR, because the pin is expected to lag wrapper changes on feature branches. The currently-stale pin in `action.yml` (which predated the #136 hosted-baseline fix) has been bumped to the latest `main` commit.
|
|
48
|
+
|
|
49
|
+
- **Workspace patch releases ship previously-unpublished fixes.** `@snapdrift/compare-core` 1.0.1 and `@snapdrift/adapter-report-md` 1.1.1 publish the #154 and #158 changes that landed after 1.0.0 / 1.1.0 were already on npm; without new versions those fixes would not have shipped. `@snapdrift/manifest` 1.3.0 and `@snapdrift/adapter-fs` 1.1.0 publish for the first time in this release. Workflow examples in the README, Integration Guide, and baseline template now reference `@v0.9.0`.
|
|
50
|
+
|
|
51
|
+
## 0.8.2 - 2026-08-24
|
|
52
|
+
|
|
53
|
+
### Fixes
|
|
54
|
+
|
|
55
|
+
- **Hosted baseline publication is now complete and fail-closed** — a scoped `route-ids` / `--routes` baseline run could publish only the changed routes and replace a larger accepted baseline, causing every omitted route to appear missing on the next PR (i2Dev-com/snap#720). `provider: "snap"` baseline capture now requires every configured route (local baselines, non-publishing captures, and hosted PR diffs remain scopeable), records the exact configured/selected route and viewport set plus one resolved branch/commit and publication workflow identity/sequence, and publishes only when the source run contains a successful stored capture for every expected identity. Complete requests send `publicationMode: "complete"` and `sourceRunId`, reuse the run's `branch` / `prHeadSha` as publication `refBranch` / `refSha`, and derive the baseline id deterministically from the run id so retries are stable. GitHub Actions supplies publication metadata automatically; other CI systems may set the explicit publication metadata variables. Missing, failed, duplicate, unexpected, or stale captures abort without publishing.
|
|
56
|
+
- **Snap outage handling is consistent across the CLI and both wrapper actions** — `warn-and-skip` and `fallback-local` were re-implemented independently in `lib/cli.mjs`, `actions/pr-diff` and `actions/baseline`, and the copies had drifted. `lib/outage-policy.mjs` now holds the single implementation (`captureWithPolicy`, `diffWithPolicy`, `publishBaselineWithPolicy`) that all three call, and the following are fixed as a result (#125):
|
|
57
|
+
- A `fallback-local` capture in `actions/pr-diff` captured locally but still emitted `provider=snap`, so the compare step rebuilt `SnapProvider` and handed it local results carrying no run id. The steps now receive the *effective* provider.
|
|
58
|
+
- A `fallback-local` diff swapped only the diff provider. A capture Snap had rendered server-side has a manifest but no PNGs on the runner, so the local pixel engine produced a dimension-only result rather than a comparison. The routes are now recaptured locally first, and the recaptured artifacts are what get staged and uploaded. When no baseline artifact was resolved, the run reports `missing_main_baseline_artifact` instead of crashing the local diff on a missing results file.
|
|
59
|
+
- `warn-and-skip` exited the wrapper without writing the documented skipped summary, so the PR comment fell through to its "Capture Failed" body. Capture-time and diff-time skips now write a skipped `summary.json`/`summary.md` with reason `snap_unavailable`, expose the summary outputs, stage the report, and exit 0. `diff.mode` is not enforced against a skipped summary.
|
|
60
|
+
- `actions/baseline` applied no policy at all to `publishBaseline()`. `warn-and-skip` now exits 0 without an artifact, and `fallback-local` captures locally and stages/uploads that bundle, so the run still leaves a usable baseline.
|
|
61
|
+
- `snapdrift diff` let a capture-time `SnapSkipError` reach the CLI entry point, which exits 1 — the opposite of what `warn-and-skip` is configured to do.
|
|
62
|
+
- **`shouldFailDriftCheck` no longer throws on a skipped summary** — it assumed every summary carried the full diff shape, so the final branch dereferenced `summaryData.errors.length` on `undefined` for the `{ status, reason, message, selectedRoutes }` shape a skipped run writes. `actions/enforce` defaults `summary-path` to exactly where those summaries land, so anyone orchestrating with the low-level actions rather than `actions/pr-diff` got a raw `TypeError` instead of a clean pass whenever a run was skipped for scope, a missing baseline, or a Snap outage. A summary with `status: "skipped"` or no `diff.mode` now returns `false` before any mode branch, and both `shouldFailDriftCheck` and `determineDriftStatus` default their array and counter reads (#132).
|
|
63
|
+
|
|
3
64
|
## 0.8.1 - 2026-08-06
|
|
4
65
|
|
|
5
66
|
### Fixes
|
package/README.md
CHANGED
|
@@ -48,17 +48,19 @@ You keep ownership of checkout, build, startup, readiness, and teardown. SnapDri
|
|
|
48
48
|
|
|
49
49
|
```yaml
|
|
50
50
|
- name: SnapDrift Baseline
|
|
51
|
-
uses: ranacseruet/snapdrift@v0.
|
|
51
|
+
uses: ranacseruet/snapdrift@v0.9.0
|
|
52
52
|
with:
|
|
53
53
|
mode: baseline
|
|
54
54
|
repo-config-path: .github/snapdrift.json
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
+
With `provider: "snap"`, baseline publication is a complete, fail-closed snapshot: it must run from the project's default branch and capture every configured route. Snap records the publication workflow identity plus a monotonic sequence so the API can reject stale overlapping publishers. GitHub Actions supplies these values automatically; other CI systems can provide `SNAPDRIFT_PUBLICATION_WORKFLOW_REF` and `SNAPDRIFT_PUBLICATION_SEQUENCE`. Route scoping remains available for pull request diffs, non-publishing `snapdrift capture`, and local-provider baselines.
|
|
58
|
+
|
|
57
59
|
**3. Run SnapDrift on pull requests:**
|
|
58
60
|
|
|
59
61
|
```yaml
|
|
60
62
|
- name: SnapDrift Report
|
|
61
|
-
uses: ranacseruet/snapdrift@v0.
|
|
63
|
+
uses: ranacseruet/snapdrift@v0.9.0
|
|
62
64
|
with:
|
|
63
65
|
mode: pr-diff
|
|
64
66
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -69,12 +71,17 @@ That is the full integration. See the [Integration Guide](docs/integration-guide
|
|
|
69
71
|
|
|
70
72
|
The root action dispatches on `mode`. The same pipelines are also published as standalone actions — `ranacseruet/snapdrift/actions/baseline` and `ranacseruet/snapdrift/actions/pr-diff` — along with the lower-level `capture`, `compare`, `scope`, `resolve-baseline`, `stage`, `comment`, and `enforce` actions for custom orchestration.
|
|
71
73
|
|
|
74
|
+
The lower-level `resolve-baseline` action reports `resolution-status` as `found`, `missing`, or
|
|
75
|
+
`error`. A successful lookup with no non-expired artifact is `missing`; GitHub API, permission,
|
|
76
|
+
network, or malformed-response failures are `error` and fail the standalone action so custom
|
|
77
|
+
workflows do not mistake an unavailable baseline for a first-run skip.
|
|
78
|
+
|
|
72
79
|
## Local CLI
|
|
73
80
|
|
|
74
|
-
SnapDrift ships a `snapdrift` CLI for running captures, diffs, migrations, and config initialization locally against a running app
|
|
81
|
+
SnapDrift ships a `snapdrift` CLI for running captures, diffs, migrations, and config initialization locally against a running app. Use it during development to validate UI changes before pushing. Canonical hosted baseline publication is the exception: `provider: "snap"` baselines require the project default branch plus explicit CI publication metadata; GitHub Actions provides that metadata automatically.
|
|
75
82
|
|
|
76
83
|
```bash
|
|
77
|
-
# Establish a
|
|
84
|
+
# Establish a local-provider baseline
|
|
78
85
|
snapdrift baseline
|
|
79
86
|
|
|
80
87
|
# Capture screenshots without publishing anything
|
|
@@ -111,6 +118,7 @@ Start with `report-only` while baselines settle. Move to `fail-on-changes` or st
|
|
|
111
118
|
- One global `diff.threshold`
|
|
112
119
|
- Dimension shifts are reported separately from pixel drift
|
|
113
120
|
- Local provider writes artifacts to the runner filesystem; for a hosted backend with a dashboard and a shared baseline store, configure `provider: "snap"` (see the [Integration Guide](docs/integration-guide.md#hosted-snap-provider))
|
|
121
|
+
- Hosted Snap baselines publish only from the default branch and always include the complete configured route set
|
|
114
122
|
|
|
115
123
|
## Outgrew GitHub artifacts?
|
|
116
124
|
|
package/action.yml
CHANGED
|
@@ -15,12 +15,16 @@ branding:
|
|
|
15
15
|
# action's own directory. `tests/marketplace-metadata.test.js` fails the build if those pins drift
|
|
16
16
|
# from the root `package.json` version.
|
|
17
17
|
#
|
|
18
|
-
#
|
|
19
|
-
# rather than a mutable tag one level down.
|
|
20
|
-
#
|
|
21
|
-
#
|
|
22
|
-
#
|
|
23
|
-
#
|
|
18
|
+
# Released versions use commit SHAs, so a consumer pinning this action to a SHA gets an immutable
|
|
19
|
+
# chain rather than a mutable tag one level down. Before that release commit exists, an unreleased
|
|
20
|
+
# version uses its upcoming `@vX.Y.Z` tag; the release process replaces it with the commit that holds
|
|
21
|
+
# the wrapper content. The contract test accepts either form and requires the declared version to
|
|
22
|
+
# match package.json.
|
|
23
|
+
#
|
|
24
|
+
# The release workflow also runs `npm run check:pin-stale`, which fails when the pinned SHA predates
|
|
25
|
+
# the newest commit touching `actions/`; that keeps a stale pin from silently serving old wrapper code
|
|
26
|
+
# to Marketplace consumers. The check is release-time only, since the pin is expected to lag wrapper
|
|
27
|
+
# changes on feature branches.
|
|
24
28
|
|
|
25
29
|
inputs:
|
|
26
30
|
mode:
|
|
@@ -32,7 +36,7 @@ inputs:
|
|
|
32
36
|
required: false
|
|
33
37
|
default: .github/snapdrift.json
|
|
34
38
|
route-ids:
|
|
35
|
-
description: Optional comma-separated route ids
|
|
39
|
+
description: Optional comma-separated route ids for PR diffs or local baselines. Hosted baselines must capture every configured route.
|
|
36
40
|
required: false
|
|
37
41
|
default: ''
|
|
38
42
|
artifact-retention-days:
|
|
@@ -121,6 +125,9 @@ outputs:
|
|
|
121
125
|
baseline-found:
|
|
122
126
|
description: Whether a usable baseline was found.
|
|
123
127
|
value: ${{ steps.pr-diff.outputs.baseline-found }}
|
|
128
|
+
baseline-resolution-status:
|
|
129
|
+
description: 'Baseline lookup status: found, missing, or error.'
|
|
130
|
+
value: ${{ steps.pr-diff.outputs.baseline-resolution-status }}
|
|
124
131
|
status:
|
|
125
132
|
description: Final SnapDrift status.
|
|
126
133
|
value: ${{ steps.pr-diff.outputs.status }}
|
|
@@ -148,7 +155,7 @@ runs:
|
|
|
148
155
|
- name: SnapDrift baseline
|
|
149
156
|
id: baseline
|
|
150
157
|
if: inputs.mode == 'baseline'
|
|
151
|
-
uses: ranacseruet/snapdrift/actions/baseline@
|
|
158
|
+
uses: ranacseruet/snapdrift/actions/baseline@331e62a7db4b9e743922667adacfe8fd93e51bb2 # v0.9.0
|
|
152
159
|
with:
|
|
153
160
|
repo-config-path: ${{ inputs.repo-config-path }}
|
|
154
161
|
route-ids: ${{ inputs.route-ids }}
|
|
@@ -158,7 +165,7 @@ runs:
|
|
|
158
165
|
- name: SnapDrift pull request diff
|
|
159
166
|
id: pr-diff
|
|
160
167
|
if: inputs.mode == 'pr-diff'
|
|
161
|
-
uses: ranacseruet/snapdrift/actions/pr-diff@
|
|
168
|
+
uses: ranacseruet/snapdrift/actions/pr-diff@331e62a7db4b9e743922667adacfe8fd93e51bb2 # v0.9.0
|
|
162
169
|
with:
|
|
163
170
|
github-token: ${{ inputs.github-token }}
|
|
164
171
|
repo-config-path: ${{ inputs.repo-config-path }}
|
|
@@ -11,7 +11,7 @@ inputs:
|
|
|
11
11
|
required: false
|
|
12
12
|
default: .github/snapdrift.json
|
|
13
13
|
route-ids:
|
|
14
|
-
description: Optional comma-separated route ids
|
|
14
|
+
description: Optional comma-separated route ids for local-provider baselines. Hosted Snap baselines must capture every configured route.
|
|
15
15
|
required: false
|
|
16
16
|
default: ''
|
|
17
17
|
artifact-retention-days:
|
|
@@ -32,16 +32,16 @@ outputs:
|
|
|
32
32
|
value: ${{ steps.stage.outputs.bundle_dir }}
|
|
33
33
|
results-file:
|
|
34
34
|
description: Absolute path to the generated baseline results file.
|
|
35
|
-
value: ${{ steps.capture.outputs.results_file }}
|
|
35
|
+
value: ${{ steps.publish.outputs.results_file || steps.capture.outputs.results_file }}
|
|
36
36
|
manifest-file:
|
|
37
37
|
description: Absolute path to the generated baseline manifest file.
|
|
38
|
-
value: ${{ steps.capture.outputs.manifest_file }}
|
|
38
|
+
value: ${{ steps.publish.outputs.manifest_file || steps.capture.outputs.manifest_file }}
|
|
39
39
|
screenshots-root:
|
|
40
40
|
description: Absolute path to the generated screenshots root.
|
|
41
|
-
value: ${{ steps.capture.outputs.screenshots_root }}
|
|
41
|
+
value: ${{ steps.publish.outputs.screenshots_root || steps.capture.outputs.screenshots_root }}
|
|
42
42
|
selected-route-ids:
|
|
43
43
|
description: Comma-separated route ids captured in this run.
|
|
44
|
-
value: ${{ steps.capture.outputs.selected_route_ids }}
|
|
44
|
+
value: ${{ steps.publish.outputs.selected_route_ids || steps.capture.outputs.selected_route_ids }}
|
|
45
45
|
|
|
46
46
|
runs:
|
|
47
47
|
using: composite
|
|
@@ -103,49 +103,37 @@ runs:
|
|
|
103
103
|
|
|
104
104
|
const providerModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/provider.mjs');
|
|
105
105
|
const configModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/snapdrift-config.mjs');
|
|
106
|
-
const
|
|
106
|
+
const policyModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/outage-policy.mjs');
|
|
107
|
+
const { createProvider } = await import(pathToFileURL(providerModulePath));
|
|
107
108
|
const { loadSnapdriftConfig, splitCommaList } = await import(pathToFileURL(configModulePath));
|
|
109
|
+
const { captureWithPolicy } = await import(pathToFileURL(policyModulePath));
|
|
108
110
|
|
|
109
111
|
const { config } = await loadSnapdriftConfig(process.env.REPO_CONFIG_PATH || undefined);
|
|
110
|
-
const
|
|
112
|
+
const providerName = config.provider || 'local';
|
|
111
113
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
114
|
+
const captured = await captureWithPolicy({
|
|
115
|
+
provider: createProvider(providerName, config),
|
|
116
|
+
providerName,
|
|
117
|
+
config,
|
|
118
|
+
captureOptions: {
|
|
115
119
|
configPath: process.env.REPO_CONFIG_PATH || undefined,
|
|
116
120
|
routeIds: splitCommaList(process.env.ROUTE_IDS),
|
|
117
121
|
purpose: 'baseline'
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
process.stderr.write(`[SnapDrift] Snap unavailable — falling back to local provider.\n`);
|
|
127
|
-
const { createProvider: cp2 } = await import(pathToFileURL(providerModulePath));
|
|
128
|
-
result = await cp2('local', config).capture({
|
|
129
|
-
configPath: process.env.REPO_CONFIG_PATH || undefined,
|
|
130
|
-
routeIds: splitCommaList(process.env.ROUTE_IDS)
|
|
131
|
-
});
|
|
132
|
-
// Report as local so publish is skipped and stage/upload run instead
|
|
133
|
-
const outputs = [
|
|
134
|
-
`provider=local`,
|
|
135
|
-
`results_file=${result.resultsPath}`,
|
|
136
|
-
`manifest_file=${result.manifestPath}`,
|
|
137
|
-
`screenshots_root=${result.screenshotsRoot}`,
|
|
138
|
-
`selected_route_ids=${result.selectedRouteIds.join(',')}`
|
|
139
|
-
].join('\n') + '\n';
|
|
140
|
-
await fs.appendFile(process.env.GITHUB_OUTPUT, outputs);
|
|
141
|
-
process.exit(0);
|
|
142
|
-
} else {
|
|
143
|
-
throw err;
|
|
144
|
-
}
|
|
122
|
+
},
|
|
123
|
+
onSkip: () => process.stderr.write('[SnapDrift] Snap unavailable — skipping baseline capture (warn-and-skip).\n'),
|
|
124
|
+
onFallback: () => process.stderr.write('[SnapDrift] Snap unavailable — falling back to local provider.\n')
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
if (captured.outcome === 'skipped') {
|
|
128
|
+
await fs.appendFile(process.env.GITHUB_OUTPUT, `provider=skipped\n`);
|
|
129
|
+
process.exit(0);
|
|
145
130
|
}
|
|
146
131
|
|
|
132
|
+
const result = captured.result;
|
|
147
133
|
const outputs = [
|
|
148
|
-
|
|
134
|
+
// The effective provider: reporting "local" after a fallback-local
|
|
135
|
+
// capture is what makes publish skip and stage/upload run instead.
|
|
136
|
+
`provider=${captured.providerName}`,
|
|
149
137
|
`results_file=${result.resultsPath}`,
|
|
150
138
|
`manifest_file=${result.manifestPath}`,
|
|
151
139
|
`screenshots_root=${result.screenshotsRoot}`,
|
|
@@ -160,34 +148,67 @@ runs:
|
|
|
160
148
|
shell: bash
|
|
161
149
|
env:
|
|
162
150
|
REPO_CONFIG_PATH: ${{ inputs.repo-config-path }}
|
|
151
|
+
ROUTE_IDS: ${{ inputs.route-ids }}
|
|
163
152
|
RESULTS_PATH: ${{ steps.capture.outputs.results_file }}
|
|
164
153
|
run: |
|
|
165
154
|
node --input-type=module <<'EOF'
|
|
155
|
+
import fs from 'node:fs/promises';
|
|
166
156
|
import path from 'node:path';
|
|
167
157
|
import { pathToFileURL } from 'node:url';
|
|
168
158
|
|
|
169
159
|
const providerModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/provider.mjs');
|
|
170
160
|
const configModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/snapdrift-config.mjs');
|
|
161
|
+
const policyModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/outage-policy.mjs');
|
|
171
162
|
const { createProvider } = await import(pathToFileURL(providerModulePath));
|
|
172
|
-
const { loadSnapdriftConfig } = await import(pathToFileURL(configModulePath));
|
|
163
|
+
const { loadSnapdriftConfig, splitCommaList } = await import(pathToFileURL(configModulePath));
|
|
164
|
+
const { publishBaselineWithPolicy } = await import(pathToFileURL(policyModulePath));
|
|
173
165
|
|
|
174
166
|
const { config } = await loadSnapdriftConfig(process.env.REPO_CONFIG_PATH || undefined);
|
|
175
|
-
const provider = createProvider('snap', config);
|
|
176
167
|
|
|
177
|
-
|
|
178
|
-
|
|
168
|
+
// Snap can go down after the capture submission succeeds — while polling
|
|
169
|
+
// the render or posting the baseline. onUnavailable has to cover that
|
|
170
|
+
// window too, or warn-and-skip still fails the build and fallback-local
|
|
171
|
+
// still leaves the repo with no baseline artifact. See #125.
|
|
172
|
+
const published = await publishBaselineWithPolicy({
|
|
173
|
+
provider: createProvider('snap', config),
|
|
174
|
+
publishOptions: { resultsPath: process.env.RESULTS_PATH || undefined },
|
|
175
|
+
captureOptions: {
|
|
176
|
+
configPath: process.env.REPO_CONFIG_PATH || undefined,
|
|
177
|
+
routeIds: splitCommaList(process.env.ROUTE_IDS),
|
|
178
|
+
purpose: 'baseline'
|
|
179
|
+
},
|
|
180
|
+
onSkip: () => process.stderr.write('[SnapDrift] Snap unavailable — skipping baseline publish (warn-and-skip).\n'),
|
|
181
|
+
onFallback: () => process.stderr.write('[SnapDrift] Snap unavailable during publish — capturing locally so the run still produces a baseline artifact.\n')
|
|
179
182
|
});
|
|
180
183
|
|
|
184
|
+
if (published.outcome === 'skipped') {
|
|
185
|
+
await fs.appendFile(process.env.GITHUB_OUTPUT, `provider=skipped\n`);
|
|
186
|
+
process.exit(0);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (published.outcome === 'fell-back') {
|
|
190
|
+
const recapture = published.recapture;
|
|
191
|
+
const outputs = [
|
|
192
|
+
`provider=local`,
|
|
193
|
+
`results_file=${recapture.resultsPath}`,
|
|
194
|
+
`manifest_file=${recapture.manifestPath}`,
|
|
195
|
+
`screenshots_root=${recapture.screenshotsRoot}`,
|
|
196
|
+
`selected_route_ids=${recapture.selectedRouteIds.join(',')}`
|
|
197
|
+
].join('\n') + '\n';
|
|
198
|
+
await fs.appendFile(process.env.GITHUB_OUTPUT, outputs);
|
|
199
|
+
process.exit(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
181
202
|
process.stdout.write('[SnapDrift] Snap baseline published successfully.\n');
|
|
182
203
|
EOF
|
|
183
204
|
|
|
184
205
|
- id: stage
|
|
185
|
-
if: steps.capture.outputs.provider != 'snap' && steps.capture.outputs.provider != 'skipped'
|
|
206
|
+
if: (steps.capture.outputs.provider != 'snap' && steps.capture.outputs.provider != 'skipped') || steps.publish.outputs.provider == 'local'
|
|
186
207
|
shell: bash
|
|
187
208
|
env:
|
|
188
|
-
RESULTS_PATH: ${{ steps.capture.outputs.results_file }}
|
|
189
|
-
MANIFEST_PATH: ${{ steps.capture.outputs.manifest_file }}
|
|
190
|
-
SCREENSHOTS_DIR: ${{ steps.capture.outputs.screenshots_root }}/screenshots
|
|
209
|
+
RESULTS_PATH: ${{ steps.publish.outputs.results_file || steps.capture.outputs.results_file }}
|
|
210
|
+
MANIFEST_PATH: ${{ steps.publish.outputs.manifest_file || steps.capture.outputs.manifest_file }}
|
|
211
|
+
SCREENSHOTS_DIR: ${{ steps.publish.outputs.screenshots_root || steps.capture.outputs.screenshots_root }}/screenshots
|
|
191
212
|
run: |
|
|
192
213
|
node --input-type=module <<'EOF'
|
|
193
214
|
import fs from 'node:fs/promises';
|
|
@@ -207,7 +228,7 @@ runs:
|
|
|
207
228
|
await fs.appendFile(process.env.GITHUB_OUTPUT, `bundle_dir=${result.bundleDir}\n`);
|
|
208
229
|
EOF
|
|
209
230
|
|
|
210
|
-
- if: inputs.upload-artifact == 'true' && steps.capture.outputs.provider != 'snap' && steps.capture.outputs.provider != 'skipped'
|
|
231
|
+
- if: inputs.upload-artifact == 'true' && ((steps.capture.outputs.provider != 'snap' && steps.capture.outputs.provider != 'skipped') || steps.publish.outputs.provider == 'local')
|
|
211
232
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
212
233
|
with:
|
|
213
234
|
name: ${{ steps.config.outputs.artifact_name }}
|