styleproof 3.21.0 → 4.0.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.
@@ -0,0 +1,108 @@
1
+ # StyleProof approval gate — copy to .github/workflows/ on your DEFAULT branch.
2
+ #
3
+ # issue_comment workflows only ever run from the default branch, so this must
4
+ # live on main (not on the feature branch) to take effect. It flips the
5
+ # `StyleProof` commit status when a write-access reviewer ticks the single
6
+ # "Approve all changes" box in the StyleProof report comment — one tick signs
7
+ # off every changed or new surface. Pair it with the Action's `require-approval: true` input and
8
+ # either a branch-protection rule requiring the `StyleProof` status, or
9
+ # `styleproof.config.json`'s `"blocking": true` to fail the job on unapproved
10
+ # changes (the blocking option for repos without branch protection).
11
+ name: StyleProof approve
12
+
13
+ on:
14
+ issue_comment:
15
+ types: [edited] # ticking a checkbox edits the comment
16
+
17
+ # statuses:write to flip the gate; pull-requests:read to resolve the PR head SHA;
18
+ # issues:write only for the "needs write access" reply on an unauthorized tick.
19
+ permissions:
20
+ statuses: write
21
+ pull-requests: read
22
+ issues: write
23
+
24
+ jobs:
25
+ approve:
26
+ # A HUMAN editing the BOT's own StyleProof report comment on a PR. This is the
27
+ # trust gate, not the marker: it excludes the bot's own upsert-edits
28
+ # (sender is a Bot) and any attacker-authored comment (comment.user is a User),
29
+ # so the only edits that reach the logic are humans ticking boxes in our comment.
30
+ if: >-
31
+ github.event.issue.pull_request &&
32
+ github.event.comment.user.type == 'Bot' &&
33
+ github.event.sender.type == 'User' &&
34
+ contains(github.event.comment.body, '<!-- styleproof-report -->')
35
+ # ubuntu-latest is fine where you have GitHub-hosted Actions minutes. On a
36
+ # PRIVATE repo without them, this job silently fails to start — point it at
37
+ # the same self-hosted runner your CI uses, e.g. runs-on: [self-hosted, …].
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/github-script@v7
41
+ with:
42
+ script: |
43
+ const STATUS = 'StyleProof'; // must match the Action's status-context input
44
+
45
+ // Re-fetch the comment so concurrent ticks all compute from the LATEST
46
+ // body and converge (the event payload is the body as of one edit). The
47
+ // body is untrusted — only ever pattern-matched here, never evaluated.
48
+ const fresh = await github.rest.issues.getComment({ ...context.repo, comment_id: context.payload.comment.id });
49
+ const body = fresh.data.body || '';
50
+
51
+ // Bind approval to the exact commit the report was generated for. A
52
+ // push after the report leaves this stale, so a new render can never
53
+ // inherit a green status.
54
+ const shaMatch = body.match(/<!-- styleproof-sha:([0-9a-f]{40}) -->/i);
55
+ if (!shaMatch) return;
56
+ const reportSha = shaMatch[1];
57
+
58
+ // One "Approve all changes" box signs off every changed or new surface.
59
+ // No box means nothing to approve, leave the status as the Action set it.
60
+ const allBox = body.split('\n').find((l) => /^\s*-\s+\[[ xX]\]\s+\*\*Approve all changes\*\*/.test(l));
61
+ if (!allBox) return;
62
+ const approved = /\[[xX]\]/.test(allBox);
63
+
64
+ // The human who toggled the box must have write access. (`sender` on an
65
+ // edited event is the editor; combined with the bot-authored + non-bot
66
+ // guards in `if:`, that's the person who ticked.)
67
+ const actor = context.payload.sender.login;
68
+ let perm = 'none';
69
+ try {
70
+ perm = (await github.rest.repos.getCollaboratorPermissionLevel({ ...context.repo, username: actor })).data.permission;
71
+ } catch {
72
+ /* not a collaborator → stays 'none', fails closed */
73
+ }
74
+ if (!['admin', 'maintain', 'write'].includes(perm)) {
75
+ await github.rest.issues.createComment({
76
+ ...context.repo,
77
+ issue_number: context.payload.issue.number,
78
+ body: `@${actor} — approving the StyleProof gate needs write access to this repo, so the check stays red.`,
79
+ });
80
+ return;
81
+ }
82
+
83
+ // Only ever stamp the exact reviewed commit. If the PR moved since the
84
+ // report was posted, do nothing — the Action re-posts (red) for the new SHA.
85
+ const pr = await github.rest.pulls.get({ ...context.repo, pull_number: context.payload.issue.number });
86
+ const headSha = pr.data.head.sha;
87
+ if (headSha !== reportSha) return;
88
+
89
+ // The status description is the source of truth for WHO approved, so a
90
+ // later report re-run (e.g. to clear a blocking check) can reconstruct
91
+ // "approved by @x" without this workflow having run again.
92
+ await github.rest.repos.createCommitStatus({
93
+ ...context.repo,
94
+ sha: headSha,
95
+ context: STATUS,
96
+ state: approved ? 'success' : 'failure',
97
+ description: approved ? `Approved by @${actor}` : 'Tick "Approve all changes" to sign off',
98
+ });
99
+
100
+ // Reflect the approver in the comment itself for instant feedback. Rebuild
101
+ // the box line from scratch (dropping any prior "approved by" suffix) so a
102
+ // re-tick by someone else updates it and an untick clears it. Editing our
103
+ // own comment via GITHUB_TOKEN never retriggers this workflow.
104
+ const boxRe = /^(\s*-\s+\[[ xX]\]\s+\*\*Approve all changes\*\*).*$/m;
105
+ const newBody = body.replace(boxRe, (_, head) => (approved ? `${head} — _approved by @${actor}_` : head));
106
+ if (newBody !== body) {
107
+ await github.rest.issues.updateComment({ ...context.repo, comment_id: context.payload.comment.id, body: newBody });
108
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "styleproof",
3
- "version": "3.21.0",
3
+ "version": "4.0.0",
4
4
  "description": "Catch every CSS change before it ships — review PRs and certify refactors by the browser's computed styles, not pixels. Works with any styling system.",
5
5
  "keywords": [
6
6
  "playwright",
@@ -46,6 +46,7 @@
46
46
  "files": [
47
47
  "dist",
48
48
  "bin",
49
+ "example/styleproof-approve.yml",
49
50
  "docs/demo-composite.png",
50
51
  "README.md",
51
52
  "CHANGELOG.md",
@@ -80,16 +81,18 @@
80
81
  "pngjs": "^7.0.0"
81
82
  },
82
83
  "devDependencies": {
84
+ "@commitlint/cli": "^21.2.0",
85
+ "@commitlint/config-conventional": "^21.2.0",
83
86
  "@eslint/js": "^10.0.1",
84
87
  "@playwright/test": "^1.60.0",
85
88
  "@types/node": "^26.0.0",
86
89
  "@types/pngjs": "^6.0.5",
87
90
  "eslint": "^10.5.0",
91
+ "fallow": "^3.2.0",
88
92
  "globals": "^17.6.0",
93
+ "husky": "^9.1.7",
89
94
  "prettier": "^3.4.2",
90
95
  "typescript": "^6.0.3",
91
- "typescript-eslint": "^8.18.0",
92
- "husky": "^9.1.7",
93
- "fallow": "^3.2.0"
96
+ "typescript-eslint": "^8.18.0"
94
97
  }
95
98
  }