-
Notifications
You must be signed in to change notification settings - Fork 138
352 lines (301 loc) · 13.6 KB
/
screenshots.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
# This workflow notifies pull requests when a screen was changed. It first takes screenshots
# of all screens, and then compares this with screenshots on the base branch.
name: Screenshots
# Cancel currently running workflows for the same branch
concurrency:
group: ${{ github.ref }}
cancel-in-progress: true
on:
pull_request:
jobs:
# Job that starts the showcase and takes a screenshot of every page and
# saves it in artifacts (both head & base branches)
screenshots:
runs-on: ubuntu-latest
strategy:
matrix:
device: [ 'desktop', 'mobile' ]
ref: [ 'head', 'base' ]
# Make sure that one failing test does not cancel all other matrix jobs
fail-fast: false
steps:
- uses: actions/checkout@v4
if: ${{ matrix.ref == 'head' }}
- uses: actions/checkout@v4
if: ${{ matrix.ref == 'base' }}
with:
ref: ${{ github.event.pull_request.base.sha }}
- name: Setup node
uses: ./.github/actions/setup-node
- name: Remove old screenshots
run: rm -v -f screenshots/${{ matrix.device }}/*.png
- run: npm ci
- name: Save screenshots
run: |
npm run showcase&
showcase_pid=$!
# regularly check if localhost:5174 is reachable (i.e. devserver is up)
while ! nc -z localhost 5174; do
echo "dev server not ready; waiting"
sleep 5
done
SCREENSHOTS_DIR="./screenshots/${{ matrix.device }}" \
SCREENSHOTS_TYPE=${{ matrix.device }} \
npm run screenshots
kill "$showcase_pid"
- name: Summary
run: |
echo "The following screenshots were created:"
shasum -a 256 screenshots/${{ matrix.device }}/*.png | sort -k2 # sort by 2nd column (filename)
- name: Upload screenshots
uses: actions/upload-artifact@v4
with:
name: e2e-screenshots-${{ matrix.ref }}-${{ matrix.device }}
path: screenshots/${{ matrix.device }}/*.png
notify:
runs-on: ubuntu-latest
needs: screenshots
concurrency: image-dumpster
# This should never take long, but _in case_ our setup changes inadvertently
# and this starts taking longer than expected, then we want to be aware of it.
timeout-minutes: 5
steps:
# Configuration used throughout the job
- name: Initialize config
run: |
now=$(date +%s)
# The current & expiration timestamps, used to evict old images
echo "timestamp=$now" >> "$GITHUB_OUTPUT"
echo "expire_before=$(( now - (7 * 24 * 60 * 60) ))" >> "$GITHUB_OUTPUT"
# Temporary directory where to unpack artifacts
echo "tmpdir=$(mktemp -d)" >> "$GITHUB_OUTPUT"
# A single-word representation of the branch name (branch names may contain
# slashes which makes everything more complicated when using them as dir names)
branch_alias=$(echo ${{ github.event.pull_request.head.ref }} | md5sum | cut -c 1-9)
echo "branch=$branch_alias" >> "$GITHUB_OUTPUT"
# The path for this run's objects (will overwrite previous objects)
echo "dumpster_path=objs/$branch_alias" >> "$GITHUB_OUTPUT"
id: sys
- uses: actions/checkout@v4
# Checks out 'image-dumpster', a single-commit branch used to keep objects (screenshots).
# We store the images in a branch (as opposed to e.g. artifacts) so that GitHub will serve
# them (on rawgithubusercontent), meaning we can display them on PRs.
- name: Set up image-dumpster branch
run: |
git fetch origin image-dumpster:image-dumpster
git checkout image-dumpster
# A dummy user; not very relevant since the branch should only ever have one
# commit that we keep overwriting
git config user.name "Image Dumpster"
git config user.email "<>"
# Always reset to the first commit; we don't want any history
initial_commit=$(git log --format=format:%H --reverse | head -1 )
git reset "$initial_commit"
# This runs some garbage collection on the stored images. The branch keeps two top-level
# directories:
# * objs/: the actual objects (images) we want to store
# * timestamps/: symlinks to the objects
# When an object is created in objs/<foo>, a timestamp timestamps/1234 is created that
# points to this new object. Check GCing, we remove expired timestamps. This means we can
# then safely remove all objects not pointed to by symlinks -- i.e. expired objects.
#
# We do also use objs/<branch-ish> to index directly based on the branch, so that we can
# evict old versions of a branch (to avoid creating lots of objects on PRs with many
# commits)
- name: Remove outdated directories
run: |
mkdir -p ./timestamps
mkdir -p ./objs
# Remove expired timestamps
while read -r timestamp
do
echo "found symlink $timestamp"
timestamp_num=$(basename $timestamp)
if (( timestamp_num > ${{ steps.sys.outputs.expire_before }} ))
then
echo "$timestamp_num > ${{ steps.sys.outputs.expire_before }}, keeping $timestamp"
else
echo "$timestamp_num < ${{ steps.sys.outputs.expire_before }}, removing $timestamp"
rm "$timestamp"
fi
done < <(find ./timestamps \
-maxdepth 1 -mindepth 1 \
-type l \
)
# Remove objects without timestamps
while read -r obj
do
links=$(find -L ./timestamps -samefile "$obj")
if [ -n "$links" ]
then
echo "object $obj has timestamp, keeping: $links"
else
echo "object $obj does not have any timestamps, removing"
rm -r "$obj"
fi
done < <(find ./objs \
-maxdepth 1 -mindepth 1 \
-type d)
dumpster_path="${{ steps.sys.outputs.dumpster_path }}"
# If we already have a (live) path for this branch, clean it
if [ -d "$dumpster_path" ]
then
echo "path $dumpster_path for branch already exists, wiping"
link=$(find -L ./timestamps -samefile "$dumpster_path")
if [ -f "$link" ]
then
echo "removing associated timestamp $link"
rm "$link"
fi
rm -r "$dumpster_path"
fi
mkdir -p "$dumpster_path"
ln -s "../$dumpster_path" ./timestamps/${{ steps.sys.outputs.timestamp }}
# Clean up all timestamps that don't have a target anymore
find ./timestamps -xtype l -delete
# Download the screenshots artifacts (head & base, mobile & desktop)
- uses: actions/download-artifact@v4
with:
name: e2e-screenshots-head-desktop
path: ${{ steps.sys.outputs.tmpdir }}/head/desktop/
- uses: actions/download-artifact@v4
with:
name: e2e-screenshots-head-mobile
path: ${{ steps.sys.outputs.tmpdir }}/head/mobile/
- uses: actions/download-artifact@v4
with:
name: e2e-screenshots-base-desktop
path: ${{ steps.sys.outputs.tmpdir }}/base/desktop/
- uses: actions/download-artifact@v4
with:
name: e2e-screenshots-base-mobile
path: ${{ steps.sys.outputs.tmpdir }}/base/mobile/
# Compare screenshots on head & base and report differences (as step outputs)
- name: Diff images
id: diff
run: |
base_dir="${{ steps.sys.outputs.tmpdir }}/base"
head_dir="${{ steps.sys.outputs.tmpdir }}/head"
dumpster_path="${{ steps.sys.outputs.dumpster_path }}"
echo "using dumpster path $dumpster_path"
# Couple of temp files where we store the list of added/changed/removed files,
# so that we can them dump them as github outputs
output_removed=$(mktemp)
output_changed=$(mktemp)
output_added=$(mktemp)
# We iterate through _all_ files, i.e. also the removed & added ones, for
# clarity. Then for each (deduped) filename we can check whether it's present in
# the base, head or both (and whether they are different).
while read -r some_file
do
file_head="$head_dir/$some_file"
file_base="$base_dir/$some_file"
full_outpath="$dumpster_path/$some_file"
mkdir -p "$(dirname $full_outpath)"
if ! [ -f "$file_base" ]
then
# the file exists in head but not base, so it's a new file
echo "head introduced $some_file"
echo "$some_file" >> "$output_added"
cp "$file_head" "$full_outpath"
elif ! [ -f "$file_head" ]
then
# the file exists in base but not head, so it's been removed
echo "head removed $some_file"
echo "$some_file" >> "$output_removed"
cp "$file_base" "$full_outpath"
else
# the file exists in head & base, so we check whether it's changed
magick_out=$(mktemp)
magick_diff=$(mktemp)
metric=0
# The 'AE' metric counts the number of pixels that differ between the two images
# and we store the visual diff (to be uploaded later if necessary)
# NOTE: imagemagick prints the value to stderr
if ! compare -metric AE "$file_head" "$file_base" "$magick_diff" 2> "$magick_out"
then
metric=$(<"$magick_out")
printf -v metric "%.f" "$metric"
fi
rm "$magick_out"
# Ensure that we got a meaningful output
if ! [[ $metric =~ ^[0-9]+$ ]]
then
echo "Magick didn't return a number: $metric"
exit 1
fi
if (( metric > 100 ))
then
echo "Big pixel difference for $some_file"
echo "$some_file" >> "$output_changed"
cp "$magick_diff" "$full_outpath"
fi
rm "$magick_diff"
fi
done < <( {
find "$head_dir" -type f | sed "s|$head_dir/||";
find "$base_dir" -type f | sed "s|$base_dir/||";
} | sort | uniq )
# Dump the file lists to github outputs
echo 'FILES_ADDED<<EOF' >> "$GITHUB_OUTPUT"
cat "$output_added" >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
echo 'FILES_CHANGED<<EOF' >> "$GITHUB_OUTPUT"
cat "$output_changed" >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
echo 'FILES_REMOVED<<EOF' >> "$GITHUB_OUTPUT"
cat "$output_removed" >> "$GITHUB_OUTPUT"
echo 'EOF' >> "$GITHUB_OUTPUT"
# Force push the updates to the image-dumpster branch as a single commit
- name: Commit to image-dumpster
run: |
git add .
git commit --amen -m "Update from ${{ steps.sys.outputs.timestamp }}"
git config --add --bool push.autoSetupRemote true
git push --force
# Use the GitHub API to post links to screenshots if necessary
- name: Notify PR of screen changes
uses: actions/github-script@v7
env:
FILES_ADDED: ${{ steps.diff.outputs.FILES_ADDED }}
FILES_CHANGED: ${{ steps.diff.outputs.FILES_CHANGED }}
FILES_REMOVED: ${{ steps.diff.outputs.FILES_REMOVED }}
with:
script: |
// Anchors we use to figure out where we've written to in the PR description before,
// if we have
const REPORT_START = "<!-- SCREENSHOTS REPORT START -->";
const REPORT_STOP = "<!-- SCREENSHOTS REPORT STOP -->";
const pr = await github.rest.issues.get({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
})
let [ before, rest ] = (pr.data.body ?? "").split(REPORT_START);
rest = rest ?? "";
let [ _outdated, after ] = rest.split(REPORT_STOP);
after = after ?? "";
// Create the new report as details/summary tags, inside of which we show the
// screenshots
let report = "";
const addSection = (summ, files) => {
if(files.length <= 0) { return ; }
if(report === "") { report += "<hr/>" } // add separation before screenshots
report += "<details>";
report += `<summary>${summ}</summary>`;
files.forEach(file => {
const imgUrl = "https://raw.githubusercontent.com/dfinity/internet-identity/image-dumpster/${{ steps.sys.outputs.dumpster_path }}/" + file;
report += `<img src="${imgUrl}" width="250">`;
});
report += "</details>";
}
const getFiles = (varname) => process.env[varname].split("\n").filter(Boolean);
addSection("🟢 Some screens were added", getFiles("FILES_ADDED"));
addSection("🟡 Some screens were changed", getFiles("FILES_CHANGED"));
addSection("🔴 Some screens were removed", getFiles("FILES_REMOVED"));
github.rest.issues.update({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: [before, REPORT_START, report, REPORT_STOP, after ].join("\n"),
});