claude-dev-env 7.0.0 → 7.2.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/pull-request/scripts/pull_request.py +2 -0
- package/.agents/skills/pull-request/scripts/test_pull_request.py +34 -0
- package/.agents/skills/session-continuity/AGENTS.md +1 -0
- package/.agents/skills/session-continuity/CLAUDE.md +1 -0
- package/.agents/skills/session-continuity/README.md +213 -0
- package/.agents/skills/session-continuity/SKILL.md +125 -0
- package/.agents/skills/session-continuity/continuity.mjs +447 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +373 -0
- package/.agents/skills/syncing-submodules/scripts/test_sync_parent_pointer.py +0 -7
- package/.agents/skills/windows-scheduled-task/SKILL.md +153 -0
- package/bin/cde.mjs +36 -8
- package/bin/cde.test.mjs +48 -0
- package/bin/ever-shipped-skills.mjs +2 -20
- package/bin/ever-shipped-skills.test.mjs +8 -0
- package/bin/expand_home_directory_tokens.mjs +4 -5
- package/bin/expand_home_directory_tokens.test.mjs +128 -0
- package/bin/git_hooks_installer.mjs +22 -3
- package/bin/git_hooks_installer.test.mjs +159 -5
- package/bin/install-session-continuity.mjs +88 -0
- package/bin/install-session-continuity.test.mjs +55 -0
- package/bin/install.codex-rules.test.mjs +5 -2
- package/bin/install.cursor-rules.test.mjs +5 -2
- package/bin/install.mjs +478 -63
- package/bin/install.policy-lint.test.mjs +56 -4
- package/bin/install.profiles.test.mjs +10 -0
- package/bin/install.prune.test.mjs +227 -17
- package/bin/install.test.mjs +500 -120
- package/bin/install.transaction.test.mjs +3 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/code_rules_banned_identifiers.py +3 -0
- package/hooks/blocking/precommit_code_rules_gate.py +5 -8
- package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +7 -52
- package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -69
- package/hooks/blocking/test_code_rules_banned_identifiers.py +64 -0
- package/hooks/blocking/test_pre_tool_use_dispatcher.py +10 -244
- package/hooks/blocking/test_shared_stdin_adoption.py +1 -47
- package/hooks/git-hooks/git_hooks_constants/verification_notice_constants.py +116 -0
- package/hooks/git-hooks/post_commit.py +33 -0
- package/hooks/git-hooks/pre_commit.py +18 -90
- package/hooks/git-hooks/pre_push.py +45 -541
- package/hooks/git-hooks/test_installed_policy_lint.py +55 -6
- package/hooks/git-hooks/test_native_hook_support.py +129 -0
- package/hooks/git-hooks/test_native_policy_commit.py +39 -6
- package/hooks/git-hooks/test_post_commit.py +43 -0
- package/hooks/git-hooks/test_pre_commit.py +36 -392
- package/hooks/git-hooks/test_pre_push.py +48 -1631
- package/hooks/git-hooks/test_verification_notice.py +60 -0
- package/hooks/git-hooks/test_verification_start.py +173 -0
- package/hooks/git-hooks/verification_notice.py +192 -0
- package/hooks/git-hooks/verification_notice_context.py +253 -0
- package/hooks/git-hooks/verification_notice_state.py +237 -0
- package/hooks/git-hooks/verification_start.py +100 -0
- package/hooks/hooks.json +0 -119
- package/hooks/hooks_constants/banned_identifiers_constants.py +4 -0
- package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -15
- package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +0 -4
- package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -50
- package/hooks/hooks_constants/stop_dispatcher_constants.py +1 -4
- package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +3 -66
- package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +42 -185
- package/hooks/hooks_constants/test_stop_dispatcher_constants.py +3 -25
- package/hooks/validation/test_post_tool_use_dispatcher.py +35 -298
- package/hooks/validators/run_all_validators.py +5 -1
- package/hooks/validators/security_checks.py +57 -2
- package/hooks/validators/test_hook_timing_harness.py +2 -5
- package/hooks/validators/test_run_all_validators_mutation_tool_parity.py +3 -11
- package/hooks/validators/test_run_all_validators_policy_lint.py +36 -0
- package/hooks/validators/test_security_checks.py +57 -0
- package/package.json +1 -1
- package/rules/confirm-implementation-forks.md +2 -0
- package/rules/git-workflow.md +12 -0
- package/rules/pstack-host-mapping.md +48 -0
- package/rules/vault-context.md +3 -5
- package/rules/verify-runtime-state.md +7 -0
- package/scripts/audit-hooks-cli.mjs +5 -0
- package/scripts/audit-hooks.mjs +72 -16
- package/scripts/automatic_advisory/__init__.py +1 -0
- package/scripts/automatic_advisory/checkout.py +51 -0
- package/scripts/automatic_advisory/cli.py +334 -0
- package/scripts/automatic_advisory/config/__init__.py +1 -0
- package/scripts/automatic_advisory/config/constants.py +93 -0
- package/scripts/automatic_advisory/config/timing.py +4 -0
- package/scripts/automatic_advisory/configuration.py +197 -0
- package/scripts/automatic_advisory/execution.py +350 -0
- package/scripts/automatic_advisory/git.py +69 -0
- package/scripts/automatic_advisory/model.py +107 -0
- package/scripts/automatic_advisory/process_host.py +32 -0
- package/scripts/automatic_advisory/publisher.py +84 -0
- package/scripts/automatic_advisory/runner.py +399 -0
- package/scripts/automatic_advisory/state.py +116 -0
- package/scripts/automatic_advisory/window_flags.py +41 -0
- package/scripts/automatic_advisory/windows_job.py +122 -0
- package/scripts/codex_compat_materializer.py +99 -13
- package/scripts/durable_post_lint.py +30 -2
- package/scripts/durable_post_lint_config/config/constants.py +10 -0
- package/scripts/durable_post_lint_config/config/test_constants.py +13 -0
- package/scripts/local_report_cli.py +184 -0
- package/scripts/local_report_core.py +247 -0
- package/scripts/local_report_publisher.py +12 -0
- package/scripts/local_report_validation.py +357 -0
- package/scripts/local_verification/__init__.py +23 -0
- package/scripts/local_verification/check_runner.py +364 -0
- package/scripts/local_verification/check_support.py +99 -0
- package/scripts/local_verification/cli.py +158 -0
- package/scripts/local_verification/command_runner.py +84 -0
- package/scripts/local_verification/config/__init__.py +1 -0
- package/scripts/local_verification/config/constants.py +89 -0
- package/scripts/local_verification/config/timing.py +1 -0
- package/scripts/local_verification/git_state.py +146 -0
- package/scripts/local_verification/manifest.py +224 -0
- package/scripts/local_verification/model.py +174 -0
- package/scripts/local_verification/runner.py +234 -0
- package/scripts/policy_lint/adapter_pairing.py +58 -3
- package/scripts/policy_lint/config/approved_test_pairs.py +323 -0
- package/scripts/pr_verification/__init__.py +1 -0
- package/scripts/pr_verification/branch_refs.py +114 -0
- package/scripts/pr_verification/config/__init__.py +1 -0
- package/scripts/pr_verification/config/constants.py +84 -0
- package/scripts/pr_verification/config/timing.py +5 -0
- package/scripts/pr_verification/github.py +137 -0
- package/scripts/pr_verification/github_api.py +376 -0
- package/scripts/pr_verification/github_labels.py +88 -0
- package/scripts/pr_verification/github_parsing.py +182 -0
- package/scripts/pr_verification/github_transport.py +91 -0
- package/scripts/pr_verification/lock.py +122 -0
- package/scripts/pr_verification/model.py +43 -0
- package/scripts/pr_verification/requirements.txt +1 -0
- package/scripts/test_durable_post_lint.py +97 -0
- package/scripts/tests/test-audit-hooks-cli.mjs +150 -0
- package/scripts/tests/test-audit-hooks.mjs +94 -2
- package/scripts/tests/test_automatic_advisory.py +376 -0
- package/scripts/tests/test_automatic_advisory_job_cleanup.py +71 -0
- package/scripts/tests/test_automatic_advisory_polling.py +216 -0
- package/scripts/tests/test_automatic_advisory_recovery.py +320 -0
- package/scripts/tests/test_automatic_advisory_repairs.py +400 -0
- package/scripts/tests/test_automatic_advisory_settings.py +48 -0
- package/scripts/tests/test_automatic_advisory_window_flags.py +163 -0
- package/scripts/tests/test_closed_pr_label.py +157 -0
- package/scripts/tests/test_codex_compat_materializer.py +189 -6
- package/scripts/tests/test_github_labels.py +101 -0
- package/scripts/tests/test_github_transport.py +125 -0
- package/scripts/tests/test_local_report_publisher.py +396 -0
- package/scripts/tests/test_local_report_publisher_cli.py +85 -0
- package/scripts/tests/test_local_verification.py +348 -0
- package/scripts/tests/test_local_verification_cli.py +98 -0
- package/scripts/tests/test_policy_lint_rules_pairing.py +163 -0
- package/scripts/tests/test_pr_verification_github.py +392 -0
- package/scripts/tests/test_pr_verification_lock.py +168 -0
- package/scripts/tests/test_verification_notice.py +347 -0
- package/hooks/blocking/ask_user_question_shape_blocker.py +0 -83
- package/hooks/blocking/bot_mention_comment_blocker.py +0 -76
- package/hooks/blocking/claude_md_orphan_file_blocker.py +0 -129
- package/hooks/blocking/cursor_cli_python_misfire_blocker.py +0 -108
- package/hooks/blocking/destructive_command_blocker.py +0 -2073
- package/hooks/blocking/fable_spawn_gate.py +0 -187
- package/hooks/blocking/luna_fast_mode_gate.py +0 -160
- package/hooks/blocking/nas_ssh_binary_enforcer.py +0 -163
- package/hooks/blocking/orchestrator_refresh_reschedule_gate.py +0 -260
- package/hooks/blocking/package_inventory_stale_blocker.py +0 -107
- package/hooks/blocking/pii_prevention_blocker.py +0 -365
- package/hooks/blocking/piped_pytest_blocker.py +0 -1223
- package/hooks/blocking/question_to_user_enforcer.py +0 -130
- package/hooks/blocking/send_user_file_open_locally_blocker.py +0 -71
- package/hooks/blocking/sensitive_file_protector.py +0 -167
- package/hooks/blocking/session_edit_stage_gate.py +0 -684
- package/hooks/blocking/session_handoff_blocker.py +0 -182
- package/hooks/blocking/shell_substitution_blocker.py +0 -350
- package/hooks/blocking/test_ask_user_question_shape.py +0 -69
- package/hooks/blocking/test_bot_mention_comment_blocker.py +0 -131
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +0 -1277
- package/hooks/blocking/test_cursor_cli_python_misfire_blocker.py +0 -208
- package/hooks/blocking/test_destructive_command_blocker.py +0 -2024
- package/hooks/blocking/test_destructive_command_blocker_deny_mode.py +0 -105
- package/hooks/blocking/test_destructive_command_blocker_no_verify.py +0 -122
- package/hooks/blocking/test_destructive_command_blocker_patterns.py +0 -133
- package/hooks/blocking/test_fable_spawn_gate.py +0 -381
- package/hooks/blocking/test_inventory_deadlock_resolution.py +0 -154
- package/hooks/blocking/test_luna_fast_mode_gate.py +0 -211
- package/hooks/blocking/test_nas_ssh_binary_enforcer.py +0 -225
- package/hooks/blocking/test_orchestrator_refresh_reschedule_gate.py +0 -231
- package/hooks/blocking/test_package_inventory_stale_blocker.py +0 -382
- package/hooks/blocking/test_pii_prevention_blocker.py +0 -1222
- package/hooks/blocking/test_pii_prevention_windows_git_path.py +0 -41
- package/hooks/blocking/test_pii_write_surface_ephemeral.py +0 -221
- package/hooks/blocking/test_piped_pytest_blocker.py +0 -587
- package/hooks/blocking/test_precommit_code_rules_gate_native_owner.py +0 -233
- package/hooks/blocking/test_question_to_user_enforcer.py +0 -169
- package/hooks/blocking/test_send_user_file_open_locally_blocker.py +0 -118
- package/hooks/blocking/test_sensitive_file_protector.py +0 -365
- package/hooks/blocking/test_session_edit_stage_gate.py +0 -537
- package/hooks/blocking/test_session_edit_stage_gate_staging.py +0 -64
- package/hooks/blocking/test_session_edit_stage_gate_support.py +0 -204
- package/hooks/blocking/test_session_edit_stage_gate_tracker.py +0 -116
- package/hooks/blocking/test_session_handoff_blocker.py +0 -318
- package/hooks/blocking/test_shell_substitution_blocker.py +0 -298
- package/hooks/blocking/test_unscoped_search_blocker.py +0 -102
- package/hooks/blocking/test_write_existing_file_blocker.py +0 -122
- package/hooks/blocking/tests/test_pii_prevention_blocker.py +0 -259
- package/hooks/blocking/unscoped_search_blocker.py +0 -391
- package/hooks/blocking/write_existing_file_blocker.py +0 -124
- package/hooks/hooks_constants/bot_mention_comment_blocker_constants.py +0 -19
- package/hooks/hooks_constants/cursor_cli_python_misfire_blocker_constants.py +0 -79
- package/hooks/hooks_constants/fable_spawn_gate_constants.py +0 -61
- package/hooks/hooks_constants/luna_fast_mode_gate_constants.py +0 -45
- package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +0 -20
- package/hooks/hooks_constants/orchestrator_refresh_reschedule_gate_constants.py +0 -57
- package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +0 -17
- package/hooks/hooks_constants/sensitive_file_protector_constants.py +0 -40
- package/hooks/hooks_constants/session_handoff_blocker_constants.py +0 -10
- package/hooks/hooks_constants/shell_substitution_blocker_constants.py +0 -80
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import os
|
|
5
|
+
import signal
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
from typing import IO
|
|
11
|
+
|
|
12
|
+
from .config.constants import (
|
|
13
|
+
CHILD_BASE_FLAG,
|
|
14
|
+
CHILD_EXECUTOR_FLAG,
|
|
15
|
+
CHILD_OUTPUT_FLAG,
|
|
16
|
+
LOCAL_VERIFICATION_CLI_FILENAME,
|
|
17
|
+
PYTEST_AUTO_WORKERS_ENVIRONMENT_KEY,
|
|
18
|
+
PYTEST_AUTO_WORKERS_LIMIT,
|
|
19
|
+
PYTHON_MODULE_ARGUMENT,
|
|
20
|
+
PYTHONPATH_ENVIRONMENT_KEY,
|
|
21
|
+
SCOPED_VERIFICATION_SCRIPT_PATH,
|
|
22
|
+
WINDOWS_PLATFORM,
|
|
23
|
+
WINDOWS_PRIORITY_CLASS_ATTRIBUTE,
|
|
24
|
+
WINDOWS_PROCESS_HOST_MODULE,
|
|
25
|
+
WINDOWS_PROCESS_START_SIGNAL,
|
|
26
|
+
)
|
|
27
|
+
from .config.timing import CHILD_CLEANUP_TIMEOUT_SECONDS
|
|
28
|
+
from .model import AdvisoryRegistration, ChildOutcome
|
|
29
|
+
from .window_flags import hidden_window_creation_flags
|
|
30
|
+
from .windows_job import (
|
|
31
|
+
_assign_process,
|
|
32
|
+
_close_handle,
|
|
33
|
+
_create_kill_on_close_job,
|
|
34
|
+
_terminate_job,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass(frozen=True)
|
|
39
|
+
class OwnedChildProcess:
|
|
40
|
+
process: subprocess.Popen[str]
|
|
41
|
+
process_group_identifier: int | None
|
|
42
|
+
windows_job_handle: int | None
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def run_verification_child(
|
|
46
|
+
registration: AdvisoryRegistration,
|
|
47
|
+
base_ref: str,
|
|
48
|
+
timeout_seconds: float,
|
|
49
|
+
) -> ChildOutcome:
|
|
50
|
+
"""Run the existing local verification CLI with a bounded timeout.
|
|
51
|
+
|
|
52
|
+
Args:
|
|
53
|
+
registration: Checkout, manifest, and report paths for the pair.
|
|
54
|
+
base_ref: Dedicated fetched base revision.
|
|
55
|
+
timeout_seconds: Maximum child runtime.
|
|
56
|
+
|
|
57
|
+
Returns:
|
|
58
|
+
Captured child status and streams.
|
|
59
|
+
"""
|
|
60
|
+
scripts_directory = Path(__file__).resolve().parents[1]
|
|
61
|
+
all_arguments = _build_child_arguments(registration, base_ref, scripts_directory)
|
|
62
|
+
child_environment = _build_child_environment(registration, scripts_directory)
|
|
63
|
+
if not _clear_child_artifacts(registration):
|
|
64
|
+
return ChildOutcome(None, "", "child artifacts could not be cleared", False)
|
|
65
|
+
maybe_owned_child = _start_child_process(
|
|
66
|
+
all_arguments, registration, child_environment
|
|
67
|
+
)
|
|
68
|
+
if maybe_owned_child is None:
|
|
69
|
+
return ChildOutcome(None, "", "child process could not start", False)
|
|
70
|
+
return _collect_child_outcome(maybe_owned_child, timeout_seconds)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _build_child_arguments(
|
|
74
|
+
registration: AdvisoryRegistration,
|
|
75
|
+
base_ref: str,
|
|
76
|
+
scripts_directory: Path,
|
|
77
|
+
) -> tuple[str, ...]:
|
|
78
|
+
verification_cli_path = (
|
|
79
|
+
scripts_directory / "local_verification" / LOCAL_VERIFICATION_CLI_FILENAME
|
|
80
|
+
)
|
|
81
|
+
return (
|
|
82
|
+
sys.executable,
|
|
83
|
+
str(registration.checkout_path / SCOPED_VERIFICATION_SCRIPT_PATH),
|
|
84
|
+
CHILD_BASE_FLAG,
|
|
85
|
+
base_ref,
|
|
86
|
+
CHILD_EXECUTOR_FLAG,
|
|
87
|
+
str(verification_cli_path),
|
|
88
|
+
CHILD_OUTPUT_FLAG,
|
|
89
|
+
str(registration.report_path),
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def _build_child_environment(
|
|
94
|
+
registration: AdvisoryRegistration, scripts_directory: Path
|
|
95
|
+
) -> dict[str, str]:
|
|
96
|
+
child_environment = os.environ.copy()
|
|
97
|
+
all_python_paths = [str(registration.checkout_path), str(scripts_directory)]
|
|
98
|
+
existing_python_path = child_environment.get(PYTHONPATH_ENVIRONMENT_KEY)
|
|
99
|
+
if existing_python_path:
|
|
100
|
+
all_python_paths.append(existing_python_path)
|
|
101
|
+
child_environment[PYTHONPATH_ENVIRONMENT_KEY] = os.pathsep.join(all_python_paths)
|
|
102
|
+
child_environment[PYTEST_AUTO_WORKERS_ENVIRONMENT_KEY] = PYTEST_AUTO_WORKERS_LIMIT
|
|
103
|
+
return child_environment
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _clear_child_artifacts(registration: AdvisoryRegistration) -> bool:
|
|
107
|
+
try:
|
|
108
|
+
for each_artifact_path in (
|
|
109
|
+
registration.report_path,
|
|
110
|
+
registration.selected_manifest_path,
|
|
111
|
+
):
|
|
112
|
+
each_artifact_path.unlink(missing_ok=True)
|
|
113
|
+
except OSError:
|
|
114
|
+
return False
|
|
115
|
+
return True
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def _child_produced_report(
|
|
119
|
+
child_outcome: ChildOutcome,
|
|
120
|
+
registration: AdvisoryRegistration,
|
|
121
|
+
) -> bool:
|
|
122
|
+
if child_outcome.exit_code is None or child_outcome.timed_out:
|
|
123
|
+
return False
|
|
124
|
+
if not registration.selected_manifest_path.is_file():
|
|
125
|
+
return False
|
|
126
|
+
try:
|
|
127
|
+
report_fields = json.loads(registration.report_path.read_text(encoding="utf-8"))
|
|
128
|
+
except (OSError, json.JSONDecodeError):
|
|
129
|
+
return False
|
|
130
|
+
return (
|
|
131
|
+
isinstance(report_fields, dict)
|
|
132
|
+
and report_fields.get("exit_code") == child_outcome.exit_code
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _start_child_process(
|
|
137
|
+
all_arguments: tuple[str, ...],
|
|
138
|
+
registration: AdvisoryRegistration,
|
|
139
|
+
all_child_environment: dict[str, str],
|
|
140
|
+
) -> OwnedChildProcess | None:
|
|
141
|
+
if sys.platform == WINDOWS_PLATFORM:
|
|
142
|
+
return _start_windows_child(
|
|
143
|
+
all_arguments,
|
|
144
|
+
registration,
|
|
145
|
+
all_child_environment,
|
|
146
|
+
)
|
|
147
|
+
return _start_posix_child(
|
|
148
|
+
all_arguments,
|
|
149
|
+
registration,
|
|
150
|
+
all_child_environment,
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _start_posix_child(
|
|
155
|
+
all_arguments: tuple[str, ...],
|
|
156
|
+
registration: AdvisoryRegistration,
|
|
157
|
+
all_child_environment: dict[str, str],
|
|
158
|
+
) -> OwnedChildProcess | None:
|
|
159
|
+
try:
|
|
160
|
+
child_process = subprocess.Popen(
|
|
161
|
+
all_arguments,
|
|
162
|
+
cwd=registration.checkout_path,
|
|
163
|
+
stdout=subprocess.PIPE,
|
|
164
|
+
stderr=subprocess.PIPE,
|
|
165
|
+
text=True,
|
|
166
|
+
env=all_child_environment,
|
|
167
|
+
start_new_session=True,
|
|
168
|
+
)
|
|
169
|
+
except OSError:
|
|
170
|
+
return None
|
|
171
|
+
return OwnedChildProcess(child_process, child_process.pid, None)
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
def _start_windows_child(
|
|
175
|
+
all_arguments: tuple[str, ...],
|
|
176
|
+
registration: AdvisoryRegistration,
|
|
177
|
+
all_child_environment: dict[str, str],
|
|
178
|
+
) -> OwnedChildProcess | None:
|
|
179
|
+
maybe_job_handle = _create_kill_on_close_job()
|
|
180
|
+
if maybe_job_handle is None:
|
|
181
|
+
return None
|
|
182
|
+
maybe_child_process = _spawn_windows_process_host(
|
|
183
|
+
all_arguments,
|
|
184
|
+
registration,
|
|
185
|
+
all_child_environment,
|
|
186
|
+
)
|
|
187
|
+
if maybe_child_process is None:
|
|
188
|
+
_close_handle(maybe_job_handle)
|
|
189
|
+
return None
|
|
190
|
+
if not _assign_process(maybe_job_handle, maybe_child_process.pid):
|
|
191
|
+
_stop_unowned_process(maybe_child_process)
|
|
192
|
+
_close_handle(maybe_job_handle)
|
|
193
|
+
return None
|
|
194
|
+
if not _release_windows_process_host(maybe_child_process):
|
|
195
|
+
_terminate_job(maybe_job_handle)
|
|
196
|
+
_close_handle(maybe_job_handle)
|
|
197
|
+
return None
|
|
198
|
+
return OwnedChildProcess(maybe_child_process, None, maybe_job_handle)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def _spawn_windows_process_host(
|
|
202
|
+
all_arguments: tuple[str, ...],
|
|
203
|
+
registration: AdvisoryRegistration,
|
|
204
|
+
all_child_environment: dict[str, str],
|
|
205
|
+
) -> subprocess.Popen[str] | None:
|
|
206
|
+
try:
|
|
207
|
+
return subprocess.Popen(
|
|
208
|
+
(
|
|
209
|
+
sys.executable,
|
|
210
|
+
PYTHON_MODULE_ARGUMENT,
|
|
211
|
+
WINDOWS_PROCESS_HOST_MODULE,
|
|
212
|
+
*all_arguments,
|
|
213
|
+
),
|
|
214
|
+
cwd=registration.checkout_path,
|
|
215
|
+
stdin=subprocess.PIPE,
|
|
216
|
+
stdout=subprocess.PIPE,
|
|
217
|
+
stderr=subprocess.PIPE,
|
|
218
|
+
text=True,
|
|
219
|
+
env=all_child_environment,
|
|
220
|
+
creationflags=hidden_window_creation_flags()
|
|
221
|
+
| getattr(subprocess, WINDOWS_PRIORITY_CLASS_ATTRIBUTE, 0),
|
|
222
|
+
)
|
|
223
|
+
except OSError:
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _release_windows_process_host(child_process: subprocess.Popen[str]) -> bool:
|
|
228
|
+
stdin_stream = child_process.stdin
|
|
229
|
+
if stdin_stream is None:
|
|
230
|
+
return False
|
|
231
|
+
child_process.stdin = None
|
|
232
|
+
try:
|
|
233
|
+
stdin_stream.write(WINDOWS_PROCESS_START_SIGNAL)
|
|
234
|
+
stdin_stream.flush()
|
|
235
|
+
return True
|
|
236
|
+
except OSError:
|
|
237
|
+
return False
|
|
238
|
+
finally:
|
|
239
|
+
_close_stream_without_raising(stdin_stream)
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _close_stream_without_raising(stream: IO[str]) -> None:
|
|
243
|
+
try:
|
|
244
|
+
stream.close()
|
|
245
|
+
except OSError:
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def _collect_child_outcome(
|
|
250
|
+
owned_child: OwnedChildProcess,
|
|
251
|
+
timeout_seconds: float,
|
|
252
|
+
) -> ChildOutcome:
|
|
253
|
+
child_process = owned_child.process
|
|
254
|
+
try:
|
|
255
|
+
try:
|
|
256
|
+
stdout_text, stderr_text = child_process.communicate(
|
|
257
|
+
timeout=timeout_seconds
|
|
258
|
+
)
|
|
259
|
+
except subprocess.TimeoutExpired as timeout_error:
|
|
260
|
+
return _stop_timed_out_child(owned_child, timeout_error)
|
|
261
|
+
return ChildOutcome(
|
|
262
|
+
child_process.returncode,
|
|
263
|
+
stdout_text,
|
|
264
|
+
stderr_text,
|
|
265
|
+
False,
|
|
266
|
+
)
|
|
267
|
+
finally:
|
|
268
|
+
_close_process_ownership(owned_child)
|
|
269
|
+
_close_child_streams(child_process)
|
|
270
|
+
_reap_child_process(child_process)
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def _stop_timed_out_child(
|
|
274
|
+
owned_child: OwnedChildProcess,
|
|
275
|
+
timeout_error: subprocess.TimeoutExpired,
|
|
276
|
+
) -> ChildOutcome:
|
|
277
|
+
child_process = owned_child.process
|
|
278
|
+
_terminate_owned_process_tree(owned_child)
|
|
279
|
+
try:
|
|
280
|
+
stdout_text, stderr_text = child_process.communicate(
|
|
281
|
+
timeout=CHILD_CLEANUP_TIMEOUT_SECONDS
|
|
282
|
+
)
|
|
283
|
+
except subprocess.TimeoutExpired as cleanup_error:
|
|
284
|
+
stdout_text = str(cleanup_error.stdout or timeout_error.stdout or "")
|
|
285
|
+
stderr_text = str(cleanup_error.stderr or timeout_error.stderr or "")
|
|
286
|
+
return ChildOutcome(None, str(stdout_text), str(stderr_text), True)
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def _terminate_owned_process_tree(
|
|
290
|
+
owned_child: OwnedChildProcess,
|
|
291
|
+
) -> None:
|
|
292
|
+
if owned_child.windows_job_handle is not None:
|
|
293
|
+
_terminate_job(owned_child.windows_job_handle)
|
|
294
|
+
if owned_child.process_group_identifier is not None:
|
|
295
|
+
_kill_posix_process_group(owned_child.process_group_identifier)
|
|
296
|
+
_kill_direct_child(owned_child.process)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
def _kill_posix_process_group(process_group_identifier: int) -> None:
|
|
300
|
+
if sys.platform == "win32":
|
|
301
|
+
return
|
|
302
|
+
try:
|
|
303
|
+
os.killpg(process_group_identifier, signal.SIGKILL)
|
|
304
|
+
except OSError:
|
|
305
|
+
return
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
def _kill_direct_child(child_process: subprocess.Popen[str]) -> None:
|
|
309
|
+
if child_process.poll() is None:
|
|
310
|
+
try:
|
|
311
|
+
child_process.kill()
|
|
312
|
+
except ProcessLookupError:
|
|
313
|
+
pass
|
|
314
|
+
|
|
315
|
+
|
|
316
|
+
def _close_process_ownership(owned_child: OwnedChildProcess) -> None:
|
|
317
|
+
if owned_child.windows_job_handle is None:
|
|
318
|
+
return
|
|
319
|
+
_close_handle(owned_child.windows_job_handle)
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def _close_child_streams(child_process: subprocess.Popen[str]) -> None:
|
|
323
|
+
for each_stream in (
|
|
324
|
+
child_process.stdout,
|
|
325
|
+
child_process.stderr,
|
|
326
|
+
child_process.stdin,
|
|
327
|
+
):
|
|
328
|
+
if each_stream is None:
|
|
329
|
+
continue
|
|
330
|
+
try:
|
|
331
|
+
each_stream.close()
|
|
332
|
+
except OSError:
|
|
333
|
+
pass
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def _stop_unowned_process(child_process: subprocess.Popen[str]) -> None:
|
|
337
|
+
try:
|
|
338
|
+
child_process.kill()
|
|
339
|
+
except ProcessLookupError:
|
|
340
|
+
return
|
|
341
|
+
_reap_child_process(child_process)
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
def _reap_child_process(child_process: subprocess.Popen[str]) -> None:
|
|
345
|
+
if child_process.poll() is not None:
|
|
346
|
+
return
|
|
347
|
+
try:
|
|
348
|
+
child_process.wait(timeout=CHILD_CLEANUP_TIMEOUT_SECONDS)
|
|
349
|
+
except subprocess.TimeoutExpired:
|
|
350
|
+
return
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import subprocess
|
|
5
|
+
|
|
6
|
+
from pr_verification.model import PullRequestCandidate
|
|
7
|
+
|
|
8
|
+
from .config.constants import (
|
|
9
|
+
GIT_BASE_REFSPEC_TEMPLATE,
|
|
10
|
+
GIT_EXECUTABLE,
|
|
11
|
+
GIT_FETCH_ARGUMENT,
|
|
12
|
+
GIT_FORCE_FLAG,
|
|
13
|
+
GIT_NO_TAGS_FLAG,
|
|
14
|
+
GIT_PROMPT_DISABLED_VALUE,
|
|
15
|
+
GIT_REMOTE_BASE_REFSPEC_TEMPLATE,
|
|
16
|
+
GIT_TERMINAL_PROMPT_ENVIRONMENT_KEY,
|
|
17
|
+
)
|
|
18
|
+
from .config.timing import GIT_FETCH_TIMEOUT_SECONDS
|
|
19
|
+
from .model import AdvisoryRegistration
|
|
20
|
+
from .window_flags import hidden_window_creation_flags
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def _fetch_base(
|
|
24
|
+
registration: AdvisoryRegistration,
|
|
25
|
+
candidate: PullRequestCandidate,
|
|
26
|
+
) -> bool:
|
|
27
|
+
all_fetch_refspecs = _base_fetch_refspecs(registration, candidate)
|
|
28
|
+
try:
|
|
29
|
+
completed_process = subprocess.run(
|
|
30
|
+
(
|
|
31
|
+
GIT_EXECUTABLE,
|
|
32
|
+
GIT_FETCH_ARGUMENT,
|
|
33
|
+
GIT_FORCE_FLAG,
|
|
34
|
+
GIT_NO_TAGS_FLAG,
|
|
35
|
+
registration.remote_name,
|
|
36
|
+
*all_fetch_refspecs,
|
|
37
|
+
),
|
|
38
|
+
cwd=registration.checkout_path,
|
|
39
|
+
capture_output=True,
|
|
40
|
+
check=False,
|
|
41
|
+
env=_build_git_environment(),
|
|
42
|
+
creationflags=hidden_window_creation_flags(),
|
|
43
|
+
timeout=GIT_FETCH_TIMEOUT_SECONDS,
|
|
44
|
+
)
|
|
45
|
+
except subprocess.TimeoutExpired:
|
|
46
|
+
return False
|
|
47
|
+
return completed_process.returncode == 0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _base_fetch_refspecs(
|
|
51
|
+
registration: AdvisoryRegistration,
|
|
52
|
+
candidate: PullRequestCandidate,
|
|
53
|
+
) -> tuple[str, str]:
|
|
54
|
+
return (
|
|
55
|
+
GIT_BASE_REFSPEC_TEMPLATE.format(
|
|
56
|
+
base_ref=candidate.base_ref,
|
|
57
|
+
pull_request=registration.pull_request_number,
|
|
58
|
+
),
|
|
59
|
+
GIT_REMOTE_BASE_REFSPEC_TEMPLATE.format(
|
|
60
|
+
base_ref=candidate.base_ref,
|
|
61
|
+
remote_name=registration.remote_name,
|
|
62
|
+
),
|
|
63
|
+
)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _build_git_environment() -> dict[str, str]:
|
|
67
|
+
git_environment = os.environ.copy()
|
|
68
|
+
git_environment[GIT_TERMINAL_PROMPT_ENVIRONMENT_KEY] = GIT_PROMPT_DISABLED_VALUE
|
|
69
|
+
return git_environment
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from pathlib import Path, PurePosixPath
|
|
5
|
+
|
|
6
|
+
from pr_verification.model import PullRequestCandidate, RepositorySettings
|
|
7
|
+
|
|
8
|
+
from .config.constants import (
|
|
9
|
+
ALL_TERMINAL_STATUSES,
|
|
10
|
+
POLL_ERROR_LOG_FILE_NAME,
|
|
11
|
+
POLL_LOCK_DIRECTORY_NAME,
|
|
12
|
+
SELECTED_MANIFEST_SUFFIX,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@dataclass(frozen=True)
|
|
17
|
+
class AdvisoryRegistration:
|
|
18
|
+
repository: RepositorySettings
|
|
19
|
+
pull_request_number: int
|
|
20
|
+
checkout_path: Path
|
|
21
|
+
manifest_path: PurePosixPath
|
|
22
|
+
report_path: Path
|
|
23
|
+
state_path: Path
|
|
24
|
+
base_ref: str
|
|
25
|
+
remote_name: str
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def manifest_absolute_path(self) -> Path:
|
|
29
|
+
return self.checkout_path / Path(self.manifest_path)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def selected_manifest_path(self) -> Path:
|
|
33
|
+
return self.report_path.with_name(
|
|
34
|
+
self.report_path.stem + SELECTED_MANIFEST_SUFFIX
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
@property
|
|
38
|
+
def repository_lock_root(self) -> Path:
|
|
39
|
+
return self.state_path.parent
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@dataclass(frozen=True)
|
|
43
|
+
class AdvisorySettings:
|
|
44
|
+
api_url: str
|
|
45
|
+
app_id: int
|
|
46
|
+
installation_id: int
|
|
47
|
+
private_key_path: Path
|
|
48
|
+
poll_seconds: float
|
|
49
|
+
child_timeout_seconds: float
|
|
50
|
+
registrations: tuple[AdvisoryRegistration, ...]
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def poll_lock_root(self) -> Path:
|
|
54
|
+
return self.registrations[0].state_path.parent / POLL_LOCK_DIRECTORY_NAME
|
|
55
|
+
|
|
56
|
+
@property
|
|
57
|
+
def poll_error_log_path(self) -> Path:
|
|
58
|
+
return self.registrations[0].state_path.parent / POLL_ERROR_LOG_FILE_NAME
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass(frozen=True)
|
|
62
|
+
class AdvisoryState:
|
|
63
|
+
repository: str
|
|
64
|
+
pull_request_number: int
|
|
65
|
+
status: str
|
|
66
|
+
reason: str
|
|
67
|
+
head_sha: str | None
|
|
68
|
+
base_sha: str | None
|
|
69
|
+
last_poll: str
|
|
70
|
+
last_run: str | None
|
|
71
|
+
report_path: str
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@dataclass(frozen=True)
|
|
75
|
+
class ChildOutcome:
|
|
76
|
+
exit_code: int | None
|
|
77
|
+
stdout_text: str
|
|
78
|
+
stderr_text: str
|
|
79
|
+
timed_out: bool
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
@dataclass(frozen=True)
|
|
83
|
+
class LocalCheckout:
|
|
84
|
+
head_sha: str | None
|
|
85
|
+
is_clean: bool
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def candidate_identity_matches_state(
|
|
89
|
+
candidate: PullRequestCandidate,
|
|
90
|
+
maybe_state: AdvisoryState | None,
|
|
91
|
+
) -> bool:
|
|
92
|
+
"""Return whether a terminal state already covers the candidate identity.
|
|
93
|
+
|
|
94
|
+
Args:
|
|
95
|
+
candidate: Current remote pull request identity.
|
|
96
|
+
maybe_state: Recorded advisory state.
|
|
97
|
+
|
|
98
|
+
Returns:
|
|
99
|
+
True when the same head and base already reached a terminal state.
|
|
100
|
+
"""
|
|
101
|
+
if maybe_state is None:
|
|
102
|
+
return False
|
|
103
|
+
return (
|
|
104
|
+
maybe_state.head_sha == candidate.head_sha
|
|
105
|
+
and maybe_state.base_sha == candidate.base_sha
|
|
106
|
+
and maybe_state.status in ALL_TERMINAL_STATUSES
|
|
107
|
+
)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import subprocess
|
|
4
|
+
import sys
|
|
5
|
+
from collections.abc import Sequence
|
|
6
|
+
|
|
7
|
+
from pr_verification.config.constants import INCOMPLETE_EXIT_CODE
|
|
8
|
+
|
|
9
|
+
from .config.constants import WINDOWS_PROCESS_START_SIGNAL
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def run_owned_child(all_arguments: Sequence[str]) -> int:
|
|
13
|
+
"""Wait for ownership, then run the advisory child.
|
|
14
|
+
|
|
15
|
+
Args:
|
|
16
|
+
all_arguments: Child executable and arguments.
|
|
17
|
+
|
|
18
|
+
Returns:
|
|
19
|
+
Child exit code or the incomplete exit code when ownership is absent.
|
|
20
|
+
"""
|
|
21
|
+
if sys.stdin.read(1) != WINDOWS_PROCESS_START_SIGNAL:
|
|
22
|
+
return INCOMPLETE_EXIT_CODE
|
|
23
|
+
completed_process = subprocess.run(
|
|
24
|
+
tuple(all_arguments),
|
|
25
|
+
shell=False,
|
|
26
|
+
check=False,
|
|
27
|
+
)
|
|
28
|
+
return completed_process.returncode
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
if __name__ == "__main__":
|
|
32
|
+
raise SystemExit(run_owned_child(sys.argv[1:]))
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from importlib import import_module
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Protocol
|
|
8
|
+
|
|
9
|
+
from pr_verification.config.constants import STATUS_FAILURE, STATUS_SUCCESS
|
|
10
|
+
from pr_verification.model import RepositorySettings
|
|
11
|
+
|
|
12
|
+
from .config.constants import STATE_STATUS_FAILED, STATE_STATUS_PASSED
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Publication(Protocol):
|
|
16
|
+
@property
|
|
17
|
+
def status(self) -> str: ...
|
|
18
|
+
|
|
19
|
+
@property
|
|
20
|
+
def description(self) -> str: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class PublicationStatus(Protocol):
|
|
24
|
+
@property
|
|
25
|
+
def value(self) -> str: ...
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class PublisherOutcome(Protocol):
|
|
29
|
+
@property
|
|
30
|
+
def status(self) -> PublicationStatus: ...
|
|
31
|
+
|
|
32
|
+
@property
|
|
33
|
+
def description(self) -> str: ...
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass(frozen=True)
|
|
37
|
+
class PublicationRecord:
|
|
38
|
+
status: str
|
|
39
|
+
description: str
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _publish_report(
|
|
43
|
+
github: object,
|
|
44
|
+
repository: RepositorySettings,
|
|
45
|
+
pull_request_number: int,
|
|
46
|
+
checkout_path: Path,
|
|
47
|
+
manifest_path: Path,
|
|
48
|
+
report_path: Path,
|
|
49
|
+
) -> Publication:
|
|
50
|
+
publisher_module = import_module("local_report_publisher")
|
|
51
|
+
publisher_function: Callable[..., PublisherOutcome] = (
|
|
52
|
+
publisher_module.publish_local_report
|
|
53
|
+
)
|
|
54
|
+
publication_outcome = publisher_function(
|
|
55
|
+
github,
|
|
56
|
+
repository,
|
|
57
|
+
pull_request_number,
|
|
58
|
+
checkout_path,
|
|
59
|
+
manifest_path,
|
|
60
|
+
report_path,
|
|
61
|
+
)
|
|
62
|
+
publication_status = publication_outcome.status
|
|
63
|
+
publication_description = publication_outcome.description
|
|
64
|
+
return PublicationRecord(
|
|
65
|
+
_normalize_publication_status(publication_status),
|
|
66
|
+
publication_description,
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _normalize_publication_status(publication_status: PublicationStatus) -> str:
|
|
71
|
+
status_text = publication_status.value
|
|
72
|
+
if status_text == STATUS_SUCCESS:
|
|
73
|
+
return STATUS_SUCCESS
|
|
74
|
+
if status_text == STATUS_FAILURE:
|
|
75
|
+
return STATUS_FAILURE
|
|
76
|
+
return status_text
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def _cache_status_for_publication(publication_status: str) -> str:
|
|
80
|
+
if publication_status == STATUS_SUCCESS:
|
|
81
|
+
return STATE_STATUS_PASSED
|
|
82
|
+
if publication_status == STATUS_FAILURE:
|
|
83
|
+
return STATE_STATUS_FAILED
|
|
84
|
+
return publication_status
|