@graphty/visual-review 0.0.1 → 0.1.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/package.json CHANGED
@@ -1,12 +1,71 @@
1
1
  {
2
- "name": "@graphty/visual-review",
3
- "version": "0.0.1",
4
- "description": "Placeholder: reserves the name so the package can be published through npm trusted publishing. The first real release follows from github.com/graphty-org/graphty-monorepo.",
5
- "license": "MIT",
6
- "repository": {
7
- "type": "git",
8
- "url": "git+https://github.com/graphty-org/graphty-monorepo.git",
9
- "directory": "visual-review"
10
- },
11
- "files": ["README.md"]
12
- }
2
+ "name": "@graphty/visual-review",
3
+ "version": "0.1.0",
4
+ "description": "Self-hosted visual review of Storybook stories: capture in GitHub Actions, keep the baselines in git (Git LFS), accept or reject each change in a local page, and gate pull requests",
5
+ "author": "Adam Powers <apowers@ato.ms>",
6
+ "type": "module",
7
+ "bin": {
8
+ "visual-review": "./trusted/cli.mjs"
9
+ },
10
+ "exports": {
11
+ "./package.json": "./package.json"
12
+ },
13
+ "files": [
14
+ "trusted/",
15
+ "capture/",
16
+ "templates/",
17
+ "README.md",
18
+ "LICENSE"
19
+ ],
20
+ "publishConfig": {
21
+ "access": "public",
22
+ "provenance": true
23
+ },
24
+ "engines": {
25
+ "node": ">=20.0.0"
26
+ },
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/graphty-org/graphty-monorepo.git",
30
+ "directory": "visual-review"
31
+ },
32
+ "keywords": [
33
+ "storybook",
34
+ "visual-regression",
35
+ "visual-testing",
36
+ "screenshot",
37
+ "snapshot",
38
+ "playwright",
39
+ "github-actions",
40
+ "git-lfs",
41
+ "chromatic-alternative",
42
+ "review"
43
+ ],
44
+ "license": "MIT",
45
+ "bugs": {
46
+ "url": "https://github.com/graphty-org/graphty-monorepo/issues"
47
+ },
48
+ "homepage": "https://graphty.app/docs/visual-review/",
49
+ "dependencies": {
50
+ "pngjs": "7.0.0"
51
+ },
52
+ "peerDependencies": {
53
+ "playwright": ">=1.49.0"
54
+ },
55
+ "devDependencies": {
56
+ "@types/node": "^20.19.9",
57
+ "@vitest/coverage-v8": "4.1.11",
58
+ "eslint": "^9.29.0",
59
+ "playwright": "1.57.0",
60
+ "typescript": "^5.9.3",
61
+ "vitest": "4.1.11"
62
+ },
63
+ "scripts": {
64
+ "test": "vitest",
65
+ "test:run": "vitest run",
66
+ "coverage": "vitest run --coverage",
67
+ "lint": "eslint . && npm run typecheck",
68
+ "lint:knip": "cd .. && ./tools/run-knip.sh --workspace visual-review",
69
+ "typecheck": "tsc -p tsconfig.json"
70
+ }
71
+ }
@@ -0,0 +1,141 @@
1
+ # Generated by visual-review init (@graphty/visual-review). `visual-review init --force` rewrites
2
+ # this file; change visual-review.config.json rather than this file where you can.
3
+ #
4
+ # Visual review. On every pull request and every push to __BRANCH__, each Storybook named in
5
+ # visual-review.config.json is built, every story is screenshot and compared with its baseline PNG
6
+ # in git, and the results are uploaded as the artifact "visual-<project>-<attempt>" for
7
+ # `visual-review serve`, which finds this workflow by its file name (the config's "workflow") and
8
+ # the capture jobs by their names, "visual (<project>)". A difference never fails a capture job;
9
+ # on a pull request the "Visual gate" job fails while any difference is not accepted, so make
10
+ # "Visual gate" a required status check (Settings, Branches or Rulesets).
11
+
12
+ name: Visual review
13
+
14
+ on:
15
+ pull_request:
16
+ push:
17
+ branches: [__BRANCH__]
18
+
19
+ permissions:
20
+ contents: read
21
+
22
+ jobs:
23
+ # The projects of visual-review.config.json, as the capture job's matrix.
24
+ plan:
25
+ name: Plan
26
+ runs-on: ubuntu-24.04
27
+ outputs:
28
+ projects: ${{ steps.plan.outputs.projects }}
29
+ steps:
30
+ - uses: actions/checkout@v4
31
+ with:
32
+ sparse-checkout: visual-review.config.json
33
+ sparse-checkout-cone-mode: false
34
+
35
+ - name: List the projects
36
+ id: plan
37
+ run: |
38
+ echo "projects=$(jq -c '(.baselines // "visual-baselines") as $b
39
+ | [.projects | to_entries[] | {project: .key, storybook: .value.storybook,
40
+ build: .value.build, baselines: $b}]' visual-review.config.json)" >> "$GITHUB_OUTPUT"
41
+
42
+ # continue-on-error: a crash of the tool itself must not fail the run; the gate below judges.
43
+ visual:
44
+ name: visual (${{ matrix.project }})
45
+ needs: plan
46
+ runs-on: ubuntu-24.04
47
+ permissions:
48
+ contents: read
49
+ # To download the default branch's newest capture (the reference step below).
50
+ actions: read
51
+ continue-on-error: true
52
+ timeout-minutes: 45
53
+ strategy:
54
+ fail-fast: false
55
+ matrix:
56
+ include: ${{ fromJSON(needs.plan.outputs.projects) }}
57
+ steps:
58
+ - uses: actions/checkout@v4
59
+
60
+ # Baseline PNGs are Git LFS objects. They are cached by the pointer files' hash, so a run
61
+ # downloads from LFS only the images accepted since the cache was saved.
62
+ - name: Restore this project's baseline images
63
+ uses: actions/cache@v4
64
+ with:
65
+ path: .git/lfs/objects
66
+ key: lfs-${{ matrix.project }}-${{ hashFiles(format('{0}/{1}/**/*.png', matrix.baselines, matrix.project)) }}
67
+ restore-keys: lfs-${{ matrix.project }}-
68
+
69
+ - name: Fetch this project's baseline images from Git LFS
70
+ run: git lfs pull --include "${{ matrix.baselines }}/${{ matrix.project }}/**"
71
+
72
+ # __SETUP__
73
+
74
+ - name: Install Chromium
75
+ run: __CLI__ install-browser
76
+
77
+ - name: Build Storybook
78
+ if: matrix.build
79
+ run: ${{ matrix.build }}
80
+
81
+ # A story with no baseline that looks as it does in the default branch's newest capture is
82
+ # "unseeded" and does not block this pull request; without a reference every one is "new".
83
+ - name: Download the default branch's newest capture
84
+ id: reference
85
+ if: github.event_name == 'pull_request'
86
+ continue-on-error: true
87
+ env:
88
+ GH_TOKEN: ${{ github.token }}
89
+ run: echo "dir=$(__CLI__ reference --project ${{ matrix.project }} --out "$RUNNER_TEMP/reference")" >> "$GITHUB_OUTPUT"
90
+
91
+ - name: Capture
92
+ run: __CLI__ capture --project ${{ matrix.project }} --out "$RUNNER_TEMP/visual" --reference "${{ steps.reference.outputs.dir }}"
93
+
94
+ # Kept even when capture crashed, so the reviewer sees the partial results.json.
95
+ - name: Upload captures
96
+ if: always()
97
+ uses: actions/upload-artifact@v4
98
+ with:
99
+ name: visual-${{ matrix.project }}-${{ github.run_attempt }}
100
+ path: ${{ runner.temp }}/visual/
101
+ retention-days: 30
102
+
103
+ - name: Write the counts to the job summary
104
+ if: always()
105
+ run: |
106
+ f="$RUNNER_TEMP/visual/results.json"
107
+ {
108
+ echo "### Visual review (${{ matrix.project }})"
109
+ if [[ -f "$f" ]]; then
110
+ jq -r '"seeded: \(.seeded), complete: \(.complete), \(.items | length) of \(.expected) items, reference run: \(.reference)",
111
+ "", "| status | items |", "|---|---|",
112
+ (.items | group_by(.status)[] | "| \(.[0].status) | \(length) |")' "$f"
113
+ else
114
+ echo "No results.json: capture did not start."
115
+ fi
116
+ } >> "$GITHUB_STEP_SUMMARY"
117
+
118
+ # Fails while a project with baselines on the base branch holds anything but unchanged, excluded
119
+ # or unseeded items in its newest capture, or a baseline file changed without a review record.
120
+ # It runs the published gate at a pinned version, so a pull request's own dependencies cannot
121
+ # change it. Depth 2: HEAD^1, the merge commit's first parent, is the base branch tip.
122
+ visual-gate:
123
+ name: Visual gate
124
+ needs: visual
125
+ if: always() && github.event_name == 'pull_request'
126
+ runs-on: ubuntu-24.04
127
+ steps:
128
+ - uses: actions/checkout@v4
129
+ with:
130
+ fetch-depth: 2
131
+ filter: blob:none
132
+
133
+ - name: Download visual captures
134
+ continue-on-error: true
135
+ uses: actions/download-artifact@v4
136
+ with:
137
+ pattern: visual-*
138
+ path: ${{ runner.temp }}/visual
139
+
140
+ - name: Check visual changes were accepted
141
+ run: npx --yes @graphty/visual-review@__VERSION__ gate --captures "$RUNNER_TEMP/visual" --base HEAD^1 --head HEAD
@@ -0,0 +1,144 @@
1
+ # Generated by visual-review init (@graphty/visual-review). `visual-review init --force` rewrites
2
+ # this file; change visual-review.config.json rather than this file where you can.
3
+ #
4
+ # Captures the Storybooks of an older, known-good commit with the default branch's capture tool, so
5
+ # baselines can be seeded from it: `visual-review serve --master-run <this run's id>` lists the run
6
+ # as the default branch, and Finish opens the seed pull request. Dispatch it from __BRANCH__:
7
+ # gh workflow run visual-seed.yml --ref __BRANCH__ -f ref=<sha>
8
+ # Only the projects whose "seedFromDefaultBranch" is not false are captured. The capture job
9
+ # repeats the "visual" job of the review workflow, except that it has no reference (every story
10
+ # without a baseline is "new") and results.json names the captured commit.
11
+
12
+ name: Visual seed capture
13
+
14
+ on:
15
+ workflow_dispatch:
16
+ inputs:
17
+ ref:
18
+ description: "The commit (or branch or tag) whose Storybooks are captured"
19
+ required: true
20
+ type: string
21
+
22
+ permissions:
23
+ contents: read
24
+
25
+ jobs:
26
+ plan:
27
+ name: Plan
28
+ runs-on: ubuntu-24.04
29
+ outputs:
30
+ projects: ${{ steps.plan.outputs.projects }}
31
+ steps:
32
+ - uses: actions/checkout@v4
33
+ with:
34
+ sparse-checkout: visual-review.config.json
35
+ sparse-checkout-cone-mode: false
36
+
37
+ - name: List the projects
38
+ id: plan
39
+ run: |
40
+ echo "projects=$(jq -c '(.baselines // "visual-baselines") as $b
41
+ | [.projects | to_entries[] | select(.value.seedFromDefaultBranch != false)
42
+ | {project: .key, storybook: .value.storybook, build: .value.build, baselines: $b}]' \
43
+ visual-review.config.json)" >> "$GITHUB_OUTPUT"
44
+
45
+ storybook:
46
+ name: Build ${{ matrix.project }} at ${{ inputs.ref }}
47
+ needs: plan
48
+ runs-on: ubuntu-24.04
49
+ outputs:
50
+ sha: ${{ steps.sha.outputs.sha }}
51
+ strategy:
52
+ fail-fast: false
53
+ matrix:
54
+ include: ${{ fromJSON(needs.plan.outputs.projects) }}
55
+ steps:
56
+ - uses: actions/checkout@v4
57
+ with:
58
+ ref: ${{ inputs.ref }}
59
+
60
+ - name: Resolve the commit
61
+ id: sha
62
+ run: echo "sha=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT"
63
+
64
+ # __SETUP__
65
+
66
+ - name: Build Storybook
67
+ run: ${{ matrix.build }}
68
+
69
+ - uses: actions/upload-artifact@v4
70
+ with:
71
+ name: storybook-${{ matrix.project }}
72
+ path: ${{ matrix.storybook }}/
73
+
74
+ visual:
75
+ name: visual (${{ matrix.project }})
76
+ needs: [plan, storybook]
77
+ runs-on: ubuntu-24.04
78
+ timeout-minutes: 45
79
+ strategy:
80
+ fail-fast: false
81
+ matrix:
82
+ include: ${{ fromJSON(needs.plan.outputs.projects) }}
83
+ steps:
84
+ # The default branch's checkout: the capture tool, the config and the baselines come from here.
85
+ - uses: actions/checkout@v4
86
+
87
+ - name: Restore this project's baseline images
88
+ uses: actions/cache@v4
89
+ with:
90
+ path: .git/lfs/objects
91
+ key: lfs-${{ matrix.project }}-${{ hashFiles(format('{0}/{1}/**/*.png', matrix.baselines, matrix.project)) }}
92
+ restore-keys: lfs-${{ matrix.project }}-
93
+
94
+ - name: Fetch this project's baseline images from Git LFS
95
+ run: git lfs pull --include "${{ matrix.baselines }}/${{ matrix.project }}/**"
96
+
97
+ # __SETUP__
98
+
99
+ - name: Install Chromium
100
+ run: __CLI__ install-browser
101
+
102
+ - name: Download Storybook build
103
+ uses: actions/download-artifact@v4
104
+ with:
105
+ name: storybook-${{ matrix.project }}
106
+ path: ${{ matrix.storybook }}/
107
+
108
+ - name: Capture
109
+ run: __CLI__ capture --project ${{ matrix.project }} --out "$RUNNER_TEMP/visual"
110
+
111
+ # capture takes the commit from GITHUB_SHA, the default branch's here; a seed Finish branches
112
+ # from results.json's commit, so it must name the commit whose Storybook was captured.
113
+ - name: Record the captured commit
114
+ if: always()
115
+ env:
116
+ SHA: ${{ needs.storybook.outputs.sha }}
117
+ run: |
118
+ f="$RUNNER_TEMP/visual/results.json"
119
+ if [[ -f "$f" ]]; then
120
+ jq --arg sha "$SHA" '.commit = $sha' "$f" > "$f.tmp" && mv "$f.tmp" "$f"
121
+ fi
122
+
123
+ - name: Upload captures
124
+ if: always()
125
+ uses: actions/upload-artifact@v4
126
+ with:
127
+ name: visual-${{ matrix.project }}-${{ github.run_attempt }}
128
+ path: ${{ runner.temp }}/visual/
129
+ retention-days: 30
130
+
131
+ - name: Write the counts to the job summary
132
+ if: always()
133
+ run: |
134
+ f="$RUNNER_TEMP/visual/results.json"
135
+ {
136
+ echo "### Visual seed capture (${{ matrix.project }}) at ${{ needs.storybook.outputs.sha }}"
137
+ if [[ -f "$f" ]]; then
138
+ jq -r '"complete: \(.complete), \(.items | length) of \(.expected) items",
139
+ "", "| status | items |", "|---|---|",
140
+ (.items | group_by(.status)[] | "| \(.[0].status) | \(length) |")' "$f"
141
+ else
142
+ echo "No results.json: capture did not start."
143
+ fi
144
+ } >> "$GITHUB_STEP_SUMMARY"