claude-dev-env 8.6.0 → 8.7.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/.agents/skills/privacy-hygiene/reference/sweep-procedure.md +1 -1
- package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +1 -0
- package/_shared/advisor/scripts/model_tier_run_validator.py +3 -2
- package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +35 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/terminology_sweep_constants.py +4 -0
- package/_shared/pr-loop/scripts/terminology_sweep.py +155 -18
- package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +130 -0
- package/bin/install.test.mjs +25 -0
- package/docs/high-trust-agent-delivery.md +132 -0
- package/hooks/advisory/refactor_guard.py +2 -2
- package/hooks/advisory/test_refactor_guard.py +21 -0
- package/hooks/blocking/code_rules_enforcer_test_support.py +10 -0
- package/hooks/blocking/code_rules_shared.py +4 -7
- package/hooks/blocking/conftest.py +22 -0
- package/hooks/blocking/state_description_blocker.py +232 -90
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body.py +2 -11
- package/hooks/blocking/test_code_rules_enforcer_duplicate_body_hook_routing.py +27 -54
- package/hooks/blocking/test_code_rules_enforcer_test_support.py +21 -0
- package/hooks/blocking/test_code_rules_enforcer_zero_payload_alias_hook_routing.py +3 -19
- package/hooks/blocking/test_code_rules_shared.py +18 -0
- package/hooks/blocking/test_conftest.py +35 -0
- package/hooks/blocking/test_state_description_blocker.py +59 -0
- package/hooks/git-hooks/git_hooks_constants/__init__.py +1 -14
- package/hooks/git-hooks/pull_request_handoff.py +2 -2
- package/hooks/git-hooks/test_config.py +10 -3
- package/hooks/git-hooks/test_pull_request_handoff.py +49 -0
- package/hooks/git-hooks/test_verification_notice_state.py +50 -0
- package/hooks/git-hooks/verification_notice_state.py +3 -1
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +13 -5
- package/hooks/hooks_constants/refactor_guard_constants.py +2 -0
- package/hooks/hooks_constants/session_edit_stage_gate_constants.py +3 -4
- package/hooks/hooks_constants/state_description_blocker_constants.py +2 -1
- package/hooks/hooks_constants/test_state_description_blocker_constants.py +87 -0
- package/hooks/validators/exempt_paths.py +3 -18
- package/hooks/validators/test_exempt_paths.py +20 -0
- package/hooks/validators/test_python_antipattern_checks.py +14 -0
- package/package.json +1 -1
- package/rules/agent-merges-its-own-green-pull-request.md +54 -0
- package/rules/archiving-agent-config.md +10 -0
- package/rules/code-standards.md +1 -0
- package/rules/correction-lens.md +134 -0
- package/rules/flag-non-breaking-findings.md +11 -0
- package/rules/no-contrast-framing.md +68 -0
- package/rules/review-closure-is-a-check.md +44 -0
- package/scripts/Invoke-PesterSuites.ps1 +42 -0
- package/scripts/agent_merge_check.py +381 -0
- package/scripts/check.ps1 +12 -1
- package/scripts/ci/install-playtest.mjs +291 -0
- package/scripts/ci/install-playtest.test.mjs +84 -0
- package/scripts/ci/installer-lifecycle.mjs +11 -30
- package/scripts/ci/installer-lifecycle.test.mjs +14 -0
- package/scripts/ci/pstack-command-shim.test.mjs +33 -0
- package/scripts/ci/scratch-home-install.mjs +73 -0
- package/scripts/ci/scratch-home-install.test.mjs +49 -0
- package/scripts/codex_compat_materializer.py +33 -16
- package/scripts/contrast_framing.py +101 -0
- package/scripts/dev_env_scripts_constants/agent_merge_check_constants.py +121 -0
- package/scripts/dev_env_scripts_constants/contrast_framing_constants.py +82 -0
- package/scripts/dev_env_scripts_constants/merge_gate_check_constants.py +23 -0
- package/scripts/dev_env_scripts_constants/review_closure_constants.py +241 -0
- package/scripts/dev_env_scripts_constants/test_code_review_constants.py +12 -0
- package/scripts/dev_env_scripts_constants/test_contrast_framing_constants.py +60 -0
- package/scripts/durable_post_lint.py +30 -4
- package/scripts/followup_cli.py +6 -10
- package/scripts/merge_gate_checks.py +236 -0
- package/scripts/policy_lint/adapter_contrast_framing.py +59 -0
- package/scripts/policy_lint/adapter_detectors.py +1 -1
- package/scripts/policy_lint/adapter_support.py +3 -14
- package/scripts/policy_lint/adapters.py +30 -1
- package/scripts/policy_lint/config/constants.py +2 -0
- package/scripts/policy_lint/registry.py +5 -0
- package/scripts/repository_checks/config/constants.py +12 -0
- package/scripts/repository_checks/config/policy_document.py +28 -2
- package/scripts/repository_checks/followups.py +54 -0
- package/scripts/repository_checks/match_exemptions.py +67 -0
- package/scripts/repository_checks/models.py +46 -2
- package/scripts/repository_checks/reporting.py +23 -7
- package/scripts/repository_checks/tracked_secrets.py +2 -2
- package/scripts/repository_policy.py +4 -2
- package/scripts/review_closure.py +134 -0
- package/scripts/review_closure_github.py +345 -0
- package/scripts/review_closure_model.py +360 -0
- package/scripts/test_agent_merge_check.py +317 -0
- package/scripts/test_durable_post_lint.py +46 -0
- package/scripts/test_followup_cli.py +40 -0
- package/scripts/test_merge_gate_checks.py +92 -0
- package/scripts/tests/test_code_review_constants.py +78 -0
- package/scripts/tests/test_codex_compat_materializer.py +48 -4
- package/scripts/tests/test_contrast_framing.py +117 -0
- package/scripts/tests/test_followups.py +88 -0
- package/scripts/tests/test_match_exemptions.py +291 -0
- package/scripts/tests/test_models.py +67 -2
- package/scripts/tests/test_package_inventory.py +6 -4
- package/scripts/tests/test_pester_suites_run.py +53 -0
- package/scripts/tests/test_policy_document.py +39 -1
- package/scripts/tests/test_policy_lint_rules_contrast_framing.py +68 -0
- package/scripts/tests/test_policy_lint_rules_engine.py +4 -1
- package/scripts/tests/test_reporting.py +18 -0
- package/scripts/tests/test_repository_policy.py +43 -1
- package/scripts/tests/test_review_closure.py +126 -0
- package/scripts/tests/test_review_closure_github.py +240 -0
- package/scripts/tests/test_review_closure_model.py +299 -0
- package/scripts/tests/test_tracked_secrets.py +42 -0
- package/hooks/hooks_constants/precommit_code_rules_gate_constants.py +0 -12
- package/scripts/repository_checks/email_exemptions.py +0 -39
- package/scripts/tests/test_email_exemptions.py +0 -158
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Report whether the agent driving a pull request may merge it now.
|
|
3
|
+
|
|
4
|
+
An agent that drives a pull request in this repository merges it once the
|
|
5
|
+
repository's own gate passes and no review thread is open. This command reads
|
|
6
|
+
that state from GitHub and prints one verdict line, so the decision rests on
|
|
7
|
+
the pull request's live state rather than on the agent's reading of it.
|
|
8
|
+
|
|
9
|
+
Usage::
|
|
10
|
+
|
|
11
|
+
python3 agent_merge_check.py jl-cmd/claude-dev-env 1442
|
|
12
|
+
-> MERGE jl-cmd/claude-dev-env#1442 ab845eb :: green, no open review
|
|
13
|
+
thread, ready for the agent to merge
|
|
14
|
+
|
|
15
|
+
Exit status is 0 for MERGE, 1 for HOLD, and 2 when the state could not be
|
|
16
|
+
read.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import functools
|
|
23
|
+
import json
|
|
24
|
+
import os
|
|
25
|
+
import sys
|
|
26
|
+
import time
|
|
27
|
+
import urllib.error
|
|
28
|
+
import urllib.request
|
|
29
|
+
from collections.abc import Callable, Mapping, Sequence
|
|
30
|
+
|
|
31
|
+
from dev_env_scripts_constants.agent_merge_check_constants import (
|
|
32
|
+
ACCEPT_HEADER,
|
|
33
|
+
ALL_HOLD_REASONS_BY_STATE,
|
|
34
|
+
ALL_OUTDATED_KEYS,
|
|
35
|
+
ALL_RESOLVED_KEYS,
|
|
36
|
+
ALL_THREAD_NODE_KEYS,
|
|
37
|
+
ALL_TOKEN_ENVIRONMENT_VARIABLES,
|
|
38
|
+
AUTHORIZATION_HEADER,
|
|
39
|
+
BEARER_PREFIX,
|
|
40
|
+
COMMAND_DESCRIPTION,
|
|
41
|
+
CONTENT_TYPE_HEADER,
|
|
42
|
+
DRAFT_HOLD_REASON,
|
|
43
|
+
DRAFT_KEY,
|
|
44
|
+
ERROR_EXIT_CODE,
|
|
45
|
+
GITHUB_ACCEPT_TYPE,
|
|
46
|
+
GITHUB_API_ROOT,
|
|
47
|
+
GITHUB_GRAPHQL_ENDPOINT,
|
|
48
|
+
HEAD_KEY,
|
|
49
|
+
HOLD_EXIT_CODE,
|
|
50
|
+
HOLD_VERDICT_LABEL,
|
|
51
|
+
JSON_CONTENT_TYPE,
|
|
52
|
+
MERGE_EXIT_CODE,
|
|
53
|
+
MERGE_VERDICT_LABEL,
|
|
54
|
+
MERGEABLE_STATE_CLEAN,
|
|
55
|
+
MERGEABLE_STATE_KEY,
|
|
56
|
+
MERGEABLE_STATE_UNKNOWN,
|
|
57
|
+
NAME_VARIABLE,
|
|
58
|
+
NO_SIGN_IN_MESSAGE,
|
|
59
|
+
NUMBER_ARGUMENT_HELP,
|
|
60
|
+
NUMBER_KEY,
|
|
61
|
+
NUMBER_VARIABLE,
|
|
62
|
+
OWNER_VARIABLE,
|
|
63
|
+
PAGE_SIZE_VARIABLE,
|
|
64
|
+
PULL_REQUEST_ENDPOINT_TEMPLATE,
|
|
65
|
+
QUERY_KEY,
|
|
66
|
+
READY_DETAIL,
|
|
67
|
+
REQUEST_TIMEOUT_SECONDS,
|
|
68
|
+
REVIEW_THREAD_PAGE_SIZE,
|
|
69
|
+
REVIEW_THREADS_ENDPOINT_TEMPLATE,
|
|
70
|
+
SETTLE_ATTEMPT_COUNT,
|
|
71
|
+
SETTLE_WAIT_SECONDS,
|
|
72
|
+
SHA_KEY,
|
|
73
|
+
SHORT_SHA_LENGTH,
|
|
74
|
+
SLUG_ARGUMENT_HELP,
|
|
75
|
+
SLUG_SEPARATOR,
|
|
76
|
+
UNKNOWN_STATE_HOLD_TEMPLATE,
|
|
77
|
+
UNRESOLVED_THREAD_QUERY,
|
|
78
|
+
UNRESOLVED_THREADS_HOLD_TEMPLATE,
|
|
79
|
+
UTF8_ENCODING,
|
|
80
|
+
VARIABLES_KEY,
|
|
81
|
+
VERDICT_LINE_TEMPLATE,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class MergeCheckError(Exception):
|
|
86
|
+
"""Raised when the pull request state could not be read."""
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def hold_reason(
|
|
90
|
+
all_pull_request_fields: Mapping[str, object],
|
|
91
|
+
unresolved_thread_count: int,
|
|
92
|
+
) -> str | None:
|
|
93
|
+
"""Return why this pull request stays open, or None when it may merge.
|
|
94
|
+
|
|
95
|
+
Args:
|
|
96
|
+
all_pull_request_fields: The pull request as the GitHub API reports
|
|
97
|
+
it, carrying its draft flag and its merge state.
|
|
98
|
+
unresolved_thread_count: How many review threads are open on it.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
The reason text for a draft, for a merge state other than clean, and
|
|
102
|
+
for an open review thread. None when the pull request is ready for
|
|
103
|
+
the agent that drives it to merge it.
|
|
104
|
+
"""
|
|
105
|
+
if all_pull_request_fields.get(DRAFT_KEY):
|
|
106
|
+
return DRAFT_HOLD_REASON
|
|
107
|
+
merge_state = all_pull_request_fields.get(MERGEABLE_STATE_KEY)
|
|
108
|
+
if merge_state != MERGEABLE_STATE_CLEAN:
|
|
109
|
+
return ALL_HOLD_REASONS_BY_STATE.get(
|
|
110
|
+
merge_state,
|
|
111
|
+
UNKNOWN_STATE_HOLD_TEMPLATE.format(state=merge_state),
|
|
112
|
+
)
|
|
113
|
+
if unresolved_thread_count > 0:
|
|
114
|
+
return UNRESOLVED_THREADS_HOLD_TEMPLATE.format(count=unresolved_thread_count)
|
|
115
|
+
return None
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def verdict_line(
|
|
119
|
+
slug: str,
|
|
120
|
+
all_pull_request_fields: Mapping[str, object],
|
|
121
|
+
reason: str | None,
|
|
122
|
+
) -> str:
|
|
123
|
+
"""Build the one line this command prints for a pull request.
|
|
124
|
+
|
|
125
|
+
Args:
|
|
126
|
+
slug: The repository as ``owner/name``.
|
|
127
|
+
all_pull_request_fields: The pull request the verdict describes.
|
|
128
|
+
reason: The hold reason, or None for a pull request that may merge.
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
A line naming the verdict, the pull request, its head commit, and
|
|
132
|
+
either the hold reason or the ready detail.
|
|
133
|
+
"""
|
|
134
|
+
all_head_fields = all_pull_request_fields.get(HEAD_KEY, {})
|
|
135
|
+
head_sha = ""
|
|
136
|
+
if isinstance(all_head_fields, Mapping):
|
|
137
|
+
head_sha = str(all_head_fields.get(SHA_KEY, ""))[:SHORT_SHA_LENGTH]
|
|
138
|
+
return VERDICT_LINE_TEMPLATE.format(
|
|
139
|
+
label=HOLD_VERDICT_LABEL if reason else MERGE_VERDICT_LABEL,
|
|
140
|
+
slug=slug,
|
|
141
|
+
number=all_pull_request_fields.get(NUMBER_KEY),
|
|
142
|
+
sha=head_sha,
|
|
143
|
+
detail=reason or READY_DETAIL,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def count_unresolved_threads(all_thread_records: Sequence[object]) -> int:
|
|
148
|
+
"""Count the review threads that still wait on an answer.
|
|
149
|
+
|
|
150
|
+
Args:
|
|
151
|
+
all_thread_records: The review threads GitHub reports for the pull
|
|
152
|
+
request.
|
|
153
|
+
|
|
154
|
+
Returns:
|
|
155
|
+
How many of them are unresolved and still point at live code. An
|
|
156
|
+
outdated thread names code the pull request has since replaced, so it
|
|
157
|
+
holds nothing back.
|
|
158
|
+
"""
|
|
159
|
+
return sum(
|
|
160
|
+
1
|
|
161
|
+
for each_thread in all_thread_records
|
|
162
|
+
if isinstance(each_thread, Mapping)
|
|
163
|
+
and not _any_flag(each_thread, ALL_RESOLVED_KEYS)
|
|
164
|
+
and not _any_flag(each_thread, ALL_OUTDATED_KEYS)
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _any_flag(all_thread_fields: Mapping[str, object], all_keys: Sequence[str]) -> bool:
|
|
169
|
+
return any(all_thread_fields.get(each_key) for each_key in all_keys)
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _github_token() -> str:
|
|
173
|
+
for each_variable in ALL_TOKEN_ENVIRONMENT_VARIABLES:
|
|
174
|
+
token = os.environ.get(each_variable)
|
|
175
|
+
if token:
|
|
176
|
+
return token
|
|
177
|
+
raise MergeCheckError(NO_SIGN_IN_MESSAGE)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _request_json(
|
|
181
|
+
url: str,
|
|
182
|
+
token: str,
|
|
183
|
+
all_payload_fields: Mapping[str, object] | None,
|
|
184
|
+
) -> object:
|
|
185
|
+
request = urllib.request.Request(url)
|
|
186
|
+
request.add_header(ACCEPT_HEADER, GITHUB_ACCEPT_TYPE)
|
|
187
|
+
request.add_header(AUTHORIZATION_HEADER, BEARER_PREFIX + token)
|
|
188
|
+
body = None
|
|
189
|
+
if all_payload_fields is not None:
|
|
190
|
+
request.add_header(CONTENT_TYPE_HEADER, JSON_CONTENT_TYPE)
|
|
191
|
+
body = json.dumps(all_payload_fields).encode(UTF8_ENCODING)
|
|
192
|
+
try:
|
|
193
|
+
with urllib.request.urlopen(
|
|
194
|
+
request,
|
|
195
|
+
data=body,
|
|
196
|
+
timeout=REQUEST_TIMEOUT_SECONDS,
|
|
197
|
+
) as answer:
|
|
198
|
+
return json.loads(answer.read().decode(UTF8_ENCODING))
|
|
199
|
+
except (urllib.error.URLError, ValueError) as failure:
|
|
200
|
+
raise MergeCheckError(str(failure)) from failure
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def read_pull_request(slug: str, number: int, token: str) -> Mapping[str, object]:
|
|
204
|
+
"""Read one pull request from the GitHub REST API.
|
|
205
|
+
|
|
206
|
+
Args:
|
|
207
|
+
slug: The repository as ``owner/name``.
|
|
208
|
+
number: The pull request number.
|
|
209
|
+
token: The GitHub token the request authenticates with.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
The pull request fields, carrying its draft flag, its merge state,
|
|
213
|
+
and its head commit.
|
|
214
|
+
|
|
215
|
+
Raises:
|
|
216
|
+
MergeCheckError: The API call failed, or answered with something
|
|
217
|
+
other than a pull request object.
|
|
218
|
+
"""
|
|
219
|
+
document = _request_json(
|
|
220
|
+
PULL_REQUEST_ENDPOINT_TEMPLATE.format(
|
|
221
|
+
api_root=GITHUB_API_ROOT,
|
|
222
|
+
slug=slug,
|
|
223
|
+
number=number,
|
|
224
|
+
),
|
|
225
|
+
token,
|
|
226
|
+
None,
|
|
227
|
+
)
|
|
228
|
+
if not isinstance(document, Mapping):
|
|
229
|
+
raise MergeCheckError(str(document))
|
|
230
|
+
return document
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def read_unresolved_thread_count(slug: str, number: int, token: str) -> int:
|
|
234
|
+
"""Read how many review threads on a pull request stay open.
|
|
235
|
+
|
|
236
|
+
Two routes carry the same fact. A Claude Code session reaches GitHub
|
|
237
|
+
through a proxy that refuses GraphQL and serves the review threads over a
|
|
238
|
+
REST route of its own, so this reads that route first and falls back to
|
|
239
|
+
the GraphQL query every other caller has.
|
|
240
|
+
|
|
241
|
+
Args:
|
|
242
|
+
slug: The repository as ``owner/name``.
|
|
243
|
+
number: The pull request number.
|
|
244
|
+
token: The GitHub token the request authenticates with.
|
|
245
|
+
|
|
246
|
+
Returns:
|
|
247
|
+
The count of unresolved, current review threads.
|
|
248
|
+
|
|
249
|
+
Raises:
|
|
250
|
+
MergeCheckError: Both routes failed, or both answered with a shape
|
|
251
|
+
that carries no review threads.
|
|
252
|
+
"""
|
|
253
|
+
all_thread_records = _thread_records_over_rest(slug, number, token)
|
|
254
|
+
if all_thread_records is None:
|
|
255
|
+
return _read_unresolved_thread_count_over_graphql(slug, number, token)
|
|
256
|
+
return count_unresolved_threads(all_thread_records)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _thread_records_over_rest(
|
|
260
|
+
slug: str,
|
|
261
|
+
number: int,
|
|
262
|
+
token: str,
|
|
263
|
+
) -> list[object] | None:
|
|
264
|
+
try:
|
|
265
|
+
document = _request_json(
|
|
266
|
+
REVIEW_THREADS_ENDPOINT_TEMPLATE.format(
|
|
267
|
+
api_root=GITHUB_API_ROOT,
|
|
268
|
+
slug=slug,
|
|
269
|
+
number=number,
|
|
270
|
+
),
|
|
271
|
+
token,
|
|
272
|
+
None,
|
|
273
|
+
)
|
|
274
|
+
except MergeCheckError:
|
|
275
|
+
return None
|
|
276
|
+
return document if isinstance(document, list) else None
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
def _thread_query_payload(owner: str, name: str, number: int) -> dict[str, object]:
|
|
280
|
+
return {
|
|
281
|
+
QUERY_KEY: UNRESOLVED_THREAD_QUERY,
|
|
282
|
+
VARIABLES_KEY: {
|
|
283
|
+
OWNER_VARIABLE: owner,
|
|
284
|
+
NAME_VARIABLE: name,
|
|
285
|
+
NUMBER_VARIABLE: number,
|
|
286
|
+
PAGE_SIZE_VARIABLE: REVIEW_THREAD_PAGE_SIZE,
|
|
287
|
+
},
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
def _read_unresolved_thread_count_over_graphql(
|
|
292
|
+
slug: str,
|
|
293
|
+
number: int,
|
|
294
|
+
token: str,
|
|
295
|
+
) -> int:
|
|
296
|
+
owner, _, name = slug.partition(SLUG_SEPARATOR)
|
|
297
|
+
document = _request_json(
|
|
298
|
+
GITHUB_GRAPHQL_ENDPOINT,
|
|
299
|
+
token,
|
|
300
|
+
_thread_query_payload(owner, name, number),
|
|
301
|
+
)
|
|
302
|
+
all_thread_records = functools.reduce(
|
|
303
|
+
functools.partial(_field_at, document),
|
|
304
|
+
ALL_THREAD_NODE_KEYS,
|
|
305
|
+
document,
|
|
306
|
+
)
|
|
307
|
+
if not isinstance(all_thread_records, Sequence):
|
|
308
|
+
raise MergeCheckError(str(document))
|
|
309
|
+
return count_unresolved_threads(all_thread_records)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def _field_at(document: object, all_fields: object, key: str) -> object:
|
|
313
|
+
if not isinstance(all_fields, Mapping):
|
|
314
|
+
raise MergeCheckError(str(document))
|
|
315
|
+
return all_fields.get(key)
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def read_settled_pull_request(
|
|
319
|
+
slug: str,
|
|
320
|
+
number: int,
|
|
321
|
+
token: str,
|
|
322
|
+
sleep: Callable[[float], None],
|
|
323
|
+
) -> Mapping[str, object]:
|
|
324
|
+
"""Read a pull request whose merge state GitHub has worked out.
|
|
325
|
+
|
|
326
|
+
GitHub reports ``unknown`` while it computes mergeability after a push,
|
|
327
|
+
so this reads again until the state settles.
|
|
328
|
+
|
|
329
|
+
Args:
|
|
330
|
+
slug: The repository as ``owner/name``.
|
|
331
|
+
number: The pull request number.
|
|
332
|
+
token: The GitHub token the request authenticates with.
|
|
333
|
+
sleep: How the caller waits between reads.
|
|
334
|
+
|
|
335
|
+
Returns:
|
|
336
|
+
The fields the last read reported.
|
|
337
|
+
|
|
338
|
+
Raises:
|
|
339
|
+
MergeCheckError: A read failed.
|
|
340
|
+
"""
|
|
341
|
+
for each_attempt in range(SETTLE_ATTEMPT_COUNT):
|
|
342
|
+
all_pull_request_fields = read_pull_request(slug, number, token)
|
|
343
|
+
if all_pull_request_fields.get(MERGEABLE_STATE_KEY) != MERGEABLE_STATE_UNKNOWN:
|
|
344
|
+
return all_pull_request_fields
|
|
345
|
+
if each_attempt + 1 < SETTLE_ATTEMPT_COUNT:
|
|
346
|
+
sleep(SETTLE_WAIT_SECONDS)
|
|
347
|
+
return all_pull_request_fields
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def main(all_arguments: Sequence[str]) -> int:
|
|
351
|
+
"""Print the merge verdict for one pull request.
|
|
352
|
+
|
|
353
|
+
Args:
|
|
354
|
+
all_arguments: The command line, without the program name.
|
|
355
|
+
|
|
356
|
+
Returns:
|
|
357
|
+
Zero when the pull request may merge, one when it holds, and two
|
|
358
|
+
when its state could not be read.
|
|
359
|
+
"""
|
|
360
|
+
parser = argparse.ArgumentParser(description=COMMAND_DESCRIPTION)
|
|
361
|
+
parser.add_argument("slug", help=SLUG_ARGUMENT_HELP)
|
|
362
|
+
parser.add_argument("number", type=int, help=NUMBER_ARGUMENT_HELP)
|
|
363
|
+
parsed = parser.parse_args(all_arguments)
|
|
364
|
+
try:
|
|
365
|
+
token = _github_token()
|
|
366
|
+
all_pull_request_fields = read_settled_pull_request(
|
|
367
|
+
parsed.slug, parsed.number, token, time.sleep
|
|
368
|
+
)
|
|
369
|
+
unresolved_thread_count = read_unresolved_thread_count(
|
|
370
|
+
parsed.slug, parsed.number, token
|
|
371
|
+
)
|
|
372
|
+
except MergeCheckError as failure:
|
|
373
|
+
print(failure, file=sys.stderr)
|
|
374
|
+
return ERROR_EXIT_CODE
|
|
375
|
+
reason = hold_reason(all_pull_request_fields, unresolved_thread_count)
|
|
376
|
+
print(verdict_line(parsed.slug, all_pull_request_fields, reason))
|
|
377
|
+
return HOLD_EXIT_CODE if reason else MERGE_EXIT_CODE
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
if __name__ == "__main__":
|
|
381
|
+
raise SystemExit(main(sys.argv[1:]))
|
package/scripts/check.ps1
CHANGED
|
@@ -30,13 +30,16 @@
|
|
|
30
30
|
.PARAMETER SkipRepositoryPolicy
|
|
31
31
|
Skip the committed-tree repository_policy.py run.
|
|
32
32
|
|
|
33
|
+
.PARAMETER SkipPester
|
|
34
|
+
Skip the Pester run over the PowerShell suites in scripts/tests.
|
|
35
|
+
|
|
33
36
|
.PARAMETER CommentPolicyBase
|
|
34
37
|
Optional git reference used as the comment-policy baseline.
|
|
35
38
|
|
|
36
39
|
.OUTPUTS
|
|
37
40
|
Per-tool status lines on stdout. Final summary line:
|
|
38
41
|
CHECK: OK
|
|
39
|
-
CHECK: FAILED tools=ruff,repository-policy,mypy,mypy-pr-loop,mypy-process-tree,pytest
|
|
42
|
+
CHECK: FAILED tools=ruff,repository-policy,mypy,mypy-pr-loop,mypy-process-tree,pytest,pester
|
|
40
43
|
#>
|
|
41
44
|
[CmdletBinding()]
|
|
42
45
|
param(
|
|
@@ -44,6 +47,7 @@ param(
|
|
|
44
47
|
[switch]$SkipMypy,
|
|
45
48
|
[switch]$SkipRuff,
|
|
46
49
|
[switch]$SkipRepositoryPolicy,
|
|
50
|
+
[switch]$SkipPester,
|
|
47
51
|
[string]$CommentPolicyBase
|
|
48
52
|
)
|
|
49
53
|
|
|
@@ -54,6 +58,7 @@ $repositoryRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '..' '..')
|
|
|
54
58
|
$blockingRoot = Join-Path $hooksRoot 'blocking'
|
|
55
59
|
$prLoopScriptsRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '_shared' 'pr-loop' 'scripts')
|
|
56
60
|
$processTreeScriptsRoot = Resolve-Path (Join-Path $PSScriptRoot '..' '_shared' 'process-tree' 'scripts')
|
|
61
|
+
$pesterTestsRoot = Resolve-Path (Join-Path $PSScriptRoot 'tests')
|
|
57
62
|
|
|
58
63
|
$failedTools = @()
|
|
59
64
|
$firstNonZeroExitCode = 0
|
|
@@ -188,6 +193,12 @@ if (-not $SkipTests) {
|
|
|
188
193
|
}
|
|
189
194
|
}
|
|
190
195
|
|
|
196
|
+
if (-not $SkipPester) {
|
|
197
|
+
Invoke-Tool -Label 'pester' -Action {
|
|
198
|
+
pwsh -NoProfile -File (Join-Path $PSScriptRoot 'Invoke-PesterSuites.ps1') -TestsRoot $pesterTestsRoot
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
191
202
|
Write-Host ""
|
|
192
203
|
if ($failedTools.Count -eq 0) {
|
|
193
204
|
Write-Host "CHECK: OK" -ForegroundColor Green
|