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 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.8.1
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.8.1
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 — no GitHub Actions required. Use it during development to validate UI changes before pushing.
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 baseline — for provider "snap" this also publishes it
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
- # The pins are commit SHAs, so that a consumer pinning this action to a SHA gets an immutable chain
19
- # rather than a mutable tag one level down. The SHA is the commit holding the wrapper content this
20
- # release ships — not the release tag itself, which a release commit cannot reference. It therefore
21
- # only moves when the wrappers change. The trailing comment records the release the pin was last
22
- # verified for, and the test requires it to match the package version, so every release has to look
23
- # at it. The test accepts a plain `@vX.Y.Z` tag too, for unreleased refs.
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. In `pr-diff` mode, providing these skips changed-file scope detection.
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@539953ab07065be805be393d69bd3c9b6d53cf6e # v0.8.1
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@539953ab07065be805be393d69bd3c9b6d53cf6e # v0.8.1
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 to capture.
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 { createProvider, SnapSkipError, SnapFallbackError } = await import(pathToFileURL(providerModulePath));
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 provider = createProvider(config.provider || 'local', config);
112
+ const providerName = config.provider || 'local';
111
113
 
112
- let result;
113
- try {
114
- result = await provider.capture({
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
- } catch (err) {
120
- if (err instanceof SnapSkipError) {
121
- process.stderr.write(`[SnapDrift] Snap unavailable — skipping baseline capture (warn-and-skip).\n`);
122
- await fs.appendFile(process.env.GITHUB_OUTPUT, `provider=skipped\n`);
123
- process.exit(0);
124
- }
125
- if (err instanceof SnapFallbackError) {
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
- `provider=${config.provider || 'local'}`,
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
- await provider.publishBaseline({
178
- resultsPath: process.env.RESULTS_PATH || undefined
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 }}