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,91 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import urllib.error
|
|
4
|
+
import urllib.request
|
|
5
|
+
from collections.abc import Callable
|
|
6
|
+
from dataclasses import dataclass
|
|
7
|
+
from http.client import HTTPResponse as OpenedHttpReply
|
|
8
|
+
|
|
9
|
+
import jwt
|
|
10
|
+
|
|
11
|
+
from .config.constants import (
|
|
12
|
+
UTF8_ENCODING,
|
|
13
|
+
)
|
|
14
|
+
from .config.timing import HTTP_TIMEOUT_SECONDS
|
|
15
|
+
from .github_parsing import GitHubError
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass(frozen=True)
|
|
19
|
+
class HttpReply:
|
|
20
|
+
status_code: int
|
|
21
|
+
headers: dict[str, str]
|
|
22
|
+
body: bytes
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
HttpRequester = Callable[[str, str, dict[str, str], bytes | None], HttpReply]
|
|
26
|
+
JwtEncoder = Callable[[dict[str, int | str], str, str], str]
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _read_http_error(error: urllib.error.HTTPError) -> HttpReply:
|
|
30
|
+
return HttpReply(
|
|
31
|
+
status_code=error.code,
|
|
32
|
+
headers=dict(error.headers.items()),
|
|
33
|
+
body=error.read(),
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def request_http(
|
|
38
|
+
method: str,
|
|
39
|
+
url: str,
|
|
40
|
+
all_headers: dict[str, str],
|
|
41
|
+
body: bytes | None,
|
|
42
|
+
) -> HttpReply:
|
|
43
|
+
"""Send one GitHub HTTP request.
|
|
44
|
+
|
|
45
|
+
Args:
|
|
46
|
+
method: HTTP method.
|
|
47
|
+
url: Absolute request URL.
|
|
48
|
+
all_headers: Request headers.
|
|
49
|
+
body: Encoded request body, or None.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
The HTTP reply, including error replies with a status code.
|
|
53
|
+
|
|
54
|
+
Raises:
|
|
55
|
+
GitHubError: If the request fails before a status code exists.
|
|
56
|
+
"""
|
|
57
|
+
request = urllib.request.Request(
|
|
58
|
+
url=url, data=body, headers=all_headers, method=method
|
|
59
|
+
)
|
|
60
|
+
try:
|
|
61
|
+
with urllib.request.urlopen(request, timeout=HTTP_TIMEOUT_SECONDS) as reply:
|
|
62
|
+
return _read_http_reply(reply)
|
|
63
|
+
except urllib.error.HTTPError as error:
|
|
64
|
+
return _read_http_error(error)
|
|
65
|
+
except (urllib.error.URLError, OSError) as error:
|
|
66
|
+
raise GitHubError(str(error)) from error
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _read_http_reply(reply: OpenedHttpReply) -> HttpReply:
|
|
70
|
+
return HttpReply(
|
|
71
|
+
status_code=reply.status,
|
|
72
|
+
headers=dict(reply.headers.items()),
|
|
73
|
+
body=reply.read(),
|
|
74
|
+
)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def encode_app_jwt(
|
|
78
|
+
all_claims: dict[str, int | str], private_key: str, algorithm: str
|
|
79
|
+
) -> str:
|
|
80
|
+
"""Encode an app JWT.
|
|
81
|
+
|
|
82
|
+
Args:
|
|
83
|
+
all_claims: JWT claims.
|
|
84
|
+
|
|
85
|
+
Returns:
|
|
86
|
+
Encoded token.
|
|
87
|
+
"""
|
|
88
|
+
encoded_token = jwt.encode(all_claims, private_key, algorithm=algorithm)
|
|
89
|
+
if isinstance(encoded_token, bytes):
|
|
90
|
+
return encoded_token.decode(UTF8_ENCODING)
|
|
91
|
+
return encoded_token
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
import sys
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from types import TracebackType
|
|
7
|
+
from typing import Self
|
|
8
|
+
|
|
9
|
+
if sys.platform == "win32":
|
|
10
|
+
import msvcrt
|
|
11
|
+
else:
|
|
12
|
+
import fcntl
|
|
13
|
+
|
|
14
|
+
from .config.constants import (
|
|
15
|
+
SUPERVISOR_LOCK_ERROR,
|
|
16
|
+
SUPERVISOR_LOCK_FILE_MODE,
|
|
17
|
+
SUPERVISOR_LOCK_FILENAME,
|
|
18
|
+
SUPERVISOR_LOCK_IO_ERROR,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
if sys.platform == "win32":
|
|
22
|
+
|
|
23
|
+
def _acquire_file_lock(file_descriptor: int) -> None:
|
|
24
|
+
"""Acquire one nonblocking Windows lock."""
|
|
25
|
+
os.lseek(file_descriptor, 0, os.SEEK_SET)
|
|
26
|
+
msvcrt.locking(file_descriptor, msvcrt.LK_NBLCK, 1)
|
|
27
|
+
|
|
28
|
+
def _release_file_lock(file_descriptor: int) -> None:
|
|
29
|
+
"""Release one Windows lock."""
|
|
30
|
+
os.lseek(file_descriptor, 0, os.SEEK_SET)
|
|
31
|
+
msvcrt.locking(file_descriptor, msvcrt.LK_UNLCK, 1)
|
|
32
|
+
|
|
33
|
+
else:
|
|
34
|
+
|
|
35
|
+
def _acquire_file_lock(file_descriptor: int) -> None:
|
|
36
|
+
"""Acquire one nonblocking POSIX lock."""
|
|
37
|
+
fcntl.flock(file_descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
38
|
+
|
|
39
|
+
def _release_file_lock(file_descriptor: int) -> None:
|
|
40
|
+
"""Release one POSIX lock."""
|
|
41
|
+
fcntl.flock(file_descriptor, fcntl.LOCK_UN)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SupervisorLockError(RuntimeError):
|
|
45
|
+
"""Raised when the supervisor lock cannot be acquired or released."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SupervisorLock:
|
|
49
|
+
def __init__(self, cache_root: Path) -> None:
|
|
50
|
+
self.cache_root = cache_root.resolve()
|
|
51
|
+
self.lock_path = self.cache_root / SUPERVISOR_LOCK_FILENAME
|
|
52
|
+
self.file_descriptor: int | None = None
|
|
53
|
+
|
|
54
|
+
def __enter__(self) -> Self:
|
|
55
|
+
self.acquire()
|
|
56
|
+
return self
|
|
57
|
+
|
|
58
|
+
def __exit__(
|
|
59
|
+
self,
|
|
60
|
+
exception_type: type[BaseException] | None,
|
|
61
|
+
exception: BaseException | None,
|
|
62
|
+
traceback: TracebackType | None,
|
|
63
|
+
) -> None:
|
|
64
|
+
self.release()
|
|
65
|
+
|
|
66
|
+
def acquire(self) -> None:
|
|
67
|
+
"""Acquire the operating-system lock for this cache root.
|
|
68
|
+
|
|
69
|
+
Raises:
|
|
70
|
+
SupervisorLockError: If another owner holds the lock or I/O fails.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
self.cache_root.mkdir(parents=True, exist_ok=True)
|
|
74
|
+
file_descriptor = os.open(
|
|
75
|
+
self.lock_path,
|
|
76
|
+
os.O_CREAT | os.O_RDWR,
|
|
77
|
+
SUPERVISOR_LOCK_FILE_MODE,
|
|
78
|
+
)
|
|
79
|
+
except OSError as error:
|
|
80
|
+
raise SupervisorLockError(SUPERVISOR_LOCK_IO_ERROR) from error
|
|
81
|
+
try:
|
|
82
|
+
_acquire_file_lock(file_descriptor)
|
|
83
|
+
except (BlockingIOError, PermissionError) as error:
|
|
84
|
+
_close_file_descriptor_without_raising(file_descriptor)
|
|
85
|
+
raise SupervisorLockError(SUPERVISOR_LOCK_ERROR) from error
|
|
86
|
+
except OSError as error:
|
|
87
|
+
_close_file_descriptor_without_raising(file_descriptor)
|
|
88
|
+
raise SupervisorLockError(SUPERVISOR_LOCK_IO_ERROR) from error
|
|
89
|
+
self.file_descriptor = file_descriptor
|
|
90
|
+
|
|
91
|
+
def release(self) -> None:
|
|
92
|
+
"""Release the operating-system lock and close its file descriptor.
|
|
93
|
+
|
|
94
|
+
Raises:
|
|
95
|
+
SupervisorLockError: If unlocking or closing the file fails.
|
|
96
|
+
"""
|
|
97
|
+
file_descriptor = self.file_descriptor
|
|
98
|
+
if file_descriptor is None:
|
|
99
|
+
return
|
|
100
|
+
self.file_descriptor = None
|
|
101
|
+
try:
|
|
102
|
+
_release_file_lock(file_descriptor)
|
|
103
|
+
except OSError as error:
|
|
104
|
+
raise SupervisorLockError(SUPERVISOR_LOCK_IO_ERROR) from error
|
|
105
|
+
finally:
|
|
106
|
+
_close_file_descriptor(file_descriptor)
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _close_file_descriptor(file_descriptor: int) -> None:
|
|
110
|
+
"""Close one lock file descriptor."""
|
|
111
|
+
try:
|
|
112
|
+
os.close(file_descriptor)
|
|
113
|
+
except OSError as error:
|
|
114
|
+
raise SupervisorLockError(SUPERVISOR_LOCK_IO_ERROR) from error
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _close_file_descriptor_without_raising(file_descriptor: int) -> None:
|
|
118
|
+
"""Close one lock file descriptor while a lock failure is already being raised."""
|
|
119
|
+
try:
|
|
120
|
+
os.close(file_descriptor)
|
|
121
|
+
except OSError:
|
|
122
|
+
return
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from enum import Enum
|
|
5
|
+
|
|
6
|
+
from .config.constants import (
|
|
7
|
+
STATUS_ERROR,
|
|
8
|
+
STATUS_FAILURE,
|
|
9
|
+
STATUS_PENDING,
|
|
10
|
+
STATUS_SUCCESS,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class StatusState(str, Enum):
|
|
15
|
+
PENDING = STATUS_PENDING
|
|
16
|
+
ERROR = STATUS_ERROR
|
|
17
|
+
FAILURE = STATUS_FAILURE
|
|
18
|
+
SUCCESS = STATUS_SUCCESS
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class RepositorySettings:
|
|
23
|
+
slug: str
|
|
24
|
+
clone_url: str
|
|
25
|
+
|
|
26
|
+
@property
|
|
27
|
+
def name(self) -> str:
|
|
28
|
+
return self.slug.split("/", maxsplit=1)[1]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@dataclass(frozen=True)
|
|
32
|
+
class PullRequestCandidate:
|
|
33
|
+
repository_slug: str
|
|
34
|
+
pull_request_number: int
|
|
35
|
+
base_ref: str
|
|
36
|
+
base_sha: str
|
|
37
|
+
head_sha: str
|
|
38
|
+
merge_sha: str
|
|
39
|
+
is_draft: bool
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def key(self) -> tuple[str, int]:
|
|
43
|
+
return self.repository_slug, self.pull_request_number
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
PyJWT[crypto]>=2.10,<3
|
|
@@ -330,3 +330,100 @@ def test_cli_has_no_inline_body_flag() -> None:
|
|
|
330
330
|
)
|
|
331
331
|
assert completed_process.returncode == 2
|
|
332
332
|
assert "unrecognized arguments" in completed_process.stderr
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
RELEASE_BRANCH = "release-please--branches--main--components--claude-dev-env"
|
|
336
|
+
RELEASE_BODY = """:robot: I have created a release *beep* *boop*
|
|
337
|
+
---
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
## [7.0.0](https://example.invalid/compare/v6.0.0...v7.0.0) (2026-01-01)
|
|
341
|
+
|
|
342
|
+
|
|
343
|
+
### Features
|
|
344
|
+
|
|
345
|
+
* a shipped change
|
|
346
|
+
|
|
347
|
+
---
|
|
348
|
+
This PR was generated with [Release Please](https://example.invalid/release-please).
|
|
349
|
+
"""
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def should_accept_a_release_body_without_the_house_headings() -> None:
|
|
353
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
354
|
+
action="pr-edit",
|
|
355
|
+
title=None,
|
|
356
|
+
body_text=RELEASE_BODY,
|
|
357
|
+
head_branch=RELEASE_BRANCH,
|
|
358
|
+
)
|
|
359
|
+
assert all_findings == ()
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
@pytest.mark.parametrize(
|
|
363
|
+
"damaged_body",
|
|
364
|
+
[
|
|
365
|
+
RELEASE_BODY.replace(":robot: I have created a release *beep* *boop*", ""),
|
|
366
|
+
RELEASE_BODY.replace("This PR was generated with [Release Please]", ""),
|
|
367
|
+
],
|
|
368
|
+
)
|
|
369
|
+
def should_report_a_release_body_that_lost_a_marker(damaged_body: str) -> None:
|
|
370
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
371
|
+
action="pr-edit",
|
|
372
|
+
title=None,
|
|
373
|
+
body_text=damaged_body,
|
|
374
|
+
head_branch=RELEASE_BRANCH,
|
|
375
|
+
)
|
|
376
|
+
assert [each_finding.code for each_finding in all_findings] == [
|
|
377
|
+
"rewritten-release-body"
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
|
|
381
|
+
def should_report_a_release_body_rewritten_into_house_prose() -> None:
|
|
382
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
383
|
+
action="pr-edit",
|
|
384
|
+
title=None,
|
|
385
|
+
body_text="This pull request publishes claude-dev-env 2.37.0.",
|
|
386
|
+
head_branch=RELEASE_BRANCH,
|
|
387
|
+
)
|
|
388
|
+
assert [each_finding.code for each_finding in all_findings] == [
|
|
389
|
+
"rewritten-release-body"
|
|
390
|
+
]
|
|
391
|
+
|
|
392
|
+
|
|
393
|
+
def should_keep_the_house_headings_for_an_ordinary_branch() -> None:
|
|
394
|
+
all_findings = durable_post_lint.lint_durable_post(
|
|
395
|
+
action="pr-create",
|
|
396
|
+
title="fix(cli): tighten input handling",
|
|
397
|
+
body_text="Body without the required headings.",
|
|
398
|
+
head_branch="fix/ordinary-branch",
|
|
399
|
+
)
|
|
400
|
+
assert MISSING_HEADING_CODE_NAME in {
|
|
401
|
+
each_finding.code for each_finding in all_findings
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
MISSING_HEADING_CODE_NAME = "missing-pr-description-heading"
|
|
406
|
+
|
|
407
|
+
|
|
408
|
+
def should_report_a_rewritten_release_body_through_the_command_line(
|
|
409
|
+
tmp_path: Path,
|
|
410
|
+
) -> None:
|
|
411
|
+
body_path = tmp_path / "body.md"
|
|
412
|
+
body_path.write_text("This pull request publishes the package.", encoding="utf-8")
|
|
413
|
+
completed_process = subprocess.run(
|
|
414
|
+
[
|
|
415
|
+
sys.executable,
|
|
416
|
+
str(SCRIPT_PATH),
|
|
417
|
+
"--action",
|
|
418
|
+
"pr-edit",
|
|
419
|
+
"--body-file",
|
|
420
|
+
str(body_path),
|
|
421
|
+
"--head-branch",
|
|
422
|
+
RELEASE_BRANCH,
|
|
423
|
+
],
|
|
424
|
+
check=False,
|
|
425
|
+
capture_output=True,
|
|
426
|
+
text=True,
|
|
427
|
+
)
|
|
428
|
+
assert completed_process.returncode == 1
|
|
429
|
+
assert "keep the generated text whole" in completed_process.stderr
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { spawnSync } from "node:child_process";
|
|
3
|
+
import {
|
|
4
|
+
mkdirSync,
|
|
5
|
+
mkdtempSync,
|
|
6
|
+
readFileSync,
|
|
7
|
+
rmSync,
|
|
8
|
+
writeFileSync,
|
|
9
|
+
} from "node:fs";
|
|
10
|
+
import { tmpdir } from "node:os";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import test from "node:test";
|
|
13
|
+
import { fileURLToPath } from "node:url";
|
|
14
|
+
|
|
15
|
+
const packageRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
16
|
+
const auditCliPath = path.join(packageRoot, "audit-hooks-cli.mjs");
|
|
17
|
+
|
|
18
|
+
function writeJson(filePath, jsonDocument) {
|
|
19
|
+
writeFileSync(filePath, JSON.stringify(jsonDocument), "utf8");
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function createAuditFixture({
|
|
23
|
+
canonicalHooks = {},
|
|
24
|
+
claudeHooks = { hooks: {} },
|
|
25
|
+
codexHooks = { hooks: {} },
|
|
26
|
+
} = {}) {
|
|
27
|
+
const fixtureRoot = mkdtempSync(path.join(tmpdir(), "hook-audit-cli-"));
|
|
28
|
+
const canonicalHooksRoot = path.join(fixtureRoot, "packages", "claude-dev-env", "hooks");
|
|
29
|
+
const homeDirectory = path.join(fixtureRoot, "home");
|
|
30
|
+
mkdirSync(canonicalHooksRoot, { recursive: true });
|
|
31
|
+
mkdirSync(path.join(homeDirectory, ".claude", "hooks"), { recursive: true });
|
|
32
|
+
mkdirSync(path.join(homeDirectory, ".codex", "hooks"), { recursive: true });
|
|
33
|
+
writeJson(path.join(canonicalHooksRoot, "hooks.json"), { hooks: canonicalHooks });
|
|
34
|
+
writeJson(path.join(homeDirectory, ".claude", "settings.json"), claudeHooks);
|
|
35
|
+
writeJson(path.join(homeDirectory, ".codex", "hooks.json"), codexHooks);
|
|
36
|
+
return { fixtureRoot, homeDirectory };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function runAuditCli(allArguments) {
|
|
40
|
+
const childProcess = spawnSync(process.execPath, [auditCliPath, ...allArguments], {
|
|
41
|
+
cwd: packageRoot,
|
|
42
|
+
encoding: "utf8",
|
|
43
|
+
env: { ...process.env, HOME: path.join(tmpdir(), "unused-audit-cli-home") },
|
|
44
|
+
shell: false,
|
|
45
|
+
});
|
|
46
|
+
assert.equal(childProcess.error, undefined);
|
|
47
|
+
return childProcess;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
test("writes a JSON audit report to stdout through the CLI", () => {
|
|
51
|
+
const fixture = createAuditFixture({
|
|
52
|
+
canonicalHooks: {
|
|
53
|
+
PreToolUse: [{
|
|
54
|
+
matcher: "Write",
|
|
55
|
+
hooks: [{
|
|
56
|
+
type: "command",
|
|
57
|
+
command: "python3 " + "$" + "{CLAUDE_PLUGIN_ROOT}/hooks/blocking/write_existing_file_blocker.py",
|
|
58
|
+
}],
|
|
59
|
+
}],
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
try {
|
|
63
|
+
const childProcess = runAuditCli([
|
|
64
|
+
"--format",
|
|
65
|
+
"json",
|
|
66
|
+
"--repository-root",
|
|
67
|
+
fixture.fixtureRoot,
|
|
68
|
+
]);
|
|
69
|
+
|
|
70
|
+
assert.equal(childProcess.status, 0);
|
|
71
|
+
assert.equal(childProcess.stderr, "");
|
|
72
|
+
const report = JSON.parse(childProcess.stdout);
|
|
73
|
+
assert.equal(report.summary.directCount, 1);
|
|
74
|
+
assert.equal(report.registrationEligibility.status, "not_checked");
|
|
75
|
+
} finally {
|
|
76
|
+
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("writes the JSON audit report to the requested file through the CLI", () => {
|
|
81
|
+
const fixture = createAuditFixture();
|
|
82
|
+
const reportPath = path.join(fixture.fixtureRoot, "audit-report.json");
|
|
83
|
+
try {
|
|
84
|
+
const childProcess = runAuditCli([
|
|
85
|
+
"--format",
|
|
86
|
+
"json",
|
|
87
|
+
"--output",
|
|
88
|
+
reportPath,
|
|
89
|
+
"--repository-root",
|
|
90
|
+
fixture.fixtureRoot,
|
|
91
|
+
]);
|
|
92
|
+
|
|
93
|
+
assert.equal(childProcess.status, 0);
|
|
94
|
+
assert.equal(childProcess.stdout, "");
|
|
95
|
+
const report = JSON.parse(readFileSync(reportPath, "utf8"));
|
|
96
|
+
assert.equal(report.summary.directCount, 0);
|
|
97
|
+
assert.equal(report.summary.findingCount, 0);
|
|
98
|
+
} finally {
|
|
99
|
+
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("returns a strict nonblocking failure through the CLI", () => {
|
|
104
|
+
const fixture = createAuditFixture({
|
|
105
|
+
claudeHooks: {
|
|
106
|
+
hooks: {
|
|
107
|
+
SessionStart: [{
|
|
108
|
+
hooks: [{ command: "node cleanup.mjs", type: "command" }],
|
|
109
|
+
}],
|
|
110
|
+
},
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
const catalogPath = path.join(fixture.fixtureRoot, "lifecycle.json");
|
|
114
|
+
writeJson(catalogPath, {
|
|
115
|
+
hooks: {
|
|
116
|
+
"external:cleanup.mjs": {
|
|
117
|
+
lifecycle: "delete",
|
|
118
|
+
reason: "The installed entry is retired.",
|
|
119
|
+
replacement: null,
|
|
120
|
+
},
|
|
121
|
+
},
|
|
122
|
+
});
|
|
123
|
+
try {
|
|
124
|
+
const childProcess = runAuditCli([
|
|
125
|
+
"--format",
|
|
126
|
+
"json",
|
|
127
|
+
"--catalog",
|
|
128
|
+
catalogPath,
|
|
129
|
+
"--home",
|
|
130
|
+
fixture.homeDirectory,
|
|
131
|
+
"--installed",
|
|
132
|
+
"--require-nonblocking",
|
|
133
|
+
"--repository-root",
|
|
134
|
+
fixture.fixtureRoot,
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
assert.equal(childProcess.status, 1);
|
|
138
|
+
assert.equal(childProcess.stderr, "");
|
|
139
|
+
const report = JSON.parse(childProcess.stdout);
|
|
140
|
+
assert.equal(report.registrationEligibility.status, "ineligible");
|
|
141
|
+
assert.equal(
|
|
142
|
+
report.findings.some(({ code, target }) =>
|
|
143
|
+
code === "NONBLOCKING_LIFECYCLE_MISMATCH" && target === "external:cleanup.mjs",
|
|
144
|
+
),
|
|
145
|
+
true,
|
|
146
|
+
);
|
|
147
|
+
} finally {
|
|
148
|
+
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
149
|
+
}
|
|
150
|
+
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import { spawnSync } from "node:child_process";
|
|
3
|
-
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import test from "node:test";
|
|
@@ -73,6 +73,37 @@ test("renders deterministic private JSON", () => {
|
|
|
73
73
|
assert.equal(first.includes("python3 ${CLAUDE_PLUGIN_ROOT}"), false);
|
|
74
74
|
});
|
|
75
75
|
|
|
76
|
+
test("does not compare the live graph with retired baseline counts", () => {
|
|
77
|
+
const report = auditHooks({ repositoryRoot });
|
|
78
|
+
assert.equal(report.summary.directCount, report.directRegistrations.length);
|
|
79
|
+
assert.equal(report.summary.effectiveCount, report.hostedRegistrations.length);
|
|
80
|
+
assert.equal(report.summary.logicalAssociationCount, report.logicalRegistrations.length);
|
|
81
|
+
assert.ok(report.summary.directCount > 0);
|
|
82
|
+
assert.ok(report.summary.effectiveCount > 0);
|
|
83
|
+
assert.ok(report.summary.logicalAssociationCount > report.summary.directCount);
|
|
84
|
+
assert.equal(report.findings.some(({ code }) => code === "DIRECT_COUNT_DRIFT"), false);
|
|
85
|
+
assert.equal(report.findings.some(({ code }) => code === "HOSTED_COUNT_DRIFT"), false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("does not execute registered commands", () => {
|
|
89
|
+
const fixture = createAuditFixture({
|
|
90
|
+
canonicalHooks: {
|
|
91
|
+
PreToolUse: [{
|
|
92
|
+
hooks: [{
|
|
93
|
+
command: "node -e \"require('node:fs').writeFileSync('audit-executed', 'yes')\"",
|
|
94
|
+
type: "command",
|
|
95
|
+
}],
|
|
96
|
+
}],
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
try {
|
|
100
|
+
auditHooks({ repositoryRoot: fixture.fixtureRoot });
|
|
101
|
+
assert.equal(existsSync(path.join(fixture.fixtureRoot, "audit-executed")), false);
|
|
102
|
+
} finally {
|
|
103
|
+
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
104
|
+
}
|
|
105
|
+
});
|
|
106
|
+
|
|
76
107
|
test("reports every unclassified target", () => {
|
|
77
108
|
const temporaryRoot = mkdtempSync(path.join(tmpdir(), "hook-audit-"));
|
|
78
109
|
const catalogPath = path.join(temporaryRoot, "lifecycle.json");
|
|
@@ -86,6 +117,15 @@ test("reports every unclassified target", () => {
|
|
|
86
117
|
}
|
|
87
118
|
});
|
|
88
119
|
|
|
120
|
+
test("reports canonical native Git hooks from the installer registry", () => {
|
|
121
|
+
const report = auditHooks({ repositoryRoot });
|
|
122
|
+
assert.deepEqual(
|
|
123
|
+
report.knownGitRegistrations.map(({ target }) => target),
|
|
124
|
+
["git:pre-commit", "git:pre-push", "git:post-commit"],
|
|
125
|
+
);
|
|
126
|
+
assert.equal(report.summary.knownGitHookCount, 3);
|
|
127
|
+
});
|
|
128
|
+
|
|
89
129
|
test("keeps each hosted matcher as narrow as its roster", () => {
|
|
90
130
|
const fixture = createAuditFixture({
|
|
91
131
|
canonicalHooks: {
|
|
@@ -161,6 +201,7 @@ test("classifies installed-only targets", () => {
|
|
|
161
201
|
catalogPath,
|
|
162
202
|
homeDirectory: fixture.homeDirectory,
|
|
163
203
|
includeInstalled: true,
|
|
204
|
+
requireNonblocking: true,
|
|
164
205
|
repositoryRoot: temporaryRoot,
|
|
165
206
|
});
|
|
166
207
|
const missingTargets = new Set(
|
|
@@ -266,6 +307,46 @@ test("reports missing installed dispatcher targets", () => {
|
|
|
266
307
|
}
|
|
267
308
|
});
|
|
268
309
|
|
|
310
|
+
test("rejects an installed-only registration without nonblocking lifecycle", () => {
|
|
311
|
+
const fixture = createAuditFixture({
|
|
312
|
+
claudeHooks: {
|
|
313
|
+
hooks: {
|
|
314
|
+
SessionStart: [{
|
|
315
|
+
hooks: [{ command: "node cleanup.mjs", type: "command" }],
|
|
316
|
+
}],
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
});
|
|
320
|
+
const catalogPath = path.join(fixture.fixtureRoot, "lifecycle.json");
|
|
321
|
+
writeJson(catalogPath, {
|
|
322
|
+
hooks: {
|
|
323
|
+
"external:cleanup.mjs": {
|
|
324
|
+
lifecycle: "delete",
|
|
325
|
+
reason: "The installed entry is retired.",
|
|
326
|
+
replacement: null,
|
|
327
|
+
},
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
try {
|
|
331
|
+
const report = auditHooks({
|
|
332
|
+
catalogPath,
|
|
333
|
+
homeDirectory: fixture.homeDirectory,
|
|
334
|
+
includeInstalled: true,
|
|
335
|
+
requireNonblocking: true,
|
|
336
|
+
repositoryRoot: fixture.fixtureRoot,
|
|
337
|
+
});
|
|
338
|
+
assert.equal(report.registrationEligibility.status, "ineligible");
|
|
339
|
+
assert.equal(
|
|
340
|
+
report.findings.some(({ code, target }) =>
|
|
341
|
+
code === "NONBLOCKING_LIFECYCLE_MISMATCH" && target === "external:cleanup.mjs",
|
|
342
|
+
),
|
|
343
|
+
true,
|
|
344
|
+
);
|
|
345
|
+
} finally {
|
|
346
|
+
rmSync(fixture.fixtureRoot, { force: true, recursive: true });
|
|
347
|
+
}
|
|
348
|
+
});
|
|
349
|
+
|
|
269
350
|
test("checks canonical managed parity for Codex registrations", () => {
|
|
270
351
|
const fixture = createAuditFixture({
|
|
271
352
|
canonicalHooks: {
|
|
@@ -355,6 +436,7 @@ test("finds default Git hooks from a worktree", () => {
|
|
|
355
436
|
runGit(sourceRoot, ["commit", "--quiet", "--allow-empty", "-m", "fixture"], environment);
|
|
356
437
|
runGit(sourceRoot, ["worktree", "add", "--quiet", worktreeRoot, "HEAD"], environment);
|
|
357
438
|
writeHookFile(path.join(sourceRoot, ".git", "hooks"), "pre-commit");
|
|
439
|
+
writeHookFile(path.join(sourceRoot, ".git", "hooks"), "post-commit");
|
|
358
440
|
const canonicalHooksRoot = path.join(worktreeRoot, "packages", "claude-dev-env", "hooks");
|
|
359
441
|
const homeDirectory = path.join(sourceRoot, "home");
|
|
360
442
|
mkdirSync(canonicalHooksRoot, { recursive: true });
|
|
@@ -366,9 +448,19 @@ test("finds default Git hooks from a worktree", () => {
|
|
|
366
448
|
const report = auditHooks({
|
|
367
449
|
homeDirectory,
|
|
368
450
|
includeInstalled: true,
|
|
451
|
+
requireNonblocking: true,
|
|
369
452
|
repositoryRoot: worktreeRoot,
|
|
370
453
|
});
|
|
371
|
-
assert.deepEqual(report.gitRegistrations.map(({ target }) => target), [
|
|
454
|
+
assert.deepEqual(report.gitRegistrations.map(({ target }) => target), [
|
|
455
|
+
"git:pre-commit",
|
|
456
|
+
"git:post-commit",
|
|
457
|
+
]);
|
|
458
|
+
assert.equal(
|
|
459
|
+
report.findings.some(({ code, target }) =>
|
|
460
|
+
code === "NATIVE_GIT_BLOCKING_HOOK" && target === "git:pre-commit",
|
|
461
|
+
),
|
|
462
|
+
true,
|
|
463
|
+
);
|
|
372
464
|
} finally {
|
|
373
465
|
if (previousGlobalConfig === undefined) delete process.env.GIT_CONFIG_GLOBAL;
|
|
374
466
|
else process.env.GIT_CONFIG_GLOBAL = previousGlobalConfig;
|