claude-dev-env 7.0.0 → 7.1.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 +189 -0
- package/.agents/skills/session-continuity/SKILL.md +114 -0
- package/.agents/skills/session-continuity/continuity.mjs +375 -0
- package/.agents/skills/session-continuity/continuity.test.mjs +303 -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 +78 -0
- package/bin/install-session-continuity.test.mjs +42 -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,247 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from local_report_validation import (
|
|
9
|
+
ReportDecision,
|
|
10
|
+
_load_all_report_fields,
|
|
11
|
+
_matches_candidate,
|
|
12
|
+
_validate_report,
|
|
13
|
+
)
|
|
14
|
+
from local_verification.git_state import CandidateSnapshot, capture_candidate_snapshot
|
|
15
|
+
from local_verification.manifest import ManifestRunFatal, load_manifest
|
|
16
|
+
from local_verification.model import VerificationManifest
|
|
17
|
+
from pr_verification.config.constants import (
|
|
18
|
+
ERROR_DESCRIPTION,
|
|
19
|
+
LOCAL_CHECKS_CONTEXT,
|
|
20
|
+
LOCAL_CHECKS_PASSED_LABEL,
|
|
21
|
+
PENDING_DESCRIPTION,
|
|
22
|
+
)
|
|
23
|
+
from pr_verification.github import GitHubApi
|
|
24
|
+
from pr_verification.model import (
|
|
25
|
+
PullRequestCandidate,
|
|
26
|
+
RepositorySettings,
|
|
27
|
+
StatusState,
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class PublicationOutcome:
|
|
33
|
+
status: StatusState
|
|
34
|
+
description: str
|
|
35
|
+
publishable: bool
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def publish_local_report(
|
|
39
|
+
github: GitHubApi,
|
|
40
|
+
repository: RepositorySettings,
|
|
41
|
+
pull_request_number: int,
|
|
42
|
+
local_repository_path: Path,
|
|
43
|
+
manifest_path: Path,
|
|
44
|
+
report_path: Path,
|
|
45
|
+
) -> PublicationOutcome:
|
|
46
|
+
"""Publish one local report.
|
|
47
|
+
|
|
48
|
+
Args:
|
|
49
|
+
github: GitHub API client.
|
|
50
|
+
repository: Repository settings.
|
|
51
|
+
pull_request_number: Pull request number that owns the report.
|
|
52
|
+
local_repository_path: Local checkout that produced the report.
|
|
53
|
+
manifest_path: Verification manifest path.
|
|
54
|
+
report_path: Verification report path.
|
|
55
|
+
|
|
56
|
+
Returns:
|
|
57
|
+
The publication outcome.
|
|
58
|
+
"""
|
|
59
|
+
return _publish_report_inputs(
|
|
60
|
+
github,
|
|
61
|
+
repository,
|
|
62
|
+
pull_request_number,
|
|
63
|
+
local_repository_path,
|
|
64
|
+
_load_report_inputs(manifest_path, report_path),
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _publish_report_inputs(
|
|
69
|
+
github: GitHubApi,
|
|
70
|
+
repository: RepositorySettings,
|
|
71
|
+
pull_request_number: int,
|
|
72
|
+
local_repository_path: Path,
|
|
73
|
+
all_report_inputs: tuple[VerificationManifest, Mapping[str, object]] | None,
|
|
74
|
+
) -> PublicationOutcome:
|
|
75
|
+
candidate = github.get_candidate(
|
|
76
|
+
repository, pull_request_number, should_require_merge_commit=False
|
|
77
|
+
)
|
|
78
|
+
if all_report_inputs is None:
|
|
79
|
+
return _publish_advisory(
|
|
80
|
+
github, repository, candidate, StatusState.PENDING, PENDING_DESCRIPTION
|
|
81
|
+
)
|
|
82
|
+
manifest, all_report_fields = all_report_inputs
|
|
83
|
+
return _publish_loaded_report(
|
|
84
|
+
github,
|
|
85
|
+
repository,
|
|
86
|
+
pull_request_number,
|
|
87
|
+
local_repository_path,
|
|
88
|
+
manifest,
|
|
89
|
+
all_report_fields,
|
|
90
|
+
candidate,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
def _load_report_inputs(
|
|
95
|
+
manifest_path: Path, report_path: Path
|
|
96
|
+
) -> tuple[VerificationManifest, Mapping[str, object]] | None:
|
|
97
|
+
try:
|
|
98
|
+
manifest = load_manifest(manifest_path)
|
|
99
|
+
all_report_fields = _load_all_report_fields(report_path)
|
|
100
|
+
except (
|
|
101
|
+
ManifestRunFatal,
|
|
102
|
+
OSError,
|
|
103
|
+
UnicodeDecodeError,
|
|
104
|
+
json.JSONDecodeError,
|
|
105
|
+
TypeError,
|
|
106
|
+
ValueError,
|
|
107
|
+
):
|
|
108
|
+
return None
|
|
109
|
+
return manifest, all_report_fields
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def _publish_loaded_report(
|
|
113
|
+
github: GitHubApi,
|
|
114
|
+
repository: RepositorySettings,
|
|
115
|
+
pull_request_number: int,
|
|
116
|
+
local_repository_path: Path,
|
|
117
|
+
manifest: VerificationManifest,
|
|
118
|
+
all_report_fields: Mapping[str, object],
|
|
119
|
+
candidate: PullRequestCandidate,
|
|
120
|
+
) -> PublicationOutcome:
|
|
121
|
+
decision = _validate_report(
|
|
122
|
+
manifest, all_report_fields, candidate, local_repository_path
|
|
123
|
+
)
|
|
124
|
+
if not decision.publishable:
|
|
125
|
+
return _publish_advisory(
|
|
126
|
+
github, repository, candidate, decision.status, decision.description
|
|
127
|
+
)
|
|
128
|
+
return _publish_success(
|
|
129
|
+
github,
|
|
130
|
+
repository,
|
|
131
|
+
pull_request_number,
|
|
132
|
+
local_repository_path,
|
|
133
|
+
candidate,
|
|
134
|
+
decision,
|
|
135
|
+
)
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _publish_advisory(
|
|
139
|
+
github: GitHubApi,
|
|
140
|
+
repository: RepositorySettings,
|
|
141
|
+
candidate: PullRequestCandidate,
|
|
142
|
+
status: StatusState,
|
|
143
|
+
description: str,
|
|
144
|
+
) -> PublicationOutcome:
|
|
145
|
+
github.remove_label(
|
|
146
|
+
repository, candidate.pull_request_number, LOCAL_CHECKS_PASSED_LABEL
|
|
147
|
+
)
|
|
148
|
+
github.post_status(
|
|
149
|
+
repository, candidate.head_sha, status, LOCAL_CHECKS_CONTEXT, description
|
|
150
|
+
)
|
|
151
|
+
return PublicationOutcome(status, description, False)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _publish_success(
|
|
155
|
+
github: GitHubApi,
|
|
156
|
+
repository: RepositorySettings,
|
|
157
|
+
pull_request_number: int,
|
|
158
|
+
local_repository_path: Path,
|
|
159
|
+
candidate: PullRequestCandidate,
|
|
160
|
+
decision: ReportDecision,
|
|
161
|
+
) -> PublicationOutcome:
|
|
162
|
+
latest_candidate, latest_snapshot = _read_current_candidate(
|
|
163
|
+
github, repository, pull_request_number, local_repository_path
|
|
164
|
+
)
|
|
165
|
+
if not _matches_candidate(
|
|
166
|
+
latest_snapshot,
|
|
167
|
+
latest_candidate,
|
|
168
|
+
candidate.head_sha,
|
|
169
|
+
candidate.base_sha,
|
|
170
|
+
):
|
|
171
|
+
return _publish_advisory(
|
|
172
|
+
github, repository, latest_candidate, StatusState.ERROR, ERROR_DESCRIPTION
|
|
173
|
+
)
|
|
174
|
+
_post_success(github, repository, pull_request_number, latest_candidate, decision)
|
|
175
|
+
return _confirm_success(
|
|
176
|
+
github,
|
|
177
|
+
repository,
|
|
178
|
+
pull_request_number,
|
|
179
|
+
local_repository_path,
|
|
180
|
+
latest_candidate,
|
|
181
|
+
decision,
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def _read_current_candidate(
|
|
186
|
+
github: GitHubApi,
|
|
187
|
+
repository: RepositorySettings,
|
|
188
|
+
pull_request_number: int,
|
|
189
|
+
local_repository_path: Path,
|
|
190
|
+
) -> tuple[PullRequestCandidate, CandidateSnapshot]:
|
|
191
|
+
candidate = github.get_candidate(
|
|
192
|
+
repository, pull_request_number, should_require_merge_commit=False
|
|
193
|
+
)
|
|
194
|
+
snapshot = capture_candidate_snapshot(local_repository_path, candidate.base_sha)
|
|
195
|
+
return candidate, snapshot
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def _post_success(
|
|
199
|
+
github: GitHubApi,
|
|
200
|
+
repository: RepositorySettings,
|
|
201
|
+
pull_request_number: int,
|
|
202
|
+
candidate: PullRequestCandidate,
|
|
203
|
+
decision: ReportDecision,
|
|
204
|
+
) -> None:
|
|
205
|
+
github.post_status(
|
|
206
|
+
repository,
|
|
207
|
+
candidate.head_sha,
|
|
208
|
+
StatusState.SUCCESS,
|
|
209
|
+
LOCAL_CHECKS_CONTEXT,
|
|
210
|
+
decision.description,
|
|
211
|
+
)
|
|
212
|
+
github.add_label(repository, pull_request_number, LOCAL_CHECKS_PASSED_LABEL)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _confirm_success(
|
|
216
|
+
github: GitHubApi,
|
|
217
|
+
repository: RepositorySettings,
|
|
218
|
+
pull_request_number: int,
|
|
219
|
+
local_repository_path: Path,
|
|
220
|
+
candidate: PullRequestCandidate,
|
|
221
|
+
decision: ReportDecision,
|
|
222
|
+
) -> PublicationOutcome:
|
|
223
|
+
final_candidate, final_snapshot = _read_current_candidate(
|
|
224
|
+
github, repository, pull_request_number, local_repository_path
|
|
225
|
+
)
|
|
226
|
+
if _matches_candidate(
|
|
227
|
+
final_snapshot,
|
|
228
|
+
final_candidate,
|
|
229
|
+
candidate.head_sha,
|
|
230
|
+
candidate.base_sha,
|
|
231
|
+
):
|
|
232
|
+
return _decision_to_outcome(decision)
|
|
233
|
+
github.remove_label(repository, pull_request_number, LOCAL_CHECKS_PASSED_LABEL)
|
|
234
|
+
github.post_status(
|
|
235
|
+
repository,
|
|
236
|
+
final_candidate.head_sha,
|
|
237
|
+
StatusState.ERROR,
|
|
238
|
+
LOCAL_CHECKS_CONTEXT,
|
|
239
|
+
ERROR_DESCRIPTION,
|
|
240
|
+
)
|
|
241
|
+
return PublicationOutcome(StatusState.ERROR, ERROR_DESCRIPTION, False)
|
|
242
|
+
|
|
243
|
+
|
|
244
|
+
def _decision_to_outcome(decision: ReportDecision) -> PublicationOutcome:
|
|
245
|
+
return PublicationOutcome(
|
|
246
|
+
decision.status, decision.description, decision.publishable
|
|
247
|
+
)
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import sys
|
|
2
|
+
|
|
3
|
+
import local_report_cli as report_cli
|
|
4
|
+
import local_report_core as report_core
|
|
5
|
+
|
|
6
|
+
main = report_cli.main
|
|
7
|
+
PublicationOutcome = report_core.PublicationOutcome
|
|
8
|
+
publish_local_report = report_core.publish_local_report
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
if __name__ == "__main__":
|
|
12
|
+
raise SystemExit(main(sys.argv[1:]))
|
|
@@ -0,0 +1,357 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from local_verification.check_support import _substitute_arguments
|
|
9
|
+
from local_verification.config import (
|
|
10
|
+
CHECK_FAILED_EXIT_CODE,
|
|
11
|
+
COLLECTION_ERROR_KIND,
|
|
12
|
+
FAILED_STATUS,
|
|
13
|
+
INCOMPLETE_EXIT_CODE,
|
|
14
|
+
INCOMPLETE_STATUS,
|
|
15
|
+
MINIMUM_TESTS_ERROR_KIND,
|
|
16
|
+
PASSED_STATUS,
|
|
17
|
+
SUCCESS_EXIT_CODE,
|
|
18
|
+
)
|
|
19
|
+
from local_verification.git_state import CandidateSnapshot, capture_candidate_snapshot
|
|
20
|
+
from local_verification.manifest import compute_manifest_digest
|
|
21
|
+
from local_verification.model import CheckSpec, VerificationManifest
|
|
22
|
+
from pr_verification.config.constants import ERROR_DESCRIPTION, FAILURE_DESCRIPTION
|
|
23
|
+
from pr_verification.model import PullRequestCandidate, StatusState
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass(frozen=True)
|
|
27
|
+
class ReportDecision:
|
|
28
|
+
status: StatusState
|
|
29
|
+
description: str
|
|
30
|
+
publishable: bool
|
|
31
|
+
selected_check_ids: tuple[str, ...] = ()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _load_all_report_fields(report_path: Path) -> Mapping[str, object]:
|
|
35
|
+
parsed_report = json.loads(report_path.read_text(encoding="utf-8"))
|
|
36
|
+
if not isinstance(parsed_report, Mapping):
|
|
37
|
+
raise TypeError("Verification report must be an object")
|
|
38
|
+
return parsed_report
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _validate_report(
|
|
42
|
+
manifest: VerificationManifest,
|
|
43
|
+
all_report_fields: Mapping[str, object],
|
|
44
|
+
candidate: PullRequestCandidate,
|
|
45
|
+
local_repository_path: Path,
|
|
46
|
+
) -> ReportDecision:
|
|
47
|
+
report_head = _text_field(all_report_fields, "head")
|
|
48
|
+
report_base = _text_field(all_report_fields, "base")
|
|
49
|
+
if report_head is None or report_base is None:
|
|
50
|
+
return _invalid_report()
|
|
51
|
+
status = _validate_all_check_fields(
|
|
52
|
+
all_report_fields, manifest.checks, local_repository_path, candidate.base_sha
|
|
53
|
+
)
|
|
54
|
+
if status != PASSED_STATUS:
|
|
55
|
+
return _status_decision(status)
|
|
56
|
+
if not _matches_report_metadata(all_report_fields, manifest):
|
|
57
|
+
return _invalid_report()
|
|
58
|
+
snapshot = capture_candidate_snapshot(local_repository_path, report_base)
|
|
59
|
+
if not _matches_candidate(snapshot, candidate, report_head, report_base):
|
|
60
|
+
return _invalid_report()
|
|
61
|
+
check_count = len(manifest.checks)
|
|
62
|
+
return ReportDecision(
|
|
63
|
+
StatusState.SUCCESS,
|
|
64
|
+
f"Selected local checks passed ({check_count} checks)",
|
|
65
|
+
True,
|
|
66
|
+
tuple(each_check.check_id for each_check in manifest.checks),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _status_decision(status: str | None) -> ReportDecision:
|
|
71
|
+
if status == FAILED_STATUS:
|
|
72
|
+
return ReportDecision(StatusState.FAILURE, FAILURE_DESCRIPTION, False)
|
|
73
|
+
return ReportDecision(StatusState.ERROR, ERROR_DESCRIPTION, False)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _matches_report_metadata(
|
|
77
|
+
all_report_fields: Mapping[str, object], manifest: VerificationManifest
|
|
78
|
+
) -> bool:
|
|
79
|
+
return (
|
|
80
|
+
all_report_fields.get("manifest_digest") == compute_manifest_digest(manifest)
|
|
81
|
+
and all_report_fields.get("publishable") is True
|
|
82
|
+
and all_report_fields.get("worktree_clean") is True
|
|
83
|
+
and all_report_fields.get("inputs_unchanged") is True
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def _validate_all_check_fields(
|
|
88
|
+
all_report_fields: Mapping[str, object],
|
|
89
|
+
all_manifest_checks: tuple[CheckSpec, ...],
|
|
90
|
+
local_repository_path: Path,
|
|
91
|
+
base_revision: str,
|
|
92
|
+
) -> str | None:
|
|
93
|
+
all_statuses = _read_check_statuses(
|
|
94
|
+
all_report_fields, all_manifest_checks, local_repository_path, base_revision
|
|
95
|
+
)
|
|
96
|
+
if all_statuses is None:
|
|
97
|
+
return None
|
|
98
|
+
return _validate_aggregate(all_report_fields, all_statuses)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _read_check_statuses(
|
|
102
|
+
all_report_fields: Mapping[str, object],
|
|
103
|
+
all_manifest_checks: tuple[CheckSpec, ...],
|
|
104
|
+
local_repository_path: Path,
|
|
105
|
+
base_revision: str,
|
|
106
|
+
) -> list[str] | None:
|
|
107
|
+
raw_checks = all_report_fields.get("checks")
|
|
108
|
+
if not isinstance(raw_checks, list):
|
|
109
|
+
return None
|
|
110
|
+
all_check_ids: list[str] = []
|
|
111
|
+
all_statuses: list[str] = []
|
|
112
|
+
for each_index, each_raw_check in enumerate(raw_checks):
|
|
113
|
+
check_fields = _read_check_fields(
|
|
114
|
+
each_raw_check,
|
|
115
|
+
all_manifest_checks,
|
|
116
|
+
each_index,
|
|
117
|
+
local_repository_path,
|
|
118
|
+
base_revision,
|
|
119
|
+
)
|
|
120
|
+
if check_fields is None:
|
|
121
|
+
return None
|
|
122
|
+
check_id, status = check_fields
|
|
123
|
+
all_check_ids.append(check_id)
|
|
124
|
+
all_statuses.append(status)
|
|
125
|
+
if not _check_records_match_manifest(
|
|
126
|
+
all_check_ids, all_statuses, all_manifest_checks
|
|
127
|
+
):
|
|
128
|
+
return None
|
|
129
|
+
return all_statuses
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def _check_records_match_manifest(
|
|
133
|
+
all_check_ids: list[str],
|
|
134
|
+
all_statuses: list[str],
|
|
135
|
+
all_manifest_checks: tuple[CheckSpec, ...],
|
|
136
|
+
) -> bool:
|
|
137
|
+
selected_check_ids = tuple(
|
|
138
|
+
each_check.check_id for each_check in all_manifest_checks
|
|
139
|
+
)
|
|
140
|
+
if tuple(all_check_ids) != selected_check_ids:
|
|
141
|
+
return False
|
|
142
|
+
return all(
|
|
143
|
+
each_status in {PASSED_STATUS, FAILED_STATUS, INCOMPLETE_STATUS}
|
|
144
|
+
for each_status in all_statuses
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _read_check_fields(
|
|
149
|
+
raw_check: object,
|
|
150
|
+
all_manifest_checks: tuple[CheckSpec, ...],
|
|
151
|
+
check_index: int,
|
|
152
|
+
local_repository_path: Path,
|
|
153
|
+
base_revision: str,
|
|
154
|
+
) -> tuple[str, str] | None:
|
|
155
|
+
if not isinstance(raw_check, Mapping):
|
|
156
|
+
return None
|
|
157
|
+
check_id = raw_check.get("id")
|
|
158
|
+
status = raw_check.get("status")
|
|
159
|
+
if not isinstance(check_id, str) or not isinstance(status, str):
|
|
160
|
+
return None
|
|
161
|
+
if not _all_check_fields_matches_spec(
|
|
162
|
+
raw_check,
|
|
163
|
+
all_manifest_checks,
|
|
164
|
+
check_index,
|
|
165
|
+
status,
|
|
166
|
+
local_repository_path,
|
|
167
|
+
base_revision,
|
|
168
|
+
):
|
|
169
|
+
return None
|
|
170
|
+
return check_id, status
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def _validate_aggregate(
|
|
174
|
+
all_report_fields: Mapping[str, object], all_statuses: list[str]
|
|
175
|
+
) -> str | None:
|
|
176
|
+
aggregate_status = _aggregate_status(all_statuses)
|
|
177
|
+
aggregate_mapping = all_report_fields.get("aggregate")
|
|
178
|
+
if not isinstance(aggregate_mapping, Mapping):
|
|
179
|
+
return None
|
|
180
|
+
if aggregate_mapping.get("status") != aggregate_status:
|
|
181
|
+
return None
|
|
182
|
+
if all_report_fields.get("status") != aggregate_status:
|
|
183
|
+
return None
|
|
184
|
+
expected_exit_code = _status_exit_code(aggregate_status)
|
|
185
|
+
if not _matches_integer(all_report_fields.get("exit_code"), expected_exit_code):
|
|
186
|
+
return None
|
|
187
|
+
expected_counts = {
|
|
188
|
+
"total": len(all_statuses),
|
|
189
|
+
"passed": all_statuses.count(PASSED_STATUS),
|
|
190
|
+
"failed": all_statuses.count(FAILED_STATUS),
|
|
191
|
+
"incomplete": all_statuses.count(INCOMPLETE_STATUS),
|
|
192
|
+
"exit_code": expected_exit_code,
|
|
193
|
+
}
|
|
194
|
+
if any(
|
|
195
|
+
not _matches_integer(aggregate_mapping.get(each_name), each_count)
|
|
196
|
+
for each_name, each_count in expected_counts.items()
|
|
197
|
+
):
|
|
198
|
+
return None
|
|
199
|
+
return aggregate_status
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _all_check_fields_matches_spec(
|
|
203
|
+
all_check_fields: Mapping[str, object],
|
|
204
|
+
all_manifest_checks: tuple[CheckSpec, ...],
|
|
205
|
+
check_index: int,
|
|
206
|
+
status: str,
|
|
207
|
+
local_repository_path: Path,
|
|
208
|
+
base_revision: str,
|
|
209
|
+
) -> bool:
|
|
210
|
+
if check_index >= len(all_manifest_checks):
|
|
211
|
+
return False
|
|
212
|
+
if not _matches_execution_fields(all_check_fields, status):
|
|
213
|
+
return False
|
|
214
|
+
check_spec = all_manifest_checks[check_index]
|
|
215
|
+
if not _matches_command_arguments(
|
|
216
|
+
all_check_fields, check_spec, local_repository_path, base_revision
|
|
217
|
+
):
|
|
218
|
+
return False
|
|
219
|
+
if not _matches_check_directory(
|
|
220
|
+
all_check_fields, check_spec, local_repository_path
|
|
221
|
+
):
|
|
222
|
+
return False
|
|
223
|
+
if all_check_fields.get("minimum_tests") != check_spec.minimum_tests:
|
|
224
|
+
return False
|
|
225
|
+
return _matches_collection_fields(all_check_fields, check_spec, status)
|
|
226
|
+
|
|
227
|
+
|
|
228
|
+
def _matches_command_arguments(
|
|
229
|
+
all_check_fields: Mapping[str, object],
|
|
230
|
+
check_spec: CheckSpec,
|
|
231
|
+
local_repository_path: Path,
|
|
232
|
+
base_revision: str,
|
|
233
|
+
) -> bool:
|
|
234
|
+
all_recorded_arguments = all_check_fields.get("argv")
|
|
235
|
+
if not isinstance(all_recorded_arguments, list):
|
|
236
|
+
return False
|
|
237
|
+
if any(
|
|
238
|
+
not isinstance(each_argument, str) for each_argument in all_recorded_arguments
|
|
239
|
+
):
|
|
240
|
+
return False
|
|
241
|
+
expected_arguments = _substitute_arguments(
|
|
242
|
+
check_spec.command_arguments, local_repository_path, base_revision
|
|
243
|
+
)
|
|
244
|
+
return tuple(all_recorded_arguments) == expected_arguments
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _matches_execution_fields(
|
|
248
|
+
all_check_fields: Mapping[str, object], status: str
|
|
249
|
+
) -> bool:
|
|
250
|
+
exit_code = all_check_fields.get("exit_code")
|
|
251
|
+
error_kind = all_check_fields.get("error_kind")
|
|
252
|
+
error_message = all_check_fields.get("error_message")
|
|
253
|
+
if status == PASSED_STATUS:
|
|
254
|
+
return (
|
|
255
|
+
exit_code == SUCCESS_EXIT_CODE
|
|
256
|
+
and error_kind is None
|
|
257
|
+
and error_message is None
|
|
258
|
+
)
|
|
259
|
+
if not isinstance(error_kind, str) or not error_kind:
|
|
260
|
+
return False
|
|
261
|
+
if not isinstance(error_message, str) or not error_message:
|
|
262
|
+
return False
|
|
263
|
+
if status == FAILED_STATUS and error_kind == MINIMUM_TESTS_ERROR_KIND:
|
|
264
|
+
return exit_code == SUCCESS_EXIT_CODE
|
|
265
|
+
if status == FAILED_STATUS:
|
|
266
|
+
return _is_integer(exit_code) and exit_code != SUCCESS_EXIT_CODE
|
|
267
|
+
return status == INCOMPLETE_STATUS
|
|
268
|
+
|
|
269
|
+
|
|
270
|
+
def _matches_check_directory(
|
|
271
|
+
all_check_fields: Mapping[str, object],
|
|
272
|
+
check_spec: CheckSpec,
|
|
273
|
+
local_repository_path: Path,
|
|
274
|
+
) -> bool:
|
|
275
|
+
expected_check_directory = (local_repository_path / check_spec.cwd).resolve()
|
|
276
|
+
recorded_check_directory = all_check_fields.get("cwd")
|
|
277
|
+
return (
|
|
278
|
+
isinstance(recorded_check_directory, str)
|
|
279
|
+
and Path(recorded_check_directory).resolve() == expected_check_directory
|
|
280
|
+
)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _matches_collection_fields(
|
|
284
|
+
all_check_fields: Mapping[str, object], check_spec: CheckSpec, status: str
|
|
285
|
+
) -> bool:
|
|
286
|
+
if check_spec.minimum_tests is None:
|
|
287
|
+
return True
|
|
288
|
+
collected_tests = _read_integer(all_check_fields.get("collected_tests"))
|
|
289
|
+
collection_exit_code = _read_integer(all_check_fields.get("collection_exit_code"))
|
|
290
|
+
if collected_tests is None or collection_exit_code is None:
|
|
291
|
+
return False
|
|
292
|
+
if status == PASSED_STATUS:
|
|
293
|
+
return (
|
|
294
|
+
collection_exit_code == SUCCESS_EXIT_CODE
|
|
295
|
+
and collected_tests >= check_spec.minimum_tests
|
|
296
|
+
)
|
|
297
|
+
if (
|
|
298
|
+
status == FAILED_STATUS
|
|
299
|
+
and all_check_fields.get("error_kind") == MINIMUM_TESTS_ERROR_KIND
|
|
300
|
+
):
|
|
301
|
+
return collected_tests < check_spec.minimum_tests
|
|
302
|
+
if all_check_fields.get("error_kind") != COLLECTION_ERROR_KIND:
|
|
303
|
+
return True
|
|
304
|
+
return collection_exit_code != SUCCESS_EXIT_CODE
|
|
305
|
+
|
|
306
|
+
|
|
307
|
+
def _aggregate_status(all_statuses: list[str]) -> str:
|
|
308
|
+
if FAILED_STATUS in all_statuses:
|
|
309
|
+
return FAILED_STATUS
|
|
310
|
+
if INCOMPLETE_STATUS in all_statuses:
|
|
311
|
+
return INCOMPLETE_STATUS
|
|
312
|
+
return PASSED_STATUS
|
|
313
|
+
|
|
314
|
+
|
|
315
|
+
def _status_exit_code(status: str) -> int:
|
|
316
|
+
if status == FAILED_STATUS:
|
|
317
|
+
return CHECK_FAILED_EXIT_CODE
|
|
318
|
+
if status == INCOMPLETE_STATUS:
|
|
319
|
+
return INCOMPLETE_EXIT_CODE
|
|
320
|
+
return SUCCESS_EXIT_CODE
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def _matches_candidate(
|
|
324
|
+
candidate_snapshot: CandidateSnapshot,
|
|
325
|
+
candidate: PullRequestCandidate,
|
|
326
|
+
report_head: str,
|
|
327
|
+
report_base: str,
|
|
328
|
+
) -> bool:
|
|
329
|
+
return (
|
|
330
|
+
candidate_snapshot.is_git_repository
|
|
331
|
+
and candidate_snapshot.worktree_clean
|
|
332
|
+
and candidate_snapshot.head_revision == report_head == candidate.head_sha
|
|
333
|
+
and candidate_snapshot.base_revision == report_base == candidate.base_sha
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
|
|
337
|
+
def _read_integer(raw_integer: object) -> int | None:
|
|
338
|
+
if isinstance(raw_integer, bool) or not isinstance(raw_integer, int):
|
|
339
|
+
return None
|
|
340
|
+
return raw_integer
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
def _is_integer(raw_integer: object) -> bool:
|
|
344
|
+
return isinstance(raw_integer, int) and not isinstance(raw_integer, bool)
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def _matches_integer(raw_integer: object, expected_integer: int) -> bool:
|
|
348
|
+
return _is_integer(raw_integer) and raw_integer == expected_integer
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
def _invalid_report() -> ReportDecision:
|
|
352
|
+
return ReportDecision(StatusState.ERROR, ERROR_DESCRIPTION, False)
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
def _text_field(all_report_fields: Mapping[str, object], field_name: str) -> str | None:
|
|
356
|
+
field_text = all_report_fields.get(field_name)
|
|
357
|
+
return field_text if isinstance(field_text, str) and field_text else None
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from .cli import main
|
|
2
|
+
from .manifest import ManifestRunFatal, compute_manifest_digest, load_manifest
|
|
3
|
+
from .model import (
|
|
4
|
+
CheckRecord,
|
|
5
|
+
CheckSpec,
|
|
6
|
+
ExclusionSpec,
|
|
7
|
+
VerificationManifest,
|
|
8
|
+
VerificationReport,
|
|
9
|
+
)
|
|
10
|
+
from .runner import run_verification
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
"CheckRecord",
|
|
14
|
+
"CheckSpec",
|
|
15
|
+
"ExclusionSpec",
|
|
16
|
+
"ManifestRunFatal",
|
|
17
|
+
"VerificationManifest",
|
|
18
|
+
"VerificationReport",
|
|
19
|
+
"compute_manifest_digest",
|
|
20
|
+
"load_manifest",
|
|
21
|
+
"main",
|
|
22
|
+
"run_verification",
|
|
23
|
+
]
|