roadmap-cli 0.1.1__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.
- roadmap/__init__.py +27 -0
- roadmap/adapters/__init__.py +1 -0
- roadmap/adapters/base_paginated_adapter.py +117 -0
- roadmap/adapters/cli/__init__.py +249 -0
- roadmap/adapters/cli/analysis/__init__.py +5 -0
- roadmap/adapters/cli/analysis/commands.py +370 -0
- roadmap/adapters/cli/analysis/presenter.py +125 -0
- roadmap/adapters/cli/archive_operations.py +86 -0
- roadmap/adapters/cli/cli_command_helpers.py +109 -0
- roadmap/adapters/cli/cli_confirmations.py +74 -0
- roadmap/adapters/cli/cli_error_handlers.py +146 -0
- roadmap/adapters/cli/cli_validators.py +39 -0
- roadmap/adapters/cli/commands/sync_status.py +308 -0
- roadmap/adapters/cli/comment/__init__.py +5 -0
- roadmap/adapters/cli/comment/commands.py +106 -0
- roadmap/adapters/cli/config/__init__.py +5 -0
- roadmap/adapters/cli/config/commands.py +198 -0
- roadmap/adapters/cli/console_exports.py +10 -0
- roadmap/adapters/cli/core.py +26 -0
- roadmap/adapters/cli/crud/__init__.py +47 -0
- roadmap/adapters/cli/crud/base_archive.py +208 -0
- roadmap/adapters/cli/crud/base_create.py +154 -0
- roadmap/adapters/cli/crud/base_delete.py +155 -0
- roadmap/adapters/cli/crud/base_restore.py +283 -0
- roadmap/adapters/cli/crud/base_update.py +193 -0
- roadmap/adapters/cli/crud/crud_helpers.py +206 -0
- roadmap/adapters/cli/crud/crud_utils.py +152 -0
- roadmap/adapters/cli/crud/entity_builders.py +374 -0
- roadmap/adapters/cli/data/__init__.py +8 -0
- roadmap/adapters/cli/data/commands.py +149 -0
- roadmap/adapters/cli/decorators.py +208 -0
- roadmap/adapters/cli/dtos/__init__.py +155 -0
- roadmap/adapters/cli/exception_handler.py +105 -0
- roadmap/adapters/cli/git/__init__.py +8 -0
- roadmap/adapters/cli/git/commands.py +475 -0
- roadmap/adapters/cli/git/handlers/__init__.py +1 -0
- roadmap/adapters/cli/git/handlers/git_authentication_handler.py +124 -0
- roadmap/adapters/cli/git/handlers/git_branch_handler.py +238 -0
- roadmap/adapters/cli/git/handlers/git_connectivity_handler.py +122 -0
- roadmap/adapters/cli/git/handlers/git_hooks_handler.py +105 -0
- roadmap/adapters/cli/git/hooks_config.py +327 -0
- roadmap/adapters/cli/git/status_display.py +158 -0
- roadmap/adapters/cli/health/__init__.py +8 -0
- roadmap/adapters/cli/health/db_integrity.py +204 -0
- roadmap/adapters/cli/health/enhancer.py +227 -0
- roadmap/adapters/cli/health/fixer.py +295 -0
- roadmap/adapters/cli/health/fixers/__init__.py +10 -0
- roadmap/adapters/cli/health/fixers/corrupted_comments_fixer.py +231 -0
- roadmap/adapters/cli/health/fixers/data_integrity_fixer.py +137 -0
- roadmap/adapters/cli/health/fixers/duplicate_issues_fixer.py +138 -0
- roadmap/adapters/cli/health/fixers/folder_structure_fixer.py +114 -0
- roadmap/adapters/cli/health/fixers/label_normalization_fixer.py +162 -0
- roadmap/adapters/cli/health/fixers/milestone_name_normalization_fixer.py +181 -0
- roadmap/adapters/cli/health/fixers/milestone_naming_compliance_fixer.py +244 -0
- roadmap/adapters/cli/health/fixers/milestone_parse_error_fixer.py +162 -0
- roadmap/adapters/cli/health/fixers/milestone_validation_fixer.py +211 -0
- roadmap/adapters/cli/health/fixers/old_backups_fixer.py +135 -0
- roadmap/adapters/cli/health/fixers/orphaned_issues_fixer.py +252 -0
- roadmap/adapters/cli/health/formatter.py +250 -0
- roadmap/adapters/cli/health/formatters.py +427 -0
- roadmap/adapters/cli/health/scan.py +293 -0
- roadmap/adapters/cli/init/__init__.py +5 -0
- roadmap/adapters/cli/init/commands.py +587 -0
- roadmap/adapters/cli/init.py +9 -0
- roadmap/adapters/cli/issues/__init__.py +70 -0
- roadmap/adapters/cli/issues/archive.py +137 -0
- roadmap/adapters/cli/issues/archive_class.py +253 -0
- roadmap/adapters/cli/issues/block.py +39 -0
- roadmap/adapters/cli/issues/close.py +216 -0
- roadmap/adapters/cli/issues/comment.py +178 -0
- roadmap/adapters/cli/issues/create.py +162 -0
- roadmap/adapters/cli/issues/delete.py +31 -0
- roadmap/adapters/cli/issues/deps.py +161 -0
- roadmap/adapters/cli/issues/issue_status_helpers.py +111 -0
- roadmap/adapters/cli/issues/link.py +308 -0
- roadmap/adapters/cli/issues/list.py +402 -0
- roadmap/adapters/cli/issues/lookup.py +96 -0
- roadmap/adapters/cli/issues/progress.py +102 -0
- roadmap/adapters/cli/issues/restore.py +96 -0
- roadmap/adapters/cli/issues/restore_class.py +57 -0
- roadmap/adapters/cli/issues/start.py +165 -0
- roadmap/adapters/cli/issues/sync_status.py +374 -0
- roadmap/adapters/cli/issues/unblock.py +43 -0
- roadmap/adapters/cli/issues/unlink.py +104 -0
- roadmap/adapters/cli/issues/update.py +120 -0
- roadmap/adapters/cli/issues/view.py +36 -0
- roadmap/adapters/cli/layout.py +220 -0
- roadmap/adapters/cli/mappers.py +200 -0
- roadmap/adapters/cli/milestones/__init__.py +52 -0
- roadmap/adapters/cli/milestones/archive.py +125 -0
- roadmap/adapters/cli/milestones/archive_class.py +236 -0
- roadmap/adapters/cli/milestones/assign.py +62 -0
- roadmap/adapters/cli/milestones/close.py +177 -0
- roadmap/adapters/cli/milestones/create.py +131 -0
- roadmap/adapters/cli/milestones/delete.py +27 -0
- roadmap/adapters/cli/milestones/kanban.py +123 -0
- roadmap/adapters/cli/milestones/list.py +87 -0
- roadmap/adapters/cli/milestones/recalculate.py +79 -0
- roadmap/adapters/cli/milestones/restore.py +87 -0
- roadmap/adapters/cli/milestones/restore_class.py +99 -0
- roadmap/adapters/cli/milestones/update.py +61 -0
- roadmap/adapters/cli/milestones/view.py +142 -0
- roadmap/adapters/cli/output_manager.py +319 -0
- roadmap/adapters/cli/presentation/base_presenter.py +121 -0
- roadmap/adapters/cli/presentation/cleanup_presenter.py +235 -0
- roadmap/adapters/cli/presentation/core_initialization_presenter.py +277 -0
- roadmap/adapters/cli/presentation/crud_presenter.py +134 -0
- roadmap/adapters/cli/presentation/daily_summary_presenter.py +266 -0
- roadmap/adapters/cli/presentation/issue_presenter.py +258 -0
- roadmap/adapters/cli/presentation/milestone_list_presenter.py +135 -0
- roadmap/adapters/cli/presentation/milestone_presenter.py +297 -0
- roadmap/adapters/cli/presentation/project_presenter.py +233 -0
- roadmap/adapters/cli/presentation/project_status_presenter.py +186 -0
- roadmap/adapters/cli/presentation/table_builders.py +95 -0
- roadmap/adapters/cli/projects/__init__.py +42 -0
- roadmap/adapters/cli/projects/archive.py +127 -0
- roadmap/adapters/cli/projects/archive_class.py +96 -0
- roadmap/adapters/cli/projects/close.py +94 -0
- roadmap/adapters/cli/projects/create.py +50 -0
- roadmap/adapters/cli/projects/delete.py +27 -0
- roadmap/adapters/cli/projects/list.py +227 -0
- roadmap/adapters/cli/projects/restore.py +87 -0
- roadmap/adapters/cli/projects/restore_class.py +56 -0
- roadmap/adapters/cli/projects/update.py +57 -0
- roadmap/adapters/cli/projects/view.py +157 -0
- roadmap/adapters/cli/services/__init__.py +1 -0
- roadmap/adapters/cli/services/daily_summary_service.py +313 -0
- roadmap/adapters/cli/services/export_manager.py +268 -0
- roadmap/adapters/cli/services/milestone_list_service.py +266 -0
- roadmap/adapters/cli/services/project_initialization_service.py +29 -0
- roadmap/adapters/cli/services/project_status_service.py +462 -0
- roadmap/adapters/cli/services/sync_service.py +62 -0
- roadmap/adapters/cli/status.py +408 -0
- roadmap/adapters/cli/styling.py +21 -0
- roadmap/adapters/cli/sync.py +1117 -0
- roadmap/adapters/cli/sync_context.py +432 -0
- roadmap/adapters/cli/sync_handlers/__init__.py +77 -0
- roadmap/adapters/cli/sync_handlers/apply_ops.py +522 -0
- roadmap/adapters/cli/sync_handlers/baseline_ops.py +366 -0
- roadmap/adapters/cli/sync_handlers/conflict_ops.py +161 -0
- roadmap/adapters/cli/sync_handlers/dry_run_display.py +237 -0
- roadmap/adapters/cli/sync_handlers/interactive_resolver.py +396 -0
- roadmap/adapters/cli/sync_handlers/progress_tracker.py +128 -0
- roadmap/adapters/cli/sync_metrics_command.py +190 -0
- roadmap/adapters/cli/sync_presenter.py +58 -0
- roadmap/adapters/cli/sync_validation.py +295 -0
- roadmap/adapters/cli/today.py +52 -0
- roadmap/adapters/cli/utils/click_options.py +38 -0
- roadmap/adapters/git/__init__.py +1 -0
- roadmap/adapters/git/git.py +208 -0
- roadmap/adapters/git/git_branch_manager.py +400 -0
- roadmap/adapters/git/git_command_executor.py +56 -0
- roadmap/adapters/git/git_commit_analyzer.py +334 -0
- roadmap/adapters/git/git_hooks.py +22 -0
- roadmap/adapters/git/git_hooks_manager.py +455 -0
- roadmap/adapters/git/git_repository_info.py +65 -0
- roadmap/adapters/git/hook_installer.py +106 -0
- roadmap/adapters/git/hook_registry.py +97 -0
- roadmap/adapters/git/hook_script_generator.py +58 -0
- roadmap/adapters/git/sync_monitor.py +409 -0
- roadmap/adapters/git/workflow_automation.py +286 -0
- roadmap/adapters/github/__init__.py +1 -0
- roadmap/adapters/github/github.py +360 -0
- roadmap/adapters/github/handlers/__init__.py +17 -0
- roadmap/adapters/github/handlers/base.py +337 -0
- roadmap/adapters/github/handlers/collaborators.py +165 -0
- roadmap/adapters/github/handlers/comments.py +118 -0
- roadmap/adapters/github/handlers/issues.py +150 -0
- roadmap/adapters/github/handlers/labels.py +164 -0
- roadmap/adapters/github/handlers/milestones.py +104 -0
- roadmap/adapters/persistence/__init__.py +35 -0
- roadmap/adapters/persistence/conflict_resolver.py +190 -0
- roadmap/adapters/persistence/database_manager.py +492 -0
- roadmap/adapters/persistence/entity_sync_coordinators.py +672 -0
- roadmap/adapters/persistence/file_locking.py +348 -0
- roadmap/adapters/persistence/file_parser.py +92 -0
- roadmap/adapters/persistence/file_synchronizer.py +107 -0
- roadmap/adapters/persistence/focused_managers.py +140 -0
- roadmap/adapters/persistence/git_history.py +330 -0
- roadmap/adapters/persistence/parser/__init__.py +20 -0
- roadmap/adapters/persistence/parser/frontmatter.py +152 -0
- roadmap/adapters/persistence/parser/issue.py +245 -0
- roadmap/adapters/persistence/parser/milestone.py +146 -0
- roadmap/adapters/persistence/parser/project.py +106 -0
- roadmap/adapters/persistence/persistence.py +288 -0
- roadmap/adapters/persistence/repositories/__init__.py +15 -0
- roadmap/adapters/persistence/repositories/issue_repository.py +223 -0
- roadmap/adapters/persistence/repositories/milestone_repository.py +140 -0
- roadmap/adapters/persistence/repositories/project_repository.py +168 -0
- roadmap/adapters/persistence/repositories/remote_link_repository.py +355 -0
- roadmap/adapters/persistence/repositories/sync_state_repository.py +66 -0
- roadmap/adapters/persistence/storage/__init__.py +36 -0
- roadmap/adapters/persistence/storage/conflicts.py +101 -0
- roadmap/adapters/persistence/storage/connection_manager.py +79 -0
- roadmap/adapters/persistence/storage/issue_storage.py +89 -0
- roadmap/adapters/persistence/storage/milestone_storage.py +80 -0
- roadmap/adapters/persistence/storage/project_storage.py +102 -0
- roadmap/adapters/persistence/storage/queries.py +227 -0
- roadmap/adapters/persistence/storage/state_manager.py +652 -0
- roadmap/adapters/persistence/storage/sync_state_storage.py +207 -0
- roadmap/adapters/persistence/sync_metrics_repository.py +342 -0
- roadmap/adapters/persistence/sync_orchestrator.py +293 -0
- roadmap/adapters/persistence/sync_state_tracker.py +86 -0
- roadmap/adapters/persistence/yaml_repositories.py +711 -0
- roadmap/adapters/sync/__init__.py +21 -0
- roadmap/adapters/sync/backend_factory.py +108 -0
- roadmap/adapters/sync/backends/__init__.py +15 -0
- roadmap/adapters/sync/backends/converters.py +209 -0
- roadmap/adapters/sync/backends/github_backend_helpers.py +380 -0
- roadmap/adapters/sync/backends/github_client.py +99 -0
- roadmap/adapters/sync/backends/github_sync_backend.py +792 -0
- roadmap/adapters/sync/backends/github_sync_ops.py +908 -0
- roadmap/adapters/sync/backends/services/__init__.py +1 -0
- roadmap/adapters/sync/backends/services/github_authentication_service.py +105 -0
- roadmap/adapters/sync/backends/services/github_issue_delete_service.py +423 -0
- roadmap/adapters/sync/backends/services/github_issue_dependency_service.py +63 -0
- roadmap/adapters/sync/backends/services/github_issue_fetch_service.py +158 -0
- roadmap/adapters/sync/backends/services/github_label_sync_service.py +126 -0
- roadmap/adapters/sync/backends/services/github_local_issue_sync_service.py +229 -0
- roadmap/adapters/sync/backends/services/github_milestone_fetch_service.py +204 -0
- roadmap/adapters/sync/backends/vanilla_git_sync_backend.py +198 -0
- roadmap/adapters/sync/services/__init__.py +11 -0
- roadmap/adapters/sync/services/baseline_state_handler.py +83 -0
- roadmap/adapters/sync/services/conflict_converter.py +85 -0
- roadmap/adapters/sync/services/issue_persistence_service.py +263 -0
- roadmap/adapters/sync/services/issue_state_service.py +243 -0
- roadmap/adapters/sync/services/local_change_filter.py +129 -0
- roadmap/adapters/sync/services/pull_result_processor.py +71 -0
- roadmap/adapters/sync/services/remote_issue_creation_service.py +112 -0
- roadmap/adapters/sync/services/sync_analysis_service.py +250 -0
- roadmap/adapters/sync/services/sync_authentication_service.py +57 -0
- roadmap/adapters/sync/services/sync_data_fetch_service.py +212 -0
- roadmap/adapters/sync/services/sync_linking_service.py +329 -0
- roadmap/adapters/sync/services/sync_report_service.py +62 -0
- roadmap/adapters/sync/services/sync_state_update_service.py +51 -0
- roadmap/adapters/sync/sync_cache_orchestrator.py +454 -0
- roadmap/adapters/sync/sync_merge_engine.py +416 -0
- roadmap/adapters/sync/sync_merge_orchestrator.py +1313 -0
- roadmap/adapters/sync/sync_retrieval_orchestrator.py +810 -0
- roadmap/application/services/__init__.py +1 -0
- roadmap/application/services/deduplicate_service.py +517 -0
- roadmap/application/use_cases/__init__.py +1 -0
- roadmap/common/__init__.py +308 -0
- roadmap/common/cache.py +209 -0
- roadmap/common/cli_errors.py +147 -0
- roadmap/common/configuration/__init__.py +33 -0
- roadmap/common/configuration/config_loader.py +264 -0
- roadmap/common/configuration/config_manager.py +164 -0
- roadmap/common/configuration/config_schema.py +145 -0
- roadmap/common/configuration/github/__init__.py +9 -0
- roadmap/common/configuration/github/config_manager.py +154 -0
- roadmap/common/configuration/github/token_resolver.py +84 -0
- roadmap/common/console.py +184 -0
- roadmap/common/constants.py +134 -0
- roadmap/common/datetime_parser.py +319 -0
- roadmap/common/error_formatter.py +84 -0
- roadmap/common/errors/__init__.py +143 -0
- roadmap/common/errors/error_base.py +73 -0
- roadmap/common/errors/error_file.py +82 -0
- roadmap/common/errors/error_git.py +67 -0
- roadmap/common/errors/error_handler.py +166 -0
- roadmap/common/errors/error_network.py +90 -0
- roadmap/common/errors/error_security.py +78 -0
- roadmap/common/errors/error_standards.py +561 -0
- roadmap/common/errors/error_validation.py +111 -0
- roadmap/common/errors/exceptions.py +432 -0
- roadmap/common/formatters/__init__.py +29 -0
- roadmap/common/formatters/base_table_formatter.py +96 -0
- roadmap/common/formatters/export/__init__.py +7 -0
- roadmap/common/formatters/export/issue_exporter.py +97 -0
- roadmap/common/formatters/helpers.py +28 -0
- roadmap/common/formatters/kanban/__init__.py +9 -0
- roadmap/common/formatters/kanban/layout.py +48 -0
- roadmap/common/formatters/kanban/organizer.py +81 -0
- roadmap/common/formatters/output/__init__.py +15 -0
- roadmap/common/formatters/tables/__init__.py +11 -0
- roadmap/common/formatters/tables/column_factory.py +312 -0
- roadmap/common/formatters/tables/issue_table.py +276 -0
- roadmap/common/formatters/tables/milestone_table.py +247 -0
- roadmap/common/formatters/tables/project_table.py +195 -0
- roadmap/common/formatters/text/__init__.py +57 -0
- roadmap/common/formatters/text/basic.py +217 -0
- roadmap/common/formatters/text/display.py +42 -0
- roadmap/common/formatters/text/duration.py +36 -0
- roadmap/common/formatters/text/operations.py +325 -0
- roadmap/common/formatters/text/status_badges.py +57 -0
- roadmap/common/initialization/github/__init__.py +11 -0
- roadmap/common/initialization/github/setup_service.py +238 -0
- roadmap/common/initialization/github/setup_validator.py +78 -0
- roadmap/common/logging/__init__.py +113 -0
- roadmap/common/logging/audit_logging.py +5 -0
- roadmap/common/logging/correlation.py +45 -0
- roadmap/common/logging/decorators.py +267 -0
- roadmap/common/logging/error_logging.py +217 -0
- roadmap/common/logging/formatters.py +124 -0
- roadmap/common/logging/loggers.py +68 -0
- roadmap/common/logging/performance_tracking.py +267 -0
- roadmap/common/logging/utils.py +335 -0
- roadmap/common/models/__init__.py +44 -0
- roadmap/common/models/cli_models.py +118 -0
- roadmap/common/models/config_models.py +199 -0
- roadmap/common/models/output_models.py +381 -0
- roadmap/common/observability/__init__.py +28 -0
- roadmap/common/observability/instrumentation.py +103 -0
- roadmap/common/observability/observability.py +76 -0
- roadmap/common/observability/otel_init.py +109 -0
- roadmap/common/output_formatter.py +393 -0
- roadmap/common/progress.py +379 -0
- roadmap/common/result.py +396 -0
- roadmap/common/security/__init__.py +48 -0
- roadmap/common/security/exceptions.py +13 -0
- roadmap/common/security/export_cleanup.py +79 -0
- roadmap/common/security/file_operations.py +104 -0
- roadmap/common/security/filename_sanitization.py +54 -0
- roadmap/common/security/logging.py +27 -0
- roadmap/common/security/path_validation.py +137 -0
- roadmap/common/security/temp_files.py +41 -0
- roadmap/common/services/__init__.py +65 -0
- roadmap/common/services/decorators.py +183 -0
- roadmap/common/services/logging_utils.py +208 -0
- roadmap/common/services/metrics.py +161 -0
- roadmap/common/services/performance.py +219 -0
- roadmap/common/services/profiling.py +288 -0
- roadmap/common/services/retry.py +288 -0
- roadmap/common/status_style_manager.py +114 -0
- roadmap/common/union_find.py +88 -0
- roadmap/common/update_constants.py +13 -0
- roadmap/common/utils/__init__.py +65 -0
- roadmap/common/utils/cli_helpers.py +381 -0
- roadmap/common/utils/file_utils.py +440 -0
- roadmap/common/utils/path_utils.py +38 -0
- roadmap/common/utils/status_utils.py +130 -0
- roadmap/common/utils/timezone_utils.py +480 -0
- roadmap/common/validation/__init__.py +37 -0
- roadmap/common/validation/field_validator.py +104 -0
- roadmap/common/validation/result.py +37 -0
- roadmap/common/validation/roadmap_validator.py +310 -0
- roadmap/common/validation/schema_validator.py +36 -0
- roadmap/common/validation/validators.py +50 -0
- roadmap/core/__init__.py +1 -0
- roadmap/core/domain/__init__.py +30 -0
- roadmap/core/domain/comment.py +24 -0
- roadmap/core/domain/health.py +15 -0
- roadmap/core/domain/issue.py +274 -0
- roadmap/core/domain/milestone.py +173 -0
- roadmap/core/domain/ports/__init__.py +1 -0
- roadmap/core/domain/ports/baseline_repository.py +24 -0
- roadmap/core/domain/ports/issue_repository.py +34 -0
- roadmap/core/domain/ports/remote_backend_port.py +31 -0
- roadmap/core/domain/project.py +130 -0
- roadmap/core/interfaces/__init__.py +109 -0
- roadmap/core/interfaces/assignee_validator.py +40 -0
- roadmap/core/interfaces/backend_factory.py +175 -0
- roadmap/core/interfaces/github.py +102 -0
- roadmap/core/interfaces/parsers.py +140 -0
- roadmap/core/interfaces/persistence.py +45 -0
- roadmap/core/interfaces/repositories.py +174 -0
- roadmap/core/interfaces/state_managers.py +111 -0
- roadmap/core/interfaces/state_storage.py +64 -0
- roadmap/core/interfaces/sync_backend.py +275 -0
- roadmap/core/interfaces/sync_services.py +119 -0
- roadmap/core/interfaces/sync_validators.py +180 -0
- roadmap/core/models/__init__.py +31 -0
- roadmap/core/models/sync_models.py +162 -0
- roadmap/core/models.py +115 -0
- roadmap/core/observability/__init__.py +17 -0
- roadmap/core/observability/sync_metrics.py +699 -0
- roadmap/core/repositories.py +200 -0
- roadmap/core/services/__init__.py +313 -0
- roadmap/core/services/baseline/__init__.py +1 -0
- roadmap/core/services/baseline/baseline_builder_progress.py +274 -0
- roadmap/core/services/baseline/baseline_retriever.py +63 -0
- roadmap/core/services/baseline/baseline_selector.py +248 -0
- roadmap/core/services/baseline/baseline_state_retriever.py +284 -0
- roadmap/core/services/baseline/optimized_baseline_builder.py +425 -0
- roadmap/core/services/comment/__init__.py +1 -0
- roadmap/core/services/comment/comment_service.py +277 -0
- roadmap/core/services/git/__init__.py +1 -0
- roadmap/core/services/git/git_hook_auto_sync_service.py +403 -0
- roadmap/core/services/github/__init__.py +1 -0
- roadmap/core/services/github/github_change_detector.py +162 -0
- roadmap/core/services/github/github_config_validator.py +148 -0
- roadmap/core/services/github/github_conflict_detector.py +179 -0
- roadmap/core/services/github/github_entity_classifier.py +51 -0
- roadmap/core/services/github/github_integration_service.py +439 -0
- roadmap/core/services/github/github_issue_client.py +346 -0
- roadmap/core/services/health/__init__.py +1 -0
- roadmap/core/services/health/backup_cleanup_service.py +202 -0
- roadmap/core/services/health/data_integrity_validator_service.py +90 -0
- roadmap/core/services/health/entity_health_scanner.py +460 -0
- roadmap/core/services/health/file_repair_service.py +193 -0
- roadmap/core/services/health/health_check_service.py +166 -0
- roadmap/core/services/health/health_models.py +80 -0
- roadmap/core/services/health/infrastructure_validator_service.py +328 -0
- roadmap/core/services/health/issue_health_scanner.py +263 -0
- roadmap/core/services/helpers/__init__.py +18 -0
- roadmap/core/services/helpers/status_change_helpers.py +88 -0
- roadmap/core/services/initialization/__init__.py +19 -0
- roadmap/core/services/initialization/utils.py +104 -0
- roadmap/core/services/initialization/validator.py +78 -0
- roadmap/core/services/initialization/workflow.py +182 -0
- roadmap/core/services/initialization_service.py +111 -0
- roadmap/core/services/issue/__init__.py +1 -0
- roadmap/core/services/issue/assignee_validation_service.py +312 -0
- roadmap/core/services/issue/issue_creation_service.py +309 -0
- roadmap/core/services/issue/issue_filter_service.py +302 -0
- roadmap/core/services/issue/issue_matching_service.py +127 -0
- roadmap/core/services/issue/issue_service.py +765 -0
- roadmap/core/services/issue/issue_update_service.py +144 -0
- roadmap/core/services/issue/start_issue_service.py +174 -0
- roadmap/core/services/issue_helpers/__init__.py +18 -0
- roadmap/core/services/issue_helpers/issue_filters.py +296 -0
- roadmap/core/services/milestone_service.py +438 -0
- roadmap/core/services/project/__init__.py +1 -0
- roadmap/core/services/project/project_service.py +434 -0
- roadmap/core/services/project/project_status_service.py +125 -0
- roadmap/core/services/project_init/__init__.py +22 -0
- roadmap/core/services/project_init/context_detection.py +146 -0
- roadmap/core/services/project_init/creation.py +76 -0
- roadmap/core/services/project_init/detection.py +51 -0
- roadmap/core/services/project_init/template.py +228 -0
- roadmap/core/services/status_change_service.py +88 -0
- roadmap/core/services/sync/__init__.py +1 -0
- roadmap/core/services/sync/batch_processor.py +290 -0
- roadmap/core/services/sync/conflict_resolver.py +154 -0
- roadmap/core/services/sync/dependency_resolver.py +367 -0
- roadmap/core/services/sync/duplicate_detector.py +771 -0
- roadmap/core/services/sync/duplicate_resolver.py +315 -0
- roadmap/core/services/sync/error_classification.py +470 -0
- roadmap/core/services/sync/performance.py +263 -0
- roadmap/core/services/sync/sync_change_computer.py +211 -0
- roadmap/core/services/sync/sync_checkpoint.py +348 -0
- roadmap/core/services/sync/sync_conflict_detector.py +147 -0
- roadmap/core/services/sync/sync_conflict_resolver.py +448 -0
- roadmap/core/services/sync/sync_errors.py +383 -0
- roadmap/core/services/sync/sync_key_normalizer.py +222 -0
- roadmap/core/services/sync/sync_metadata_service.py +361 -0
- roadmap/core/services/sync/sync_plan.py +171 -0
- roadmap/core/services/sync/sync_plan_executor.py +447 -0
- roadmap/core/services/sync/sync_report.py +296 -0
- roadmap/core/services/sync/sync_state.py +173 -0
- roadmap/core/services/sync/sync_state_comparator.py +690 -0
- roadmap/core/services/sync/sync_state_manager.py +431 -0
- roadmap/core/services/sync/sync_state_normalizer.py +98 -0
- roadmap/core/services/sync/sync_three_way.py +78 -0
- roadmap/core/services/sync/three_way_merger.py +178 -0
- roadmap/core/services/utils/__init__.py +1 -0
- roadmap/core/services/utils/configuration_service.py +113 -0
- roadmap/core/services/utils/critical_path_calculator.py +418 -0
- roadmap/core/services/utils/dependency_analyzer.py +360 -0
- roadmap/core/services/utils/field_conflict_detector.py +126 -0
- roadmap/core/services/utils/remote_fetcher.py +202 -0
- roadmap/core/services/utils/remote_state_normalizer.py +110 -0
- roadmap/core/services/utils/retry_policy.py +51 -0
- roadmap/core/services/validator_base.py +130 -0
- roadmap/core/services/validators/__init__.py +28 -0
- roadmap/core/services/validators/_utils.py +20 -0
- roadmap/core/services/validators/archivable_issues_validator.py +71 -0
- roadmap/core/services/validators/archivable_milestones_validator.py +69 -0
- roadmap/core/services/validators/backup_validator.py +89 -0
- roadmap/core/services/validators/data_integrity_validator.py +84 -0
- roadmap/core/services/validators/duplicate_issues_validator.py +75 -0
- roadmap/core/services/validators/duplicate_milestones_validator.py +141 -0
- roadmap/core/services/validators/folder_structure_validator.py +165 -0
- roadmap/core/services/validators/health_status_utils.py +28 -0
- roadmap/core/services/validators/milestone_naming_validator.py +146 -0
- roadmap/core/services/validators/missing_headlines_validator.py +97 -0
- roadmap/core/services/validators/orphaned_issues_validator.py +142 -0
- roadmap/core/services/validators/orphaned_milestones_validator.py +85 -0
- roadmap/core/utils/git_remote_parser.py +83 -0
- roadmap/core/utils/github_urls.py +66 -0
- roadmap/domain/validation.py +30 -0
- roadmap/infrastructure/__init__.py +55 -0
- roadmap/infrastructure/coordination/__init__.py +8 -0
- roadmap/infrastructure/coordination/coordinator_params.py +82 -0
- roadmap/infrastructure/coordination/core.py +446 -0
- roadmap/infrastructure/coordination/git_coordinator.py +160 -0
- roadmap/infrastructure/coordination/initialization.py +348 -0
- roadmap/infrastructure/coordination/issue_coordinator.py +154 -0
- roadmap/infrastructure/coordination/issue_operations.py +456 -0
- roadmap/infrastructure/coordination/milestone_coordinator.py +135 -0
- roadmap/infrastructure/coordination/milestone_operations.py +201 -0
- roadmap/infrastructure/coordination/project_coordinator.py +113 -0
- roadmap/infrastructure/coordination/project_operations.py +144 -0
- roadmap/infrastructure/coordination/team_coordinator.py +107 -0
- roadmap/infrastructure/coordination/user_operations.py +150 -0
- roadmap/infrastructure/coordination/validation_coordinator.py +249 -0
- roadmap/infrastructure/coordination_gateway.py +152 -0
- roadmap/infrastructure/git/__init__.py +6 -0
- roadmap/infrastructure/git/git_integration_ops.py +234 -0
- roadmap/infrastructure/git_gateway.py +35 -0
- roadmap/infrastructure/github_gateway.py +52 -0
- roadmap/infrastructure/maintenance/__init__.py +5 -0
- roadmap/infrastructure/maintenance/cleanup.py +483 -0
- roadmap/infrastructure/observability/__init__.py +7 -0
- roadmap/infrastructure/observability/health.py +196 -0
- roadmap/infrastructure/observability/health_formatter.py +73 -0
- roadmap/infrastructure/observability/specialized_health_checkers.py +188 -0
- roadmap/infrastructure/persistence_gateway.py +164 -0
- roadmap/infrastructure/security/__init__.py +1 -0
- roadmap/infrastructure/security/credentials.py +489 -0
- roadmap/infrastructure/sync_gateway.py +94 -0
- roadmap/infrastructure/validation/__init__.py +7 -0
- roadmap/infrastructure/validation/file_enumeration.py +219 -0
- roadmap/infrastructure/validation/github_assignee_validator.py +47 -0
- roadmap/infrastructure/validation/github_validator.py +101 -0
- roadmap/infrastructure/validation/milestone_consistency_validator.py +96 -0
- roadmap/infrastructure/validation/vanilla_assignee_validator.py +37 -0
- roadmap/infrastructure/validation_gateway.py +96 -0
- roadmap/presentation/formatters/sync_metrics_formatter.py +355 -0
- roadmap/settings.py +345 -0
- roadmap/templates/github_workflows/ci-cd-roadmap.yml +241 -0
- roadmap/templates/github_workflows/issue-lifecycle.yml +236 -0
- roadmap/templates/github_workflows/roadmap-integration.yml +137 -0
- roadmap/templates/github_workflows/roadmap-starter.yml +59 -0
- roadmap/version.py +381 -0
- roadmap_cli-0.1.1.dist-info/METADATA +448 -0
- roadmap_cli-0.1.1.dist-info/RECORD +521 -0
- roadmap_cli-0.1.1.dist-info/WHEEL +4 -0
- roadmap_cli-0.1.1.dist-info/entry_points.txt +2 -0
- roadmap_cli-0.1.1.dist-info/licenses/LICENSE.md +21 -0
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""High-level workflow automation for Git integration."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from datetime import UTC, datetime
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from structlog import get_logger
|
|
9
|
+
|
|
10
|
+
from roadmap.adapters.persistence.parser import IssueParser
|
|
11
|
+
from roadmap.core.domain import Issue, Status
|
|
12
|
+
from roadmap.infrastructure.coordination.core import RoadmapCore
|
|
13
|
+
|
|
14
|
+
from .git import GitCommit, GitIntegration
|
|
15
|
+
from .git_hooks_manager import GitHookManager
|
|
16
|
+
|
|
17
|
+
logger = get_logger()
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class WorkflowAutomation:
|
|
21
|
+
"""High-level workflow automation orchestrator."""
|
|
22
|
+
|
|
23
|
+
def __init__(self, roadmap_core: RoadmapCore):
|
|
24
|
+
"""Initialize WorkflowAutomation.
|
|
25
|
+
|
|
26
|
+
Args:
|
|
27
|
+
roadmap_core: Core roadmap integration instance.
|
|
28
|
+
"""
|
|
29
|
+
self.core = roadmap_core
|
|
30
|
+
self.hook_manager = GitHookManager(roadmap_core)
|
|
31
|
+
self.git_integration = GitIntegration()
|
|
32
|
+
|
|
33
|
+
def setup_automation(self, features: list[str] | None = None) -> dict[str, bool]:
|
|
34
|
+
"""Set up automated workflow features.
|
|
35
|
+
|
|
36
|
+
Args:
|
|
37
|
+
features: List of features to enable. Options:
|
|
38
|
+
['git-hooks', 'status-automation', 'progress-tracking']
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
Dict mapping feature names to success status.
|
|
42
|
+
"""
|
|
43
|
+
all_features = ["git-hooks", "status-automation", "progress-tracking"]
|
|
44
|
+
features_to_enable = features or all_features
|
|
45
|
+
|
|
46
|
+
results = {}
|
|
47
|
+
|
|
48
|
+
if "git-hooks" in features_to_enable:
|
|
49
|
+
results["git-hooks"] = self.hook_manager.install_hooks()
|
|
50
|
+
|
|
51
|
+
if "status-automation" in features_to_enable:
|
|
52
|
+
results["status-automation"] = self._setup_status_automation()
|
|
53
|
+
|
|
54
|
+
if "progress-tracking" in features_to_enable:
|
|
55
|
+
results["progress-tracking"] = self._setup_progress_tracking()
|
|
56
|
+
|
|
57
|
+
return results
|
|
58
|
+
|
|
59
|
+
def disable_automation(self) -> bool:
|
|
60
|
+
"""Disable all workflow automation."""
|
|
61
|
+
try:
|
|
62
|
+
# Remove Git hooks
|
|
63
|
+
self.hook_manager.uninstall_hooks()
|
|
64
|
+
|
|
65
|
+
# Clean up context files
|
|
66
|
+
context_files = [
|
|
67
|
+
".roadmap_branch_context.json",
|
|
68
|
+
".roadmap_automation_config.json",
|
|
69
|
+
".roadmap_progress_tracking.json",
|
|
70
|
+
]
|
|
71
|
+
|
|
72
|
+
for file_path in context_files:
|
|
73
|
+
if Path(file_path).exists():
|
|
74
|
+
Path(file_path).unlink()
|
|
75
|
+
|
|
76
|
+
return True
|
|
77
|
+
|
|
78
|
+
except Exception as e:
|
|
79
|
+
logger.error(
|
|
80
|
+
"context_cleanup_failed", error=str(e), severity="system_error"
|
|
81
|
+
)
|
|
82
|
+
return False
|
|
83
|
+
|
|
84
|
+
def _setup_status_automation(self) -> bool:
|
|
85
|
+
"""Set up intelligent status automation rules."""
|
|
86
|
+
try:
|
|
87
|
+
# Create automation config
|
|
88
|
+
config = {
|
|
89
|
+
"status_rules": {
|
|
90
|
+
"auto_in_progress": True, # Move to in-progress on first commit
|
|
91
|
+
"auto_done_on_close": True, # Mark done on "closes" commit
|
|
92
|
+
"auto_blocked_detection": True, # Detect blocked status from patterns
|
|
93
|
+
},
|
|
94
|
+
"progress_rules": {
|
|
95
|
+
"commit_based_progress": True, # Update progress from commits
|
|
96
|
+
"milestone_auto_complete": True, # Auto-complete milestones
|
|
97
|
+
"velocity_tracking": True, # Track team velocity
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
config_file = Path(".roadmap_automation_config.json")
|
|
102
|
+
config_file.write_text(json.dumps(config, indent=2))
|
|
103
|
+
return True
|
|
104
|
+
|
|
105
|
+
except Exception as e:
|
|
106
|
+
logger.error(
|
|
107
|
+
"status_automation_setup_failed", error=str(e), severity="system_error"
|
|
108
|
+
)
|
|
109
|
+
return False
|
|
110
|
+
|
|
111
|
+
def _setup_progress_tracking(self) -> bool:
|
|
112
|
+
"""Set up automated progress tracking."""
|
|
113
|
+
try:
|
|
114
|
+
# Initialize progress tracking state
|
|
115
|
+
tracking_state = {
|
|
116
|
+
"enabled": True,
|
|
117
|
+
"last_sync": datetime.now(UTC).isoformat(),
|
|
118
|
+
"tracked_metrics": [
|
|
119
|
+
"commit_frequency",
|
|
120
|
+
"issue_completion_rate",
|
|
121
|
+
"milestone_progress",
|
|
122
|
+
"team_velocity",
|
|
123
|
+
],
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
tracking_file = Path(".roadmap_progress_tracking.json")
|
|
127
|
+
tracking_file.write_text(json.dumps(tracking_state, indent=2))
|
|
128
|
+
return True
|
|
129
|
+
|
|
130
|
+
except Exception as e:
|
|
131
|
+
logger.error(
|
|
132
|
+
"progress_tracking_setup_failed", error=str(e), severity="system_error"
|
|
133
|
+
)
|
|
134
|
+
return False
|
|
135
|
+
|
|
136
|
+
def sync_all_issues_with_git(self) -> dict[str, Any]:
|
|
137
|
+
"""Sync all issues with their Git activity."""
|
|
138
|
+
results = {"synced_issues": 0, "updated_issues": [], "errors": []}
|
|
139
|
+
|
|
140
|
+
try:
|
|
141
|
+
issues = self.core.issues.list()
|
|
142
|
+
commits = self.git_integration.get_recent_commits(count=100)
|
|
143
|
+
|
|
144
|
+
# Build commit lookup by issue references
|
|
145
|
+
issue_commits = {}
|
|
146
|
+
for commit in commits:
|
|
147
|
+
references = commit.extract_roadmap_references()
|
|
148
|
+
for issue_id in references:
|
|
149
|
+
if issue_id not in issue_commits:
|
|
150
|
+
issue_commits[issue_id] = []
|
|
151
|
+
issue_commits[issue_id].append(commit)
|
|
152
|
+
|
|
153
|
+
# Update issues based on their Git activity
|
|
154
|
+
for issue in issues:
|
|
155
|
+
if issue.id in issue_commits:
|
|
156
|
+
try:
|
|
157
|
+
updated = self._sync_issue_with_commits(
|
|
158
|
+
issue, issue_commits[issue.id]
|
|
159
|
+
)
|
|
160
|
+
if updated:
|
|
161
|
+
results["synced_issues"] += 1
|
|
162
|
+
results["updated_issues"].append(
|
|
163
|
+
{
|
|
164
|
+
"id": issue.id,
|
|
165
|
+
"title": issue.title,
|
|
166
|
+
"commits": len(issue_commits[issue.id]),
|
|
167
|
+
}
|
|
168
|
+
)
|
|
169
|
+
except Exception as e:
|
|
170
|
+
results["errors"].append(f"Issue {issue.id}: {str(e)}")
|
|
171
|
+
|
|
172
|
+
except Exception as e:
|
|
173
|
+
results["errors"].append(f"Sync error: {str(e)}")
|
|
174
|
+
|
|
175
|
+
return results
|
|
176
|
+
|
|
177
|
+
def _sync_issue_with_commits(self, issue: Issue, commits: list[GitCommit]) -> bool:
|
|
178
|
+
"""Sync a single issue with its Git commits."""
|
|
179
|
+
updated = False
|
|
180
|
+
|
|
181
|
+
# Sort commits by date
|
|
182
|
+
commits.sort(key=lambda c: c.date)
|
|
183
|
+
|
|
184
|
+
# Initialize git_commits if not present
|
|
185
|
+
if not hasattr(issue, "git_commits"):
|
|
186
|
+
issue.git_commits = []
|
|
187
|
+
|
|
188
|
+
# Process commits and extract progress information
|
|
189
|
+
highest_progress, is_completed = self._process_commits_for_issue(issue, commits)
|
|
190
|
+
|
|
191
|
+
# Update issue status and progress based on extracted data
|
|
192
|
+
updated = self._update_issue_status_and_progress(
|
|
193
|
+
issue, highest_progress, is_completed
|
|
194
|
+
)
|
|
195
|
+
|
|
196
|
+
if updated:
|
|
197
|
+
issue_path = self.core.issues_dir / issue.filename
|
|
198
|
+
IssueParser.save_issue_file(issue, issue_path)
|
|
199
|
+
|
|
200
|
+
return updated
|
|
201
|
+
|
|
202
|
+
def _process_commits_for_issue(
|
|
203
|
+
self, issue: Issue, commits: list[GitCommit]
|
|
204
|
+
) -> tuple[float | None, bool]:
|
|
205
|
+
"""Process commits for an issue and extract progress and completion status.
|
|
206
|
+
|
|
207
|
+
Returns:
|
|
208
|
+
Tuple of (highest_progress_percentage, is_completed_bool)
|
|
209
|
+
"""
|
|
210
|
+
highest_progress: float | None = None
|
|
211
|
+
is_completed = False
|
|
212
|
+
|
|
213
|
+
for commit in commits:
|
|
214
|
+
# Check if commit already tracked
|
|
215
|
+
if any(ref.get("hash") == commit.hash for ref in issue.git_commits):
|
|
216
|
+
continue
|
|
217
|
+
|
|
218
|
+
# Extract progress from commit
|
|
219
|
+
progress = commit.extract_progress_info()
|
|
220
|
+
if progress is not None:
|
|
221
|
+
if highest_progress is None or progress > highest_progress:
|
|
222
|
+
highest_progress = progress
|
|
223
|
+
|
|
224
|
+
# Check for completion keywords in commit message
|
|
225
|
+
if self._is_completion_commit(commit):
|
|
226
|
+
is_completed = True
|
|
227
|
+
|
|
228
|
+
# Build and add commit reference
|
|
229
|
+
self._add_commit_reference_to_issue(
|
|
230
|
+
issue, commit, progress, is_completed, commits
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
return highest_progress, is_completed
|
|
234
|
+
|
|
235
|
+
def _is_completion_commit(self, commit: GitCommit) -> bool:
|
|
236
|
+
"""Check if commit contains completion keywords."""
|
|
237
|
+
completion_keywords = ["closes roadmap:", "fixes roadmap:"]
|
|
238
|
+
return any(keyword in commit.message.lower() for keyword in completion_keywords)
|
|
239
|
+
|
|
240
|
+
def _add_commit_reference_to_issue(
|
|
241
|
+
self,
|
|
242
|
+
issue: Issue,
|
|
243
|
+
commit: GitCommit,
|
|
244
|
+
progress: float | None,
|
|
245
|
+
is_completed: bool,
|
|
246
|
+
commits: list[GitCommit],
|
|
247
|
+
):
|
|
248
|
+
"""Add a commit reference to the issue's git_commits list."""
|
|
249
|
+
commit_ref = {
|
|
250
|
+
"hash": commit.hash,
|
|
251
|
+
"message": commit.message,
|
|
252
|
+
"date": commit.date.isoformat(),
|
|
253
|
+
"progress": progress,
|
|
254
|
+
"completion": is_completed and commit == commits[-1],
|
|
255
|
+
}
|
|
256
|
+
issue.git_commits.append(commit_ref)
|
|
257
|
+
|
|
258
|
+
def _update_issue_status_and_progress(
|
|
259
|
+
self, issue: Issue, highest_progress: float | None, is_completed: bool
|
|
260
|
+
) -> bool:
|
|
261
|
+
"""Update issue status and progress based on extracted data.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
True if issue was updated, False otherwise.
|
|
265
|
+
"""
|
|
266
|
+
updated = False
|
|
267
|
+
|
|
268
|
+
# Update progress percentage
|
|
269
|
+
if highest_progress is not None:
|
|
270
|
+
issue.progress_percentage = highest_progress
|
|
271
|
+
updated = True
|
|
272
|
+
elif is_completed:
|
|
273
|
+
# If completed without explicit progress, set to 100%
|
|
274
|
+
issue.progress_percentage = 100.0
|
|
275
|
+
updated = True
|
|
276
|
+
|
|
277
|
+
# Update status based on completion and progress
|
|
278
|
+
if is_completed and issue.status != Status.CLOSED:
|
|
279
|
+
issue.status = Status.CLOSED
|
|
280
|
+
issue.completed_date = datetime.now(UTC).isoformat()
|
|
281
|
+
updated = True
|
|
282
|
+
elif highest_progress and highest_progress > 0 and issue.status == Status.TODO:
|
|
283
|
+
issue.status = Status.IN_PROGRESS
|
|
284
|
+
updated = True
|
|
285
|
+
|
|
286
|
+
return updated
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
"""GitHub API adapter and issue operations."""
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
"""GitHub API client for roadmap CLI."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from typing import Any, cast
|
|
5
|
+
|
|
6
|
+
import requests
|
|
7
|
+
from requests.adapters import HTTPAdapter
|
|
8
|
+
from structlog import get_logger
|
|
9
|
+
from urllib3.util.retry import Retry
|
|
10
|
+
|
|
11
|
+
from roadmap.adapters.github.handlers.base import (
|
|
12
|
+
BaseGitHubHandler,
|
|
13
|
+
GitHubAPIError,
|
|
14
|
+
)
|
|
15
|
+
from roadmap.adapters.github.handlers.labels import LabelHandler
|
|
16
|
+
from roadmap.infrastructure.security.credentials import get_credential_manager
|
|
17
|
+
|
|
18
|
+
logger = get_logger()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class GitHubClient(BaseGitHubHandler):
|
|
22
|
+
"""GitHub API client for managing issues and milestones."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
token: str | None = None,
|
|
27
|
+
owner: str | None = None,
|
|
28
|
+
repo: str | None = None,
|
|
29
|
+
):
|
|
30
|
+
"""Initialize GitHub client.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
token: GitHub personal access token
|
|
34
|
+
owner: Repository owner (username or organization)
|
|
35
|
+
repo: Repository name
|
|
36
|
+
"""
|
|
37
|
+
# Try to get token from multiple sources with priority:
|
|
38
|
+
# 1. Explicit parameter
|
|
39
|
+
# 2. Environment variable
|
|
40
|
+
# 3. Credential manager
|
|
41
|
+
self.token = token or self._get_token_secure()
|
|
42
|
+
self.owner = owner
|
|
43
|
+
self.repo = repo
|
|
44
|
+
|
|
45
|
+
if not self.token:
|
|
46
|
+
raise GitHubAPIError(
|
|
47
|
+
"GitHub token is required. Set GITHUB_TOKEN environment variable, use credential manager, or provide token."
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
# Set up session with retry strategy
|
|
51
|
+
self.session = requests.Session()
|
|
52
|
+
retry_strategy = Retry(
|
|
53
|
+
total=3,
|
|
54
|
+
status_forcelist=[429, 500, 502, 503, 504],
|
|
55
|
+
allowed_methods=[
|
|
56
|
+
"HEAD",
|
|
57
|
+
"GET",
|
|
58
|
+
"POST",
|
|
59
|
+
"PUT",
|
|
60
|
+
"DELETE",
|
|
61
|
+
"OPTIONS",
|
|
62
|
+
"TRACE",
|
|
63
|
+
],
|
|
64
|
+
)
|
|
65
|
+
adapter = HTTPAdapter(max_retries=cast(int, retry_strategy))
|
|
66
|
+
self.session.mount("http://", adapter)
|
|
67
|
+
self.session.mount("https://", adapter)
|
|
68
|
+
|
|
69
|
+
# Set default headers
|
|
70
|
+
self.session.headers.update(
|
|
71
|
+
{
|
|
72
|
+
"Authorization": f"token {self.token}",
|
|
73
|
+
"Accept": "application/vnd.github.v3+json",
|
|
74
|
+
"User-Agent": "roadmap-cli/1.0",
|
|
75
|
+
}
|
|
76
|
+
)
|
|
77
|
+
|
|
78
|
+
# Initialize label handler
|
|
79
|
+
self._label_handler = LabelHandler(self.session, owner, repo)
|
|
80
|
+
|
|
81
|
+
# Initialize issue handler
|
|
82
|
+
from roadmap.adapters.github.handlers.issues import IssueHandler
|
|
83
|
+
|
|
84
|
+
self._issue_handler = IssueHandler(self.session, owner, repo)
|
|
85
|
+
|
|
86
|
+
def _get_token_secure(self) -> str | None:
|
|
87
|
+
"""Get token from secure sources (environment variable or credential manager)."""
|
|
88
|
+
# First try environment variable
|
|
89
|
+
env_token = os.getenv("GITHUB_TOKEN")
|
|
90
|
+
if env_token:
|
|
91
|
+
return env_token
|
|
92
|
+
|
|
93
|
+
# Then try credential manager
|
|
94
|
+
try:
|
|
95
|
+
credential_manager = get_credential_manager()
|
|
96
|
+
return credential_manager.get_token()
|
|
97
|
+
except Exception as e:
|
|
98
|
+
logger.debug("credential_manager_failed", error=str(e))
|
|
99
|
+
return None
|
|
100
|
+
|
|
101
|
+
def set_repository(self, owner: str, repo: str) -> None:
|
|
102
|
+
"""Set the target repository."""
|
|
103
|
+
self.owner = owner
|
|
104
|
+
self.repo = repo
|
|
105
|
+
|
|
106
|
+
def test_authentication(self) -> dict[str, Any]:
|
|
107
|
+
"""Test authentication and get user info."""
|
|
108
|
+
response = self._make_request("GET", "/user")
|
|
109
|
+
return response.json()
|
|
110
|
+
|
|
111
|
+
def test_repository_access(self) -> dict[str, Any]:
|
|
112
|
+
"""Test repository access."""
|
|
113
|
+
self._check_repository()
|
|
114
|
+
response = self._make_request("GET", f"/repos/{self.owner}/{self.repo}")
|
|
115
|
+
return response.json()
|
|
116
|
+
|
|
117
|
+
def status_to_labels(self, status: str) -> list[str]:
|
|
118
|
+
"""Convert issue status to GitHub labels.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
status: The issue status (e.g., 'todo', 'in-progress', 'blocked', 'review', 'closed')
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
List of GitHub labels corresponding to the status
|
|
125
|
+
"""
|
|
126
|
+
status_label_map = {
|
|
127
|
+
"todo": ["status:todo"],
|
|
128
|
+
"in-progress": ["status:in-progress"],
|
|
129
|
+
"blocked": ["status:blocked"],
|
|
130
|
+
"review": ["status:review"],
|
|
131
|
+
"closed": ["status:closed"],
|
|
132
|
+
}
|
|
133
|
+
return status_label_map.get(status, [])
|
|
134
|
+
|
|
135
|
+
def labels_to_status(self, labels: list[str]) -> str | None:
|
|
136
|
+
"""Convert GitHub labels to issue status.
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
labels: List of GitHub labels
|
|
140
|
+
|
|
141
|
+
Returns:
|
|
142
|
+
The corresponding issue status, or None if no status label found
|
|
143
|
+
"""
|
|
144
|
+
label_status_map = {
|
|
145
|
+
"status:todo": "todo",
|
|
146
|
+
"status:in-progress": "in-progress",
|
|
147
|
+
"status:blocked": "blocked",
|
|
148
|
+
"status:review": "review",
|
|
149
|
+
"status:closed": "closed",
|
|
150
|
+
}
|
|
151
|
+
for label in labels:
|
|
152
|
+
if label in label_status_map:
|
|
153
|
+
return label_status_map[label]
|
|
154
|
+
return None
|
|
155
|
+
|
|
156
|
+
def get_labels(self) -> list[dict[str, Any]]:
|
|
157
|
+
"""Get repository labels.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
List of label dictionaries
|
|
161
|
+
"""
|
|
162
|
+
self._label_handler.owner = self.owner
|
|
163
|
+
self._label_handler.repo = self.repo
|
|
164
|
+
return self._label_handler.get_labels()
|
|
165
|
+
|
|
166
|
+
def create_label(
|
|
167
|
+
self, name: str, color: str, description: str | None = None
|
|
168
|
+
) -> dict[str, Any]:
|
|
169
|
+
"""Create a new label.
|
|
170
|
+
|
|
171
|
+
Args:
|
|
172
|
+
name: Label name
|
|
173
|
+
color: Label color (hex code)
|
|
174
|
+
description: Label description
|
|
175
|
+
|
|
176
|
+
Returns:
|
|
177
|
+
Created label dictionary
|
|
178
|
+
"""
|
|
179
|
+
self._label_handler.owner = self.owner
|
|
180
|
+
self._label_handler.repo = self.repo
|
|
181
|
+
return self._label_handler.create_label(name, color, description)
|
|
182
|
+
|
|
183
|
+
def setup_default_labels(self) -> None:
|
|
184
|
+
"""Set up default labels for priority and status."""
|
|
185
|
+
self._label_handler.owner = self.owner
|
|
186
|
+
self._label_handler.repo = self.repo
|
|
187
|
+
self._label_handler.setup_default_labels()
|
|
188
|
+
|
|
189
|
+
def get_issues(
|
|
190
|
+
self,
|
|
191
|
+
state: str = "all",
|
|
192
|
+
labels: list[str] | None = None,
|
|
193
|
+
milestone: str | None = None,
|
|
194
|
+
assignee: str | None = None,
|
|
195
|
+
per_page: int = 100,
|
|
196
|
+
) -> list[dict[str, Any]]:
|
|
197
|
+
"""Get issues from the repository.
|
|
198
|
+
|
|
199
|
+
Args:
|
|
200
|
+
state: Issue state ('open', 'closed', 'all')
|
|
201
|
+
labels: Optional list of label names to filter by
|
|
202
|
+
milestone: Optional milestone title to filter by
|
|
203
|
+
assignee: Optional assignee username to filter by
|
|
204
|
+
per_page: Number of issues per page (max 100)
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
List of issue dictionaries
|
|
208
|
+
"""
|
|
209
|
+
self._issue_handler.owner = self.owner
|
|
210
|
+
self._issue_handler.repo = self.repo
|
|
211
|
+
return self._issue_handler.get_issues(
|
|
212
|
+
state=state,
|
|
213
|
+
labels=labels,
|
|
214
|
+
milestone=milestone,
|
|
215
|
+
assignee=assignee,
|
|
216
|
+
per_page=per_page,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def fetch_issue(self, issue_number: int) -> dict[str, Any]:
|
|
220
|
+
"""Fetch a GitHub issue by number.
|
|
221
|
+
|
|
222
|
+
Args:
|
|
223
|
+
issue_number: GitHub issue number (must be positive integer)
|
|
224
|
+
|
|
225
|
+
Returns:
|
|
226
|
+
Dictionary with issue details: number, title, body, state, labels, assignees
|
|
227
|
+
|
|
228
|
+
Raises:
|
|
229
|
+
GitHubAPIError: If issue not found or API error occurs
|
|
230
|
+
ValueError: If issue_number is invalid
|
|
231
|
+
"""
|
|
232
|
+
if not isinstance(issue_number, int) or issue_number <= 0:
|
|
233
|
+
raise ValueError(
|
|
234
|
+
f"Issue number must be a positive integer, got {issue_number}"
|
|
235
|
+
)
|
|
236
|
+
|
|
237
|
+
self._check_repository()
|
|
238
|
+
try:
|
|
239
|
+
response = self._make_request(
|
|
240
|
+
"GET", f"/repos/{self.owner}/{self.repo}/issues/{issue_number}"
|
|
241
|
+
)
|
|
242
|
+
data = response.json()
|
|
243
|
+
|
|
244
|
+
# Extract relevant fields
|
|
245
|
+
return {
|
|
246
|
+
"number": data.get("number"),
|
|
247
|
+
"title": data.get("title"),
|
|
248
|
+
"body": data.get("body") or "",
|
|
249
|
+
"state": data.get("state"), # 'open' or 'closed'
|
|
250
|
+
"labels": [label["name"] for label in data.get("labels", [])],
|
|
251
|
+
"assignees": [
|
|
252
|
+
assignee["login"] for assignee in data.get("assignees", [])
|
|
253
|
+
],
|
|
254
|
+
"milestone": data.get("milestone", {}).get("title")
|
|
255
|
+
if data.get("milestone")
|
|
256
|
+
else None,
|
|
257
|
+
"url": data.get("html_url"),
|
|
258
|
+
"created_at": data.get("created_at"),
|
|
259
|
+
"updated_at": data.get("updated_at"),
|
|
260
|
+
}
|
|
261
|
+
except requests.exceptions.HTTPError as e:
|
|
262
|
+
resp = e.response
|
|
263
|
+
if resp is None:
|
|
264
|
+
raise GitHubAPIError(f"GitHub API error: {e}") from e
|
|
265
|
+
if resp.status_code == 404:
|
|
266
|
+
raise GitHubAPIError(
|
|
267
|
+
f"GitHub issue #{issue_number} not found in {self.owner}/{self.repo}"
|
|
268
|
+
) from e
|
|
269
|
+
elif resp.status_code == 401:
|
|
270
|
+
raise GitHubAPIError(
|
|
271
|
+
"GitHub authentication failed - invalid token"
|
|
272
|
+
) from e
|
|
273
|
+
else:
|
|
274
|
+
raise GitHubAPIError(
|
|
275
|
+
f"GitHub API error: {resp.status_code} {resp.reason}"
|
|
276
|
+
) from e
|
|
277
|
+
except requests.exceptions.RequestException as e:
|
|
278
|
+
raise GitHubAPIError(f"GitHub API connection error: {str(e)}") from e
|
|
279
|
+
|
|
280
|
+
def validate_github_token(self) -> tuple[bool, str]:
|
|
281
|
+
"""Validate GitHub token by making a test API call.
|
|
282
|
+
|
|
283
|
+
Returns:
|
|
284
|
+
Tuple of (is_valid, message)
|
|
285
|
+
- (True, "Token is valid") if successful
|
|
286
|
+
- (False, error_message) if invalid or error
|
|
287
|
+
"""
|
|
288
|
+
try:
|
|
289
|
+
response = self._make_request("GET", "/user")
|
|
290
|
+
if response.status_code == 200:
|
|
291
|
+
user_data = response.json()
|
|
292
|
+
login = user_data.get("login", "unknown")
|
|
293
|
+
return True, f"Token is valid (authenticated as {login})"
|
|
294
|
+
else:
|
|
295
|
+
return False, f"Token validation failed: {response.status_code}"
|
|
296
|
+
except requests.exceptions.HTTPError as e:
|
|
297
|
+
resp = e.response
|
|
298
|
+
if resp is None:
|
|
299
|
+
return False, f"GitHub API error: {e}"
|
|
300
|
+
if resp.status_code == 401:
|
|
301
|
+
return False, "GitHub token is invalid or expired"
|
|
302
|
+
else:
|
|
303
|
+
return False, f"GitHub API error: {resp.status_code}"
|
|
304
|
+
except requests.exceptions.RequestException as e:
|
|
305
|
+
return False, f"GitHub API connection error: {str(e)}"
|
|
306
|
+
|
|
307
|
+
def validate_assignee(self, assignee: str) -> tuple[bool, str]:
|
|
308
|
+
"""Validate that a user is a valid assignee for this repository.
|
|
309
|
+
|
|
310
|
+
Args:
|
|
311
|
+
assignee: GitHub username to validate
|
|
312
|
+
|
|
313
|
+
Returns:
|
|
314
|
+
Tuple of (is_valid, error_message)
|
|
315
|
+
- (True, "") if valid
|
|
316
|
+
- (False, error_message) if invalid
|
|
317
|
+
"""
|
|
318
|
+
if not assignee or not assignee.strip():
|
|
319
|
+
return False, "Assignee cannot be empty"
|
|
320
|
+
|
|
321
|
+
assignee = assignee.strip()
|
|
322
|
+
|
|
323
|
+
try:
|
|
324
|
+
self._check_repository()
|
|
325
|
+
# Check if the user exists and has access to the repository
|
|
326
|
+
# We'll try to get the user info from GitHub
|
|
327
|
+
response = self._make_request("GET", f"/users/{assignee}")
|
|
328
|
+
|
|
329
|
+
if response.status_code == 200:
|
|
330
|
+
# User exists, now check if they have access to the repository
|
|
331
|
+
# Try to get their permission level in the repository
|
|
332
|
+
perm_response = self._make_request(
|
|
333
|
+
"GET",
|
|
334
|
+
f"/repos/{self.owner}/{self.repo}/collaborators/{assignee}/permission",
|
|
335
|
+
)
|
|
336
|
+
|
|
337
|
+
if perm_response.status_code == 204:
|
|
338
|
+
# User has some level of access to the repository
|
|
339
|
+
return True, ""
|
|
340
|
+
elif perm_response.status_code == 404:
|
|
341
|
+
# User exists but is not a collaborator - they can still be assigned
|
|
342
|
+
# to public repositories by maintainers, so return True
|
|
343
|
+
return True, ""
|
|
344
|
+
else:
|
|
345
|
+
return False, f"Could not verify access for {assignee}"
|
|
346
|
+
elif response.status_code == 404:
|
|
347
|
+
return False, f"GitHub user '{assignee}' not found"
|
|
348
|
+
else:
|
|
349
|
+
return False, f"GitHub API error: {response.status_code}"
|
|
350
|
+
|
|
351
|
+
except requests.exceptions.HTTPError as e:
|
|
352
|
+
resp = e.response
|
|
353
|
+
if resp is None:
|
|
354
|
+
return False, f"GitHub API error: {e}"
|
|
355
|
+
if resp.status_code == 404:
|
|
356
|
+
return False, f"GitHub user '{assignee}' not found"
|
|
357
|
+
else:
|
|
358
|
+
return False, f"GitHub API error: {resp.status_code}"
|
|
359
|
+
except requests.exceptions.RequestException as e:
|
|
360
|
+
return False, f"GitHub API connection error: {str(e)}"
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"""GitHub API handlers for different resource types."""
|
|
2
|
+
|
|
3
|
+
from roadmap.adapters.github.handlers.base import BaseGitHubHandler
|
|
4
|
+
from roadmap.adapters.github.handlers.collaborators import CollaboratorsHandler
|
|
5
|
+
from roadmap.adapters.github.handlers.comments import CommentsHandler
|
|
6
|
+
from roadmap.adapters.github.handlers.issues import IssueHandler
|
|
7
|
+
from roadmap.adapters.github.handlers.labels import LabelHandler
|
|
8
|
+
from roadmap.adapters.github.handlers.milestones import MilestoneHandler
|
|
9
|
+
|
|
10
|
+
__all__ = [
|
|
11
|
+
"BaseGitHubHandler",
|
|
12
|
+
"IssueHandler",
|
|
13
|
+
"MilestoneHandler",
|
|
14
|
+
"LabelHandler",
|
|
15
|
+
"CommentsHandler",
|
|
16
|
+
"CollaboratorsHandler",
|
|
17
|
+
]
|