browser_review_gate 0.1.1 → 0.1.2
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +7 -0
- data/README.md +1 -1
- data/lib/browser_review_gate/cli.rb +6 -6
- data/lib/browser_review_gate/hook.rb +10 -60
- data/lib/browser_review_gate/installer.rb +21 -15
- data/lib/browser_review_gate/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1d796f7cca8b1df8cb9baf1dde44f2ce6c7417a97df23904d83f57ff1d2b6585
|
|
4
|
+
data.tar.gz: 2dca99945ced333c56b66906d0e5603e6c1f6c71b65557a8acfbd5c364bafcbc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 16f2cac1e96bfaa0f6b2a7e2033e1375b358f3323bc9095282eb59f28718f94ec5a225b1ace02f8e3379266887972c16b9ec333c93046ba40967929181c0e996
|
|
7
|
+
data.tar.gz: 3731b22121fd1cd20d16f0af4f44f375247966e56a5faab65301def109784823de3b8ff01d047174ef19967ef559d4fcdbbc4d5457bbf28af29d64d19c8de657
|
data/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.1.2
|
|
4
|
+
|
|
5
|
+
- Nothing holds a command back any more. The before-command hooks of 0.1.1, which stopped
|
|
6
|
+
`gh pr create` and review requests while a run was owed, are removed: running the verification is
|
|
7
|
+
the author's choice, and only CI decides whether a review request stays. The installer deletes the
|
|
8
|
+
old hook entries; until then they do nothing.
|
|
9
|
+
|
|
3
10
|
## 0.1.1
|
|
4
11
|
|
|
5
12
|
- Reviewers whose requests were taken back are asked again automatically once the run is published.
|
data/README.md
CHANGED
|
@@ -84,7 +84,7 @@ Local commands need the `gh` CLI, signed in.
|
|
|
84
84
|
- **Reviewers come back on their own.** A request taken back is remembered; once the run is published, the same people are asked again.
|
|
85
85
|
- **A waiver for what cannot be run.** Someone other than the author adds the `browser-verification-waived` label and review proceeds without a run.
|
|
86
86
|
- **A status next to the checks.** `browser-verification` is pending while a run is owed and green otherwise. Make it a required check if you want it to block merging.
|
|
87
|
-
- **
|
|
87
|
+
- **Nothing is forced.** Opening a PR never requires a run. The PR comment says when one is needed and how to do it; it only becomes necessary when the author wants a reviewer.
|
|
88
88
|
- **The agent does not start your app.** It uses the one that is running and asks you to start it otherwise.
|
|
89
89
|
|
|
90
90
|
## What it does not do
|
|
@@ -16,7 +16,6 @@ module BrowserReviewGate
|
|
|
16
16
|
save --report PATH keep results until the PR for this commit is opened
|
|
17
17
|
request-assessment NUMBER ask CI to assess a PR that has no decision yet
|
|
18
18
|
hook AGENT after a command: publish the saved run after `gh pr create`
|
|
19
|
-
hook AGENT --before before a command: say when a browser run is still owed
|
|
20
19
|
|
|
21
20
|
For CI
|
|
22
21
|
ci NUMBER assess the PR and take back premature reviewer requests
|
|
@@ -118,13 +117,14 @@ module BrowserReviewGate
|
|
|
118
117
|
0
|
|
119
118
|
end
|
|
120
119
|
|
|
120
|
+
# Version 0.1.1 installed a `--before` hook that held commands back. It is gone; a project that
|
|
121
|
+
# still has the entry gets a silent no-op until the installer removes it.
|
|
121
122
|
def hook
|
|
122
|
-
|
|
123
|
+
return 0 if @argv.delete("--before")
|
|
124
|
+
|
|
123
125
|
agent = @argv.shift
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
message = before ? hook.before(@stdin.read) : hook.after(@stdin.read)
|
|
127
|
-
output = Hook.render(message, agent, before: before)
|
|
126
|
+
message = Hook.new(publisher_factory: -> { Publisher.new(github: github, config: config) }).after(@stdin.read)
|
|
127
|
+
output = Hook.render(message, agent)
|
|
128
128
|
@stdout.puts output if output
|
|
129
129
|
0
|
|
130
130
|
end
|
|
@@ -1,27 +1,23 @@
|
|
|
1
1
|
require "json"
|
|
2
2
|
|
|
3
3
|
module BrowserReviewGate
|
|
4
|
-
# Runs
|
|
5
|
-
#
|
|
6
|
-
#
|
|
7
|
-
# Before a command that opens a PR or asks for review, the agent is told when a browser run is still
|
|
8
|
-
# owed, so the request is not made only to be taken back. It never fails the agent's session.
|
|
4
|
+
# Runs after an agent's shell command. When the command opened a PR, the browser run saved for this
|
|
5
|
+
# commit is published. It never holds a command back and never fails the agent: running the
|
|
6
|
+
# verification is the author's choice, and the gate on review requests lives in CI.
|
|
9
7
|
class Hook
|
|
10
8
|
OPEN_PR = "gh pr create"
|
|
11
|
-
REVIEW_REQUEST = /gh pr (create|edit)\b.*(--reviewer|--add-reviewer|-r )|requested_reviewers/
|
|
12
|
-
SKIP = "BROWSER_REVIEW_GATE_SKIP=1"
|
|
13
9
|
|
|
14
|
-
def initialize(publisher_factory:,
|
|
10
|
+
def initialize(publisher_factory:, shell: Shell.new)
|
|
15
11
|
@publisher_factory = publisher_factory
|
|
16
|
-
@status_factory = status_factory
|
|
17
|
-
@config = config
|
|
18
12
|
@shell = shell
|
|
19
13
|
end
|
|
20
14
|
|
|
21
15
|
# `payload` is the raw hook input. Returns a message for the agent, or nil when nothing was done.
|
|
22
16
|
def after(payload)
|
|
23
17
|
return unless payload.to_s.include?(OPEN_PR)
|
|
24
|
-
|
|
18
|
+
|
|
19
|
+
sha = @shell.call("git", "rev-parse", "HEAD").strip
|
|
20
|
+
return unless SavedReport.new(sha: sha, shell: @shell).exist?
|
|
25
21
|
|
|
26
22
|
verified = @publisher_factory.call.publish
|
|
27
23
|
verified ? "Saved browser verification published; human review can be requested." :
|
|
@@ -30,58 +26,12 @@ module BrowserReviewGate
|
|
|
30
26
|
"Saved browser verification was not published: #{error.message}"
|
|
31
27
|
end
|
|
32
28
|
|
|
33
|
-
#
|
|
34
|
-
def
|
|
35
|
-
text = payload.to_s
|
|
36
|
-
return if text.include?(SKIP)
|
|
37
|
-
|
|
38
|
-
reason = review_request_reason(text) if text.match?(REVIEW_REQUEST)
|
|
39
|
-
reason ||= open_pr_reason if text.include?(OPEN_PR)
|
|
40
|
-
reason && "#{reason} If the person asked to go ahead anyway, run the same command prefixed with #{SKIP}."
|
|
41
|
-
rescue Error
|
|
42
|
-
nil
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
# Claude Code and Codex read a JSON object; Cursor reads its own shape.
|
|
46
|
-
def self.render(message, agent, before: false)
|
|
29
|
+
# Claude Code and Codex read a JSON object; other agents show plain text.
|
|
30
|
+
def self.render(message, agent)
|
|
47
31
|
return if message.nil?
|
|
48
|
-
return JSON.generate(permission: "deny", agentMessage: message, userMessage: message) if before && agent == "cursor"
|
|
49
32
|
return message unless %w[claude codex].include?(agent)
|
|
50
|
-
return JSON.generate(hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: message }) unless before
|
|
51
|
-
|
|
52
|
-
JSON.generate(hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: message })
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
private
|
|
56
|
-
|
|
57
|
-
def saved_run?
|
|
58
|
-
SavedReport.new(sha: @shell.call("git", "rev-parse", "HEAD").strip, shell: @shell).exist?
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
# No PR exists yet, so CI has not decided; only the changed paths can tell.
|
|
62
|
-
def open_pr_reason
|
|
63
|
-
return if saved_run? || changed_files.all? { |path| @config.ignored?(path) }
|
|
64
|
-
|
|
65
|
-
"No browser run is saved for this commit, and the change may alter browser behavior. " \
|
|
66
|
-
"Run #{@config.command} first: the run is published when the PR is opened and review requests stay."
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def review_request_reason(text)
|
|
70
|
-
status = @status_factory.call.to_h(text[/gh pr edit\s+(\d+)/, 1]&.to_i)
|
|
71
|
-
case status["next"]
|
|
72
|
-
when "publish_saved" then "A browser run is saved but not published. Run `browser-review-gate publish` first, or the review request will be taken back."
|
|
73
|
-
when "run_and_publish", "ask_to_start_app" then "This PR needs a browser run (#{@config.command} #{status["pull_request"]}); a review request made now will be taken back."
|
|
74
|
-
end
|
|
75
|
-
end
|
|
76
33
|
|
|
77
|
-
|
|
78
|
-
def changed_files
|
|
79
|
-
%w[origin/HEAD origin/main origin/master].each do |base|
|
|
80
|
-
return @shell.call("git", "diff", "--name-only", "#{base}...HEAD").lines.map(&:chomp)
|
|
81
|
-
rescue Error
|
|
82
|
-
next
|
|
83
|
-
end
|
|
84
|
-
[ "unknown" ]
|
|
34
|
+
JSON.generate(hookSpecificOutput: { hookEventName: "PostToolUse", additionalContext: message })
|
|
85
35
|
end
|
|
86
36
|
end
|
|
87
37
|
end
|
|
@@ -110,25 +110,23 @@ module BrowserReviewGate
|
|
|
110
110
|
|
|
111
111
|
def install_claude
|
|
112
112
|
managed(".claude/skills/browser-pr-verification/SKILL.md", render("claude_skill.md"))
|
|
113
|
-
|
|
114
|
-
(hooks[
|
|
115
|
-
{ "matcher" => "Bash", "hooks" => [ { "type" => "command", "command" => command, "timeout" => 60 } ] }
|
|
113
|
+
add_hook(".claude/settings.json", "claude", %w[PreToolUse]) do |hooks, command|
|
|
114
|
+
(hooks["PostToolUse"] ||= []) << { "matcher" => "Bash", "hooks" => [ { "type" => "command", "command" => command, "timeout" => 60 } ] }
|
|
116
115
|
end
|
|
117
116
|
end
|
|
118
117
|
|
|
119
118
|
def install_cursor
|
|
120
119
|
managed(".cursor/commands/browser-pr-verification.md", render("cursor_command.md"))
|
|
121
|
-
|
|
122
|
-
(hooks[
|
|
120
|
+
add_hook(".cursor/hooks.json", "cursor", %w[beforeShellExecution], "version" => 1) do |hooks, command|
|
|
121
|
+
(hooks["afterShellExecution"] ||= []) << { "command" => command }
|
|
123
122
|
end
|
|
124
123
|
end
|
|
125
124
|
|
|
126
125
|
# Codex runs a project hook only after the person trusts it in /hooks, so the AGENTS.md section also
|
|
127
126
|
# tells the agent to publish after it opens the PR.
|
|
128
127
|
def install_codex
|
|
129
|
-
|
|
130
|
-
(hooks[
|
|
131
|
-
{ "matcher" => "^Bash$", "hooks" => [ { "type" => "command", "command" => command, "timeout" => 60 } ] }
|
|
128
|
+
add_hook(".codex/hooks.json", "codex", %w[PreToolUse]) do |hooks, command|
|
|
129
|
+
(hooks["PostToolUse"] ||= []) << { "matcher" => "^Bash$", "hooks" => [ { "type" => "command", "command" => command, "timeout" => 60 } ] }
|
|
132
130
|
end
|
|
133
131
|
path = "AGENTS.md"
|
|
134
132
|
current = exist?(path) ? File.read(File.join(@root, path)) : nil
|
|
@@ -139,8 +137,8 @@ module BrowserReviewGate
|
|
|
139
137
|
|
|
140
138
|
# A machine without the gem (a cloud agent, a fresh container) must not see a failing hook after
|
|
141
139
|
# every command.
|
|
142
|
-
def hook_command(agent
|
|
143
|
-
"command -v browser-review-gate >/dev/null 2>&1 && #{HOOK} #{agent}
|
|
140
|
+
def hook_command(agent)
|
|
141
|
+
"command -v browser-review-gate >/dev/null 2>&1 && #{HOOK} #{agent} || true"
|
|
144
142
|
end
|
|
145
143
|
|
|
146
144
|
def render(name)
|
|
@@ -161,14 +159,22 @@ module BrowserReviewGate
|
|
|
161
159
|
write(path, content, current)
|
|
162
160
|
end
|
|
163
161
|
|
|
164
|
-
# Adds the after-command
|
|
165
|
-
|
|
162
|
+
# Adds the after-command hook entry when the file lacks it, and removes the before-command entries
|
|
163
|
+
# version 0.1.1 put under `stale_events`: nothing holds a command back any more.
|
|
164
|
+
def add_hook(path, agent, stale_events, defaults = {})
|
|
166
165
|
current = exist?(path) ? File.read(File.join(@root, path)) : nil
|
|
167
|
-
|
|
168
|
-
return @log.puts(" unchanged #{path}") if
|
|
166
|
+
present = current.to_s.match?(/hook #{agent}(?! --before)/)
|
|
167
|
+
return @log.puts(" unchanged #{path}") if present && !current.include?("hook #{agent} --before")
|
|
169
168
|
|
|
170
169
|
settings = defaults.merge(current ? JSON.parse(current) : {})
|
|
171
|
-
|
|
170
|
+
hooks = (settings["hooks"] ||= {})
|
|
171
|
+
stale_events.each do |event|
|
|
172
|
+
next unless hooks[event]
|
|
173
|
+
|
|
174
|
+
hooks[event].reject! { |entry| JSON.generate(entry).include?("hook #{agent} --before") }
|
|
175
|
+
hooks.delete(event) if hooks[event].empty?
|
|
176
|
+
end
|
|
177
|
+
yield(hooks, hook_command(agent)) unless present
|
|
172
178
|
write(path, "#{JSON.pretty_generate(settings)}\n", current)
|
|
173
179
|
end
|
|
174
180
|
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: browser_review_gate
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.2
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- JetRockets
|
|
@@ -66,7 +66,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
67
|
version: '0'
|
|
68
68
|
requirements: []
|
|
69
|
-
rubygems_version: 4.0.
|
|
69
|
+
rubygems_version: 4.0.1
|
|
70
70
|
specification_version: 4
|
|
71
71
|
summary: Keeps human review of a pull request behind a browser run of its browser-visible
|
|
72
72
|
changes.
|