snapdrift 0.8.2 → 0.10.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 +74 -0
- package/README.md +8 -3
- package/action.yml +10 -2
- package/actions/comment/action.yml +6 -0
- package/actions/compare/action.yml +6 -0
- package/actions/pr-diff/action.yml +102 -55
- package/actions/resolve-baseline/action.yml +36 -50
- package/actions/scope/action.yml +24 -1
- package/actions/stage/action.yml +7 -1
- package/bin/snapdrift.mjs +6 -3
- package/docs/contracts.md +125 -9
- package/docs/integration-guide.md +61 -12
- package/docs/local-cli.md +15 -1
- package/docs/workflow-templates/refresh-baseline-on-merge.yml +1 -1
- package/lib/cli.mjs +140 -52
- package/lib/init-from-action.mjs +24 -13
- package/lib/outage-policy.mjs +35 -2
- package/lib/provider.mjs +39 -12
- package/lib/report.mjs +3 -2
- package/lib/resolve-baseline-artifact.mjs +147 -0
- package/lib/resolve-baseline-paths.mjs +40 -0
- package/lib/snap-provider.mjs +1164 -481
- package/package.json +15 -11
- package/types/visual-diff-types.d.ts +30 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,79 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.10.0 - 2026-09-10
|
|
4
|
+
|
|
5
|
+
### Fixes
|
|
6
|
+
|
|
7
|
+
- **PR diff artifacts remain reviewable from GitHub comments** — generated local
|
|
8
|
+
diff paths are now linked to the authenticated Actions artifact (or workflow
|
|
9
|
+
run fallback) instead of being emitted as broken relative image URLs. Hosted
|
|
10
|
+
legacy captures also retain their reported dimensions and pixel denominator
|
|
11
|
+
when v1 comparison metadata is absent.
|
|
12
|
+
|
|
13
|
+
### Features
|
|
14
|
+
|
|
15
|
+
- **Unequal-dimension comparisons** — `@snapdrift/compare-core` now exposes
|
|
16
|
+
`compareImages()` for top-left-aligned union-canvas comparisons while the
|
|
17
|
+
existing strict APIs remain unchanged. Updated local and hosted providers
|
|
18
|
+
explicitly request comparison policy v1, record comparison dimensions and the
|
|
19
|
+
geometric union denominator, stage a generated diff PNG, and render it in
|
|
20
|
+
Markdown, HTML, and PR reports. Low-level public adapter calls remain strict
|
|
21
|
+
unless they opt in with `diff.comparisonPolicy`. Legacy dimension shifts remain
|
|
22
|
+
in `dimensionChanges[]`; completed v1 dimension changes are `changed[]` signals
|
|
23
|
+
and do not fail `fail-on-incomplete` alone.
|
|
24
|
+
|
|
25
|
+
- **Workspace packages** — publish `@snapdrift/manifest` 1.4.0,
|
|
26
|
+
`@snapdrift/compare-core` 1.1.0, `@snapdrift/adapter-report-md` 1.2.0, and
|
|
27
|
+
`@snapdrift/adapter-fs` 1.2.0 before activating the updated hosted Snap client.
|
|
28
|
+
|
|
29
|
+
## 0.9.0 - 2026-09-09
|
|
30
|
+
|
|
31
|
+
### Fixes
|
|
32
|
+
|
|
33
|
+
- **Workspace packages now declare their runtime dependency boundaries** (#148).
|
|
34
|
+
`@snapdrift/adapter-fs` declares `@snapdrift/adapter-report-md`, and an isolated
|
|
35
|
+
packed-consumer gate imports every workspace package with only its declared
|
|
36
|
+
local dependency closure. Repository workspaces can no longer mask a missing
|
|
37
|
+
runtime dependency.
|
|
38
|
+
|
|
39
|
+
- **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.
|
|
40
|
+
|
|
41
|
+
- **Snap transport now has enforceable request and operation deadlines** (#150). JSON requests
|
|
42
|
+
settle stalled headers or bodies within 30 seconds, binary exports within 120 seconds, and each
|
|
43
|
+
Snap operation shares a 10-minute budget across requests, retries, backoff, and polling. Aborted
|
|
44
|
+
or otherwise exhausted transport operations now reach the configured `onUnavailable` policy;
|
|
45
|
+
received 4xx responses remain immediate and non-retryable, while stalled 4xx diagnostics retain
|
|
46
|
+
their HTTP status with a timeout diagnostic.
|
|
47
|
+
- **Renamed files now retain both paths during route scoping** (#149). The
|
|
48
|
+
`scope` and `pr-diff` actions include a nonempty `previous_filename` for
|
|
49
|
+
GitHub rename records, deduplicate the combined paths, and preserve the
|
|
50
|
+
existing force-run and lookup-fallback behavior (with explicit route ids
|
|
51
|
+
still taking precedence in `pr-diff`). If GitHub returns its 3,000-record
|
|
52
|
+
cap, both actions run all configured routes with reason
|
|
53
|
+
`changed_files_truncated` rather than trust an incomplete list. A file moved
|
|
54
|
+
out of a watched directory can no longer silently skip its route.
|
|
55
|
+
- **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.
|
|
56
|
+
- **Baseline lookup failures no longer masquerade as missing artifacts** (#147). The baseline
|
|
57
|
+
resolver now reports `found`, `missing`, or `error`, fails standalone resolution on API/network/
|
|
58
|
+
malformed responses, and keeps the PR wrapper's hosted Snap path available while failing local
|
|
59
|
+
comparisons and local fallbacks when GitHub cannot provide the baseline. The existing intentional
|
|
60
|
+
first-run skipped summary remains limited to a successful lookup with no non-expired artifact.
|
|
61
|
+
- **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.
|
|
62
|
+
|
|
63
|
+
### Changed
|
|
64
|
+
|
|
65
|
+
- **Standalone baseline resolution now fails on lookup errors** (#147). Consumers orchestrating
|
|
66
|
+
`actions/resolve-baseline` should handle its `resolution-status` output: `missing` is the
|
|
67
|
+
intentional first-baseline case, while `error` means the GitHub lookup could not be trusted and
|
|
68
|
+
fails the action. The PR wrapper keeps hosted Snap comparisons available and reports the status
|
|
69
|
+
through `baseline-resolution-status`.
|
|
70
|
+
|
|
71
|
+
### Chore
|
|
72
|
+
|
|
73
|
+
- **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.
|
|
74
|
+
|
|
75
|
+
- **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`.
|
|
76
|
+
|
|
3
77
|
## 0.8.2 - 2026-08-24
|
|
4
78
|
|
|
5
79
|
### Fixes
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ 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.10.0
|
|
52
52
|
with:
|
|
53
53
|
mode: baseline
|
|
54
54
|
repo-config-path: .github/snapdrift.json
|
|
@@ -60,7 +60,7 @@ With `provider: "snap"`, baseline publication is a complete, fail-closed snapsho
|
|
|
60
60
|
|
|
61
61
|
```yaml
|
|
62
62
|
- name: SnapDrift Report
|
|
63
|
-
uses: ranacseruet/snapdrift@v0.
|
|
63
|
+
uses: ranacseruet/snapdrift@v0.10.0
|
|
64
64
|
with:
|
|
65
65
|
mode: pr-diff
|
|
66
66
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -71,6 +71,11 @@ That is the full integration. See the [Integration Guide](docs/integration-guide
|
|
|
71
71
|
|
|
72
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.
|
|
73
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
|
+
|
|
74
79
|
## Local CLI
|
|
75
80
|
|
|
76
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.
|
|
@@ -111,7 +116,7 @@ Start with `report-only` while baselines settle. Move to `fail-on-changes` or st
|
|
|
111
116
|
- Full-page capture only
|
|
112
117
|
- Viewport presets: `desktop` (1440×900) and `mobile` (390×844), or custom `{ "width": number, "height": number }`
|
|
113
118
|
- One global `diff.threshold`
|
|
114
|
-
-
|
|
119
|
+
- Unequal dimensions remain strict by default; local updated clients may opt in with `diff.comparisonPolicy: { "version": 1, "threshold": number }`
|
|
115
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))
|
|
116
121
|
- Hosted Snap baselines publish only from the default branch and always include the complete configured route set
|
|
117
122
|
|
package/action.yml
CHANGED
|
@@ -20,6 +20,11 @@ branding:
|
|
|
20
20
|
# version uses its upcoming `@vX.Y.Z` tag; the release process replaces it with the commit that holds
|
|
21
21
|
# the wrapper content. The contract test accepts either form and requires the declared version to
|
|
22
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.
|
|
23
28
|
|
|
24
29
|
inputs:
|
|
25
30
|
mode:
|
|
@@ -120,6 +125,9 @@ outputs:
|
|
|
120
125
|
baseline-found:
|
|
121
126
|
description: Whether a usable baseline was found.
|
|
122
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 }}
|
|
123
131
|
status:
|
|
124
132
|
description: Final SnapDrift status.
|
|
125
133
|
value: ${{ steps.pr-diff.outputs.status }}
|
|
@@ -147,7 +155,7 @@ runs:
|
|
|
147
155
|
- name: SnapDrift baseline
|
|
148
156
|
id: baseline
|
|
149
157
|
if: inputs.mode == 'baseline'
|
|
150
|
-
uses: ranacseruet/snapdrift/actions/baseline@
|
|
158
|
+
uses: ranacseruet/snapdrift/actions/baseline@80d61b668942dd87c33cf51598623a904becf82f # v0.10.0
|
|
151
159
|
with:
|
|
152
160
|
repo-config-path: ${{ inputs.repo-config-path }}
|
|
153
161
|
route-ids: ${{ inputs.route-ids }}
|
|
@@ -157,7 +165,7 @@ runs:
|
|
|
157
165
|
- name: SnapDrift pull request diff
|
|
158
166
|
id: pr-diff
|
|
159
167
|
if: inputs.mode == 'pr-diff'
|
|
160
|
-
uses: ranacseruet/snapdrift/actions/pr-diff@
|
|
168
|
+
uses: ranacseruet/snapdrift/actions/pr-diff@80d61b668942dd87c33cf51598623a904becf82f # v0.10.0
|
|
161
169
|
with:
|
|
162
170
|
github-token: ${{ inputs.github-token }}
|
|
163
171
|
repo-config-path: ${{ inputs.repo-config-path }}
|
|
@@ -24,6 +24,10 @@ inputs:
|
|
|
24
24
|
description: Artifact label to include in the comment.
|
|
25
25
|
required: false
|
|
26
26
|
default: ''
|
|
27
|
+
artifact-url:
|
|
28
|
+
description: Authenticated URL to the uploaded report artifact.
|
|
29
|
+
required: false
|
|
30
|
+
default: ''
|
|
27
31
|
repo-config-path:
|
|
28
32
|
description: Path to the SnapDrift config file (needed for provider-aware comment rendering).
|
|
29
33
|
required: false
|
|
@@ -58,6 +62,7 @@ runs:
|
|
|
58
62
|
VISUAL_DIFF_SUMMARY_PATH: ${{ inputs.summary-path }}
|
|
59
63
|
VISUAL_DIFF_RUN_URL: ${{ inputs.run-url }}
|
|
60
64
|
VISUAL_DIFF_ARTIFACT_NAME: ${{ inputs.artifact-name }}
|
|
65
|
+
VISUAL_DIFF_ARTIFACT_URL: ${{ inputs.artifact-url }}
|
|
61
66
|
INPUT_PR_NUMBER: ${{ inputs.pr-number }}
|
|
62
67
|
REPO_CONFIG_PATH: ${{ inputs.repo-config-path }}
|
|
63
68
|
MAX_CHANGED_ROWS: ${{ inputs.max-changed-rows }}
|
|
@@ -105,6 +110,7 @@ runs:
|
|
|
105
110
|
const maxErrorRows = Number(process.env.MAX_ERROR_ROWS) || 10;
|
|
106
111
|
const body = provider.buildCommentBody(summary, {
|
|
107
112
|
artifactName: process.env.VISUAL_DIFF_ARTIFACT_NAME || '',
|
|
113
|
+
artifactUrl: process.env.VISUAL_DIFF_ARTIFACT_URL || '',
|
|
108
114
|
runUrl,
|
|
109
115
|
maxChangedRows,
|
|
110
116
|
maxErrorRows
|
|
@@ -67,6 +67,9 @@ outputs:
|
|
|
67
67
|
markdown-path:
|
|
68
68
|
description: Absolute path to the generated summary markdown file.
|
|
69
69
|
value: ${{ steps.compare.outputs.markdown_path }}
|
|
70
|
+
diff-images-dir:
|
|
71
|
+
description: Absolute path to generated local diff PNGs.
|
|
72
|
+
value: ${{ steps.compare.outputs.diff_images_dir }}
|
|
70
73
|
status:
|
|
71
74
|
description: Computed SnapDrift status.
|
|
72
75
|
value: ${{ steps.compare.outputs.status }}
|
|
@@ -133,6 +136,7 @@ runs:
|
|
|
133
136
|
const resolvedOutDir = path.resolve(process.env.OUT_DIR || 'qa-artifacts/snapdrift/drift/current');
|
|
134
137
|
const resolvedSummaryPath = path.resolve(process.env.SUMMARY_PATH || path.join(resolvedOutDir, 'summary.json'));
|
|
135
138
|
const resolvedMarkdownPath = path.resolve(process.env.MARKDOWN_PATH || path.join(resolvedOutDir, 'summary.md'));
|
|
139
|
+
const resolvedDiffImagesDir = path.join(resolvedOutDir, 'diffs');
|
|
136
140
|
|
|
137
141
|
await runDriftCheckCli({
|
|
138
142
|
configPath: process.env.REPO_CONFIG_PATH || undefined,
|
|
@@ -143,6 +147,7 @@ runs:
|
|
|
143
147
|
baselineRunDir: process.env.BASELINE_RUN_DIR || undefined,
|
|
144
148
|
currentRunDir: process.env.CURRENT_RUN_DIR || undefined,
|
|
145
149
|
outDir: resolvedOutDir,
|
|
150
|
+
diffImagesDir: resolvedDiffImagesDir,
|
|
146
151
|
summaryPath: resolvedSummaryPath,
|
|
147
152
|
markdownPath: resolvedMarkdownPath,
|
|
148
153
|
routeIds: splitCommaList(process.env.ROUTE_IDS),
|
|
@@ -155,6 +160,7 @@ runs:
|
|
|
155
160
|
const outputs = [
|
|
156
161
|
`summary_path=${resolvedSummaryPath}`,
|
|
157
162
|
`markdown_path=${resolvedMarkdownPath}`,
|
|
163
|
+
`diff_images_dir=${resolvedDiffImagesDir}`,
|
|
158
164
|
`status=${summary.status || ''}`,
|
|
159
165
|
`changed_screenshots=${summary.changedScreenshots || 0}`,
|
|
160
166
|
`matched_screenshots=${summary.matchedScreenshots || 0}`,
|
|
@@ -71,6 +71,9 @@ outputs:
|
|
|
71
71
|
baseline-found:
|
|
72
72
|
description: Whether a usable baseline was found.
|
|
73
73
|
value: ${{ steps.finalize.outputs.baseline_found }}
|
|
74
|
+
baseline-resolution-status:
|
|
75
|
+
description: 'Baseline lookup status: found, missing, or error.'
|
|
76
|
+
value: ${{ steps.finalize.outputs.baseline_resolution_status }}
|
|
74
77
|
status:
|
|
75
78
|
description: Final SnapDrift status.
|
|
76
79
|
value: ${{ steps.finalize.outputs.status }}
|
|
@@ -182,10 +185,33 @@ runs:
|
|
|
182
185
|
per_page: 100
|
|
183
186
|
});
|
|
184
187
|
|
|
188
|
+
if (!Array.isArray(files)) {
|
|
189
|
+
throw new Error('Malformed GitHub changed-file response: expected an array of file records.');
|
|
190
|
+
}
|
|
191
|
+
for (const [index, file] of files.entries()) {
|
|
192
|
+
if (!file || typeof file !== 'object' || typeof file.filename !== 'string' || file.filename.length === 0) {
|
|
193
|
+
throw new Error(`Malformed GitHub changed-file response: file record ${index} must include a non-empty string filename.`);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (files.length >= 3000) {
|
|
197
|
+
core.warning('GitHub returned the maximum 3000 changed-file records; the list may be truncated, so running all configured captures.');
|
|
198
|
+
setOutputs({
|
|
199
|
+
shouldRun: true,
|
|
200
|
+
reason: 'changed_files_truncated',
|
|
201
|
+
selectedRouteIds: allRouteIds
|
|
202
|
+
});
|
|
203
|
+
return;
|
|
204
|
+
}
|
|
205
|
+
const changedFiles = [...new Set(files.flatMap((file) => [
|
|
206
|
+
file.filename,
|
|
207
|
+
...(file.status === 'renamed' && typeof file.previous_filename === 'string' && file.previous_filename
|
|
208
|
+
? [file.previous_filename]
|
|
209
|
+
: [])
|
|
210
|
+
]))];
|
|
185
211
|
setOutputs(
|
|
186
212
|
selectRoutesForChangedFiles(
|
|
187
213
|
config,
|
|
188
|
-
|
|
214
|
+
changedFiles
|
|
189
215
|
)
|
|
190
216
|
);
|
|
191
217
|
} catch (error) {
|
|
@@ -212,61 +238,40 @@ runs:
|
|
|
212
238
|
const { pathToFileURL } = require('url');
|
|
213
239
|
|
|
214
240
|
const repository = process.env.INPUT_REPOSITORY || `${context.repo.owner}/${context.repo.repo}`;
|
|
215
|
-
const [owner, repo] = repository.split('/');
|
|
216
241
|
const modulePath = path.resolve(process.env.ACTION_ROOT, 'lib/snapdrift-config.mjs');
|
|
242
|
+
const resolverModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/resolve-baseline-artifact.mjs');
|
|
217
243
|
const { loadSnapdriftConfig } = await import(pathToFileURL(modulePath));
|
|
218
|
-
const {
|
|
219
|
-
|
|
244
|
+
const { resolveBaselineArtifact } = await import(pathToFileURL(resolverModulePath));
|
|
245
|
+
let artifactName = '';
|
|
246
|
+
|
|
247
|
+
const setLookupError = (error) => {
|
|
248
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
249
|
+
const message = `Unable to resolve the SnapDrift baseline artifact: ${detail}`;
|
|
250
|
+
core.setOutput('found', 'false');
|
|
251
|
+
core.setOutput('resolution_status', 'error');
|
|
252
|
+
core.setOutput('artifact_name', artifactName);
|
|
253
|
+
core.setOutput('message', message);
|
|
254
|
+
core.warning(message);
|
|
255
|
+
};
|
|
220
256
|
|
|
221
257
|
try {
|
|
222
|
-
const
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
258
|
+
const { config } = await loadSnapdriftConfig(process.env.REPO_CONFIG_PATH || undefined);
|
|
259
|
+
artifactName = config.baselineArtifactName;
|
|
260
|
+
const result = await resolveBaselineArtifact({
|
|
261
|
+
github,
|
|
262
|
+
repository,
|
|
263
|
+
workflowId: process.env.INPUT_WORKFLOW_ID || 'ci.yml',
|
|
226
264
|
branch: process.env.INPUT_BRANCH || 'main',
|
|
227
|
-
|
|
228
|
-
per_page: 20
|
|
265
|
+
artifactName
|
|
229
266
|
});
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
continue;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
|
240
|
-
owner,
|
|
241
|
-
repo,
|
|
242
|
-
run_id: run.id,
|
|
243
|
-
per_page: 100
|
|
244
|
-
});
|
|
245
|
-
|
|
246
|
-
const match = artifacts.data.artifacts.find((artifact) => artifact.name === artifactName && !artifact.expired);
|
|
247
|
-
if (match) {
|
|
248
|
-
selectedRun = run;
|
|
249
|
-
selectedArtifact = match;
|
|
250
|
-
break;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
if (!selectedRun || !selectedArtifact) {
|
|
255
|
-
core.setOutput('found', 'false');
|
|
256
|
-
core.setOutput('artifact_name', artifactName);
|
|
257
|
-
core.setOutput('message', `No non-expired SnapDrift baseline artifact named ${artifactName} was found.`);
|
|
258
|
-
return;
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
core.setOutput('found', 'true');
|
|
262
|
-
core.setOutput('artifact_name', selectedArtifact.name);
|
|
263
|
-
core.setOutput('head_sha', selectedRun.head_sha);
|
|
264
|
-
core.setOutput('run_id', String(selectedRun.id));
|
|
265
|
-
core.setOutput('message', '');
|
|
267
|
+
core.setOutput('found', String(result.found));
|
|
268
|
+
core.setOutput('resolution_status', result.resolutionStatus);
|
|
269
|
+
core.setOutput('artifact_name', result.artifactName);
|
|
270
|
+
core.setOutput('head_sha', result.headSha || '');
|
|
271
|
+
core.setOutput('run_id', result.runId || '');
|
|
272
|
+
core.setOutput('message', result.message);
|
|
266
273
|
} catch (error) {
|
|
267
|
-
|
|
268
|
-
core.setOutput('artifact_name', artifactName);
|
|
269
|
-
core.setOutput('message', `Unable to resolve the SnapDrift baseline artifact: ${error.message}`);
|
|
274
|
+
setLookupError(error);
|
|
270
275
|
}
|
|
271
276
|
|
|
272
277
|
- if: steps.scope.outputs.should_run == 'true' && steps.baseline.outputs.found == 'true'
|
|
@@ -320,6 +325,16 @@ runs:
|
|
|
320
325
|
await fs.appendFile(process.env.GITHUB_OUTPUT, outputs);
|
|
321
326
|
EOF
|
|
322
327
|
|
|
328
|
+
- id: baseline_resolution_guard
|
|
329
|
+
if: steps.scope.outputs.should_run == 'true' && steps.baseline.outputs.resolution_status == 'error' && steps.config.outputs.provider != 'snap'
|
|
330
|
+
shell: bash
|
|
331
|
+
env:
|
|
332
|
+
BASELINE_MESSAGE: ${{ steps.baseline.outputs.message }}
|
|
333
|
+
run: |
|
|
334
|
+
set -euo pipefail
|
|
335
|
+
printf 'SnapDrift baseline lookup failed. %s\n' "$BASELINE_MESSAGE" >&2
|
|
336
|
+
exit 1
|
|
337
|
+
|
|
323
338
|
- name: Install Playwright Chromium
|
|
324
339
|
if: steps.scope.outputs.should_run == 'true' && (steps.config.outputs.provider != 'snap' || steps.config.outputs.on_unavailable == 'fallback-local' || steps.config.outputs.snap_local_capture == 'true')
|
|
325
340
|
shell: bash
|
|
@@ -332,6 +347,8 @@ runs:
|
|
|
332
347
|
env:
|
|
333
348
|
REPO_CONFIG_PATH: ${{ inputs.repo-config-path }}
|
|
334
349
|
ROUTE_IDS: ${{ steps.scope.outputs.selected_route_ids }}
|
|
350
|
+
BASELINE_RESOLUTION_STATUS: ${{ steps.baseline.outputs.resolution_status }}
|
|
351
|
+
BASELINE_RESOLUTION_MESSAGE: ${{ steps.baseline.outputs.message }}
|
|
335
352
|
run: |
|
|
336
353
|
node --input-type=module <<'EOF'
|
|
337
354
|
import fs from 'node:fs/promises';
|
|
@@ -359,6 +376,8 @@ runs:
|
|
|
359
376
|
configPath: process.env.REPO_CONFIG_PATH || undefined,
|
|
360
377
|
routeIds
|
|
361
378
|
},
|
|
379
|
+
baselineResolutionStatus: process.env.BASELINE_RESOLUTION_STATUS,
|
|
380
|
+
baselineResolutionMessage: process.env.BASELINE_RESOLUTION_MESSAGE,
|
|
362
381
|
onSkip: () => process.stderr.write('[SnapDrift] Snap unavailable — skipping visual regression (warn-and-skip).\n'),
|
|
363
382
|
onFallback: () => process.stderr.write('[SnapDrift] Snap unavailable — falling back to local provider.\n')
|
|
364
383
|
});
|
|
@@ -414,6 +433,8 @@ runs:
|
|
|
414
433
|
CAPTURE_PROVIDER: ${{ steps.capture.outputs.provider }}
|
|
415
434
|
LOCAL_SCREENSHOTS: ${{ steps.capture.outputs.local_screenshots }}
|
|
416
435
|
BASELINE_FOUND: ${{ steps.baseline.outputs.found }}
|
|
436
|
+
BASELINE_RESOLUTION_STATUS: ${{ steps.baseline.outputs.resolution_status }}
|
|
437
|
+
BASELINE_RESOLUTION_MESSAGE: ${{ steps.baseline.outputs.message }}
|
|
417
438
|
run: |
|
|
418
439
|
node --input-type=module <<'EOF'
|
|
419
440
|
import fs from 'node:fs/promises';
|
|
@@ -438,6 +459,7 @@ runs:
|
|
|
438
459
|
const resolvedOutDir = path.resolve('qa-artifacts/snapdrift/drift/current');
|
|
439
460
|
const resolvedSummaryPath = path.join(resolvedOutDir, 'summary.json');
|
|
440
461
|
const resolvedMarkdownPath = path.join(resolvedOutDir, 'summary.md');
|
|
462
|
+
const resolvedDiffImagesDir = path.join(resolvedOutDir, 'diffs');
|
|
441
463
|
|
|
442
464
|
const captureOptions = {
|
|
443
465
|
configPath: process.env.REPO_CONFIG_PATH || undefined,
|
|
@@ -454,7 +476,8 @@ runs:
|
|
|
454
476
|
baselineSourceSha: process.env.BASELINE_SOURCE_SHA || undefined,
|
|
455
477
|
currentResultsPath: process.env.CURRENT_RESULTS_PATH || undefined,
|
|
456
478
|
currentManifestPath: process.env.CURRENT_MANIFEST_PATH || undefined,
|
|
457
|
-
currentRunDir: process.env.CURRENT_SCREENSHOTS_ROOT || undefined
|
|
479
|
+
currentRunDir: process.env.CURRENT_SCREENSHOTS_ROOT || undefined,
|
|
480
|
+
diffImagesDir: resolvedDiffImagesDir
|
|
458
481
|
};
|
|
459
482
|
|
|
460
483
|
const diffed = await diffWithPolicy({
|
|
@@ -464,10 +487,18 @@ runs:
|
|
|
464
487
|
captureOptions,
|
|
465
488
|
localScreenshots: process.env.LOCAL_SCREENSHOTS !== 'false',
|
|
466
489
|
baselineAvailable: process.env.BASELINE_FOUND === 'true',
|
|
490
|
+
baselineResolutionStatus: process.env.BASELINE_RESOLUTION_STATUS,
|
|
491
|
+
baselineResolutionMessage: process.env.BASELINE_RESOLUTION_MESSAGE,
|
|
467
492
|
onSkip: () => process.stderr.write('[SnapDrift] Snap unavailable during diff — skipping (warn-and-skip).\n'),
|
|
468
493
|
onFallback: () => process.stderr.write('[SnapDrift] Snap unavailable during diff — falling back to local provider.\n'),
|
|
469
494
|
onRecapture: () => process.stderr.write('[SnapDrift] The current capture was rendered by Snap, so it has no local screenshots — recapturing locally before diffing.\n'),
|
|
470
|
-
onBaselineUnavailable: () =>
|
|
495
|
+
onBaselineUnavailable: () => {
|
|
496
|
+
if (process.env.BASELINE_RESOLUTION_STATUS === 'error') {
|
|
497
|
+
process.stderr.write(`[SnapDrift] ${process.env.BASELINE_RESOLUTION_MESSAGE || 'The GitHub baseline lookup failed before local diff fallback.'}\n`);
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
process.stderr.write('[SnapDrift] No baseline artifact is available, so the local diff cannot run.\n');
|
|
501
|
+
}
|
|
471
502
|
});
|
|
472
503
|
|
|
473
504
|
// Every non-throwing outcome writes a summary. A wrapper run that ends
|
|
@@ -543,7 +574,7 @@ runs:
|
|
|
543
574
|
EOF
|
|
544
575
|
|
|
545
576
|
- id: skipped_baseline
|
|
546
|
-
if: steps.scope.outputs.should_run == 'true' && steps.baseline.outputs.
|
|
577
|
+
if: steps.scope.outputs.should_run == 'true' && steps.baseline.outputs.resolution_status == 'missing' && steps.capture.outputs.provider != 'snap' && steps.capture.outputs.provider != 'skipped'
|
|
547
578
|
shell: bash
|
|
548
579
|
env:
|
|
549
580
|
MESSAGE: ${{ steps.baseline.outputs.message }}
|
|
@@ -586,6 +617,7 @@ runs:
|
|
|
586
617
|
CURRENT_MANIFEST_PATH: ${{ steps.compare.outputs.current_manifest_file || steps.capture.outputs.manifest_file }}
|
|
587
618
|
BASELINE_SCREENSHOTS_DIR: ${{ steps.baseline_paths.outputs.screenshots_dir }}
|
|
588
619
|
CURRENT_SCREENSHOTS_ROOT: ${{ steps.compare.outputs.current_screenshots_root || steps.capture.outputs.screenshots_root }}
|
|
620
|
+
DIFF_IMAGES_DIR: ${{ steps.compare.outputs.diff_images_dir }}
|
|
589
621
|
run: |
|
|
590
622
|
node --input-type=module <<'EOF'
|
|
591
623
|
import fs from 'node:fs/promises';
|
|
@@ -606,13 +638,15 @@ runs:
|
|
|
606
638
|
baselineManifestPath: process.env.BASELINE_MANIFEST_PATH || undefined,
|
|
607
639
|
currentManifestPath: process.env.CURRENT_MANIFEST_PATH || undefined,
|
|
608
640
|
baselineScreenshotsDir: process.env.BASELINE_SCREENSHOTS_DIR || undefined,
|
|
609
|
-
currentScreenshotsDir: currentScreenshotsRoot ? path.join(currentScreenshotsRoot, 'screenshots') : undefined
|
|
641
|
+
currentScreenshotsDir: currentScreenshotsRoot ? path.join(currentScreenshotsRoot, 'screenshots') : undefined,
|
|
642
|
+
diffImagesDir: process.env.DIFF_IMAGES_DIR || undefined
|
|
610
643
|
});
|
|
611
644
|
|
|
612
645
|
await fs.appendFile(process.env.GITHUB_OUTPUT, `bundle_dir=${result.bundleDir}\n`);
|
|
613
646
|
EOF
|
|
614
647
|
|
|
615
|
-
-
|
|
648
|
+
- id: upload
|
|
649
|
+
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
616
650
|
with:
|
|
617
651
|
name: ${{ steps.context.outputs.artifact_name }}
|
|
618
652
|
retention-days: ${{ inputs.artifact-retention-days }}
|
|
@@ -634,10 +668,13 @@ runs:
|
|
|
634
668
|
PR_NUMBER: ${{ steps.context.outputs.pr_number }}
|
|
635
669
|
SUMMARY_PATH: ${{ steps.compare.outputs.summary_path || steps.capture.outputs.summary_path || steps.skipped_baseline.outputs.summary_path || steps.skipped_scope.outputs.summary_path }}
|
|
636
670
|
ARTIFACT_NAME: ${{ steps.context.outputs.artifact_name }}
|
|
671
|
+
ARTIFACT_URL: ${{ steps.upload.outputs.artifact-url }}
|
|
637
672
|
RUN_URL: ${{ steps.context.outputs.run_url }}
|
|
638
673
|
REPO_CONFIG_PATH: ${{ inputs.repo-config-path }}
|
|
639
674
|
MAX_CHANGED_ROWS: ${{ inputs.max-changed-rows }}
|
|
640
675
|
MAX_ERROR_ROWS: ${{ inputs.max-error-rows }}
|
|
676
|
+
BASELINE_RESOLUTION_STATUS: ${{ steps.baseline.outputs.resolution_status }}
|
|
677
|
+
BASELINE_RESOLUTION_MESSAGE: ${{ steps.baseline.outputs.message }}
|
|
641
678
|
with:
|
|
642
679
|
github-token: ${{ inputs.github-token }}
|
|
643
680
|
script: |
|
|
@@ -649,6 +686,8 @@ runs:
|
|
|
649
686
|
const PR_COMMENT_MARKERS = [PR_COMMENT_MARKER];
|
|
650
687
|
const iconUrl = 'https://raw.githubusercontent.com/ranacseruet/snapdrift/main/assets/snapdrift-logo-icon.png';
|
|
651
688
|
const repoUrl = 'https://github.com/ranacseruet/snapdrift';
|
|
689
|
+
const prCommentModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/pr-comment.mjs');
|
|
690
|
+
const { escapeMarkdown } = await import(pathToFileURL(prCommentModulePath));
|
|
652
691
|
|
|
653
692
|
const summaryPath = process.env.SUMMARY_PATH;
|
|
654
693
|
const issueNumber = Number(process.env.PR_NUMBER || context.payload.pull_request?.number || context.issue?.number || '');
|
|
@@ -659,7 +698,12 @@ runs:
|
|
|
659
698
|
|
|
660
699
|
if (!summaryPath || !fs.existsSync(summaryPath)) {
|
|
661
700
|
const runUrl = process.env.RUN_URL || '';
|
|
662
|
-
const
|
|
701
|
+
const lookupFailure = process.env.BASELINE_RESOLUTION_STATUS === 'error';
|
|
702
|
+
const heading = lookupFailure ? 'Baseline Lookup Failed' : 'Capture Failed';
|
|
703
|
+
const detail = escapeMarkdown(lookupFailure
|
|
704
|
+
? process.env.BASELINE_RESOLUTION_MESSAGE || 'The GitHub baseline lookup failed before comparison could run.'
|
|
705
|
+
: 'SnapDrift did not produce a summary. This usually means capture failed before the report could be assembled.');
|
|
706
|
+
const body = `${PR_COMMENT_MARKER}\n<img src="${iconUrl}" alt="SnapDrift" width="20" height="20" />\n\n### ⚠️ SnapDrift Report — ${heading}\n\n${detail}\n\n[View run](${runUrl}) for details.\n\n<div align="right"><sub>Powered by <a href="${repoUrl}">SnapDrift</a></sub></div>`;
|
|
663
707
|
await github.rest.issues.createComment({
|
|
664
708
|
owner: context.repo.owner,
|
|
665
709
|
repo: context.repo.repo,
|
|
@@ -689,6 +733,7 @@ runs:
|
|
|
689
733
|
const maxErrorRows = Number(process.env.MAX_ERROR_ROWS) || 10;
|
|
690
734
|
const body = provider.buildCommentBody(summary, {
|
|
691
735
|
artifactName: process.env.ARTIFACT_NAME || '',
|
|
736
|
+
artifactUrl: process.env.ARTIFACT_URL || '',
|
|
692
737
|
runUrl: process.env.RUN_URL || '',
|
|
693
738
|
maxChangedRows,
|
|
694
739
|
maxErrorRows
|
|
@@ -761,6 +806,7 @@ runs:
|
|
|
761
806
|
SCOPE_REASON: ${{ steps.scope.outputs.reason }}
|
|
762
807
|
SELECTED_ROUTE_IDS: ${{ steps.scope.outputs.selected_route_ids }}
|
|
763
808
|
BASELINE_FOUND: ${{ steps.baseline.outputs.found || 'false' }}
|
|
809
|
+
BASELINE_RESOLUTION_STATUS: ${{ steps.baseline.outputs.resolution_status || '' }}
|
|
764
810
|
STATUS: ${{ steps.compare.outputs.status || steps.capture.outputs.status || steps.skipped_baseline.outputs.status || steps.skipped_scope.outputs.status || '' }}
|
|
765
811
|
SUMMARY_PATH: ${{ steps.compare.outputs.summary_path || steps.capture.outputs.summary_path || steps.skipped_baseline.outputs.summary_path || steps.skipped_scope.outputs.summary_path || '' }}
|
|
766
812
|
MARKDOWN_PATH: ${{ steps.compare.outputs.markdown_path || steps.capture.outputs.markdown_path || steps.skipped_baseline.outputs.markdown_path || steps.skipped_scope.outputs.markdown_path || '' }}
|
|
@@ -771,6 +817,7 @@ runs:
|
|
|
771
817
|
echo "scope_reason=${SCOPE_REASON:-}"
|
|
772
818
|
echo "selected_route_ids=${SELECTED_ROUTE_IDS:-}"
|
|
773
819
|
echo "baseline_found=${BASELINE_FOUND:-false}"
|
|
820
|
+
echo "baseline_resolution_status=${BASELINE_RESOLUTION_STATUS:-}"
|
|
774
821
|
echo "status=${STATUS:-}"
|
|
775
822
|
echo "summary_path=${SUMMARY_PATH:-}"
|
|
776
823
|
echo "markdown_path=${MARKDOWN_PATH:-}"
|
|
@@ -38,6 +38,9 @@ outputs:
|
|
|
38
38
|
found:
|
|
39
39
|
description: Whether a non-expired baseline was found.
|
|
40
40
|
value: ${{ steps.resolve.outputs.found }}
|
|
41
|
+
resolution-status:
|
|
42
|
+
description: 'Baseline lookup status: found, missing, or error.'
|
|
43
|
+
value: ${{ steps.resolve.outputs.resolution_status }}
|
|
41
44
|
artifact-name:
|
|
42
45
|
description: Resolved artifact name.
|
|
43
46
|
value: ${{ steps.resolve.outputs.artifact_name }}
|
|
@@ -48,7 +51,7 @@ outputs:
|
|
|
48
51
|
description: Workflow run id that produced the baseline.
|
|
49
52
|
value: ${{ steps.resolve.outputs.run_id }}
|
|
50
53
|
message:
|
|
51
|
-
description:
|
|
54
|
+
description: Resolution message for a missing or failed lookup.
|
|
52
55
|
value: ${{ steps.resolve.outputs.message }}
|
|
53
56
|
results-file:
|
|
54
57
|
description: Path to the downloaded baseline results JSON.
|
|
@@ -81,61 +84,40 @@ runs:
|
|
|
81
84
|
const { pathToFileURL } = require('url');
|
|
82
85
|
|
|
83
86
|
const repository = process.env.INPUT_REPOSITORY || `${context.repo.owner}/${context.repo.repo}`;
|
|
84
|
-
const [owner, repo] = repository.split('/');
|
|
85
87
|
const modulePath = path.resolve(process.env.GITHUB_WORKSPACE, process.env.GITHUB_ACTION_PATH, '..', '..', 'lib', 'snapdrift-config.mjs');
|
|
88
|
+
const resolverModulePath = path.resolve(process.env.GITHUB_WORKSPACE, process.env.GITHUB_ACTION_PATH, '..', '..', 'lib', 'resolve-baseline-artifact.mjs');
|
|
86
89
|
const { loadSnapdriftConfig } = await import(pathToFileURL(modulePath));
|
|
87
|
-
const {
|
|
88
|
-
|
|
90
|
+
const { resolveBaselineArtifact } = await import(pathToFileURL(resolverModulePath));
|
|
91
|
+
let artifactName = process.env.INPUT_ARTIFACT_NAME || '';
|
|
92
|
+
|
|
93
|
+
const setLookupError = (error) => {
|
|
94
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
95
|
+
const message = `Unable to resolve the SnapDrift baseline artifact: ${detail}`;
|
|
96
|
+
core.setOutput('found', 'false');
|
|
97
|
+
core.setOutput('resolution_status', 'error');
|
|
98
|
+
core.setOutput('artifact_name', artifactName);
|
|
99
|
+
core.setOutput('message', message);
|
|
100
|
+
core.setFailed(message);
|
|
101
|
+
};
|
|
89
102
|
|
|
90
103
|
try {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
104
|
+
const { config } = await loadSnapdriftConfig(process.env.REPO_CONFIG_PATH || undefined);
|
|
105
|
+
artifactName = process.env.INPUT_ARTIFACT_NAME || config.baselineArtifactName;
|
|
106
|
+
const result = await resolveBaselineArtifact({
|
|
107
|
+
github,
|
|
108
|
+
repository,
|
|
109
|
+
workflowId: process.env.INPUT_WORKFLOW_ID || 'ci.yml',
|
|
95
110
|
branch: process.env.INPUT_BRANCH || 'main',
|
|
96
|
-
|
|
97
|
-
per_page: 20
|
|
111
|
+
artifactName
|
|
98
112
|
});
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
continue;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const artifacts = await github.rest.actions.listWorkflowRunArtifacts({
|
|
109
|
-
owner,
|
|
110
|
-
repo,
|
|
111
|
-
run_id: run.id,
|
|
112
|
-
per_page: 100
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
const match = artifacts.data.artifacts.find((artifact) => artifact.name === artifactName && !artifact.expired);
|
|
116
|
-
if (match) {
|
|
117
|
-
selectedRun = run;
|
|
118
|
-
selectedArtifact = match;
|
|
119
|
-
break;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (!selectedRun || !selectedArtifact) {
|
|
124
|
-
core.setOutput('found', 'false');
|
|
125
|
-
core.setOutput('artifact_name', artifactName);
|
|
126
|
-
core.setOutput('message', `No non-expired SnapDrift baseline artifact named ${artifactName} was found.`);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
core.setOutput('found', 'true');
|
|
131
|
-
core.setOutput('artifact_name', selectedArtifact.name);
|
|
132
|
-
core.setOutput('head_sha', selectedRun.head_sha);
|
|
133
|
-
core.setOutput('run_id', String(selectedRun.id));
|
|
134
|
-
core.setOutput('message', '');
|
|
113
|
+
core.setOutput('found', String(result.found));
|
|
114
|
+
core.setOutput('resolution_status', result.resolutionStatus);
|
|
115
|
+
core.setOutput('artifact_name', result.artifactName);
|
|
116
|
+
core.setOutput('head_sha', result.headSha || '');
|
|
117
|
+
core.setOutput('run_id', result.runId || '');
|
|
118
|
+
core.setOutput('message', result.message);
|
|
135
119
|
} catch (error) {
|
|
136
|
-
|
|
137
|
-
core.setOutput('artifact_name', artifactName);
|
|
138
|
-
core.setOutput('message', `Unable to resolve the SnapDrift baseline artifact: ${error.message}`);
|
|
120
|
+
setLookupError(error);
|
|
139
121
|
}
|
|
140
122
|
|
|
141
123
|
- if: steps.resolve.outputs.found == 'true'
|
|
@@ -164,6 +146,10 @@ runs:
|
|
|
164
146
|
{
|
|
165
147
|
echo "results_file=$RESULTS_FILE"
|
|
166
148
|
echo "manifest_file=$MANIFEST_FILE"
|
|
167
|
-
|
|
149
|
+
case "$DOWNLOAD_PATH" in
|
|
150
|
+
/*) RUN_DIR="$DOWNLOAD_PATH" ;;
|
|
151
|
+
*) RUN_DIR="${GITHUB_WORKSPACE}/${DOWNLOAD_PATH}" ;;
|
|
152
|
+
esac
|
|
153
|
+
echo "run_dir=$RUN_DIR"
|
|
168
154
|
echo "screenshots_dir=$(dirname "$MANIFEST_FILE")/screenshots"
|
|
169
155
|
} >> "$GITHUB_OUTPUT"
|