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,334 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
import subprocess
|
|
7
|
+
import sys
|
|
8
|
+
import time
|
|
9
|
+
from collections.abc import Callable, Sequence
|
|
10
|
+
from dataclasses import asdict
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import TextIO
|
|
13
|
+
|
|
14
|
+
scripts_directory = Path(__file__).resolve().parents[1]
|
|
15
|
+
if str(scripts_directory) not in sys.path:
|
|
16
|
+
sys.path.insert(0, str(scripts_directory))
|
|
17
|
+
|
|
18
|
+
from pr_verification.config.constants import INCOMPLETE_EXIT_CODE, SUCCESS_EXIT_CODE
|
|
19
|
+
from pr_verification.github import GitHubAppAuthenticator
|
|
20
|
+
from pr_verification.lock import SupervisorLock, SupervisorLockError
|
|
21
|
+
from pr_verification.model import RepositorySettings
|
|
22
|
+
|
|
23
|
+
from automatic_advisory.config.constants import (
|
|
24
|
+
POLL_ERROR_KEY,
|
|
25
|
+
POLL_ERROR_LOG_LINE_LIMIT,
|
|
26
|
+
REPORT_NEWLINE,
|
|
27
|
+
STATE_FILE_SUFFIX,
|
|
28
|
+
STATE_STATUS_KEY,
|
|
29
|
+
STATE_STATUS_NEVER_RUN,
|
|
30
|
+
UTF8_ENCODING,
|
|
31
|
+
)
|
|
32
|
+
from automatic_advisory.configuration import (
|
|
33
|
+
AdvisoryConfigurationError,
|
|
34
|
+
load_advisory_settings,
|
|
35
|
+
)
|
|
36
|
+
from automatic_advisory.model import AdvisorySettings, AdvisoryState
|
|
37
|
+
from automatic_advisory.runner import (
|
|
38
|
+
AdvisoryGitHub,
|
|
39
|
+
AutomaticAdvisoryRunner,
|
|
40
|
+
)
|
|
41
|
+
from automatic_advisory.window_flags import detached_poller_creation_flags
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
45
|
+
"""Build the automatic advisory command parser.
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
Parser for status, one-shot, and polling modes.
|
|
49
|
+
"""
|
|
50
|
+
parser = argparse.ArgumentParser(prog="cde automatic-advisory")
|
|
51
|
+
parser.add_argument("--settings", type=Path, required=True)
|
|
52
|
+
mode = parser.add_mutually_exclusive_group(required=True)
|
|
53
|
+
mode.add_argument("--once", action="store_true")
|
|
54
|
+
mode.add_argument("--poll", action="store_true")
|
|
55
|
+
mode.add_argument("--status", action="store_true")
|
|
56
|
+
mode.add_argument("--start", action="store_true")
|
|
57
|
+
parser.add_argument("--rerun", action="store_true")
|
|
58
|
+
return parser
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def main(
|
|
62
|
+
all_arguments: Sequence[str],
|
|
63
|
+
*,
|
|
64
|
+
stdout: TextIO = sys.stdout,
|
|
65
|
+
stderr: TextIO = sys.stderr,
|
|
66
|
+
sleeper: Callable[[float], None] = time.sleep,
|
|
67
|
+
) -> int:
|
|
68
|
+
"""Run the selected automatic advisory command.
|
|
69
|
+
|
|
70
|
+
Args:
|
|
71
|
+
all_arguments: Command arguments after the executable name.
|
|
72
|
+
stdout: Stream for state output.
|
|
73
|
+
stderr: Stream for configuration and runtime errors.
|
|
74
|
+
sleeper: Delay function used by polling mode.
|
|
75
|
+
|
|
76
|
+
Returns:
|
|
77
|
+
Zero for a completed command or three for an incomplete run.
|
|
78
|
+
"""
|
|
79
|
+
return _run_main_command(all_arguments, stdout, stderr, sleeper)
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def _run_main_command(
|
|
83
|
+
all_arguments: Sequence[str],
|
|
84
|
+
stdout: TextIO,
|
|
85
|
+
stderr: TextIO,
|
|
86
|
+
sleeper: Callable[[float], None],
|
|
87
|
+
) -> int:
|
|
88
|
+
try:
|
|
89
|
+
parsed_arguments, settings = _load_command_inputs(all_arguments)
|
|
90
|
+
return _run_selected_mode(parsed_arguments, settings, stdout, sleeper)
|
|
91
|
+
except (AdvisoryConfigurationError, OSError, SupervisorLockError) as error:
|
|
92
|
+
stderr.write(f"{error}{REPORT_NEWLINE}")
|
|
93
|
+
return INCOMPLETE_EXIT_CODE
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _load_command_inputs(
|
|
97
|
+
all_arguments: Sequence[str],
|
|
98
|
+
) -> tuple[argparse.Namespace, AdvisorySettings]:
|
|
99
|
+
parsed_arguments = build_parser().parse_args(list(all_arguments))
|
|
100
|
+
return parsed_arguments, load_advisory_settings(parsed_arguments.settings)
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def _run_selected_mode(
|
|
104
|
+
parsed_arguments: argparse.Namespace,
|
|
105
|
+
settings: AdvisorySettings,
|
|
106
|
+
stdout: TextIO,
|
|
107
|
+
sleeper: Callable[[float], None],
|
|
108
|
+
) -> int:
|
|
109
|
+
if parsed_arguments.status:
|
|
110
|
+
write_status(settings, stdout)
|
|
111
|
+
return SUCCESS_EXIT_CODE
|
|
112
|
+
if parsed_arguments.start:
|
|
113
|
+
return start_polling(parsed_arguments.settings)
|
|
114
|
+
advisory_runner = _build_runner(settings)
|
|
115
|
+
if parsed_arguments.once:
|
|
116
|
+
write_states(advisory_runner.run_once(parsed_arguments.rerun), stdout)
|
|
117
|
+
return SUCCESS_EXIT_CODE
|
|
118
|
+
with SupervisorLock(settings.poll_lock_root):
|
|
119
|
+
return run_polling(
|
|
120
|
+
advisory_runner,
|
|
121
|
+
settings.poll_seconds,
|
|
122
|
+
stdout,
|
|
123
|
+
settings.poll_error_log_path,
|
|
124
|
+
sleeper,
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def start_polling(settings_path: Path) -> int:
|
|
129
|
+
"""Start one detached polling process and return without waiting.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
settings_path: JSON settings passed to the detached process.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
Zero after the child process starts.
|
|
136
|
+
"""
|
|
137
|
+
process_flags = detached_poller_creation_flags()
|
|
138
|
+
resolved_settings_path = settings_path.resolve()
|
|
139
|
+
subprocess.Popen(
|
|
140
|
+
(
|
|
141
|
+
sys.executable,
|
|
142
|
+
str(Path(__file__).resolve()),
|
|
143
|
+
"--settings",
|
|
144
|
+
str(resolved_settings_path),
|
|
145
|
+
"--poll",
|
|
146
|
+
),
|
|
147
|
+
cwd=Path(__file__).resolve().parents[1],
|
|
148
|
+
stdin=subprocess.DEVNULL,
|
|
149
|
+
stdout=subprocess.DEVNULL,
|
|
150
|
+
stderr=subprocess.DEVNULL,
|
|
151
|
+
creationflags=process_flags,
|
|
152
|
+
start_new_session=True,
|
|
153
|
+
)
|
|
154
|
+
return SUCCESS_EXIT_CODE
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _build_runner(
|
|
158
|
+
settings: AdvisorySettings,
|
|
159
|
+
) -> AutomaticAdvisoryRunner:
|
|
160
|
+
authenticator = GitHubAppAuthenticator(
|
|
161
|
+
settings.api_url,
|
|
162
|
+
settings.app_id,
|
|
163
|
+
settings.installation_id,
|
|
164
|
+
settings.private_key_path,
|
|
165
|
+
)
|
|
166
|
+
return AutomaticAdvisoryRunner(
|
|
167
|
+
settings,
|
|
168
|
+
None,
|
|
169
|
+
github_factory=lambda repository: _issue_repository_api(
|
|
170
|
+
authenticator, repository
|
|
171
|
+
),
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
def _issue_repository_api(
|
|
176
|
+
authenticator: GitHubAppAuthenticator,
|
|
177
|
+
repository: RepositorySettings,
|
|
178
|
+
) -> AdvisoryGitHub:
|
|
179
|
+
return authenticator.issue_repository_api(
|
|
180
|
+
repository,
|
|
181
|
+
should_write_issue_labels=True,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def run_polling(
|
|
186
|
+
advisory_runner: AutomaticAdvisoryRunner,
|
|
187
|
+
poll_seconds: float,
|
|
188
|
+
stdout: TextIO,
|
|
189
|
+
poll_error_log_path: Path,
|
|
190
|
+
sleeper: Callable[[float], None],
|
|
191
|
+
) -> int:
|
|
192
|
+
"""Run advisory checks until the caller interrupts polling.
|
|
193
|
+
|
|
194
|
+
A SupervisorLockError or OSError in a cycle writes one poll_error JSON
|
|
195
|
+
line to stdout and to poll-errors.log, newest 200 lines, then the next
|
|
196
|
+
cycle runs. A detached poller sends stdout to DEVNULL, so the log file
|
|
197
|
+
beside the state files is the readable copy.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
advisory_runner: Runner for explicit checkout and pull request pairs.
|
|
201
|
+
poll_seconds: Delay between completed cycles.
|
|
202
|
+
stdout: Stream for each persisted state and each failed cycle.
|
|
203
|
+
poll_error_log_path: Log file receiving each failed cycle line.
|
|
204
|
+
sleeper: Delay function used between cycles.
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Zero after a user interrupt.
|
|
208
|
+
"""
|
|
209
|
+
try:
|
|
210
|
+
_repeat_poll_cycles(
|
|
211
|
+
advisory_runner, poll_seconds, stdout, poll_error_log_path, sleeper
|
|
212
|
+
)
|
|
213
|
+
except KeyboardInterrupt:
|
|
214
|
+
return SUCCESS_EXIT_CODE
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _repeat_poll_cycles(
|
|
218
|
+
advisory_runner: AutomaticAdvisoryRunner,
|
|
219
|
+
poll_seconds: float,
|
|
220
|
+
stdout: TextIO,
|
|
221
|
+
poll_error_log_path: Path,
|
|
222
|
+
sleeper: Callable[[float], None],
|
|
223
|
+
) -> None:
|
|
224
|
+
while True:
|
|
225
|
+
_run_one_poll_cycle(advisory_runner, stdout, poll_error_log_path)
|
|
226
|
+
sleeper(poll_seconds)
|
|
227
|
+
|
|
228
|
+
|
|
229
|
+
def _run_one_poll_cycle(
|
|
230
|
+
advisory_runner: AutomaticAdvisoryRunner,
|
|
231
|
+
stdout: TextIO,
|
|
232
|
+
poll_error_log_path: Path,
|
|
233
|
+
) -> None:
|
|
234
|
+
try:
|
|
235
|
+
write_states(advisory_runner.run_once(), stdout)
|
|
236
|
+
except (OSError, SupervisorLockError) as cycle_error:
|
|
237
|
+
_write_poll_error(cycle_error, stdout, poll_error_log_path)
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
def _write_poll_error(
|
|
241
|
+
cycle_error: Exception,
|
|
242
|
+
stdout: TextIO,
|
|
243
|
+
poll_error_log_path: Path,
|
|
244
|
+
) -> None:
|
|
245
|
+
error_line = (
|
|
246
|
+
json.dumps({POLL_ERROR_KEY: str(cycle_error)}, sort_keys=True) + REPORT_NEWLINE
|
|
247
|
+
)
|
|
248
|
+
stdout.write(error_line)
|
|
249
|
+
stdout.flush()
|
|
250
|
+
_append_poll_error_line(poll_error_log_path, error_line)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _append_poll_error_line(poll_error_log_path: Path, error_line: str) -> None:
|
|
254
|
+
"""Append one failed-cycle JSON line, keeping the newest 200 lines.
|
|
255
|
+
|
|
256
|
+
Skip the log on OSError or ValueError. A raise here would stop the poller.
|
|
257
|
+
The line already reached stdout. Undecodable bytes are replaced on read.
|
|
258
|
+
|
|
259
|
+
Args:
|
|
260
|
+
poll_error_log_path: Log file receiving the line.
|
|
261
|
+
error_line: JSON error line, newline included.
|
|
262
|
+
"""
|
|
263
|
+
try:
|
|
264
|
+
_replace_poll_error_log(poll_error_log_path, error_line)
|
|
265
|
+
except (OSError, ValueError):
|
|
266
|
+
return
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def _replace_poll_error_log(poll_error_log_path: Path, error_line: str) -> None:
|
|
270
|
+
poll_error_log_path.parent.mkdir(parents=True, exist_ok=True)
|
|
271
|
+
all_lines = _poll_error_log_lines(poll_error_log_path)
|
|
272
|
+
all_lines.append(error_line)
|
|
273
|
+
temporary_log_path = poll_error_log_path.with_name(
|
|
274
|
+
poll_error_log_path.name + STATE_FILE_SUFFIX
|
|
275
|
+
)
|
|
276
|
+
temporary_log_path.write_text(
|
|
277
|
+
"".join(all_lines[-POLL_ERROR_LOG_LINE_LIMIT:]), encoding=UTF8_ENCODING
|
|
278
|
+
)
|
|
279
|
+
os.replace(temporary_log_path, poll_error_log_path)
|
|
280
|
+
|
|
281
|
+
|
|
282
|
+
def _poll_error_log_lines(poll_error_log_path: Path) -> list[str]:
|
|
283
|
+
if not poll_error_log_path.exists():
|
|
284
|
+
return []
|
|
285
|
+
return poll_error_log_path.read_text(
|
|
286
|
+
encoding=UTF8_ENCODING, errors="replace"
|
|
287
|
+
).splitlines(keepends=True)
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
def write_states(
|
|
291
|
+
all_states: tuple[AdvisoryState, ...],
|
|
292
|
+
stdout: TextIO,
|
|
293
|
+
) -> None:
|
|
294
|
+
"""Write one JSON state line for each registration.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
all_states: Persisted registration states.
|
|
298
|
+
stdout: Stream receiving each JSON state line.
|
|
299
|
+
"""
|
|
300
|
+
stdout.writelines(
|
|
301
|
+
json.dumps(asdict(each_state), sort_keys=True) + REPORT_NEWLINE
|
|
302
|
+
for each_state in all_states
|
|
303
|
+
)
|
|
304
|
+
stdout.flush()
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def write_status(settings: AdvisorySettings, stdout: TextIO) -> None:
|
|
308
|
+
"""Write the latest state or never-run marker for each registration.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
settings: Explicit advisory registrations to inspect.
|
|
312
|
+
stdout: Stream receiving one JSON record per registration.
|
|
313
|
+
"""
|
|
314
|
+
for each_registration in settings.registrations:
|
|
315
|
+
state_path = each_registration.state_path
|
|
316
|
+
if not state_path.is_file():
|
|
317
|
+
stdout.write(
|
|
318
|
+
json.dumps(
|
|
319
|
+
{
|
|
320
|
+
"repository": each_registration.repository.slug,
|
|
321
|
+
"pull_request": each_registration.pull_request_number,
|
|
322
|
+
STATE_STATUS_KEY: STATE_STATUS_NEVER_RUN,
|
|
323
|
+
},
|
|
324
|
+
sort_keys=True,
|
|
325
|
+
)
|
|
326
|
+
+ REPORT_NEWLINE
|
|
327
|
+
)
|
|
328
|
+
continue
|
|
329
|
+
stdout.write(state_path.read_text(encoding="utf-8"))
|
|
330
|
+
stdout.flush()
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
if __name__ == "__main__":
|
|
334
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""Configuration for the automatic advisory runner."""
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
SETTINGS_VERSION = 1
|
|
4
|
+
SETTINGS_VERSION_KEY = "version"
|
|
5
|
+
SETTINGS_REGISTRATIONS_KEY = "registrations"
|
|
6
|
+
SETTINGS_API_URL_KEY = "api_url"
|
|
7
|
+
SETTINGS_APP_ID_KEY = "app_id"
|
|
8
|
+
SETTINGS_INSTALLATION_ID_KEY = "installation_id"
|
|
9
|
+
SETTINGS_SIGNING_FILE_FIELD = "private_" + "key_path"
|
|
10
|
+
SETTINGS_POLL_SECONDS_KEY = "poll_seconds"
|
|
11
|
+
SETTINGS_CHILD_TIMEOUT_SECONDS_KEY = "child_timeout_seconds"
|
|
12
|
+
REGISTRATION_REPOSITORY_KEY = "repository"
|
|
13
|
+
REGISTRATION_PULL_REQUEST_KEY = "pull_request"
|
|
14
|
+
REGISTRATION_CHECKOUT_KEY = "checkout"
|
|
15
|
+
REGISTRATION_MANIFEST_KEY = "manifest"
|
|
16
|
+
REGISTRATION_REPORT_KEY = "report"
|
|
17
|
+
REGISTRATION_STATE_KEY = "state"
|
|
18
|
+
REGISTRATION_BASE_REF_KEY = "base_ref"
|
|
19
|
+
REGISTRATION_REMOTE_KEY = "remote"
|
|
20
|
+
STATE_VERSION = 1
|
|
21
|
+
STATE_VERSION_KEY = "version"
|
|
22
|
+
STATE_REPOSITORY_KEY = "repository"
|
|
23
|
+
STATE_PULL_REQUEST_KEY = "pull_request"
|
|
24
|
+
STATE_STATUS_KEY = "status"
|
|
25
|
+
STATE_REASON_KEY = "reason"
|
|
26
|
+
STATE_HEAD_SHA_KEY = "head_sha"
|
|
27
|
+
STATE_BASE_SHA_KEY = "base_sha"
|
|
28
|
+
STATE_LAST_POLL_KEY = "last_poll"
|
|
29
|
+
STATE_LAST_RUN_KEY = "last_run"
|
|
30
|
+
STATE_REPORT_KEY = "report"
|
|
31
|
+
STATE_STATUS_WAITING = "waiting"
|
|
32
|
+
STATE_STATUS_OFFLINE = "offline"
|
|
33
|
+
STATE_STATUS_CLOSED = "closed"
|
|
34
|
+
STATE_STATUS_PASSED = "passed"
|
|
35
|
+
STATE_STATUS_FAILED = "failed"
|
|
36
|
+
STATE_STATUS_PENDING = "pending"
|
|
37
|
+
STATE_STATUS_ERROR = "error"
|
|
38
|
+
STATE_STATUS_NEVER_RUN = "never_run"
|
|
39
|
+
POLL_ERROR_KEY = "poll_error"
|
|
40
|
+
POLL_ERROR_LOG_FILE_NAME = "poll-errors.log"
|
|
41
|
+
POLL_ERROR_LOG_LINE_LIMIT = 200
|
|
42
|
+
ALL_TERMINAL_STATUSES = frozenset({STATE_STATUS_PASSED, STATE_STATUS_FAILED})
|
|
43
|
+
STATE_REASON_CHECKOUT_MISMATCH = "checkout does not match the open pull request"
|
|
44
|
+
STATE_REASON_DIRTY_CHECKOUT = "checkout has uncommitted changes"
|
|
45
|
+
STATE_REASON_PULL_REQUEST_CLOSED = "pull request is not open"
|
|
46
|
+
STATE_REASON_GITHUB_UNAVAILABLE = "GitHub is unavailable"
|
|
47
|
+
STATE_REASON_BASE_FETCH_FAILED = "base ref fetch failed"
|
|
48
|
+
STATE_REASON_PUBLISH_FAILED = "report publication failed"
|
|
49
|
+
STATE_REASON_CHILD_INCOMPLETE = "verification child did not produce a report"
|
|
50
|
+
STATE_REASON_UNCHANGED = "unchanged remote head and base"
|
|
51
|
+
GIT_EXECUTABLE = "git"
|
|
52
|
+
GIT_FETCH_ARGUMENT = "fetch"
|
|
53
|
+
GIT_NO_TAGS_FLAG = "--no-tags"
|
|
54
|
+
GIT_TERMINAL_PROMPT_ENVIRONMENT_KEY = "GIT_TERMINAL_PROMPT"
|
|
55
|
+
GIT_PROMPT_DISABLED_VALUE = "0"
|
|
56
|
+
GIT_FORCE_FLAG = "--force"
|
|
57
|
+
ALL_GIT_HEAD_ARGUMENTS = ("rev-parse", "HEAD")
|
|
58
|
+
ALL_GIT_STATUS_ARGUMENTS = ("status", "--porcelain=v1", "--untracked-files=all")
|
|
59
|
+
GIT_BASE_REFSPEC_TEMPLATE = (
|
|
60
|
+
"+refs/heads/{base_ref}:refs/cde/advisory-base/{pull_request}"
|
|
61
|
+
)
|
|
62
|
+
GIT_REMOTE_BASE_REFSPEC_TEMPLATE = (
|
|
63
|
+
"+refs/heads/{base_ref}:refs/remotes/{remote_name}/{base_ref}"
|
|
64
|
+
)
|
|
65
|
+
GIT_BASE_REF_TEMPLATE = "refs/cde/advisory-base/{pull_request}"
|
|
66
|
+
LOCAL_VERIFICATION_CLI_FILENAME = "cli.py"
|
|
67
|
+
WINDOWS_PRIORITY_CLASS_ATTRIBUTE = "BELOW_NORMAL_PRIORITY_CLASS"
|
|
68
|
+
WINDOWS_PLATFORM = "win32"
|
|
69
|
+
PYTHON_MODULE_ARGUMENT = "-m"
|
|
70
|
+
WINDOWS_PROCESS_HOST_MODULE = "automatic_advisory.process_host"
|
|
71
|
+
WINDOWS_PROCESS_START_SIGNAL = "1"
|
|
72
|
+
WINDOWS_PROCESS_SET_QUOTA_ACCESS = 0x0100
|
|
73
|
+
WINDOWS_PROCESS_TERMINATE_ACCESS = 0x0001
|
|
74
|
+
WINDOWS_EXTENDED_LIMIT_INFORMATION_CLASS = 9
|
|
75
|
+
WINDOWS_JOB_KILL_ON_CLOSE_LIMIT = 0x2000
|
|
76
|
+
DEFAULT_CLONE_URL_SUFFIX = ".git"
|
|
77
|
+
POLL_LOCK_DIRECTORY_NAME = ".poll-process"
|
|
78
|
+
CREATE_NEW_PROCESS_GROUP_ATTRIBUTE = "CREATE_NEW_PROCESS_GROUP"
|
|
79
|
+
CREATE_NO_WINDOW_ATTRIBUTE = "CREATE_NO_WINDOW"
|
|
80
|
+
CHILD_MANIFEST_FLAG = "--manifest"
|
|
81
|
+
CHILD_REPOSITORY_FLAG = "--repo"
|
|
82
|
+
CHILD_BASE_FLAG = "--base"
|
|
83
|
+
CHILD_EXECUTOR_FLAG = "--executor"
|
|
84
|
+
CHILD_OUTPUT_FLAG = "--output"
|
|
85
|
+
SCOPED_VERIFICATION_SCRIPT_PATH = ".github/ci/local_verify.py"
|
|
86
|
+
PYTHONPATH_ENVIRONMENT_KEY = "PYTHONPATH"
|
|
87
|
+
PYTEST_AUTO_WORKERS_ENVIRONMENT_KEY = "PYTEST_XDIST_AUTO_NUM_WORKERS"
|
|
88
|
+
PYTEST_AUTO_WORKERS_LIMIT = "2"
|
|
89
|
+
STATE_FILE_SUFFIX = ".tmp"
|
|
90
|
+
SELECTED_MANIFEST_SUFFIX = ".selected-manifest.json"
|
|
91
|
+
UTF8_ENCODING = "utf-8"
|
|
92
|
+
REPORT_NEWLINE = "\n"
|
|
93
|
+
JSON_INDENT_SPACES = 2
|
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import math
|
|
5
|
+
from collections.abc import Mapping
|
|
6
|
+
from pathlib import Path, PurePosixPath
|
|
7
|
+
|
|
8
|
+
from pr_verification.config.constants import GITHUB_API_URL, REPOSITORY_SLUG_PATTERN
|
|
9
|
+
from pr_verification.model import RepositorySettings
|
|
10
|
+
|
|
11
|
+
from .config.constants import (
|
|
12
|
+
DEFAULT_CLONE_URL_SUFFIX,
|
|
13
|
+
REGISTRATION_BASE_REF_KEY,
|
|
14
|
+
REGISTRATION_CHECKOUT_KEY,
|
|
15
|
+
REGISTRATION_MANIFEST_KEY,
|
|
16
|
+
REGISTRATION_PULL_REQUEST_KEY,
|
|
17
|
+
REGISTRATION_REMOTE_KEY,
|
|
18
|
+
REGISTRATION_REPORT_KEY,
|
|
19
|
+
REGISTRATION_REPOSITORY_KEY,
|
|
20
|
+
REGISTRATION_STATE_KEY,
|
|
21
|
+
SETTINGS_API_URL_KEY,
|
|
22
|
+
SETTINGS_APP_ID_KEY,
|
|
23
|
+
SETTINGS_CHILD_TIMEOUT_SECONDS_KEY,
|
|
24
|
+
SETTINGS_INSTALLATION_ID_KEY,
|
|
25
|
+
SETTINGS_POLL_SECONDS_KEY,
|
|
26
|
+
SETTINGS_REGISTRATIONS_KEY,
|
|
27
|
+
SETTINGS_SIGNING_FILE_FIELD,
|
|
28
|
+
SETTINGS_VERSION,
|
|
29
|
+
SETTINGS_VERSION_KEY,
|
|
30
|
+
)
|
|
31
|
+
from .config.timing import DEFAULT_CHILD_TIMEOUT_SECONDS, DEFAULT_POLL_SECONDS
|
|
32
|
+
from .model import AdvisoryRegistration, AdvisorySettings
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
class AdvisoryConfigurationError(ValueError):
|
|
36
|
+
"""Raised when automatic advisory settings are invalid."""
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def load_advisory_settings(settings_path: Path) -> AdvisorySettings:
|
|
40
|
+
"""Load and validate the explicit automatic advisory registrations.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
settings_path: JSON file containing the advisory settings.
|
|
44
|
+
|
|
45
|
+
Returns:
|
|
46
|
+
Validated settings for the registered checkout and pull request pairs.
|
|
47
|
+
|
|
48
|
+
Raises:
|
|
49
|
+
AdvisoryConfigurationError: If the file or any registration is invalid.
|
|
50
|
+
"""
|
|
51
|
+
try:
|
|
52
|
+
parsed_settings = json.loads(settings_path.read_text(encoding="utf-8"))
|
|
53
|
+
except (OSError, json.JSONDecodeError) as error:
|
|
54
|
+
raise AdvisoryConfigurationError(str(error)) from error
|
|
55
|
+
all_settings = _require_mapping(parsed_settings)
|
|
56
|
+
_require_version(all_settings)
|
|
57
|
+
return _build_settings(all_settings)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _build_settings(all_settings: Mapping[str, object]) -> AdvisorySettings:
|
|
61
|
+
registrations = _load_registrations(all_settings)
|
|
62
|
+
return AdvisorySettings(
|
|
63
|
+
_load_api_url(all_settings),
|
|
64
|
+
_require_positive_integer(all_settings, SETTINGS_APP_ID_KEY),
|
|
65
|
+
_require_positive_integer(all_settings, SETTINGS_INSTALLATION_ID_KEY),
|
|
66
|
+
_require_absolute_path(all_settings, SETTINGS_SIGNING_FILE_FIELD),
|
|
67
|
+
_load_positive_number(
|
|
68
|
+
all_settings,
|
|
69
|
+
SETTINGS_POLL_SECONDS_KEY,
|
|
70
|
+
DEFAULT_POLL_SECONDS,
|
|
71
|
+
),
|
|
72
|
+
_load_positive_number(
|
|
73
|
+
all_settings,
|
|
74
|
+
SETTINGS_CHILD_TIMEOUT_SECONDS_KEY,
|
|
75
|
+
DEFAULT_CHILD_TIMEOUT_SECONDS,
|
|
76
|
+
),
|
|
77
|
+
registrations,
|
|
78
|
+
)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _load_registrations(
|
|
82
|
+
all_settings: Mapping[str, object],
|
|
83
|
+
) -> tuple[AdvisoryRegistration, ...]:
|
|
84
|
+
raw_registrations = all_settings.get(SETTINGS_REGISTRATIONS_KEY)
|
|
85
|
+
if not isinstance(raw_registrations, list) or not raw_registrations:
|
|
86
|
+
raise AdvisoryConfigurationError("registrations must be a non-empty array")
|
|
87
|
+
all_registrations = tuple(
|
|
88
|
+
_load_registration(each_registration) for each_registration in raw_registrations
|
|
89
|
+
)
|
|
90
|
+
all_repositories = tuple(
|
|
91
|
+
each_registration.repository.slug for each_registration in all_registrations
|
|
92
|
+
)
|
|
93
|
+
if len(set(all_repositories)) != len(all_repositories):
|
|
94
|
+
raise AdvisoryConfigurationError(
|
|
95
|
+
"each repository can have only one automatic advisory registration"
|
|
96
|
+
)
|
|
97
|
+
return all_registrations
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _load_registration(raw_registration: object) -> AdvisoryRegistration:
|
|
101
|
+
registration = _require_mapping(raw_registration)
|
|
102
|
+
repository_slug = _require_text(registration, REGISTRATION_REPOSITORY_KEY)
|
|
103
|
+
if REPOSITORY_SLUG_PATTERN.fullmatch(repository_slug) is None:
|
|
104
|
+
raise AdvisoryConfigurationError("repository must use owner/name")
|
|
105
|
+
pull_request_number = _require_positive_integer(
|
|
106
|
+
registration, REGISTRATION_PULL_REQUEST_KEY
|
|
107
|
+
)
|
|
108
|
+
checkout_path = _require_absolute_path(registration, REGISTRATION_CHECKOUT_KEY)
|
|
109
|
+
manifest_path = _require_relative_manifest(registration)
|
|
110
|
+
report_path = _require_absolute_path(registration, REGISTRATION_REPORT_KEY)
|
|
111
|
+
state_path = _require_absolute_path(registration, REGISTRATION_STATE_KEY)
|
|
112
|
+
return AdvisoryRegistration(
|
|
113
|
+
RepositorySettings(repository_slug, repository_slug + DEFAULT_CLONE_URL_SUFFIX),
|
|
114
|
+
pull_request_number,
|
|
115
|
+
checkout_path,
|
|
116
|
+
manifest_path,
|
|
117
|
+
report_path,
|
|
118
|
+
state_path,
|
|
119
|
+
_require_text(registration, REGISTRATION_BASE_REF_KEY),
|
|
120
|
+
_require_text(registration, REGISTRATION_REMOTE_KEY),
|
|
121
|
+
)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _load_api_url(all_settings: Mapping[str, object]) -> str:
|
|
125
|
+
return _require_text_or_default(
|
|
126
|
+
all_settings, SETTINGS_API_URL_KEY, GITHUB_API_URL
|
|
127
|
+
).rstrip("/")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _load_positive_number(
|
|
131
|
+
all_settings: Mapping[str, object],
|
|
132
|
+
field_name: str,
|
|
133
|
+
default_seconds: float,
|
|
134
|
+
) -> float:
|
|
135
|
+
raw_number = all_settings.get(field_name, default_seconds)
|
|
136
|
+
if isinstance(raw_number, bool) or not isinstance(raw_number, (float, int)):
|
|
137
|
+
raise AdvisoryConfigurationError(f"{field_name} must be positive")
|
|
138
|
+
if raw_number <= 0 or not math.isfinite(raw_number):
|
|
139
|
+
raise AdvisoryConfigurationError(f"{field_name} must be positive")
|
|
140
|
+
return float(raw_number)
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _require_mapping(raw_payload: object) -> Mapping[str, object]:
|
|
144
|
+
if not isinstance(raw_payload, Mapping):
|
|
145
|
+
raise AdvisoryConfigurationError("settings must be an object")
|
|
146
|
+
return raw_payload
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _require_version(all_settings: Mapping[str, object]) -> None:
|
|
150
|
+
if all_settings.get(SETTINGS_VERSION_KEY) != SETTINGS_VERSION:
|
|
151
|
+
raise AdvisoryConfigurationError("settings version is unsupported")
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _require_text(all_settings: Mapping[str, object], field_name: str) -> str:
|
|
155
|
+
field_text = all_settings.get(field_name)
|
|
156
|
+
if not isinstance(field_text, str) or not field_text.strip():
|
|
157
|
+
raise AdvisoryConfigurationError(f"{field_name} must be a non-empty string")
|
|
158
|
+
return field_text
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _require_text_or_default(
|
|
162
|
+
all_settings: Mapping[str, object],
|
|
163
|
+
field_name: str,
|
|
164
|
+
default_text: str,
|
|
165
|
+
) -> str:
|
|
166
|
+
field_text = all_settings.get(field_name, default_text)
|
|
167
|
+
if not isinstance(field_text, str) or not field_text.strip():
|
|
168
|
+
raise AdvisoryConfigurationError(f"{field_name} must be a non-empty string")
|
|
169
|
+
return field_text
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _require_positive_integer(
|
|
173
|
+
all_settings: Mapping[str, object], field_name: str
|
|
174
|
+
) -> int:
|
|
175
|
+
field_number = all_settings.get(field_name)
|
|
176
|
+
if isinstance(field_number, bool) or not isinstance(field_number, int):
|
|
177
|
+
raise AdvisoryConfigurationError(f"{field_name} must be positive")
|
|
178
|
+
if field_number < 1:
|
|
179
|
+
raise AdvisoryConfigurationError(f"{field_name} must be positive")
|
|
180
|
+
return field_number
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _require_absolute_path(all_settings: Mapping[str, object], field_name: str) -> Path:
|
|
184
|
+
configured_path = Path(_require_text(all_settings, field_name))
|
|
185
|
+
if not configured_path.is_absolute():
|
|
186
|
+
raise AdvisoryConfigurationError(f"{field_name} must be absolute")
|
|
187
|
+
return configured_path.resolve()
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
def _require_relative_manifest(
|
|
191
|
+
all_settings: Mapping[str, object],
|
|
192
|
+
) -> PurePosixPath:
|
|
193
|
+
manifest_text = _require_text(all_settings, REGISTRATION_MANIFEST_KEY)
|
|
194
|
+
manifest_path = PurePosixPath(manifest_text.replace("\\", "/"))
|
|
195
|
+
if manifest_path.is_absolute() or ".." in manifest_path.parts:
|
|
196
|
+
raise AdvisoryConfigurationError("manifest must stay inside checkout")
|
|
197
|
+
return manifest_path
|