snapdrift 0.12.0 → 0.14.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 +113 -0
- package/README.md +7 -3
- package/action.yml +2 -2
- package/actions/comment/action.yml +8 -37
- package/actions/pr-diff/action.yml +24 -121
- package/actions/scope/action.yml +15 -73
- package/docs/contracts.md +148 -8
- package/docs/integration-guide.md +18 -12
- package/docs/local-cli.md +67 -5
- package/docs/workflow-templates/refresh-baseline-on-merge.yml +1 -1
- package/lib/cli.mjs +4 -0
- package/lib/github-requests.mjs +134 -0
- package/lib/outage-policy.mjs +49 -14
- package/lib/provider.mjs +19 -1
- package/lib/snap-provider.mjs +80 -11
- package/package.json +6 -5
- package/types/visual-diff-types.d.ts +29 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,118 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## Unreleased
|
|
4
|
+
|
|
5
|
+
## 0.14.0 - 2026-09-21
|
|
6
|
+
|
|
7
|
+
Workspace releases this version:
|
|
8
|
+
|
|
9
|
+
- `@snapdrift/compare-core` 1.5.0 — bounded vertical row alignment and comparison
|
|
10
|
+
policy v2, with conservative coordinate fallbacks and resource limits.
|
|
11
|
+
- `@snapdrift/manifest` 1.6.0 — policy v2 validation, constants, and comparison types.
|
|
12
|
+
- `@snapdrift/adapter-fs` 1.5.0 — local policy v2 comparison and summary types.
|
|
13
|
+
- `@snapdrift/adapter-report-md` 1.4.0 — alignment and fallback details in Markdown,
|
|
14
|
+
HTML, and bounded PR comments.
|
|
15
|
+
|
|
16
|
+
Root and workspace dependency floors require these versions so upgrades receive
|
|
17
|
+
all parts of the policy v2 implementation. The dispatcher pins `576e774`, the
|
|
18
|
+
#176 merge commit on `main`, including the runtime loaded by both wrapper actions.
|
|
19
|
+
|
|
20
|
+
### Features
|
|
21
|
+
|
|
22
|
+
- Added opt-in comparison policy v2 with bounded vertical row alignment for
|
|
23
|
+
inserted and deleted page rows. Unchanged content that shifts after an
|
|
24
|
+
insertion remains matched; reports record aligned results and conservative
|
|
25
|
+
coordinate fallbacks. Deprecated policy v1 remains the compatibility default
|
|
26
|
+
so existing configurations are unchanged; new configurations should use v2.
|
|
27
|
+
|
|
28
|
+
- Snap-provider requests forward policy v2 and validate hosted acknowledgements
|
|
29
|
+
and alignment metadata. Hosted use requires matching Snap service support;
|
|
30
|
+
publishing this library does not deploy the hosted service.
|
|
31
|
+
|
|
32
|
+
## 0.13.0 - 2026-09-19
|
|
33
|
+
|
|
34
|
+
Workspace releases this version:
|
|
35
|
+
|
|
36
|
+
- `@snapdrift/compare-core` 1.4.0 — the semantic diff palette (`addedColor`/`removedColor`
|
|
37
|
+
options and the orange default for changed pixels).
|
|
38
|
+
- `@snapdrift/adapter-report-md` 1.3.0 — the exported `DIFF_IMAGE_LEGEND` constant rendered
|
|
39
|
+
alongside diff images.
|
|
40
|
+
- `@snapdrift/adapter-fs` 1.4.0 — cross-route browser storage isolation (#171), capture
|
|
41
|
+
profile v2 validation and the compatibility-before-pixels gate (#172), and the semantic
|
|
42
|
+
diff image handoff to the reporting packages.
|
|
43
|
+
- `@snapdrift/manifest` 1.5.0 — capture profile v2 schema, capture-profile helpers, and the
|
|
44
|
+
`captureCompatibility` summary types (#172).
|
|
45
|
+
|
|
46
|
+
The root dependency floors move to `@snapdrift/adapter-fs ^1.4.0`,
|
|
47
|
+
`@snapdrift/compare-core ^1.4.0`, and `@snapdrift/manifest ^1.5.0`; `adapter-fs` and
|
|
48
|
+
`adapter-report-md` in turn require `compare-core ^1.4.0`, `adapter-report-md ^1.3.0`, and
|
|
49
|
+
`manifest ^1.5.0`. The dispatcher pins 3967590 — the #174 merge commit on `main`, which
|
|
50
|
+
contains the newest `actions/` implementation (#173) and the `lib/`/`packages/` runtime the
|
|
51
|
+
wrappers load.
|
|
52
|
+
|
|
53
|
+
### Features
|
|
54
|
+
|
|
55
|
+
- Local manifests now persist capture profile v2: engine, browser/Playwright, OS,
|
|
56
|
+
locale/timezone, and rendering settings. Local comparison validates manifests,
|
|
57
|
+
requires exact v2 profile equality, and checks configured route paths and
|
|
58
|
+
normalized viewports before reading PNGs. Incompatibilities are
|
|
59
|
+
`incompatible_capture` errors, not product drift from changed image dimensions;
|
|
60
|
+
the summary's `captureCompatibility` status describes profiles only.
|
|
61
|
+
- Legacy missing/v1 profiles remain unverified and pixel-comparable after route
|
|
62
|
+
checks unless shared metadata conflicts or an explicit foreign engine is found.
|
|
63
|
+
The unverified diagnostic appears in CLI, Markdown, and HTML reports as well as
|
|
64
|
+
JSON summaries and PR comments. Malformed or unsupported profiles are rejected.
|
|
65
|
+
Refresh using `snapdrift baseline`
|
|
66
|
+
(`provider: "local"`) or the existing baseline action; `report-only` acknowledges
|
|
67
|
+
incompatibility nonblockingly but never bypasses it to compare pixels.
|
|
68
|
+
- Local diff images now use a semantic palette so the rendered PNG reflects the
|
|
69
|
+
kind of change, not just that pixels differ. `@snapdrift/compare-core` union-canvas
|
|
70
|
+
comparisons render pixels present only in the current capture in green (`addedColor`,
|
|
71
|
+
default `[0, 170, 0, 255]`), pixels present only in the baseline in red (`removedColor`,
|
|
72
|
+
default `[255, 0, 0, 255]`), and overlapping changed pixels in orange (`highlightColor`,
|
|
73
|
+
default `[255, 140, 0, 255]`). Metrics are unchanged — one-sided pixels still count as
|
|
74
|
+
changed under comparison policy v1. Strict same-dimension `generateDiffImage` diffs
|
|
75
|
+
switch their default highlight from red to orange for parity and cannot contain
|
|
76
|
+
added/removed pixels. Markdown, HTML, and PR-comment reports render a legend
|
|
77
|
+
("orange = changed · green = added · red = removed") whenever a local diff image is
|
|
78
|
+
embedded. Consumers comparing diff-PNG bytes between versions will see different
|
|
79
|
+
output by design; the new options restore any previous palette.
|
|
80
|
+
|
|
81
|
+
### Refactoring
|
|
82
|
+
|
|
83
|
+
- GitHub-script steps in `actions/scope`, `actions/pr-diff` (scope and PR report),
|
|
84
|
+
and `actions/comment` now delegate to the shared, dependency-injected helpers in
|
|
85
|
+
`lib/github-requests.mjs` (`fetchPullRequestFiles`, `resolveScopeDecision`,
|
|
86
|
+
`resolvePullRequestScope`, `upsertPullRequestReportComment`), so the changed-file
|
|
87
|
+
selection and comment-upsert behavior is defined once and exercised by unit and
|
|
88
|
+
real-YAML parity tests. Outputs, reasons, warnings, markers, and fallback bodies
|
|
89
|
+
are byte-for-byte identical.
|
|
90
|
+
- Providers and `captureWithPolicy`/`diffWithPolicy` results now include an explicit
|
|
91
|
+
`artifacts` descriptor (`describeCaptureArtifacts`: `localScreenshots` plus
|
|
92
|
+
`artifactsRoot` for local or hosted local-capture hybrid runs). Fallback decisions
|
|
93
|
+
prefer provider-returned capabilities, with provider-name/URL inference retained
|
|
94
|
+
for legacy results and explicit caller overrides taking precedence. Diff callers
|
|
95
|
+
without capability information or config retain the no-recapture legacy default.
|
|
96
|
+
`localScreenshots` remains, and
|
|
97
|
+
`hasLocalScreenshots` is kept as a thin wrapper. `lib/provider.mjs` exports
|
|
98
|
+
`PROVIDER_CAPABILITIES` and `providerSupports(provider, capability)` so the
|
|
99
|
+
unavailable `LocalProvider.fetchLatestBaseline` baseline-store capability is
|
|
100
|
+
discoverable without a runtime throw.
|
|
101
|
+
|
|
102
|
+
### Behavior changes
|
|
103
|
+
|
|
104
|
+
- Local capture explicitly sets `en-US` / `UTC`; localized text/date changes may
|
|
105
|
+
require baseline recapture. Fonts are not fingerprinted. See the
|
|
106
|
+
[capture profile contract](docs/contracts.md#local-capture-profile-v2) and
|
|
107
|
+
[baseline refresh guide](docs/local-cli.md#refreshing-or-acknowledging-local-baselines).
|
|
108
|
+
|
|
109
|
+
### Fixes
|
|
110
|
+
|
|
111
|
+
- Isolate each local route capture and retry in a fresh browser context, preventing
|
|
112
|
+
cookies and browser storage from leaking between routes sharing a viewport.
|
|
113
|
+
Per-viewport concurrency and result ordering are preserved. This also fixes
|
|
114
|
+
Snap local-capture hybrid runs; recapture baselines affected by leaked storage.
|
|
115
|
+
|
|
3
116
|
## 0.12.0 - 2026-09-16
|
|
4
117
|
|
|
5
118
|
### 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.14.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.14.0
|
|
64
64
|
with:
|
|
65
65
|
mode: pr-diff
|
|
66
66
|
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
@@ -110,6 +110,10 @@ Start with `report-only` while baselines settle. Move to `fail-on-changes` or st
|
|
|
110
110
|
| `fail-on-incomplete` | Captures are missing or comparison errors occur |
|
|
111
111
|
| `strict` | Any drift signal or incomplete comparison appears |
|
|
112
112
|
|
|
113
|
+
Local comparisons check capture profiles and configured route/path/viewport identity
|
|
114
|
+
before pixels. Incompatible captures produce incomplete errors, not product drift;
|
|
115
|
+
`report-only` never bypasses these checks. See [compatibility and baseline refresh](docs/local-cli.md#refreshing-or-acknowledging-local-baselines).
|
|
116
|
+
|
|
113
117
|
## Current constraints
|
|
114
118
|
|
|
115
119
|
- Ubuntu runners only (local CLI works on any OS Node 22+ supports)
|
|
@@ -117,7 +121,7 @@ Start with `report-only` while baselines settle. Move to `fail-on-changes` or st
|
|
|
117
121
|
- Viewport presets: `desktop` (1440×900) and `mobile` (390×844), or custom `{ "width": number, "height": number }`
|
|
118
122
|
- Full-page comparisons are bounded to a 32 Mi-pixel union canvas; see the [screenshot size budget](docs/contracts.md#screenshot-size-budget) for viewport, device-scale, and remediation guidance
|
|
119
123
|
- One global `diff.threshold`
|
|
120
|
-
- Unequal dimensions
|
|
124
|
+
- Unequal dimensions use deprecated top-left union comparison policy v1 by default for compatibility; new configurations should opt into policy v2 with `{ "version": 2, "threshold": number }` for bounded vertical row alignment of inserted/deleted content
|
|
121
125
|
- 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))
|
|
122
126
|
- Hosted Snap baselines publish only from the default branch and always include the complete configured route set
|
|
123
127
|
|
package/action.yml
CHANGED
|
@@ -155,7 +155,7 @@ runs:
|
|
|
155
155
|
- name: SnapDrift baseline
|
|
156
156
|
id: baseline
|
|
157
157
|
if: inputs.mode == 'baseline'
|
|
158
|
-
uses: ranacseruet/snapdrift/actions/baseline@
|
|
158
|
+
uses: ranacseruet/snapdrift/actions/baseline@576e774845c29288a0061365e4905009533e0e01 # v0.14.0
|
|
159
159
|
with:
|
|
160
160
|
repo-config-path: ${{ inputs.repo-config-path }}
|
|
161
161
|
route-ids: ${{ inputs.route-ids }}
|
|
@@ -165,7 +165,7 @@ runs:
|
|
|
165
165
|
- name: SnapDrift pull request diff
|
|
166
166
|
id: pr-diff
|
|
167
167
|
if: inputs.mode == 'pr-diff'
|
|
168
|
-
uses: ranacseruet/snapdrift/actions/pr-diff@
|
|
168
|
+
uses: ranacseruet/snapdrift/actions/pr-diff@576e774845c29288a0061365e4905009533e0e01 # v0.14.0
|
|
169
169
|
with:
|
|
170
170
|
github-token: ${{ inputs.github-token }}
|
|
171
171
|
repo-config-path: ${{ inputs.repo-config-path }}
|
|
@@ -116,41 +116,12 @@ runs:
|
|
|
116
116
|
maxErrorRows
|
|
117
117
|
});
|
|
118
118
|
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
119
|
+
const requestsModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/github-requests.mjs');
|
|
120
|
+
const { upsertPullRequestReportComment } = await import(pathToFileURL(requestsModulePath));
|
|
121
|
+
await upsertPullRequestReportComment({
|
|
122
|
+
github,
|
|
123
|
+
...context.repo,
|
|
124
|
+
issueNumber,
|
|
125
|
+
body,
|
|
126
|
+
markers: PR_COMMENT_MARKERS
|
|
124
127
|
});
|
|
125
|
-
const matchingComments = comments
|
|
126
|
-
.filter((comment) => comment.body && PR_COMMENT_MARKERS.some((marker) => comment.body.includes(marker)))
|
|
127
|
-
.sort((left, right) => {
|
|
128
|
-
const leftTime = new Date(left.updated_at || left.created_at).getTime();
|
|
129
|
-
const rightTime = new Date(right.updated_at || right.created_at).getTime();
|
|
130
|
-
return rightTime - leftTime;
|
|
131
|
-
});
|
|
132
|
-
const existing = matchingComments[0];
|
|
133
|
-
|
|
134
|
-
if (existing) {
|
|
135
|
-
await github.rest.issues.updateComment({
|
|
136
|
-
owner: context.repo.owner,
|
|
137
|
-
repo: context.repo.repo,
|
|
138
|
-
comment_id: existing.id,
|
|
139
|
-
body
|
|
140
|
-
});
|
|
141
|
-
} else {
|
|
142
|
-
await github.rest.issues.createComment({
|
|
143
|
-
owner: context.repo.owner,
|
|
144
|
-
repo: context.repo.repo,
|
|
145
|
-
issue_number: issueNumber,
|
|
146
|
-
body
|
|
147
|
-
});
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
for (const duplicate of matchingComments.slice(1)) {
|
|
151
|
-
await github.rest.issues.deleteComment({
|
|
152
|
-
owner: context.repo.owner,
|
|
153
|
-
repo: context.repo.repo,
|
|
154
|
-
comment_id: duplicate.id
|
|
155
|
-
});
|
|
156
|
-
}
|
|
@@ -137,91 +137,23 @@ runs:
|
|
|
137
137
|
const { pathToFileURL } = require('url');
|
|
138
138
|
|
|
139
139
|
const configModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/snapdrift-config.mjs');
|
|
140
|
-
const { loadSnapdriftConfig
|
|
140
|
+
const { loadSnapdriftConfig } = await import(pathToFileURL(configModulePath));
|
|
141
|
+
const requestsModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/github-requests.mjs');
|
|
142
|
+
const { resolvePullRequestScope } = await import(pathToFileURL(requestsModulePath));
|
|
141
143
|
const { config } = await loadSnapdriftConfig(process.env.REPO_CONFIG_PATH || undefined);
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
reason: 'explicit_route_ids',
|
|
156
|
-
selectedRouteIds: selected.selectedRouteIds
|
|
157
|
-
});
|
|
158
|
-
return;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
if (process.env.FORCE_RUN === 'true') {
|
|
162
|
-
setOutputs({
|
|
163
|
-
shouldRun: true,
|
|
164
|
-
reason: process.env.FORCE_RUN_REASON || 'forced',
|
|
165
|
-
selectedRouteIds: allRouteIds
|
|
166
|
-
});
|
|
167
|
-
return;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
const prNumber = Number(process.env.INPUT_PR_NUMBER || context.payload.pull_request?.number || '');
|
|
171
|
-
if (!Number.isFinite(prNumber) || prNumber <= 0) {
|
|
172
|
-
setOutputs({
|
|
173
|
-
shouldRun: true,
|
|
174
|
-
reason: 'missing_pr_number',
|
|
175
|
-
selectedRouteIds: allRouteIds
|
|
176
|
-
});
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
try {
|
|
181
|
-
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
182
|
-
owner: context.repo.owner,
|
|
183
|
-
repo: context.repo.repo,
|
|
184
|
-
pull_number: prNumber,
|
|
185
|
-
per_page: 100
|
|
186
|
-
});
|
|
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
|
-
]))];
|
|
211
|
-
setOutputs(
|
|
212
|
-
selectRoutesForChangedFiles(
|
|
213
|
-
config,
|
|
214
|
-
changedFiles
|
|
215
|
-
)
|
|
216
|
-
);
|
|
217
|
-
} catch (error) {
|
|
218
|
-
core.warning(`Unable to inspect PR files for SnapDrift scope; running all configured captures instead. ${error.message}`);
|
|
219
|
-
setOutputs({
|
|
220
|
-
shouldRun: true,
|
|
221
|
-
reason: 'snapdrift_scope_check_failed',
|
|
222
|
-
selectedRouteIds: allRouteIds
|
|
223
|
-
});
|
|
224
|
-
}
|
|
144
|
+
const scope = await resolvePullRequestScope({
|
|
145
|
+
github,
|
|
146
|
+
...context.repo,
|
|
147
|
+
config,
|
|
148
|
+
pullNumber: Number(process.env.INPUT_PR_NUMBER || context.payload.pull_request?.number || ''),
|
|
149
|
+
routeIds: process.env.ROUTE_IDS || '',
|
|
150
|
+
forceRun: process.env.FORCE_RUN === 'true',
|
|
151
|
+
forceRunReason: process.env.FORCE_RUN_REASON || 'forced',
|
|
152
|
+
warning: (message) => core.warning(message)
|
|
153
|
+
});
|
|
154
|
+
core.setOutput('should_run', scope.shouldRun ? 'true' : 'false');
|
|
155
|
+
core.setOutput('reason', scope.reason);
|
|
156
|
+
core.setOutput('selected_route_ids', (scope.selectedRouteIds || []).join(','));
|
|
225
157
|
|
|
226
158
|
- id: baseline
|
|
227
159
|
if: steps.scope.outputs.should_run == 'true'
|
|
@@ -739,44 +671,15 @@ runs:
|
|
|
739
671
|
maxErrorRows
|
|
740
672
|
});
|
|
741
673
|
|
|
742
|
-
const
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
674
|
+
const requestsModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/github-requests.mjs');
|
|
675
|
+
const { upsertPullRequestReportComment } = await import(pathToFileURL(requestsModulePath));
|
|
676
|
+
await upsertPullRequestReportComment({
|
|
677
|
+
github,
|
|
678
|
+
...context.repo,
|
|
679
|
+
issueNumber,
|
|
680
|
+
body,
|
|
681
|
+
markers: PR_COMMENT_MARKERS
|
|
747
682
|
});
|
|
748
|
-
const matchingComments = comments
|
|
749
|
-
.filter((comment) => comment.body && PR_COMMENT_MARKERS.some((marker) => comment.body.includes(marker)))
|
|
750
|
-
.sort((left, right) => {
|
|
751
|
-
const leftTime = new Date(left.updated_at || left.created_at).getTime();
|
|
752
|
-
const rightTime = new Date(right.updated_at || right.created_at).getTime();
|
|
753
|
-
return rightTime - leftTime;
|
|
754
|
-
});
|
|
755
|
-
const existing = matchingComments[0];
|
|
756
|
-
|
|
757
|
-
if (existing) {
|
|
758
|
-
await github.rest.issues.updateComment({
|
|
759
|
-
owner: context.repo.owner,
|
|
760
|
-
repo: context.repo.repo,
|
|
761
|
-
comment_id: existing.id,
|
|
762
|
-
body
|
|
763
|
-
});
|
|
764
|
-
} else {
|
|
765
|
-
await github.rest.issues.createComment({
|
|
766
|
-
owner: context.repo.owner,
|
|
767
|
-
repo: context.repo.repo,
|
|
768
|
-
issue_number: issueNumber,
|
|
769
|
-
body
|
|
770
|
-
});
|
|
771
|
-
}
|
|
772
|
-
|
|
773
|
-
for (const duplicate of matchingComments.slice(1)) {
|
|
774
|
-
await github.rest.issues.deleteComment({
|
|
775
|
-
owner: context.repo.owner,
|
|
776
|
-
repo: context.repo.repo,
|
|
777
|
-
comment_id: duplicate.id
|
|
778
|
-
});
|
|
779
|
-
}
|
|
780
683
|
|
|
781
684
|
# A skipped compare writes a summary with no diff counters, so enforcement
|
|
782
685
|
# has nothing to evaluate — `warn-and-skip` means "do not fail my build".
|
package/actions/scope/action.yml
CHANGED
|
@@ -67,77 +67,19 @@ runs:
|
|
|
67
67
|
const { pathToFileURL } = require('url');
|
|
68
68
|
|
|
69
69
|
const configModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/snapdrift-config.mjs');
|
|
70
|
-
const { loadSnapdriftConfig
|
|
70
|
+
const { loadSnapdriftConfig } = await import(pathToFileURL(configModulePath));
|
|
71
|
+
const requestsModulePath = path.resolve(process.env.ACTION_ROOT, 'lib/github-requests.mjs');
|
|
72
|
+
const { resolvePullRequestScope } = await import(pathToFileURL(requestsModulePath));
|
|
71
73
|
const { config } = await loadSnapdriftConfig(process.env.REPO_CONFIG_PATH || undefined);
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
shouldRun: true,
|
|
85
|
-
reason: process.env.FORCE_RUN_REASON || 'forced',
|
|
86
|
-
selectedRouteIds: allRouteIds
|
|
87
|
-
});
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
if (!Number.isFinite(resolvedPrNumber) || resolvedPrNumber <= 0) {
|
|
92
|
-
setOutputs({
|
|
93
|
-
shouldRun: true,
|
|
94
|
-
reason: 'missing_pr_number',
|
|
95
|
-
selectedRouteIds: allRouteIds
|
|
96
|
-
});
|
|
97
|
-
return;
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
try {
|
|
101
|
-
const files = await github.paginate(github.rest.pulls.listFiles, {
|
|
102
|
-
owner: context.repo.owner,
|
|
103
|
-
repo: context.repo.repo,
|
|
104
|
-
pull_number: resolvedPrNumber,
|
|
105
|
-
per_page: 100
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
if (!Array.isArray(files)) {
|
|
109
|
-
throw new Error('Malformed GitHub changed-file response: expected an array of file records.');
|
|
110
|
-
}
|
|
111
|
-
for (const [index, file] of files.entries()) {
|
|
112
|
-
if (!file || typeof file !== 'object' || typeof file.filename !== 'string' || file.filename.length === 0) {
|
|
113
|
-
throw new Error(`Malformed GitHub changed-file response: file record ${index} must include a non-empty string filename.`);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
if (files.length >= 3000) {
|
|
117
|
-
core.warning('GitHub returned the maximum 3000 changed-file records; the list may be truncated, so running all configured captures.');
|
|
118
|
-
setOutputs({
|
|
119
|
-
shouldRun: true,
|
|
120
|
-
reason: 'changed_files_truncated',
|
|
121
|
-
selectedRouteIds: allRouteIds
|
|
122
|
-
});
|
|
123
|
-
return;
|
|
124
|
-
}
|
|
125
|
-
const changedFiles = [...new Set(files.flatMap((file) => [
|
|
126
|
-
file.filename,
|
|
127
|
-
...(file.status === 'renamed' && typeof file.previous_filename === 'string' && file.previous_filename
|
|
128
|
-
? [file.previous_filename]
|
|
129
|
-
: [])
|
|
130
|
-
]))];
|
|
131
|
-
const scope = selectRoutesForChangedFiles(
|
|
132
|
-
config,
|
|
133
|
-
changedFiles
|
|
134
|
-
);
|
|
135
|
-
setOutputs(scope);
|
|
136
|
-
} catch (error) {
|
|
137
|
-
core.warning(`Unable to inspect PR files for SnapDrift scope; running all configured captures instead. ${error.message}`);
|
|
138
|
-
setOutputs({
|
|
139
|
-
shouldRun: true,
|
|
140
|
-
reason: 'snapdrift_scope_check_failed',
|
|
141
|
-
selectedRouteIds: allRouteIds
|
|
142
|
-
});
|
|
143
|
-
}
|
|
74
|
+
const scope = await resolvePullRequestScope({
|
|
75
|
+
github,
|
|
76
|
+
...context.repo,
|
|
77
|
+
config,
|
|
78
|
+
pullNumber: Number(process.env.INPUT_PR_NUMBER || context.payload.pull_request?.number || ''),
|
|
79
|
+
forceRun: process.env.FORCE_RUN === 'true',
|
|
80
|
+
forceRunReason: process.env.FORCE_RUN_REASON || 'forced',
|
|
81
|
+
warning: (message) => core.warning(message)
|
|
82
|
+
});
|
|
83
|
+
core.setOutput('should_run', scope.shouldRun ? 'true' : 'false');
|
|
84
|
+
core.setOutput('reason', scope.reason);
|
|
85
|
+
core.setOutput('selected_route_ids', (scope.selectedRouteIds || []).join(','));
|