claude-dev-env 8.19.1 → 8.20.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.mjs +29 -0
- package/bin/install.settings-defaults.test.mjs +53 -0
- package/bin/merge_settings_defaults.mjs +50 -0
- package/package.json +1 -1
- package/rules/agent-merges-its-own-green-pull-request.md +2 -1
- package/rules/review-closure-is-a-check.md +2 -0
- package/scripts/agent_merge_check.py +508 -9
- package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +89 -0
- package/scripts/dev_env_scripts_constants/review_closure_constants.py +6 -0
- package/scripts/review_closure_model.py +25 -5
- package/scripts/test_agent_merge_check.py +467 -0
- package/scripts/tests/test_review_closure.py +1 -0
- package/scripts/tests/test_review_closure_model.py +49 -4
- package/settings.json +2 -1
|
@@ -24,8 +24,44 @@ MERGEABLE_STATE_KEY = "mergeable_state"
|
|
|
24
24
|
NUMBER_KEY = "number"
|
|
25
25
|
TITLE_KEY = "title"
|
|
26
26
|
HEAD_KEY = "head"
|
|
27
|
+
BASE_KEY = "base"
|
|
28
|
+
REF_KEY = "ref"
|
|
27
29
|
SHA_KEY = "sha"
|
|
28
30
|
|
|
31
|
+
BRANCH_RULES_ENDPOINT_TEMPLATE = "{api_root}/repos/{slug}/rules/branches/{branch}"
|
|
32
|
+
CHECK_RUNS_ENDPOINT_TEMPLATE = (
|
|
33
|
+
"{api_root}/repos/{slug}/commits/{sha}/check-runs?{query}"
|
|
34
|
+
)
|
|
35
|
+
COMBINED_STATUS_ENDPOINT_TEMPLATE = (
|
|
36
|
+
"{api_root}/repos/{slug}/commits/{sha}/status?{query}"
|
|
37
|
+
)
|
|
38
|
+
COMPARE_ENDPOINT_TEMPLATE = "{api_root}/repos/{slug}/compare/{base}...{head}"
|
|
39
|
+
CHECK_NAME_PARAMETER = "check_name"
|
|
40
|
+
PER_PAGE_PARAMETER = "per_page"
|
|
41
|
+
CHECK_PAGE_SIZE = 100
|
|
42
|
+
|
|
43
|
+
RULE_TYPE_KEY = "type"
|
|
44
|
+
RULE_PARAMETERS_KEY = "parameters"
|
|
45
|
+
REQUIRED_STATUS_CHECKS_RULE_TYPE = "required_status_checks"
|
|
46
|
+
MERGE_QUEUE_RULE_TYPE = "merge_queue"
|
|
47
|
+
REQUIRED_STATUS_CHECKS_KEY = "required_status_checks"
|
|
48
|
+
CONTEXT_KEY = "context"
|
|
49
|
+
INTEGRATION_ID_KEY = "integration_id"
|
|
50
|
+
CHECK_RUNS_KEY = "check_runs"
|
|
51
|
+
CHECK_RUN_NAME_KEY = "name"
|
|
52
|
+
CHECK_RUN_ID_KEY = "id"
|
|
53
|
+
CHECK_RUN_APP_KEY = "app"
|
|
54
|
+
CHECK_RUN_APP_ID_KEY = "id"
|
|
55
|
+
CHECK_RUN_STATUS_KEY = "status"
|
|
56
|
+
CHECK_RUN_CONCLUSION_KEY = "conclusion"
|
|
57
|
+
CHECK_RUN_COMPLETED_STATUS = "completed"
|
|
58
|
+
ALL_PASSING_CHECK_CONCLUSIONS = frozenset({"success", "neutral", "skipped"})
|
|
59
|
+
STATUSES_KEY = "statuses"
|
|
60
|
+
STATUS_STATE_KEY = "state"
|
|
61
|
+
SUCCESS_STATUS_STATE = "success"
|
|
62
|
+
PENDING_STATUS_STATE = "pending"
|
|
63
|
+
BEHIND_BY_KEY = "behind_by"
|
|
64
|
+
|
|
29
65
|
MERGEABLE_STATE_CLEAN = "clean"
|
|
30
66
|
MERGEABLE_STATE_BEHIND = "behind"
|
|
31
67
|
MERGEABLE_STATE_DIRTY = "dirty"
|
|
@@ -61,6 +97,19 @@ ALL_HOLD_REASONS_BY_STATE = {
|
|
|
61
97
|
MERGEABLE_STATE_BLOCKED: BLOCKED_HOLD_REASON,
|
|
62
98
|
MERGEABLE_STATE_UNSTABLE: UNSTABLE_HOLD_REASON,
|
|
63
99
|
}
|
|
100
|
+
FAILING_REQUIRED_CHECKS_HOLD_TEMPLATE = (
|
|
101
|
+
"A required check is not passing on this head: {checks}. Read the "
|
|
102
|
+
"failing check, fix it, and push."
|
|
103
|
+
)
|
|
104
|
+
REQUIRED_CHECK_STATE_TEMPLATE = "{context} ({state})"
|
|
105
|
+
REQUIRED_CHECK_SEPARATOR = ", "
|
|
106
|
+
MISSING_CHECK_STATE = "missing"
|
|
107
|
+
PENDING_CHECK_STATE = "pending"
|
|
108
|
+
BEHIND_MERGE_QUEUE_HOLD_TEMPLATE = (
|
|
109
|
+
"Every required check passes, but the head is {count} commit(s) behind "
|
|
110
|
+
"the base branch and the merge queue will not take it. Merge the base "
|
|
111
|
+
"branch into this one and push."
|
|
112
|
+
)
|
|
64
113
|
UNKNOWN_STATE_HOLD_TEMPLATE = (
|
|
65
114
|
"GitHub reports the merge state as {state}, which this check does not "
|
|
66
115
|
"treat as ready. Read the pull request page."
|
|
@@ -68,6 +117,13 @@ UNKNOWN_STATE_HOLD_TEMPLATE = (
|
|
|
68
117
|
UNRESOLVED_THREADS_HOLD_TEMPLATE = (
|
|
69
118
|
"{count} review thread(s) are open. Answer each one and resolve it."
|
|
70
119
|
)
|
|
120
|
+
MERGE_QUEUE_EJECTION_HOLD_TEMPLATE = (
|
|
121
|
+
"The merge queue ejected this head for failed checks, and no commit has "
|
|
122
|
+
"landed since. Read the merge_group run for the "
|
|
123
|
+
"gh-readonly-queue/{base}/pr-{number}-<sha> branch, fix the failure, and "
|
|
124
|
+
"push before you enqueue it again."
|
|
125
|
+
)
|
|
126
|
+
FAILED_CHECKS_REMOVAL_REASON = "failed_checks"
|
|
71
127
|
|
|
72
128
|
MERGE_VERDICT_LABEL = "MERGE"
|
|
73
129
|
HOLD_VERDICT_LABEL = "HOLD"
|
|
@@ -99,6 +155,19 @@ query($owner: String!, $name: String!, $number: Int!, $pageSize: Int!) {
|
|
|
99
155
|
}
|
|
100
156
|
}
|
|
101
157
|
"""
|
|
158
|
+
MERGE_QUEUE_REMOVAL_PAGE_SIZE = 100
|
|
159
|
+
MERGE_QUEUE_REMOVAL_QUERY = """
|
|
160
|
+
query($owner: String!, $name: String!, $number: Int!, $pageSize: Int!) {
|
|
161
|
+
repository(owner: $owner, name: $name) {
|
|
162
|
+
pullRequest(number: $number) {
|
|
163
|
+
commits(last: 1) { nodes { commit { committedDate } } }
|
|
164
|
+
timelineItems(last: $pageSize, itemTypes: [REMOVED_FROM_MERGE_QUEUE_EVENT]) {
|
|
165
|
+
nodes { ... on RemovedFromMergeQueueEvent { createdAt reason } }
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
"""
|
|
102
171
|
QUERY_KEY = "query"
|
|
103
172
|
VARIABLES_KEY = "variables"
|
|
104
173
|
OWNER_VARIABLE = "owner"
|
|
@@ -117,5 +186,25 @@ ALL_THREAD_NODE_KEYS = (
|
|
|
117
186
|
REVIEW_THREADS_KEY,
|
|
118
187
|
NODES_KEY,
|
|
119
188
|
)
|
|
189
|
+
COMMITS_KEY = "commits"
|
|
190
|
+
COMMIT_KEY = "commit"
|
|
191
|
+
COMMITTED_DATE_KEY = "committedDate"
|
|
192
|
+
TIMELINE_ITEMS_KEY = "timelineItems"
|
|
193
|
+
CREATED_AT_KEY = "createdAt"
|
|
194
|
+
REMOVAL_REASON_KEY = "reason"
|
|
195
|
+
ALL_MERGE_QUEUE_REMOVAL_NODE_KEYS = (
|
|
196
|
+
DATA_KEY,
|
|
197
|
+
REPOSITORY_KEY,
|
|
198
|
+
PULL_REQUEST_KEY,
|
|
199
|
+
TIMELINE_ITEMS_KEY,
|
|
200
|
+
NODES_KEY,
|
|
201
|
+
)
|
|
202
|
+
ALL_HEAD_COMMIT_NODE_KEYS = (
|
|
203
|
+
DATA_KEY,
|
|
204
|
+
REPOSITORY_KEY,
|
|
205
|
+
PULL_REQUEST_KEY,
|
|
206
|
+
COMMITS_KEY,
|
|
207
|
+
NODES_KEY,
|
|
208
|
+
)
|
|
120
209
|
ALL_RESOLVED_KEYS = ("isResolved", "is_resolved", "resolved")
|
|
121
210
|
ALL_OUTDATED_KEYS = ("isOutdated", "is_outdated", "outdated")
|
|
@@ -131,6 +131,12 @@ NUMBER_KEY: str = "number"
|
|
|
131
131
|
USER_KEY: str = "user"
|
|
132
132
|
"""Field carrying the account that opened a pull request."""
|
|
133
133
|
|
|
134
|
+
USER_TYPE_KEY: str = "type"
|
|
135
|
+
"""Field on a REST user naming whether the account is a person or a bot."""
|
|
136
|
+
|
|
137
|
+
BOT_USER_TYPE: str = "Bot"
|
|
138
|
+
"""The user type GitHub reports for an app account such as a review bot."""
|
|
139
|
+
|
|
134
140
|
SHORT_SHA_LENGTH: int = 7
|
|
135
141
|
"""How much of a commit identifier the verdict line prints."""
|
|
136
142
|
|
|
@@ -13,10 +13,14 @@ and the approvals conclusion, and name what waits.
|
|
|
13
13
|
thread: a red circle, resolved in silence -> open
|
|
14
14
|
top-level: a bot comment, no agent comment -> open
|
|
15
15
|
top-level: a bot comment, then an agent one -> closed
|
|
16
|
-
top-level: the bot edits after that reply ->
|
|
16
|
+
top-level: the bot edits after that reply -> closed
|
|
17
|
+
top-level: a person edits after that reply -> open
|
|
17
18
|
|
|
18
19
|
A red circle marks a finding a review states as blocking, so resolution alone
|
|
19
20
|
leaves it open. The reply says what changed, or why the finding stands.
|
|
21
|
+
A review bot rewrites its summary comment on each pass, and any new finding it
|
|
22
|
+
has arrives as a review thread or a new comment, so its edit alone reopens
|
|
23
|
+
nothing.
|
|
20
24
|
"""
|
|
21
25
|
|
|
22
26
|
from __future__ import annotations
|
|
@@ -34,6 +38,7 @@ from dev_env_scripts_constants.review_closure_constants import (
|
|
|
34
38
|
APPROVALS_CHECK_NAME,
|
|
35
39
|
APPROVALS_OPEN_REASON,
|
|
36
40
|
AUTHOR_LOGIN_KEY,
|
|
41
|
+
BOT_USER_TYPE,
|
|
37
42
|
CHECK_RUN_CONCLUSION_KEY,
|
|
38
43
|
CHECK_RUN_NAME_KEY,
|
|
39
44
|
CLOSED_DETAIL,
|
|
@@ -58,6 +63,7 @@ from dev_env_scripts_constants.review_closure_constants import (
|
|
|
58
63
|
UNNAMED_THREAD_SUBJECT,
|
|
59
64
|
UPDATED_AT_KEY,
|
|
60
65
|
USER_KEY,
|
|
66
|
+
USER_TYPE_KEY,
|
|
61
67
|
VERDICT_LINE_TEMPLATE,
|
|
62
68
|
)
|
|
63
69
|
|
|
@@ -86,6 +92,7 @@ class TopLevelComment:
|
|
|
86
92
|
|
|
87
93
|
identifier: object
|
|
88
94
|
author_login: str
|
|
95
|
+
is_bot: bool
|
|
89
96
|
created_at: datetime
|
|
90
97
|
updated_at: datetime
|
|
91
98
|
url: str
|
|
@@ -212,7 +219,8 @@ def top_level_finding(
|
|
|
212
219
|
"""
|
|
213
220
|
if comment.author_login in all_driver_logins:
|
|
214
221
|
return None
|
|
215
|
-
|
|
222
|
+
last_counted_at = comment.created_at if comment.is_bot else comment.updated_at
|
|
223
|
+
if latest_driver_time is not None and last_counted_at <= latest_driver_time:
|
|
216
224
|
return None
|
|
217
225
|
return OpenFinding(
|
|
218
226
|
subject=comment.url,
|
|
@@ -228,15 +236,18 @@ def top_level_findings(
|
|
|
228
236
|
|
|
229
237
|
The driving agent answers a top-level comment by posting a top-level
|
|
230
238
|
comment of its own. Its latest one answers every comment last touched
|
|
231
|
-
before it
|
|
239
|
+
before it. A person's comment edited after it waits again. A bot's edit
|
|
240
|
+
leaves its comment answered, since a bot posts each new finding as a
|
|
241
|
+
review thread or a new comment.
|
|
232
242
|
|
|
233
243
|
Args:
|
|
234
244
|
all_comments: The top-level comments on the pull request.
|
|
235
245
|
all_driver_logins: The logins whose comments count as the answer.
|
|
236
246
|
|
|
237
247
|
Returns:
|
|
238
|
-
One finding per comment from another account
|
|
239
|
-
|
|
248
|
+
One finding per comment from another account posted after the
|
|
249
|
+
driving agent's latest top-level comment, and one per person's
|
|
250
|
+
comment edited after it.
|
|
240
251
|
"""
|
|
241
252
|
all_listed_comments = tuple(all_comments)
|
|
242
253
|
latest_driver_time = latest_driver_comment_time(
|
|
@@ -424,6 +435,7 @@ def parse_top_level_comment(
|
|
|
424
435
|
return TopLevelComment(
|
|
425
436
|
identifier=all_comment_fields.get(COMMENT_IDENTIFIER_KEY),
|
|
426
437
|
author_login=_comment_author_login(all_comment_fields),
|
|
438
|
+
is_bot=_is_bot_author(all_comment_fields),
|
|
427
439
|
created_at=datetime.fromisoformat(str(all_comment_fields.get(CREATED_AT_KEY))),
|
|
428
440
|
updated_at=datetime.fromisoformat(str(all_comment_fields.get(UPDATED_AT_KEY))),
|
|
429
441
|
url=str(all_comment_fields.get(HTML_URL_KEY) or ""),
|
|
@@ -477,6 +489,14 @@ def _parse_comment(all_comment_fields: Mapping[str, object]) -> ReviewComment:
|
|
|
477
489
|
)
|
|
478
490
|
|
|
479
491
|
|
|
492
|
+
def _is_bot_author(all_comment_fields: Mapping[str, object]) -> bool:
|
|
493
|
+
all_author_fields = all_comment_fields.get(USER_KEY)
|
|
494
|
+
return (
|
|
495
|
+
isinstance(all_author_fields, Mapping)
|
|
496
|
+
and all_author_fields.get(USER_TYPE_KEY) == BOT_USER_TYPE
|
|
497
|
+
)
|
|
498
|
+
|
|
499
|
+
|
|
480
500
|
def _comment_author_login(all_comment_fields: Mapping[str, object]) -> str:
|
|
481
501
|
for each_key in ALL_COMMENT_AUTHOR_KEYS:
|
|
482
502
|
all_author_fields = all_comment_fields.get(each_key)
|