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,792 @@
|
|
|
1
|
+
"""GitHub-specific implementation of SyncBackendInterface.
|
|
2
|
+
|
|
3
|
+
This module provides the GitHub API backend for syncing roadmap issues
|
|
4
|
+
with GitHub repositories. It implements the SyncBackendInterface protocol.
|
|
5
|
+
|
|
6
|
+
Updated to use Result<T, SyncError> pattern for explicit error handling.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import time
|
|
10
|
+
from collections.abc import Callable
|
|
11
|
+
from datetime import datetime # noqa: F401 # Used in type hints
|
|
12
|
+
from typing import Any, TypeVar
|
|
13
|
+
|
|
14
|
+
from structlog import get_logger
|
|
15
|
+
|
|
16
|
+
from roadmap.adapters.sync.backends.github_backend_helpers import GitHubBackendHelpers
|
|
17
|
+
from roadmap.adapters.sync.backends.github_client import GitHubClientWrapper
|
|
18
|
+
from roadmap.adapters.sync.backends.services.github_authentication_service import (
|
|
19
|
+
GitHubAuthenticationService,
|
|
20
|
+
)
|
|
21
|
+
from roadmap.adapters.sync.backends.services.github_issue_delete_service import (
|
|
22
|
+
GitHubIssueDeleteService,
|
|
23
|
+
)
|
|
24
|
+
from roadmap.adapters.sync.backends.services.github_issue_fetch_service import (
|
|
25
|
+
GitHubIssueFetchService,
|
|
26
|
+
)
|
|
27
|
+
from roadmap.common.result import Err, Ok, Result
|
|
28
|
+
from roadmap.core.domain.issue import Issue
|
|
29
|
+
from roadmap.core.interfaces import (
|
|
30
|
+
SyncConflict,
|
|
31
|
+
SyncReport,
|
|
32
|
+
)
|
|
33
|
+
from roadmap.core.models.sync_models import (
|
|
34
|
+
SyncIssue,
|
|
35
|
+
SyncMilestone,
|
|
36
|
+
SyncProject,
|
|
37
|
+
)
|
|
38
|
+
from roadmap.core.services.sync.sync_errors import (
|
|
39
|
+
SyncError,
|
|
40
|
+
SyncErrorType,
|
|
41
|
+
authentication_error,
|
|
42
|
+
)
|
|
43
|
+
from roadmap.core.services.sync.sync_metadata_service import SyncMetadataService
|
|
44
|
+
from roadmap.infrastructure.coordination.core import RoadmapCore
|
|
45
|
+
|
|
46
|
+
logger = get_logger()
|
|
47
|
+
|
|
48
|
+
T = TypeVar("T")
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class GitHubSyncBackend:
|
|
52
|
+
"""GitHub API implementation of the SyncBackendInterface.
|
|
53
|
+
|
|
54
|
+
Syncs roadmap issues with GitHub issues using the GitHub REST API.
|
|
55
|
+
Requires a GitHub Personal Access Token with 'repo' scope.
|
|
56
|
+
|
|
57
|
+
Attributes:
|
|
58
|
+
core: RoadmapCore instance with access to local issues
|
|
59
|
+
config: GitHub configuration with 'owner', 'repo', 'token'
|
|
60
|
+
github_client: GitHubIssueClient for API communication
|
|
61
|
+
"""
|
|
62
|
+
|
|
63
|
+
def __init__(self, core: RoadmapCore, config: dict[str, Any]):
|
|
64
|
+
"""Initialize GitHub sync backend.
|
|
65
|
+
|
|
66
|
+
Args:
|
|
67
|
+
core: RoadmapCore instance
|
|
68
|
+
config: Dict with required keys:
|
|
69
|
+
- owner: GitHub repository owner
|
|
70
|
+
- repo: GitHub repository name
|
|
71
|
+
- token: GitHub Personal Access Token (optional, checked at auth time)
|
|
72
|
+
|
|
73
|
+
Raises:
|
|
74
|
+
ValueError: If required config keys are missing
|
|
75
|
+
"""
|
|
76
|
+
if not config.get("owner") or not config.get("repo"):
|
|
77
|
+
raise ValueError("GitHub config must include 'owner' and 'repo'")
|
|
78
|
+
|
|
79
|
+
self.core = core
|
|
80
|
+
self.config = config
|
|
81
|
+
|
|
82
|
+
# Initialize GitHub client if token is provided, otherwise defer to authenticate()
|
|
83
|
+
token = config.get("token")
|
|
84
|
+
owner = config.get("owner")
|
|
85
|
+
repo = config.get("repo")
|
|
86
|
+
self.github_client = self._safe_init(
|
|
87
|
+
lambda: GitHubClientWrapper(token, owner, repo) if token else None,
|
|
88
|
+
"GitHubClientWrapper",
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
# Initialize metadata service if available
|
|
92
|
+
self.metadata_service = self._safe_init(
|
|
93
|
+
lambda: SyncMetadataService(core),
|
|
94
|
+
"SyncMetadataService",
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
# Initialize remote link repository if available
|
|
98
|
+
# This enables fast database lookups during sync operations
|
|
99
|
+
self.remote_link_repo = None
|
|
100
|
+
if (
|
|
101
|
+
hasattr(core, "db")
|
|
102
|
+
and core.db is not None
|
|
103
|
+
and hasattr(core.db, "remote_links")
|
|
104
|
+
):
|
|
105
|
+
self.remote_link_repo = core.db.remote_links
|
|
106
|
+
|
|
107
|
+
# Helper utilities extracted for mapping and local persistence
|
|
108
|
+
self._helpers = GitHubBackendHelpers(
|
|
109
|
+
core=self.core, remote_link_repo=self.remote_link_repo
|
|
110
|
+
)
|
|
111
|
+
|
|
112
|
+
# Initialize delegated services
|
|
113
|
+
self._auth_service = GitHubAuthenticationService(config)
|
|
114
|
+
self._fetch_service = None # Initialized lazily after auth
|
|
115
|
+
self._issue_delete_service = GitHubIssueDeleteService(config)
|
|
116
|
+
|
|
117
|
+
def _safe_init(self, factory: Callable[[], T], name: str) -> T | None:
|
|
118
|
+
"""Safely initialize a component, returning None on failure.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
factory: Callable that returns the initialized component
|
|
122
|
+
name: Component name for logging
|
|
123
|
+
|
|
124
|
+
Returns:
|
|
125
|
+
Initialized component or None if initialization fails
|
|
126
|
+
"""
|
|
127
|
+
try:
|
|
128
|
+
return factory()
|
|
129
|
+
except (ImportError, AttributeError) as e:
|
|
130
|
+
logger.warning(
|
|
131
|
+
"initialization_failed",
|
|
132
|
+
component=name,
|
|
133
|
+
error=str(e),
|
|
134
|
+
error_type=type(e).__name__,
|
|
135
|
+
suggested_action="check_dependencies",
|
|
136
|
+
)
|
|
137
|
+
return None
|
|
138
|
+
except Exception as e:
|
|
139
|
+
logger.warning(
|
|
140
|
+
"initialization_failed",
|
|
141
|
+
component=name,
|
|
142
|
+
error=str(e),
|
|
143
|
+
error_type=type(e).__name__,
|
|
144
|
+
error_classification="unknown_error",
|
|
145
|
+
)
|
|
146
|
+
return None
|
|
147
|
+
|
|
148
|
+
def get_backend_name(self) -> str:
|
|
149
|
+
"""Get the canonical name of this backend.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
'github' - used as key in Issue.remote_ids dict
|
|
153
|
+
"""
|
|
154
|
+
return "github"
|
|
155
|
+
|
|
156
|
+
def authenticate(self) -> Result[bool, SyncError]:
|
|
157
|
+
"""Verify credentials and remote connectivity.
|
|
158
|
+
|
|
159
|
+
Returns:
|
|
160
|
+
Ok(True) if authentication succeeds (token valid and repo accessible)
|
|
161
|
+
Err(SyncError) with authentication error details
|
|
162
|
+
"""
|
|
163
|
+
try:
|
|
164
|
+
result = self._auth_service.authenticate()
|
|
165
|
+
if result and self.github_client is None:
|
|
166
|
+
# Update our client reference after successful auth
|
|
167
|
+
self.github_client = self._auth_service.github_client
|
|
168
|
+
|
|
169
|
+
if result:
|
|
170
|
+
return Ok(True)
|
|
171
|
+
else:
|
|
172
|
+
return Err(authentication_error("GitHub authentication failed"))
|
|
173
|
+
except Exception as e:
|
|
174
|
+
logger.error(
|
|
175
|
+
"github_authenticate_exception",
|
|
176
|
+
error=str(e),
|
|
177
|
+
error_type=type(e).__name__,
|
|
178
|
+
)
|
|
179
|
+
return Err(
|
|
180
|
+
SyncError.from_exception(
|
|
181
|
+
e,
|
|
182
|
+
error_type=SyncErrorType.AUTHENTICATION_FAILED,
|
|
183
|
+
)
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
def get_api_client(self) -> Any:
|
|
187
|
+
"""Get the GitHub API client with handler methods for push/pull operations.
|
|
188
|
+
|
|
189
|
+
Returns:
|
|
190
|
+
IssueHandler instance with create_issue(), update_issue(), etc. methods.
|
|
191
|
+
|
|
192
|
+
This method is used internally by GitHubSyncOps for actual API calls.
|
|
193
|
+
It ensures operations use the proper handler-based client for low-level
|
|
194
|
+
API operations, not the high-level GitHubClient wrapper.
|
|
195
|
+
"""
|
|
196
|
+
import requests
|
|
197
|
+
|
|
198
|
+
from roadmap.adapters.github.handlers.issues import IssueHandler
|
|
199
|
+
|
|
200
|
+
# Create a session with the token
|
|
201
|
+
session = requests.Session()
|
|
202
|
+
token = self.config.get("token")
|
|
203
|
+
if token:
|
|
204
|
+
session.headers.update(
|
|
205
|
+
{
|
|
206
|
+
"Authorization": f"token {token}",
|
|
207
|
+
"Accept": "application/vnd.github.v3+json",
|
|
208
|
+
"User-Agent": "roadmap-cli/1.0",
|
|
209
|
+
}
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
# Return a properly configured issue handler for API calls
|
|
213
|
+
return IssueHandler(
|
|
214
|
+
session=session,
|
|
215
|
+
owner=self.config.get("owner"),
|
|
216
|
+
repo=self.config.get("repo"),
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
def get_label_client(self) -> Any | None:
|
|
220
|
+
"""Get a GitHub client that supports label operations."""
|
|
221
|
+
token = self.config.get("token")
|
|
222
|
+
if not token:
|
|
223
|
+
return None
|
|
224
|
+
|
|
225
|
+
from roadmap.adapters.github.github import GitHubClient
|
|
226
|
+
|
|
227
|
+
return GitHubClient(
|
|
228
|
+
token=token,
|
|
229
|
+
owner=self.config.get("owner"),
|
|
230
|
+
repo=self.config.get("repo"),
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
def get_issues(self) -> Result[dict[str, SyncIssue], SyncError]:
|
|
234
|
+
"""Fetch all issues from GitHub remote.
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
Ok(dict) mapping issue_id -> SyncIssue objects on success
|
|
238
|
+
Err(SyncError) with error details on failure
|
|
239
|
+
"""
|
|
240
|
+
try:
|
|
241
|
+
# Lazily initialize fetch service after first auth
|
|
242
|
+
if self._fetch_service is None and self.github_client:
|
|
243
|
+
self._fetch_service = GitHubIssueFetchService(
|
|
244
|
+
self.github_client, self.config, self._helpers
|
|
245
|
+
)
|
|
246
|
+
|
|
247
|
+
if self._fetch_service is None:
|
|
248
|
+
logger.warning("github_fetch_service_not_initialized")
|
|
249
|
+
return Err(
|
|
250
|
+
SyncError(
|
|
251
|
+
error_type=SyncErrorType.AUTHENTICATION_FAILED,
|
|
252
|
+
message="GitHub fetch service not initialized - authenticate first",
|
|
253
|
+
suggested_fix="Call authenticate() before get_issues()",
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
issues = self._fetch_service.get_issues()
|
|
258
|
+
return Ok(issues)
|
|
259
|
+
except Exception as e:
|
|
260
|
+
logger.error(
|
|
261
|
+
"github_get_issues_exception",
|
|
262
|
+
error=str(e),
|
|
263
|
+
error_type=type(e).__name__,
|
|
264
|
+
)
|
|
265
|
+
return Err(
|
|
266
|
+
SyncError.from_exception(
|
|
267
|
+
e,
|
|
268
|
+
error_type=SyncErrorType.NETWORK_ERROR,
|
|
269
|
+
)
|
|
270
|
+
)
|
|
271
|
+
|
|
272
|
+
def push_issues(self, local_issues: list[Issue]) -> Result[SyncReport, SyncError]:
|
|
273
|
+
"""Push multiple local issues to GitHub.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
local_issues: List of Issue objects to push
|
|
277
|
+
|
|
278
|
+
Returns:
|
|
279
|
+
Ok(SyncReport) with pushed, conflicts, and errors on success
|
|
280
|
+
Err(SyncError) with fatal error details if operation cannot proceed
|
|
281
|
+
"""
|
|
282
|
+
try:
|
|
283
|
+
# Delegate orchestration to helper class
|
|
284
|
+
from roadmap.adapters.sync.backends.github_sync_ops import GitHubSyncOps
|
|
285
|
+
|
|
286
|
+
ops = GitHubSyncOps(self)
|
|
287
|
+
report = ops.push_issues(local_issues)
|
|
288
|
+
return Ok(report)
|
|
289
|
+
except Exception as e:
|
|
290
|
+
logger.error(
|
|
291
|
+
"github_push_issues_exception",
|
|
292
|
+
error=str(e),
|
|
293
|
+
error_type=type(e).__name__,
|
|
294
|
+
issue_count=len(local_issues),
|
|
295
|
+
)
|
|
296
|
+
return Err(
|
|
297
|
+
SyncError.from_exception(
|
|
298
|
+
e,
|
|
299
|
+
error_type=SyncErrorType.UNKNOWN_ERROR,
|
|
300
|
+
entity_type="Issue",
|
|
301
|
+
)
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
def push_issue(self, local_issue: Issue) -> Result[bool, SyncError]:
|
|
305
|
+
"""Push a single local issue to GitHub.
|
|
306
|
+
|
|
307
|
+
Args:
|
|
308
|
+
local_issue: The Issue object to push
|
|
309
|
+
|
|
310
|
+
Returns:
|
|
311
|
+
Ok(True) if push succeeds
|
|
312
|
+
Err(SyncError) with error details if push fails
|
|
313
|
+
|
|
314
|
+
Deprecated:
|
|
315
|
+
Use push_issues([issue]) instead. This method exists for backward
|
|
316
|
+
compatibility but is not used by the sync orchestrator.
|
|
317
|
+
"""
|
|
318
|
+
report_result = self.push_issues([local_issue])
|
|
319
|
+
if report_result.is_err():
|
|
320
|
+
return report_result # type: ignore[return-value]
|
|
321
|
+
|
|
322
|
+
report = report_result.unwrap()
|
|
323
|
+
if len(report.pushed) > 0 and len(report.errors) == 0:
|
|
324
|
+
return Ok(True)
|
|
325
|
+
else:
|
|
326
|
+
error_msg = report.errors.get(str(local_issue.id), "Push failed")
|
|
327
|
+
return Err(
|
|
328
|
+
SyncError(
|
|
329
|
+
error_type=SyncErrorType.UNKNOWN_ERROR,
|
|
330
|
+
message=error_msg,
|
|
331
|
+
entity_type="Issue",
|
|
332
|
+
entity_id=str(local_issue.id),
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
def pull_issues(self, issue_ids: list[str]) -> Result[SyncReport, SyncError]:
|
|
337
|
+
"""Pull specified remote GitHub issues to local.
|
|
338
|
+
|
|
339
|
+
Args:
|
|
340
|
+
issue_ids: List of remote issue IDs to pull
|
|
341
|
+
|
|
342
|
+
Returns:
|
|
343
|
+
Ok(SyncReport) with pulled, conflicts, and errors on success
|
|
344
|
+
Err(SyncError) with fatal error details if operation cannot proceed
|
|
345
|
+
|
|
346
|
+
Notes:
|
|
347
|
+
- Each ID should correspond to a remote issue
|
|
348
|
+
- Updates or creates local files as needed
|
|
349
|
+
- Uses parallel execution with thread pool for performance
|
|
350
|
+
"""
|
|
351
|
+
try:
|
|
352
|
+
from roadmap.adapters.sync.backends.github_sync_ops import GitHubSyncOps
|
|
353
|
+
|
|
354
|
+
ops = GitHubSyncOps(self)
|
|
355
|
+
report = ops.pull_issues(issue_ids)
|
|
356
|
+
return Ok(report)
|
|
357
|
+
except Exception as e:
|
|
358
|
+
logger.error(
|
|
359
|
+
"github_pull_issues_exception",
|
|
360
|
+
error=str(e),
|
|
361
|
+
error_type=type(e).__name__,
|
|
362
|
+
issue_count=len(issue_ids),
|
|
363
|
+
)
|
|
364
|
+
return Err(
|
|
365
|
+
SyncError.from_exception(
|
|
366
|
+
e,
|
|
367
|
+
error_type=SyncErrorType.UNKNOWN_ERROR,
|
|
368
|
+
entity_type="Issue",
|
|
369
|
+
)
|
|
370
|
+
)
|
|
371
|
+
|
|
372
|
+
def pull_milestones(self, milestone_ids: list[str]) -> SyncReport:
|
|
373
|
+
"""Pull specified remote GitHub milestones to local.
|
|
374
|
+
|
|
375
|
+
Args:
|
|
376
|
+
milestone_ids: List of remote milestone IDs to pull (GitHub milestone numbers)
|
|
377
|
+
|
|
378
|
+
Returns:
|
|
379
|
+
SyncReport with pulled, conflicts, and errors.
|
|
380
|
+
"""
|
|
381
|
+
from roadmap.adapters.sync.backends.github_sync_ops import GitHubSyncOps
|
|
382
|
+
|
|
383
|
+
ops = GitHubSyncOps(self)
|
|
384
|
+
return ops.pull_milestones(milestone_ids)
|
|
385
|
+
|
|
386
|
+
def push_milestones(self, local_milestones: list) -> SyncReport:
|
|
387
|
+
"""Push multiple local milestones to GitHub.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
local_milestones: List of Milestone objects to push
|
|
391
|
+
|
|
392
|
+
Returns:
|
|
393
|
+
SyncReport with pushed, conflicts, and errors.
|
|
394
|
+
|
|
395
|
+
Note:
|
|
396
|
+
Currently not implemented - milestones must be created on GitHub first.
|
|
397
|
+
"""
|
|
398
|
+
from roadmap.adapters.sync.backends.github_sync_ops import GitHubSyncOps
|
|
399
|
+
|
|
400
|
+
ops = GitHubSyncOps(self)
|
|
401
|
+
return ops.push_milestones(local_milestones)
|
|
402
|
+
|
|
403
|
+
def pull_issue(self, issue_id: str) -> Result[bool, SyncError]:
|
|
404
|
+
"""Pull a single remote GitHub issue to local.
|
|
405
|
+
|
|
406
|
+
Args:
|
|
407
|
+
issue_id: The remote issue ID (GitHub issue number like "_remote_123" or "123")
|
|
408
|
+
|
|
409
|
+
Returns:
|
|
410
|
+
Ok(True) if issue is valid and can be pulled
|
|
411
|
+
Err(SyncError) if error during validation
|
|
412
|
+
|
|
413
|
+
Notes:
|
|
414
|
+
- Validates the issue_id can be parsed
|
|
415
|
+
- Actual fetching/saving is handled by pull_issues caller
|
|
416
|
+
- Returns Ok(True) to indicate success to GitHubSyncOps
|
|
417
|
+
"""
|
|
418
|
+
try:
|
|
419
|
+
# Parse issue_id - extract numeric part from "_remote_123" format
|
|
420
|
+
if issue_id.startswith("_remote_"):
|
|
421
|
+
issue_num_str = issue_id[8:] # Remove "_remote_" prefix
|
|
422
|
+
else:
|
|
423
|
+
issue_num_str = issue_id
|
|
424
|
+
|
|
425
|
+
# Validate it's a valid integer
|
|
426
|
+
try:
|
|
427
|
+
int(issue_num_str)
|
|
428
|
+
except (ValueError, TypeError):
|
|
429
|
+
logger.debug(
|
|
430
|
+
"pull_issue_invalid_id",
|
|
431
|
+
issue_id=issue_id,
|
|
432
|
+
reason="Cannot parse as integer",
|
|
433
|
+
)
|
|
434
|
+
return Err(
|
|
435
|
+
SyncError(
|
|
436
|
+
error_type=SyncErrorType.VALIDATION_ERROR,
|
|
437
|
+
message=f"Invalid issue ID format: {issue_id}",
|
|
438
|
+
suggested_fix="Ensure issue_id is a valid number",
|
|
439
|
+
)
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
# If we got here, the issue_id is valid
|
|
443
|
+
# The actual pull work happens elsewhere in the sync pipeline
|
|
444
|
+
return Ok(True)
|
|
445
|
+
|
|
446
|
+
except Exception as e:
|
|
447
|
+
logger.debug(
|
|
448
|
+
"pull_issue_validation_failed",
|
|
449
|
+
issue_id=issue_id,
|
|
450
|
+
error=str(e),
|
|
451
|
+
error_type=type(e).__name__,
|
|
452
|
+
)
|
|
453
|
+
return Err(
|
|
454
|
+
SyncError.from_exception(
|
|
455
|
+
e,
|
|
456
|
+
error_type=SyncErrorType.VALIDATION_ERROR,
|
|
457
|
+
)
|
|
458
|
+
)
|
|
459
|
+
|
|
460
|
+
def _convert_sync_to_issue(self, issue_id: str, sync_issue: SyncIssue) -> "Issue":
|
|
461
|
+
"""Convert SyncIssue to local Issue object.
|
|
462
|
+
|
|
463
|
+
Args:
|
|
464
|
+
issue_id: Local issue ID
|
|
465
|
+
sync_issue: SyncIssue with remote data
|
|
466
|
+
|
|
467
|
+
Returns:
|
|
468
|
+
Issue instance with converted data
|
|
469
|
+
|
|
470
|
+
Raises:
|
|
471
|
+
ValueError: If conversion fails
|
|
472
|
+
"""
|
|
473
|
+
return self._helpers._convert_sync_to_issue(issue_id, sync_issue)
|
|
474
|
+
|
|
475
|
+
def _convert_github_to_issue(
|
|
476
|
+
self, issue_id: str, remote_data: dict[str, Any]
|
|
477
|
+
) -> "Issue":
|
|
478
|
+
"""Convert GitHub issue dict to local Issue object.
|
|
479
|
+
|
|
480
|
+
Args:
|
|
481
|
+
issue_id: Local issue ID
|
|
482
|
+
remote_data: GitHub issue data from API
|
|
483
|
+
|
|
484
|
+
Returns:
|
|
485
|
+
Issue instance with converted data
|
|
486
|
+
|
|
487
|
+
Raises:
|
|
488
|
+
ValueError: If conversion fails
|
|
489
|
+
"""
|
|
490
|
+
return self._helpers._convert_github_to_issue(issue_id, remote_data)
|
|
491
|
+
|
|
492
|
+
def _parse_timestamp(self, timestamp_str: str | None) -> "datetime | None":
|
|
493
|
+
"""Parse ISO format timestamp string.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
timestamp_str: ISO format timestamp (may have 'Z' suffix)
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
datetime object or None if parsing fails
|
|
500
|
+
"""
|
|
501
|
+
return self._helpers._parse_timestamp(timestamp_str)
|
|
502
|
+
|
|
503
|
+
def get_conflict_resolution_options(self, conflict: SyncConflict) -> list[str]:
|
|
504
|
+
"""Get available resolution strategies for a conflict.
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
conflict: The SyncConflict to resolve
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
List of resolution option codes (e.g., ['use_local', 'use_remote', 'merge'])
|
|
511
|
+
"""
|
|
512
|
+
# GitHub backend supports the three basic strategies
|
|
513
|
+
return ["use_local", "use_remote", "merge"]
|
|
514
|
+
|
|
515
|
+
def resolve_conflict(self, conflict: SyncConflict, resolution: str) -> bool:
|
|
516
|
+
"""Resolve a sync conflict using specified strategy.
|
|
517
|
+
|
|
518
|
+
Args:
|
|
519
|
+
conflict: The SyncConflict to resolve
|
|
520
|
+
resolution: The resolution strategy code ('use_local', 'use_remote', or 'merge')
|
|
521
|
+
|
|
522
|
+
Returns:
|
|
523
|
+
True if resolution succeeds, False otherwise.
|
|
524
|
+
"""
|
|
525
|
+
from structlog import get_logger
|
|
526
|
+
|
|
527
|
+
logger = get_logger()
|
|
528
|
+
|
|
529
|
+
try:
|
|
530
|
+
logger.debug(
|
|
531
|
+
"github_resolve_conflict_started",
|
|
532
|
+
issue_id=conflict.issue_id,
|
|
533
|
+
resolution=resolution,
|
|
534
|
+
)
|
|
535
|
+
|
|
536
|
+
# For now, return True to indicate success
|
|
537
|
+
# Full implementation will handle actual conflict resolution
|
|
538
|
+
# when GitHub backend API is fully implemented
|
|
539
|
+
|
|
540
|
+
logger.info(
|
|
541
|
+
"github_resolve_conflict_completed",
|
|
542
|
+
issue_id=conflict.issue_id,
|
|
543
|
+
resolution=resolution,
|
|
544
|
+
)
|
|
545
|
+
return True
|
|
546
|
+
|
|
547
|
+
except Exception as e:
|
|
548
|
+
logger.warning(
|
|
549
|
+
"github_resolve_conflict_failed",
|
|
550
|
+
issue_id=conflict.issue_id,
|
|
551
|
+
resolution=resolution,
|
|
552
|
+
error=str(e),
|
|
553
|
+
)
|
|
554
|
+
return False
|
|
555
|
+
|
|
556
|
+
def get_milestones(self) -> Result[dict[str, SyncMilestone], SyncError]:
|
|
557
|
+
"""Fetch all milestones from GitHub.
|
|
558
|
+
|
|
559
|
+
Returns:
|
|
560
|
+
Ok(dict) mapping milestone_number -> SyncMilestone objects on success
|
|
561
|
+
Err(SyncError) with error details on failure
|
|
562
|
+
"""
|
|
563
|
+
from roadmap.adapters.sync.backends.services.github_milestone_fetch_service import (
|
|
564
|
+
GitHubMilestoneFetchService,
|
|
565
|
+
)
|
|
566
|
+
|
|
567
|
+
if not self.github_client:
|
|
568
|
+
logger.warning("github_milestone_fetch_no_client")
|
|
569
|
+
return Err(
|
|
570
|
+
SyncError(
|
|
571
|
+
error_type=SyncErrorType.AUTHENTICATION_FAILED,
|
|
572
|
+
message="GitHub client not initialized - authenticate first",
|
|
573
|
+
entity_type="Milestone",
|
|
574
|
+
suggested_fix="Call authenticate() before get_milestones()",
|
|
575
|
+
)
|
|
576
|
+
)
|
|
577
|
+
|
|
578
|
+
try:
|
|
579
|
+
fetch_service = GitHubMilestoneFetchService(self.github_client, self.config)
|
|
580
|
+
milestones = fetch_service.get_milestones(state="all")
|
|
581
|
+
|
|
582
|
+
logger.info(
|
|
583
|
+
"github_milestones_retrieved",
|
|
584
|
+
count=len(milestones),
|
|
585
|
+
owner=self.config.get("owner"),
|
|
586
|
+
repo=self.config.get("repo"),
|
|
587
|
+
)
|
|
588
|
+
|
|
589
|
+
return Ok(milestones)
|
|
590
|
+
|
|
591
|
+
except Exception as e:
|
|
592
|
+
logger.error(
|
|
593
|
+
"github_get_milestones_failed",
|
|
594
|
+
error=str(e),
|
|
595
|
+
error_type=type(e).__name__,
|
|
596
|
+
owner=self.config.get("owner"),
|
|
597
|
+
repo=self.config.get("repo"),
|
|
598
|
+
)
|
|
599
|
+
return Err(
|
|
600
|
+
SyncError.from_exception(
|
|
601
|
+
e,
|
|
602
|
+
error_type=SyncErrorType.NETWORK_ERROR,
|
|
603
|
+
entity_type="Milestone",
|
|
604
|
+
)
|
|
605
|
+
)
|
|
606
|
+
|
|
607
|
+
# --- Helper methods for pull/create matching and persistence ---
|
|
608
|
+
def _find_matching_local_issue(
|
|
609
|
+
self, title: str, github_issue_number: str | int | None
|
|
610
|
+
):
|
|
611
|
+
"""Find a matching local issue by GitHub number or by title.
|
|
612
|
+
|
|
613
|
+
Accepts either numeric or string GitHub IDs; comparison is done
|
|
614
|
+
by normalizing both sides to string.
|
|
615
|
+
"""
|
|
616
|
+
return self._helpers._find_matching_local_issue(title, github_issue_number)
|
|
617
|
+
|
|
618
|
+
def _apply_or_create_local_issue(
|
|
619
|
+
self,
|
|
620
|
+
issue_id: str,
|
|
621
|
+
matching_local_issue,
|
|
622
|
+
updates: dict,
|
|
623
|
+
github_issue_number: str | int | None,
|
|
624
|
+
remote_issue: SyncIssue | None = None,
|
|
625
|
+
) -> None:
|
|
626
|
+
"""Apply updates to an existing local issue or create a new one, and ensure linking/persistence.
|
|
627
|
+
|
|
628
|
+
Args:
|
|
629
|
+
issue_id: remote issue id (e.g. 'gh-123') used as fallback local id
|
|
630
|
+
matching_local_issue: existing local Issue object or None
|
|
631
|
+
updates: dict of fields to update on local issue
|
|
632
|
+
github_issue_number: numeric GitHub issue number if available
|
|
633
|
+
remote_issue: original SyncIssue object from GitHub (used when creating)
|
|
634
|
+
"""
|
|
635
|
+
return self._helpers._apply_or_create_local_issue(
|
|
636
|
+
issue_id, matching_local_issue, updates, github_issue_number, remote_issue
|
|
637
|
+
)
|
|
638
|
+
|
|
639
|
+
def get_projects(self) -> Result[dict[str, SyncProject], SyncError]:
|
|
640
|
+
"""Fetch all projects from GitHub.
|
|
641
|
+
|
|
642
|
+
Currently returns empty dict as project fetching is not yet implemented.
|
|
643
|
+
GitHub uses classic Project Boards and newer Projects (beta).
|
|
644
|
+
"""
|
|
645
|
+
# TODO: Implement GitHub Projects API integration
|
|
646
|
+
return Ok({})
|
|
647
|
+
|
|
648
|
+
def delete_issues(self, issue_numbers: list[int]) -> int:
|
|
649
|
+
"""Delete GitHub issues via GraphQL batch mutation.
|
|
650
|
+
|
|
651
|
+
Args:
|
|
652
|
+
issue_numbers: GitHub issue numbers to delete
|
|
653
|
+
|
|
654
|
+
Returns:
|
|
655
|
+
Count of successfully deleted issues
|
|
656
|
+
"""
|
|
657
|
+
return self._issue_delete_service.delete_issues(issue_numbers)
|
|
658
|
+
|
|
659
|
+
def _get_delete_issue_config(self) -> tuple[str, str, str] | None:
|
|
660
|
+
"""Resolve required delete configuration or log why delete is skipped."""
|
|
661
|
+
return self._issue_delete_service.get_delete_issue_config()
|
|
662
|
+
|
|
663
|
+
def _process_issue_lookup_batch(
|
|
664
|
+
self,
|
|
665
|
+
*,
|
|
666
|
+
batch: list[int],
|
|
667
|
+
owner: str,
|
|
668
|
+
repo: str,
|
|
669
|
+
token: str,
|
|
670
|
+
delete_batch_size: int,
|
|
671
|
+
inter_batch_delay_seconds: float,
|
|
672
|
+
rate_limit_delay_seconds: float,
|
|
673
|
+
) -> tuple[int, list[int]]:
|
|
674
|
+
"""Resolve node IDs for a lookup batch and process delete chunks."""
|
|
675
|
+
return self._issue_delete_service.process_issue_lookup_batch(
|
|
676
|
+
batch=batch,
|
|
677
|
+
owner=owner,
|
|
678
|
+
repo=repo,
|
|
679
|
+
token=token,
|
|
680
|
+
delete_batch_size=delete_batch_size,
|
|
681
|
+
inter_batch_delay_seconds=inter_batch_delay_seconds,
|
|
682
|
+
rate_limit_delay_seconds=rate_limit_delay_seconds,
|
|
683
|
+
)
|
|
684
|
+
|
|
685
|
+
def _delete_issue_node_chunks(
|
|
686
|
+
self,
|
|
687
|
+
*,
|
|
688
|
+
node_ids: dict[int, str],
|
|
689
|
+
token: str,
|
|
690
|
+
delete_batch_size: int,
|
|
691
|
+
inter_batch_delay_seconds: float,
|
|
692
|
+
rate_limit_delay_seconds: float,
|
|
693
|
+
) -> int:
|
|
694
|
+
"""Delete issues in chunked GraphQL mutations with optional retry."""
|
|
695
|
+
return self._issue_delete_service.delete_issue_node_chunks(
|
|
696
|
+
node_ids=node_ids,
|
|
697
|
+
token=token,
|
|
698
|
+
delete_batch_size=delete_batch_size,
|
|
699
|
+
inter_batch_delay_seconds=inter_batch_delay_seconds,
|
|
700
|
+
rate_limit_delay_seconds=rate_limit_delay_seconds,
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
def _retry_failed_rate_limited_deletes(
|
|
704
|
+
self,
|
|
705
|
+
*,
|
|
706
|
+
failed_numbers: list[int],
|
|
707
|
+
rate_limited: bool,
|
|
708
|
+
node_ids: dict[int, str],
|
|
709
|
+
token: str,
|
|
710
|
+
rate_limit_delay_seconds: float,
|
|
711
|
+
) -> int:
|
|
712
|
+
"""Retry failed delete mutations when they failed under rate limiting."""
|
|
713
|
+
return self._issue_delete_service.retry_failed_rate_limited_deletes(
|
|
714
|
+
failed_numbers=failed_numbers,
|
|
715
|
+
rate_limited=rate_limited,
|
|
716
|
+
node_ids=node_ids,
|
|
717
|
+
token=token,
|
|
718
|
+
rate_limit_delay_seconds=rate_limit_delay_seconds,
|
|
719
|
+
)
|
|
720
|
+
|
|
721
|
+
def _resolve_issue_node_ids(
|
|
722
|
+
self,
|
|
723
|
+
issue_numbers: list[int],
|
|
724
|
+
owner: str,
|
|
725
|
+
repo: str,
|
|
726
|
+
token: str,
|
|
727
|
+
) -> tuple[dict[int, str], list[int]]:
|
|
728
|
+
"""Resolve GitHub issue numbers to GraphQL node IDs.
|
|
729
|
+
|
|
730
|
+
Returns tuple of (node_ids, skipped_pr_numbers).
|
|
731
|
+
"""
|
|
732
|
+
return self._issue_delete_service.resolve_issue_node_ids(
|
|
733
|
+
issue_numbers,
|
|
734
|
+
owner,
|
|
735
|
+
repo,
|
|
736
|
+
token,
|
|
737
|
+
)
|
|
738
|
+
|
|
739
|
+
def _delete_issues_batch(
|
|
740
|
+
self, node_ids: dict[int, str], token: str
|
|
741
|
+
) -> tuple[int, bool, list[int]]:
|
|
742
|
+
"""Delete issues by node ID via GraphQL mutation."""
|
|
743
|
+
return self._issue_delete_service.delete_issues_batch(node_ids, token)
|
|
744
|
+
|
|
745
|
+
def _extract_graphql_error_types(self, payload: dict[str, Any]) -> set[str]:
|
|
746
|
+
"""Extract error types from a GraphQL payload."""
|
|
747
|
+
return self._issue_delete_service.extract_graphql_error_types(payload)
|
|
748
|
+
|
|
749
|
+
def _extract_graphql_error_details(
|
|
750
|
+
self, payload: dict[str, Any]
|
|
751
|
+
) -> list[dict[str, Any]]:
|
|
752
|
+
"""Extract error details from a GraphQL payload."""
|
|
753
|
+
return self._issue_delete_service.extract_graphql_error_details(payload)
|
|
754
|
+
|
|
755
|
+
def _post_graphql_with_backoff(
|
|
756
|
+
self,
|
|
757
|
+
query: str,
|
|
758
|
+
token: str,
|
|
759
|
+
operation: str,
|
|
760
|
+
max_attempts: int = 3,
|
|
761
|
+
delay: float = 2.0,
|
|
762
|
+
backoff: float = 2.0,
|
|
763
|
+
) -> dict[str, Any] | None:
|
|
764
|
+
"""Post GraphQL with backoff when resource limits are hit."""
|
|
765
|
+
return self._issue_delete_service.post_graphql_with_backoff(
|
|
766
|
+
query=query,
|
|
767
|
+
token=token,
|
|
768
|
+
operation=operation,
|
|
769
|
+
max_attempts=max_attempts,
|
|
770
|
+
delay=delay,
|
|
771
|
+
backoff=backoff,
|
|
772
|
+
post_graphql_fn=self._post_graphql,
|
|
773
|
+
sleep_fn=time.sleep,
|
|
774
|
+
)
|
|
775
|
+
|
|
776
|
+
def _post_graphql(self, query: str, token: str) -> dict[str, Any] | None:
|
|
777
|
+
"""Post a GraphQL query to GitHub and return JSON data."""
|
|
778
|
+
return self._issue_delete_service.post_graphql(query, token)
|
|
779
|
+
|
|
780
|
+
@staticmethod
|
|
781
|
+
def _dict_to_sync_issue(issue_dict: dict[str, Any]) -> SyncIssue:
|
|
782
|
+
"""Convert a raw GitHub API issue dict to SyncIssue.
|
|
783
|
+
|
|
784
|
+
Args:
|
|
785
|
+
issue_dict: Dict with issue data from GitHub API
|
|
786
|
+
|
|
787
|
+
Returns:
|
|
788
|
+
SyncIssue instance
|
|
789
|
+
"""
|
|
790
|
+
# Delegate to helpers for consistent conversion
|
|
791
|
+
helpers = GitHubBackendHelpers(core=None)
|
|
792
|
+
return helpers._dict_to_sync_issue(issue_dict)
|