claude-dev-env 8.99.0 → 8.100.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/bin/install-pstack-plugin.mjs +55 -27
- package/bin/install-pstack-plugin.test.mjs +27 -2
- package/hooks/blocking/merge_review_gate.py +66 -105
- package/hooks/blocking/merge_review_gate_parts/__init__.py +1 -0
- package/hooks/blocking/merge_review_gate_parts/open_request.py +163 -0
- package/hooks/blocking/merge_review_gate_parts/receipts.py +192 -0
- package/hooks/blocking/test_merge_review_gate.py +218 -0
- package/hooks/blocking/tests/test_open_request.py +38 -0
- package/hooks/blocking/tests/test_receipts.py +57 -0
- package/hooks/features/conduct-gates.md +3 -3
- package/hooks/hooks.json +1 -1
- package/hooks/hooks_constants/merge_review_gate_constants.py +46 -1
- package/package.json +1 -1
- package/scripts/merge_review_receipt.py +56 -4
- package/scripts/test_merge_review_receipt.py +53 -0
|
@@ -13,8 +13,14 @@ clean once the rulings are posted and linked::
|
|
|
13
13
|
python3 merge_review_receipt.py owner/name 12 def5678 2 --ruled-out 2 --rulings <url>
|
|
14
14
|
-> CLEAN owner/name#12 def5678 (2 findings ruled out: <url>)
|
|
15
15
|
|
|
16
|
+
A pass run before the pull request exists names ``open`` in place of the
|
|
17
|
+
number, and records the head it reviewed::
|
|
18
|
+
|
|
19
|
+
python3 merge_review_receipt.py owner/name open def5678 0 -> CLEAN owner/name open def5678
|
|
20
|
+
|
|
16
21
|
``hooks/blocking/merge_review_gate.py`` allows a merge pinned to the head of
|
|
17
|
-
the newest pass only when that pass is clean.
|
|
22
|
+
the newest pass only when that pass is clean. It allows a pull request open
|
|
23
|
+
whose head has a clean ``open`` pass, and a merge pinned to that same head.
|
|
18
24
|
"""
|
|
19
25
|
|
|
20
26
|
from __future__ import annotations
|
|
@@ -30,12 +36,19 @@ hooks_directory = str(Path(__file__).resolve().parent.parent / "hooks")
|
|
|
30
36
|
if hooks_directory not in sys.path:
|
|
31
37
|
sys.path.insert(0, hooks_directory)
|
|
32
38
|
|
|
33
|
-
from blocking.
|
|
39
|
+
from blocking.merge_review_gate_parts.receipts import (
|
|
40
|
+
is_clean_receipt,
|
|
41
|
+
open_receipt_path,
|
|
42
|
+
receipt_path,
|
|
43
|
+
)
|
|
34
44
|
from hooks_constants.merge_review_gate_constants import (
|
|
45
|
+
OPEN_RECEIPT_HEAD_PATTERN,
|
|
46
|
+
OPEN_TARGET_WORD,
|
|
35
47
|
RECEIPT_FINDINGS_KEY,
|
|
36
48
|
RECEIPT_HEAD_KEY,
|
|
37
49
|
RECEIPT_RULED_OUT_KEY,
|
|
38
50
|
RECEIPT_RULINGS_KEY,
|
|
51
|
+
REPOSITORY_SLUG_PATTERN,
|
|
39
52
|
)
|
|
40
53
|
|
|
41
54
|
|
|
@@ -64,9 +77,29 @@ def record_review(
|
|
|
64
77
|
return _write_receipt(slug, number, receipt)
|
|
65
78
|
|
|
66
79
|
|
|
80
|
+
def record_open_review(slug: str, head: str, finding_count: int) -> Path:
|
|
81
|
+
"""Write the receipt for one review pass run before the pull request opens.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
slug: Repository as ``owner/name``.
|
|
85
|
+
head: The commit the pass reviewed.
|
|
86
|
+
finding_count: How many findings the pass reported.
|
|
87
|
+
|
|
88
|
+
Returns:
|
|
89
|
+
The receipt path written.
|
|
90
|
+
"""
|
|
91
|
+
owner, repo = slug.split("/", 1)
|
|
92
|
+
return _write_receipt_at(
|
|
93
|
+
open_receipt_path(owner, repo, head), _receipt_fields(head, finding_count, 0, "")
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
|
|
67
97
|
def _write_receipt(slug: str, number: int, all_receipt_fields: dict[str, object]) -> Path:
|
|
68
98
|
owner, repo = slug.split("/", 1)
|
|
69
|
-
|
|
99
|
+
return _write_receipt_at(receipt_path(owner, repo, number), all_receipt_fields)
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _write_receipt_at(path: Path, all_receipt_fields: dict[str, object]) -> Path:
|
|
70
103
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
71
104
|
path.write_text(json.dumps(all_receipt_fields) + "\n", encoding="utf-8")
|
|
72
105
|
return path
|
|
@@ -94,7 +127,9 @@ def main(all_arguments: Sequence[str], stdout: TextIO = sys.stdout) -> int:
|
|
|
94
127
|
"""
|
|
95
128
|
parser = argparse.ArgumentParser(prog="merge_review_receipt")
|
|
96
129
|
parser.add_argument("slug", help="Repository as owner/name.")
|
|
97
|
-
parser.add_argument(
|
|
130
|
+
parser.add_argument(
|
|
131
|
+
"number", help=f"Pull request number, or {OPEN_TARGET_WORD} before the pull request opens."
|
|
132
|
+
)
|
|
98
133
|
parser.add_argument("head", help="The head commit the pass reviewed.")
|
|
99
134
|
parser.add_argument("findings", type=int, help="Findings the pass reported.")
|
|
100
135
|
parser.add_argument(
|
|
@@ -103,6 +138,23 @@ def main(all_arguments: Sequence[str], stdout: TextIO = sys.stdout) -> int:
|
|
|
103
138
|
parser.add_argument("--rulings", default="", help="Link to the posted rulings.")
|
|
104
139
|
parsed = parser.parse_args(all_arguments)
|
|
105
140
|
rulings = parsed.rulings.strip()
|
|
141
|
+
if not REPOSITORY_SLUG_PATTERN.fullmatch(parsed.slug) or ".." in parsed.slug:
|
|
142
|
+
parser.error("slug must be owner/name")
|
|
143
|
+
if parsed.number == OPEN_TARGET_WORD:
|
|
144
|
+
if parsed.ruled_out or rulings:
|
|
145
|
+
parser.error(f"an {OPEN_TARGET_WORD} pass is clean only at zero findings")
|
|
146
|
+
if parsed.findings < 0:
|
|
147
|
+
parser.error("findings must be zero or more")
|
|
148
|
+
if not OPEN_RECEIPT_HEAD_PATTERN.fullmatch(parsed.head):
|
|
149
|
+
parser.error(f"an {OPEN_TARGET_WORD} pass needs the full 40 character commit")
|
|
150
|
+
record_open_review(parsed.slug, parsed.head, parsed.findings)
|
|
151
|
+
verdict = "CLEAN" if parsed.findings == 0 else "REVIEWED"
|
|
152
|
+
count_note = "" if parsed.findings == 0 else f" {parsed.findings} findings"
|
|
153
|
+
stdout.write(f"{verdict} {parsed.slug} {OPEN_TARGET_WORD} {parsed.head}{count_note}\n")
|
|
154
|
+
return 0 if parsed.findings == 0 else 1
|
|
155
|
+
if not parsed.number.isdigit():
|
|
156
|
+
parser.error(f"number must be a pull request number or {OPEN_TARGET_WORD}")
|
|
157
|
+
parsed.number = int(parsed.number)
|
|
106
158
|
if parsed.ruled_out < 0 or parsed.ruled_out > parsed.findings:
|
|
107
159
|
parser.error("--ruled-out must be between 0 and the finding count")
|
|
108
160
|
if parsed.ruled_out and not rulings:
|
|
@@ -122,3 +122,56 @@ def test_should_report_reviewed_when_the_rulings_link_points_at_another_pull_req
|
|
|
122
122
|
)
|
|
123
123
|
assert exit_code == 1
|
|
124
124
|
assert verdict_stream.getvalue() == f"REVIEWED acme/tools#12 {HEAD} 2 findings\n"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def test_should_record_a_clean_open_pass_under_heads(receipt_folder: Path) -> None:
|
|
128
|
+
verdict_stream = StringIO()
|
|
129
|
+
assert merge_review_receipt.main(["acme/tools", "open", HEAD, "0"], verdict_stream) == 0
|
|
130
|
+
assert verdict_stream.getvalue() == f"CLEAN acme/tools open {HEAD}\n"
|
|
131
|
+
receipt = json.loads((receipt_folder / "heads" / f"acme__tools__{HEAD}.json").read_text())
|
|
132
|
+
assert receipt == {"head": HEAD, "findings": 0}
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
def test_record_open_review_writes_the_head_receipt_in_lowercase(receipt_folder: Path) -> None:
|
|
136
|
+
path = merge_review_receipt.record_open_review("Acme/Tools", HEAD.upper(), 2)
|
|
137
|
+
assert path == receipt_folder / "heads" / f"acme__tools__{HEAD}.json"
|
|
138
|
+
assert json.loads(path.read_text(encoding="utf-8")) == {"head": HEAD.upper(), "findings": 2}
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def test_should_exit_one_for_an_open_pass_with_findings() -> None:
|
|
142
|
+
verdict_stream = StringIO()
|
|
143
|
+
assert merge_review_receipt.main(["acme/tools", "open", HEAD, "2"], verdict_stream) == 1
|
|
144
|
+
assert verdict_stream.getvalue() == f"REVIEWED acme/tools open {HEAD} 2 findings\n"
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
@pytest.mark.parametrize(
|
|
148
|
+
"all_arguments",
|
|
149
|
+
[
|
|
150
|
+
["acme/tools", "open", HEAD, "-1"],
|
|
151
|
+
["acme/tools", "open", HEAD, "2", "--ruled-out", "2", "--rulings", RULINGS_LINK],
|
|
152
|
+
["acme/tools", "twelve", HEAD, "0"],
|
|
153
|
+
],
|
|
154
|
+
)
|
|
155
|
+
def test_should_refuse_an_open_pass_it_cannot_record(
|
|
156
|
+
all_arguments: list[str], receipt_folder: Path
|
|
157
|
+
) -> None:
|
|
158
|
+
with pytest.raises(SystemExit):
|
|
159
|
+
merge_review_receipt.main(all_arguments, StringIO())
|
|
160
|
+
assert list(receipt_folder.iterdir()) == []
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
@pytest.mark.parametrize("head", ["abc", "abc1234", "../../escape", "zzzzzzz"])
|
|
164
|
+
def test_should_refuse_an_open_pass_whose_head_is_not_a_commit(head: str, receipt_folder: Path) -> None:
|
|
165
|
+
with pytest.raises(SystemExit):
|
|
166
|
+
merge_review_receipt.main(["acme/tools", "open", head, "0"], StringIO())
|
|
167
|
+
assert list(receipt_folder.iterdir()) == []
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
@pytest.mark.parametrize("slug", ["owner-repo", "a/b/../c", "a/.."])
|
|
171
|
+
@pytest.mark.parametrize("number", ["open", "12"])
|
|
172
|
+
def test_should_refuse_a_slug_that_is_not_owner_and_name(
|
|
173
|
+
slug: str, number: str, receipt_folder: Path
|
|
174
|
+
) -> None:
|
|
175
|
+
with pytest.raises(SystemExit):
|
|
176
|
+
merge_review_receipt.main([slug, number, HEAD, "0"], StringIO())
|
|
177
|
+
assert list(receipt_folder.iterdir()) == []
|