browser_review_gate 0.2.1 → 0.3.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +38 -0
- data/LICENSE.txt +1 -1
- data/README.md +134 -62
- data/lib/browser_review_gate/assessment.rb +74 -33
- data/lib/browser_review_gate/assessor.rb +38 -10
- data/lib/browser_review_gate/cli.rb +44 -19
- data/lib/browser_review_gate/config.rb +223 -10
- data/lib/browser_review_gate/doctor.rb +216 -0
- data/lib/browser_review_gate/evidence.rb +170 -0
- data/lib/browser_review_gate/gate.rb +8 -5
- data/lib/browser_review_gate/github.rb +7 -0
- data/lib/browser_review_gate/model_api.rb +16 -0
- data/lib/browser_review_gate/model_client.rb +10 -0
- data/lib/browser_review_gate/prompts.rb +10 -5
- data/lib/browser_review_gate/publisher.rb +32 -9
- data/lib/browser_review_gate/report.rb +222 -47
- data/lib/browser_review_gate/saved_report.rb +15 -1
- data/lib/browser_review_gate/stats.rb +90 -6
- data/lib/browser_review_gate/status.rb +52 -21
- data/lib/browser_review_gate/templates/agents_block.md.erb +1 -1
- data/lib/browser_review_gate/templates/config.yml.erb +63 -5
- data/lib/browser_review_gate/templates/playbook.md.erb +63 -25
- data/lib/browser_review_gate/templates/rules.md.erb +4 -1
- data/lib/browser_review_gate/verify.rb +86 -0
- data/lib/browser_review_gate/version.rb +1 -1
- data/lib/browser_review_gate.rb +3 -0
- metadata +31 -5
|
@@ -3,35 +3,60 @@ require "openssl"
|
|
|
3
3
|
require "uri"
|
|
4
4
|
|
|
5
5
|
module BrowserReviewGate
|
|
6
|
-
# What an agent needs to know before a browser run: the decision for the PR head
|
|
6
|
+
# What an agent needs to know before a browser run: the decision for the PR head, the scenarios still
|
|
7
|
+
# owed with their status, the environment to use, the policy, and the next step.
|
|
7
8
|
class Status
|
|
8
9
|
RUN_STEPS = %w[run_and_publish run_and_save].freeze
|
|
9
10
|
|
|
10
|
-
# `probe` takes
|
|
11
|
+
# `probe` takes a URL and says whether something answers there.
|
|
11
12
|
def initialize(github:, config: Config.new, shell: Shell.new, probe: nil, prompts: Prompts.new)
|
|
12
13
|
@prompts = prompts
|
|
13
14
|
@github = github
|
|
14
15
|
@config = config
|
|
15
16
|
@shell = shell
|
|
16
|
-
@probe = probe || method(:answers?)
|
|
17
|
+
@probe = probe || Status.method(:answers?)
|
|
17
18
|
end
|
|
18
19
|
|
|
19
|
-
def to_h(number = nil)
|
|
20
|
+
def to_h(number = nil, environment: nil)
|
|
20
21
|
result = facts(number)
|
|
21
22
|
result["project_rules"] = Prompts::RULES if @prompts.rules
|
|
22
|
-
result["
|
|
23
|
+
result["environment"] = environment_details(environment)
|
|
24
|
+
result["policy"] = @config.policy
|
|
25
|
+
result["report"] = report_instructions
|
|
23
26
|
# The agent never starts the app itself: a person decides what runs on their machine.
|
|
24
|
-
result["next"] = "ask_to_start_app" if RUN_STEPS.include?(result["next"]) && result["
|
|
27
|
+
result["next"] = "ask_to_start_app" if RUN_STEPS.include?(result["next"]) && result["environment"]["reachable"] == false
|
|
25
28
|
result
|
|
26
29
|
end
|
|
27
30
|
|
|
31
|
+
# Any HTTP answer counts, redirects are not followed, and only http(s) is tried. A local
|
|
32
|
+
# certificate is not checked. The URL comes from the project settings.
|
|
33
|
+
def self.answers?(url)
|
|
34
|
+
uri = URI(url)
|
|
35
|
+
return false unless %w[http https].include?(uri.scheme) && uri.host
|
|
36
|
+
|
|
37
|
+
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", verify_mode: OpenSSL::SSL::VERIFY_NONE,
|
|
38
|
+
open_timeout: 2, read_timeout: 3) { |http| http.head(uri.request_uri) }
|
|
39
|
+
true
|
|
40
|
+
rescue StandardError
|
|
41
|
+
false
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
# The URL a health check asks: an absolute `healthcheck`, or its path on the environment URL.
|
|
45
|
+
def self.healthcheck_url(environment)
|
|
46
|
+
check = environment["healthcheck"]
|
|
47
|
+
return environment["url"] if check.nil?
|
|
48
|
+
return check if check.start_with?("http://", "https://")
|
|
49
|
+
|
|
50
|
+
URI.join(environment["url"], check).to_s
|
|
51
|
+
end
|
|
52
|
+
|
|
28
53
|
private
|
|
29
54
|
|
|
30
55
|
def facts(number)
|
|
31
56
|
local_sha = @shell.call("git", "rev-parse", "HEAD").strip
|
|
32
57
|
saved = SavedReport.new(sha: local_sha, shell: @shell).exist?
|
|
33
58
|
number ||= @github.pull_request_number_for_branch
|
|
34
|
-
return { "pull_request" => nil, "local_head" => local_sha, "saved_run" => saved, "next" => saved ? "open_pull_request" : "run_and_save" } unless number
|
|
59
|
+
return { "pull_request" => nil, "local_head" => local_sha, "saved_run" => saved, "scenarios" => [], "next" => saved ? "open_pull_request" : "run_and_save" } unless number
|
|
35
60
|
|
|
36
61
|
pull_request = @github.pull_request(number)
|
|
37
62
|
comments = @github.trusted_comments(number)
|
|
@@ -39,17 +64,25 @@ module BrowserReviewGate
|
|
|
39
64
|
assessment = Assessment.latest(comments)
|
|
40
65
|
assessment = nil unless assessment&.for?(head_sha)
|
|
41
66
|
labels = pull_request.fetch("labels").map { |label| label["name"] }
|
|
42
|
-
report = Report.latest(comments)
|
|
43
|
-
|
|
67
|
+
report = Report.latest(comments, config: @config)
|
|
68
|
+
commits = @github.pull_request_commits(number)
|
|
69
|
+
reuse = @config.scenarios["reuse_passing"]
|
|
70
|
+
verified = labels.include?(@config.label) && assessment&.covered_by?(report, commits: commits, reuse_passing: reuse) == true
|
|
44
71
|
waived = labels.include?(@config.waiver_label)
|
|
45
72
|
|
|
46
73
|
{ "pull_request" => number, "pr_head" => head_sha, "local_head" => local_sha,
|
|
47
74
|
"decision" => assessment&.decision, "reason" => assessment&.reason,
|
|
48
75
|
# What the run must exercise; each id is the `id` of the case that reports it.
|
|
49
|
-
"scenarios" => assessment ? assessment
|
|
76
|
+
"scenarios" => assessment ? scenarios(assessment, report, commits, reuse) : [], "verified" => verified, "waived" => waived,
|
|
50
77
|
"saved_run" => saved, "next" => next_step(assessment, verified || waived, saved, head_sha == local_sha) }
|
|
51
78
|
end
|
|
52
79
|
|
|
80
|
+
def scenarios(assessment, report, commits, reuse)
|
|
81
|
+
assessment.outstanding(report, commits: commits, reuse_passing: reuse).map do |scenario|
|
|
82
|
+
scenario.merge("status" => report ? report.status_of(scenario["id"], commits: commits) : "missing")
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
53
86
|
def next_step(assessment, cleared, saved, on_pr_head)
|
|
54
87
|
return "nothing" if cleared || (assessment && !assessment.required?)
|
|
55
88
|
return "check_out_pr_head" unless on_pr_head
|
|
@@ -59,20 +92,18 @@ module BrowserReviewGate
|
|
|
59
92
|
"run_and_publish"
|
|
60
93
|
end
|
|
61
94
|
|
|
62
|
-
def
|
|
63
|
-
|
|
64
|
-
|
|
95
|
+
def environment_details(name)
|
|
96
|
+
environment = @config.environment(name)
|
|
97
|
+
return { "name" => nil, "reachable" => nil, "note" => "No environment is configured in #{Config::PATH}." } unless environment
|
|
98
|
+
|
|
99
|
+
details = environment.slice("name", "url", "healthcheck", "start_command", "sign_in")
|
|
100
|
+
details["reachable"] = environment["url"] ? @probe.call(Status.healthcheck_url(environment)) : nil
|
|
65
101
|
details
|
|
66
102
|
end
|
|
67
103
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", verify_mode: OpenSSL::SSL::VERIFY_NONE,
|
|
72
|
-
open_timeout: 2, read_timeout: 3) { |http| http.head(uri.path.empty? ? "/" : uri.path) }
|
|
73
|
-
true
|
|
74
|
-
rescue StandardError
|
|
75
|
-
false
|
|
104
|
+
def report_instructions
|
|
105
|
+
{ "schema_version" => Report::SCHEMA_VERSION, "results" => Report::RESULTS, "capabilities" => Report::CAPABILITIES,
|
|
106
|
+
"evidence_paths" => "relative to the directory of the report file, outside the repository" }
|
|
76
107
|
end
|
|
77
108
|
end
|
|
78
109
|
end
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
<!-- browser_review_gate:start -->
|
|
2
2
|
## Browser verification before human review
|
|
3
3
|
|
|
4
|
-
A PR that changes browser-visible behavior needs a browser run before a human reviews it; CI takes back reviewer requests until one is published. When asked to run `<%= command %>` (with or without a PR number), or to verify a branch or PR in the browser, read `<%= playbook_path %>` and follow it exactly. Right after opening a PR with `gh pr create`, run `browser-review-gate
|
|
4
|
+
A PR that changes browser-visible behavior needs a browser run before a human reviews it; CI takes back reviewer requests until one is published. When asked to run `<%= command %>` (with or without a PR number), or to verify a branch or PR in the browser, read `<%= playbook_path %>` and follow it exactly. Right after opening a PR with `gh pr create`, run `browser-review-gate verify`: it publishes a run saved for this commit.
|
|
5
5
|
<!-- browser_review_gate:end -->
|
|
@@ -1,10 +1,68 @@
|
|
|
1
1
|
# Settings for browser_review_gate. Every key is optional.
|
|
2
|
+
#
|
|
3
|
+
# [enforced] settings are checked by the gem: it refuses a report or keeps review on hold.
|
|
4
|
+
# [agent] settings are instructions the AI coding agent receives; the gem cannot make a browser
|
|
5
|
+
# tool take a screenshot, it can only refuse a run without one when `gate.require_evidence` is on.
|
|
2
6
|
|
|
3
|
-
# Where the
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
#
|
|
7
|
-
|
|
7
|
+
# Where the app runs for a browser run. The agent opens `url`; `healthcheck` (a path on `url`, or a
|
|
8
|
+
# full URL) is what `verify`, `status` and `doctor` ask to see whether the app answers. Nobody starts
|
|
9
|
+
# the app automatically: `start_command` is what the person runs. `sign_in` is a hint such as "use the
|
|
10
|
+
# seed account from db/seeds.rb"; never put real credentials here, this file is in the repository.
|
|
11
|
+
# A cloud agent cannot reach localhost: give it an environment with a URL it can reach (staging, a
|
|
12
|
+
# preview deployment) and select it with `--environment`.
|
|
13
|
+
environments:
|
|
14
|
+
local:
|
|
15
|
+
url:<%= url ? " #{url.inspect}" : "" %>
|
|
16
|
+
healthcheck:<%= url ? ' "/up"' : "" %>
|
|
17
|
+
start_command:<%= start_command ? " #{start_command.inspect}" : "" %>
|
|
18
|
+
sign_in:
|
|
19
|
+
# staging:
|
|
20
|
+
# url: "https://staging.example.com"
|
|
21
|
+
# healthcheck: "/up"
|
|
22
|
+
|
|
23
|
+
verification:
|
|
24
|
+
# The environment used when none is named with --environment.
|
|
25
|
+
environment: local
|
|
26
|
+
# [enforced] A case that passes but reports console errors / failed network requests counts as failed.
|
|
27
|
+
fail_on_console_errors: false
|
|
28
|
+
fail_on_network_errors: false
|
|
29
|
+
|
|
30
|
+
evidence:
|
|
31
|
+
# [agent] never, on_failure or always. [enforced] with gate.require_evidence, for tools that can.
|
|
32
|
+
screenshots: on_failure
|
|
33
|
+
trace: never
|
|
34
|
+
# [agent] Record console errors and failed requests per case. [enforced] with gate.require_evidence.
|
|
35
|
+
console_logs: true
|
|
36
|
+
network_logs: true
|
|
37
|
+
# [enforced] Saved runs and kept evidence files in the git directory are removed after this many days.
|
|
38
|
+
retention_days: 14
|
|
39
|
+
# [enforced] Hosts that evidence links may point to, e.g. your artifact bucket. Empty: no links.
|
|
40
|
+
link_hosts: []
|
|
41
|
+
|
|
42
|
+
scenarios:
|
|
43
|
+
# [enforced] The most scenarios the assessing model may ask for (1-30). Mandatory ones come on top.
|
|
44
|
+
max_count: 12
|
|
45
|
+
# [enforced] A passing case from an earlier commit of the PR keeps counting after unrelated commits.
|
|
46
|
+
# false: every scenario must pass on the newest commit.
|
|
47
|
+
reuse_passing: true
|
|
48
|
+
# [enforced] Scenarios added to every assessment that needs a run, whatever the model proposes. With
|
|
49
|
+
# `paths`, only when the change touches them, and then a run is needed whatever the model decides.
|
|
50
|
+
# `matrix: true` repeats the scenario for every combination below (at most 24 cases in total).
|
|
51
|
+
mandatory: []
|
|
52
|
+
# - id: SIGN-IN
|
|
53
|
+
# name: Sign in and land on the dashboard
|
|
54
|
+
# why: Every change to authentication must keep sign-in working.
|
|
55
|
+
# paths: ["app/controllers/sessions_controller.rb", "app/views/sessions/**"]
|
|
56
|
+
# matrix: true
|
|
57
|
+
matrix: {}
|
|
58
|
+
# role: [admin, member]
|
|
59
|
+
# viewport: [desktop, mobile]
|
|
60
|
+
|
|
61
|
+
gate:
|
|
62
|
+
# [enforced] Refuse runs whose passing cases lack the evidence asked for above.
|
|
63
|
+
require_evidence: false
|
|
64
|
+
# [enforced] Any failing case keeps review on hold, not only failing required scenarios.
|
|
65
|
+
block_on_failure: true
|
|
8
66
|
|
|
9
67
|
# Who assesses a PR in CI. Unset: the Claude Code CLI with a CLAUDE_CODE_OAUTH_TOKEN or
|
|
10
68
|
# ANTHROPIC_API_KEY secret; with only an OPENAI_API_KEY or GEMINI_API_KEY secret, that provider's API.
|
|
@@ -1,56 +1,94 @@
|
|
|
1
1
|
<!-- browser_review_gate:managed <%= BrowserReviewGate::VERSION %> — re-run `browser-review-gate install` to update. -->
|
|
2
2
|
# Browser verification before human review
|
|
3
3
|
|
|
4
|
-
A pull request that changes what a person sees or does in a browser must be exercised in a browser before a human reviews it. CI decides whether a PR needs that and writes the decision in one "Browser verification" comment. Until a
|
|
4
|
+
A pull request that changes what a person sees or does in a browser must be exercised in a browser before a human reviews it. CI decides whether a PR needs that and writes the decision in one "Browser verification" comment, with the scenarios a run must pass. Until a run covering them is published, reviewer requests on such a PR are taken back.
|
|
5
5
|
|
|
6
6
|
This file is the playbook for the AI coding agent that does the run. The person starts it with `<%= command %> NUMBER` for an open PR, or `<%= command %>` on a branch that has no PR yet.
|
|
7
7
|
|
|
8
|
+
**Who does what.** You, the agent, drive the browser with your browser tool (Playwright, Chrome DevTools MCP or similar) and write down what you saw. `browser-review-gate` does the bookkeeping: which scenarios are owed, which environment to use, whether the app answers, and checking and publishing your report. It never drives a browser, and it cannot see what you did: your report is your account, so it must be accurate.
|
|
9
|
+
|
|
8
10
|
## Steps
|
|
9
11
|
|
|
10
|
-
1. Run `browser-review-gate
|
|
11
|
-
- `nothing`: no run is needed, or the PR is already verified. Say that review can be requested.
|
|
12
|
+
1. Run `browser-review-gate verify --json` (add `--pr NUMBER` when a number was given, `--environment NAME` when the person named one). Follow `outcome`:
|
|
13
|
+
- `nothing`: no run is needed, or the PR is already verified or waived. Say that review can be requested.
|
|
12
14
|
- `check_out_pr_head`: check out the PR head with a clean worktree, then start again.
|
|
13
|
-
- `
|
|
14
|
-
- `
|
|
15
|
+
- `assessment_requested`: CI was asked to assess the PR. Wait about a minute, then start again.
|
|
16
|
+
- `published` / `not_verified`: a run saved for this commit was published. Report the result and stop.
|
|
15
17
|
- `open_pull_request`: a run is saved and waits for the PR. Tell the person to open it.
|
|
16
|
-
- `ask_to_start_app`: the
|
|
18
|
+
- `ask_to_start_app`: the environment does not answer. Do not start it yourself: ask the person to start it (`environment.start_command` says how) and stop until they have.
|
|
17
19
|
- `run_and_publish` or `run_and_save`: continue with step 2.
|
|
18
20
|
2. Decide what to run.
|
|
19
|
-
- `scenarios`
|
|
20
|
-
- With no PR yet, or an empty list, choose the scenarios yourself: read the diff against the base branch, then `<%= rules_path %>`, which says what this project always checks
|
|
21
|
-
- Do not re-run
|
|
22
|
-
3. Open
|
|
23
|
-
4. Run every scenario end to end.
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
21
|
+
- `scenarios` is the list CI wrote for this PR, with each one's `status` (`missing`, `failed`, `blocked`, `skipped`, `stale`). Run every one of them and give each case the `id` of its scenario. Scenarios marked `critical` or in the `mandatory` category come from the project settings; they cannot be left out. You may add cases of your own; you may not drop or rename one from the list.
|
|
22
|
+
- With no PR yet, or an empty list, choose the scenarios yourself: read the diff against the base branch, then `<%= rules_path %>`, which says what this project always checks. Its rules are mandatory. CI compares your cases with its own list once the PR is open and asks for what is missing.
|
|
23
|
+
- Do not re-run a scenario whose earlier case still counts; `verify` lists only what is owed.
|
|
24
|
+
3. Open `environment.url` in your browser tool. `environment.sign_in` is a sign-in hint. Never start, restart or stop the app yourself. Use test data and do not destroy existing data. If the app, the data or a browser tool is missing, report the blocker. Reading code and running unit tests does not replace a browser run.
|
|
25
|
+
4. Run every scenario end to end. For each case record:
|
|
26
|
+
- `url`: the page it was exercised on; `details`: what you observed;
|
|
27
|
+
- evidence, as `policy.agent` in the `verify` output asks: screenshots (`never`, `on_failure`, `always`), a trace, and the console errors and failed requests you saw (`[]` when there were none). Save files in a directory outside the repository, next to the report.
|
|
28
|
+
- A scenario you could not run is `blocked` (something prevented it) or `skipped` (deliberately not run), with a `reason`. Neither is a pass.
|
|
29
|
+
5. Write the report as JSON next to the evidence (see the shape below). Set `coverage_complete` to true only when every scenario from step 2 ran.
|
|
30
|
+
6. Publish, from the checkout that was tested, with everything committed and pushed: `browser-review-gate verify --report PATH` (add `--pr NUMBER` when the branch has several PRs). With a PR it is published; without one it is saved and published when the PR for this commit is opened.
|
|
31
|
+
7. Check the result. Exit status 0 means review can proceed; 7 means the report was published but review stays on hold (a scenario is missing, failed, or broke a project policy); 1 means the report was refused, with the reason. A refused report is not published: fix it and publish again.
|
|
32
|
+
8. Request a reviewer only when the person named one and only after the run passed. Reviewers whose requests CI took back earlier are asked again automatically.
|
|
33
|
+
|
|
34
|
+
## What the gem checks, and what it cannot
|
|
35
|
+
|
|
36
|
+
- It refuses a report with an unknown key, a missing field, an evidence path outside the report directory, a file that is not the image or archive its name says, a link to a host the project did not list, or a commit other than the checked-out one.
|
|
37
|
+
- With `gate.require_evidence`, a passing case without the screenshots, trace or logs the project asks for does not count, unless your tool lacks that capability (say so in `tool.capabilities`).
|
|
38
|
+
- With `verification.fail_on_console_errors` / `fail_on_network_errors`, a passing case that lists such errors counts as failed.
|
|
39
|
+
- Only cases tested on a commit of this PR count.
|
|
40
|
+
- It cannot tell whether a browser really did what you wrote. File digests show that files existed when the run was published, not who made them.
|
|
30
41
|
|
|
31
42
|
## When a scenario cannot be run
|
|
32
43
|
|
|
33
|
-
Some scenarios cannot be exercised locally, for example a sign-in through an external provider.
|
|
44
|
+
Some scenarios cannot be exercised locally, for example a sign-in through an external provider. Report it as `blocked` with the reason, or, when the person decided so, list it under `excluded`. Neither counts as a pass, so review stays on hold for that scenario: tell the person. Someone other than the author can put the `<%= waiver_label %>` label on the PR to let review proceed without a run.
|
|
34
45
|
|
|
35
46
|
## Report shape
|
|
36
47
|
|
|
37
48
|
```json
|
|
38
49
|
{
|
|
39
|
-
"
|
|
50
|
+
"schema_version": 2,
|
|
51
|
+
"tested_sha": "<full 40-character SHA of the commit that was tested>",
|
|
52
|
+
"environment": "local",
|
|
53
|
+
"tool": { "name": "Playwright MCP", "version": "0.0.40", "capabilities": ["screenshots", "trace", "console", "network"] },
|
|
54
|
+
"started_at": "2026-11-11T10:00:00Z",
|
|
55
|
+
"finished_at": "2026-11-11T10:06:30Z",
|
|
40
56
|
"coverage_complete": true,
|
|
41
57
|
"cases": [
|
|
42
|
-
{
|
|
58
|
+
{
|
|
59
|
+
"id": "BROWSER-1",
|
|
60
|
+
"name": "Submit the sign-up form and land on the dashboard",
|
|
61
|
+
"result": "pass",
|
|
62
|
+
"url": "/signup",
|
|
63
|
+
"details": "The form submitted; the dashboard showed the new account's name.",
|
|
64
|
+
"evidence": {
|
|
65
|
+
"screenshots": ["artifacts/signup-done.png"],
|
|
66
|
+
"trace": "artifacts/signup-trace.zip",
|
|
67
|
+
"console_errors": [],
|
|
68
|
+
"network_errors": []
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"id": "SIGN-IN.member.mobile",
|
|
73
|
+
"name": "Sign in as a member at phone width",
|
|
74
|
+
"result": "blocked",
|
|
75
|
+
"reason": "The member seed account is missing from the local database.",
|
|
76
|
+
"details": "The sign-in form rejected the seed credentials."
|
|
77
|
+
}
|
|
43
78
|
],
|
|
44
79
|
"excluded": [
|
|
45
|
-
{ "name": "Sign-in through an external provider", "reason": "Needs a real provider account; covered by integration tests." }
|
|
80
|
+
{ "id": "OAUTH-1", "name": "Sign-in through an external provider", "reason": "Needs a real provider account; covered by integration tests." }
|
|
46
81
|
]
|
|
47
82
|
}
|
|
48
83
|
```
|
|
49
84
|
|
|
50
|
-
`result`
|
|
85
|
+
- `result`: `pass`, `fail`, `blocked` or `skipped`. `url` is an http(s) URL or a path starting with `/`; required for `pass` and `fail`. `reason` is required for `blocked` and `skipped`.
|
|
86
|
+
- `tool.capabilities`: what your browser tool could record, from `screenshots`, `trace`, `console`, `network`. Claim only what it really can.
|
|
87
|
+
- `evidence` paths are relative to the report file's directory. Accepted: `.png`, `.jpg`, `.jpeg`, `.webp` screenshots and a `.zip` trace, at most 10 files of 20 MB each. `network_errors` entries may be strings or `{ "method", "url", "status" }`; query strings are dropped. `links` may point to evidence kept elsewhere, on hosts listed in `evidence.link_hosts`.
|
|
88
|
+
- Everything you write is scrubbed of values that look like credentials, but do not rely on it: never put cookies, tokens, passwords or customer data into a report, a screenshot or a trace. Keep customer and real company names out of the report.
|
|
51
89
|
|
|
52
90
|
## Rules
|
|
53
91
|
|
|
54
|
-
- Run the awkward path in the browser, or
|
|
55
|
-
- Keep the report file and
|
|
56
|
-
- After an unrelated commit, do not run everything again:
|
|
92
|
+
- Run the awkward path in the browser, or report it as blocked and leave review on hold. Do not declare a run done from tests and code reading.
|
|
93
|
+
- Keep the report file and evidence outside the repository. The publisher refuses a checkout with modified or untracked files and lists them.
|
|
94
|
+
- After an unrelated commit, do not run everything again: `verify` lists only what is new or no longer counts.
|
|
@@ -16,7 +16,10 @@ one is ignored. The built-in rules stay in force; these are added on top and win
|
|
|
16
16
|
## Always check
|
|
17
17
|
|
|
18
18
|
<!-- Example: Run every scenario once as an administrator and once as a regular member.
|
|
19
|
-
Example: Repeat the main scenario at a phone-sized window.
|
|
19
|
+
Example: Repeat the main scenario at a phone-sized window.
|
|
20
|
+
These are read by the model and the agent. To make a scenario or a role/viewport combination
|
|
21
|
+
impossible to leave out, declare it under `scenarios.mandatory` and `scenarios.matrix` in
|
|
22
|
+
.github/browser-review-gate.yml: those are added by the gem and checked case by case. -->
|
|
20
23
|
|
|
21
24
|
## Scenarios by area
|
|
22
25
|
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
module BrowserReviewGate
|
|
2
|
+
# One entry point for the verification workflow. Ruby does the bookkeeping: which PR, which decision,
|
|
3
|
+
# which scenarios are still owed, which environment, whether the app answers, and publishing what an
|
|
4
|
+
# agent observed. The browser run itself happens in the AI coding agent (Claude Code, Cursor, Codex)
|
|
5
|
+
# with its browser tool; nothing here drives a browser.
|
|
6
|
+
class Verify
|
|
7
|
+
# Exit statuses, so an agent or a script can branch on the outcome.
|
|
8
|
+
EXIT = {
|
|
9
|
+
"nothing" => 0, "published" => 0, "saved" => 0,
|
|
10
|
+
"run_and_publish" => 2, "run_and_save" => 2,
|
|
11
|
+
"ask_to_start_app" => 3,
|
|
12
|
+
"request_assessment" => 4, "assessment_requested" => 4,
|
|
13
|
+
"check_out_pr_head" => 5, "open_pull_request" => 5,
|
|
14
|
+
"publish_saved" => 6, "not_verified" => 7
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
def initialize(github:, config:, prompts:, shell: Shell.new, probe: nil, publisher_factory: nil)
|
|
18
|
+
@github = github
|
|
19
|
+
@config = config
|
|
20
|
+
@prompts = prompts
|
|
21
|
+
@shell = shell
|
|
22
|
+
@probe = probe
|
|
23
|
+
@publisher_factory = publisher_factory || lambda { |number, path|
|
|
24
|
+
Publisher.new(github: github, number: number, report_path: path, config: config, shell: shell)
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
# Returns [outcome, details]. `report` publishes (or saves, before the PR exists) a finished run.
|
|
29
|
+
# `request` lets a missing assessment be requested from CI.
|
|
30
|
+
def run(number: nil, environment: nil, report: nil, request: true)
|
|
31
|
+
status = Status.new(github: @github, config: @config, shell: @shell, probe: @probe, prompts: @prompts).to_h(number, environment: environment)
|
|
32
|
+
outcome = status["next"]
|
|
33
|
+
return [ submit(status, report), status ] if report && %w[run_and_publish run_and_save publish_saved].include?(outcome)
|
|
34
|
+
|
|
35
|
+
if outcome == "publish_saved"
|
|
36
|
+
outcome = @publisher_factory.call(status["pull_request"], nil).publish ? "published" : "not_verified"
|
|
37
|
+
elsif outcome == "request_assessment" && request
|
|
38
|
+
@github.dispatch_workflow(@config.workflow, status["pull_request"])
|
|
39
|
+
outcome = "assessment_requested"
|
|
40
|
+
end
|
|
41
|
+
[ outcome, status ]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.exit_status(outcome) = EXIT.fetch(outcome, 1)
|
|
45
|
+
|
|
46
|
+
# What to tell the person or the agent for each outcome.
|
|
47
|
+
def self.describe(outcome, status)
|
|
48
|
+
environment = status["environment"] || {}
|
|
49
|
+
case outcome
|
|
50
|
+
when "nothing" then "Nothing to run: #{status["waived"] ? "verification was waived" : "no browser run is needed or the PR is verified"}. Human review can be requested."
|
|
51
|
+
when "published" then "Browser verification published; human review can be requested."
|
|
52
|
+
when "saved" then "Browser run saved. It is published when the PR for this commit is opened."
|
|
53
|
+
when "not_verified" then "Results published, but required scenarios still need a passing case. Review stays on hold."
|
|
54
|
+
when "assessment_requested" then "CI has been asked to assess PR #{status["pull_request"]}. Run verify again in about a minute."
|
|
55
|
+
when "request_assessment" then "PR #{status["pull_request"]} has no assessment yet. Run verify without --no-request to ask CI for one."
|
|
56
|
+
when "check_out_pr_head" then "Check out the PR head (#{status["pr_head"]}) with a clean worktree, then run verify again."
|
|
57
|
+
when "open_pull_request" then "A run is saved for this commit. Open the pull request; it is published then."
|
|
58
|
+
when "ask_to_start_app" then "The #{environment["name"]} environment does not answer at #{environment["url"]}. Ask the person to start it" \
|
|
59
|
+
"#{environment["start_command"] ? " (#{environment["start_command"]})" : ""}; do not start it yourself."
|
|
60
|
+
else run_instructions(status)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.run_instructions(status)
|
|
65
|
+
environment = status["environment"] || {}
|
|
66
|
+
scenarios = status["scenarios"]
|
|
67
|
+
list = scenarios.any? ? scenarios.map { |scenario| " - #{scenario["id"]} [#{scenario["status"]}] #{scenario["name"]}" } :
|
|
68
|
+
[ " - none listed by CI yet: choose them from the diff and #{Prompts::RULES}" ]
|
|
69
|
+
[ "A browser run is needed#{status["pull_request"] ? " for PR ##{status["pull_request"]}" : ""}.",
|
|
70
|
+
"Environment: #{environment["name"] || "not configured"}#{environment["url"] ? " at #{environment["url"]}" : ""}.",
|
|
71
|
+
"Scenarios to run in your browser tool, one case per id:", *list,
|
|
72
|
+
"Then: browser-review-gate verify --report PATH (publishes, or saves before the PR exists)." ].join("\n")
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
private
|
|
76
|
+
|
|
77
|
+
def submit(status, report)
|
|
78
|
+
if status["pull_request"]
|
|
79
|
+
@publisher_factory.call(status["pull_request"], report).publish ? "published" : "not_verified"
|
|
80
|
+
else
|
|
81
|
+
@publisher_factory.call(nil, report).save
|
|
82
|
+
"saved"
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
data/lib/browser_review_gate.rb
CHANGED
|
@@ -23,6 +23,9 @@ module BrowserReviewGate
|
|
|
23
23
|
autoload :Assessment, File.join(__dir__, "browser_review_gate/assessment")
|
|
24
24
|
autoload :Assessor, File.join(__dir__, "browser_review_gate/assessor")
|
|
25
25
|
autoload :CLI, File.join(__dir__, "browser_review_gate/cli")
|
|
26
|
+
autoload :Doctor, File.join(__dir__, "browser_review_gate/doctor")
|
|
27
|
+
autoload :Evidence, File.join(__dir__, "browser_review_gate/evidence")
|
|
28
|
+
autoload :Verify, File.join(__dir__, "browser_review_gate/verify")
|
|
26
29
|
autoload :Config, File.join(__dir__, "browser_review_gate/config")
|
|
27
30
|
autoload :Gate, File.join(__dir__, "browser_review_gate/gate")
|
|
28
31
|
autoload :GitHub, File.join(__dir__, "browser_review_gate/github")
|
metadata
CHANGED
|
@@ -1,18 +1,34 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: browser_review_gate
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
|
-
-
|
|
7
|
+
- Islam Gagiev
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
10
|
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
-
dependencies:
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: json
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '2.7'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '2.7'
|
|
12
26
|
description: CI decides whether a pull request needs a browser run and takes back
|
|
13
27
|
reviewer requests until an AI coding agent (Claude Code, Cursor, Codex) has exercised
|
|
14
28
|
the change in a browser and published what it observed. One command sets a project
|
|
15
29
|
up.
|
|
30
|
+
email:
|
|
31
|
+
- omniacinis@gmail.com
|
|
16
32
|
executables:
|
|
17
33
|
- browser-review-gate
|
|
18
34
|
extensions: []
|
|
@@ -27,6 +43,8 @@ files:
|
|
|
27
43
|
- lib/browser_review_gate/assessor.rb
|
|
28
44
|
- lib/browser_review_gate/cli.rb
|
|
29
45
|
- lib/browser_review_gate/config.rb
|
|
46
|
+
- lib/browser_review_gate/doctor.rb
|
|
47
|
+
- lib/browser_review_gate/evidence.rb
|
|
30
48
|
- lib/browser_review_gate/gate.rb
|
|
31
49
|
- lib/browser_review_gate/github.rb
|
|
32
50
|
- lib/browser_review_gate/hook.rb
|
|
@@ -49,11 +67,19 @@ files:
|
|
|
49
67
|
- lib/browser_review_gate/templates/policy.md
|
|
50
68
|
- lib/browser_review_gate/templates/rules.md.erb
|
|
51
69
|
- lib/browser_review_gate/templates/workflow.yml.erb
|
|
70
|
+
- lib/browser_review_gate/verify.rb
|
|
52
71
|
- lib/browser_review_gate/version.rb
|
|
53
72
|
- lib/generators/browser_review_gate/install/install_generator.rb
|
|
73
|
+
homepage: https://github.com/Halvanhelv/browser_review_gate
|
|
54
74
|
licenses:
|
|
55
75
|
- MIT
|
|
56
|
-
metadata:
|
|
76
|
+
metadata:
|
|
77
|
+
allowed_push_host: https://rubygems.org
|
|
78
|
+
homepage_uri: https://github.com/Halvanhelv/browser_review_gate
|
|
79
|
+
source_code_uri: https://github.com/Halvanhelv/browser_review_gate
|
|
80
|
+
changelog_uri: https://github.com/Halvanhelv/browser_review_gate/blob/master/CHANGELOG.md
|
|
81
|
+
bug_tracker_uri: https://github.com/Halvanhelv/browser_review_gate/issues
|
|
82
|
+
rubygems_mfa_required: 'true'
|
|
57
83
|
rdoc_options: []
|
|
58
84
|
require_paths:
|
|
59
85
|
- lib
|
|
@@ -68,7 +94,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
68
94
|
- !ruby/object:Gem::Version
|
|
69
95
|
version: '0'
|
|
70
96
|
requirements: []
|
|
71
|
-
rubygems_version:
|
|
97
|
+
rubygems_version: 3.6.9
|
|
72
98
|
specification_version: 4
|
|
73
99
|
summary: Keeps human review of a pull request behind a browser run of its browser-visible
|
|
74
100
|
changes.
|