crackerjack 0.37.9__py3-none-any.whl → 0.45.2__py3-none-any.whl
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.
- crackerjack/README.md +19 -0
- crackerjack/__init__.py +30 -1
- crackerjack/__main__.py +342 -1263
- crackerjack/adapters/README.md +18 -0
- crackerjack/adapters/__init__.py +27 -5
- crackerjack/adapters/_output_paths.py +167 -0
- crackerjack/adapters/_qa_adapter_base.py +309 -0
- crackerjack/adapters/_tool_adapter_base.py +706 -0
- crackerjack/adapters/ai/README.md +65 -0
- crackerjack/adapters/ai/__init__.py +5 -0
- crackerjack/adapters/ai/claude.py +853 -0
- crackerjack/adapters/complexity/README.md +53 -0
- crackerjack/adapters/complexity/__init__.py +10 -0
- crackerjack/adapters/complexity/complexipy.py +641 -0
- crackerjack/adapters/dependency/__init__.py +22 -0
- crackerjack/adapters/dependency/pip_audit.py +418 -0
- crackerjack/adapters/format/README.md +72 -0
- crackerjack/adapters/format/__init__.py +11 -0
- crackerjack/adapters/format/mdformat.py +313 -0
- crackerjack/adapters/format/ruff.py +516 -0
- crackerjack/adapters/lint/README.md +47 -0
- crackerjack/adapters/lint/__init__.py +11 -0
- crackerjack/adapters/lint/codespell.py +273 -0
- crackerjack/adapters/lsp/README.md +49 -0
- crackerjack/adapters/lsp/__init__.py +27 -0
- crackerjack/adapters/{rust_tool_manager.py → lsp/_manager.py} +3 -3
- crackerjack/adapters/{skylos_adapter.py → lsp/skylos.py} +59 -7
- crackerjack/adapters/{zuban_adapter.py → lsp/zuban.py} +3 -6
- crackerjack/adapters/refactor/README.md +59 -0
- crackerjack/adapters/refactor/__init__.py +12 -0
- crackerjack/adapters/refactor/creosote.py +318 -0
- crackerjack/adapters/refactor/refurb.py +406 -0
- crackerjack/adapters/refactor/skylos.py +494 -0
- crackerjack/adapters/sast/README.md +132 -0
- crackerjack/adapters/sast/__init__.py +32 -0
- crackerjack/adapters/sast/_base.py +201 -0
- crackerjack/adapters/sast/bandit.py +423 -0
- crackerjack/adapters/sast/pyscn.py +405 -0
- crackerjack/adapters/sast/semgrep.py +241 -0
- crackerjack/adapters/security/README.md +111 -0
- crackerjack/adapters/security/__init__.py +17 -0
- crackerjack/adapters/security/gitleaks.py +339 -0
- crackerjack/adapters/type/README.md +52 -0
- crackerjack/adapters/type/__init__.py +12 -0
- crackerjack/adapters/type/pyrefly.py +402 -0
- crackerjack/adapters/type/ty.py +402 -0
- crackerjack/adapters/type/zuban.py +522 -0
- crackerjack/adapters/utility/README.md +51 -0
- crackerjack/adapters/utility/__init__.py +10 -0
- crackerjack/adapters/utility/checks.py +884 -0
- crackerjack/agents/README.md +264 -0
- crackerjack/agents/__init__.py +40 -12
- crackerjack/agents/base.py +1 -0
- crackerjack/agents/claude_code_bridge.py +641 -0
- crackerjack/agents/coordinator.py +49 -53
- crackerjack/agents/dry_agent.py +187 -3
- crackerjack/agents/enhanced_coordinator.py +279 -0
- crackerjack/agents/enhanced_proactive_agent.py +185 -0
- crackerjack/agents/error_middleware.py +53 -0
- crackerjack/agents/formatting_agent.py +6 -8
- crackerjack/agents/helpers/__init__.py +9 -0
- crackerjack/agents/helpers/performance/__init__.py +22 -0
- crackerjack/agents/helpers/performance/performance_ast_analyzer.py +357 -0
- crackerjack/agents/helpers/performance/performance_pattern_detector.py +909 -0
- crackerjack/agents/helpers/performance/performance_recommender.py +572 -0
- crackerjack/agents/helpers/refactoring/__init__.py +22 -0
- crackerjack/agents/helpers/refactoring/code_transformer.py +536 -0
- crackerjack/agents/helpers/refactoring/complexity_analyzer.py +344 -0
- crackerjack/agents/helpers/refactoring/dead_code_detector.py +437 -0
- crackerjack/agents/helpers/test_creation/__init__.py +19 -0
- crackerjack/agents/helpers/test_creation/test_ast_analyzer.py +216 -0
- crackerjack/agents/helpers/test_creation/test_coverage_analyzer.py +643 -0
- crackerjack/agents/helpers/test_creation/test_template_generator.py +1031 -0
- crackerjack/agents/performance_agent.py +121 -1152
- crackerjack/agents/refactoring_agent.py +156 -655
- crackerjack/agents/semantic_agent.py +479 -0
- crackerjack/agents/semantic_helpers.py +356 -0
- crackerjack/agents/test_creation_agent.py +19 -1605
- crackerjack/api.py +5 -7
- crackerjack/cli/README.md +394 -0
- crackerjack/cli/__init__.py +1 -1
- crackerjack/cli/cache_handlers.py +23 -18
- crackerjack/cli/cache_handlers_enhanced.py +1 -4
- crackerjack/cli/facade.py +70 -8
- crackerjack/cli/formatting.py +13 -0
- crackerjack/cli/handlers/__init__.py +85 -0
- crackerjack/cli/handlers/advanced.py +103 -0
- crackerjack/cli/handlers/ai_features.py +62 -0
- crackerjack/cli/handlers/analytics.py +479 -0
- crackerjack/cli/handlers/changelog.py +271 -0
- crackerjack/cli/handlers/config_handlers.py +16 -0
- crackerjack/cli/handlers/coverage.py +84 -0
- crackerjack/cli/handlers/documentation.py +280 -0
- crackerjack/cli/handlers/main_handlers.py +497 -0
- crackerjack/cli/handlers/monitoring.py +371 -0
- crackerjack/cli/handlers.py +249 -49
- crackerjack/cli/interactive.py +8 -5
- crackerjack/cli/options.py +203 -110
- crackerjack/cli/semantic_handlers.py +292 -0
- crackerjack/cli/version.py +19 -0
- crackerjack/code_cleaner.py +60 -24
- crackerjack/config/README.md +472 -0
- crackerjack/config/__init__.py +256 -0
- crackerjack/config/global_lock_config.py +191 -54
- crackerjack/config/hooks.py +188 -16
- crackerjack/config/loader.py +239 -0
- crackerjack/config/settings.py +141 -0
- crackerjack/config/tool_commands.py +331 -0
- crackerjack/core/README.md +393 -0
- crackerjack/core/async_workflow_orchestrator.py +79 -53
- crackerjack/core/autofix_coordinator.py +22 -9
- crackerjack/core/container.py +10 -9
- crackerjack/core/enhanced_container.py +9 -9
- crackerjack/core/performance.py +1 -1
- crackerjack/core/performance_monitor.py +5 -3
- crackerjack/core/phase_coordinator.py +1018 -634
- crackerjack/core/proactive_workflow.py +3 -3
- crackerjack/core/retry.py +275 -0
- crackerjack/core/service_watchdog.py +167 -23
- crackerjack/core/session_coordinator.py +187 -382
- crackerjack/core/timeout_manager.py +161 -44
- crackerjack/core/workflow/__init__.py +21 -0
- crackerjack/core/workflow/workflow_ai_coordinator.py +863 -0
- crackerjack/core/workflow/workflow_event_orchestrator.py +1107 -0
- crackerjack/core/workflow/workflow_issue_parser.py +714 -0
- crackerjack/core/workflow/workflow_phase_executor.py +1158 -0
- crackerjack/core/workflow/workflow_security_gates.py +400 -0
- crackerjack/core/workflow_orchestrator.py +1247 -953
- crackerjack/data/README.md +11 -0
- crackerjack/data/__init__.py +8 -0
- crackerjack/data/models.py +79 -0
- crackerjack/data/repository.py +210 -0
- crackerjack/decorators/README.md +180 -0
- crackerjack/decorators/__init__.py +35 -0
- crackerjack/decorators/error_handling.py +649 -0
- crackerjack/decorators/error_handling_decorators.py +334 -0
- crackerjack/decorators/helpers.py +58 -0
- crackerjack/decorators/patterns.py +281 -0
- crackerjack/decorators/utils.py +58 -0
- crackerjack/docs/README.md +11 -0
- crackerjack/docs/generated/api/CLI_REFERENCE.md +1 -1
- crackerjack/documentation/README.md +11 -0
- crackerjack/documentation/ai_templates.py +1 -1
- crackerjack/documentation/dual_output_generator.py +11 -9
- crackerjack/documentation/reference_generator.py +104 -59
- crackerjack/dynamic_config.py +52 -61
- crackerjack/errors.py +1 -1
- crackerjack/events/README.md +11 -0
- crackerjack/events/__init__.py +16 -0
- crackerjack/events/telemetry.py +175 -0
- crackerjack/events/workflow_bus.py +346 -0
- crackerjack/exceptions/README.md +301 -0
- crackerjack/exceptions/__init__.py +5 -0
- crackerjack/exceptions/config.py +4 -0
- crackerjack/exceptions/tool_execution_error.py +245 -0
- crackerjack/executors/README.md +591 -0
- crackerjack/executors/__init__.py +2 -0
- crackerjack/executors/async_hook_executor.py +539 -77
- crackerjack/executors/cached_hook_executor.py +3 -3
- crackerjack/executors/hook_executor.py +967 -102
- crackerjack/executors/hook_lock_manager.py +31 -22
- crackerjack/executors/individual_hook_executor.py +66 -32
- crackerjack/executors/lsp_aware_hook_executor.py +136 -57
- crackerjack/executors/progress_hook_executor.py +282 -0
- crackerjack/executors/tool_proxy.py +23 -7
- crackerjack/hooks/README.md +485 -0
- crackerjack/hooks/lsp_hook.py +8 -9
- crackerjack/intelligence/README.md +557 -0
- crackerjack/interactive.py +37 -10
- crackerjack/managers/README.md +369 -0
- crackerjack/managers/async_hook_manager.py +41 -57
- crackerjack/managers/hook_manager.py +449 -79
- crackerjack/managers/publish_manager.py +81 -36
- crackerjack/managers/test_command_builder.py +290 -12
- crackerjack/managers/test_executor.py +93 -8
- crackerjack/managers/test_manager.py +1082 -75
- crackerjack/managers/test_progress.py +118 -26
- crackerjack/mcp/README.md +374 -0
- crackerjack/mcp/cache.py +25 -2
- crackerjack/mcp/client_runner.py +35 -18
- crackerjack/mcp/context.py +9 -9
- crackerjack/mcp/dashboard.py +24 -8
- crackerjack/mcp/enhanced_progress_monitor.py +34 -23
- crackerjack/mcp/file_monitor.py +27 -6
- crackerjack/mcp/progress_components.py +45 -34
- crackerjack/mcp/progress_monitor.py +6 -9
- crackerjack/mcp/rate_limiter.py +11 -7
- crackerjack/mcp/server.py +2 -0
- crackerjack/mcp/server_core.py +187 -55
- crackerjack/mcp/service_watchdog.py +12 -9
- crackerjack/mcp/task_manager.py +2 -2
- crackerjack/mcp/tools/README.md +27 -0
- crackerjack/mcp/tools/__init__.py +2 -0
- crackerjack/mcp/tools/core_tools.py +75 -52
- crackerjack/mcp/tools/execution_tools.py +87 -31
- crackerjack/mcp/tools/intelligence_tools.py +2 -2
- crackerjack/mcp/tools/proactive_tools.py +1 -1
- crackerjack/mcp/tools/semantic_tools.py +584 -0
- crackerjack/mcp/tools/utility_tools.py +180 -132
- crackerjack/mcp/tools/workflow_executor.py +87 -46
- crackerjack/mcp/websocket/README.md +31 -0
- crackerjack/mcp/websocket/app.py +11 -1
- crackerjack/mcp/websocket/event_bridge.py +188 -0
- crackerjack/mcp/websocket/jobs.py +27 -4
- crackerjack/mcp/websocket/monitoring/__init__.py +25 -0
- crackerjack/mcp/websocket/monitoring/api/__init__.py +19 -0
- crackerjack/mcp/websocket/monitoring/api/dependencies.py +141 -0
- crackerjack/mcp/websocket/monitoring/api/heatmap.py +154 -0
- crackerjack/mcp/websocket/monitoring/api/intelligence.py +199 -0
- crackerjack/mcp/websocket/monitoring/api/metrics.py +203 -0
- crackerjack/mcp/websocket/monitoring/api/telemetry.py +101 -0
- crackerjack/mcp/websocket/monitoring/dashboard.py +18 -0
- crackerjack/mcp/websocket/monitoring/factory.py +109 -0
- crackerjack/mcp/websocket/monitoring/filters.py +10 -0
- crackerjack/mcp/websocket/monitoring/metrics.py +64 -0
- crackerjack/mcp/websocket/monitoring/models.py +90 -0
- crackerjack/mcp/websocket/monitoring/utils.py +171 -0
- crackerjack/mcp/websocket/monitoring/websocket_manager.py +78 -0
- crackerjack/mcp/websocket/monitoring/websockets/__init__.py +17 -0
- crackerjack/mcp/websocket/monitoring/websockets/dependencies.py +126 -0
- crackerjack/mcp/websocket/monitoring/websockets/heatmap.py +176 -0
- crackerjack/mcp/websocket/monitoring/websockets/intelligence.py +291 -0
- crackerjack/mcp/websocket/monitoring/websockets/metrics.py +291 -0
- crackerjack/mcp/websocket/monitoring_endpoints.py +16 -2930
- crackerjack/mcp/websocket/server.py +1 -3
- crackerjack/mcp/websocket/websocket_handler.py +107 -6
- crackerjack/models/README.md +308 -0
- crackerjack/models/__init__.py +10 -1
- crackerjack/models/config.py +639 -22
- crackerjack/models/config_adapter.py +6 -6
- crackerjack/models/protocols.py +1167 -23
- crackerjack/models/pydantic_models.py +320 -0
- crackerjack/models/qa_config.py +145 -0
- crackerjack/models/qa_results.py +134 -0
- crackerjack/models/results.py +35 -0
- crackerjack/models/semantic_models.py +258 -0
- crackerjack/models/task.py +19 -3
- crackerjack/models/test_models.py +60 -0
- crackerjack/monitoring/README.md +11 -0
- crackerjack/monitoring/ai_agent_watchdog.py +5 -4
- crackerjack/monitoring/metrics_collector.py +4 -3
- crackerjack/monitoring/regression_prevention.py +4 -3
- crackerjack/monitoring/websocket_server.py +4 -241
- crackerjack/orchestration/README.md +340 -0
- crackerjack/orchestration/__init__.py +43 -0
- crackerjack/orchestration/advanced_orchestrator.py +20 -67
- crackerjack/orchestration/cache/README.md +312 -0
- crackerjack/orchestration/cache/__init__.py +37 -0
- crackerjack/orchestration/cache/memory_cache.py +338 -0
- crackerjack/orchestration/cache/tool_proxy_cache.py +340 -0
- crackerjack/orchestration/config.py +297 -0
- crackerjack/orchestration/coverage_improvement.py +13 -6
- crackerjack/orchestration/execution_strategies.py +6 -6
- crackerjack/orchestration/hook_orchestrator.py +1398 -0
- crackerjack/orchestration/strategies/README.md +401 -0
- crackerjack/orchestration/strategies/__init__.py +39 -0
- crackerjack/orchestration/strategies/adaptive_strategy.py +630 -0
- crackerjack/orchestration/strategies/parallel_strategy.py +237 -0
- crackerjack/orchestration/strategies/sequential_strategy.py +299 -0
- crackerjack/orchestration/test_progress_streamer.py +1 -1
- crackerjack/plugins/README.md +11 -0
- crackerjack/plugins/hooks.py +3 -2
- crackerjack/plugins/loader.py +3 -3
- crackerjack/plugins/managers.py +1 -1
- crackerjack/py313.py +191 -0
- crackerjack/security/README.md +11 -0
- crackerjack/services/README.md +374 -0
- crackerjack/services/__init__.py +8 -21
- crackerjack/services/ai/README.md +295 -0
- crackerjack/services/ai/__init__.py +7 -0
- crackerjack/services/ai/advanced_optimizer.py +878 -0
- crackerjack/services/{contextual_ai_assistant.py → ai/contextual_ai_assistant.py} +5 -3
- crackerjack/services/ai/embeddings.py +444 -0
- crackerjack/services/ai/intelligent_commit.py +328 -0
- crackerjack/services/ai/predictive_analytics.py +510 -0
- crackerjack/services/api_extractor.py +5 -3
- crackerjack/services/bounded_status_operations.py +45 -5
- crackerjack/services/cache.py +249 -318
- crackerjack/services/changelog_automation.py +7 -3
- crackerjack/services/command_execution_service.py +305 -0
- crackerjack/services/config_integrity.py +83 -39
- crackerjack/services/config_merge.py +9 -6
- crackerjack/services/config_service.py +198 -0
- crackerjack/services/config_template.py +13 -26
- crackerjack/services/coverage_badge_service.py +6 -4
- crackerjack/services/coverage_ratchet.py +53 -27
- crackerjack/services/debug.py +18 -7
- crackerjack/services/dependency_analyzer.py +4 -4
- crackerjack/services/dependency_monitor.py +13 -13
- crackerjack/services/documentation_generator.py +4 -2
- crackerjack/services/documentation_service.py +62 -33
- crackerjack/services/enhanced_filesystem.py +81 -27
- crackerjack/services/enterprise_optimizer.py +1 -1
- crackerjack/services/error_pattern_analyzer.py +10 -10
- crackerjack/services/file_filter.py +221 -0
- crackerjack/services/file_hasher.py +5 -7
- crackerjack/services/file_io_service.py +361 -0
- crackerjack/services/file_modifier.py +615 -0
- crackerjack/services/filesystem.py +80 -109
- crackerjack/services/git.py +99 -5
- crackerjack/services/health_metrics.py +4 -6
- crackerjack/services/heatmap_generator.py +12 -3
- crackerjack/services/incremental_executor.py +380 -0
- crackerjack/services/initialization.py +101 -49
- crackerjack/services/log_manager.py +2 -2
- crackerjack/services/logging.py +120 -68
- crackerjack/services/lsp_client.py +12 -12
- crackerjack/services/memory_optimizer.py +27 -22
- crackerjack/services/monitoring/README.md +30 -0
- crackerjack/services/monitoring/__init__.py +9 -0
- crackerjack/services/monitoring/dependency_monitor.py +678 -0
- crackerjack/services/monitoring/error_pattern_analyzer.py +676 -0
- crackerjack/services/monitoring/health_metrics.py +716 -0
- crackerjack/services/monitoring/metrics.py +587 -0
- crackerjack/services/{performance_benchmarks.py → monitoring/performance_benchmarks.py} +100 -14
- crackerjack/services/{performance_cache.py → monitoring/performance_cache.py} +21 -15
- crackerjack/services/{performance_monitor.py → monitoring/performance_monitor.py} +10 -6
- crackerjack/services/parallel_executor.py +166 -55
- crackerjack/services/patterns/__init__.py +142 -0
- crackerjack/services/patterns/agents.py +107 -0
- crackerjack/services/patterns/code/__init__.py +15 -0
- crackerjack/services/patterns/code/detection.py +118 -0
- crackerjack/services/patterns/code/imports.py +107 -0
- crackerjack/services/patterns/code/paths.py +159 -0
- crackerjack/services/patterns/code/performance.py +119 -0
- crackerjack/services/patterns/code/replacement.py +36 -0
- crackerjack/services/patterns/core.py +212 -0
- crackerjack/services/patterns/documentation/__init__.py +14 -0
- crackerjack/services/patterns/documentation/badges_markdown.py +96 -0
- crackerjack/services/patterns/documentation/comments_blocks.py +83 -0
- crackerjack/services/patterns/documentation/docstrings.py +89 -0
- crackerjack/services/patterns/formatting.py +226 -0
- crackerjack/services/patterns/operations.py +339 -0
- crackerjack/services/patterns/security/__init__.py +23 -0
- crackerjack/services/patterns/security/code_injection.py +122 -0
- crackerjack/services/patterns/security/credentials.py +190 -0
- crackerjack/services/patterns/security/path_traversal.py +221 -0
- crackerjack/services/patterns/security/unsafe_operations.py +216 -0
- crackerjack/services/patterns/templates.py +62 -0
- crackerjack/services/patterns/testing/__init__.py +18 -0
- crackerjack/services/patterns/testing/error_patterns.py +107 -0
- crackerjack/services/patterns/testing/pytest_output.py +126 -0
- crackerjack/services/patterns/tool_output/__init__.py +16 -0
- crackerjack/services/patterns/tool_output/bandit.py +72 -0
- crackerjack/services/patterns/tool_output/other.py +97 -0
- crackerjack/services/patterns/tool_output/pyright.py +67 -0
- crackerjack/services/patterns/tool_output/ruff.py +44 -0
- crackerjack/services/patterns/url_sanitization.py +114 -0
- crackerjack/services/patterns/utilities.py +42 -0
- crackerjack/services/patterns/utils.py +339 -0
- crackerjack/services/patterns/validation.py +46 -0
- crackerjack/services/patterns/versioning.py +62 -0
- crackerjack/services/predictive_analytics.py +21 -8
- crackerjack/services/profiler.py +280 -0
- crackerjack/services/quality/README.md +415 -0
- crackerjack/services/quality/__init__.py +11 -0
- crackerjack/services/quality/anomaly_detector.py +392 -0
- crackerjack/services/quality/pattern_cache.py +333 -0
- crackerjack/services/quality/pattern_detector.py +479 -0
- crackerjack/services/quality/qa_orchestrator.py +491 -0
- crackerjack/services/{quality_baseline.py → quality/quality_baseline.py} +163 -2
- crackerjack/services/{quality_baseline_enhanced.py → quality/quality_baseline_enhanced.py} +4 -1
- crackerjack/services/{quality_intelligence.py → quality/quality_intelligence.py} +180 -16
- crackerjack/services/regex_patterns.py +58 -2987
- crackerjack/services/regex_utils.py +55 -29
- crackerjack/services/secure_status_formatter.py +42 -15
- crackerjack/services/secure_subprocess.py +35 -2
- crackerjack/services/security.py +16 -8
- crackerjack/services/server_manager.py +40 -51
- crackerjack/services/smart_scheduling.py +46 -6
- crackerjack/services/status_authentication.py +3 -3
- crackerjack/services/thread_safe_status_collector.py +1 -0
- crackerjack/services/tool_filter.py +368 -0
- crackerjack/services/tool_version_service.py +9 -5
- crackerjack/services/unified_config.py +43 -351
- crackerjack/services/vector_store.py +689 -0
- crackerjack/services/version_analyzer.py +6 -4
- crackerjack/services/version_checker.py +14 -8
- crackerjack/services/zuban_lsp_service.py +5 -4
- crackerjack/slash_commands/README.md +11 -0
- crackerjack/slash_commands/init.md +2 -12
- crackerjack/slash_commands/run.md +84 -50
- crackerjack/tools/README.md +11 -0
- crackerjack/tools/__init__.py +30 -0
- crackerjack/tools/_git_utils.py +105 -0
- crackerjack/tools/check_added_large_files.py +139 -0
- crackerjack/tools/check_ast.py +105 -0
- crackerjack/tools/check_json.py +103 -0
- crackerjack/tools/check_jsonschema.py +297 -0
- crackerjack/tools/check_toml.py +103 -0
- crackerjack/tools/check_yaml.py +110 -0
- crackerjack/tools/codespell_wrapper.py +72 -0
- crackerjack/tools/end_of_file_fixer.py +202 -0
- crackerjack/tools/format_json.py +128 -0
- crackerjack/tools/mdformat_wrapper.py +114 -0
- crackerjack/tools/trailing_whitespace.py +198 -0
- crackerjack/tools/validate_regex_patterns.py +7 -3
- crackerjack/ui/README.md +11 -0
- crackerjack/ui/dashboard_renderer.py +28 -0
- crackerjack/ui/templates/README.md +11 -0
- crackerjack/utils/console_utils.py +13 -0
- crackerjack/utils/dependency_guard.py +230 -0
- crackerjack/utils/retry_utils.py +275 -0
- crackerjack/workflows/README.md +590 -0
- crackerjack/workflows/__init__.py +46 -0
- crackerjack/workflows/actions.py +811 -0
- crackerjack/workflows/auto_fix.py +444 -0
- crackerjack/workflows/container_builder.py +499 -0
- crackerjack/workflows/definitions.py +443 -0
- crackerjack/workflows/engine.py +177 -0
- crackerjack/workflows/event_bridge.py +242 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/METADATA +678 -98
- crackerjack-0.45.2.dist-info/RECORD +478 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/WHEEL +1 -1
- crackerjack/managers/test_manager_backup.py +0 -1075
- crackerjack/mcp/tools/execution_tools_backup.py +0 -1011
- crackerjack/mixins/__init__.py +0 -3
- crackerjack/mixins/error_handling.py +0 -145
- crackerjack/services/config.py +0 -358
- crackerjack/ui/server_panels.py +0 -125
- crackerjack-0.37.9.dist-info/RECORD +0 -231
- /crackerjack/adapters/{rust_tool_adapter.py → lsp/_base.py} +0 -0
- /crackerjack/adapters/{lsp_client.py → lsp/_client.py} +0 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/entry_points.txt +0 -0
- {crackerjack-0.37.9.dist-info → crackerjack-0.45.2.dist-info}/licenses/LICENSE +0 -0
|
@@ -1,52 +1,80 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import re
|
|
3
4
|
import typing as t
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
6
|
-
from
|
|
7
|
+
from acb.console import Console
|
|
8
|
+
from acb.depends import Inject, depends
|
|
9
|
+
from acb.logger import Logger
|
|
10
|
+
from rich import box
|
|
11
|
+
from rich.panel import Panel
|
|
12
|
+
from rich.progress import (
|
|
13
|
+
BarColumn,
|
|
14
|
+
MofNCompleteColumn,
|
|
15
|
+
Progress,
|
|
16
|
+
SpinnerColumn,
|
|
17
|
+
TextColumn,
|
|
18
|
+
TimeElapsedColumn,
|
|
19
|
+
)
|
|
20
|
+
from rich.table import Table
|
|
7
21
|
|
|
8
|
-
from crackerjack.
|
|
22
|
+
from crackerjack.cli.formatting import separator as make_separator
|
|
23
|
+
from crackerjack.code_cleaner import CodeCleaner
|
|
24
|
+
from crackerjack.config import get_console_width
|
|
9
25
|
from crackerjack.core.autofix_coordinator import AutofixCoordinator
|
|
10
|
-
from crackerjack.
|
|
26
|
+
from crackerjack.core.session_coordinator import SessionCoordinator
|
|
27
|
+
from crackerjack.decorators import handle_errors
|
|
11
28
|
from crackerjack.models.protocols import (
|
|
12
29
|
ConfigMergeServiceProtocol,
|
|
13
30
|
FileSystemInterface,
|
|
14
31
|
GitInterface,
|
|
15
32
|
HookManager,
|
|
33
|
+
MemoryOptimizerProtocol,
|
|
16
34
|
OptionsProtocol,
|
|
17
35
|
PublishManager,
|
|
18
36
|
TestManagerProtocol,
|
|
19
37
|
)
|
|
20
|
-
from crackerjack.
|
|
21
|
-
|
|
22
|
-
|
|
38
|
+
from crackerjack.models.task import HookResult
|
|
39
|
+
from crackerjack.services.memory_optimizer import create_lazy_service
|
|
40
|
+
from crackerjack.services.monitoring.performance_cache import (
|
|
41
|
+
FileSystemCache,
|
|
42
|
+
GitOperationCache,
|
|
23
43
|
)
|
|
24
44
|
from crackerjack.services.parallel_executor import (
|
|
25
|
-
|
|
26
|
-
|
|
45
|
+
AsyncCommandExecutor,
|
|
46
|
+
ParallelHookExecutor,
|
|
27
47
|
)
|
|
28
|
-
from crackerjack.services.performance_cache import get_filesystem_cache, get_git_cache
|
|
29
48
|
|
|
30
|
-
|
|
49
|
+
if t.TYPE_CHECKING:
|
|
50
|
+
pass # All imports moved to top-level for runtime availability
|
|
31
51
|
|
|
32
52
|
|
|
33
|
-
class PhaseCoordinator
|
|
53
|
+
class PhaseCoordinator:
|
|
54
|
+
@depends.inject
|
|
34
55
|
def __init__(
|
|
35
56
|
self,
|
|
36
|
-
console: Console,
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
57
|
+
console: Inject[Console],
|
|
58
|
+
logger: Inject[Logger],
|
|
59
|
+
memory_optimizer: Inject[MemoryOptimizerProtocol],
|
|
60
|
+
parallel_executor: Inject[ParallelHookExecutor],
|
|
61
|
+
async_executor: Inject[AsyncCommandExecutor],
|
|
62
|
+
git_cache: Inject[GitOperationCache],
|
|
63
|
+
filesystem_cache: Inject[FileSystemCache],
|
|
64
|
+
pkg_path: Inject[Path],
|
|
65
|
+
session: Inject[SessionCoordinator],
|
|
66
|
+
filesystem: Inject[FileSystemInterface],
|
|
67
|
+
git_service: Inject[GitInterface],
|
|
68
|
+
hook_manager: Inject[HookManager],
|
|
69
|
+
test_manager: Inject[TestManagerProtocol],
|
|
70
|
+
publish_manager: Inject[PublishManager],
|
|
71
|
+
config_merge_service: Inject[ConfigMergeServiceProtocol],
|
|
45
72
|
) -> None:
|
|
46
73
|
self.console = console
|
|
47
74
|
self.pkg_path = pkg_path
|
|
48
75
|
self.session = session
|
|
49
76
|
|
|
77
|
+
# Dependencies injected via ACB's depends.get() from WorkflowOrchestrator
|
|
50
78
|
self.filesystem = filesystem
|
|
51
79
|
self.git_service = git_service
|
|
52
80
|
self.hook_manager = hook_manager
|
|
@@ -65,779 +93,1135 @@ class PhaseCoordinator(ErrorHandlingMixin):
|
|
|
65
93
|
backup_service=None,
|
|
66
94
|
)
|
|
67
95
|
|
|
68
|
-
|
|
96
|
+
# Ensure logger is a proper instance, not an empty tuple, string, or other invalid value
|
|
97
|
+
if isinstance(logger, tuple) and len(logger) == 0:
|
|
98
|
+
# Log this issue for debugging
|
|
99
|
+
print(
|
|
100
|
+
"WARNING: PhaseCoordinator received empty tuple for logger dependency, creating fallback"
|
|
101
|
+
)
|
|
102
|
+
# Import and create a fallback logger if we got an empty tuple
|
|
103
|
+
from acb.logger import Logger as ACBLogger
|
|
104
|
+
|
|
105
|
+
self._logger = ACBLogger()
|
|
106
|
+
elif isinstance(logger, str):
|
|
107
|
+
# Log this issue for debugging
|
|
108
|
+
print(
|
|
109
|
+
f"WARNING: PhaseCoordinator received string for logger dependency: {logger!r}, creating fallback"
|
|
110
|
+
)
|
|
111
|
+
# Import and create a fallback logger if we got a string
|
|
112
|
+
from acb.logger import Logger as ACBLogger
|
|
69
113
|
|
|
70
|
-
|
|
114
|
+
self._logger = ACBLogger()
|
|
115
|
+
else:
|
|
116
|
+
self._logger = logger
|
|
71
117
|
|
|
72
|
-
|
|
118
|
+
# Services injected via ACB DI
|
|
119
|
+
self._memory_optimizer = memory_optimizer
|
|
120
|
+
self._parallel_executor = parallel_executor
|
|
121
|
+
self._async_executor = async_executor
|
|
122
|
+
self._git_cache = git_cache
|
|
123
|
+
self._filesystem_cache = filesystem_cache
|
|
73
124
|
|
|
74
|
-
self.
|
|
75
|
-
self.
|
|
76
|
-
self._async_executor = get_async_executor()
|
|
77
|
-
self._git_cache = get_git_cache()
|
|
78
|
-
self._filesystem_cache = get_filesystem_cache()
|
|
125
|
+
self._last_hook_summary: dict[str, t.Any] | None = None
|
|
126
|
+
self._last_hook_results: list[HookResult] = []
|
|
79
127
|
|
|
80
128
|
self._lazy_autofix = create_lazy_service(
|
|
81
|
-
lambda: AutofixCoordinator(
|
|
129
|
+
lambda: AutofixCoordinator(pkg_path=pkg_path),
|
|
82
130
|
"autofix_coordinator",
|
|
83
131
|
)
|
|
132
|
+
self.console.print()
|
|
133
|
+
|
|
134
|
+
# Track if fast hooks have already started in this session to prevent duplicates
|
|
135
|
+
self._fast_hooks_started: bool = False
|
|
84
136
|
|
|
85
|
-
|
|
137
|
+
@property
|
|
138
|
+
def logger(self) -> Logger:
|
|
139
|
+
"""Safely access the logger instance, ensuring it's not an empty tuple or string."""
|
|
140
|
+
if hasattr(self, "_logger") and (
|
|
141
|
+
(isinstance(self._logger, tuple) and len(self._logger) == 0)
|
|
142
|
+
or isinstance(self._logger, str)
|
|
143
|
+
):
|
|
144
|
+
from acb.logger import Logger as ACBLogger
|
|
145
|
+
|
|
146
|
+
print(
|
|
147
|
+
f"WARNING: PhaseCoordinator logger was invalid type ({type(self._logger).__name__}: {self._logger!r}), creating fresh logger instance"
|
|
148
|
+
)
|
|
149
|
+
self._logger = ACBLogger()
|
|
150
|
+
return self._logger
|
|
151
|
+
|
|
152
|
+
@logger.setter
|
|
153
|
+
def logger(self, value: Logger) -> None:
|
|
154
|
+
"""Set the logger instance."""
|
|
155
|
+
self._logger = value
|
|
156
|
+
|
|
157
|
+
# --- Output/formatting helpers -------------------------------------------------
|
|
158
|
+
@staticmethod
|
|
159
|
+
def _strip_ansi(text: str) -> str:
|
|
160
|
+
"""Remove ANSI escape sequences (SGR and cursor controls).
|
|
161
|
+
|
|
162
|
+
This is more comprehensive than stripping only color codes ending with 'm'.
|
|
163
|
+
"""
|
|
164
|
+
ansi_re = re.compile(r"\x1b(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])")
|
|
165
|
+
return ansi_re.sub("", text)
|
|
166
|
+
|
|
167
|
+
def _is_plain_output(self) -> bool:
|
|
168
|
+
"""Detect if we should avoid rich formatting entirely.
|
|
169
|
+
|
|
170
|
+
Leverages ACB Console's plain-mode flag when available and falls back
|
|
171
|
+
to Rich Console properties when not.
|
|
172
|
+
"""
|
|
173
|
+
try:
|
|
174
|
+
if bool(getattr(self.console, "_plain_mode", False)):
|
|
175
|
+
return True
|
|
176
|
+
# Fallback on Rich Console capabilities
|
|
177
|
+
is_tty = bool(getattr(self.console, "is_terminal", True))
|
|
178
|
+
color_system = getattr(self.console, "color_system", None)
|
|
179
|
+
return (not is_tty) or (color_system in (None, "null"))
|
|
180
|
+
except Exception:
|
|
181
|
+
# Prefer plain in ambiguous environments
|
|
182
|
+
return True
|
|
86
183
|
|
|
184
|
+
@handle_errors
|
|
87
185
|
def run_cleaning_phase(self, options: OptionsProtocol) -> bool:
|
|
88
186
|
if not options.clean:
|
|
89
187
|
return True
|
|
90
188
|
|
|
91
189
|
self.session.track_task("cleaning", "Code cleaning")
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
return self._execute_cleaning_process()
|
|
95
|
-
except Exception as e:
|
|
96
|
-
self.handle_subprocess_error(e, [], "Code cleaning", critical=False)
|
|
97
|
-
self.session.fail_task("cleaning", str(e))
|
|
98
|
-
return False
|
|
190
|
+
self._display_cleaning_header()
|
|
191
|
+
return self._execute_cleaning_process()
|
|
99
192
|
|
|
100
|
-
|
|
101
|
-
|
|
193
|
+
@handle_errors
|
|
194
|
+
def run_configuration_phase(self, options: OptionsProtocol) -> bool:
|
|
195
|
+
if options.no_config_updates:
|
|
196
|
+
return True
|
|
197
|
+
self.session.track_task("configuration", "Configuration updates")
|
|
102
198
|
self.console.print(
|
|
103
|
-
"[
|
|
199
|
+
"[dim]⚙️ Configuration phase skipped (no automated updates defined).[/dim]"
|
|
104
200
|
)
|
|
105
|
-
self.
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
def _display_version_bump_header(self, version_type: str) -> None:
|
|
109
|
-
self.console.print("\n" + "-" * 74)
|
|
110
|
-
self.console.print(
|
|
111
|
-
f"[bold bright_magenta]📦 BUMP VERSION[/bold bright_magenta] [bold bright_white]Updating package version ({version_type})[/bold bright_white]",
|
|
201
|
+
self.session.complete_task(
|
|
202
|
+
"configuration", "No configuration updates were required."
|
|
112
203
|
)
|
|
113
|
-
|
|
204
|
+
return True
|
|
114
205
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
)
|
|
120
|
-
self.console.print("-" * 74 + "\n")
|
|
206
|
+
@handle_errors
|
|
207
|
+
def run_hooks_phase(self, options: OptionsProtocol) -> bool:
|
|
208
|
+
if options.skip_hooks:
|
|
209
|
+
return True
|
|
121
210
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
self.console.print(
|
|
125
|
-
"[bold bright_cyan]🏷️ GIT OPERATIONS[/bold bright_cyan] [bold bright_white]Staging files and creating tags[/bold bright_white]",
|
|
126
|
-
)
|
|
127
|
-
self.console.print("-" * 74 + "\n")
|
|
211
|
+
if not self.run_fast_hooks_only(options):
|
|
212
|
+
return False
|
|
128
213
|
|
|
129
|
-
|
|
130
|
-
self.console.print("\n" + "-" * 74)
|
|
131
|
-
self.console.print(
|
|
132
|
-
"[bold bright_green]📤 COMMIT & PUSH[/bold bright_green] [bold bright_white]Committing and pushing changes[/bold bright_white]",
|
|
133
|
-
)
|
|
134
|
-
self.console.print("-" * 74 + "\n")
|
|
214
|
+
return self.run_comprehensive_hooks_only(options)
|
|
135
215
|
|
|
136
|
-
def
|
|
137
|
-
|
|
216
|
+
def run_fast_hooks_only(self, options: OptionsProtocol) -> bool:
|
|
217
|
+
if options.skip_hooks:
|
|
218
|
+
self.console.print("[yellow]⚠️[/yellow] Skipping fast hooks (--skip-hooks)")
|
|
219
|
+
return True
|
|
138
220
|
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
self._report_package_cleaning_results(cleaning_result)
|
|
145
|
-
return cleaning_result.overall_success
|
|
221
|
+
# Prevent multiple fast-hook runs in a single workflow session unless
|
|
222
|
+
# explicitly reset by post-cleaning sanity check.
|
|
223
|
+
if getattr(self, "_fast_hooks_started", False):
|
|
224
|
+
self.logger.debug("Duplicate fast hooks invocation detected; skipping")
|
|
225
|
+
return True
|
|
146
226
|
|
|
147
|
-
|
|
148
|
-
self.
|
|
149
|
-
self.session.
|
|
150
|
-
return True
|
|
227
|
+
# Mark fast hooks as started immediately to prevent duplicate calls in case of failures
|
|
228
|
+
self._fast_hooks_started = True
|
|
229
|
+
self.session.track_task("hooks_fast", "Fast quality checks")
|
|
151
230
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
self.session.complete_task(
|
|
156
|
-
"cleaning",
|
|
157
|
-
f"Cleaned {len(cleaned_files)} files",
|
|
158
|
-
)
|
|
159
|
-
else:
|
|
160
|
-
self.console.print("[green]✅[/ green] No cleaning needed")
|
|
161
|
-
self.session.complete_task("cleaning", "No cleaning needed")
|
|
231
|
+
# Fast hooks get 2 attempts (auto-fix on failure), comprehensive hooks run once
|
|
232
|
+
max_attempts = 2
|
|
233
|
+
attempt = 0
|
|
162
234
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
self.console.print(
|
|
166
|
-
f"[green]✅[/ green] Package cleaning completed successfully! "
|
|
167
|
-
f"({result.successful_files}/{result.total_files} files cleaned)"
|
|
168
|
-
)
|
|
169
|
-
self.session.complete_task(
|
|
170
|
-
"cleaning",
|
|
171
|
-
f"Cleaned {result.successful_files}/{result.total_files} files with backup protection",
|
|
172
|
-
)
|
|
173
|
-
else:
|
|
174
|
-
self.console.print(
|
|
175
|
-
f"[red]❌[/ red] Package cleaning failed! "
|
|
176
|
-
f"({result.failed_files}/{result.total_files} files failed)"
|
|
177
|
-
)
|
|
178
|
-
|
|
179
|
-
if result.backup_restored:
|
|
180
|
-
self.console.print(
|
|
181
|
-
"[yellow]⚠️[/ yellow] Files were automatically restored from backup"
|
|
182
|
-
)
|
|
183
|
-
self.session.complete_task(
|
|
184
|
-
"cleaning", "Failed with automatic backup restoration"
|
|
185
|
-
)
|
|
186
|
-
else:
|
|
187
|
-
self.session.fail_task(
|
|
188
|
-
"cleaning", f"Failed to clean {result.failed_files} files"
|
|
189
|
-
)
|
|
235
|
+
while attempt < max_attempts:
|
|
236
|
+
attempt += 1
|
|
190
237
|
|
|
191
|
-
|
|
238
|
+
# Display stage header for each attempt
|
|
239
|
+
if attempt > 1:
|
|
192
240
|
self.console.print(
|
|
193
|
-
f"[
|
|
241
|
+
f"\n[yellow]♻️[/yellow] Verification Retry {attempt}/{max_attempts}\n"
|
|
194
242
|
)
|
|
195
243
|
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
try:
|
|
201
|
-
success = self._execute_configuration_steps(options)
|
|
202
|
-
self._complete_configuration_task(success)
|
|
203
|
-
return success
|
|
204
|
-
except Exception as e:
|
|
205
|
-
self.handle_subprocess_error(e, [], "Configuration phase", critical=False)
|
|
206
|
-
self.session.fail_task("configuration", str(e))
|
|
207
|
-
return False
|
|
208
|
-
|
|
209
|
-
def _execute_configuration_steps(self, options: OptionsProtocol) -> bool:
|
|
210
|
-
success = True
|
|
244
|
+
self._display_hook_phase_header(
|
|
245
|
+
"FAST HOOKS",
|
|
246
|
+
"Formatters, import sorting, and quick static analysis",
|
|
247
|
+
)
|
|
211
248
|
|
|
212
|
-
|
|
249
|
+
# Run hooks (now configured to run in fix mode by default)
|
|
250
|
+
success = self._execute_hooks_once(
|
|
251
|
+
"fast", self.hook_manager.run_fast_hooks, options, attempt
|
|
252
|
+
)
|
|
213
253
|
|
|
214
|
-
|
|
254
|
+
if success:
|
|
255
|
+
break
|
|
215
256
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
success = False
|
|
220
|
-
if not self.config_service.update_pyproject_config(options):
|
|
221
|
-
success = False
|
|
222
|
-
return success
|
|
257
|
+
# Fast iteration mode intentionally avoids retries
|
|
258
|
+
if getattr(options, "fast_iteration", False):
|
|
259
|
+
break
|
|
223
260
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
if success
|
|
228
|
-
else "Some configuration updates failed"
|
|
229
|
-
)
|
|
230
|
-
self.session.complete_task("configuration", message)
|
|
261
|
+
# If we have more attempts, continue to retry to verify fixes worked
|
|
262
|
+
if attempt < max_attempts:
|
|
263
|
+
self._display_hook_failures("fast", self._last_hook_results, options)
|
|
231
264
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
if not pyproject_path.exists():
|
|
235
|
-
return False
|
|
265
|
+
summary = self._last_hook_summary or {}
|
|
266
|
+
details = self._format_hook_summary(summary)
|
|
236
267
|
|
|
237
|
-
|
|
238
|
-
|
|
268
|
+
if success:
|
|
269
|
+
self.session.complete_task("hooks_fast", details=details)
|
|
270
|
+
else:
|
|
271
|
+
self.session.fail_task("hooks_fast", "Fast hook failures detected")
|
|
239
272
|
|
|
240
|
-
|
|
241
|
-
|
|
273
|
+
# Ensure fast hooks output is fully rendered before comprehensive hooks start
|
|
274
|
+
self.console.print()
|
|
242
275
|
|
|
243
|
-
|
|
244
|
-
return project_name == "crackerjack"
|
|
245
|
-
except Exception:
|
|
246
|
-
return False
|
|
276
|
+
return success
|
|
247
277
|
|
|
248
|
-
def
|
|
278
|
+
def run_comprehensive_hooks_only(self, options: OptionsProtocol) -> bool:
|
|
249
279
|
if options.skip_hooks:
|
|
280
|
+
self.console.print(
|
|
281
|
+
"[yellow]⚠️[/yellow] Skipping comprehensive hooks (--skip-hooks)"
|
|
282
|
+
)
|
|
250
283
|
return True
|
|
251
284
|
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
return False
|
|
285
|
+
self.session.track_task("hooks_comprehensive", "Comprehensive quality checks")
|
|
286
|
+
self._display_hook_phase_header(
|
|
287
|
+
"COMPREHENSIVE HOOKS",
|
|
288
|
+
"Type, security, and complexity checking",
|
|
289
|
+
)
|
|
258
290
|
|
|
259
|
-
|
|
291
|
+
# Comprehensive hooks run once (no retry)
|
|
292
|
+
success = self._execute_hooks_once(
|
|
293
|
+
"comprehensive",
|
|
294
|
+
self.hook_manager.run_comprehensive_hooks,
|
|
295
|
+
options,
|
|
296
|
+
attempt=1,
|
|
297
|
+
)
|
|
260
298
|
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
299
|
+
if not success:
|
|
300
|
+
self._display_hook_failures(
|
|
301
|
+
"comprehensive", self._last_hook_results, options
|
|
302
|
+
)
|
|
264
303
|
|
|
265
|
-
|
|
266
|
-
|
|
304
|
+
summary = self._last_hook_summary or {}
|
|
305
|
+
details = self._format_hook_summary(summary)
|
|
267
306
|
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
307
|
+
if success:
|
|
308
|
+
self.session.complete_task("hooks_comprehensive", details=details)
|
|
309
|
+
else:
|
|
310
|
+
self.session.fail_task(
|
|
311
|
+
"hooks_comprehensive", "Comprehensive hook failures detected"
|
|
312
|
+
)
|
|
271
313
|
|
|
272
|
-
|
|
273
|
-
return all(r.status == "passed" for r in hook_results)
|
|
314
|
+
return success
|
|
274
315
|
|
|
316
|
+
@handle_errors
|
|
275
317
|
def run_testing_phase(self, options: OptionsProtocol) -> bool:
|
|
276
318
|
if not options.test:
|
|
277
319
|
return True
|
|
278
320
|
self.session.track_task("testing", "Test execution")
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
321
|
+
self.console.print("\n" + make_separator("-"))
|
|
322
|
+
self.console.print(
|
|
323
|
+
"[bold bright_blue]🧪 TESTS[/bold bright_blue] [bold bright_white]Running test suite[/bold bright_white]",
|
|
324
|
+
)
|
|
325
|
+
self.console.print(make_separator("-") + "\n")
|
|
326
|
+
if not self.test_manager.validate_test_environment():
|
|
327
|
+
self.session.fail_task("testing", "Test environment validation failed")
|
|
328
|
+
return False
|
|
329
|
+
test_success = self.test_manager.run_tests(options)
|
|
330
|
+
if test_success:
|
|
331
|
+
coverage_info = self.test_manager.get_coverage()
|
|
332
|
+
self.session.complete_task(
|
|
333
|
+
"testing",
|
|
334
|
+
f"Tests passed, coverage: {coverage_info.get('total_coverage', 0):.1f}%",
|
|
283
335
|
)
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
self.session.fail_task("testing", "Test environment validation failed")
|
|
287
|
-
return False
|
|
288
|
-
test_success = self.test_manager.run_tests(options)
|
|
289
|
-
if test_success:
|
|
290
|
-
coverage_info = self.test_manager.get_coverage()
|
|
291
|
-
self.session.complete_task(
|
|
292
|
-
"testing",
|
|
293
|
-
f"Tests passed, coverage: {coverage_info.get('total_coverage', 0): .1f}%",
|
|
294
|
-
)
|
|
295
|
-
else:
|
|
296
|
-
self.session.fail_task("testing", "Tests failed")
|
|
336
|
+
else:
|
|
337
|
+
self.session.fail_task("testing", "Tests failed")
|
|
297
338
|
|
|
298
|
-
|
|
299
|
-
except Exception as e:
|
|
300
|
-
self.console.print(f"Testing error: {e}")
|
|
301
|
-
self.session.fail_task("testing", str(e))
|
|
302
|
-
return False
|
|
339
|
+
return test_success
|
|
303
340
|
|
|
341
|
+
@handle_errors
|
|
304
342
|
def run_publishing_phase(self, options: OptionsProtocol) -> bool:
|
|
305
343
|
version_type = self._determine_version_type(options)
|
|
306
344
|
if not version_type:
|
|
307
345
|
return True
|
|
308
346
|
|
|
309
347
|
self.session.track_task("publishing", f"Publishing ({version_type})")
|
|
310
|
-
|
|
311
|
-
return self._execute_publishing_workflow(options, version_type)
|
|
312
|
-
except Exception as e:
|
|
313
|
-
self.console.print(f"[red]❌[/ red] Publishing failed: {e}")
|
|
314
|
-
self.session.fail_task("publishing", str(e))
|
|
315
|
-
return False
|
|
348
|
+
return self._execute_publishing_workflow(options, version_type)
|
|
316
349
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
)
|
|
322
|
-
return publish_value
|
|
323
|
-
if options.all:
|
|
324
|
-
all_value: str | None = (
|
|
325
|
-
options.all if isinstance(options.all, str) else None
|
|
326
|
-
)
|
|
327
|
-
return all_value
|
|
328
|
-
if options.bump:
|
|
329
|
-
self._handle_version_bump_only(options.bump)
|
|
330
|
-
return None
|
|
331
|
-
return None
|
|
332
|
-
|
|
333
|
-
def _execute_publishing_workflow(
|
|
334
|
-
self,
|
|
335
|
-
options: OptionsProtocol,
|
|
336
|
-
version_type: str,
|
|
337
|
-
) -> bool:
|
|
338
|
-
# Display version bump header
|
|
339
|
-
self._display_version_bump_header(version_type)
|
|
340
|
-
new_version = self.publish_manager.bump_version(version_type)
|
|
350
|
+
@handle_errors
|
|
351
|
+
def run_commit_phase(self, options: OptionsProtocol) -> bool:
|
|
352
|
+
if not options.commit:
|
|
353
|
+
return True
|
|
341
354
|
|
|
342
|
-
#
|
|
343
|
-
|
|
344
|
-
self.
|
|
345
|
-
if
|
|
355
|
+
# Skip if publishing phase already handled commits
|
|
356
|
+
# (Publishing phase handles both pre-publish and version-bump commits when -c is used)
|
|
357
|
+
version_type = self._determine_version_type(options)
|
|
358
|
+
if version_type:
|
|
359
|
+
# Publishing workflow already committed everything
|
|
346
360
|
self.console.print(
|
|
347
|
-
"[
|
|
361
|
+
"[dim]ℹ️ Commit phase skipped (handled by publish workflow)[/dim]"
|
|
348
362
|
)
|
|
349
|
-
|
|
350
|
-
if not options.no_git_tags:
|
|
351
|
-
self.publish_manager.create_git_tag(new_version)
|
|
352
|
-
|
|
353
|
-
# Display publish header
|
|
354
|
-
self._display_publish_header()
|
|
355
|
-
if self.publish_manager.publish_package():
|
|
356
|
-
self._handle_successful_publish(options, new_version)
|
|
357
363
|
return True
|
|
358
|
-
self.session.fail_task("publishing", "Package publishing failed")
|
|
359
|
-
return False
|
|
360
364
|
|
|
361
|
-
|
|
365
|
+
# Display commit & push header
|
|
366
|
+
self._display_commit_push_header()
|
|
367
|
+
self.session.track_task("commit", "Git commit and push")
|
|
368
|
+
changed_files = self.git_service.get_changed_files()
|
|
369
|
+
if not changed_files:
|
|
370
|
+
return self._handle_no_changes_to_commit()
|
|
371
|
+
commit_message = self._get_commit_message(changed_files, options)
|
|
372
|
+
return self._execute_commit_and_push(changed_files, commit_message)
|
|
373
|
+
|
|
374
|
+
def _execute_hooks_once(
|
|
362
375
|
self,
|
|
376
|
+
suite_name: str,
|
|
377
|
+
hook_runner: t.Callable[[], list[HookResult]],
|
|
363
378
|
options: OptionsProtocol,
|
|
364
|
-
|
|
365
|
-
) ->
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
self.publish_manager.cleanup_old_releases(options.keep_releases)
|
|
379
|
+
attempt: int,
|
|
380
|
+
) -> bool:
|
|
381
|
+
"""Execute a hook suite once with progress bar (no retry logic - retry is handled at stage level)."""
|
|
382
|
+
self._last_hook_summary = None
|
|
383
|
+
self._last_hook_results = []
|
|
370
384
|
|
|
371
|
-
self.
|
|
385
|
+
hook_count = self.hook_manager.get_hook_count(suite_name)
|
|
386
|
+
progress = self._create_progress_bar()
|
|
372
387
|
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
388
|
+
callbacks = self._setup_progress_callbacks(progress)
|
|
389
|
+
elapsed_time = self._run_hooks_with_progress(
|
|
390
|
+
suite_name, hook_runner, progress, hook_count, attempt, callbacks
|
|
391
|
+
)
|
|
376
392
|
|
|
377
|
-
|
|
378
|
-
self._display_commit_push_header()
|
|
379
|
-
self.session.track_task("commit", "Git commit and push")
|
|
380
|
-
try:
|
|
381
|
-
changed_files = self.git_service.get_changed_files()
|
|
382
|
-
if not changed_files:
|
|
383
|
-
return self._handle_no_changes_to_commit()
|
|
384
|
-
commit_message = self._get_commit_message(changed_files, options)
|
|
385
|
-
return self._execute_commit_and_push(changed_files, commit_message)
|
|
386
|
-
except Exception as e:
|
|
387
|
-
self.console.print(f"[red]❌[/ red] Commit failed: {e}")
|
|
388
|
-
self.session.fail_task("commit", str(e))
|
|
393
|
+
if elapsed_time is None:
|
|
389
394
|
return False
|
|
390
395
|
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
396
|
+
return self._process_hook_results(suite_name, elapsed_time, attempt)
|
|
397
|
+
|
|
398
|
+
def _create_progress_bar(self) -> Progress:
|
|
399
|
+
"""Create compact progress bar for hook execution."""
|
|
400
|
+
return Progress(
|
|
401
|
+
SpinnerColumn(spinner_name="dots"),
|
|
402
|
+
TextColumn("[cyan]{task.description}[/cyan]"),
|
|
403
|
+
BarColumn(bar_width=20),
|
|
404
|
+
MofNCompleteColumn(),
|
|
405
|
+
TimeElapsedColumn(),
|
|
406
|
+
console=self.console,
|
|
407
|
+
transient=True,
|
|
408
|
+
)
|
|
395
409
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
)
|
|
402
|
-
if self.git_service.push():
|
|
403
|
-
self.session.complete_task(
|
|
404
|
-
"commit",
|
|
405
|
-
f"No new changes, pushed {commit_count} existing commit(s)",
|
|
406
|
-
)
|
|
407
|
-
return True
|
|
408
|
-
else:
|
|
409
|
-
self.console.print(
|
|
410
|
-
"[yellow]⚠️[/ yellow] Push failed for existing commits"
|
|
411
|
-
)
|
|
410
|
+
def _setup_progress_callbacks(
|
|
411
|
+
self, progress: Progress
|
|
412
|
+
) -> dict[str, t.Callable[[int, int], None] | None | dict[str, t.Any]]:
|
|
413
|
+
"""Setup progress callbacks and store originals for restoration."""
|
|
414
|
+
task_id_holder = {"task_id": None}
|
|
412
415
|
|
|
413
|
-
|
|
414
|
-
|
|
416
|
+
def update_progress(completed: int, total: int) -> None:
|
|
417
|
+
if task_id_holder["task_id"] is not None:
|
|
418
|
+
progress.update(task_id_holder["task_id"], completed=completed)
|
|
415
419
|
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
commit_message: str,
|
|
420
|
-
) -> bool:
|
|
421
|
-
if not self.git_service.add_files(changed_files):
|
|
422
|
-
self.session.fail_task("commit", "Failed to stage files")
|
|
423
|
-
return False
|
|
420
|
+
def update_progress_started(started: int, total: int) -> None:
|
|
421
|
+
if task_id_holder["task_id"] is not None:
|
|
422
|
+
progress.update(task_id_holder["task_id"], completed=started)
|
|
424
423
|
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
424
|
+
original_callback = getattr(self.hook_manager, "_progress_callback", None)
|
|
425
|
+
original_start_callback = getattr(
|
|
426
|
+
self.hook_manager, "_progress_start_callback", None
|
|
427
|
+
)
|
|
428
428
|
|
|
429
|
-
|
|
429
|
+
self.hook_manager._progress_callback = update_progress
|
|
430
|
+
self.hook_manager._progress_start_callback = update_progress_started
|
|
430
431
|
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
f"Committed and pushed: {commit_message}",
|
|
439
|
-
)
|
|
440
|
-
else:
|
|
441
|
-
self.console.print(
|
|
442
|
-
f"[yellow]⚠️[/ yellow] Committed but push failed: {commit_message}",
|
|
443
|
-
)
|
|
444
|
-
self.session.complete_task(
|
|
445
|
-
"commit",
|
|
446
|
-
f"Committed (push failed): {commit_message}",
|
|
447
|
-
)
|
|
448
|
-
return True
|
|
432
|
+
return {
|
|
433
|
+
"update": update_progress,
|
|
434
|
+
"update_started": update_progress_started,
|
|
435
|
+
"original": original_callback,
|
|
436
|
+
"original_started": original_start_callback,
|
|
437
|
+
"task_id_holder": task_id_holder,
|
|
438
|
+
}
|
|
449
439
|
|
|
450
|
-
def
|
|
440
|
+
def _run_hooks_with_progress(
|
|
451
441
|
self,
|
|
452
|
-
|
|
453
|
-
hook_runner: t.Callable[[], list[
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
442
|
+
suite_name: str,
|
|
443
|
+
hook_runner: t.Callable[[], list[HookResult]],
|
|
444
|
+
progress: Progress,
|
|
445
|
+
hook_count: int,
|
|
446
|
+
attempt: int,
|
|
447
|
+
callbacks: dict[str, t.Any],
|
|
448
|
+
) -> float | None:
|
|
449
|
+
"""Run hooks with progress tracking, return elapsed time or None on error."""
|
|
460
450
|
try:
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
self.session.complete_task("version_bump", f"Bumped to {new_version}")
|
|
466
|
-
return True
|
|
467
|
-
except Exception as e:
|
|
468
|
-
self.console.print(f"[red]❌[/ red] Version bump failed: {e}")
|
|
469
|
-
self.session.fail_task("version_bump", str(e))
|
|
470
|
-
return False
|
|
471
|
-
|
|
472
|
-
def _get_commit_message(
|
|
473
|
-
self,
|
|
474
|
-
changed_files: list[str],
|
|
475
|
-
options: OptionsProtocol,
|
|
476
|
-
) -> str:
|
|
477
|
-
# Smart commit is now enabled by default for all commit operations
|
|
478
|
-
# Use basic commit messages only if explicitly disabled
|
|
479
|
-
use_smart_commit = getattr(options, "smart_commit", True)
|
|
480
|
-
disable_smart_commit = getattr(options, "basic_commit", False)
|
|
481
|
-
|
|
482
|
-
if use_smart_commit and not disable_smart_commit:
|
|
483
|
-
try:
|
|
484
|
-
from crackerjack.services.intelligent_commit import (
|
|
485
|
-
CommitMessageGenerator,
|
|
451
|
+
with progress:
|
|
452
|
+
task_id = progress.add_task(
|
|
453
|
+
f"Running {suite_name} hooks:",
|
|
454
|
+
total=hook_count,
|
|
486
455
|
)
|
|
456
|
+
callbacks["task_id_holder"]["task_id"] = task_id
|
|
487
457
|
|
|
488
|
-
|
|
489
|
-
"[cyan]🤖[/cyan] Generating intelligent commit message..."
|
|
490
|
-
)
|
|
491
|
-
commit_generator = CommitMessageGenerator(
|
|
492
|
-
console=self.console, git_service=self.git_service
|
|
493
|
-
)
|
|
458
|
+
import time
|
|
494
459
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
)
|
|
460
|
+
start_time = time.time()
|
|
461
|
+
hook_results = hook_runner()
|
|
462
|
+
self._last_hook_results = hook_results
|
|
463
|
+
elapsed_time = time.time() - start_time
|
|
499
464
|
|
|
500
|
-
|
|
501
|
-
self.console.print(
|
|
502
|
-
f"[green]✨[/green] Generated: {intelligent_message}"
|
|
503
|
-
)
|
|
504
|
-
return intelligent_message
|
|
465
|
+
return elapsed_time
|
|
505
466
|
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
suggestions.extend(
|
|
512
|
-
fallback_suggestions[:3]
|
|
513
|
-
) # Add up to 3 fallback options
|
|
467
|
+
except Exception as exc:
|
|
468
|
+
self._handle_hook_execution_error(suite_name, exc, attempt)
|
|
469
|
+
return None
|
|
470
|
+
finally:
|
|
471
|
+
self._restore_progress_callbacks(callbacks)
|
|
514
472
|
|
|
515
|
-
|
|
473
|
+
def _handle_hook_execution_error(
|
|
474
|
+
self, suite_name: str, exc: Exception, attempt: int
|
|
475
|
+
) -> None:
|
|
476
|
+
"""Handle errors during hook execution."""
|
|
477
|
+
self.console.print(
|
|
478
|
+
f"[red]❌[/red] {suite_name.title()} hooks encountered an unexpected error: {exc}"
|
|
479
|
+
)
|
|
480
|
+
self.logger.exception(
|
|
481
|
+
"Hook execution raised exception",
|
|
482
|
+
extra={"suite": suite_name, "attempt": attempt},
|
|
483
|
+
)
|
|
516
484
|
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
# Fallback to original logic
|
|
485
|
+
def _restore_progress_callbacks(self, callbacks: dict[str, t.Any]) -> None:
|
|
486
|
+
"""Restore original progress callbacks."""
|
|
487
|
+
self.hook_manager._progress_callback = callbacks["original"]
|
|
488
|
+
self.hook_manager._progress_start_callback = callbacks["original_started"]
|
|
522
489
|
|
|
523
|
-
|
|
524
|
-
|
|
490
|
+
def _process_hook_results(
|
|
491
|
+
self, suite_name: str, elapsed_time: float, attempt: int
|
|
492
|
+
) -> bool:
|
|
493
|
+
"""Process hook results and determine success."""
|
|
494
|
+
summary = self.hook_manager.get_hook_summary(
|
|
495
|
+
self._last_hook_results, elapsed_time=elapsed_time
|
|
496
|
+
)
|
|
497
|
+
self._last_hook_summary = summary
|
|
498
|
+
self._report_hook_results(suite_name, self._last_hook_results, summary, attempt)
|
|
525
499
|
|
|
526
|
-
if
|
|
527
|
-
return
|
|
500
|
+
if summary.get("failed", 0) == 0 == summary.get("errors", 0):
|
|
501
|
+
return True
|
|
528
502
|
|
|
529
|
-
|
|
530
|
-
|
|
503
|
+
self.logger.warning(
|
|
504
|
+
"Hook suite reported failures",
|
|
505
|
+
extra={
|
|
506
|
+
"suite": suite_name,
|
|
507
|
+
"attempt": attempt,
|
|
508
|
+
"failed": summary.get("failed", 0),
|
|
509
|
+
"errors": summary.get("errors", 0),
|
|
510
|
+
},
|
|
511
|
+
)
|
|
512
|
+
return False
|
|
531
513
|
|
|
532
|
-
|
|
514
|
+
def _display_hook_phase_header(self, title: str, description: str) -> None:
|
|
515
|
+
sep = make_separator("-")
|
|
516
|
+
self.console.print("\n" + sep)
|
|
517
|
+
# Combine title and description into a single line with leading icon
|
|
518
|
+
pretty_title = title.title()
|
|
519
|
+
message = (
|
|
520
|
+
f"[bold bright_cyan]🔍 {pretty_title}[/bold bright_cyan][bold bright_white]"
|
|
521
|
+
f" - {description}[/bold bright_white]"
|
|
522
|
+
)
|
|
523
|
+
self.console.print(message)
|
|
524
|
+
self.console.print(sep + "\n")
|
|
533
525
|
|
|
534
|
-
def
|
|
535
|
-
self
|
|
526
|
+
def _report_hook_results(
|
|
527
|
+
self,
|
|
528
|
+
suite_name: str,
|
|
529
|
+
results: list[HookResult],
|
|
530
|
+
summary: dict[str, t.Any],
|
|
531
|
+
attempt: int,
|
|
532
|
+
) -> None:
|
|
533
|
+
total = summary.get("total", 0)
|
|
534
|
+
passed = summary.get("passed", 0)
|
|
535
|
+
failed = summary.get("failed", 0)
|
|
536
|
+
errors = summary.get("errors", 0)
|
|
537
|
+
duration = summary.get("total_duration", 0.0)
|
|
536
538
|
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
f"
|
|
539
|
+
if total == 0:
|
|
540
|
+
self.console.print(
|
|
541
|
+
f"[yellow]⚠️[/yellow] No {suite_name} hooks are configured for this project."
|
|
540
542
|
)
|
|
541
|
-
return
|
|
542
|
-
except (KeyboardInterrupt, EOFError):
|
|
543
|
-
return suggestions[0]
|
|
543
|
+
return
|
|
544
544
|
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
545
|
+
base_message = (
|
|
546
|
+
f"{suite_name.title()} hooks attempt {attempt}: "
|
|
547
|
+
f"{passed}/{total} passed in {duration:.2f}s"
|
|
548
|
+
)
|
|
549
549
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
550
|
+
if failed or errors:
|
|
551
|
+
self.console.print(f"\n[red]❌[/red] {base_message}\n")
|
|
552
|
+
# Always show a results table to aid debugging when there are failures
|
|
553
|
+
self._render_hook_results_table(suite_name, results)
|
|
554
|
+
else:
|
|
555
|
+
self.console.print(f"\n[green]✅[/green] {base_message}\n")
|
|
556
|
+
self._render_hook_results_table(suite_name, results)
|
|
554
557
|
|
|
555
|
-
def
|
|
558
|
+
def _render_hook_results_table(
|
|
556
559
|
self,
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
max_retries = self._get_max_retries(hook_type)
|
|
563
|
-
|
|
564
|
-
for attempt in range(max_retries):
|
|
565
|
-
try:
|
|
566
|
-
execution_result = self._execute_single_hook_attempt(hook_runner)
|
|
567
|
-
if execution_result is None:
|
|
568
|
-
return False
|
|
569
|
-
|
|
570
|
-
results, summary = execution_result
|
|
571
|
-
should_continue = self._process_hook_results(
|
|
572
|
-
hook_type, options, summary, results, attempt, max_retries
|
|
573
|
-
)
|
|
560
|
+
suite_name: str,
|
|
561
|
+
results: list[HookResult],
|
|
562
|
+
) -> None:
|
|
563
|
+
if not results:
|
|
564
|
+
return
|
|
574
565
|
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
else:
|
|
580
|
-
return False
|
|
566
|
+
if self._is_plain_output():
|
|
567
|
+
self._render_plain_hook_results(suite_name, results)
|
|
568
|
+
else:
|
|
569
|
+
self._render_rich_hook_results(suite_name, results)
|
|
581
570
|
|
|
582
|
-
|
|
583
|
-
|
|
571
|
+
def _render_plain_hook_results(
|
|
572
|
+
self, suite_name: str, results: list[HookResult]
|
|
573
|
+
) -> None:
|
|
574
|
+
"""Render hook results in plain text format."""
|
|
575
|
+
self.console.print(f"{suite_name.title()} Hook Results:", highlight=False)
|
|
576
|
+
|
|
577
|
+
stats = self._calculate_hook_statistics(results)
|
|
578
|
+
hooks_to_show = stats["failed_hooks"] or results
|
|
579
|
+
|
|
580
|
+
for result in hooks_to_show:
|
|
581
|
+
self._print_plain_hook_result(result)
|
|
582
|
+
|
|
583
|
+
if not stats["failed_hooks"] and results:
|
|
584
|
+
self._print_plain_summary(stats)
|
|
585
|
+
|
|
586
|
+
self.console.print()
|
|
587
|
+
|
|
588
|
+
def _calculate_hook_statistics(self, results: list[HookResult]) -> dict[str, t.Any]:
|
|
589
|
+
"""Calculate statistics from hook results."""
|
|
590
|
+
passed_hooks = [r for r in results if r.status.lower() in {"passed", "success"}]
|
|
591
|
+
failed_hooks = [
|
|
592
|
+
r for r in results if r.status.lower() in {"failed", "error", "timeout"}
|
|
593
|
+
]
|
|
594
|
+
other_hooks = [
|
|
595
|
+
r
|
|
596
|
+
for r in results
|
|
597
|
+
if r.status.lower()
|
|
598
|
+
not in {"passed", "success", "failed", "error", "timeout"}
|
|
599
|
+
]
|
|
600
|
+
|
|
601
|
+
# Calculate total issues using issues_count (which may be larger than len(issues_found))
|
|
602
|
+
# Passed hooks always contribute 0 issues
|
|
603
|
+
# Config errors (is_config_error=True) are counted separately
|
|
604
|
+
total_issues = 0
|
|
605
|
+
config_errors = 0
|
|
606
|
+
for r in results:
|
|
607
|
+
if r.status == "passed":
|
|
608
|
+
continue
|
|
609
|
+
# Count config errors separately - they're not code quality issues
|
|
610
|
+
if hasattr(r, "is_config_error") and r.is_config_error:
|
|
611
|
+
config_errors += 1
|
|
612
|
+
continue
|
|
613
|
+
# Use issues_count directly (don't fall back to len(issues_found))
|
|
614
|
+
# because issues_found may contain error detail lines, not actual issues
|
|
615
|
+
if hasattr(r, "issues_count"):
|
|
616
|
+
total_issues += r.issues_count
|
|
617
|
+
elif r.issues_found:
|
|
618
|
+
# Legacy fallback for old HookResults without issues_count
|
|
619
|
+
total_issues += len(r.issues_found)
|
|
620
|
+
|
|
621
|
+
return {
|
|
622
|
+
"total_hooks": len(results),
|
|
623
|
+
"passed_hooks": passed_hooks,
|
|
624
|
+
"failed_hooks": failed_hooks,
|
|
625
|
+
"other_hooks": other_hooks,
|
|
626
|
+
"total_passed": len(passed_hooks),
|
|
627
|
+
"total_failed": len(failed_hooks),
|
|
628
|
+
"total_other": len(other_hooks),
|
|
629
|
+
"total_issues_found": total_issues,
|
|
630
|
+
"config_errors": config_errors,
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
def _print_plain_hook_result(self, result: HookResult) -> None:
|
|
634
|
+
"""Print a single hook result in plain format."""
|
|
635
|
+
name = self._strip_ansi(result.name)
|
|
636
|
+
status = result.status.upper()
|
|
637
|
+
duration = f"{result.duration:.2f}s"
|
|
638
|
+
|
|
639
|
+
# Determine issues display (matches Rich table logic)
|
|
640
|
+
if result.status == "passed":
|
|
641
|
+
issues = "0"
|
|
642
|
+
elif hasattr(result, "is_config_error") and result.is_config_error:
|
|
643
|
+
# Config/tool error - show simple symbol instead of misleading count
|
|
644
|
+
issues = "[yellow]![/yellow]"
|
|
645
|
+
else:
|
|
646
|
+
# For failed hooks with code violations, use issues_count
|
|
647
|
+
# Don't fall back to len(issues_found) - it may contain error detail lines
|
|
648
|
+
issues = str(result.issues_count if hasattr(result, "issues_count") else 0)
|
|
584
649
|
|
|
585
|
-
|
|
650
|
+
self.console.print(
|
|
651
|
+
f" - {name} :: {status} | {duration} | issues={issues}",
|
|
652
|
+
)
|
|
586
653
|
|
|
587
|
-
def
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
summary = self.hook_manager.get_hook_summary(results)
|
|
593
|
-
return results, summary
|
|
594
|
-
except Exception:
|
|
595
|
-
return None
|
|
654
|
+
def _print_plain_summary(self, stats: dict[str, t.Any]) -> None:
|
|
655
|
+
"""Print summary statistics in plain format."""
|
|
656
|
+
issues_text = f"{stats['total_issues_found']} issues found"
|
|
657
|
+
if stats.get("config_errors", 0) > 0:
|
|
658
|
+
issues_text += f" ({stats['config_errors']} config)"
|
|
596
659
|
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
summary: dict[str, t.Any],
|
|
602
|
-
results: list[t.Any],
|
|
603
|
-
attempt: int,
|
|
604
|
-
max_retries: int,
|
|
605
|
-
) -> str:
|
|
606
|
-
if not self._has_hook_failures(summary):
|
|
607
|
-
self._handle_hook_success(hook_type, summary)
|
|
608
|
-
return "success"
|
|
660
|
+
self.console.print(
|
|
661
|
+
f" Summary: {stats['total_passed']}/{stats['total_hooks']} hooks passed, {issues_text}",
|
|
662
|
+
highlight=False,
|
|
663
|
+
)
|
|
609
664
|
|
|
610
|
-
|
|
611
|
-
|
|
665
|
+
def _render_rich_hook_results(
|
|
666
|
+
self, suite_name: str, results: list[HookResult]
|
|
667
|
+
) -> None:
|
|
668
|
+
"""Render hook results in Rich format."""
|
|
669
|
+
stats = self._calculate_hook_statistics(results)
|
|
670
|
+
summary_text = self._build_summary_text(stats)
|
|
671
|
+
table = self._build_results_table(results)
|
|
672
|
+
panel = self._build_results_panel(suite_name, table, summary_text)
|
|
612
673
|
|
|
613
|
-
self.
|
|
614
|
-
hook_type, options, summary, results, attempt, max_retries
|
|
615
|
-
)
|
|
616
|
-
return "failure"
|
|
674
|
+
self.console.print(panel)
|
|
617
675
|
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
f"{hook_type}_hooks",
|
|
622
|
-
f"{hook_type.title()} hooks execution",
|
|
676
|
+
# Add legend if any config errors are present
|
|
677
|
+
has_config_errors = any(
|
|
678
|
+
hasattr(r, "is_config_error") and r.is_config_error for r in results
|
|
623
679
|
)
|
|
680
|
+
if has_config_errors:
|
|
681
|
+
self.console.print(
|
|
682
|
+
" [dim][yellow]![/yellow] = Configuration or tool error (not code "
|
|
683
|
+
"issues)[/dim]"
|
|
684
|
+
)
|
|
624
685
|
|
|
625
|
-
|
|
626
|
-
return 2 if hook_type == "fast" else 1
|
|
686
|
+
self.console.print()
|
|
627
687
|
|
|
628
|
-
|
|
629
|
-
|
|
688
|
+
@staticmethod
|
|
689
|
+
def _build_summary_text(stats: dict[str, t.Any]) -> str:
|
|
690
|
+
"""Build summary text for Rich display."""
|
|
691
|
+
summary_text = (
|
|
692
|
+
f"Total: [white]{stats['total_hooks']}[/white] | Passed:"
|
|
693
|
+
f" [green]{stats['total_passed']}[/green] | Failed: [red]{stats['total_failed']}[/red]"
|
|
694
|
+
)
|
|
695
|
+
if stats["total_other"] > 0:
|
|
696
|
+
summary_text += f" | Other: [yellow]{stats['total_other']}[/yellow]"
|
|
697
|
+
|
|
698
|
+
# Show issues found with config count in parentheses if present
|
|
699
|
+
issues_text = f"[white]{stats['total_issues_found']}[/white]"
|
|
700
|
+
if stats.get("config_errors", 0) > 0:
|
|
701
|
+
issues_text += f" [dim]({stats['config_errors']} config)[/dim]"
|
|
702
|
+
summary_text += f" | Issues found: {issues_text}"
|
|
703
|
+
return summary_text
|
|
704
|
+
|
|
705
|
+
def _build_results_table(self, results: list[HookResult]) -> Table:
|
|
706
|
+
"""Build Rich table from hook results."""
|
|
707
|
+
table = Table(
|
|
708
|
+
box=box.SIMPLE,
|
|
709
|
+
header_style="bold bright_white",
|
|
710
|
+
expand=True,
|
|
711
|
+
)
|
|
712
|
+
table.add_column("Hook", style="cyan", overflow="fold", min_width=20)
|
|
713
|
+
table.add_column("Status", style="bright_white", min_width=8)
|
|
714
|
+
table.add_column("Duration", justify="right", style="magenta", min_width=10)
|
|
715
|
+
table.add_column("Issues", justify="right", style="bright_white", min_width=8)
|
|
630
716
|
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
717
|
+
for result in results:
|
|
718
|
+
status_style = self._status_style(result.status)
|
|
719
|
+
# Passed hooks always show 0 issues (files processed != issues found)
|
|
720
|
+
if result.status == "passed":
|
|
721
|
+
issues_display = "0"
|
|
722
|
+
elif hasattr(result, "is_config_error") and result.is_config_error:
|
|
723
|
+
# Config/tool error - show simple symbol instead of misleading count
|
|
724
|
+
# Using "!" instead of emoji to avoid width issues in terminal
|
|
725
|
+
issues_display = "[yellow]![/yellow]"
|
|
726
|
+
else:
|
|
727
|
+
# For failed hooks with code violations, use issues_count
|
|
728
|
+
# IMPORTANT: Use issues_count directly, don't fall back to len(issues_found)
|
|
729
|
+
# because issues_found may contain display messages that aren't actual issues
|
|
730
|
+
issues_display = str(
|
|
731
|
+
result.issues_count if hasattr(result, "issues_count") else 0
|
|
642
732
|
)
|
|
643
|
-
|
|
644
|
-
|
|
733
|
+
table.add_row(
|
|
734
|
+
self._strip_ansi(result.name),
|
|
735
|
+
f"[{status_style}]{result.status.upper()}[/{status_style}]",
|
|
736
|
+
f"{result.duration:.2f}s",
|
|
737
|
+
issues_display,
|
|
738
|
+
)
|
|
645
739
|
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
740
|
+
return table
|
|
741
|
+
|
|
742
|
+
def _format_issues(self, issues: list[str]) -> list[dict[str, str | int | None]]:
|
|
743
|
+
"""Format hook issues into structured dictionaries."""
|
|
744
|
+
|
|
745
|
+
def _format_single_issue(issue):
|
|
746
|
+
if hasattr(issue, "file_path") and hasattr(issue, "line_number"):
|
|
747
|
+
return {
|
|
748
|
+
"file": str(getattr(issue, "file_path", "unknown")),
|
|
749
|
+
"line": getattr(issue, "line_number", 0),
|
|
750
|
+
"message": getattr(issue, "message", str(issue)),
|
|
751
|
+
"code": getattr(issue, "code", None),
|
|
752
|
+
"severity": getattr(issue, "severity", "warning"),
|
|
753
|
+
"suggestion": getattr(issue, "suggestion", None),
|
|
754
|
+
}
|
|
755
|
+
|
|
756
|
+
return {
|
|
757
|
+
"file": "unknown",
|
|
758
|
+
"line": 0,
|
|
759
|
+
"message": str(issue),
|
|
760
|
+
"code": None,
|
|
761
|
+
"severity": "warning",
|
|
762
|
+
"suggestion": None,
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
return [_format_single_issue(issue) for issue in issues]
|
|
766
|
+
|
|
767
|
+
def to_json(self, results: list[HookResult], suite_name: str = "") -> dict:
|
|
768
|
+
"""Export hook results as structured JSON for automation.
|
|
769
|
+
|
|
770
|
+
Args:
|
|
771
|
+
results: List of HookResult objects to export
|
|
772
|
+
suite_name: Optional suite name (fast/comprehensive)
|
|
773
|
+
|
|
774
|
+
Returns:
|
|
775
|
+
Dictionary with structured results data
|
|
776
|
+
|
|
777
|
+
Example:
|
|
778
|
+
>>> json_data = coordinator.to_json(results, "comprehensive")
|
|
779
|
+
>>> print(json.dumps(json_data, indent=2))
|
|
780
|
+
"""
|
|
781
|
+
return {
|
|
782
|
+
"suite": suite_name,
|
|
783
|
+
"summary": self._calculate_hook_statistics(results),
|
|
784
|
+
"hooks": [
|
|
785
|
+
{
|
|
786
|
+
"name": result.name,
|
|
787
|
+
"status": result.status,
|
|
788
|
+
"duration": round(result.duration, 2),
|
|
789
|
+
"issues_count": len(result.issues_found)
|
|
790
|
+
if result.issues_found
|
|
791
|
+
else 0,
|
|
792
|
+
"issues": self._format_issues(result.issues_found)
|
|
793
|
+
if result.issues_found
|
|
794
|
+
else [],
|
|
795
|
+
}
|
|
796
|
+
for result in results
|
|
797
|
+
],
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
def _build_results_panel(
|
|
801
|
+
self, suite_name: str, table: Table, summary_text: str
|
|
802
|
+
) -> Panel:
|
|
803
|
+
"""Build Rich panel containing results table."""
|
|
804
|
+
return Panel(
|
|
805
|
+
table,
|
|
806
|
+
title=f"[bold]{suite_name.title()} Hook Results[/bold]",
|
|
807
|
+
subtitle=summary_text,
|
|
808
|
+
border_style="cyan" if suite_name == "fast" else "magenta",
|
|
809
|
+
padding=(0, 1),
|
|
810
|
+
width=get_console_width(),
|
|
811
|
+
expand=True,
|
|
812
|
+
)
|
|
649
813
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
814
|
+
def _format_failing_hooks(
|
|
815
|
+
self, suite_name: str, results: list[HookResult]
|
|
816
|
+
) -> list[HookResult]:
|
|
817
|
+
"""Get list of failing hooks and print header.
|
|
818
|
+
|
|
819
|
+
Returns:
|
|
820
|
+
List of failing hook results
|
|
821
|
+
"""
|
|
822
|
+
failing = [
|
|
823
|
+
result
|
|
824
|
+
for result in results
|
|
825
|
+
if result.status.lower() in {"failed", "error", "timeout"}
|
|
826
|
+
]
|
|
827
|
+
|
|
828
|
+
if failing:
|
|
829
|
+
self.console.print(
|
|
830
|
+
f"[red]Details for failing {suite_name} hooks:[/red]", highlight=False
|
|
831
|
+
)
|
|
832
|
+
|
|
833
|
+
return failing
|
|
656
834
|
|
|
657
|
-
def
|
|
835
|
+
def _display_issue_details(self, result: HookResult) -> None:
|
|
836
|
+
"""Display specific issue details if found."""
|
|
837
|
+
if not result.issues_found:
|
|
838
|
+
return
|
|
839
|
+
|
|
840
|
+
for issue in result.issues_found:
|
|
841
|
+
self.console.print(f" - {self._strip_ansi(issue)}", highlight=False)
|
|
842
|
+
|
|
843
|
+
def _display_timeout_info(self, result: HookResult) -> None:
|
|
844
|
+
"""Display timeout information."""
|
|
845
|
+
if result.is_timeout:
|
|
846
|
+
self.console.print(
|
|
847
|
+
" - Hook timed out during execution", highlight=False
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
def _display_exit_code_info(self, result: HookResult) -> None:
|
|
851
|
+
"""Display exit code with helpful context."""
|
|
852
|
+
if result.exit_code is not None and result.exit_code != 0:
|
|
853
|
+
exit_msg = f"Exit code: {result.exit_code}"
|
|
854
|
+
# Add helpful context for common exit codes
|
|
855
|
+
if result.exit_code == 137:
|
|
856
|
+
exit_msg += " (killed - possibly timeout or out of memory)"
|
|
857
|
+
elif result.exit_code == 139:
|
|
858
|
+
exit_msg += " (segmentation fault)"
|
|
859
|
+
elif result.exit_code in {126, 127}:
|
|
860
|
+
exit_msg += " (command not found or not executable)"
|
|
861
|
+
self.console.print(f" - {exit_msg}", highlight=False)
|
|
862
|
+
|
|
863
|
+
def _display_error_message(self, result: HookResult) -> None:
|
|
864
|
+
"""Display error message preview."""
|
|
865
|
+
if result.error_message:
|
|
866
|
+
# Show first line or first 200 chars of error
|
|
867
|
+
error_preview = result.error_message.split("\n")[0][:200]
|
|
868
|
+
self.console.print(f" - Error: {error_preview}", highlight=False)
|
|
869
|
+
|
|
870
|
+
def _display_generic_failure(self, result: HookResult) -> None:
|
|
871
|
+
"""Display generic failure message if no specific details available."""
|
|
872
|
+
if not result.is_timeout and not result.exit_code and not result.error_message:
|
|
873
|
+
self.console.print(
|
|
874
|
+
" - Hook failed with no detailed error information",
|
|
875
|
+
highlight=False,
|
|
876
|
+
)
|
|
877
|
+
|
|
878
|
+
def _display_hook_failures(
|
|
658
879
|
self,
|
|
659
|
-
|
|
880
|
+
suite_name: str,
|
|
881
|
+
results: list[HookResult],
|
|
660
882
|
options: OptionsProtocol,
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
) -> bool:
|
|
666
|
-
self.logger.debug(
|
|
667
|
-
f"{hook_type} hooks failed: {summary['failed']} failed, {summary['errors']} errors",
|
|
668
|
-
)
|
|
883
|
+
) -> None:
|
|
884
|
+
# Show detailed failures if --verbose or --ai-debug flag is set
|
|
885
|
+
if not (options.verbose or getattr(options, "ai_debug", False)):
|
|
886
|
+
return
|
|
669
887
|
|
|
888
|
+
failing = self._format_failing_hooks(suite_name, results)
|
|
889
|
+
if not failing:
|
|
890
|
+
return
|
|
891
|
+
|
|
892
|
+
# Process each failing hook
|
|
893
|
+
for result in failing:
|
|
894
|
+
self._print_single_hook_failure(result)
|
|
895
|
+
|
|
896
|
+
self.console.print()
|
|
897
|
+
|
|
898
|
+
def _print_single_hook_failure(self, result: HookResult) -> None:
|
|
899
|
+
"""Print details of a single hook failure."""
|
|
670
900
|
self.console.print(
|
|
671
|
-
f"[red]
|
|
901
|
+
f" - [red]{self._strip_ansi(result.name)}[/red] ({result.status})",
|
|
902
|
+
highlight=False,
|
|
672
903
|
)
|
|
673
904
|
|
|
674
|
-
if
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
905
|
+
if result.issues_found:
|
|
906
|
+
self._print_hook_issues(result)
|
|
907
|
+
else:
|
|
908
|
+
self._display_failure_reasons(result)
|
|
909
|
+
|
|
910
|
+
def _print_hook_issues(self, result: HookResult) -> None:
|
|
911
|
+
"""Print issues found for a hook."""
|
|
912
|
+
# Type assertion: issues_found is never None after __post_init__
|
|
913
|
+
assert result.issues_found is not None
|
|
914
|
+
for issue in result.issues_found:
|
|
915
|
+
# Show the issue with consistent formatting
|
|
916
|
+
self.console.print(f" - {self._strip_ansi(issue)}", highlight=False)
|
|
917
|
+
|
|
918
|
+
def _display_failure_reasons(self, result: HookResult) -> None:
|
|
919
|
+
"""Display reasons why a hook failed."""
|
|
920
|
+
self._display_timeout_info(result)
|
|
921
|
+
self._display_exit_code_info(result)
|
|
922
|
+
self._display_error_message(result)
|
|
923
|
+
self._display_generic_failure(result)
|
|
680
924
|
|
|
681
|
-
|
|
682
|
-
|
|
925
|
+
def _display_cleaning_header(self) -> None:
|
|
926
|
+
sep = make_separator("-")
|
|
927
|
+
self.console.print("\n" + sep)
|
|
928
|
+
self.console.print("[bold bright_green]🧹 CLEANING[/bold bright_green]")
|
|
929
|
+
self.console.print(sep + "\n")
|
|
683
930
|
|
|
684
|
-
|
|
931
|
+
def _execute_cleaning_process(self) -> bool:
|
|
932
|
+
py_files = list(self.pkg_path.rglob("*.py"))
|
|
933
|
+
if not py_files:
|
|
934
|
+
return self._handle_no_files_to_clean()
|
|
685
935
|
|
|
686
|
-
self.
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
)
|
|
690
|
-
return False
|
|
936
|
+
cleaned_files = self._clean_python_files(py_files)
|
|
937
|
+
self._report_cleaning_results(cleaned_files)
|
|
938
|
+
return True
|
|
691
939
|
|
|
692
|
-
def
|
|
693
|
-
self
|
|
694
|
-
|
|
695
|
-
|
|
696
|
-
f"\n[bold yellow]📋 Detailed {hook_type} hook errors: [/bold yellow]"
|
|
697
|
-
)
|
|
940
|
+
def _handle_no_files_to_clean(self) -> bool:
|
|
941
|
+
self.console.print("No Python files found to clean")
|
|
942
|
+
self.session.complete_task("cleaning", "No files to clean")
|
|
943
|
+
return True
|
|
698
944
|
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
945
|
+
def _clean_python_files(self, files: list[Path]) -> list[str]:
|
|
946
|
+
cleaned_files = []
|
|
947
|
+
for file in files:
|
|
948
|
+
if self.code_cleaner.should_process_file(file):
|
|
949
|
+
result = self.code_cleaner.clean_file(file)
|
|
950
|
+
if result.success:
|
|
951
|
+
cleaned_files.append(str(file))
|
|
952
|
+
return cleaned_files
|
|
953
|
+
|
|
954
|
+
def _report_cleaning_results(self, cleaned_files: list[str]) -> None:
|
|
955
|
+
if cleaned_files:
|
|
956
|
+
self.console.print(f"Cleaned {len(cleaned_files)} files")
|
|
957
|
+
self.session.complete_task(
|
|
958
|
+
"cleaning", f"Cleaned {len(cleaned_files)} files"
|
|
959
|
+
)
|
|
960
|
+
else:
|
|
961
|
+
self.console.print("No cleaning needed for any files")
|
|
962
|
+
self.session.complete_task("cleaning", "No cleaning needed")
|
|
703
963
|
|
|
704
|
-
|
|
705
|
-
|
|
964
|
+
@staticmethod
|
|
965
|
+
def _determine_version_type(options: OptionsProtocol) -> str | None:
|
|
966
|
+
return options.publish or options.all or options.bump
|
|
706
967
|
|
|
707
|
-
|
|
968
|
+
def _execute_publishing_workflow(
|
|
969
|
+
self, options: OptionsProtocol, version_type: str
|
|
970
|
+
) -> bool:
|
|
971
|
+
# Store reference to current HEAD to allow rollback if needed
|
|
972
|
+
original_head = (
|
|
973
|
+
self.git_service.get_current_commit_hash()
|
|
974
|
+
if hasattr(self.git_service, "get_current_commit_hash")
|
|
975
|
+
else None
|
|
976
|
+
)
|
|
708
977
|
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
cleaned_issue = issue.strip()
|
|
713
|
-
self.console.print(f" {cleaned_issue}")
|
|
714
|
-
else:
|
|
715
|
-
self.console.print(f" Hook failed with exit code (status: {status})")
|
|
978
|
+
# STAGE 0: Pre-publish commit if needed
|
|
979
|
+
if not self._handle_pre_publish_commit(options):
|
|
980
|
+
return False
|
|
716
981
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
982
|
+
# STAGE 1: Version bump
|
|
983
|
+
new_version = self._perform_version_bump(version_type)
|
|
984
|
+
if not new_version:
|
|
985
|
+
return False
|
|
721
986
|
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
result, "name", "unknown"
|
|
727
|
-
)
|
|
728
|
-
failed_hooks.append(hook_name.lower())
|
|
987
|
+
# STAGE 2: Commit, tag, and push changes
|
|
988
|
+
current_commit_hash = self._commit_version_changes(new_version)
|
|
989
|
+
if not current_commit_hash:
|
|
990
|
+
return False
|
|
729
991
|
|
|
730
|
-
|
|
731
|
-
|
|
992
|
+
# STAGE 3: Publish to PyPI
|
|
993
|
+
if not self._publish_to_pypi(
|
|
994
|
+
options, new_version, original_head, current_commit_hash
|
|
995
|
+
):
|
|
996
|
+
return False
|
|
732
997
|
|
|
733
|
-
|
|
998
|
+
# Finalize publishing
|
|
999
|
+
self._finalize_publishing(options, new_version)
|
|
734
1000
|
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
if hasattr(result, "failed") and result.failed:
|
|
738
|
-
return True
|
|
1001
|
+
self.session.complete_task("publishing", f"Published version {new_version}")
|
|
1002
|
+
return True
|
|
739
1003
|
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
1004
|
+
def _handle_pre_publish_commit(self, options: OptionsProtocol) -> bool:
|
|
1005
|
+
"""Handle committing existing changes before version bump if needed."""
|
|
1006
|
+
if not options.commit:
|
|
1007
|
+
return True
|
|
744
1008
|
|
|
745
|
-
|
|
746
|
-
if
|
|
747
|
-
|
|
748
|
-
self.logger.debug(f"Applying exponential backoff: {backoff_delay}s")
|
|
749
|
-
self.console.print(f"[dim]Waiting {backoff_delay}s before retry...[/ dim]")
|
|
750
|
-
time.sleep(backoff_delay)
|
|
1009
|
+
existing_changes = self.git_service.get_changed_files()
|
|
1010
|
+
if not existing_changes:
|
|
1011
|
+
return True
|
|
751
1012
|
|
|
752
|
-
|
|
753
|
-
self.logger.info(
|
|
754
|
-
f"{hook_type} hooks passed: {summary['passed']} / {summary['total']}",
|
|
755
|
-
)
|
|
1013
|
+
self._display_commit_push_header()
|
|
756
1014
|
self.console.print(
|
|
757
|
-
|
|
1015
|
+
"[cyan]ℹ️[/cyan] Committing existing changes before version bump..."
|
|
758
1016
|
)
|
|
759
|
-
self.
|
|
760
|
-
|
|
761
|
-
|
|
1017
|
+
commit_message = self._get_commit_message(existing_changes, options)
|
|
1018
|
+
if not self._execute_commit_and_push(existing_changes, commit_message):
|
|
1019
|
+
self.session.fail_task("publishing", "Failed to commit pre-publish changes")
|
|
1020
|
+
return False
|
|
1021
|
+
self.console.print(
|
|
1022
|
+
"[green]✅[/green] Pre-publish changes committed and pushed\n"
|
|
762
1023
|
)
|
|
763
1024
|
return True
|
|
764
1025
|
|
|
765
|
-
def
|
|
766
|
-
|
|
767
|
-
self.
|
|
768
|
-
return False
|
|
1026
|
+
def _perform_version_bump(self, version_type: str) -> str | None:
|
|
1027
|
+
"""Perform the version bump operation."""
|
|
1028
|
+
self._display_version_bump_header()
|
|
769
1029
|
|
|
770
|
-
|
|
1030
|
+
new_version = self.publish_manager.bump_version(version_type)
|
|
1031
|
+
if not new_version:
|
|
1032
|
+
self.session.fail_task("publishing", "Version bumping failed")
|
|
1033
|
+
return None
|
|
1034
|
+
|
|
1035
|
+
self.console.print(f"[green]✅[/green] Version bumped to {new_version}")
|
|
1036
|
+
self.console.print(
|
|
1037
|
+
f"[green]✅[/green] Changelog updated for version {new_version}"
|
|
1038
|
+
)
|
|
1039
|
+
return new_version
|
|
1040
|
+
|
|
1041
|
+
def _commit_version_changes(self, new_version: str) -> str | None:
|
|
1042
|
+
"""Commit the version changes to git."""
|
|
1043
|
+
self._display_commit_push_header()
|
|
1044
|
+
|
|
1045
|
+
# Stage changes
|
|
1046
|
+
changed_files = self.git_service.get_changed_files()
|
|
1047
|
+
if not changed_files:
|
|
1048
|
+
self.console.print("[yellow]⚠️[/yellow] No changes to stage")
|
|
1049
|
+
self.session.fail_task("publishing", "No changes to commit")
|
|
1050
|
+
return None
|
|
1051
|
+
|
|
1052
|
+
if not self.git_service.add_files(changed_files):
|
|
1053
|
+
self.session.fail_task("publishing", "Failed to stage files")
|
|
1054
|
+
return None
|
|
1055
|
+
self.console.print(f"[green]✅[/green] Staged {len(changed_files)} files")
|
|
1056
|
+
|
|
1057
|
+
# Commit
|
|
1058
|
+
commit_message = f"chore: bump version to {new_version}"
|
|
1059
|
+
if not self.git_service.commit(commit_message):
|
|
1060
|
+
self.session.fail_task("publishing", "Failed to commit changes")
|
|
1061
|
+
return None
|
|
1062
|
+
current_commit_hash = (
|
|
1063
|
+
self.git_service.get_current_commit_hash()
|
|
1064
|
+
if hasattr(self.git_service, "get_current_commit_hash")
|
|
1065
|
+
else None
|
|
1066
|
+
)
|
|
1067
|
+
return current_commit_hash
|
|
1068
|
+
|
|
1069
|
+
def _publish_to_pypi(
|
|
771
1070
|
self,
|
|
772
|
-
hook_type: str,
|
|
773
|
-
hook_runner: t.Callable[[], list[t.Any]],
|
|
774
1071
|
options: OptionsProtocol,
|
|
1072
|
+
new_version: str,
|
|
1073
|
+
original_head: str | None,
|
|
1074
|
+
current_commit_hash: str | None,
|
|
775
1075
|
) -> bool:
|
|
776
|
-
|
|
1076
|
+
"""Publish the package to PyPI."""
|
|
1077
|
+
self._display_publish_header()
|
|
1078
|
+
|
|
1079
|
+
# Build and publish package
|
|
1080
|
+
if not self.publish_manager.publish_package():
|
|
1081
|
+
self.session.fail_task("publishing", "Package publishing failed")
|
|
1082
|
+
# Attempt to rollback the version bump commit if publishing fails
|
|
1083
|
+
if current_commit_hash and original_head:
|
|
1084
|
+
self._attempt_rollback_version_bump(original_head, current_commit_hash)
|
|
1085
|
+
return False
|
|
1086
|
+
return True
|
|
1087
|
+
|
|
1088
|
+
def _finalize_publishing(self, options: OptionsProtocol, new_version: str) -> None:
|
|
1089
|
+
"""Finalize the publishing process after successful PyPI publishing."""
|
|
1090
|
+
# Create git tag and push only after successful PyPI publishing
|
|
1091
|
+
if not options.no_git_tags:
|
|
1092
|
+
if not self.publish_manager.create_git_tag_local(new_version):
|
|
1093
|
+
self.console.print(
|
|
1094
|
+
f"[yellow]⚠️[/yellow] Failed to create git tag v{new_version}"
|
|
1095
|
+
)
|
|
1096
|
+
|
|
1097
|
+
# Push commit and tag together in single operation only after successful PyPI publishing
|
|
1098
|
+
if not self.git_service.push_with_tags():
|
|
1099
|
+
self.console.print("[yellow]⚠️[/yellow] Push failed. Please push manually")
|
|
1100
|
+
# Not failing the whole workflow for a push failure
|
|
777
1101
|
|
|
1102
|
+
def _attempt_rollback_version_bump(
|
|
1103
|
+
self, original_head: str, current_commit_hash: str
|
|
1104
|
+
) -> bool:
|
|
1105
|
+
"""Attempt to undo the version bump commit if publishing fails."""
|
|
778
1106
|
try:
|
|
779
|
-
|
|
780
|
-
|
|
1107
|
+
self.console.print(
|
|
1108
|
+
"[yellow]🔄 Attempting to rollback version bump commit...[/yellow]"
|
|
781
1109
|
)
|
|
782
1110
|
|
|
1111
|
+
# Reset to the original HEAD (before version bump commit)
|
|
1112
|
+
result = self.git_service.reset_hard(original_head)
|
|
1113
|
+
|
|
1114
|
+
if result:
|
|
1115
|
+
self.console.print(
|
|
1116
|
+
f"[green]✅ Version bump commit ({current_commit_hash[:8]}...) reverted[/green]"
|
|
1117
|
+
)
|
|
1118
|
+
return True
|
|
1119
|
+
else:
|
|
1120
|
+
self.console.print("[red]❌ Failed to revert version bump commit[/red]")
|
|
1121
|
+
return False
|
|
783
1122
|
except Exception as e:
|
|
784
|
-
|
|
1123
|
+
self.console.print(f"[red]❌ Error during version rollback: {e}[/red]")
|
|
1124
|
+
return False
|
|
785
1125
|
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
) -> bool:
|
|
792
|
-
results = hook_runner()
|
|
793
|
-
summary = self.hook_manager.get_hook_summary(results)
|
|
1126
|
+
def _display_version_bump_header(self) -> None:
|
|
1127
|
+
sep = make_separator("-")
|
|
1128
|
+
self.console.print("\n" + sep)
|
|
1129
|
+
self.console.print("[bold bright_cyan]📝 VERSION BUMP[/bold bright_cyan]")
|
|
1130
|
+
self.console.print(sep + "\n")
|
|
794
1131
|
|
|
795
|
-
|
|
796
|
-
|
|
1132
|
+
def _display_commit_push_header(self) -> None:
|
|
1133
|
+
sep = make_separator("-")
|
|
1134
|
+
self.console.print("\n" + sep)
|
|
1135
|
+
self.console.print("[bold bright_blue]📦 COMMIT & PUSH[/bold bright_blue]")
|
|
1136
|
+
self.console.print(sep + "\n")
|
|
797
1137
|
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
)
|
|
1138
|
+
def _display_publish_header(self) -> None:
|
|
1139
|
+
sep = make_separator("-")
|
|
1140
|
+
self.console.print("\n" + sep)
|
|
1141
|
+
self.console.print("[bold bright_green]🚀 PUBLISH TO PYPI[/bold bright_green]")
|
|
1142
|
+
self.console.print(sep + "\n")
|
|
801
1143
|
|
|
802
|
-
def
|
|
803
|
-
self
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
options: OptionsProtocol,
|
|
807
|
-
results: list[t.Any],
|
|
808
|
-
summary: dict[str, t.Any],
|
|
809
|
-
) -> bool:
|
|
810
|
-
if hook_type != "fast":
|
|
811
|
-
return self._handle_hook_failures(
|
|
812
|
-
hook_type, options, summary, results, 0, 1
|
|
813
|
-
)
|
|
1144
|
+
def _handle_no_changes_to_commit(self) -> bool:
|
|
1145
|
+
self.console.print("No changes to commit")
|
|
1146
|
+
self.session.complete_task("commit", "No changes to commit")
|
|
1147
|
+
return True
|
|
814
1148
|
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
1149
|
+
def _get_commit_message(
|
|
1150
|
+
self, changed_files: list[str], options: OptionsProtocol
|
|
1151
|
+
) -> str:
|
|
1152
|
+
suggestions = self.git_service.get_commit_message_suggestions(changed_files)
|
|
1153
|
+
if not suggestions:
|
|
1154
|
+
return "Update project files"
|
|
819
1155
|
|
|
820
|
-
|
|
1156
|
+
if not options.interactive:
|
|
1157
|
+
return suggestions[0]
|
|
821
1158
|
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
)
|
|
1159
|
+
return self._interactive_commit_message_selection(suggestions)
|
|
1160
|
+
|
|
1161
|
+
def _interactive_commit_message_selection(self, suggestions: list[str]) -> str:
|
|
1162
|
+
self._display_commit_suggestions(suggestions)
|
|
1163
|
+
choice = self.console.input(
|
|
1164
|
+
"\nEnter number, custom message, or press Enter for default: "
|
|
1165
|
+
).strip()
|
|
1166
|
+
return self._process_commit_choice(choice, suggestions)
|
|
831
1167
|
|
|
832
|
-
|
|
833
|
-
|
|
1168
|
+
def _display_commit_suggestions(self, suggestions: list[str]) -> None:
|
|
1169
|
+
self.console.print("\n[bold]Commit message suggestions:[/bold]")
|
|
1170
|
+
for i, suggestion in enumerate(suggestions, 1):
|
|
1171
|
+
self.console.print(f" [cyan]{i}[/cyan]: {suggestion}")
|
|
834
1172
|
|
|
835
|
-
|
|
836
|
-
|
|
1173
|
+
@staticmethod
|
|
1174
|
+
def _process_commit_choice(choice: str, suggestions: list[str]) -> str:
|
|
1175
|
+
if not choice:
|
|
1176
|
+
return suggestions[0]
|
|
1177
|
+
if choice.isdigit() and 1 <= int(choice) <= len(suggestions):
|
|
1178
|
+
return suggestions[int(choice) - 1]
|
|
1179
|
+
return choice
|
|
837
1180
|
|
|
838
|
-
|
|
1181
|
+
def _execute_commit_and_push(
|
|
1182
|
+
self, changed_files: list[str], commit_message: str
|
|
1183
|
+
) -> bool:
|
|
1184
|
+
if not self.git_service.add_files(changed_files):
|
|
1185
|
+
self.session.fail_task("commit", "Failed to add files to git")
|
|
1186
|
+
return False
|
|
1187
|
+
if not self.git_service.commit(commit_message):
|
|
1188
|
+
self.session.fail_task("commit", "Failed to commit files")
|
|
1189
|
+
return False
|
|
1190
|
+
if not self.git_service.push():
|
|
1191
|
+
self.console.print("[yellow]⚠️[/yellow] Push failed. Please push manually")
|
|
1192
|
+
# Not failing the whole workflow for a push failure
|
|
1193
|
+
self.session.complete_task("commit", "Committed and pushed changes")
|
|
1194
|
+
return True
|
|
839
1195
|
|
|
840
|
-
@
|
|
841
|
-
def
|
|
842
|
-
|
|
843
|
-
|
|
1196
|
+
@staticmethod
|
|
1197
|
+
def _format_hook_summary(summary: dict[str, t.Any]) -> str:
|
|
1198
|
+
if not summary:
|
|
1199
|
+
return "No hooks executed"
|
|
1200
|
+
|
|
1201
|
+
total = summary.get("total", 0)
|
|
1202
|
+
passed = summary.get("passed", 0)
|
|
1203
|
+
failed = summary.get("failed", 0)
|
|
1204
|
+
errors = summary.get("errors", 0)
|
|
1205
|
+
duration = summary.get("total_duration", 0.0)
|
|
1206
|
+
|
|
1207
|
+
parts = [f"{passed}/{total} passed"]
|
|
1208
|
+
if failed:
|
|
1209
|
+
parts.append(f"{failed} failed")
|
|
1210
|
+
if errors:
|
|
1211
|
+
parts.append(f"{errors} errors")
|
|
1212
|
+
|
|
1213
|
+
summary_str = ", ".join(parts)
|
|
1214
|
+
return f"{summary_str} in {duration:.2f}s"
|
|
1215
|
+
|
|
1216
|
+
@staticmethod
|
|
1217
|
+
def _status_style(status: str) -> str:
|
|
1218
|
+
normalized = status.lower()
|
|
1219
|
+
if normalized == "passed":
|
|
1220
|
+
return "green"
|
|
1221
|
+
if normalized in {"failed", "error"}:
|
|
1222
|
+
return "red"
|
|
1223
|
+
if normalized == "timeout":
|
|
1224
|
+
return "yellow"
|
|
1225
|
+
return "bright_white"
|
|
1226
|
+
|
|
1227
|
+
# (All printing is handled by acb.console.Console which supports robust I/O.)
|