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,908 @@
|
|
|
1
|
+
"""GitHub synchronization operations with parallel execution."""
|
|
2
|
+
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
from concurrent.futures import ThreadPoolExecutor, as_completed
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from structlog import get_logger
|
|
9
|
+
|
|
10
|
+
from roadmap.adapters.sync.backends.services.github_issue_dependency_service import (
|
|
11
|
+
GitHubIssueDependencyService,
|
|
12
|
+
)
|
|
13
|
+
from roadmap.adapters.sync.backends.services.github_label_sync_service import (
|
|
14
|
+
GitHubLabelSyncService,
|
|
15
|
+
)
|
|
16
|
+
from roadmap.adapters.sync.backends.services.github_local_issue_sync_service import (
|
|
17
|
+
GitHubLocalIssueSyncService,
|
|
18
|
+
)
|
|
19
|
+
from roadmap.common.logging import log_error_with_context
|
|
20
|
+
from roadmap.common.services.retry import API_RETRY
|
|
21
|
+
from roadmap.common.utils.timezone_utils import now_utc
|
|
22
|
+
from roadmap.core.interfaces import SyncReport
|
|
23
|
+
|
|
24
|
+
logger = get_logger()
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class GitHubSyncOps:
|
|
28
|
+
"""Perform synchronization operations with GitHub backend."""
|
|
29
|
+
|
|
30
|
+
def __init__(self, backend: Any):
|
|
31
|
+
"""Initialize GitHubSyncOps.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
backend: GitHub backend instance.
|
|
35
|
+
"""
|
|
36
|
+
self.backend = backend
|
|
37
|
+
self._create_lock = threading.Lock()
|
|
38
|
+
self._last_create_time = 0.0
|
|
39
|
+
self._create_min_interval_seconds = self._get_create_min_interval_seconds()
|
|
40
|
+
self._label_service = GitHubLabelSyncService(backend)
|
|
41
|
+
self._dependency_service = GitHubIssueDependencyService()
|
|
42
|
+
self._local_issue_service = GitHubLocalIssueSyncService(backend)
|
|
43
|
+
|
|
44
|
+
@property
|
|
45
|
+
def _label_cache(self) -> set[str] | None:
|
|
46
|
+
return self._label_service.label_cache
|
|
47
|
+
|
|
48
|
+
@_label_cache.setter
|
|
49
|
+
def _label_cache(self, value: set[str] | None) -> None:
|
|
50
|
+
self._label_service.label_cache = value
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def _label_support(self) -> bool | None:
|
|
54
|
+
return self._label_service.label_support
|
|
55
|
+
|
|
56
|
+
@_label_support.setter
|
|
57
|
+
def _label_support(self, value: bool | None) -> None:
|
|
58
|
+
self._label_service.label_support = value
|
|
59
|
+
|
|
60
|
+
def _get_create_min_interval_seconds(self) -> float:
|
|
61
|
+
"""Get minimum interval between issue creations to avoid secondary limits."""
|
|
62
|
+
config = getattr(self.backend, "config", {}) or {}
|
|
63
|
+
sync_settings = (
|
|
64
|
+
config.get("sync_settings", {}) if isinstance(config, dict) else {}
|
|
65
|
+
)
|
|
66
|
+
interval = sync_settings.get("create_min_interval_seconds")
|
|
67
|
+
try:
|
|
68
|
+
if interval is not None:
|
|
69
|
+
return max(0.0, float(interval))
|
|
70
|
+
except (TypeError, ValueError):
|
|
71
|
+
logger.debug(
|
|
72
|
+
"github_create_interval_invalid",
|
|
73
|
+
interval=interval,
|
|
74
|
+
severity="data_error",
|
|
75
|
+
)
|
|
76
|
+
return 1.0
|
|
77
|
+
|
|
78
|
+
def _sync_labels_enabled(self) -> bool:
|
|
79
|
+
return self._label_service.sync_labels_enabled()
|
|
80
|
+
|
|
81
|
+
def _get_label_color(self, name: str) -> str:
|
|
82
|
+
return self._label_service.get_label_color(name)
|
|
83
|
+
|
|
84
|
+
def _get_label_client(self):
|
|
85
|
+
"""Return the label API client, or None if labels are not supported."""
|
|
86
|
+
return self._label_service.get_label_client()
|
|
87
|
+
|
|
88
|
+
def _init_label_cache(self, client) -> bool:
|
|
89
|
+
"""Populate self._label_cache from GitHub. Returns False on failure."""
|
|
90
|
+
return self._label_service.init_label_cache(client)
|
|
91
|
+
|
|
92
|
+
def _ensure_labels_exist(self, labels: list[str]) -> None:
|
|
93
|
+
self._label_service.ensure_labels_exist(labels)
|
|
94
|
+
|
|
95
|
+
def _throttle_issue_creation(self) -> None:
|
|
96
|
+
"""Throttle issue creation to reduce secondary rate limit errors."""
|
|
97
|
+
if self._create_min_interval_seconds <= 0:
|
|
98
|
+
return
|
|
99
|
+
|
|
100
|
+
with self._create_lock:
|
|
101
|
+
now = time.monotonic()
|
|
102
|
+
elapsed = now - self._last_create_time
|
|
103
|
+
delay = self._create_min_interval_seconds - elapsed
|
|
104
|
+
if delay > 0:
|
|
105
|
+
logger.debug(
|
|
106
|
+
"github_issue_create_throttle",
|
|
107
|
+
delay_seconds=round(delay, 3),
|
|
108
|
+
)
|
|
109
|
+
time.sleep(delay)
|
|
110
|
+
self._last_create_time = time.monotonic()
|
|
111
|
+
|
|
112
|
+
def _persist_issue_before_linking(self, issue: Any, issue_id: str) -> bool:
|
|
113
|
+
"""Persist issue to database before linking to GitHub.
|
|
114
|
+
|
|
115
|
+
Args:
|
|
116
|
+
issue: The Issue domain object
|
|
117
|
+
issue_id: The ID to use for persistence
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
True if persist succeeded or was skipped, False on hard error
|
|
121
|
+
"""
|
|
122
|
+
return self._local_issue_service.persist_issue_before_linking(issue, issue_id)
|
|
123
|
+
|
|
124
|
+
def _link_issue_to_github(
|
|
125
|
+
self, issue_uuid: str, github_number: int
|
|
126
|
+
) -> tuple[bool, str | None]:
|
|
127
|
+
"""Link issue to GitHub in database.
|
|
128
|
+
|
|
129
|
+
Args:
|
|
130
|
+
issue_uuid: The local issue UUID
|
|
131
|
+
github_number: The GitHub issue number
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
Tuple of (success: bool, error_message: str | None)
|
|
135
|
+
"""
|
|
136
|
+
return self._local_issue_service.link_issue_to_github(issue_uuid, github_number)
|
|
137
|
+
|
|
138
|
+
def _handle_push_error(self, issue: Any, error_msg: str) -> tuple[bool, str]:
|
|
139
|
+
"""Handle and categorize push errors.
|
|
140
|
+
|
|
141
|
+
Args:
|
|
142
|
+
issue: The Issue domain object
|
|
143
|
+
error_msg: The error message
|
|
144
|
+
|
|
145
|
+
Returns:
|
|
146
|
+
Tuple of (success: bool, categorized_error_message: str)
|
|
147
|
+
"""
|
|
148
|
+
if "Access forbidden" in error_msg or "403" in error_msg:
|
|
149
|
+
logger.debug(
|
|
150
|
+
"github_push_issue_skipped_due_to_permissions",
|
|
151
|
+
issue_id=issue.id,
|
|
152
|
+
error=error_msg,
|
|
153
|
+
severity="config",
|
|
154
|
+
)
|
|
155
|
+
return False, f"Permission denied (check token scope): {error_msg}"
|
|
156
|
+
elif "Gone" in error_msg or "410" in error_msg:
|
|
157
|
+
logger.info(
|
|
158
|
+
"github_push_issue_skipped_resource_deleted",
|
|
159
|
+
issue_id=issue.id,
|
|
160
|
+
error=error_msg,
|
|
161
|
+
severity="operational",
|
|
162
|
+
)
|
|
163
|
+
return False, f"Remote issue deleted: {error_msg}"
|
|
164
|
+
elif "not found" in error_msg.lower() or "404" in error_msg:
|
|
165
|
+
logger.warning(
|
|
166
|
+
"github_push_issue_failed_not_found",
|
|
167
|
+
issue_id=issue.id,
|
|
168
|
+
error=error_msg,
|
|
169
|
+
severity="operational",
|
|
170
|
+
)
|
|
171
|
+
return False, error_msg
|
|
172
|
+
elif "Rate limit exceeded" in error_msg or "429" in error_msg:
|
|
173
|
+
logger.warning(
|
|
174
|
+
"github_push_issue_rate_limited",
|
|
175
|
+
issue_id=issue.id,
|
|
176
|
+
error=error_msg,
|
|
177
|
+
severity="operational",
|
|
178
|
+
)
|
|
179
|
+
return False, f"Rate limited: {error_msg}"
|
|
180
|
+
elif "Validation error" in error_msg:
|
|
181
|
+
logger.warning(
|
|
182
|
+
"github_push_issue_validation_error",
|
|
183
|
+
issue_id=issue.id,
|
|
184
|
+
error=error_msg,
|
|
185
|
+
severity="data_error",
|
|
186
|
+
)
|
|
187
|
+
return False, error_msg
|
|
188
|
+
else:
|
|
189
|
+
logger.warning(
|
|
190
|
+
"github_push_issue_failed",
|
|
191
|
+
issue_id=issue.id,
|
|
192
|
+
error=error_msg,
|
|
193
|
+
error_type=type(error_msg).__name__,
|
|
194
|
+
severity="operational",
|
|
195
|
+
)
|
|
196
|
+
return False, error_msg
|
|
197
|
+
|
|
198
|
+
def _push_single_issue(self, issue: Any) -> tuple[bool, str | None]:
|
|
199
|
+
"""Push a single issue to GitHub API with retry logic.
|
|
200
|
+
|
|
201
|
+
Args:
|
|
202
|
+
issue: The Issue domain object to push
|
|
203
|
+
|
|
204
|
+
Returns:
|
|
205
|
+
Tuple of (success: bool, error_message: str | None)
|
|
206
|
+
"""
|
|
207
|
+
|
|
208
|
+
@API_RETRY
|
|
209
|
+
def _push_with_retry():
|
|
210
|
+
"""Inner function with retry decorator for API calls."""
|
|
211
|
+
from roadmap.adapters.sync.backends.converters import (
|
|
212
|
+
IssueToGitHubPayloadConverter,
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
client = self.backend.get_api_client()
|
|
216
|
+
github_number = IssueToGitHubPayloadConverter.get_github_number(issue)
|
|
217
|
+
payload = IssueToGitHubPayloadConverter.to_payload(issue, github_number)
|
|
218
|
+
|
|
219
|
+
labels = payload.get("labels") if isinstance(payload, dict) else None
|
|
220
|
+
if isinstance(labels, list) and labels:
|
|
221
|
+
self._ensure_labels_exist(labels)
|
|
222
|
+
|
|
223
|
+
if github_number:
|
|
224
|
+
# Update existing issue
|
|
225
|
+
client.update_issue(github_number, **payload)
|
|
226
|
+
logger.info(
|
|
227
|
+
"github_issue_updated",
|
|
228
|
+
issue_id=issue.id,
|
|
229
|
+
github_number=github_number,
|
|
230
|
+
title=issue.title,
|
|
231
|
+
)
|
|
232
|
+
return True, None
|
|
233
|
+
|
|
234
|
+
# Create new issue (throttled to avoid secondary rate limits)
|
|
235
|
+
self._throttle_issue_creation()
|
|
236
|
+
result = client.create_issue(**payload)
|
|
237
|
+
github_number = result.get("number")
|
|
238
|
+
|
|
239
|
+
if github_number:
|
|
240
|
+
self._persist_issue_before_linking(issue, issue.id)
|
|
241
|
+
self._link_issue_to_github(issue.id, github_number)
|
|
242
|
+
logger.info(
|
|
243
|
+
"github_issue_created",
|
|
244
|
+
issue_id=issue.id,
|
|
245
|
+
github_number=github_number,
|
|
246
|
+
title=issue.title,
|
|
247
|
+
)
|
|
248
|
+
|
|
249
|
+
return True, None
|
|
250
|
+
|
|
251
|
+
try:
|
|
252
|
+
return _push_with_retry()
|
|
253
|
+
except Exception as e:
|
|
254
|
+
error_msg = str(e)
|
|
255
|
+
return self._handle_push_error(issue, error_msg)
|
|
256
|
+
|
|
257
|
+
def push_issues(self, local_issues: list) -> SyncReport:
|
|
258
|
+
"""Push local issues to GitHub backend with parallel execution.
|
|
259
|
+
|
|
260
|
+
Args:
|
|
261
|
+
local_issues: List of local issues to push.
|
|
262
|
+
|
|
263
|
+
Returns:
|
|
264
|
+
Sync report with push results and errors.
|
|
265
|
+
"""
|
|
266
|
+
report = SyncReport()
|
|
267
|
+
|
|
268
|
+
if not local_issues:
|
|
269
|
+
logger.info("push_issues_empty")
|
|
270
|
+
return report
|
|
271
|
+
|
|
272
|
+
logger.info("push_issues_starting", issue_count=len(local_issues))
|
|
273
|
+
|
|
274
|
+
with ThreadPoolExecutor(max_workers=5) as executor:
|
|
275
|
+
futures = {
|
|
276
|
+
executor.submit(self._push_single_issue, issue): issue
|
|
277
|
+
for issue in local_issues
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
for future in as_completed(futures):
|
|
281
|
+
issue = futures[future]
|
|
282
|
+
try:
|
|
283
|
+
success, error_msg = future.result()
|
|
284
|
+
if success:
|
|
285
|
+
report.pushed.append(issue.id)
|
|
286
|
+
logger.debug("push_issue_succeeded", issue_id=issue.id)
|
|
287
|
+
else:
|
|
288
|
+
report.errors[issue.id] = error_msg or "Failed to push issue"
|
|
289
|
+
logger.debug(
|
|
290
|
+
"push_issue_failed", issue_id=issue.id, error=error_msg
|
|
291
|
+
)
|
|
292
|
+
except Exception as e:
|
|
293
|
+
error_msg = str(e)
|
|
294
|
+
report.errors[issue.id] = error_msg
|
|
295
|
+
log_error_with_context(
|
|
296
|
+
e,
|
|
297
|
+
operation="push_issue",
|
|
298
|
+
entity_type="Issue",
|
|
299
|
+
entity_id=issue.id,
|
|
300
|
+
include_traceback=False,
|
|
301
|
+
)
|
|
302
|
+
|
|
303
|
+
logger.info(
|
|
304
|
+
"push_issues_completed",
|
|
305
|
+
total=len(local_issues),
|
|
306
|
+
pushed=len(report.pushed),
|
|
307
|
+
failed=len(report.errors),
|
|
308
|
+
)
|
|
309
|
+
return report
|
|
310
|
+
|
|
311
|
+
def _get_project_id_for_synced_issue(self) -> str | None:
|
|
312
|
+
"""Get default project ID for synced issues.
|
|
313
|
+
|
|
314
|
+
Returns:
|
|
315
|
+
Project ID or None
|
|
316
|
+
"""
|
|
317
|
+
return self._local_issue_service.get_project_id_for_synced_issue()
|
|
318
|
+
|
|
319
|
+
def _resolve_local_issue_id(
|
|
320
|
+
self, github_id: str | int | None, local_issue: Any
|
|
321
|
+
) -> str:
|
|
322
|
+
"""Resolve the local issue ID for a pulled GitHub issue.
|
|
323
|
+
|
|
324
|
+
Prefers an existing remote link mapping; falls back to the local issue ID.
|
|
325
|
+
"""
|
|
326
|
+
return self._local_issue_service.resolve_local_issue_id(github_id, local_issue)
|
|
327
|
+
|
|
328
|
+
def _create_or_update_issue_locally(
|
|
329
|
+
self, sync_issue: Any, local_issue: Any, github_id: str | int | None
|
|
330
|
+
) -> str | None:
|
|
331
|
+
"""Create or update issue in local database.
|
|
332
|
+
|
|
333
|
+
Args:
|
|
334
|
+
sync_issue: The SyncIssue from GitHub
|
|
335
|
+
local_issue: The converted local Issue domain object
|
|
336
|
+
github_id: The GitHub issue ID
|
|
337
|
+
|
|
338
|
+
Returns:
|
|
339
|
+
Local issue ID if successful
|
|
340
|
+
"""
|
|
341
|
+
return self._local_issue_service.create_or_update_issue_locally(
|
|
342
|
+
sync_issue,
|
|
343
|
+
local_issue,
|
|
344
|
+
github_id,
|
|
345
|
+
)
|
|
346
|
+
|
|
347
|
+
def _link_pulled_issue_locally(
|
|
348
|
+
self, local_issue_id: str | None, github_id: str | int | None
|
|
349
|
+
) -> bool:
|
|
350
|
+
"""Link pulled issue to GitHub in local database.
|
|
351
|
+
|
|
352
|
+
Args:
|
|
353
|
+
sync_issue: The SyncIssue from GitHub
|
|
354
|
+
|
|
355
|
+
Returns:
|
|
356
|
+
True if linking succeeded or was skipped
|
|
357
|
+
"""
|
|
358
|
+
return self._local_issue_service.link_pulled_issue_locally(
|
|
359
|
+
local_issue_id,
|
|
360
|
+
github_id,
|
|
361
|
+
)
|
|
362
|
+
|
|
363
|
+
def _handle_pull_error(self, sync_issue: Any, error_msg: str) -> tuple[bool, str]:
|
|
364
|
+
"""Handle and categorize pull errors.
|
|
365
|
+
|
|
366
|
+
Args:
|
|
367
|
+
sync_issue: The SyncIssue from GitHub
|
|
368
|
+
error_msg: The error message
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
Tuple of (success: bool, categorized_error_message: str)
|
|
372
|
+
"""
|
|
373
|
+
remote_id = getattr(sync_issue, "backend_id", "unknown")
|
|
374
|
+
|
|
375
|
+
if "Access forbidden" in error_msg or "403" in error_msg:
|
|
376
|
+
logger.debug(
|
|
377
|
+
"github_pull_issue_skipped_due_to_permissions",
|
|
378
|
+
github_number=remote_id,
|
|
379
|
+
error=error_msg,
|
|
380
|
+
severity="config",
|
|
381
|
+
)
|
|
382
|
+
return False, f"Permission denied (check token scope): {error_msg}"
|
|
383
|
+
elif "Gone" in error_msg or "410" in error_msg:
|
|
384
|
+
logger.info(
|
|
385
|
+
"github_pull_issue_skipped_resource_deleted",
|
|
386
|
+
github_number=remote_id,
|
|
387
|
+
error=error_msg,
|
|
388
|
+
severity="operational",
|
|
389
|
+
)
|
|
390
|
+
return False, f"Remote issue deleted: {error_msg}"
|
|
391
|
+
elif "not found" in error_msg.lower() or "404" in error_msg:
|
|
392
|
+
logger.debug(
|
|
393
|
+
"github_pull_issue_failed_not_found",
|
|
394
|
+
github_number=remote_id,
|
|
395
|
+
error=error_msg,
|
|
396
|
+
severity="operational",
|
|
397
|
+
)
|
|
398
|
+
return False, error_msg
|
|
399
|
+
elif "Rate limit exceeded" in error_msg or "429" in error_msg:
|
|
400
|
+
logger.debug(
|
|
401
|
+
"github_pull_issue_rate_limited",
|
|
402
|
+
github_number=remote_id,
|
|
403
|
+
error=error_msg,
|
|
404
|
+
severity="operational",
|
|
405
|
+
)
|
|
406
|
+
return False, f"Rate limited: {error_msg}"
|
|
407
|
+
else:
|
|
408
|
+
logger.debug(
|
|
409
|
+
"github_pull_issue_failed",
|
|
410
|
+
github_number=remote_id,
|
|
411
|
+
error=error_msg,
|
|
412
|
+
error_type=type(error_msg).__name__,
|
|
413
|
+
severity="operational",
|
|
414
|
+
)
|
|
415
|
+
return False, error_msg
|
|
416
|
+
|
|
417
|
+
def _pull_single_issue(self, sync_issue: Any) -> tuple[bool, str | None]:
|
|
418
|
+
"""Pull a single issue from GitHub and create/update locally.
|
|
419
|
+
|
|
420
|
+
Args:
|
|
421
|
+
sync_issue: The SyncIssue from GitHub fetch
|
|
422
|
+
|
|
423
|
+
Returns:
|
|
424
|
+
Tuple of (success: bool, error_message: str | None)
|
|
425
|
+
"""
|
|
426
|
+
try:
|
|
427
|
+
from roadmap.adapters.sync.backends.converters import (
|
|
428
|
+
GitHubPayloadToIssueConverter,
|
|
429
|
+
)
|
|
430
|
+
|
|
431
|
+
local_issue = GitHubPayloadToIssueConverter.from_sync_issue(sync_issue)
|
|
432
|
+
github_id = sync_issue.remote_ids.get("github") or sync_issue.backend_id
|
|
433
|
+
|
|
434
|
+
matching_local_issue = self.backend._find_matching_local_issue(
|
|
435
|
+
local_issue.title, github_id
|
|
436
|
+
)
|
|
437
|
+
updates = {
|
|
438
|
+
"title": local_issue.title,
|
|
439
|
+
"description": local_issue.content or "",
|
|
440
|
+
"status": local_issue.status,
|
|
441
|
+
"priority": local_issue.priority,
|
|
442
|
+
"assignee": local_issue.assignee,
|
|
443
|
+
"milestone": local_issue.milestone,
|
|
444
|
+
"labels": local_issue.labels,
|
|
445
|
+
}
|
|
446
|
+
self.backend._apply_or_create_local_issue(
|
|
447
|
+
local_issue.id,
|
|
448
|
+
matching_local_issue,
|
|
449
|
+
updates,
|
|
450
|
+
github_id,
|
|
451
|
+
remote_issue=sync_issue,
|
|
452
|
+
)
|
|
453
|
+
|
|
454
|
+
return True, None
|
|
455
|
+
|
|
456
|
+
except Exception as e:
|
|
457
|
+
log_error_with_context(
|
|
458
|
+
e,
|
|
459
|
+
operation="pull_issue",
|
|
460
|
+
entity_type="Issue",
|
|
461
|
+
entity_id=str(getattr(sync_issue, "backend_id", "unknown")),
|
|
462
|
+
include_traceback=True,
|
|
463
|
+
)
|
|
464
|
+
error_msg = str(e)
|
|
465
|
+
return self._handle_pull_error(sync_issue, error_msg)
|
|
466
|
+
|
|
467
|
+
def pull_issues(self, issue_ids: list[str]) -> SyncReport:
|
|
468
|
+
"""Pull issues from GitHub backend by IDs with dependency resolution.
|
|
469
|
+
|
|
470
|
+
This method now handles dependencies by:
|
|
471
|
+
1. Fetching all milestones referenced by the requested issues
|
|
472
|
+
2. Pulling those milestones first
|
|
473
|
+
3. Then pulling the issues with proper milestone references
|
|
474
|
+
|
|
475
|
+
Args:
|
|
476
|
+
issue_ids: List of issue IDs to pull.
|
|
477
|
+
|
|
478
|
+
Returns:
|
|
479
|
+
Sync report with pull results and errors.
|
|
480
|
+
"""
|
|
481
|
+
report = SyncReport()
|
|
482
|
+
|
|
483
|
+
if not issue_ids:
|
|
484
|
+
return report
|
|
485
|
+
|
|
486
|
+
logger.info(
|
|
487
|
+
"pull_issues_with_dependencies_starting", issue_count=len(issue_ids)
|
|
488
|
+
)
|
|
489
|
+
|
|
490
|
+
# Fetch all remote data
|
|
491
|
+
all_remote_issues, all_remote_milestones = self._fetch_remote_data()
|
|
492
|
+
|
|
493
|
+
# Analyze dependencies and build pull list
|
|
494
|
+
issues_to_pull, milestones_needed, report = self._analyze_issue_dependencies(
|
|
495
|
+
issue_ids, all_remote_issues, all_remote_milestones, report
|
|
496
|
+
)
|
|
497
|
+
|
|
498
|
+
# Phase 1: Pull milestones
|
|
499
|
+
milestone_pull_report = self._pull_milestone_dependencies(milestones_needed)
|
|
500
|
+
|
|
501
|
+
# Phase 2: Pull issues
|
|
502
|
+
return self._pull_issues_phase(
|
|
503
|
+
issues_to_pull,
|
|
504
|
+
all_remote_milestones,
|
|
505
|
+
milestone_pull_report,
|
|
506
|
+
report,
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
def _fetch_remote_data(self) -> tuple[dict, dict]:
|
|
510
|
+
"""Fetch issues and milestones from GitHub."""
|
|
511
|
+
from roadmap.adapters.sync.backends.services.github_issue_fetch_service import (
|
|
512
|
+
GitHubIssueFetchService,
|
|
513
|
+
)
|
|
514
|
+
from roadmap.adapters.sync.backends.services.github_milestone_fetch_service import (
|
|
515
|
+
GitHubMilestoneFetchService,
|
|
516
|
+
)
|
|
517
|
+
|
|
518
|
+
issue_fetch_service = GitHubIssueFetchService(
|
|
519
|
+
self.backend.github_client,
|
|
520
|
+
self.backend.config,
|
|
521
|
+
self.backend._helpers,
|
|
522
|
+
)
|
|
523
|
+
all_remote_issues = issue_fetch_service.get_issues()
|
|
524
|
+
|
|
525
|
+
milestone_fetch_service = GitHubMilestoneFetchService(
|
|
526
|
+
self.backend.github_client,
|
|
527
|
+
self.backend.config,
|
|
528
|
+
)
|
|
529
|
+
all_remote_milestones = milestone_fetch_service.get_milestones()
|
|
530
|
+
|
|
531
|
+
return all_remote_issues, all_remote_milestones
|
|
532
|
+
|
|
533
|
+
def _analyze_issue_dependencies(
|
|
534
|
+
self,
|
|
535
|
+
issue_ids: list[str],
|
|
536
|
+
all_remote_issues: dict,
|
|
537
|
+
all_remote_milestones: dict,
|
|
538
|
+
report: SyncReport,
|
|
539
|
+
) -> tuple[list, set, SyncReport]:
|
|
540
|
+
"""Build list of issues to pull and their milestone dependencies."""
|
|
541
|
+
return self._dependency_service.analyze_issue_dependencies(
|
|
542
|
+
issue_ids,
|
|
543
|
+
all_remote_issues,
|
|
544
|
+
all_remote_milestones,
|
|
545
|
+
report,
|
|
546
|
+
)
|
|
547
|
+
|
|
548
|
+
def _pull_milestone_dependencies(self, milestones_needed: set) -> SyncReport:
|
|
549
|
+
"""Pull milestone dependencies first."""
|
|
550
|
+
milestone_pull_report = SyncReport()
|
|
551
|
+
if milestones_needed:
|
|
552
|
+
logger.info("pulling_dependencies_milestones", count=len(milestones_needed))
|
|
553
|
+
milestone_ids = list(milestones_needed)
|
|
554
|
+
milestone_pull_report = self.pull_milestones(milestone_ids)
|
|
555
|
+
|
|
556
|
+
if milestone_pull_report.errors:
|
|
557
|
+
logger.warning(
|
|
558
|
+
"milestone_pull_failures",
|
|
559
|
+
failed_count=len(milestone_pull_report.errors),
|
|
560
|
+
errors=list(milestone_pull_report.errors.keys())[:5],
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
return milestone_pull_report
|
|
564
|
+
|
|
565
|
+
def _pull_issues_phase(
|
|
566
|
+
self,
|
|
567
|
+
issues_to_pull: list,
|
|
568
|
+
all_remote_milestones: dict,
|
|
569
|
+
milestone_pull_report: SyncReport,
|
|
570
|
+
report: SyncReport,
|
|
571
|
+
) -> SyncReport:
|
|
572
|
+
"""Pull issues after their dependencies are satisfied."""
|
|
573
|
+
successful_pulls = []
|
|
574
|
+
failed_pulls = {}
|
|
575
|
+
|
|
576
|
+
logger.info("pulling_issues_phase", issue_count=len(issues_to_pull))
|
|
577
|
+
|
|
578
|
+
for issue_id, _lookup_id, sync_issue in issues_to_pull:
|
|
579
|
+
try:
|
|
580
|
+
# Check if issue's milestone was successfully pulled
|
|
581
|
+
if sync_issue.milestone:
|
|
582
|
+
milestone_num = self._find_milestone_number(
|
|
583
|
+
sync_issue.milestone, all_remote_milestones
|
|
584
|
+
)
|
|
585
|
+
|
|
586
|
+
if milestone_num and milestone_num in milestone_pull_report.errors:
|
|
587
|
+
failed_pulls[issue_id] = (
|
|
588
|
+
f"Milestone '{sync_issue.milestone}' pull failed: "
|
|
589
|
+
f"{milestone_pull_report.errors[milestone_num]}"
|
|
590
|
+
)
|
|
591
|
+
logger.debug(
|
|
592
|
+
"issue_skipped_milestone_failed",
|
|
593
|
+
issue_id=issue_id,
|
|
594
|
+
milestone=sync_issue.milestone,
|
|
595
|
+
)
|
|
596
|
+
continue
|
|
597
|
+
|
|
598
|
+
# Pull the issue and create/update locally
|
|
599
|
+
success, error = self._pull_single_issue(sync_issue)
|
|
600
|
+
|
|
601
|
+
if success:
|
|
602
|
+
successful_pulls.append(issue_id)
|
|
603
|
+
logger.debug("pull_issue_processed", issue_id=issue_id)
|
|
604
|
+
else:
|
|
605
|
+
failed_pulls[issue_id] = error or "Unknown error"
|
|
606
|
+
|
|
607
|
+
except Exception as e:
|
|
608
|
+
failed_pulls[issue_id] = str(e)
|
|
609
|
+
log_error_with_context(
|
|
610
|
+
e,
|
|
611
|
+
operation="pull_issue",
|
|
612
|
+
entity_type="Issue",
|
|
613
|
+
entity_id=issue_id,
|
|
614
|
+
include_traceback=False,
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
report.pulled = successful_pulls
|
|
618
|
+
report.errors = failed_pulls
|
|
619
|
+
|
|
620
|
+
logger.info(
|
|
621
|
+
"pull_issues_complete",
|
|
622
|
+
milestones_pulled=len(milestone_pull_report.pulled)
|
|
623
|
+
if hasattr(milestone_pull_report, "pulled")
|
|
624
|
+
else 0,
|
|
625
|
+
issues_successful=len(successful_pulls),
|
|
626
|
+
issues_failed=len(failed_pulls),
|
|
627
|
+
)
|
|
628
|
+
|
|
629
|
+
return report
|
|
630
|
+
|
|
631
|
+
def _find_milestone_number(
|
|
632
|
+
self, milestone_name: str, all_remote_milestones: dict
|
|
633
|
+
) -> int | None:
|
|
634
|
+
"""Find milestone number by name."""
|
|
635
|
+
return self._dependency_service.find_milestone_number(
|
|
636
|
+
milestone_name,
|
|
637
|
+
all_remote_milestones,
|
|
638
|
+
)
|
|
639
|
+
|
|
640
|
+
def pull_milestones(self, milestone_ids: list[str]) -> SyncReport:
|
|
641
|
+
"""Pull milestones from GitHub backend by IDs.
|
|
642
|
+
|
|
643
|
+
Args:
|
|
644
|
+
milestone_ids: List of milestone IDs to pull (GitHub milestone numbers as strings).
|
|
645
|
+
|
|
646
|
+
Returns:
|
|
647
|
+
Sync report with pull results and errors.
|
|
648
|
+
"""
|
|
649
|
+
report = SyncReport()
|
|
650
|
+
|
|
651
|
+
if not milestone_ids:
|
|
652
|
+
return report
|
|
653
|
+
|
|
654
|
+
logger.info("pull_milestones_starting", milestone_count=len(milestone_ids))
|
|
655
|
+
|
|
656
|
+
# Fetch all remote milestones upfront
|
|
657
|
+
from roadmap.adapters.sync.backends.services.github_milestone_fetch_service import (
|
|
658
|
+
GitHubMilestoneFetchService,
|
|
659
|
+
)
|
|
660
|
+
|
|
661
|
+
try:
|
|
662
|
+
fetch_service = GitHubMilestoneFetchService(
|
|
663
|
+
self.backend.github_client,
|
|
664
|
+
self.backend.config,
|
|
665
|
+
)
|
|
666
|
+
all_remote_milestones = fetch_service.get_milestones()
|
|
667
|
+
|
|
668
|
+
successful_pulls = []
|
|
669
|
+
failed_pulls = {}
|
|
670
|
+
|
|
671
|
+
logger.info(
|
|
672
|
+
"pull_milestones_keys_analysis",
|
|
673
|
+
requested_sample=milestone_ids[:3] if milestone_ids else [],
|
|
674
|
+
remote_keys_sample=list(all_remote_milestones.keys())[:3]
|
|
675
|
+
if all_remote_milestones
|
|
676
|
+
else [],
|
|
677
|
+
requested_count=len(milestone_ids),
|
|
678
|
+
remote_count=len(all_remote_milestones),
|
|
679
|
+
)
|
|
680
|
+
|
|
681
|
+
# Process each requested milestone and save locally
|
|
682
|
+
for milestone_id in milestone_ids:
|
|
683
|
+
try:
|
|
684
|
+
# Strip any prefix if present
|
|
685
|
+
lookup_id = milestone_id
|
|
686
|
+
if milestone_id.startswith("_remote_milestone_"):
|
|
687
|
+
lookup_id = milestone_id[18:] # Remove prefix
|
|
688
|
+
|
|
689
|
+
if lookup_id not in all_remote_milestones:
|
|
690
|
+
error_msg = "Milestone not found on remote"
|
|
691
|
+
failed_pulls[milestone_id] = error_msg
|
|
692
|
+
logger.debug(
|
|
693
|
+
"pull_milestone_not_found",
|
|
694
|
+
milestone_id=milestone_id,
|
|
695
|
+
lookup_id=lookup_id,
|
|
696
|
+
)
|
|
697
|
+
continue
|
|
698
|
+
|
|
699
|
+
# Pull the milestone and create/update locally
|
|
700
|
+
sync_milestone = all_remote_milestones[lookup_id]
|
|
701
|
+
success, error = self._pull_single_milestone(sync_milestone)
|
|
702
|
+
|
|
703
|
+
if success:
|
|
704
|
+
successful_pulls.append(milestone_id)
|
|
705
|
+
logger.debug(
|
|
706
|
+
"pull_milestone_processed", milestone_id=milestone_id
|
|
707
|
+
)
|
|
708
|
+
else:
|
|
709
|
+
failed_pulls[milestone_id] = error or "Unknown error"
|
|
710
|
+
|
|
711
|
+
except Exception as e:
|
|
712
|
+
error_msg = str(e)
|
|
713
|
+
failed_pulls[milestone_id] = error_msg
|
|
714
|
+
log_error_with_context(
|
|
715
|
+
e,
|
|
716
|
+
operation="pull_milestone",
|
|
717
|
+
entity_type="Milestone",
|
|
718
|
+
entity_id=milestone_id,
|
|
719
|
+
include_traceback=False,
|
|
720
|
+
)
|
|
721
|
+
|
|
722
|
+
report.pulled = successful_pulls
|
|
723
|
+
report.errors = failed_pulls
|
|
724
|
+
|
|
725
|
+
logger.info(
|
|
726
|
+
"pull_milestones_complete",
|
|
727
|
+
successful=len(successful_pulls),
|
|
728
|
+
failed=len(failed_pulls),
|
|
729
|
+
)
|
|
730
|
+
|
|
731
|
+
return report
|
|
732
|
+
|
|
733
|
+
except Exception as e:
|
|
734
|
+
logger.error(
|
|
735
|
+
"pull_milestones_failed", error=str(e), error_type=type(e).__name__
|
|
736
|
+
)
|
|
737
|
+
report.error = f"Failed to pull milestones: {str(e)}"
|
|
738
|
+
return report
|
|
739
|
+
|
|
740
|
+
def _pull_single_milestone(self, sync_milestone) -> tuple[bool, str | None]:
|
|
741
|
+
"""Pull a single milestone and persist it locally.
|
|
742
|
+
|
|
743
|
+
Args:
|
|
744
|
+
sync_milestone: SyncMilestone object from GitHub
|
|
745
|
+
|
|
746
|
+
Returns:
|
|
747
|
+
Tuple of (success: bool, error_message: str | None)
|
|
748
|
+
"""
|
|
749
|
+
try:
|
|
750
|
+
# Create or update milestone locally
|
|
751
|
+
if not hasattr(self.backend, "core") or not self.backend.core:
|
|
752
|
+
return False, "Core not available"
|
|
753
|
+
|
|
754
|
+
core = self.backend.core
|
|
755
|
+
|
|
756
|
+
# Check if milestone already exists by GitHub milestone number
|
|
757
|
+
github_milestone_num = sync_milestone.backend_id
|
|
758
|
+
existing_milestone = None
|
|
759
|
+
|
|
760
|
+
# Try to find existing milestone by name (GitHub milestone title)
|
|
761
|
+
all_milestones = core.milestones.list()
|
|
762
|
+
for ms in all_milestones:
|
|
763
|
+
if ms.name == sync_milestone.name:
|
|
764
|
+
existing_milestone = ms
|
|
765
|
+
break
|
|
766
|
+
|
|
767
|
+
if existing_milestone:
|
|
768
|
+
# Update existing milestone
|
|
769
|
+
logger.debug(
|
|
770
|
+
"milestone_exists_updating",
|
|
771
|
+
milestone_name=sync_milestone.name,
|
|
772
|
+
github_number=github_milestone_num,
|
|
773
|
+
)
|
|
774
|
+
|
|
775
|
+
# Update fields
|
|
776
|
+
updates = {
|
|
777
|
+
"status": sync_milestone.status,
|
|
778
|
+
"due_date": sync_milestone.due_date,
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
if sync_milestone.headline:
|
|
782
|
+
updates["headline"] = sync_milestone.headline
|
|
783
|
+
|
|
784
|
+
success = core.milestones.update(existing_milestone.name, **updates)
|
|
785
|
+
|
|
786
|
+
if not success:
|
|
787
|
+
return False, "Failed to update milestone"
|
|
788
|
+
|
|
789
|
+
# Store GitHub milestone number in database metadata
|
|
790
|
+
self._link_milestone_to_github(
|
|
791
|
+
existing_milestone.name, github_milestone_num
|
|
792
|
+
)
|
|
793
|
+
|
|
794
|
+
else:
|
|
795
|
+
# Create new milestone
|
|
796
|
+
logger.debug(
|
|
797
|
+
"milestone_creating_new",
|
|
798
|
+
milestone_name=sync_milestone.name,
|
|
799
|
+
github_number=github_milestone_num,
|
|
800
|
+
)
|
|
801
|
+
|
|
802
|
+
milestone = core.milestones.create(
|
|
803
|
+
name=sync_milestone.name,
|
|
804
|
+
headline=sync_milestone.headline or "",
|
|
805
|
+
due_date=sync_milestone.due_date,
|
|
806
|
+
status=sync_milestone.status,
|
|
807
|
+
)
|
|
808
|
+
|
|
809
|
+
# Store GitHub milestone number in database metadata
|
|
810
|
+
self._link_milestone_to_github(milestone.name, github_milestone_num)
|
|
811
|
+
|
|
812
|
+
logger.info(
|
|
813
|
+
"milestone_pulled_successfully",
|
|
814
|
+
milestone_name=sync_milestone.name,
|
|
815
|
+
github_number=github_milestone_num,
|
|
816
|
+
)
|
|
817
|
+
|
|
818
|
+
return True, None
|
|
819
|
+
|
|
820
|
+
except Exception as e:
|
|
821
|
+
error_msg = str(e)
|
|
822
|
+
logger.error(
|
|
823
|
+
"pull_single_milestone_failed",
|
|
824
|
+
milestone_name=sync_milestone.name,
|
|
825
|
+
error=error_msg,
|
|
826
|
+
error_type=type(e).__name__,
|
|
827
|
+
)
|
|
828
|
+
return False, error_msg
|
|
829
|
+
|
|
830
|
+
def _link_milestone_to_github(
|
|
831
|
+
self, milestone_name: str, github_milestone_number: int
|
|
832
|
+
) -> bool:
|
|
833
|
+
"""Link milestone to GitHub milestone number by updating the milestone.
|
|
834
|
+
|
|
835
|
+
Args:
|
|
836
|
+
milestone_name: Local milestone name
|
|
837
|
+
github_milestone_number: GitHub milestone number
|
|
838
|
+
|
|
839
|
+
Returns:
|
|
840
|
+
True if successful, False otherwise
|
|
841
|
+
"""
|
|
842
|
+
try:
|
|
843
|
+
if not hasattr(self.backend, "core") or not self.backend.core:
|
|
844
|
+
return False
|
|
845
|
+
|
|
846
|
+
# Get the milestone using the service layer
|
|
847
|
+
milestone = self.backend.core.milestone_service.get_milestone(
|
|
848
|
+
milestone_name
|
|
849
|
+
)
|
|
850
|
+
if not milestone:
|
|
851
|
+
logger.debug(
|
|
852
|
+
"milestone_not_found_for_linking",
|
|
853
|
+
milestone_name=milestone_name,
|
|
854
|
+
)
|
|
855
|
+
return False
|
|
856
|
+
|
|
857
|
+
# Update the milestone's github_milestone field directly
|
|
858
|
+
milestone.github_milestone = github_milestone_number
|
|
859
|
+
milestone.updated = now_utc()
|
|
860
|
+
|
|
861
|
+
# Save the updated milestone using the repository
|
|
862
|
+
self.backend.core.milestone_service.repository.save(milestone)
|
|
863
|
+
|
|
864
|
+
logger.debug(
|
|
865
|
+
"milestone_linked_to_github",
|
|
866
|
+
milestone_name=milestone_name,
|
|
867
|
+
github_number=github_milestone_number,
|
|
868
|
+
)
|
|
869
|
+
return True
|
|
870
|
+
|
|
871
|
+
except Exception as e:
|
|
872
|
+
logger.warning(
|
|
873
|
+
"failed_to_link_milestone",
|
|
874
|
+
milestone_name=milestone_name,
|
|
875
|
+
github_number=github_milestone_number,
|
|
876
|
+
error=str(e),
|
|
877
|
+
)
|
|
878
|
+
return False
|
|
879
|
+
|
|
880
|
+
def push_milestones(self, local_milestones: list) -> SyncReport:
|
|
881
|
+
"""Push multiple local milestones to GitHub.
|
|
882
|
+
|
|
883
|
+
Args:
|
|
884
|
+
local_milestones: List of Milestone objects to push
|
|
885
|
+
|
|
886
|
+
Returns:
|
|
887
|
+
SyncReport with pushed, conflicts, and errors.
|
|
888
|
+
"""
|
|
889
|
+
report = SyncReport()
|
|
890
|
+
|
|
891
|
+
if not local_milestones:
|
|
892
|
+
return report
|
|
893
|
+
|
|
894
|
+
logger.info("push_milestones_starting", milestone_count=len(local_milestones))
|
|
895
|
+
|
|
896
|
+
# For now, milestone pushing is not implemented
|
|
897
|
+
# GitHub milestones would need to be created via the API
|
|
898
|
+
# This is a placeholder for future implementation
|
|
899
|
+
|
|
900
|
+
for milestone in local_milestones:
|
|
901
|
+
report.errors[milestone.name] = "Milestone pushing not yet implemented"
|
|
902
|
+
|
|
903
|
+
logger.warning(
|
|
904
|
+
"push_milestones_not_implemented",
|
|
905
|
+
milestone_count=len(local_milestones),
|
|
906
|
+
)
|
|
907
|
+
|
|
908
|
+
return report
|