@ngocsangairvds/vsaf 3.1.27 → 3.2.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/global.js +65 -39
- package/tools/skills/vds-scripts-skill/.openskills.json +6 -0
- package/tools/skills/vds-scripts-skill/QUALITY.md +44 -0
- package/tools/skills/vds-scripts-skill/SKILL.md +135 -0
- package/tools/skills/vds-scripts-skill/references/audit-commands.md +171 -0
- package/tools/skills/vds-scripts-skill/references/capability-index.md +34 -0
- package/tools/skills/vds-scripts-skill/references/development-commands.md +12 -0
- package/tools/skills/vds-scripts-skill/references/google-sheets.md +73 -0
- package/tools/skills/vds-scripts-skill/references/integration-commands.md +17 -0
- package/tools/skills/vds-scripts-skill/references/platform-bootstrap.md +31 -0
- package/tools/skills/vds-scripts-skill/references/specialist-routing.md +14 -0
- package/tools/skills/vds-scripts-skill/references/validation-commands.md +15 -0
- package/tools/skills/vsaf-build/SKILL.md +32 -2
- package/tools/skills/vsaf-ship/SKILL.md +41 -10
- package/tools/skills/vsaf-test/SKILL.md +8 -0
- package/tools/vds-scripts/.mcp.json +11 -0
- package/tools/vds-scripts/.secrets.baseline +133 -0
- package/tools/vds-scripts/AGENTS.md +152 -0
- package/tools/vds-scripts/CLAUDE.md +101 -0
- package/tools/vds-scripts/CLI_COMMAND_OPTIMIZATION.md +156 -0
- package/tools/vds-scripts/PACKAGE_P125B_IMPLEMENTATION_SUMMARY.md +131 -0
- package/tools/vds-scripts/PROJECT_COMPLETION_SUMMARY.md +45 -0
- package/tools/vds-scripts/README.md +97 -0
- package/tools/vds-scripts/bitbucket_manifest_mapping.toml +34 -0
- package/tools/vds-scripts/bitbucket_orchestrator/ARCHITECTURE_ANALYSIS.md +258 -0
- package/tools/vds-scripts/bitbucket_orchestrator/BITBUCKET_API_PRACTICES.md +393 -0
- package/tools/vds-scripts/bitbucket_orchestrator/EVALUATION_REPORT.md +61 -0
- package/tools/vds-scripts/bitbucket_orchestrator/FEATURES.md +908 -0
- package/tools/vds-scripts/bitbucket_orchestrator/README.md +687 -0
- package/tools/vds-scripts/bitbucket_orchestrator/pyproject.toml +40 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/__init__.py +20 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/async_client.py +657 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/cli.py +2108 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/client.py +2534 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/config.py +171 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/errors.py +67 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/factory.py +185 -0
- package/tools/vds-scripts/bitbucket_orchestrator/src/vds_bitbucket_orchestrator/protocols.py +244 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/__init__.py +8 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/conftest.py +65 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_advanced_search.py +151 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_async_client.py +546 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_branch_permissions.py +145 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_cli.py +115 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client.py +157 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_branch_conditions.py +79 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_advanced.py +163 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_code_file.py +32 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_deployment_environments.py +194 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_issues.py +164 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pipelines_advanced.py +179 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_pr_blockers.py +119 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_client_repository_variables.py +156 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code.py +98 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_advanced.py +282 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_code_insights.py +335 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_conditions.py +147 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_config.py +131 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_deployment_env.py +352 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_factory.py +371 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_fork_operations.py +204 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_issue_cli.py +261 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pipeline_advanced.py +270 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_pr_blocker.py +204 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_protocols.py +334 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_settings.py +343 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_repo_variables.py +270 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_webhooks.py +189 -0
- package/tools/vds-scripts/bitbucket_orchestrator/tests/test_workspace.py +233 -0
- package/tools/vds-scripts/bitbucket_orchestrator/uv.lock +742 -0
- package/tools/vds-scripts/confluence_orchestrator/Dockerfile +19 -0
- package/tools/vds-scripts/confluence_orchestrator/README.md +412 -0
- package/tools/vds-scripts/confluence_orchestrator/SYNC_SCRIPTS.md +127 -0
- package/tools/vds-scripts/confluence_orchestrator/SYNC_STANDARDIZATION.md +108 -0
- package/tools/vds-scripts/confluence_orchestrator/pyproject.toml +48 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/__init__.py +20 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/cli.py +2532 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/config.py +175 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content.py +290 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/content_v2.py +94 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/crawl_tree.py +1835 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/errors.py +80 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/eventing.py +109 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/http.py +1114 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/orchestration.py +165 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/reporting.py +78 -0
- package/tools/vds-scripts/confluence_orchestrator/src/confluence_orchestrator/tree.py +121 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_from_markdown.py +213 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_pdfs_to_confluence.py +305 -0
- package/tools/vds-scripts/confluence_orchestrator/sync_png_attachments.py +305 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/__init__.py +0 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/conftest.py +8 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_content.py +224 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_advanced_search.py +188 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_cache_management.py +247 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_cli.py +499 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_config.py +83 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_content.py +186 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_content_flags.py +27 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_crawl_tree.py +2250 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_draft_management.py +223 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing.py +71 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_chaos.py +37 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_rate_limit.py +44 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_eventing_timeout.py +49 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_export.py +230 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_history.py +204 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_http.py +117 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_orchestration.py +91 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_reporting.py +24 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_search_cql.py +34 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_space_management.py +237 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_space_permissions.py +332 -0
- package/tools/vds-scripts/confluence_orchestrator/tests/test_user_group_management.py +388 -0
- package/tools/vds-scripts/confluence_orchestrator/uv.lock +1023 -0
- package/tools/vds-scripts/git_orchestrator/ENHANCEMENT_SUMMARY.md +119 -0
- package/tools/vds-scripts/git_orchestrator/README.md +280 -0
- package/tools/vds-scripts/git_orchestrator/VERIFICATION_REPORT.md +152 -0
- package/tools/vds-scripts/git_orchestrator/pyproject.toml +35 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__init__.py +7 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/__main__.py +4 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/cli.py +847 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/logging_config.py +63 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/manifest.py +129 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/orchestrator.py +819 -0
- package/tools/vds-scripts/git_orchestrator/src/vds_git_orchestrator/reporting.py +53 -0
- package/tools/vds-scripts/git_orchestrator/tests/__init__.py +0 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_cli_settings.py +21 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_integration.py +74 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_manifest.py +79 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_orchestrator.py +204 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_public_api.py +236 -0
- package/tools/vds-scripts/git_orchestrator/tests/test_resilience.py +345 -0
- package/tools/vds-scripts/git_orchestrator/uv.lock +271 -0
- package/tools/vds-scripts/jira_orchestrator/README.md +770 -0
- package/tools/vds-scripts/jira_orchestrator/pyproject.toml +39 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/__init__.py +1 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/adapter.py +1320 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/cli.py +2271 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/config.py +138 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/errors.py +67 -0
- package/tools/vds-scripts/jira_orchestrator/src/vds_jira_orchestrator/reporting.py +65 -0
- package/tools/vds-scripts/jira_orchestrator/tests/__init__.py +1 -0
- package/tools/vds-scripts/jira_orchestrator/tests/conftest.py +86 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_agile_list_payloads.py +54 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_bulk_operations.py +69 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_components.py +57 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_createmeta.py +45 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_dashboard.py +117 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_issue_properties.py +54 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_permissions_compat.py +42 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_reindex.py +42 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_remote_links.py +76 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_transitions.py +91 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_user_management.py +110 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_version_management.py +133 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_adapter_watchers.py +41 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_advanced_search.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_agile.py +256 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_application_properties.py +193 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_backlog.py +91 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_bulk_operations.py +277 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_cli.py +106 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_components.py +106 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_config.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_dashboard.py +122 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_discover_fields.py +207 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_filter_management.py +333 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_archiving.py +164 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_links.py +257 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_issue_properties.py +171 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_link_types.py +314 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_parse_set.py +37 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_permissions.py +273 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_reindex.py +81 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_remote_links.py +254 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_security_schemes.py +170 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_transitions_changelog.py +114 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_user_management.py +226 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_version_management.py +339 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_watchers.py +101 -0
- package/tools/vds-scripts/jira_orchestrator/tests/test_worklog.py +223 -0
- package/tools/vds-scripts/jira_orchestrator/uv.lock +738 -0
- package/tools/vds-scripts/mcp_server/Dockerfile +34 -0
- package/tools/vds-scripts/mcp_server/README.md +140 -0
- package/tools/vds-scripts/mcp_server/pyproject.toml +42 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/__init__.py +4 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/config.py +36 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/server.py +66 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/__init__.py +14 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/bitbucket_tools.py +47 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/confluence_tools.py +59 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/git_tools.py +71 -0
- package/tools/vds-scripts/mcp_server/src/vds_mcp_server/tools/jira_tools.py +63 -0
- package/tools/vds-scripts/mcp_server/tests/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/conftest.py +29 -0
- package/tools/vds-scripts/mcp_server/tests/unit/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_bitbucket_tools.py +25 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_confluence_tools.py +25 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_git_tools.py +32 -0
- package/tools/vds-scripts/mcp_server/tests/unit/test_jira_tools.py +32 -0
- package/tools/vds-scripts/mcp_server/tests/verification/__init__.py +2 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_confluence_tools.py +40 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_jira_tools.py +37 -0
- package/tools/vds-scripts/mcp_server/tests/verification/test_mcp_tool_registration.py +47 -0
- package/tools/vds-scripts/mcp_server/uv.lock +1032 -0
- package/tools/vds-scripts/mypy.ini +5 -0
- package/tools/vds-scripts/pyproject.toml +29 -0
- package/tools/vds-scripts/repo-manifest.yaml +273 -0
- package/tools/vds-scripts/repo-manifest.yaml.example +25 -0
- package/tools/vds-scripts/scripts/BRD-Validation-API.postman_collection.json +706 -0
- package/tools/vds-scripts/scripts/BRD-Validation-README.md +308 -0
- package/tools/vds-scripts/scripts/README.md +162 -0
- package/tools/vds-scripts/scripts/bootstrap_uv.sh +30 -0
- package/tools/vds-scripts/scripts/brd-validation-environment.json +51 -0
- package/tools/vds-scripts/scripts/brd-validation-test-results.json +13023 -0
- package/tools/vds-scripts/scripts/brd_coverage_report.json +276 -0
- package/tools/vds-scripts/scripts/create_memory_session.py +35 -0
- package/tools/vds-scripts/scripts/deployment/load_docker_images_offline.sh +90 -0
- package/tools/vds-scripts/scripts/final_completion_report.md +139 -0
- package/tools/vds-scripts/scripts/folder_structure_report.json +321 -0
- package/tools/vds-scripts/scripts/generate_completion_report.py +125 -0
- package/tools/vds-scripts/scripts/generate_intellij_modules.py +150 -0
- package/tools/vds-scripts/scripts/link_integrity_report.json +807 -0
- package/tools/vds-scripts/scripts/move_audit_artifact_pages.py +255 -0
- package/tools/vds-scripts/scripts/move_audit_artifact_pages_rest.py +165 -0
- package/tools/vds-scripts/scripts/move_wrong_dept_pages.py +216 -0
- package/tools/vds-scripts/scripts/save_intellij_memories.py +120 -0
- package/tools/vds-scripts/scripts/save_memories_to_vds_ai.py +83 -0
- package/tools/vds-scripts/scripts/save_memories_vds_style.py +129 -0
- package/tools/vds-scripts/scripts/search_intellij_memories.py +50 -0
- package/tools/vds-scripts/scripts/setup_intellij_workspace.py +65 -0
- package/tools/vds-scripts/scripts/target-state-automation/README.md +89 -0
- package/tools/vds-scripts/scripts/target-state-automation/confluence_sync_coordinator.sh +27 -0
- package/tools/vds-scripts/scripts/target-state-automation/coordination.sh +114 -0
- package/tools/vds-scripts/scripts/target-state-automation/diagram_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/docs_root.sh +22 -0
- package/tools/vds-scripts/scripts/target-state-automation/generate_diagrams.sh +22 -0
- package/tools/vds-scripts/scripts/target-state-automation/markdown_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/progress_dashboard.sh +17 -0
- package/tools/vds-scripts/scripts/target-state-automation/schema_coordinator.sh +25 -0
- package/tools/vds-scripts/scripts/target-state-automation/sync_confluence.sh +30 -0
- package/tools/vds-scripts/scripts/target-state-automation/update_dependencies.sh +19 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_links.sh +86 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_markdown.sh +52 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_schemas.sh +26 -0
- package/tools/vds-scripts/scripts/target-state-automation/validate_structure.sh +98 -0
- package/tools/vds-scripts/scripts/update_modules_xml.py +190 -0
- package/tools/vds-scripts/scripts/uv-workspace-alignment-verification-2026-03-25.md +128 -0
- package/tools/vds-scripts/scripts/validate_brd_coverage.py +179 -0
- package/tools/vds-scripts/scripts/validate_folder_structure.py +240 -0
- package/tools/vds-scripts/scripts/validate_link_integrity.py +272 -0
- package/tools/vds-scripts/scripts/vds_sh_helpers.sh +180 -0
- package/tools/vds-scripts/scripts/verification/phase2_portable_paths_ubuntu_docker.sh +26 -0
- package/tools/vds-scripts/scripts/worktree_uv.sh +48 -0
- package/tools/vds-scripts/uv.lock +8 -0
- package/tools/vds-scripts/vds_cli/README.md +126 -0
- package/tools/vds-scripts/vds_cli/VERIFICATION_REPORT.md +41 -0
- package/tools/vds-scripts/vds_cli/pyproject.toml +38 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/__init__.py +3 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/cli.py +173 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/docs_sync.py +1203 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/env.py +41 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/__init__.py +3 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/google_sheets_orchestrator/google_sheets_orchestrator.py +198 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/router.py +93 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/sync_api.py +647 -0
- package/tools/vds-scripts/vds_cli/src/vds_cli/sync_service.py +266 -0
- package/tools/vds-scripts/vds_cli/tests/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/conftest.py +49 -0
- package/tools/vds-scripts/vds_cli/tests/unit/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_cli.py +143 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_docs_sync.py +422 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_env.py +51 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_router.py +72 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_sync_api.py +357 -0
- package/tools/vds-scripts/vds_cli/tests/unit/test_sync_service.py +160 -0
- package/tools/vds-scripts/vds_cli/tests/verification/__init__.py +2 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_bitbucket_real.py +33 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_confluence_real.py +35 -0
- package/tools/vds-scripts/vds_cli/tests/verification/test_jira_real.py +41 -0
- package/tools/vds-scripts/vds_cli/uv.lock +524 -0
- package/tools/vds-scripts/vds_cli_common/README.md +190 -0
- package/tools/vds-scripts/vds_cli_common/pyproject.toml +92 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/__init__.py +34 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/completers.py +139 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/context.py +201 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/env.py +119 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/errors.py +318 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/output.py +284 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/paths.py +78 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/testing.py +213 -0
- package/tools/vds-scripts/vds_cli_common/src/vds_cli_common/version.py +85 -0
- package/tools/vds-scripts/vds_cli_common/tests/__init__.py +1 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_completers.py +148 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_context.py +192 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_env.py +102 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_errors.py +186 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_output.py +229 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_paths.py +61 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_testing.py +138 -0
- package/tools/vds-scripts/vds_cli_common/tests/test_version.py +64 -0
|
@@ -0,0 +1,2534 @@
|
|
|
1
|
+
"""Bitbucket client wrapper using atlassian-python-api for consistency."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import time
|
|
6
|
+
from functools import wraps
|
|
7
|
+
from typing import Any
|
|
8
|
+
|
|
9
|
+
import structlog
|
|
10
|
+
from atlassian import Bitbucket as AtlassianBitbucket
|
|
11
|
+
|
|
12
|
+
from .config import BitbucketSettings
|
|
13
|
+
from .errors import (
|
|
14
|
+
BitbucketAuthError,
|
|
15
|
+
BitbucketClientError,
|
|
16
|
+
BitbucketConflictError,
|
|
17
|
+
BitbucketNotFoundError,
|
|
18
|
+
BitbucketRateLimitedError,
|
|
19
|
+
BitbucketResponseError,
|
|
20
|
+
BitbucketRetryError,
|
|
21
|
+
BitbucketTransportError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def retry_with_backoff(max_retries: int = 3, backoff_factor: float = 1.0):
|
|
26
|
+
"""Decorator for retrying operations with exponential backoff."""
|
|
27
|
+
|
|
28
|
+
def decorator(func):
|
|
29
|
+
@wraps(func)
|
|
30
|
+
def wrapper(*args, **kwargs):
|
|
31
|
+
log = structlog.get_logger(__name__)
|
|
32
|
+
for attempt in range(max_retries + 1):
|
|
33
|
+
try:
|
|
34
|
+
return func(*args, **kwargs)
|
|
35
|
+
except (BitbucketTransportError, BitbucketRateLimitedError) as exc:
|
|
36
|
+
if attempt == max_retries:
|
|
37
|
+
log.error(
|
|
38
|
+
"retry_attempts_exhausted",
|
|
39
|
+
function=func.__name__,
|
|
40
|
+
attempts=max_retries,
|
|
41
|
+
final_error=str(exc),
|
|
42
|
+
)
|
|
43
|
+
raise BitbucketRetryError(
|
|
44
|
+
f"Retry attempts exhausted: {exc}",
|
|
45
|
+
attempts=max_retries,
|
|
46
|
+
context={"original_error": str(exc), "function": func.__name__},
|
|
47
|
+
) from exc
|
|
48
|
+
|
|
49
|
+
wait_time = backoff_factor * (2**attempt)
|
|
50
|
+
log.warning(
|
|
51
|
+
"retry_attempt",
|
|
52
|
+
function=func.__name__,
|
|
53
|
+
attempt=attempt + 1,
|
|
54
|
+
max_retries=max_retries,
|
|
55
|
+
wait_time=wait_time,
|
|
56
|
+
error=str(exc),
|
|
57
|
+
)
|
|
58
|
+
time.sleep(wait_time)
|
|
59
|
+
except BitbucketClientError:
|
|
60
|
+
# Don't retry on client errors (auth, not found, etc.)
|
|
61
|
+
raise
|
|
62
|
+
return None
|
|
63
|
+
|
|
64
|
+
return wrapper
|
|
65
|
+
|
|
66
|
+
return decorator
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class BitbucketClient:
|
|
70
|
+
"""Bitbucket client wrapper using atlassian-python-api SDK."""
|
|
71
|
+
|
|
72
|
+
def __init__(
|
|
73
|
+
self,
|
|
74
|
+
settings: BitbucketSettings,
|
|
75
|
+
*,
|
|
76
|
+
timeout: int | None = 30,
|
|
77
|
+
) -> None:
|
|
78
|
+
self._settings = settings
|
|
79
|
+
self._timeout = timeout or 30
|
|
80
|
+
|
|
81
|
+
# Validate credentials
|
|
82
|
+
settings.require_basic_or_token()
|
|
83
|
+
|
|
84
|
+
try:
|
|
85
|
+
auth_kwargs = settings.get_auth_kwargs()
|
|
86
|
+
url = settings.get_url()
|
|
87
|
+
|
|
88
|
+
if settings.is_cloud_mode:
|
|
89
|
+
# For Bitbucket Cloud, we need to use the cloud API
|
|
90
|
+
self._client = AtlassianBitbucket(
|
|
91
|
+
url=url, timeout=self._timeout, cloud=True, workspace=settings.cloud_workspace, **auth_kwargs
|
|
92
|
+
)
|
|
93
|
+
else:
|
|
94
|
+
# For Bitbucket Server/Data Center
|
|
95
|
+
self._client = AtlassianBitbucket(url=url, timeout=self._timeout, **auth_kwargs)
|
|
96
|
+
|
|
97
|
+
except Exception as exc:
|
|
98
|
+
raise BitbucketTransportError(
|
|
99
|
+
f"Failed to initialize Bitbucket client: {exc}",
|
|
100
|
+
context={"mode": settings.mode, "url": settings.get_url()},
|
|
101
|
+
) from exc
|
|
102
|
+
|
|
103
|
+
self._log = structlog.get_logger(__name__).bind(mode=settings.mode, url=settings.get_url())
|
|
104
|
+
|
|
105
|
+
# Store retry settings
|
|
106
|
+
self._max_retries = settings.max_retries
|
|
107
|
+
self._retry_backoff_factor = settings.retry_backoff_factor
|
|
108
|
+
|
|
109
|
+
def _with_retry(self, operation, operation_name: str):
|
|
110
|
+
"""Execute an operation with retry logic using instance settings."""
|
|
111
|
+
for attempt in range(self._max_retries + 1):
|
|
112
|
+
try:
|
|
113
|
+
result = operation()
|
|
114
|
+
if operation_name == "list_projects":
|
|
115
|
+
self._log.debug("projects_listed", count=len(result) if result else 0)
|
|
116
|
+
elif operation_name == "list_repositories":
|
|
117
|
+
self._log.debug("repositories_listed", project_key="unknown", count=len(result) if result else 0)
|
|
118
|
+
elif operation_name == "list_pull_requests":
|
|
119
|
+
self._log.debug(
|
|
120
|
+
"pull_requests_listed",
|
|
121
|
+
project_key="unknown",
|
|
122
|
+
repository="unknown",
|
|
123
|
+
count=len(result) if result else 0,
|
|
124
|
+
)
|
|
125
|
+
elif operation_name == "list_pipelines":
|
|
126
|
+
self._log.debug(
|
|
127
|
+
"pipelines_listed",
|
|
128
|
+
workspace="unknown",
|
|
129
|
+
repository="unknown",
|
|
130
|
+
count=len(result) if result else 0,
|
|
131
|
+
)
|
|
132
|
+
return result or []
|
|
133
|
+
except Exception as exc:
|
|
134
|
+
try:
|
|
135
|
+
self._handle_api_error(exc, operation_name)
|
|
136
|
+
except (BitbucketTransportError, BitbucketRateLimitedError) as retry_exc:
|
|
137
|
+
if attempt == self._max_retries:
|
|
138
|
+
self._log.error(
|
|
139
|
+
"retry_attempts_exhausted",
|
|
140
|
+
operation=operation_name,
|
|
141
|
+
attempts=self._max_retries,
|
|
142
|
+
final_error=str(retry_exc),
|
|
143
|
+
)
|
|
144
|
+
raise BitbucketRetryError(
|
|
145
|
+
f"Retry attempts exhausted: {retry_exc}",
|
|
146
|
+
attempts=self._max_retries,
|
|
147
|
+
context={"original_error": str(retry_exc), "operation": operation_name},
|
|
148
|
+
) from retry_exc
|
|
149
|
+
|
|
150
|
+
wait_time = self._retry_backoff_factor * (2**attempt)
|
|
151
|
+
self._log.warning(
|
|
152
|
+
"retry_attempt",
|
|
153
|
+
operation=operation_name,
|
|
154
|
+
attempt=attempt + 1,
|
|
155
|
+
max_retries=self._max_retries,
|
|
156
|
+
wait_time=wait_time,
|
|
157
|
+
error=str(retry_exc),
|
|
158
|
+
)
|
|
159
|
+
time.sleep(wait_time)
|
|
160
|
+
except BitbucketClientError:
|
|
161
|
+
# Don't retry on client errors (auth, not found, etc.)
|
|
162
|
+
raise
|
|
163
|
+
|
|
164
|
+
def _handle_api_error(self, error: Exception, operation: str) -> None:
|
|
165
|
+
"""Convert atlassian-python-api errors to our standardized errors."""
|
|
166
|
+
error_str = str(error).lower()
|
|
167
|
+
|
|
168
|
+
if "unauthorized" in error_str or "authentication" in error_str or "401" in error_str:
|
|
169
|
+
raise BitbucketAuthError(f"Authentication failed during {operation}", context={"error": str(error)})
|
|
170
|
+
elif "forbidden" in error_str or "403" in error_str:
|
|
171
|
+
raise BitbucketAuthError(f"Access forbidden during {operation}", context={"error": str(error)})
|
|
172
|
+
elif "not found" in error_str or "404" in error_str:
|
|
173
|
+
raise BitbucketNotFoundError(f"Resource not found during {operation}", context={"error": str(error)})
|
|
174
|
+
elif "rate limit" in error_str or "429" in error_str:
|
|
175
|
+
raise BitbucketRateLimitedError(f"Rate limited during {operation}", context={"error": str(error)})
|
|
176
|
+
elif "conflict" in error_str or "409" in error_str:
|
|
177
|
+
raise BitbucketConflictError(f"Conflict during {operation}", context={"error": str(error)})
|
|
178
|
+
elif "server error" in error_str or "500" in error_str:
|
|
179
|
+
raise BitbucketResponseError(f"Server error during {operation}", context={"error": str(error)})
|
|
180
|
+
else:
|
|
181
|
+
raise BitbucketClientError(f"API error during {operation}: {error}", context={"error": str(error)})
|
|
182
|
+
|
|
183
|
+
# Project Management
|
|
184
|
+
def list_projects(self, limit: int = 25) -> list[dict[str, Any]]:
|
|
185
|
+
"""List all projects."""
|
|
186
|
+
return self._with_retry(lambda: list(self._client.project_list(limit=limit)), "list_projects")
|
|
187
|
+
|
|
188
|
+
def get_project(self, project_key: str) -> dict[str, Any]:
|
|
189
|
+
"""Get project information."""
|
|
190
|
+
try:
|
|
191
|
+
result = self._client.project(project_key)
|
|
192
|
+
self._log.debug("project_retrieved", project_key=project_key)
|
|
193
|
+
return result
|
|
194
|
+
except Exception as exc:
|
|
195
|
+
self._handle_api_error(exc, f"get_project {project_key}")
|
|
196
|
+
|
|
197
|
+
def create_project(
|
|
198
|
+
self, key: str, name: str, description: str = "Project created via VDS orchestrator"
|
|
199
|
+
) -> dict[str, Any]:
|
|
200
|
+
"""Create a new project."""
|
|
201
|
+
try:
|
|
202
|
+
result = self._client.create_project(key, name, description)
|
|
203
|
+
self._log.info("project_created", key=key, name=name)
|
|
204
|
+
return result
|
|
205
|
+
except Exception as exc:
|
|
206
|
+
self._handle_api_error(exc, f"create_project {key}")
|
|
207
|
+
|
|
208
|
+
# Repository Management
|
|
209
|
+
def list_repositories(self, project_key: str, limit: int = 25) -> list[dict[str, Any]]:
|
|
210
|
+
"""List repositories in a project."""
|
|
211
|
+
return self._with_retry(
|
|
212
|
+
lambda: list(self._client.repo_list(project_key, limit=limit)), f"list_repositories {project_key}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
def get_repository(self, project_key: str, repository_slug: str) -> dict[str, Any]:
|
|
216
|
+
"""Get repository information."""
|
|
217
|
+
try:
|
|
218
|
+
result = self._client.get_repo(project_key, repository_slug)
|
|
219
|
+
self._log.debug("repository_retrieved", project_key=project_key, repository=repository_slug)
|
|
220
|
+
return result
|
|
221
|
+
except Exception as exc:
|
|
222
|
+
self._handle_api_error(exc, f"get_repository {project_key}/{repository_slug}")
|
|
223
|
+
|
|
224
|
+
def create_repository(
|
|
225
|
+
self, project_key: str, repository: str, forkable: bool = False, is_private: bool = True
|
|
226
|
+
) -> dict[str, Any]:
|
|
227
|
+
"""Create a new repository."""
|
|
228
|
+
try:
|
|
229
|
+
result = self._client.create_repo(project_key, repository, forkable=forkable, is_private=is_private)
|
|
230
|
+
self._log.info("repository_created", project_key=project_key, repository=repository)
|
|
231
|
+
return result
|
|
232
|
+
except Exception as exc:
|
|
233
|
+
self._handle_api_error(exc, f"create_repository {repository}")
|
|
234
|
+
|
|
235
|
+
def update_repository(
|
|
236
|
+
self, project_key: str, repository_slug: str, description: str | None = None
|
|
237
|
+
) -> dict[str, Any]:
|
|
238
|
+
"""Update repository information."""
|
|
239
|
+
try:
|
|
240
|
+
result = self._client.update_repo(project_key, repository_slug, description=description or "")
|
|
241
|
+
self._log.info("repository_updated", project_key=project_key, repository=repository_slug)
|
|
242
|
+
return result
|
|
243
|
+
except Exception as exc:
|
|
244
|
+
self._handle_api_error(exc, f"update_repository {repository_slug}")
|
|
245
|
+
|
|
246
|
+
def delete_repository(self, project_key: str, repository_slug: str) -> None:
|
|
247
|
+
"""Delete a repository (DANGER!).
|
|
248
|
+
|
|
249
|
+
Reference: https://atlassian-python-api.readthedocs.io/bitbucket.html#delete-a-repository-danger
|
|
250
|
+
"""
|
|
251
|
+
try:
|
|
252
|
+
self._client.delete_repo(project_key, repository_slug)
|
|
253
|
+
self._log.warning("repository_deleted", project_key=project_key, repository=repository_slug)
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
self._handle_api_error(exc, f"delete_repository {repository_slug}")
|
|
256
|
+
|
|
257
|
+
def get_repository_labels(self, project_key: str, repository_slug: str) -> list[str]:
|
|
258
|
+
"""Get labels for a repository."""
|
|
259
|
+
try:
|
|
260
|
+
result = self._client.get_repo_labels(project_key, repository_slug)
|
|
261
|
+
self._log.debug("repository_labels_retrieved", project_key=project_key, repository=repository_slug)
|
|
262
|
+
return result if isinstance(result, list) else []
|
|
263
|
+
except Exception as exc:
|
|
264
|
+
self._handle_api_error(exc, f"get_repository_labels {repository_slug}")
|
|
265
|
+
|
|
266
|
+
def fork_repository(
|
|
267
|
+
self, project_key: str, repository_slug: str, new_project_key: str | None = None, new_repository_slug: str | None = None
|
|
268
|
+
) -> dict[str, Any]:
|
|
269
|
+
"""Fork a repository.
|
|
270
|
+
|
|
271
|
+
Args:
|
|
272
|
+
project_key: Source project key
|
|
273
|
+
repository_slug: Source repository slug
|
|
274
|
+
new_project_key: Target project key (if forking to new project). If None, forks within same project.
|
|
275
|
+
new_repository_slug: Target repository slug. If None, uses same name as source.
|
|
276
|
+
"""
|
|
277
|
+
try:
|
|
278
|
+
if new_project_key:
|
|
279
|
+
# Fork to new project
|
|
280
|
+
if not new_repository_slug:
|
|
281
|
+
new_repository_slug = repository_slug
|
|
282
|
+
result = self._client.fork_repository_new_project( # type: ignore[attr-defined]
|
|
283
|
+
project_key, repository_slug, new_project_key, new_repository_slug
|
|
284
|
+
)
|
|
285
|
+
self._log.info(
|
|
286
|
+
"repository_forked_to_new_project",
|
|
287
|
+
source_project=project_key,
|
|
288
|
+
source_repo=repository_slug,
|
|
289
|
+
target_project=new_project_key,
|
|
290
|
+
target_repo=new_repository_slug,
|
|
291
|
+
)
|
|
292
|
+
else:
|
|
293
|
+
# Fork within same project
|
|
294
|
+
if not new_repository_slug:
|
|
295
|
+
new_repository_slug = f"{repository_slug}-fork"
|
|
296
|
+
result = self._client.fork_repository(project_key, repository_slug, new_repository_slug) # type: ignore[attr-defined]
|
|
297
|
+
self._log.info(
|
|
298
|
+
"repository_forked",
|
|
299
|
+
project_key=project_key,
|
|
300
|
+
source_repo=repository_slug,
|
|
301
|
+
target_repo=new_repository_slug,
|
|
302
|
+
)
|
|
303
|
+
return result
|
|
304
|
+
except Exception as exc:
|
|
305
|
+
self._handle_api_error(exc, f"fork_repository {project_key}/{repository_slug}")
|
|
306
|
+
|
|
307
|
+
def get_forked_repositories(self, project_key: str, repository_slug: str) -> list[dict[str, Any]]:
|
|
308
|
+
"""Get list of repositories forked from a source repository.
|
|
309
|
+
|
|
310
|
+
Note: SDK doesn't have a direct method for this. We use repository list and filter.
|
|
311
|
+
This may not be perfect but provides a way to discover forks.
|
|
312
|
+
"""
|
|
313
|
+
try:
|
|
314
|
+
# List all repositories in the project and filter for forks
|
|
315
|
+
# Note: This is a workaround - actual fork relationship may need to be checked via repository metadata
|
|
316
|
+
repos = self.list_repositories(project_key, limit=99999)
|
|
317
|
+
# Filter repositories that might be forks (this is heuristic-based)
|
|
318
|
+
# In practice, you may need to check repository origin or fork metadata
|
|
319
|
+
forks = [repo for repo in repos if repository_slug.lower() in repo.get("slug", "").lower()]
|
|
320
|
+
self._log.debug("forked_repositories_retrieved", project_key=project_key, source_repo=repository_slug, count=len(forks))
|
|
321
|
+
return forks
|
|
322
|
+
except Exception as exc:
|
|
323
|
+
self._handle_api_error(exc, f"get_forked_repositories {project_key}/{repository_slug}")
|
|
324
|
+
|
|
325
|
+
def set_repository_label(self, project_key: str, repository_slug: str, label_name: str) -> dict[str, Any]:
|
|
326
|
+
"""Set a label for a repository."""
|
|
327
|
+
try:
|
|
328
|
+
result = self._client.set_repo_label(project_key, repository_slug, label_name)
|
|
329
|
+
self._log.info(
|
|
330
|
+
"repository_label_set", project_key=project_key, repository=repository_slug, label=label_name
|
|
331
|
+
)
|
|
332
|
+
return result
|
|
333
|
+
except Exception as exc:
|
|
334
|
+
self._handle_api_error(exc, f"set_repository_label {repository_slug}")
|
|
335
|
+
|
|
336
|
+
# Pull Request Management
|
|
337
|
+
def list_pull_requests(
|
|
338
|
+
self,
|
|
339
|
+
project_key: str,
|
|
340
|
+
repository_slug: str,
|
|
341
|
+
state: str = "OPEN",
|
|
342
|
+
order: str = "newest",
|
|
343
|
+
limit: int = 100,
|
|
344
|
+
start: int = 0,
|
|
345
|
+
) -> list[dict[str, Any]]:
|
|
346
|
+
"""List pull requests.
|
|
347
|
+
|
|
348
|
+
Args:
|
|
349
|
+
project_key: Project key
|
|
350
|
+
repository_slug: Repository slug
|
|
351
|
+
state: PR state (OPEN, MERGED, DECLINED)
|
|
352
|
+
order: Sort order (newest, oldest)
|
|
353
|
+
limit: Maximum number of PRs to return
|
|
354
|
+
start: Start index for pagination
|
|
355
|
+
"""
|
|
356
|
+
return self._with_retry(
|
|
357
|
+
lambda: list(
|
|
358
|
+
self._client.get_pull_requests(
|
|
359
|
+
project_key, repository_slug, state=state, order=order, limit=limit, start=start
|
|
360
|
+
)
|
|
361
|
+
),
|
|
362
|
+
f"list_pull_requests {project_key}/{repository_slug}",
|
|
363
|
+
)
|
|
364
|
+
|
|
365
|
+
def create_pull_request(
|
|
366
|
+
self,
|
|
367
|
+
source_project: str,
|
|
368
|
+
source_repo: str,
|
|
369
|
+
dest_project: str,
|
|
370
|
+
dest_repo: str,
|
|
371
|
+
source_branch: str,
|
|
372
|
+
destination_branch: str,
|
|
373
|
+
title: str,
|
|
374
|
+
description: str,
|
|
375
|
+
reviewers: list[str] | None = None,
|
|
376
|
+
) -> dict[str, Any]:
|
|
377
|
+
"""Create a pull request."""
|
|
378
|
+
try:
|
|
379
|
+
kwargs = {}
|
|
380
|
+
if reviewers:
|
|
381
|
+
kwargs["reviewers"] = reviewers
|
|
382
|
+
|
|
383
|
+
result = self._client.open_pull_request(
|
|
384
|
+
source_project,
|
|
385
|
+
source_repo,
|
|
386
|
+
dest_project,
|
|
387
|
+
dest_repo,
|
|
388
|
+
source_branch,
|
|
389
|
+
destination_branch,
|
|
390
|
+
title,
|
|
391
|
+
description,
|
|
392
|
+
**kwargs,
|
|
393
|
+
)
|
|
394
|
+
self._log.info("pull_request_created", title=title, source=source_branch, dest=destination_branch)
|
|
395
|
+
return result
|
|
396
|
+
except Exception as exc:
|
|
397
|
+
self._handle_api_error(exc, f"create_pull_request {title}")
|
|
398
|
+
|
|
399
|
+
def get_pull_request(self, project_key: str, repository_slug: str, pull_request_id: str) -> dict[str, Any]:
|
|
400
|
+
"""Get a specific pull request."""
|
|
401
|
+
try:
|
|
402
|
+
# Get all PRs and find the one matching the ID
|
|
403
|
+
prs = self._client.get_pull_requests(project_key, repository_slug, limit=1000)
|
|
404
|
+
for pr in prs:
|
|
405
|
+
if str(pr.get("id")) == str(pull_request_id):
|
|
406
|
+
self._log.debug(
|
|
407
|
+
"pull_request_retrieved",
|
|
408
|
+
project_key=project_key,
|
|
409
|
+
repository=repository_slug,
|
|
410
|
+
pr_id=pull_request_id,
|
|
411
|
+
)
|
|
412
|
+
return pr
|
|
413
|
+
raise BitbucketNotFoundError(
|
|
414
|
+
f"Pull request {pull_request_id} not found",
|
|
415
|
+
context={"project_key": project_key, "repository_slug": repository_slug},
|
|
416
|
+
)
|
|
417
|
+
except Exception as exc:
|
|
418
|
+
self._handle_api_error(exc, f"get_pull_request {pull_request_id}")
|
|
419
|
+
|
|
420
|
+
def add_pull_request_comment(
|
|
421
|
+
self, project_key: str, repository_slug: str, pull_request_id: str, text: str, parent_id: str | None = None
|
|
422
|
+
) -> dict[str, Any]:
|
|
423
|
+
"""Add a comment to a pull request.
|
|
424
|
+
|
|
425
|
+
Args:
|
|
426
|
+
project_key: Project key
|
|
427
|
+
repository_slug: Repository slug
|
|
428
|
+
pull_request_id: Pull request ID
|
|
429
|
+
text: Comment text
|
|
430
|
+
parent_id: Optional parent comment ID for replies
|
|
431
|
+
"""
|
|
432
|
+
try:
|
|
433
|
+
result = self._client.add_pull_request_comment(
|
|
434
|
+
project_key, repository_slug, pull_request_id, text, parent_id=parent_id
|
|
435
|
+
)
|
|
436
|
+
self._log.info(
|
|
437
|
+
"pull_request_comment_added",
|
|
438
|
+
project_key=project_key,
|
|
439
|
+
repository=repository_slug,
|
|
440
|
+
pr_id=pull_request_id,
|
|
441
|
+
)
|
|
442
|
+
return result
|
|
443
|
+
except Exception as exc:
|
|
444
|
+
self._handle_api_error(exc, f"add_pull_request_comment {pull_request_id}")
|
|
445
|
+
|
|
446
|
+
def get_pull_request_activities(
|
|
447
|
+
self, project_key: str, repository_slug: str, pull_request_id: str
|
|
448
|
+
) -> list[dict[str, Any]]:
|
|
449
|
+
"""Get pull request activities."""
|
|
450
|
+
try:
|
|
451
|
+
result = self._client.get_pull_requests_activities(project_key, repository_slug, pull_request_id)
|
|
452
|
+
self._log.debug(
|
|
453
|
+
"pull_request_activities_retrieved",
|
|
454
|
+
project_key=project_key,
|
|
455
|
+
repository=repository_slug,
|
|
456
|
+
pr_id=pull_request_id,
|
|
457
|
+
)
|
|
458
|
+
return result if isinstance(result, list) else [result]
|
|
459
|
+
except Exception as exc:
|
|
460
|
+
self._handle_api_error(exc, f"get_pull_request_activities {pull_request_id}")
|
|
461
|
+
|
|
462
|
+
def get_pull_request_changes(
|
|
463
|
+
self, project_key: str, repository_slug: str, pull_request_id: str
|
|
464
|
+
) -> list[dict[str, Any]]:
|
|
465
|
+
"""Get pull request changes."""
|
|
466
|
+
try:
|
|
467
|
+
result = self._client.get_pull_requests_changes(project_key, repository_slug, pull_request_id)
|
|
468
|
+
self._log.debug(
|
|
469
|
+
"pull_request_changes_retrieved",
|
|
470
|
+
project_key=project_key,
|
|
471
|
+
repository=repository_slug,
|
|
472
|
+
pr_id=pull_request_id,
|
|
473
|
+
)
|
|
474
|
+
return result if isinstance(result, list) else [result]
|
|
475
|
+
except Exception as exc:
|
|
476
|
+
self._handle_api_error(exc, f"get_pull_request_changes {pull_request_id}")
|
|
477
|
+
|
|
478
|
+
def delete_pull_request(
|
|
479
|
+
self, project_key: str, repository_slug: str, pull_request_id: str, pull_request_version: int
|
|
480
|
+
) -> dict[str, Any]:
|
|
481
|
+
"""Delete a pull request."""
|
|
482
|
+
try:
|
|
483
|
+
result = self._client.delete_pull_request(project_key, repository_slug, pull_request_id, pull_request_version)
|
|
484
|
+
self._log.info(
|
|
485
|
+
"pull_request_deleted",
|
|
486
|
+
project_key=project_key,
|
|
487
|
+
repository=repository_slug,
|
|
488
|
+
pr_id=pull_request_id,
|
|
489
|
+
)
|
|
490
|
+
return result
|
|
491
|
+
except Exception as exc:
|
|
492
|
+
self._handle_api_error(exc, f"delete_pull_request {pull_request_id}")
|
|
493
|
+
|
|
494
|
+
def merge_pull_request(self, project_key: str, repository_slug: str, pull_request_id: str) -> dict[str, Any]:
|
|
495
|
+
"""Merge a pull request.
|
|
496
|
+
|
|
497
|
+
Note: This method attempts to use merge_pull_request if available in the SDK.
|
|
498
|
+
If it fails, consider using PR state transitions or activities API.
|
|
499
|
+
"""
|
|
500
|
+
try:
|
|
501
|
+
# Try the direct merge method first
|
|
502
|
+
if hasattr(self._client, "merge_pull_request"):
|
|
503
|
+
result = self._client.merge_pull_request(project_key, repository_slug, pull_request_id)
|
|
504
|
+
else:
|
|
505
|
+
# Fallback: Use PR activities to merge (may require different approach)
|
|
506
|
+
raise BitbucketClientError(
|
|
507
|
+
"merge_pull_request method not available in SDK. Use PR state transitions or activities API.",
|
|
508
|
+
context={"project_key": project_key, "repository_slug": repository_slug, "pr_id": pull_request_id},
|
|
509
|
+
)
|
|
510
|
+
self._log.info(
|
|
511
|
+
"pull_request_merged", project_key=project_key, repository=repository_slug, pr_id=pull_request_id
|
|
512
|
+
)
|
|
513
|
+
return result
|
|
514
|
+
except Exception as exc:
|
|
515
|
+
self._handle_api_error(exc, f"merge_pull_request {pull_request_id}")
|
|
516
|
+
|
|
517
|
+
# Pipeline Management (for Bitbucket Cloud)
|
|
518
|
+
def list_pipelines(self, workspace: str, repository: str, limit: int = 10) -> list[dict[str, Any]]:
|
|
519
|
+
"""List pipelines (Bitbucket Cloud only)."""
|
|
520
|
+
if not self._settings.is_cloud_mode:
|
|
521
|
+
raise BitbucketClientError("Pipelines are only available in Bitbucket Cloud mode")
|
|
522
|
+
|
|
523
|
+
return self._with_retry(
|
|
524
|
+
lambda: list(self._client.get_pipelines(workspace, repository, limit=limit)),
|
|
525
|
+
f"list_pipelines {workspace}/{repository}",
|
|
526
|
+
)
|
|
527
|
+
|
|
528
|
+
def trigger_pipeline(self, workspace: str, repository: str, branch: str = "master") -> dict[str, Any]:
|
|
529
|
+
"""Trigger a pipeline (Bitbucket Cloud only)."""
|
|
530
|
+
try:
|
|
531
|
+
if not self._settings.is_cloud_mode:
|
|
532
|
+
raise BitbucketClientError("Pipelines are only available in Bitbucket Cloud mode")
|
|
533
|
+
|
|
534
|
+
result = self._client.trigger_pipeline(workspace, repository, branch=branch)
|
|
535
|
+
self._log.info("pipeline_triggered", workspace=workspace, repository=repository, branch=branch)
|
|
536
|
+
return result
|
|
537
|
+
except Exception as exc:
|
|
538
|
+
self._handle_api_error(exc, f"trigger_pipeline {workspace}/{repository}")
|
|
539
|
+
|
|
540
|
+
# Branch Management
|
|
541
|
+
def get_branches(
|
|
542
|
+
self,
|
|
543
|
+
project_key: str,
|
|
544
|
+
repository_slug: str,
|
|
545
|
+
filter: str = "",
|
|
546
|
+
limit: int = 99999,
|
|
547
|
+
details: bool = True,
|
|
548
|
+
) -> list[dict[str, Any]]:
|
|
549
|
+
"""Get branches from a repository."""
|
|
550
|
+
try:
|
|
551
|
+
result = self._client.get_branches(
|
|
552
|
+
project_key, repository_slug, filter=filter, limit=limit, details=details
|
|
553
|
+
)
|
|
554
|
+
self._log.debug("branches_retrieved", project_key=project_key, repository=repository_slug)
|
|
555
|
+
return result if isinstance(result, list) else []
|
|
556
|
+
except Exception as exc:
|
|
557
|
+
self._handle_api_error(exc, f"get_branches {project_key}/{repository_slug}")
|
|
558
|
+
|
|
559
|
+
def create_branch(
|
|
560
|
+
self, project_key: str, repository_slug: str, name: str, start_point: str, message: str | None = None
|
|
561
|
+
) -> dict[str, Any]:
|
|
562
|
+
"""Create a branch."""
|
|
563
|
+
try:
|
|
564
|
+
result = self._client.create_branch(project_key, repository_slug, name, start_point, message or "")
|
|
565
|
+
self._log.info("branch_created", project_key=project_key, repository=repository_slug, branch=name)
|
|
566
|
+
return result
|
|
567
|
+
except Exception as exc:
|
|
568
|
+
self._handle_api_error(exc, f"create_branch {name}")
|
|
569
|
+
|
|
570
|
+
def delete_branch(
|
|
571
|
+
self, project_key: str, repository_slug: str, name: str, end_point: str | None = None
|
|
572
|
+
) -> dict[str, Any]:
|
|
573
|
+
"""Delete a branch."""
|
|
574
|
+
try:
|
|
575
|
+
result = self._client.delete_branch(project_key, repository_slug, name, end_point=end_point)
|
|
576
|
+
self._log.info("branch_deleted", project_key=project_key, repository=repository_slug, branch=name)
|
|
577
|
+
return result
|
|
578
|
+
except Exception as exc:
|
|
579
|
+
self._handle_api_error(exc, f"delete_branch {name}")
|
|
580
|
+
|
|
581
|
+
# Tag Management
|
|
582
|
+
def get_tags(
|
|
583
|
+
self, project_key: str, repository_slug: str, filter: str = "", limit: int = 99999
|
|
584
|
+
) -> list[dict[str, Any]]:
|
|
585
|
+
"""Get tags for a repository."""
|
|
586
|
+
try:
|
|
587
|
+
result = self._client.get_tags(project_key, repository_slug, filter=filter, limit=limit)
|
|
588
|
+
self._log.debug("tags_retrieved", project_key=project_key, repository=repository_slug)
|
|
589
|
+
return result if isinstance(result, list) else []
|
|
590
|
+
except Exception as exc:
|
|
591
|
+
self._handle_api_error(exc, f"get_tags {project_key}/{repository_slug}")
|
|
592
|
+
|
|
593
|
+
def set_tag(
|
|
594
|
+
self,
|
|
595
|
+
project_key: str,
|
|
596
|
+
repository_slug: str,
|
|
597
|
+
tag_name: str,
|
|
598
|
+
commit_revision: str,
|
|
599
|
+
description: str | None = None,
|
|
600
|
+
) -> dict[str, Any]:
|
|
601
|
+
"""Set a tag."""
|
|
602
|
+
try:
|
|
603
|
+
result = self._client.set_tag(
|
|
604
|
+
project_key, repository_slug, tag_name, commit_revision, description=description
|
|
605
|
+
)
|
|
606
|
+
self._log.info("tag_set", project_key=project_key, repository=repository_slug, tag=tag_name)
|
|
607
|
+
return result
|
|
608
|
+
except Exception as exc:
|
|
609
|
+
self._handle_api_error(exc, f"set_tag {tag_name}")
|
|
610
|
+
|
|
611
|
+
def delete_tag(self, project_key: str, repository_slug: str, tag_name: str) -> dict[str, Any]:
|
|
612
|
+
"""Delete a tag."""
|
|
613
|
+
try:
|
|
614
|
+
result = self._client.delete_tag(project_key, repository_slug, tag_name)
|
|
615
|
+
self._log.info("tag_deleted", project_key=project_key, repository=repository_slug, tag=tag_name)
|
|
616
|
+
return result
|
|
617
|
+
except Exception as exc:
|
|
618
|
+
self._handle_api_error(exc, f"delete_tag {tag_name}")
|
|
619
|
+
|
|
620
|
+
# User and Group Management
|
|
621
|
+
def list_groups(self, group_filter: str | None = None, limit: int = 25) -> list[dict[str, Any]]:
|
|
622
|
+
"""List all available groups.
|
|
623
|
+
|
|
624
|
+
Args:
|
|
625
|
+
group_filter: Optional filter string to search for specific groups
|
|
626
|
+
limit: Maximum number of groups to return
|
|
627
|
+
"""
|
|
628
|
+
try:
|
|
629
|
+
# Use get_groups with optional filter parameter
|
|
630
|
+
if group_filter:
|
|
631
|
+
result = list(self._client.get_groups(group_filter=group_filter, limit=limit))
|
|
632
|
+
else:
|
|
633
|
+
result = list(self._client.get_groups(limit=limit))
|
|
634
|
+
self._log.debug("groups_listed", count=len(result) if result else 0, filter=group_filter)
|
|
635
|
+
return result or []
|
|
636
|
+
except Exception as exc:
|
|
637
|
+
self._handle_api_error(exc, "list_groups")
|
|
638
|
+
|
|
639
|
+
def list_project_users(
|
|
640
|
+
self, project_key: str, filter_str: str | None = None, limit: int = 25
|
|
641
|
+
) -> list[dict[str, Any]]:
|
|
642
|
+
"""List users in a project.
|
|
643
|
+
|
|
644
|
+
Args:
|
|
645
|
+
project_key: Project key
|
|
646
|
+
filter_str: Optional filter string to search for specific users
|
|
647
|
+
limit: Maximum number of users to return
|
|
648
|
+
"""
|
|
649
|
+
try:
|
|
650
|
+
result = list(self._client.project_users(project_key, limit=limit, filter_str=filter_str))
|
|
651
|
+
self._log.debug(
|
|
652
|
+
"project_users_listed", project_key=project_key, count=len(result) if result else 0, filter=filter_str
|
|
653
|
+
)
|
|
654
|
+
return result or []
|
|
655
|
+
except Exception as exc:
|
|
656
|
+
self._handle_api_error(exc, f"list_project_users {project_key}")
|
|
657
|
+
|
|
658
|
+
def list_project_groups(
|
|
659
|
+
self, project_key: str, filter_str: str | None = None, limit: int = 25
|
|
660
|
+
) -> list[dict[str, Any]]:
|
|
661
|
+
"""List groups in a project.
|
|
662
|
+
|
|
663
|
+
Args:
|
|
664
|
+
project_key: Project key
|
|
665
|
+
filter_str: Optional filter string to search for specific groups
|
|
666
|
+
limit: Maximum number of groups to return
|
|
667
|
+
"""
|
|
668
|
+
try:
|
|
669
|
+
result = list(self._client.project_groups(project_key, limit=limit, filter_str=filter_str))
|
|
670
|
+
self._log.debug(
|
|
671
|
+
"project_groups_listed", project_key=project_key, count=len(result) if result else 0, filter=filter_str
|
|
672
|
+
)
|
|
673
|
+
return result or []
|
|
674
|
+
except Exception as exc:
|
|
675
|
+
self._handle_api_error(exc, f"list_project_groups {project_key}")
|
|
676
|
+
|
|
677
|
+
def grant_user_repository_permissions(
|
|
678
|
+
self, project_key: str, repository_slug: str, username: str, permission: str
|
|
679
|
+
) -> dict[str, Any]:
|
|
680
|
+
"""Grant repository permissions to a user."""
|
|
681
|
+
try:
|
|
682
|
+
result = self._client.repo_grant_user_permissions(project_key, repository_slug, username, permission)
|
|
683
|
+
self._log.info(
|
|
684
|
+
"user_repository_permissions_granted",
|
|
685
|
+
project_key=project_key,
|
|
686
|
+
repository_slug=repository_slug,
|
|
687
|
+
username=username,
|
|
688
|
+
permission=permission,
|
|
689
|
+
)
|
|
690
|
+
return result
|
|
691
|
+
except Exception as exc:
|
|
692
|
+
self._handle_api_error(
|
|
693
|
+
exc, f"grant_user_repository_permissions {project_key}/{repository_slug} for {username}"
|
|
694
|
+
)
|
|
695
|
+
|
|
696
|
+
def grant_user_project_permissions(
|
|
697
|
+
self, project_key: str, username: str, permission: str
|
|
698
|
+
) -> dict[str, Any]:
|
|
699
|
+
"""Grant project permissions to a user."""
|
|
700
|
+
try:
|
|
701
|
+
result = self._client.project_grant_user_permissions(project_key, username, permission)
|
|
702
|
+
self._log.info(
|
|
703
|
+
"user_project_permissions_granted", project_key=project_key, username=username, permission=permission
|
|
704
|
+
)
|
|
705
|
+
return result
|
|
706
|
+
except Exception as exc:
|
|
707
|
+
self._handle_api_error(exc, f"grant_user_project_permissions {project_key} for {username}")
|
|
708
|
+
|
|
709
|
+
def grant_group_project_permissions(
|
|
710
|
+
self, project_key: str, group_name: str, permission: str
|
|
711
|
+
) -> dict[str, Any]:
|
|
712
|
+
"""Grant project permissions to a group."""
|
|
713
|
+
try:
|
|
714
|
+
result = self._client.project_grant_group_permissions(project_key, group_name, permission)
|
|
715
|
+
self._log.info(
|
|
716
|
+
"group_project_permissions_granted",
|
|
717
|
+
project_key=project_key,
|
|
718
|
+
group=group_name,
|
|
719
|
+
permission=permission,
|
|
720
|
+
)
|
|
721
|
+
return result
|
|
722
|
+
except Exception as exc:
|
|
723
|
+
self._handle_api_error(exc, f"grant_group_project_permissions {project_key} for {group_name}")
|
|
724
|
+
|
|
725
|
+
def grant_group_repository_permissions(
|
|
726
|
+
self, project_key: str, repository_slug: str, group: str, permission: str
|
|
727
|
+
) -> dict[str, Any]:
|
|
728
|
+
"""Grant repository permissions to a group."""
|
|
729
|
+
try:
|
|
730
|
+
result = self._client.repo_grant_group_permissions(project_key, repository_slug, group, permission)
|
|
731
|
+
self._log.info(
|
|
732
|
+
"group_repository_permissions_granted",
|
|
733
|
+
project_key=project_key,
|
|
734
|
+
repository_slug=repository_slug,
|
|
735
|
+
group=group,
|
|
736
|
+
permission=permission,
|
|
737
|
+
)
|
|
738
|
+
return result
|
|
739
|
+
except Exception as exc:
|
|
740
|
+
self._handle_api_error(
|
|
741
|
+
exc, f"grant_group_repository_permissions {project_key}/{repository_slug} for {group}"
|
|
742
|
+
)
|
|
743
|
+
|
|
744
|
+
# Branch Permissions
|
|
745
|
+
def set_branches_permissions(
|
|
746
|
+
self,
|
|
747
|
+
project_key: str,
|
|
748
|
+
multiple_permissions: bool = False,
|
|
749
|
+
matcher_type: str | None = None,
|
|
750
|
+
matcher_value: str | None = None,
|
|
751
|
+
permission_type: str | None = None,
|
|
752
|
+
repository_slug: str | None = None,
|
|
753
|
+
except_users: list[str] | None = None,
|
|
754
|
+
except_groups: list[str] | None = None,
|
|
755
|
+
except_access_keys: list[str] | None = None,
|
|
756
|
+
start: int = 0,
|
|
757
|
+
limit: int = 25,
|
|
758
|
+
) -> dict[str, Any]:
|
|
759
|
+
"""Set branch permissions."""
|
|
760
|
+
try:
|
|
761
|
+
result = self._client.set_branches_permissions(
|
|
762
|
+
project_key,
|
|
763
|
+
multiple_permissions=multiple_permissions,
|
|
764
|
+
matcher_type=matcher_type,
|
|
765
|
+
matcher_value=matcher_value,
|
|
766
|
+
permission_type=permission_type,
|
|
767
|
+
repository_slug=repository_slug,
|
|
768
|
+
except_users=except_users or [],
|
|
769
|
+
except_groups=except_groups or [],
|
|
770
|
+
except_access_keys=except_access_keys or [],
|
|
771
|
+
start=start,
|
|
772
|
+
limit=limit,
|
|
773
|
+
)
|
|
774
|
+
self._log.info(
|
|
775
|
+
"branch_permissions_set",
|
|
776
|
+
project_key=project_key,
|
|
777
|
+
repository_slug=repository_slug,
|
|
778
|
+
permission_type=permission_type,
|
|
779
|
+
)
|
|
780
|
+
return result
|
|
781
|
+
except Exception as exc:
|
|
782
|
+
self._handle_api_error(exc, f"set_branches_permissions {project_key}")
|
|
783
|
+
|
|
784
|
+
def get_branch_permission(
|
|
785
|
+
self, project_key: str, permission_id: int, repository_slug: str | None = None
|
|
786
|
+
) -> dict[str, Any]:
|
|
787
|
+
"""Get a single branch permission by permission ID."""
|
|
788
|
+
try:
|
|
789
|
+
result = self._client.get_branch_permission(project_key, permission_id, repository_slug=repository_slug)
|
|
790
|
+
self._log.debug(
|
|
791
|
+
"branch_permission_retrieved",
|
|
792
|
+
project_key=project_key,
|
|
793
|
+
permission_id=permission_id,
|
|
794
|
+
repository_slug=repository_slug,
|
|
795
|
+
)
|
|
796
|
+
return result
|
|
797
|
+
except Exception as exc:
|
|
798
|
+
self._handle_api_error(exc, f"get_branch_permission {permission_id}")
|
|
799
|
+
|
|
800
|
+
def delete_branch_permission(
|
|
801
|
+
self, project_key: str, permission_id: int, repository_slug: str | None = None
|
|
802
|
+
) -> None:
|
|
803
|
+
"""Delete a single branch permission by permission ID."""
|
|
804
|
+
try:
|
|
805
|
+
self._client.delete_branch_permission(project_key, permission_id, repository_slug=repository_slug)
|
|
806
|
+
self._log.info(
|
|
807
|
+
"branch_permission_deleted",
|
|
808
|
+
project_key=project_key,
|
|
809
|
+
permission_id=permission_id,
|
|
810
|
+
repository_slug=repository_slug,
|
|
811
|
+
)
|
|
812
|
+
except Exception as exc:
|
|
813
|
+
self._handle_api_error(exc, f"delete_branch_permission {permission_id}")
|
|
814
|
+
|
|
815
|
+
# Webhook Management (Bitbucket Cloud only)
|
|
816
|
+
def list_webhooks(
|
|
817
|
+
self,
|
|
818
|
+
project_key: str,
|
|
819
|
+
repository_slug: str,
|
|
820
|
+
*,
|
|
821
|
+
event: str | None = None,
|
|
822
|
+
statistics: bool = False,
|
|
823
|
+
) -> list[dict[str, Any]]:
|
|
824
|
+
"""List repository webhooks."""
|
|
825
|
+
try:
|
|
826
|
+
result = self._client.get_webhooks(project_key, repository_slug, event=event, statistics=statistics)
|
|
827
|
+
self._log.debug(
|
|
828
|
+
"webhooks_listed",
|
|
829
|
+
project_key=project_key,
|
|
830
|
+
repository_slug=repository_slug,
|
|
831
|
+
event=event,
|
|
832
|
+
statistics=statistics,
|
|
833
|
+
count=len(result) if isinstance(result, list) else 0,
|
|
834
|
+
)
|
|
835
|
+
return result if isinstance(result, list) else [result] if result else []
|
|
836
|
+
except Exception as exc:
|
|
837
|
+
self._handle_api_error(exc, f"list_webhooks {project_key}/{repository_slug}")
|
|
838
|
+
|
|
839
|
+
def get_webhook(self, project_key: str, repository_slug: str, webhook_id: int) -> dict[str, Any]:
|
|
840
|
+
"""Retrieve a specific webhook."""
|
|
841
|
+
try:
|
|
842
|
+
result = self._client.get_webhook(project_key, repository_slug, webhook_id)
|
|
843
|
+
self._log.debug(
|
|
844
|
+
"webhook_retrieved",
|
|
845
|
+
project_key=project_key,
|
|
846
|
+
repository_slug=repository_slug,
|
|
847
|
+
webhook_id=webhook_id,
|
|
848
|
+
)
|
|
849
|
+
return result
|
|
850
|
+
except Exception as exc:
|
|
851
|
+
self._handle_api_error(exc, f"get_webhook {project_key}/{repository_slug}/{webhook_id}")
|
|
852
|
+
|
|
853
|
+
def create_webhook(
|
|
854
|
+
self,
|
|
855
|
+
project_key: str,
|
|
856
|
+
repository_slug: str,
|
|
857
|
+
name: str,
|
|
858
|
+
events: list[str],
|
|
859
|
+
webhook_url: str,
|
|
860
|
+
*,
|
|
861
|
+
active: bool = True,
|
|
862
|
+
secret: str | None = None,
|
|
863
|
+
) -> dict[str, Any]:
|
|
864
|
+
"""Create a repository webhook."""
|
|
865
|
+
try:
|
|
866
|
+
result = self._client.create_webhook(
|
|
867
|
+
project_key,
|
|
868
|
+
repository_slug,
|
|
869
|
+
name,
|
|
870
|
+
events,
|
|
871
|
+
webhook_url,
|
|
872
|
+
active,
|
|
873
|
+
secret=secret,
|
|
874
|
+
)
|
|
875
|
+
self._log.info(
|
|
876
|
+
"webhook_created",
|
|
877
|
+
project_key=project_key,
|
|
878
|
+
repository_slug=repository_slug,
|
|
879
|
+
name=name,
|
|
880
|
+
events=events,
|
|
881
|
+
active=active,
|
|
882
|
+
)
|
|
883
|
+
return result
|
|
884
|
+
except Exception as exc:
|
|
885
|
+
self._handle_api_error(exc, f"create_webhook {project_key}/{repository_slug}")
|
|
886
|
+
|
|
887
|
+
def update_webhook(
|
|
888
|
+
self,
|
|
889
|
+
project_key: str,
|
|
890
|
+
repository_slug: str,
|
|
891
|
+
webhook_id: int,
|
|
892
|
+
*,
|
|
893
|
+
name: str | None = None,
|
|
894
|
+
events: list[str] | None = None,
|
|
895
|
+
webhook_url: str | None = None,
|
|
896
|
+
active: bool | None = None,
|
|
897
|
+
secret: str | None = None,
|
|
898
|
+
) -> dict[str, Any]:
|
|
899
|
+
"""Update a repository webhook."""
|
|
900
|
+
try:
|
|
901
|
+
params: dict[str, Any] = {}
|
|
902
|
+
if name is not None:
|
|
903
|
+
params["name"] = name
|
|
904
|
+
if events is not None:
|
|
905
|
+
params["events"] = events
|
|
906
|
+
if webhook_url is not None:
|
|
907
|
+
params["url"] = webhook_url
|
|
908
|
+
if active is not None:
|
|
909
|
+
params["active"] = active
|
|
910
|
+
if secret is not None:
|
|
911
|
+
params["secret"] = secret
|
|
912
|
+
|
|
913
|
+
result = self._client.update_webhook(project_key, repository_slug, webhook_id, **params)
|
|
914
|
+
self._log.info(
|
|
915
|
+
"webhook_updated",
|
|
916
|
+
project_key=project_key,
|
|
917
|
+
repository_slug=repository_slug,
|
|
918
|
+
webhook_id=webhook_id,
|
|
919
|
+
updated_fields=list(params.keys()),
|
|
920
|
+
)
|
|
921
|
+
return result
|
|
922
|
+
except Exception as exc:
|
|
923
|
+
self._handle_api_error(exc, f"update_webhook {project_key}/{repository_slug}/{webhook_id}")
|
|
924
|
+
|
|
925
|
+
def delete_webhook(self, project_key: str, repository_slug: str, webhook_id: int) -> None:
|
|
926
|
+
"""Delete a repository webhook."""
|
|
927
|
+
try:
|
|
928
|
+
self._client.delete_webhook(project_key, repository_slug, webhook_id)
|
|
929
|
+
self._log.info(
|
|
930
|
+
"webhook_deleted",
|
|
931
|
+
project_key=project_key,
|
|
932
|
+
repository_slug=repository_slug,
|
|
933
|
+
webhook_id=webhook_id,
|
|
934
|
+
)
|
|
935
|
+
except Exception as exc:
|
|
936
|
+
self._handle_api_error(exc, f"delete_webhook {project_key}/{repository_slug}/{webhook_id}")
|
|
937
|
+
|
|
938
|
+
# Conditions-Reviewers Management
|
|
939
|
+
def get_project_conditions(self, project_key: str) -> list[dict[str, Any]]:
|
|
940
|
+
"""Get all project conditions with reviewers list."""
|
|
941
|
+
try:
|
|
942
|
+
result = self._client.get_project_conditions(project_key)
|
|
943
|
+
self._log.debug("project_conditions_retrieved", project_key=project_key, count=len(result) if result else 0)
|
|
944
|
+
return result if isinstance(result, list) else [result] if result else []
|
|
945
|
+
except Exception as exc:
|
|
946
|
+
self._handle_api_error(exc, f"get_project_conditions {project_key}")
|
|
947
|
+
|
|
948
|
+
def get_project_condition(self, project_key: str, condition_id: int) -> dict[str, Any]:
|
|
949
|
+
"""Get a project condition with reviewers list."""
|
|
950
|
+
try:
|
|
951
|
+
result = self._client.get_project_condition(project_key, condition_id)
|
|
952
|
+
self._log.debug("project_condition_retrieved", project_key=project_key, condition_id=condition_id)
|
|
953
|
+
return result
|
|
954
|
+
except Exception as exc:
|
|
955
|
+
self._handle_api_error(exc, f"get_project_condition {condition_id}")
|
|
956
|
+
|
|
957
|
+
def create_project_condition(self, project_key: str, condition: dict[str, Any]) -> dict[str, Any]:
|
|
958
|
+
"""Create project condition with reviewers."""
|
|
959
|
+
try:
|
|
960
|
+
result = self._client.create_project_condition(project_key, condition)
|
|
961
|
+
self._log.info("project_condition_created", project_key=project_key)
|
|
962
|
+
return result
|
|
963
|
+
except Exception as exc:
|
|
964
|
+
self._handle_api_error(exc, f"create_project_condition {project_key}")
|
|
965
|
+
|
|
966
|
+
def update_project_condition(self, project_key: str, condition: dict[str, Any], condition_id: int) -> dict[str, Any]:
|
|
967
|
+
"""Update a project condition with reviewers."""
|
|
968
|
+
try:
|
|
969
|
+
result = self._client.update_project_condition(project_key, condition, condition_id)
|
|
970
|
+
self._log.info("project_condition_updated", project_key=project_key, condition_id=condition_id)
|
|
971
|
+
return result
|
|
972
|
+
except Exception as exc:
|
|
973
|
+
self._handle_api_error(exc, f"update_project_condition {condition_id}")
|
|
974
|
+
|
|
975
|
+
def delete_project_condition(self, project_key: str, condition_id: int) -> None:
|
|
976
|
+
"""Delete a project condition."""
|
|
977
|
+
try:
|
|
978
|
+
self._client.delete_project_condition(project_key, condition_id)
|
|
979
|
+
self._log.info("project_condition_deleted", project_key=project_key, condition_id=condition_id)
|
|
980
|
+
except Exception as exc:
|
|
981
|
+
self._handle_api_error(exc, f"delete_project_condition {condition_id}")
|
|
982
|
+
|
|
983
|
+
def get_repo_conditions(self, project_key: str, repository_slug: str) -> list[dict[str, Any]]:
|
|
984
|
+
"""Get all repository conditions with reviewers list."""
|
|
985
|
+
try:
|
|
986
|
+
result = self._client.get_repo_conditions(project_key, repository_slug)
|
|
987
|
+
self._log.debug(
|
|
988
|
+
"repo_conditions_retrieved",
|
|
989
|
+
project_key=project_key,
|
|
990
|
+
repository_slug=repository_slug,
|
|
991
|
+
count=len(result) if result else 0,
|
|
992
|
+
)
|
|
993
|
+
return result if isinstance(result, list) else [result] if result else []
|
|
994
|
+
except Exception as exc:
|
|
995
|
+
self._handle_api_error(exc, f"get_repo_conditions {project_key}/{repository_slug}")
|
|
996
|
+
|
|
997
|
+
def get_repo_condition(self, project_key: str, repository_slug: str, condition_id: int) -> dict[str, Any]:
|
|
998
|
+
"""Get a repository condition with reviewers list."""
|
|
999
|
+
try:
|
|
1000
|
+
result = self._client.get_repo_condition(project_key, repository_slug, condition_id)
|
|
1001
|
+
self._log.debug(
|
|
1002
|
+
"repo_condition_retrieved",
|
|
1003
|
+
project_key=project_key,
|
|
1004
|
+
repository_slug=repository_slug,
|
|
1005
|
+
condition_id=condition_id,
|
|
1006
|
+
)
|
|
1007
|
+
return result
|
|
1008
|
+
except Exception as exc:
|
|
1009
|
+
self._handle_api_error(exc, f"get_repo_condition {condition_id}")
|
|
1010
|
+
|
|
1011
|
+
def create_repo_condition(
|
|
1012
|
+
self, project_key: str, repository_slug: str, condition: dict[str, Any]
|
|
1013
|
+
) -> dict[str, Any]:
|
|
1014
|
+
"""Create repository condition with reviewers."""
|
|
1015
|
+
try:
|
|
1016
|
+
result = self._client.create_repo_condition(project_key, repository_slug, condition)
|
|
1017
|
+
self._log.info(
|
|
1018
|
+
"repo_condition_created", project_key=project_key, repository_slug=repository_slug
|
|
1019
|
+
)
|
|
1020
|
+
return result
|
|
1021
|
+
except Exception as exc:
|
|
1022
|
+
self._handle_api_error(exc, f"create_repo_condition {project_key}/{repository_slug}")
|
|
1023
|
+
|
|
1024
|
+
def update_repo_condition(
|
|
1025
|
+
self, project_key: str, repository_slug: str, condition: dict[str, Any], condition_id: int
|
|
1026
|
+
) -> dict[str, Any]:
|
|
1027
|
+
"""Update a repository condition with reviewers."""
|
|
1028
|
+
try:
|
|
1029
|
+
result = self._client.update_repo_condition(project_key, repository_slug, condition, condition_id)
|
|
1030
|
+
self._log.info(
|
|
1031
|
+
"repo_condition_updated",
|
|
1032
|
+
project_key=project_key,
|
|
1033
|
+
repository_slug=repository_slug,
|
|
1034
|
+
condition_id=condition_id,
|
|
1035
|
+
)
|
|
1036
|
+
return result
|
|
1037
|
+
except Exception as exc:
|
|
1038
|
+
self._handle_api_error(exc, f"update_repo_condition {condition_id}")
|
|
1039
|
+
|
|
1040
|
+
def delete_repo_condition(self, project_key: str, repository_slug: str, condition_id: int) -> None:
|
|
1041
|
+
"""Delete a repository condition."""
|
|
1042
|
+
try:
|
|
1043
|
+
self._client.delete_repo_condition(project_key, repository_slug, condition_id)
|
|
1044
|
+
self._log.info(
|
|
1045
|
+
"repo_condition_deleted",
|
|
1046
|
+
project_key=project_key,
|
|
1047
|
+
repository_slug=repository_slug,
|
|
1048
|
+
condition_id=condition_id,
|
|
1049
|
+
)
|
|
1050
|
+
except Exception as exc:
|
|
1051
|
+
self._handle_api_error(exc, f"delete_repo_condition {condition_id}")
|
|
1052
|
+
|
|
1053
|
+
# Code Management
|
|
1054
|
+
def get_content_of_file(
|
|
1055
|
+
self, project_key: str, repository_slug: str, filename: str, at: str | None = None, markup: str | None = None
|
|
1056
|
+
) -> str:
|
|
1057
|
+
"""Get raw content of the file from repository."""
|
|
1058
|
+
try:
|
|
1059
|
+
result = self._client.get_content_of_file(project_key, repository_slug, filename, at=at, markup=markup)
|
|
1060
|
+
self._log.debug(
|
|
1061
|
+
"file_content_retrieved",
|
|
1062
|
+
project_key=project_key,
|
|
1063
|
+
repository_slug=repository_slug,
|
|
1064
|
+
filename=filename,
|
|
1065
|
+
)
|
|
1066
|
+
return result
|
|
1067
|
+
except Exception as exc:
|
|
1068
|
+
self._handle_api_error(exc, f"get_content_of_file {filename}")
|
|
1069
|
+
|
|
1070
|
+
def get_commits(
|
|
1071
|
+
self, project_key: str, repository_slug: str, hash_oldest: str | None = None, hash_newest: str | None = None, limit: int = 99999
|
|
1072
|
+
) -> list[dict[str, Any]]:
|
|
1073
|
+
"""Get commit list from repository."""
|
|
1074
|
+
try:
|
|
1075
|
+
result = self._client.get_commits(project_key, repository_slug, hash_oldest, hash_newest, limit=limit)
|
|
1076
|
+
# SDK may return a generator, convert to list first
|
|
1077
|
+
commits_list = result if isinstance(result, list) else list(result) if result else []
|
|
1078
|
+
self._log.debug(
|
|
1079
|
+
"commits_retrieved",
|
|
1080
|
+
project_key=project_key,
|
|
1081
|
+
repository_slug=repository_slug,
|
|
1082
|
+
count=len(commits_list),
|
|
1083
|
+
)
|
|
1084
|
+
return commits_list
|
|
1085
|
+
except Exception as exc:
|
|
1086
|
+
self._handle_api_error(exc, f"get_commits {project_key}/{repository_slug}")
|
|
1087
|
+
|
|
1088
|
+
def get_diff(
|
|
1089
|
+
self, project_key: str, repository_slug: str, path: str, hash_oldest: str, hash_newest: str
|
|
1090
|
+
) -> dict[str, Any]:
|
|
1091
|
+
"""Get diff between two commits."""
|
|
1092
|
+
try:
|
|
1093
|
+
result = self._client.get_diff(project_key, repository_slug, path, hash_oldest, hash_newest)
|
|
1094
|
+
self._log.debug(
|
|
1095
|
+
"diff_retrieved",
|
|
1096
|
+
project_key=project_key,
|
|
1097
|
+
repository_slug=repository_slug,
|
|
1098
|
+
path=path,
|
|
1099
|
+
)
|
|
1100
|
+
return result
|
|
1101
|
+
except Exception as exc:
|
|
1102
|
+
self._handle_api_error(exc, f"get_diff {path}")
|
|
1103
|
+
|
|
1104
|
+
def get_changelog(
|
|
1105
|
+
self, project_key: str, repository_slug: str, ref_from: str, ref_to: str, limit: int = 99999
|
|
1106
|
+
) -> list[dict[str, Any]]:
|
|
1107
|
+
"""Get change log between 2 refs."""
|
|
1108
|
+
try:
|
|
1109
|
+
result = self._client.get_changelog(project_key, repository_slug, ref_from, ref_to, limit=limit)
|
|
1110
|
+
# SDK may return a generator, convert to list first
|
|
1111
|
+
changelog_list = result if isinstance(result, list) else list(result) if result else []
|
|
1112
|
+
self._log.debug(
|
|
1113
|
+
"changelog_retrieved",
|
|
1114
|
+
project_key=project_key,
|
|
1115
|
+
repository_slug=repository_slug,
|
|
1116
|
+
count=len(changelog_list),
|
|
1117
|
+
)
|
|
1118
|
+
return changelog_list
|
|
1119
|
+
except Exception as exc:
|
|
1120
|
+
self._handle_api_error(exc, f"get_changelog {project_key}/{repository_slug}")
|
|
1121
|
+
|
|
1122
|
+
# Code Insights Operations (Cloud-only)
|
|
1123
|
+
def create_code_insights_report(
|
|
1124
|
+
self,
|
|
1125
|
+
project_key: str,
|
|
1126
|
+
repository_slug: str,
|
|
1127
|
+
commit_id: str,
|
|
1128
|
+
report_key: str,
|
|
1129
|
+
report_title: str,
|
|
1130
|
+
reporter: str,
|
|
1131
|
+
result: str,
|
|
1132
|
+
data: list[dict[str, Any]] | None = None,
|
|
1133
|
+
**report_params: Any,
|
|
1134
|
+
) -> dict[str, Any]:
|
|
1135
|
+
"""Create a Code Insights report.
|
|
1136
|
+
|
|
1137
|
+
Args:
|
|
1138
|
+
project_key: Project key
|
|
1139
|
+
repository_slug: Repository slug
|
|
1140
|
+
commit_id: Commit hash
|
|
1141
|
+
report_key: Unique report key
|
|
1142
|
+
report_title: Report title
|
|
1143
|
+
reporter: Reporter name
|
|
1144
|
+
result: Report result ('PASSED', 'FAILED', 'PENDING')
|
|
1145
|
+
data: Optional report data (list of data items)
|
|
1146
|
+
**report_params: Additional report parameters (e.g., report_type, details, link)
|
|
1147
|
+
"""
|
|
1148
|
+
if not self._settings.is_cloud_mode:
|
|
1149
|
+
raise BitbucketClientError(
|
|
1150
|
+
"Code Insights is only available in Bitbucket Cloud mode",
|
|
1151
|
+
context={"mode": self._settings.mode},
|
|
1152
|
+
)
|
|
1153
|
+
try:
|
|
1154
|
+
params: dict[str, Any] = {
|
|
1155
|
+
"reporter": reporter,
|
|
1156
|
+
"result": result,
|
|
1157
|
+
}
|
|
1158
|
+
if data:
|
|
1159
|
+
params["data"] = data
|
|
1160
|
+
params.update(report_params)
|
|
1161
|
+
|
|
1162
|
+
result_obj = self._client.create_code_insights_report(
|
|
1163
|
+
project_key, repository_slug, commit_id, report_key, report_title, **params
|
|
1164
|
+
)
|
|
1165
|
+
self._log.info(
|
|
1166
|
+
"code_insights_report_created",
|
|
1167
|
+
project_key=project_key,
|
|
1168
|
+
repository_slug=repository_slug,
|
|
1169
|
+
commit_id=commit_id,
|
|
1170
|
+
report_key=report_key,
|
|
1171
|
+
)
|
|
1172
|
+
return result_obj
|
|
1173
|
+
except Exception as exc:
|
|
1174
|
+
self._handle_api_error(exc, f"create_code_insights_report {project_key}/{repository_slug}/{commit_id}/{report_key}")
|
|
1175
|
+
|
|
1176
|
+
def get_code_insights_report(
|
|
1177
|
+
self, project_key: str, repository_slug: str, commit_id: str, report_key: str
|
|
1178
|
+
) -> dict[str, Any]:
|
|
1179
|
+
"""Get a Code Insights report."""
|
|
1180
|
+
if not self._settings.is_cloud_mode:
|
|
1181
|
+
raise BitbucketClientError(
|
|
1182
|
+
"Code Insights is only available in Bitbucket Cloud mode",
|
|
1183
|
+
context={"mode": self._settings.mode},
|
|
1184
|
+
)
|
|
1185
|
+
try:
|
|
1186
|
+
result = self._client.get_code_insights_report(project_key, repository_slug, commit_id, report_key)
|
|
1187
|
+
self._log.debug(
|
|
1188
|
+
"code_insights_report_retrieved",
|
|
1189
|
+
project_key=project_key,
|
|
1190
|
+
repository_slug=repository_slug,
|
|
1191
|
+
commit_id=commit_id,
|
|
1192
|
+
report_key=report_key,
|
|
1193
|
+
)
|
|
1194
|
+
return result
|
|
1195
|
+
except Exception as exc:
|
|
1196
|
+
self._handle_api_error(exc, f"get_code_insights_report {project_key}/{repository_slug}/{commit_id}/{report_key}")
|
|
1197
|
+
|
|
1198
|
+
def add_code_insights_annotations_to_report(
|
|
1199
|
+
self,
|
|
1200
|
+
project_key: str,
|
|
1201
|
+
repository_slug: str,
|
|
1202
|
+
commit_id: str,
|
|
1203
|
+
report_key: str,
|
|
1204
|
+
annotations: list[dict[str, Any]],
|
|
1205
|
+
) -> dict[str, Any]:
|
|
1206
|
+
"""Add annotations to a Code Insights report."""
|
|
1207
|
+
if not self._settings.is_cloud_mode:
|
|
1208
|
+
raise BitbucketClientError(
|
|
1209
|
+
"Code Insights is only available in Bitbucket Cloud mode",
|
|
1210
|
+
context={"mode": self._settings.mode},
|
|
1211
|
+
)
|
|
1212
|
+
try:
|
|
1213
|
+
result = self._client.add_code_insights_annotations_to_report(
|
|
1214
|
+
project_key, repository_slug, commit_id, report_key, annotations
|
|
1215
|
+
)
|
|
1216
|
+
self._log.info(
|
|
1217
|
+
"code_insights_annotations_added",
|
|
1218
|
+
project_key=project_key,
|
|
1219
|
+
repository_slug=repository_slug,
|
|
1220
|
+
commit_id=commit_id,
|
|
1221
|
+
report_key=report_key,
|
|
1222
|
+
annotation_count=len(annotations),
|
|
1223
|
+
)
|
|
1224
|
+
return result
|
|
1225
|
+
except Exception as exc:
|
|
1226
|
+
self._handle_api_error(
|
|
1227
|
+
exc, f"add_code_insights_annotations_to_report {project_key}/{repository_slug}/{commit_id}/{report_key}"
|
|
1228
|
+
)
|
|
1229
|
+
|
|
1230
|
+
def delete_code_insights_report(
|
|
1231
|
+
self, project_key: str, repository_slug: str, commit_id: str, report_key: str
|
|
1232
|
+
) -> None:
|
|
1233
|
+
"""Delete a Code Insights report."""
|
|
1234
|
+
if not self._settings.is_cloud_mode:
|
|
1235
|
+
raise BitbucketClientError(
|
|
1236
|
+
"Code Insights is only available in Bitbucket Cloud mode",
|
|
1237
|
+
context={"mode": self._settings.mode},
|
|
1238
|
+
)
|
|
1239
|
+
try:
|
|
1240
|
+
self._client.delete_code_insights_report(project_key, repository_slug, commit_id, report_key)
|
|
1241
|
+
self._log.info(
|
|
1242
|
+
"code_insights_report_deleted",
|
|
1243
|
+
project_key=project_key,
|
|
1244
|
+
repository_slug=repository_slug,
|
|
1245
|
+
commit_id=commit_id,
|
|
1246
|
+
report_key=report_key,
|
|
1247
|
+
)
|
|
1248
|
+
except Exception as exc:
|
|
1249
|
+
self._handle_api_error(exc, f"delete_code_insights_report {project_key}/{repository_slug}/{commit_id}/{report_key}")
|
|
1250
|
+
|
|
1251
|
+
# Repository Settings Operations
|
|
1252
|
+
def get_branching_model(self, project_key: str, repository_slug: str) -> dict[str, Any]:
|
|
1253
|
+
"""Get branching model configuration."""
|
|
1254
|
+
try:
|
|
1255
|
+
result = self._client.get_branching_model(project_key, repository_slug)
|
|
1256
|
+
self._log.debug(
|
|
1257
|
+
"branching_model_retrieved",
|
|
1258
|
+
project_key=project_key,
|
|
1259
|
+
repository_slug=repository_slug,
|
|
1260
|
+
)
|
|
1261
|
+
return result
|
|
1262
|
+
except Exception as exc:
|
|
1263
|
+
self._handle_api_error(exc, f"get_branching_model {project_key}/{repository_slug}")
|
|
1264
|
+
|
|
1265
|
+
def set_branching_model(self, project_key: str, repository_slug: str, model: dict[str, Any]) -> dict[str, Any]:
|
|
1266
|
+
"""Set branching model configuration."""
|
|
1267
|
+
try:
|
|
1268
|
+
result = self._client.set_branching_model(project_key, repository_slug, model)
|
|
1269
|
+
self._log.info(
|
|
1270
|
+
"branching_model_set",
|
|
1271
|
+
project_key=project_key,
|
|
1272
|
+
repository_slug=repository_slug,
|
|
1273
|
+
)
|
|
1274
|
+
return result
|
|
1275
|
+
except Exception as exc:
|
|
1276
|
+
self._handle_api_error(exc, f"set_branching_model {project_key}/{repository_slug}")
|
|
1277
|
+
|
|
1278
|
+
def enable_branching_model(self, project_key: str, repository_slug: str) -> dict[str, Any]:
|
|
1279
|
+
"""Enable branching model."""
|
|
1280
|
+
try:
|
|
1281
|
+
result = self._client.enable_branching_model(project_key, repository_slug)
|
|
1282
|
+
self._log.info(
|
|
1283
|
+
"branching_model_enabled",
|
|
1284
|
+
project_key=project_key,
|
|
1285
|
+
repository_slug=repository_slug,
|
|
1286
|
+
)
|
|
1287
|
+
return result
|
|
1288
|
+
except Exception as exc:
|
|
1289
|
+
self._handle_api_error(exc, f"enable_branching_model {project_key}/{repository_slug}")
|
|
1290
|
+
|
|
1291
|
+
def disable_branching_model(self, project_key: str, repository_slug: str) -> dict[str, Any]:
|
|
1292
|
+
"""Disable branching model."""
|
|
1293
|
+
try:
|
|
1294
|
+
result = self._client.disable_branching_model(project_key, repository_slug)
|
|
1295
|
+
self._log.info(
|
|
1296
|
+
"branching_model_disabled",
|
|
1297
|
+
project_key=project_key,
|
|
1298
|
+
repository_slug=repository_slug,
|
|
1299
|
+
)
|
|
1300
|
+
return result
|
|
1301
|
+
except Exception as exc:
|
|
1302
|
+
self._handle_api_error(exc, f"disable_branching_model {project_key}/{repository_slug}")
|
|
1303
|
+
|
|
1304
|
+
def get_default_branch(self, project_key: str, repository_slug: str) -> dict[str, Any]:
|
|
1305
|
+
"""Get default branch."""
|
|
1306
|
+
try:
|
|
1307
|
+
result = self._client.get_default_branch(project_key, repository_slug)
|
|
1308
|
+
self._log.debug(
|
|
1309
|
+
"default_branch_retrieved",
|
|
1310
|
+
project_key=project_key,
|
|
1311
|
+
repository_slug=repository_slug,
|
|
1312
|
+
)
|
|
1313
|
+
return result
|
|
1314
|
+
except Exception as exc:
|
|
1315
|
+
self._handle_api_error(exc, f"get_default_branch {project_key}/{repository_slug}")
|
|
1316
|
+
|
|
1317
|
+
def set_default_branch(self, project_key: str, repository_slug: str, branch: str) -> dict[str, Any]:
|
|
1318
|
+
"""Set default branch."""
|
|
1319
|
+
try:
|
|
1320
|
+
result = self._client.set_default_branch(project_key, repository_slug, branch)
|
|
1321
|
+
self._log.info(
|
|
1322
|
+
"default_branch_set",
|
|
1323
|
+
project_key=project_key,
|
|
1324
|
+
repository_slug=repository_slug,
|
|
1325
|
+
branch=branch,
|
|
1326
|
+
)
|
|
1327
|
+
return result
|
|
1328
|
+
except Exception as exc:
|
|
1329
|
+
self._handle_api_error(exc, f"set_default_branch {project_key}/{repository_slug}")
|
|
1330
|
+
|
|
1331
|
+
def get_repo_hook_settings(
|
|
1332
|
+
self, project_key: str, repository_slug: str, start: int = 0, limit: int | None = None, filter_type: str | None = None
|
|
1333
|
+
) -> list[dict[str, Any]]:
|
|
1334
|
+
"""Get repository hook settings."""
|
|
1335
|
+
try:
|
|
1336
|
+
result = self._client.all_repo_hook_settings(project_key, repository_slug, start=start, limit=limit, filter_type=filter_type)
|
|
1337
|
+
# SDK may return a generator, convert to list first
|
|
1338
|
+
hooks_list = result if isinstance(result, list) else list(result) if result else []
|
|
1339
|
+
self._log.debug(
|
|
1340
|
+
"repo_hook_settings_retrieved",
|
|
1341
|
+
project_key=project_key,
|
|
1342
|
+
repository_slug=repository_slug,
|
|
1343
|
+
count=len(hooks_list),
|
|
1344
|
+
)
|
|
1345
|
+
return hooks_list
|
|
1346
|
+
except Exception as exc:
|
|
1347
|
+
self._handle_api_error(exc, f"get_repo_hook_settings {project_key}/{repository_slug}")
|
|
1348
|
+
|
|
1349
|
+
def enable_repo_hook_settings(self, project_key: str, repository_slug: str, hook_key: str) -> dict[str, Any]:
|
|
1350
|
+
"""Enable repository hook."""
|
|
1351
|
+
try:
|
|
1352
|
+
result = self._client.enable_repo_hook_settings(project_key, repository_slug, hook_key)
|
|
1353
|
+
self._log.info(
|
|
1354
|
+
"repo_hook_enabled",
|
|
1355
|
+
project_key=project_key,
|
|
1356
|
+
repository_slug=repository_slug,
|
|
1357
|
+
hook_key=hook_key,
|
|
1358
|
+
)
|
|
1359
|
+
return result
|
|
1360
|
+
except Exception as exc:
|
|
1361
|
+
self._handle_api_error(exc, f"enable_repo_hook_settings {project_key}/{repository_slug}/{hook_key}")
|
|
1362
|
+
|
|
1363
|
+
def disable_repo_hook_settings(self, project_key: str, repository_slug: str, hook_key: str) -> dict[str, Any]:
|
|
1364
|
+
"""Disable repository hook."""
|
|
1365
|
+
try:
|
|
1366
|
+
result = self._client.disable_repo_hook_settings(project_key, repository_slug, hook_key)
|
|
1367
|
+
self._log.info(
|
|
1368
|
+
"repo_hook_disabled",
|
|
1369
|
+
project_key=project_key,
|
|
1370
|
+
repository_slug=repository_slug,
|
|
1371
|
+
hook_key=hook_key,
|
|
1372
|
+
)
|
|
1373
|
+
return result
|
|
1374
|
+
except Exception as exc:
|
|
1375
|
+
self._handle_api_error(exc, f"disable_repo_hook_settings {project_key}/{repository_slug}/{hook_key}")
|
|
1376
|
+
|
|
1377
|
+
def get_lfs_repo_status(self, project_key: str, repository_slug: str) -> dict[str, Any]:
|
|
1378
|
+
"""Get Git LFS repository status."""
|
|
1379
|
+
try:
|
|
1380
|
+
result = self._client.get_lfs_repo_status(project_key, repository_slug)
|
|
1381
|
+
self._log.debug(
|
|
1382
|
+
"lfs_repo_status_retrieved",
|
|
1383
|
+
project_key=project_key,
|
|
1384
|
+
repository_slug=repository_slug,
|
|
1385
|
+
)
|
|
1386
|
+
return result
|
|
1387
|
+
except Exception as exc:
|
|
1388
|
+
self._handle_api_error(exc, f"get_lfs_repo_status {project_key}/{repository_slug}")
|
|
1389
|
+
|
|
1390
|
+
def set_lfs_repo_status(self, project_key: str, repository_slug: str, enabled: bool) -> dict[str, Any]:
|
|
1391
|
+
"""Set Git LFS repository status."""
|
|
1392
|
+
try:
|
|
1393
|
+
result = self._client.set_lfs_repo_status(project_key, repository_slug, enabled)
|
|
1394
|
+
self._log.info(
|
|
1395
|
+
"lfs_repo_status_set",
|
|
1396
|
+
project_key=project_key,
|
|
1397
|
+
repository_slug=repository_slug,
|
|
1398
|
+
enabled=enabled,
|
|
1399
|
+
)
|
|
1400
|
+
return result
|
|
1401
|
+
except Exception as exc:
|
|
1402
|
+
self._handle_api_error(exc, f"set_lfs_repo_status {project_key}/{repository_slug}")
|
|
1403
|
+
|
|
1404
|
+
# Advanced Code Operations
|
|
1405
|
+
def get_file_list(
|
|
1406
|
+
self,
|
|
1407
|
+
project_key: str,
|
|
1408
|
+
repository_slug: str,
|
|
1409
|
+
sub_folder: str | None = None,
|
|
1410
|
+
query: str | None = None,
|
|
1411
|
+
start: int = 0,
|
|
1412
|
+
limit: int | None = None,
|
|
1413
|
+
) -> list[dict[str, Any]]:
|
|
1414
|
+
"""Get file list from repository.
|
|
1415
|
+
|
|
1416
|
+
Args:
|
|
1417
|
+
project_key: Project key
|
|
1418
|
+
repository_slug: Repository slug
|
|
1419
|
+
sub_folder: Optional subfolder path
|
|
1420
|
+
query: Optional query string for filtering
|
|
1421
|
+
start: Start index for pagination
|
|
1422
|
+
limit: Maximum number of results
|
|
1423
|
+
"""
|
|
1424
|
+
try:
|
|
1425
|
+
result = self._client.get_file_list(
|
|
1426
|
+
project_key, repository_slug, sub_folder=sub_folder, query=query, start=start, limit=limit
|
|
1427
|
+
)
|
|
1428
|
+
self._log.debug(
|
|
1429
|
+
"file_list_retrieved",
|
|
1430
|
+
project_key=project_key,
|
|
1431
|
+
repository_slug=repository_slug,
|
|
1432
|
+
sub_folder=sub_folder,
|
|
1433
|
+
count=len(result) if result else 0,
|
|
1434
|
+
)
|
|
1435
|
+
return result if isinstance(result, list) else [result] if result else []
|
|
1436
|
+
except Exception as exc:
|
|
1437
|
+
self._handle_api_error(exc, f"get_file_list {project_key}/{repository_slug}")
|
|
1438
|
+
|
|
1439
|
+
def get_commit_info(
|
|
1440
|
+
self, project_key: str, repository_slug: str, commit: str, path: str | None = None
|
|
1441
|
+
) -> dict[str, Any]:
|
|
1442
|
+
"""Get commit information.
|
|
1443
|
+
|
|
1444
|
+
Args:
|
|
1445
|
+
project_key: Project key
|
|
1446
|
+
repository_slug: Repository slug
|
|
1447
|
+
commit: Commit hash
|
|
1448
|
+
path: Optional file path
|
|
1449
|
+
"""
|
|
1450
|
+
try:
|
|
1451
|
+
result = self._client.get_commit_info(project_key, repository_slug, commit, path=path)
|
|
1452
|
+
self._log.debug(
|
|
1453
|
+
"commit_info_retrieved",
|
|
1454
|
+
project_key=project_key,
|
|
1455
|
+
repository_slug=repository_slug,
|
|
1456
|
+
commit=commit,
|
|
1457
|
+
path=path,
|
|
1458
|
+
)
|
|
1459
|
+
return result
|
|
1460
|
+
except Exception as exc:
|
|
1461
|
+
self._handle_api_error(exc, f"get_commit_info {project_key}/{repository_slug}/{commit}")
|
|
1462
|
+
|
|
1463
|
+
def get_commit_changes(
|
|
1464
|
+
self,
|
|
1465
|
+
project_key: str,
|
|
1466
|
+
repository_slug: str,
|
|
1467
|
+
commit_id: str | None = None,
|
|
1468
|
+
hash_newest: str | None = None,
|
|
1469
|
+
merges: str = "include",
|
|
1470
|
+
) -> dict[str, Any]:
|
|
1471
|
+
"""Get commit changes.
|
|
1472
|
+
|
|
1473
|
+
Args:
|
|
1474
|
+
project_key: Project key
|
|
1475
|
+
repository_slug: Repository slug
|
|
1476
|
+
commit_id: Commit ID (preferred)
|
|
1477
|
+
hash_newest: Newest commit hash (alternative to commit_id)
|
|
1478
|
+
merges: How to handle merges ('include', 'exclude', 'only')
|
|
1479
|
+
"""
|
|
1480
|
+
try:
|
|
1481
|
+
result = self._client.get_commit_changes(
|
|
1482
|
+
project_key, repository_slug, hash_newest=hash_newest, merges=merges, commit_id=commit_id
|
|
1483
|
+
)
|
|
1484
|
+
self._log.debug(
|
|
1485
|
+
"commit_changes_retrieved",
|
|
1486
|
+
project_key=project_key,
|
|
1487
|
+
repository_slug=repository_slug,
|
|
1488
|
+
commit_id=commit_id,
|
|
1489
|
+
hash_newest=hash_newest,
|
|
1490
|
+
)
|
|
1491
|
+
return result
|
|
1492
|
+
except Exception as exc:
|
|
1493
|
+
self._handle_api_error(exc, f"get_commit_changes {project_key}/{repository_slug}")
|
|
1494
|
+
|
|
1495
|
+
def search_code(
|
|
1496
|
+
self, team: str, search_query: str, page: int = 1, limit: int = 10
|
|
1497
|
+
) -> dict[str, Any]:
|
|
1498
|
+
"""Search code (Cloud-only).
|
|
1499
|
+
|
|
1500
|
+
Args:
|
|
1501
|
+
team: Team/workspace name (Cloud only)
|
|
1502
|
+
search_query: Search query string
|
|
1503
|
+
page: Page number for pagination
|
|
1504
|
+
limit: Maximum number of results per page
|
|
1505
|
+
"""
|
|
1506
|
+
if not self._settings.is_cloud_mode:
|
|
1507
|
+
raise BitbucketClientError(
|
|
1508
|
+
"Code search is only available in Bitbucket Cloud mode",
|
|
1509
|
+
context={"mode": self._settings.mode},
|
|
1510
|
+
)
|
|
1511
|
+
try:
|
|
1512
|
+
result = self._client.search_code(team, search_query, page=page, limit=limit)
|
|
1513
|
+
self._log.debug(
|
|
1514
|
+
"code_search_performed",
|
|
1515
|
+
team=team,
|
|
1516
|
+
search_query=search_query,
|
|
1517
|
+
page=page,
|
|
1518
|
+
)
|
|
1519
|
+
return result
|
|
1520
|
+
except Exception as exc:
|
|
1521
|
+
self._handle_api_error(exc, f"search_code {team}")
|
|
1522
|
+
|
|
1523
|
+
def search_code_advanced(
|
|
1524
|
+
self,
|
|
1525
|
+
team: str,
|
|
1526
|
+
search_query: str,
|
|
1527
|
+
*,
|
|
1528
|
+
page: int = 1,
|
|
1529
|
+
limit: int = 10,
|
|
1530
|
+
repository: str | None = None,
|
|
1531
|
+
**kwargs: Any,
|
|
1532
|
+
) -> dict[str, Any]:
|
|
1533
|
+
"""Advanced code search with additional filters (Cloud-only).
|
|
1534
|
+
|
|
1535
|
+
Args:
|
|
1536
|
+
team: Team/workspace name (Cloud only)
|
|
1537
|
+
search_query: Search query string
|
|
1538
|
+
page: Page number for pagination (default: 1)
|
|
1539
|
+
limit: Maximum number of results per page (default: 10)
|
|
1540
|
+
repository: Optional repository slug to filter by
|
|
1541
|
+
**kwargs: Additional search parameters (if supported by SDK)
|
|
1542
|
+
|
|
1543
|
+
Note: Code search is only available in Bitbucket Cloud. Server/DC instances
|
|
1544
|
+
do not have code search capabilities via the REST API.
|
|
1545
|
+
"""
|
|
1546
|
+
if not self._settings.is_cloud_mode:
|
|
1547
|
+
raise BitbucketClientError(
|
|
1548
|
+
"Code search is only available in Bitbucket Cloud mode",
|
|
1549
|
+
context={"mode": self._settings.mode},
|
|
1550
|
+
)
|
|
1551
|
+
try:
|
|
1552
|
+
# SDK search_code may support additional parameters via kwargs
|
|
1553
|
+
result = self._client.search_code(team, search_query, page=page, limit=limit, **kwargs) # type: ignore[call-arg]
|
|
1554
|
+
self._log.debug(
|
|
1555
|
+
"code_search_advanced_performed",
|
|
1556
|
+
team=team,
|
|
1557
|
+
search_query=search_query,
|
|
1558
|
+
page=page,
|
|
1559
|
+
limit=limit,
|
|
1560
|
+
repository=repository,
|
|
1561
|
+
)
|
|
1562
|
+
# If repository filter is provided, filter results client-side
|
|
1563
|
+
# (SDK may not support repository parameter directly)
|
|
1564
|
+
if repository and isinstance(result, dict) and "values" in result:
|
|
1565
|
+
filtered = [
|
|
1566
|
+
item for item in result.get("values", [])
|
|
1567
|
+
if item.get("repository", {}).get("slug") == repository
|
|
1568
|
+
]
|
|
1569
|
+
result["values"] = filtered
|
|
1570
|
+
result["size"] = len(filtered)
|
|
1571
|
+
return result
|
|
1572
|
+
except Exception as exc:
|
|
1573
|
+
self._handle_api_error(exc, f"search_code_advanced {team}")
|
|
1574
|
+
|
|
1575
|
+
def _get_cloud_repository(self, workspace: str, repository_slug: str):
|
|
1576
|
+
"""Retrieve a repository object using the cloud API."""
|
|
1577
|
+
if not self._settings.is_cloud_mode:
|
|
1578
|
+
raise BitbucketClientError(
|
|
1579
|
+
"Repository variable operations are only available in Bitbucket Cloud mode",
|
|
1580
|
+
context={"mode": self._settings.mode},
|
|
1581
|
+
)
|
|
1582
|
+
try:
|
|
1583
|
+
return self._client.cloud.repositories.get(workspace, repository_slug) # type: ignore[attr-defined]
|
|
1584
|
+
except Exception as exc:
|
|
1585
|
+
self._handle_api_error(exc, f"cloud_repository {workspace}/{repository_slug}")
|
|
1586
|
+
|
|
1587
|
+
# --- Repository Variables (Cloud-only) ---
|
|
1588
|
+
@staticmethod
|
|
1589
|
+
def _variable_to_dict(variable: Any) -> dict[str, Any]:
|
|
1590
|
+
"""Convert a repository variable object or dict to a plain dictionary."""
|
|
1591
|
+
data = getattr(variable, "data", None)
|
|
1592
|
+
if callable(data):
|
|
1593
|
+
data = data()
|
|
1594
|
+
if isinstance(data, dict):
|
|
1595
|
+
return dict(data)
|
|
1596
|
+
if isinstance(variable, dict):
|
|
1597
|
+
return dict(variable)
|
|
1598
|
+
return {}
|
|
1599
|
+
|
|
1600
|
+
@staticmethod
|
|
1601
|
+
def _deployment_environment_to_dict(environment: Any) -> dict[str, Any]:
|
|
1602
|
+
"""Convert a deployment environment object to a dict."""
|
|
1603
|
+
data = getattr(environment, "data", None)
|
|
1604
|
+
if callable(data):
|
|
1605
|
+
data = data()
|
|
1606
|
+
if isinstance(data, dict):
|
|
1607
|
+
return dict(data)
|
|
1608
|
+
if isinstance(environment, dict):
|
|
1609
|
+
return dict(environment)
|
|
1610
|
+
return {}
|
|
1611
|
+
|
|
1612
|
+
@staticmethod
|
|
1613
|
+
def _issue_to_dict(issue: Any) -> dict[str, Any]:
|
|
1614
|
+
"""Convert an issue object to a dict."""
|
|
1615
|
+
data = getattr(issue, "data", None)
|
|
1616
|
+
if callable(data):
|
|
1617
|
+
data = data()
|
|
1618
|
+
if isinstance(data, dict):
|
|
1619
|
+
return dict(data)
|
|
1620
|
+
if isinstance(issue, dict):
|
|
1621
|
+
return dict(issue)
|
|
1622
|
+
return {}
|
|
1623
|
+
|
|
1624
|
+
def _get_pull_request(
|
|
1625
|
+
self,
|
|
1626
|
+
workspace: str,
|
|
1627
|
+
repository_slug: str,
|
|
1628
|
+
pull_request_id: str | int,
|
|
1629
|
+
):
|
|
1630
|
+
"""Retrieve a pull request object."""
|
|
1631
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1632
|
+
try:
|
|
1633
|
+
return repository.pullrequests.get(pull_request_id) # type: ignore[attr-defined]
|
|
1634
|
+
except Exception as exc:
|
|
1635
|
+
self._handle_api_error(
|
|
1636
|
+
exc, f"pull_request {workspace}/{repository_slug}/{pull_request_id}"
|
|
1637
|
+
)
|
|
1638
|
+
|
|
1639
|
+
def _get_issue(
|
|
1640
|
+
self,
|
|
1641
|
+
workspace: str,
|
|
1642
|
+
repository_slug: str,
|
|
1643
|
+
issue_id: str | int,
|
|
1644
|
+
):
|
|
1645
|
+
"""Retrieve an issue object."""
|
|
1646
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1647
|
+
try:
|
|
1648
|
+
return repository.issues.get(issue_id) # type: ignore[attr-defined]
|
|
1649
|
+
except Exception as exc:
|
|
1650
|
+
self._handle_api_error(exc, f"repository_issue {workspace}/{repository_slug}/{issue_id}")
|
|
1651
|
+
|
|
1652
|
+
@staticmethod
|
|
1653
|
+
def _pipeline_to_dict(pipeline: Any) -> dict[str, Any]:
|
|
1654
|
+
"""Convert a pipeline object to a plain dictionary."""
|
|
1655
|
+
data = getattr(pipeline, "data", None)
|
|
1656
|
+
if callable(data):
|
|
1657
|
+
data = data()
|
|
1658
|
+
if isinstance(data, dict):
|
|
1659
|
+
return dict(data)
|
|
1660
|
+
if isinstance(pipeline, dict):
|
|
1661
|
+
return dict(pipeline)
|
|
1662
|
+
return {}
|
|
1663
|
+
|
|
1664
|
+
@staticmethod
|
|
1665
|
+
def _pipeline_step_to_dict(step: Any) -> dict[str, Any]:
|
|
1666
|
+
"""Convert a pipeline step object to a plain dictionary."""
|
|
1667
|
+
data = getattr(step, "data", None)
|
|
1668
|
+
if callable(data):
|
|
1669
|
+
data = data()
|
|
1670
|
+
if isinstance(data, dict):
|
|
1671
|
+
return dict(data)
|
|
1672
|
+
if isinstance(step, dict):
|
|
1673
|
+
return dict(step)
|
|
1674
|
+
return {}
|
|
1675
|
+
|
|
1676
|
+
def list_repository_variables(self, workspace: str, repository_slug: str) -> list[dict[str, Any]]:
|
|
1677
|
+
"""List repository variables."""
|
|
1678
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1679
|
+
try:
|
|
1680
|
+
variables = [
|
|
1681
|
+
self._variable_to_dict(variable)
|
|
1682
|
+
for variable in repository.repository_variables.each() # type: ignore[attr-defined]
|
|
1683
|
+
]
|
|
1684
|
+
self._log.debug(
|
|
1685
|
+
"repo_variables_listed",
|
|
1686
|
+
workspace=workspace,
|
|
1687
|
+
repository_slug=repository_slug,
|
|
1688
|
+
count=len(variables),
|
|
1689
|
+
)
|
|
1690
|
+
return variables
|
|
1691
|
+
except Exception as exc:
|
|
1692
|
+
self._handle_api_error(exc, f"list_repository_variables {workspace}/{repository_slug}")
|
|
1693
|
+
|
|
1694
|
+
def get_repository_variable(self, workspace: str, repository_slug: str, variable_uuid: str) -> dict[str, Any]:
|
|
1695
|
+
"""Get a repository variable by UUID."""
|
|
1696
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1697
|
+
try:
|
|
1698
|
+
variable = repository.repository_variables.get(variable_uuid) # type: ignore[attr-defined]
|
|
1699
|
+
data = self._variable_to_dict(variable)
|
|
1700
|
+
self._log.debug(
|
|
1701
|
+
"repo_variable_retrieved",
|
|
1702
|
+
workspace=workspace,
|
|
1703
|
+
repository_slug=repository_slug,
|
|
1704
|
+
variable_uuid=variable_uuid,
|
|
1705
|
+
)
|
|
1706
|
+
return data
|
|
1707
|
+
except Exception as exc:
|
|
1708
|
+
self._handle_api_error(exc, f"get_repository_variable {workspace}/{repository_slug}/{variable_uuid}")
|
|
1709
|
+
|
|
1710
|
+
def create_repository_variable(
|
|
1711
|
+
self,
|
|
1712
|
+
workspace: str,
|
|
1713
|
+
repository_slug: str,
|
|
1714
|
+
key: str,
|
|
1715
|
+
value: str,
|
|
1716
|
+
secured: bool = False,
|
|
1717
|
+
) -> dict[str, Any]:
|
|
1718
|
+
"""Create a repository variable."""
|
|
1719
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1720
|
+
try:
|
|
1721
|
+
variable = repository.repository_variables.create(key, value, secured) # type: ignore[attr-defined]
|
|
1722
|
+
data = self._variable_to_dict(variable)
|
|
1723
|
+
self._log.info(
|
|
1724
|
+
"repo_variable_created",
|
|
1725
|
+
workspace=workspace,
|
|
1726
|
+
repository_slug=repository_slug,
|
|
1727
|
+
key=key,
|
|
1728
|
+
secured=secured,
|
|
1729
|
+
)
|
|
1730
|
+
return data
|
|
1731
|
+
except Exception as exc:
|
|
1732
|
+
self._handle_api_error(exc, f"create_repository_variable {workspace}/{repository_slug}")
|
|
1733
|
+
|
|
1734
|
+
def update_repository_variable(
|
|
1735
|
+
self,
|
|
1736
|
+
workspace: str,
|
|
1737
|
+
repository_slug: str,
|
|
1738
|
+
variable_uuid: str,
|
|
1739
|
+
*,
|
|
1740
|
+
key: str | None = None,
|
|
1741
|
+
value: str | None = None,
|
|
1742
|
+
secured: bool | None = None,
|
|
1743
|
+
) -> dict[str, Any]:
|
|
1744
|
+
"""Update a repository variable."""
|
|
1745
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1746
|
+
update_payload: dict[str, Any] = {}
|
|
1747
|
+
if key is not None:
|
|
1748
|
+
update_payload["key"] = key
|
|
1749
|
+
if value is not None:
|
|
1750
|
+
update_payload["value"] = value
|
|
1751
|
+
if secured is not None:
|
|
1752
|
+
update_payload["secured"] = secured
|
|
1753
|
+
if not update_payload:
|
|
1754
|
+
raise BitbucketClientError(
|
|
1755
|
+
"Provide at least one of --key/--value/--secured to update repository variable",
|
|
1756
|
+
context={"workspace": workspace, "repository": repository_slug, "variable_uuid": variable_uuid},
|
|
1757
|
+
)
|
|
1758
|
+
try:
|
|
1759
|
+
variable = repository.repository_variables.get(variable_uuid) # type: ignore[attr-defined]
|
|
1760
|
+
variable.update(**update_payload) # type: ignore[attr-defined]
|
|
1761
|
+
data = self._variable_to_dict(variable)
|
|
1762
|
+
self._log.info(
|
|
1763
|
+
"repo_variable_updated",
|
|
1764
|
+
workspace=workspace,
|
|
1765
|
+
repository_slug=repository_slug,
|
|
1766
|
+
variable_uuid=variable_uuid,
|
|
1767
|
+
fields=list(update_payload.keys()),
|
|
1768
|
+
)
|
|
1769
|
+
return data
|
|
1770
|
+
except Exception as exc:
|
|
1771
|
+
self._handle_api_error(exc, f"update_repository_variable {workspace}/{repository_slug}/{variable_uuid}")
|
|
1772
|
+
|
|
1773
|
+
def delete_repository_variable(self, workspace: str, repository_slug: str, variable_uuid: str) -> None:
|
|
1774
|
+
"""Delete a repository variable."""
|
|
1775
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1776
|
+
try:
|
|
1777
|
+
variable = repository.repository_variables.get(variable_uuid) # type: ignore[attr-defined]
|
|
1778
|
+
variable.delete() # type: ignore[attr-defined]
|
|
1779
|
+
self._log.info(
|
|
1780
|
+
"repo_variable_deleted",
|
|
1781
|
+
workspace=workspace,
|
|
1782
|
+
repository_slug=repository_slug,
|
|
1783
|
+
variable_uuid=variable_uuid,
|
|
1784
|
+
)
|
|
1785
|
+
except Exception as exc:
|
|
1786
|
+
self._handle_api_error(exc, f"delete_repository_variable {workspace}/{repository_slug}/{variable_uuid}")
|
|
1787
|
+
|
|
1788
|
+
# --- Deployment Environments (Cloud-only) ---
|
|
1789
|
+
def list_deployment_environments(self, workspace: str, repository_slug: str) -> list[dict[str, Any]]:
|
|
1790
|
+
"""List deployment environments."""
|
|
1791
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1792
|
+
try:
|
|
1793
|
+
environments = [
|
|
1794
|
+
self._deployment_environment_to_dict(env)
|
|
1795
|
+
for env in repository.deployment_environments.each() # type: ignore[attr-defined]
|
|
1796
|
+
]
|
|
1797
|
+
self._log.debug(
|
|
1798
|
+
"deployment_environments_listed",
|
|
1799
|
+
workspace=workspace,
|
|
1800
|
+
repository_slug=repository_slug,
|
|
1801
|
+
count=len(environments),
|
|
1802
|
+
)
|
|
1803
|
+
return environments
|
|
1804
|
+
except Exception as exc:
|
|
1805
|
+
self._handle_api_error(exc, f"list_deployment_environments {workspace}/{repository_slug}")
|
|
1806
|
+
|
|
1807
|
+
def get_deployment_environment(
|
|
1808
|
+
self, workspace: str, repository_slug: str, environment_uuid: str
|
|
1809
|
+
) -> dict[str, Any]:
|
|
1810
|
+
"""Retrieve a deployment environment."""
|
|
1811
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1812
|
+
try:
|
|
1813
|
+
environment = repository.deployment_environments.get(environment_uuid) # type: ignore[attr-defined]
|
|
1814
|
+
data = self._deployment_environment_to_dict(environment)
|
|
1815
|
+
self._log.debug(
|
|
1816
|
+
"deployment_environment_retrieved",
|
|
1817
|
+
workspace=workspace,
|
|
1818
|
+
repository_slug=repository_slug,
|
|
1819
|
+
environment_uuid=environment_uuid,
|
|
1820
|
+
)
|
|
1821
|
+
return data
|
|
1822
|
+
except Exception as exc:
|
|
1823
|
+
self._handle_api_error(exc, f"get_deployment_environment {workspace}/{repository_slug}/{environment_uuid}")
|
|
1824
|
+
|
|
1825
|
+
def list_deployment_environment_variables(
|
|
1826
|
+
self,
|
|
1827
|
+
workspace: str,
|
|
1828
|
+
repository_slug: str,
|
|
1829
|
+
environment_uuid: str,
|
|
1830
|
+
*,
|
|
1831
|
+
pagelen: int = 10,
|
|
1832
|
+
) -> list[dict[str, Any]]:
|
|
1833
|
+
"""List variables for a deployment environment."""
|
|
1834
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1835
|
+
try:
|
|
1836
|
+
environment = repository.deployment_environments.get(environment_uuid) # type: ignore[attr-defined]
|
|
1837
|
+
variables = [
|
|
1838
|
+
self._variable_to_dict(variable)
|
|
1839
|
+
for variable in environment.deployment_environment_variables.each(pagelen=pagelen) # type: ignore[attr-defined]
|
|
1840
|
+
]
|
|
1841
|
+
self._log.debug(
|
|
1842
|
+
"deployment_env_variables_listed",
|
|
1843
|
+
workspace=workspace,
|
|
1844
|
+
repository_slug=repository_slug,
|
|
1845
|
+
environment_uuid=environment_uuid,
|
|
1846
|
+
count=len(variables),
|
|
1847
|
+
)
|
|
1848
|
+
return variables
|
|
1849
|
+
except Exception as exc:
|
|
1850
|
+
self._handle_api_error(
|
|
1851
|
+
exc,
|
|
1852
|
+
f"list_deployment_environment_variables {workspace}/{repository_slug}/{environment_uuid}",
|
|
1853
|
+
)
|
|
1854
|
+
|
|
1855
|
+
def create_deployment_environment_variable(
|
|
1856
|
+
self,
|
|
1857
|
+
workspace: str,
|
|
1858
|
+
repository_slug: str,
|
|
1859
|
+
environment_uuid: str,
|
|
1860
|
+
key: str,
|
|
1861
|
+
value: str,
|
|
1862
|
+
secured: bool = False,
|
|
1863
|
+
) -> dict[str, Any]:
|
|
1864
|
+
"""Create a deployment environment variable."""
|
|
1865
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1866
|
+
try:
|
|
1867
|
+
environment = repository.deployment_environments.get(environment_uuid) # type: ignore[attr-defined]
|
|
1868
|
+
variable = environment.deployment_environment_variables.create(key, value, secured) # type: ignore[attr-defined]
|
|
1869
|
+
data = self._variable_to_dict(variable)
|
|
1870
|
+
self._log.info(
|
|
1871
|
+
"deployment_env_variable_created",
|
|
1872
|
+
workspace=workspace,
|
|
1873
|
+
repository_slug=repository_slug,
|
|
1874
|
+
environment_uuid=environment_uuid,
|
|
1875
|
+
key=key,
|
|
1876
|
+
secured=secured,
|
|
1877
|
+
)
|
|
1878
|
+
return data
|
|
1879
|
+
except Exception as exc:
|
|
1880
|
+
self._handle_api_error(
|
|
1881
|
+
exc,
|
|
1882
|
+
f"create_deployment_environment_variable {workspace}/{repository_slug}/{environment_uuid}",
|
|
1883
|
+
)
|
|
1884
|
+
|
|
1885
|
+
def update_deployment_environment_variable(
|
|
1886
|
+
self,
|
|
1887
|
+
workspace: str,
|
|
1888
|
+
repository_slug: str,
|
|
1889
|
+
environment_uuid: str,
|
|
1890
|
+
variable_uuid: str,
|
|
1891
|
+
*,
|
|
1892
|
+
key: str | None = None,
|
|
1893
|
+
value: str | None = None,
|
|
1894
|
+
secured: bool | None = None,
|
|
1895
|
+
) -> dict[str, Any]:
|
|
1896
|
+
"""Update a deployment environment variable."""
|
|
1897
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1898
|
+
update_payload: dict[str, Any] = {}
|
|
1899
|
+
if key is not None:
|
|
1900
|
+
update_payload["key"] = key
|
|
1901
|
+
if value is not None:
|
|
1902
|
+
update_payload["value"] = value
|
|
1903
|
+
if secured is not None:
|
|
1904
|
+
update_payload["secured"] = secured
|
|
1905
|
+
if not update_payload:
|
|
1906
|
+
raise BitbucketClientError(
|
|
1907
|
+
"Provide at least one of --key/--value/--secured to update deployment environment variable",
|
|
1908
|
+
context={
|
|
1909
|
+
"workspace": workspace,
|
|
1910
|
+
"repository": repository_slug,
|
|
1911
|
+
"environment_uuid": environment_uuid,
|
|
1912
|
+
"variable_uuid": variable_uuid,
|
|
1913
|
+
},
|
|
1914
|
+
)
|
|
1915
|
+
try:
|
|
1916
|
+
environment = repository.deployment_environments.get(environment_uuid) # type: ignore[attr-defined]
|
|
1917
|
+
variable = environment.deployment_environment_variables.get(variable_uuid) # type: ignore[attr-defined]
|
|
1918
|
+
variable.update(**update_payload) # type: ignore[attr-defined]
|
|
1919
|
+
data = self._variable_to_dict(variable)
|
|
1920
|
+
self._log.info(
|
|
1921
|
+
"deployment_env_variable_updated",
|
|
1922
|
+
workspace=workspace,
|
|
1923
|
+
repository_slug=repository_slug,
|
|
1924
|
+
environment_uuid=environment_uuid,
|
|
1925
|
+
variable_uuid=variable_uuid,
|
|
1926
|
+
fields=list(update_payload.keys()),
|
|
1927
|
+
)
|
|
1928
|
+
return data
|
|
1929
|
+
except Exception as exc:
|
|
1930
|
+
self._handle_api_error(
|
|
1931
|
+
exc,
|
|
1932
|
+
f"update_deployment_environment_variable {workspace}/{repository_slug}/{environment_uuid}/{variable_uuid}",
|
|
1933
|
+
)
|
|
1934
|
+
|
|
1935
|
+
def delete_deployment_environment_variable(
|
|
1936
|
+
self,
|
|
1937
|
+
workspace: str,
|
|
1938
|
+
repository_slug: str,
|
|
1939
|
+
environment_uuid: str,
|
|
1940
|
+
variable_uuid: str,
|
|
1941
|
+
) -> None:
|
|
1942
|
+
"""Delete a deployment environment variable."""
|
|
1943
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1944
|
+
try:
|
|
1945
|
+
environment = repository.deployment_environments.get(environment_uuid) # type: ignore[attr-defined]
|
|
1946
|
+
variable = environment.deployment_environment_variables.get(variable_uuid) # type: ignore[attr-defined]
|
|
1947
|
+
variable.delete() # type: ignore[attr-defined]
|
|
1948
|
+
self._log.info(
|
|
1949
|
+
"deployment_env_variable_deleted",
|
|
1950
|
+
workspace=workspace,
|
|
1951
|
+
repository_slug=repository_slug,
|
|
1952
|
+
environment_uuid=environment_uuid,
|
|
1953
|
+
variable_uuid=variable_uuid,
|
|
1954
|
+
)
|
|
1955
|
+
except Exception as exc:
|
|
1956
|
+
self._handle_api_error(
|
|
1957
|
+
exc,
|
|
1958
|
+
f"delete_deployment_environment_variable {workspace}/{repository_slug}/{environment_uuid}/{variable_uuid}",
|
|
1959
|
+
)
|
|
1960
|
+
|
|
1961
|
+
# --- Repository Issues (Cloud-only) ---
|
|
1962
|
+
def list_repository_issues(
|
|
1963
|
+
self,
|
|
1964
|
+
workspace: str,
|
|
1965
|
+
repository_slug: str,
|
|
1966
|
+
*,
|
|
1967
|
+
query: str | None = None,
|
|
1968
|
+
sort: str | None = None,
|
|
1969
|
+
) -> list[dict[str, Any]]:
|
|
1970
|
+
"""List repository issues."""
|
|
1971
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
1972
|
+
try:
|
|
1973
|
+
issues = [
|
|
1974
|
+
self._issue_to_dict(issue)
|
|
1975
|
+
for issue in repository.issues.each(q=query, sort=sort) # type: ignore[attr-defined]
|
|
1976
|
+
]
|
|
1977
|
+
self._log.debug(
|
|
1978
|
+
"repository_issues_listed",
|
|
1979
|
+
workspace=workspace,
|
|
1980
|
+
repository_slug=repository_slug,
|
|
1981
|
+
count=len(issues),
|
|
1982
|
+
query=query,
|
|
1983
|
+
sort=sort,
|
|
1984
|
+
)
|
|
1985
|
+
return issues
|
|
1986
|
+
except Exception as exc:
|
|
1987
|
+
self._handle_api_error(exc, f"list_repository_issues {workspace}/{repository_slug}")
|
|
1988
|
+
|
|
1989
|
+
def create_repository_issue(
|
|
1990
|
+
self,
|
|
1991
|
+
workspace: str,
|
|
1992
|
+
repository_slug: str,
|
|
1993
|
+
title: str,
|
|
1994
|
+
description: str = "",
|
|
1995
|
+
*,
|
|
1996
|
+
kind: str = "task",
|
|
1997
|
+
priority: str = "major",
|
|
1998
|
+
) -> dict[str, Any]:
|
|
1999
|
+
"""Create an issue."""
|
|
2000
|
+
if not title:
|
|
2001
|
+
raise BitbucketClientError(
|
|
2002
|
+
"Issue title must not be empty",
|
|
2003
|
+
context={"workspace": workspace, "repository": repository_slug},
|
|
2004
|
+
)
|
|
2005
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
2006
|
+
try:
|
|
2007
|
+
issue = repository.issues.create(title, description, kind, priority) # type: ignore[attr-defined]
|
|
2008
|
+
issue_data = self._issue_to_dict(issue)
|
|
2009
|
+
self._log.info(
|
|
2010
|
+
"repository_issue_created",
|
|
2011
|
+
workspace=workspace,
|
|
2012
|
+
repository_slug=repository_slug,
|
|
2013
|
+
issue_id=issue_data.get("id"),
|
|
2014
|
+
kind=kind,
|
|
2015
|
+
priority=priority,
|
|
2016
|
+
)
|
|
2017
|
+
return issue_data
|
|
2018
|
+
except Exception as exc:
|
|
2019
|
+
self._handle_api_error(exc, f"create_repository_issue {workspace}/{repository_slug}")
|
|
2020
|
+
|
|
2021
|
+
def get_repository_issue(
|
|
2022
|
+
self,
|
|
2023
|
+
workspace: str,
|
|
2024
|
+
repository_slug: str,
|
|
2025
|
+
issue_id: str | int,
|
|
2026
|
+
) -> dict[str, Any]:
|
|
2027
|
+
"""Get a single issue."""
|
|
2028
|
+
issue = self._get_issue(workspace, repository_slug, issue_id)
|
|
2029
|
+
data = self._issue_to_dict(issue)
|
|
2030
|
+
self._log.debug(
|
|
2031
|
+
"repository_issue_retrieved",
|
|
2032
|
+
workspace=workspace,
|
|
2033
|
+
repository_slug=repository_slug,
|
|
2034
|
+
issue_id=issue_id,
|
|
2035
|
+
)
|
|
2036
|
+
return data
|
|
2037
|
+
|
|
2038
|
+
def update_repository_issue(
|
|
2039
|
+
self,
|
|
2040
|
+
workspace: str,
|
|
2041
|
+
repository_slug: str,
|
|
2042
|
+
issue_id: str | int,
|
|
2043
|
+
*,
|
|
2044
|
+
title: str | None = None,
|
|
2045
|
+
description: str | None = None,
|
|
2046
|
+
kind: str | None = None,
|
|
2047
|
+
priority: str | None = None,
|
|
2048
|
+
state: str | None = None,
|
|
2049
|
+
) -> dict[str, Any]:
|
|
2050
|
+
"""Update an issue."""
|
|
2051
|
+
payload: dict[str, Any] = {}
|
|
2052
|
+
if title is not None:
|
|
2053
|
+
payload["title"] = title
|
|
2054
|
+
if kind is not None:
|
|
2055
|
+
payload["kind"] = kind
|
|
2056
|
+
if priority is not None:
|
|
2057
|
+
payload["priority"] = priority
|
|
2058
|
+
if state is not None:
|
|
2059
|
+
payload["state"] = state
|
|
2060
|
+
if description is not None:
|
|
2061
|
+
payload["content"] = {"raw": description}
|
|
2062
|
+
if not payload:
|
|
2063
|
+
raise BitbucketClientError(
|
|
2064
|
+
"Provide at least one field to update issue",
|
|
2065
|
+
context={
|
|
2066
|
+
"workspace": workspace,
|
|
2067
|
+
"repository": repository_slug,
|
|
2068
|
+
"issue_id": issue_id,
|
|
2069
|
+
},
|
|
2070
|
+
)
|
|
2071
|
+
issue = self._get_issue(workspace, repository_slug, issue_id)
|
|
2072
|
+
try:
|
|
2073
|
+
issue.update(**payload) # type: ignore[attr-defined]
|
|
2074
|
+
data = self._issue_to_dict(issue)
|
|
2075
|
+
self._log.info(
|
|
2076
|
+
"repository_issue_updated",
|
|
2077
|
+
workspace=workspace,
|
|
2078
|
+
repository_slug=repository_slug,
|
|
2079
|
+
issue_id=issue_id,
|
|
2080
|
+
fields=list(payload.keys()),
|
|
2081
|
+
)
|
|
2082
|
+
return data
|
|
2083
|
+
except Exception as exc:
|
|
2084
|
+
self._handle_api_error(
|
|
2085
|
+
exc,
|
|
2086
|
+
f"update_repository_issue {workspace}/{repository_slug}/{issue_id}",
|
|
2087
|
+
)
|
|
2088
|
+
|
|
2089
|
+
def delete_repository_issue(
|
|
2090
|
+
self,
|
|
2091
|
+
workspace: str,
|
|
2092
|
+
repository_slug: str,
|
|
2093
|
+
issue_id: str | int,
|
|
2094
|
+
) -> None:
|
|
2095
|
+
"""Delete an issue."""
|
|
2096
|
+
issue = self._get_issue(workspace, repository_slug, issue_id)
|
|
2097
|
+
try:
|
|
2098
|
+
issue.delete() # type: ignore[attr-defined]
|
|
2099
|
+
self._log.info(
|
|
2100
|
+
"repository_issue_deleted",
|
|
2101
|
+
workspace=workspace,
|
|
2102
|
+
repository_slug=repository_slug,
|
|
2103
|
+
issue_id=issue_id,
|
|
2104
|
+
)
|
|
2105
|
+
except Exception as exc:
|
|
2106
|
+
self._handle_api_error(
|
|
2107
|
+
exc,
|
|
2108
|
+
f"delete_repository_issue {workspace}/{repository_slug}/{issue_id}",
|
|
2109
|
+
)
|
|
2110
|
+
|
|
2111
|
+
# --- Pull Request Blockers (Cloud-only) ---
|
|
2112
|
+
def list_pull_request_blockers(
|
|
2113
|
+
self,
|
|
2114
|
+
workspace: str,
|
|
2115
|
+
repository_slug: str,
|
|
2116
|
+
pull_request_id: str | int,
|
|
2117
|
+
*,
|
|
2118
|
+
query: str | None = None,
|
|
2119
|
+
) -> list[dict[str, Any]]:
|
|
2120
|
+
"""List pull request blockers."""
|
|
2121
|
+
pull_request = self._get_pull_request(workspace, repository_slug, pull_request_id)
|
|
2122
|
+
params = {"q": query} if query else None
|
|
2123
|
+
try:
|
|
2124
|
+
result = pull_request.get("blockers", params=params) # type: ignore[attr-defined]
|
|
2125
|
+
if isinstance(result, dict) and "values" in result:
|
|
2126
|
+
blockers = result.get("values", [])
|
|
2127
|
+
elif isinstance(result, list):
|
|
2128
|
+
blockers = result
|
|
2129
|
+
else:
|
|
2130
|
+
blockers = [result] if result else []
|
|
2131
|
+
blockers_data = [self._variable_to_dict(blocker) for blocker in blockers]
|
|
2132
|
+
self._log.debug(
|
|
2133
|
+
"pull_request_blockers_listed",
|
|
2134
|
+
workspace=workspace,
|
|
2135
|
+
repository_slug=repository_slug,
|
|
2136
|
+
pull_request_id=pull_request_id,
|
|
2137
|
+
count=len(blockers_data),
|
|
2138
|
+
query=query,
|
|
2139
|
+
)
|
|
2140
|
+
return blockers_data
|
|
2141
|
+
except Exception as exc:
|
|
2142
|
+
self._handle_api_error(
|
|
2143
|
+
exc,
|
|
2144
|
+
f"list_pull_request_blockers {workspace}/{repository_slug}/{pull_request_id}",
|
|
2145
|
+
)
|
|
2146
|
+
|
|
2147
|
+
def add_pull_request_blocker(
|
|
2148
|
+
self,
|
|
2149
|
+
workspace: str,
|
|
2150
|
+
repository_slug: str,
|
|
2151
|
+
pull_request_id: str | int,
|
|
2152
|
+
message: str,
|
|
2153
|
+
*,
|
|
2154
|
+
reason: str | None = None,
|
|
2155
|
+
severity: str | None = None,
|
|
2156
|
+
) -> dict[str, Any]:
|
|
2157
|
+
"""Add a blocker to a pull request."""
|
|
2158
|
+
if not message:
|
|
2159
|
+
raise BitbucketClientError(
|
|
2160
|
+
"Blocker message must not be empty",
|
|
2161
|
+
context={
|
|
2162
|
+
"workspace": workspace,
|
|
2163
|
+
"repository": repository_slug,
|
|
2164
|
+
"pull_request_id": pull_request_id,
|
|
2165
|
+
},
|
|
2166
|
+
)
|
|
2167
|
+
pull_request = self._get_pull_request(workspace, repository_slug, pull_request_id)
|
|
2168
|
+
payload: dict[str, Any] = {"message": message}
|
|
2169
|
+
if reason:
|
|
2170
|
+
payload["reason"] = reason
|
|
2171
|
+
if severity:
|
|
2172
|
+
payload["severity"] = severity
|
|
2173
|
+
try:
|
|
2174
|
+
blocker = pull_request.post("blockers", payload) # type: ignore[attr-defined]
|
|
2175
|
+
blocker_data = self._variable_to_dict(blocker)
|
|
2176
|
+
self._log.info(
|
|
2177
|
+
"pull_request_blocker_added",
|
|
2178
|
+
workspace=workspace,
|
|
2179
|
+
repository_slug=repository_slug,
|
|
2180
|
+
pull_request_id=pull_request_id,
|
|
2181
|
+
blocker_uuid=blocker_data.get("uuid"),
|
|
2182
|
+
)
|
|
2183
|
+
return blocker_data
|
|
2184
|
+
except Exception as exc:
|
|
2185
|
+
self._handle_api_error(
|
|
2186
|
+
exc,
|
|
2187
|
+
f"add_pull_request_blocker {workspace}/{repository_slug}/{pull_request_id}",
|
|
2188
|
+
)
|
|
2189
|
+
|
|
2190
|
+
def delete_pull_request_blocker(
|
|
2191
|
+
self,
|
|
2192
|
+
workspace: str,
|
|
2193
|
+
repository_slug: str,
|
|
2194
|
+
pull_request_id: str | int,
|
|
2195
|
+
blocker_uuid: str,
|
|
2196
|
+
) -> None:
|
|
2197
|
+
"""Delete a pull request blocker."""
|
|
2198
|
+
if not blocker_uuid:
|
|
2199
|
+
raise BitbucketClientError(
|
|
2200
|
+
"Blocker UUID is required",
|
|
2201
|
+
context={
|
|
2202
|
+
"workspace": workspace,
|
|
2203
|
+
"repository": repository_slug,
|
|
2204
|
+
"pull_request_id": pull_request_id,
|
|
2205
|
+
},
|
|
2206
|
+
)
|
|
2207
|
+
pull_request = self._get_pull_request(workspace, repository_slug, pull_request_id)
|
|
2208
|
+
try:
|
|
2209
|
+
pull_request.delete(f"blockers/{blocker_uuid}") # type: ignore[attr-defined]
|
|
2210
|
+
self._log.info(
|
|
2211
|
+
"pull_request_blocker_deleted",
|
|
2212
|
+
workspace=workspace,
|
|
2213
|
+
repository_slug=repository_slug,
|
|
2214
|
+
pull_request_id=pull_request_id,
|
|
2215
|
+
blocker_uuid=blocker_uuid,
|
|
2216
|
+
)
|
|
2217
|
+
except Exception as exc:
|
|
2218
|
+
self._handle_api_error(
|
|
2219
|
+
exc,
|
|
2220
|
+
f"delete_pull_request_blocker {workspace}/{repository_slug}/{pull_request_id}/{blocker_uuid}",
|
|
2221
|
+
)
|
|
2222
|
+
|
|
2223
|
+
def _get_pipeline(self, workspace: str, repository_slug: str, pipeline_uuid: str):
|
|
2224
|
+
"""Retrieve a pipeline object using the cloud API."""
|
|
2225
|
+
repository = self._get_cloud_repository(workspace, repository_slug)
|
|
2226
|
+
try:
|
|
2227
|
+
return repository.pipelines.get(pipeline_uuid) # type: ignore[attr-defined]
|
|
2228
|
+
except Exception as exc:
|
|
2229
|
+
self._handle_api_error(exc, f"pipeline {workspace}/{repository_slug}/{pipeline_uuid}")
|
|
2230
|
+
|
|
2231
|
+
def get_pipeline(self, workspace: str, repository_slug: str, pipeline_uuid: str) -> dict[str, Any]:
|
|
2232
|
+
"""Get pipeline details."""
|
|
2233
|
+
pipeline = self._get_pipeline(workspace, repository_slug, pipeline_uuid)
|
|
2234
|
+
data = self._pipeline_to_dict(pipeline)
|
|
2235
|
+
self._log.debug(
|
|
2236
|
+
"pipeline_retrieved",
|
|
2237
|
+
workspace=workspace,
|
|
2238
|
+
repository_slug=repository_slug,
|
|
2239
|
+
pipeline_uuid=pipeline_uuid,
|
|
2240
|
+
)
|
|
2241
|
+
return data
|
|
2242
|
+
|
|
2243
|
+
def list_pipeline_steps(
|
|
2244
|
+
self,
|
|
2245
|
+
workspace: str,
|
|
2246
|
+
repository_slug: str,
|
|
2247
|
+
pipeline_uuid: str,
|
|
2248
|
+
) -> list[dict[str, Any]]:
|
|
2249
|
+
"""List steps for a pipeline."""
|
|
2250
|
+
pipeline = self._get_pipeline(workspace, repository_slug, pipeline_uuid)
|
|
2251
|
+
try:
|
|
2252
|
+
steps = [
|
|
2253
|
+
self._pipeline_step_to_dict(step)
|
|
2254
|
+
for step in pipeline.steps() # type: ignore[attr-defined]
|
|
2255
|
+
]
|
|
2256
|
+
self._log.debug(
|
|
2257
|
+
"pipeline_steps_listed",
|
|
2258
|
+
workspace=workspace,
|
|
2259
|
+
repository_slug=repository_slug,
|
|
2260
|
+
pipeline_uuid=pipeline_uuid,
|
|
2261
|
+
count=len(steps),
|
|
2262
|
+
)
|
|
2263
|
+
return steps
|
|
2264
|
+
except Exception as exc:
|
|
2265
|
+
self._handle_api_error(
|
|
2266
|
+
exc,
|
|
2267
|
+
f"list_pipeline_steps {workspace}/{repository_slug}/{pipeline_uuid}",
|
|
2268
|
+
)
|
|
2269
|
+
|
|
2270
|
+
def get_pipeline_step(
|
|
2271
|
+
self,
|
|
2272
|
+
workspace: str,
|
|
2273
|
+
repository_slug: str,
|
|
2274
|
+
pipeline_uuid: str,
|
|
2275
|
+
step_uuid: str,
|
|
2276
|
+
) -> dict[str, Any]:
|
|
2277
|
+
"""Get details for a pipeline step."""
|
|
2278
|
+
pipeline = self._get_pipeline(workspace, repository_slug, pipeline_uuid)
|
|
2279
|
+
try:
|
|
2280
|
+
step = pipeline.step(step_uuid) # type: ignore[attr-defined]
|
|
2281
|
+
data = self._pipeline_step_to_dict(step)
|
|
2282
|
+
self._log.debug(
|
|
2283
|
+
"pipeline_step_retrieved",
|
|
2284
|
+
workspace=workspace,
|
|
2285
|
+
repository_slug=repository_slug,
|
|
2286
|
+
pipeline_uuid=pipeline_uuid,
|
|
2287
|
+
step_uuid=step_uuid,
|
|
2288
|
+
)
|
|
2289
|
+
return data
|
|
2290
|
+
except Exception as exc:
|
|
2291
|
+
self._handle_api_error(
|
|
2292
|
+
exc,
|
|
2293
|
+
f"get_pipeline_step {workspace}/{repository_slug}/{pipeline_uuid}/{step_uuid}",
|
|
2294
|
+
)
|
|
2295
|
+
|
|
2296
|
+
def get_pipeline_step_log(
|
|
2297
|
+
self,
|
|
2298
|
+
workspace: str,
|
|
2299
|
+
repository_slug: str,
|
|
2300
|
+
pipeline_uuid: str,
|
|
2301
|
+
step_uuid: str,
|
|
2302
|
+
*,
|
|
2303
|
+
start: int | None = None,
|
|
2304
|
+
end: int | None = None,
|
|
2305
|
+
) -> dict[str, Any]:
|
|
2306
|
+
"""Get log output for a pipeline step."""
|
|
2307
|
+
pipeline = self._get_pipeline(workspace, repository_slug, pipeline_uuid)
|
|
2308
|
+
try:
|
|
2309
|
+
step = pipeline.step(step_uuid) # type: ignore[attr-defined]
|
|
2310
|
+
result = step.log(start=start, end=end) # type: ignore[attr-defined]
|
|
2311
|
+
total_size: int | None = None
|
|
2312
|
+
content_bytes: bytes = b""
|
|
2313
|
+
if isinstance(result, tuple):
|
|
2314
|
+
total_size_raw, chunk = result
|
|
2315
|
+
if isinstance(total_size_raw, (bytes, bytearray)):
|
|
2316
|
+
try:
|
|
2317
|
+
total_size = int(total_size_raw.decode())
|
|
2318
|
+
except (ValueError, AttributeError):
|
|
2319
|
+
total_size = None
|
|
2320
|
+
else:
|
|
2321
|
+
try:
|
|
2322
|
+
total_size = int(total_size_raw)
|
|
2323
|
+
except (ValueError, TypeError):
|
|
2324
|
+
total_size = None
|
|
2325
|
+
content_bytes = chunk or b""
|
|
2326
|
+
elif isinstance(result, (bytes, bytearray)):
|
|
2327
|
+
content_bytes = bytes(result)
|
|
2328
|
+
elif result is None:
|
|
2329
|
+
content_bytes = b""
|
|
2330
|
+
else:
|
|
2331
|
+
# Unknown type, attempt to convert to bytes
|
|
2332
|
+
try:
|
|
2333
|
+
content_bytes = bytes(result)
|
|
2334
|
+
except Exception: # pragma: no cover - defensive
|
|
2335
|
+
content_bytes = str(result).encode("utf-8", errors="replace")
|
|
2336
|
+
|
|
2337
|
+
content_text = content_bytes.decode("utf-8", errors="replace")
|
|
2338
|
+
size = total_size if total_size is not None else len(content_bytes)
|
|
2339
|
+
payload = {
|
|
2340
|
+
"workspace": workspace,
|
|
2341
|
+
"repository": repository_slug,
|
|
2342
|
+
"pipeline_uuid": pipeline_uuid,
|
|
2343
|
+
"step_uuid": step_uuid,
|
|
2344
|
+
"size": size,
|
|
2345
|
+
"content": content_text,
|
|
2346
|
+
"range_start": start,
|
|
2347
|
+
"range_end": end,
|
|
2348
|
+
}
|
|
2349
|
+
self._log.debug(
|
|
2350
|
+
"pipeline_step_log_retrieved",
|
|
2351
|
+
workspace=workspace,
|
|
2352
|
+
repository_slug=repository_slug,
|
|
2353
|
+
pipeline_uuid=pipeline_uuid,
|
|
2354
|
+
step_uuid=step_uuid,
|
|
2355
|
+
size=size,
|
|
2356
|
+
)
|
|
2357
|
+
return payload
|
|
2358
|
+
except Exception as exc:
|
|
2359
|
+
self._handle_api_error(
|
|
2360
|
+
exc,
|
|
2361
|
+
f"get_pipeline_step_log {workspace}/{repository_slug}/{pipeline_uuid}/{step_uuid}",
|
|
2362
|
+
)
|
|
2363
|
+
|
|
2364
|
+
def stop_pipeline(
|
|
2365
|
+
self,
|
|
2366
|
+
workspace: str,
|
|
2367
|
+
repository_slug: str,
|
|
2368
|
+
pipeline_uuid: str,
|
|
2369
|
+
) -> dict[str, Any]:
|
|
2370
|
+
"""Stop a running pipeline."""
|
|
2371
|
+
pipeline = self._get_pipeline(workspace, repository_slug, pipeline_uuid)
|
|
2372
|
+
try:
|
|
2373
|
+
pipeline.stop() # type: ignore[attr-defined]
|
|
2374
|
+
self._log.info(
|
|
2375
|
+
"pipeline_stopped",
|
|
2376
|
+
workspace=workspace,
|
|
2377
|
+
repository_slug=repository_slug,
|
|
2378
|
+
pipeline_uuid=pipeline_uuid,
|
|
2379
|
+
)
|
|
2380
|
+
return {
|
|
2381
|
+
"workspace": workspace,
|
|
2382
|
+
"repository": repository_slug,
|
|
2383
|
+
"pipeline_uuid": pipeline_uuid,
|
|
2384
|
+
"status": "stopped",
|
|
2385
|
+
}
|
|
2386
|
+
except Exception as exc:
|
|
2387
|
+
self._handle_api_error(exc, f"stop_pipeline {workspace}/{repository_slug}/{pipeline_uuid}")
|
|
2388
|
+
|
|
2389
|
+
# --- Cloud Workspace Operations (Cloud-only) ---
|
|
2390
|
+
def list_workspaces(self) -> list[dict[str, Any]]:
|
|
2391
|
+
"""List all workspaces (Cloud-only).
|
|
2392
|
+
|
|
2393
|
+
Returns:
|
|
2394
|
+
List of workspace dictionaries
|
|
2395
|
+
"""
|
|
2396
|
+
if not self._settings.is_cloud_mode:
|
|
2397
|
+
raise BitbucketClientError(
|
|
2398
|
+
"Workspace operations are only available in Bitbucket Cloud mode",
|
|
2399
|
+
context={"mode": self._settings.mode},
|
|
2400
|
+
)
|
|
2401
|
+
try:
|
|
2402
|
+
# Access cloud API: cloud.workspaces.each()
|
|
2403
|
+
workspaces = list(self._client.cloud.workspaces.each()) # type: ignore[attr-defined]
|
|
2404
|
+
self._log.debug("workspaces_listed", count=len(workspaces))
|
|
2405
|
+
return workspaces
|
|
2406
|
+
except Exception as exc:
|
|
2407
|
+
self._handle_api_error(exc, "list_workspaces")
|
|
2408
|
+
|
|
2409
|
+
def get_workspace(self, workspace_slug: str) -> dict[str, Any]:
|
|
2410
|
+
"""Get workspace by slug (Cloud-only).
|
|
2411
|
+
|
|
2412
|
+
Args:
|
|
2413
|
+
workspace_slug: Workspace slug/identifier
|
|
2414
|
+
"""
|
|
2415
|
+
if not self._settings.is_cloud_mode:
|
|
2416
|
+
raise BitbucketClientError(
|
|
2417
|
+
"Workspace operations are only available in Bitbucket Cloud mode",
|
|
2418
|
+
context={"mode": self._settings.mode},
|
|
2419
|
+
)
|
|
2420
|
+
try:
|
|
2421
|
+
# Access cloud API: cloud.workspaces.get(workspace_slug)
|
|
2422
|
+
workspace = self._client.cloud.workspaces.get(workspace_slug) # type: ignore[attr-defined]
|
|
2423
|
+
self._log.debug("workspace_retrieved", workspace_slug=workspace_slug)
|
|
2424
|
+
return workspace
|
|
2425
|
+
except Exception as exc:
|
|
2426
|
+
self._handle_api_error(exc, f"get_workspace {workspace_slug}")
|
|
2427
|
+
|
|
2428
|
+
def get_workspace_permissions(self, workspace_slug: str) -> list[dict[str, Any]]:
|
|
2429
|
+
"""Get workspace permissions (Cloud-only).
|
|
2430
|
+
|
|
2431
|
+
Args:
|
|
2432
|
+
workspace_slug: Workspace slug/identifier
|
|
2433
|
+
"""
|
|
2434
|
+
if not self._settings.is_cloud_mode:
|
|
2435
|
+
raise BitbucketClientError(
|
|
2436
|
+
"Workspace operations are only available in Bitbucket Cloud mode",
|
|
2437
|
+
context={"mode": self._settings.mode},
|
|
2438
|
+
)
|
|
2439
|
+
try:
|
|
2440
|
+
workspace = self._client.cloud.workspaces.get(workspace_slug) # type: ignore[attr-defined]
|
|
2441
|
+
# Access: workplace.permissions.each()
|
|
2442
|
+
permissions = list(workspace.permissions.each()) # type: ignore[attr-defined]
|
|
2443
|
+
self._log.debug("workspace_permissions_retrieved", workspace_slug=workspace_slug, count=len(permissions))
|
|
2444
|
+
return permissions
|
|
2445
|
+
except Exception as exc:
|
|
2446
|
+
self._handle_api_error(exc, f"get_workspace_permissions {workspace_slug}")
|
|
2447
|
+
|
|
2448
|
+
def get_workspace_repository_permissions(
|
|
2449
|
+
self, workspace_slug: str, repo_slug: str | None = None
|
|
2450
|
+
) -> dict[str, Any] | list[dict[str, Any]]:
|
|
2451
|
+
"""Get workspace repository permissions (Cloud-only).
|
|
2452
|
+
|
|
2453
|
+
Args:
|
|
2454
|
+
workspace_slug: Workspace slug/identifier
|
|
2455
|
+
repo_slug: Optional repository slug. If provided, returns permissions for that repo only.
|
|
2456
|
+
If None, returns all repository permissions.
|
|
2457
|
+
"""
|
|
2458
|
+
if not self._settings.is_cloud_mode:
|
|
2459
|
+
raise BitbucketClientError(
|
|
2460
|
+
"Workspace operations are only available in Bitbucket Cloud mode",
|
|
2461
|
+
context={"mode": self._settings.mode},
|
|
2462
|
+
)
|
|
2463
|
+
try:
|
|
2464
|
+
workspace = self._client.cloud.workspaces.get(workspace_slug) # type: ignore[attr-defined]
|
|
2465
|
+
if repo_slug:
|
|
2466
|
+
# Access: workplace.permissions.repositories(repo_slug)
|
|
2467
|
+
permissions = workspace.permissions.repositories(repo_slug) # type: ignore[attr-defined]
|
|
2468
|
+
else:
|
|
2469
|
+
# Access: workplace.permissions.repositories()
|
|
2470
|
+
permissions = list(workspace.permissions.repositories()) # type: ignore[attr-defined]
|
|
2471
|
+
self._log.debug(
|
|
2472
|
+
"workspace_repository_permissions_retrieved",
|
|
2473
|
+
workspace_slug=workspace_slug,
|
|
2474
|
+
repo_slug=repo_slug,
|
|
2475
|
+
is_list=repo_slug is None,
|
|
2476
|
+
)
|
|
2477
|
+
return permissions
|
|
2478
|
+
except Exception as exc:
|
|
2479
|
+
self._handle_api_error(exc, f"get_workspace_repository_permissions {workspace_slug}/{repo_slug}")
|
|
2480
|
+
|
|
2481
|
+
def list_workspace_projects(self, workspace_slug: str) -> list[dict[str, Any]]:
|
|
2482
|
+
"""List projects in workspace (Cloud-only).
|
|
2483
|
+
|
|
2484
|
+
Args:
|
|
2485
|
+
workspace_slug: Workspace slug/identifier
|
|
2486
|
+
"""
|
|
2487
|
+
if not self._settings.is_cloud_mode:
|
|
2488
|
+
raise BitbucketClientError(
|
|
2489
|
+
"Workspace operations are only available in Bitbucket Cloud mode",
|
|
2490
|
+
context={"mode": self._settings.mode},
|
|
2491
|
+
)
|
|
2492
|
+
try:
|
|
2493
|
+
workspace = self._client.cloud.workspaces.get(workspace_slug) # type: ignore[attr-defined]
|
|
2494
|
+
# Access: workplace.projects.each()
|
|
2495
|
+
projects = list(workspace.projects.each()) # type: ignore[attr-defined]
|
|
2496
|
+
self._log.debug("workspace_projects_listed", workspace_slug=workspace_slug, count=len(projects))
|
|
2497
|
+
return projects
|
|
2498
|
+
except Exception as exc:
|
|
2499
|
+
self._handle_api_error(exc, f"list_workspace_projects {workspace_slug}")
|
|
2500
|
+
|
|
2501
|
+
def get_workspace_project(self, workspace_slug: str, project_key: str) -> dict[str, Any]:
|
|
2502
|
+
"""Get project from workspace (Cloud-only).
|
|
2503
|
+
|
|
2504
|
+
Args:
|
|
2505
|
+
workspace_slug: Workspace slug/identifier
|
|
2506
|
+
project_key: Project key
|
|
2507
|
+
"""
|
|
2508
|
+
if not self._settings.is_cloud_mode:
|
|
2509
|
+
raise BitbucketClientError(
|
|
2510
|
+
"Workspace operations are only available in Bitbucket Cloud mode",
|
|
2511
|
+
context={"mode": self._settings.mode},
|
|
2512
|
+
)
|
|
2513
|
+
try:
|
|
2514
|
+
workspace = self._client.cloud.workspaces.get(workspace_slug) # type: ignore[attr-defined]
|
|
2515
|
+
# Access: workplace.projects.get(project_key)
|
|
2516
|
+
project = workspace.projects.get(project_key) # type: ignore[attr-defined]
|
|
2517
|
+
self._log.debug("workspace_project_retrieved", workspace_slug=workspace_slug, project_key=project_key)
|
|
2518
|
+
return project
|
|
2519
|
+
except Exception as exc:
|
|
2520
|
+
self._handle_api_error(exc, f"get_workspace_project {workspace_slug}/{project_key}")
|
|
2521
|
+
|
|
2522
|
+
def close(self) -> None:
|
|
2523
|
+
"""Close client (cleanup method for compatibility)."""
|
|
2524
|
+
# atlassian-python-api doesn't require explicit cleanup
|
|
2525
|
+
pass
|
|
2526
|
+
|
|
2527
|
+
def __enter__(self) -> BitbucketClient:
|
|
2528
|
+
return self
|
|
2529
|
+
|
|
2530
|
+
def __exit__(self, exc_type, exc, tb) -> None:
|
|
2531
|
+
self.close()
|
|
2532
|
+
|
|
2533
|
+
|
|
2534
|
+
__all__ = ["BitbucketClient"]
|