@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,2108 @@
|
|
|
1
|
+
"""Bitbucket CLI using atlassian-python-api (consistent with VDS orchestrators)."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import importlib.metadata as metadata
|
|
6
|
+
import json
|
|
7
|
+
import sys
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
import structlog
|
|
11
|
+
import typer
|
|
12
|
+
|
|
13
|
+
from .client import BitbucketClient
|
|
14
|
+
from .config import BitbucketSettings, load_settings
|
|
15
|
+
from .errors import (
|
|
16
|
+
BitbucketClientError,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
# Global app instance and state
|
|
20
|
+
app = typer.Typer(
|
|
21
|
+
name="vds-bitbucket-cli",
|
|
22
|
+
help="Bitbucket operations orchestrator (atlassian-python-api)",
|
|
23
|
+
no_args_is_help=True,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
_REPORT_DIR = Path.home() / ".cache" / "vds-bitbucket-reports"
|
|
27
|
+
|
|
28
|
+
# Global state for CLI context
|
|
29
|
+
_cli_state = {"report_dir": _REPORT_DIR, "markdown": True, "reports": True}
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _emit_json(payload: dict) -> None:
|
|
33
|
+
"""Emit JSON to stdout."""
|
|
34
|
+
text = json.dumps(payload, indent=2, sort_keys=True)
|
|
35
|
+
if sys.stdout.isatty():
|
|
36
|
+
typer.echo(text)
|
|
37
|
+
else:
|
|
38
|
+
sys.stdout.write(text + "\n")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _version_callback(value: bool | None) -> bool | None:
|
|
42
|
+
"""Print CLI version when --version is provided."""
|
|
43
|
+
if value:
|
|
44
|
+
try:
|
|
45
|
+
version = metadata.version("vds-bitbucket-orchestrator")
|
|
46
|
+
except metadata.PackageNotFoundError:
|
|
47
|
+
version = "unknown"
|
|
48
|
+
typer.echo(f"vds-bitbucket-orchestrator {version}")
|
|
49
|
+
raise typer.Exit() from None
|
|
50
|
+
return value
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.callback()
|
|
54
|
+
def main(
|
|
55
|
+
version: bool = typer.Option(
|
|
56
|
+
False,
|
|
57
|
+
"--version",
|
|
58
|
+
callback=_version_callback,
|
|
59
|
+
is_eager=True,
|
|
60
|
+
help="Show CLI version and exit",
|
|
61
|
+
),
|
|
62
|
+
report_dir: Path | None = typer.Option(None, help=f"Directory for run reports (default: {_REPORT_DIR})"),
|
|
63
|
+
markdown: bool = typer.Option(True, help="Emit Markdown summaries alongside JSON reports"),
|
|
64
|
+
reports: bool = typer.Option(True, "--reports/--no-reports", help="Write report files to report-dir"),
|
|
65
|
+
json_only: bool = typer.Option(
|
|
66
|
+
False,
|
|
67
|
+
"--json-only",
|
|
68
|
+
help="Emit JSON to stdout only (implies --no-reports and --no-markdown)",
|
|
69
|
+
),
|
|
70
|
+
structured_logs: bool = typer.Option(
|
|
71
|
+
False, "--structured-logs/--no-structured-logs", help="Emit structured JSON logs to stderr"
|
|
72
|
+
),
|
|
73
|
+
) -> None:
|
|
74
|
+
resolved = report_dir.resolve() if report_dir else _REPORT_DIR
|
|
75
|
+
# json-only overrides report/markdown flags
|
|
76
|
+
if json_only:
|
|
77
|
+
markdown = False
|
|
78
|
+
reports = False
|
|
79
|
+
if structured_logs:
|
|
80
|
+
structlog.configure(
|
|
81
|
+
processors=[
|
|
82
|
+
structlog.processors.TimeStamper(fmt="iso"),
|
|
83
|
+
structlog.stdlib.add_log_level,
|
|
84
|
+
structlog.processors.StackInfoRenderer(),
|
|
85
|
+
structlog.processors.format_exc_info,
|
|
86
|
+
structlog.processors.JSONRenderer(),
|
|
87
|
+
],
|
|
88
|
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
|
89
|
+
wrapper_class=structlog.stdlib.BoundLogger,
|
|
90
|
+
cache_logger_on_first_use=True,
|
|
91
|
+
)
|
|
92
|
+
else:
|
|
93
|
+
import logging
|
|
94
|
+
|
|
95
|
+
if json_only:
|
|
96
|
+
logging.basicConfig(level=logging.CRITICAL, stream=sys.stderr)
|
|
97
|
+
structlog.configure(wrapper_class=structlog.make_filtering_bound_logger(logging.CRITICAL))
|
|
98
|
+
else:
|
|
99
|
+
logging.basicConfig(level=logging.INFO, stream=sys.stderr, format="%(levelname)s %(name)s: %(message)s")
|
|
100
|
+
|
|
101
|
+
# store in global state
|
|
102
|
+
global _cli_state
|
|
103
|
+
_cli_state = {"report_dir": resolved, "markdown": markdown, "reports": reports}
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _build_client(settings: BitbucketSettings) -> BitbucketClient:
|
|
107
|
+
"""Build and return a configured BitbucketClient."""
|
|
108
|
+
return BitbucketClient(settings)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
@app.command("fork")
|
|
112
|
+
def fork(
|
|
113
|
+
action: str = typer.Argument(..., help="Action: create, list"),
|
|
114
|
+
project_key: str = typer.Argument(..., help="Source project key"),
|
|
115
|
+
repository_slug: str = typer.Argument(..., help="Source repository slug"),
|
|
116
|
+
new_project_key: str | None = typer.Option(
|
|
117
|
+
None, "--new-project", help="Target project key (for create, if forking to new project)"
|
|
118
|
+
),
|
|
119
|
+
new_repository_slug: str | None = typer.Option(None, "--new-repo", help="Target repository slug (for create)"),
|
|
120
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operation (for create)"),
|
|
121
|
+
) -> None:
|
|
122
|
+
"""Fork repository operations."""
|
|
123
|
+
log = structlog.get_logger(__name__)
|
|
124
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
125
|
+
markdown = _cli_state.get("markdown", True)
|
|
126
|
+
reports = _cli_state.get("reports", True)
|
|
127
|
+
|
|
128
|
+
try:
|
|
129
|
+
settings = load_settings(strict=True)
|
|
130
|
+
with _build_client(settings) as client:
|
|
131
|
+
if action == "create":
|
|
132
|
+
if not yes:
|
|
133
|
+
raise typer.BadParameter("Refusing to fork repository without --yes")
|
|
134
|
+
result = client.fork_repository(project_key, repository_slug, new_project_key, new_repository_slug)
|
|
135
|
+
|
|
136
|
+
if reports and markdown:
|
|
137
|
+
output_path = report_dir / f"{project_key}_{repository_slug}_forked.md"
|
|
138
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
139
|
+
|
|
140
|
+
with output_path.open("w") as f:
|
|
141
|
+
f.write("# Repository Forked\n\n")
|
|
142
|
+
f.write(f"- **Source**: {project_key}/{repository_slug}\n")
|
|
143
|
+
if new_project_key:
|
|
144
|
+
f.write(f"- **Target Project**: {new_project_key}\n")
|
|
145
|
+
if new_repository_slug:
|
|
146
|
+
f.write(f"- **Target Repository**: {new_repository_slug}\n")
|
|
147
|
+
f.write(f"- **Forked Repository**: {result.get('slug', 'N/A')}\n")
|
|
148
|
+
|
|
149
|
+
log.info(
|
|
150
|
+
"fork_reported",
|
|
151
|
+
path=str(output_path),
|
|
152
|
+
project_key=project_key,
|
|
153
|
+
repository=repository_slug,
|
|
154
|
+
)
|
|
155
|
+
|
|
156
|
+
payload = {
|
|
157
|
+
"status": "success",
|
|
158
|
+
"source_project": project_key,
|
|
159
|
+
"source_repository": repository_slug,
|
|
160
|
+
"forked_repository": result,
|
|
161
|
+
}
|
|
162
|
+
_emit_json(payload)
|
|
163
|
+
elif action == "list":
|
|
164
|
+
result = client.get_forked_repositories(project_key, repository_slug)
|
|
165
|
+
|
|
166
|
+
if reports and markdown:
|
|
167
|
+
output_path = report_dir / f"{project_key}_{repository_slug}_forks.md"
|
|
168
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
169
|
+
|
|
170
|
+
with output_path.open("w") as f:
|
|
171
|
+
f.write(f"# Forks of {project_key}/{repository_slug}\n\n")
|
|
172
|
+
f.write(f"Found {len(result)} forks:\n\n")
|
|
173
|
+
for fork in result:
|
|
174
|
+
name = fork.get("name", "N/A")
|
|
175
|
+
slug = fork.get("slug", "N/A")
|
|
176
|
+
f.write(f"- **{slug}**: {name}\n")
|
|
177
|
+
|
|
178
|
+
log.info(
|
|
179
|
+
"forks_reported",
|
|
180
|
+
path=str(output_path),
|
|
181
|
+
project_key=project_key,
|
|
182
|
+
repository=repository_slug,
|
|
183
|
+
count=len(result),
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
payload = {
|
|
187
|
+
"source_project": project_key,
|
|
188
|
+
"source_repository": repository_slug,
|
|
189
|
+
"forks": result,
|
|
190
|
+
"count": len(result),
|
|
191
|
+
}
|
|
192
|
+
_emit_json(payload)
|
|
193
|
+
else:
|
|
194
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
195
|
+
except (BitbucketClientError, typer.BadParameter) as exc:
|
|
196
|
+
log.error("bitbucket_error", message=str(exc), context=getattr(exc, "context", {}))
|
|
197
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
198
|
+
raise typer.Exit(1) from None
|
|
199
|
+
except Exception as exc:
|
|
200
|
+
log.error("unexpected_error", message=str(exc))
|
|
201
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
202
|
+
raise typer.Exit(1) from None
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
@app.command()
|
|
206
|
+
def projects(
|
|
207
|
+
limit: int = typer.Option(25, "--limit", help="Maximum number of projects to list"),
|
|
208
|
+
) -> None:
|
|
209
|
+
"""List all Bitbucket projects."""
|
|
210
|
+
log = structlog.get_logger(__name__)
|
|
211
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
212
|
+
markdown = _cli_state.get("markdown", True)
|
|
213
|
+
reports = _cli_state.get("reports", True)
|
|
214
|
+
|
|
215
|
+
try:
|
|
216
|
+
settings = load_settings(strict=True)
|
|
217
|
+
with _build_client(settings) as client:
|
|
218
|
+
projects = client.list_projects(limit=limit)
|
|
219
|
+
|
|
220
|
+
if reports and markdown:
|
|
221
|
+
# Emit markdown summary
|
|
222
|
+
output_path = report_dir / "projects.md"
|
|
223
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
224
|
+
|
|
225
|
+
with output_path.open("w") as f:
|
|
226
|
+
f.write("# Bitbucket Projects\n\n")
|
|
227
|
+
f.write(f"Found {len(projects)} projects:\n\n")
|
|
228
|
+
for project in projects:
|
|
229
|
+
key = project.get("key", "N/A")
|
|
230
|
+
name = project.get("name", "N/A")
|
|
231
|
+
f.write(f"- **{key}**: {name}\n")
|
|
232
|
+
|
|
233
|
+
log.info("projects_reported", path=str(output_path), count=len(projects))
|
|
234
|
+
|
|
235
|
+
_emit_json({"projects": projects})
|
|
236
|
+
|
|
237
|
+
except BitbucketClientError as exc:
|
|
238
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
239
|
+
raise typer.Exit(1) from None
|
|
240
|
+
except Exception as exc:
|
|
241
|
+
log.error("unexpected_error", message=str(exc))
|
|
242
|
+
raise typer.Exit(1) from None
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
@app.command()
|
|
246
|
+
def create_project(
|
|
247
|
+
key: str = typer.Argument(..., help="Project key"),
|
|
248
|
+
name: str = typer.Argument(..., help="Project name"),
|
|
249
|
+
description: str = typer.Option(
|
|
250
|
+
"Project created via VDS orchestrator", "--description", help="Project description"
|
|
251
|
+
),
|
|
252
|
+
) -> None:
|
|
253
|
+
"""Create a new Bitbucket project."""
|
|
254
|
+
log = structlog.get_logger(__name__)
|
|
255
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
256
|
+
markdown = _cli_state.get("markdown", True)
|
|
257
|
+
reports = _cli_state.get("reports", True)
|
|
258
|
+
|
|
259
|
+
try:
|
|
260
|
+
settings = load_settings(strict=True)
|
|
261
|
+
with _build_client(settings) as client:
|
|
262
|
+
result = client.create_project(key, name, description)
|
|
263
|
+
|
|
264
|
+
if reports and markdown:
|
|
265
|
+
output_path = report_dir / "project_created.md"
|
|
266
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
267
|
+
|
|
268
|
+
with output_path.open("w") as f:
|
|
269
|
+
f.write("# Project Created\n\n")
|
|
270
|
+
f.write(f"- **Key**: {result.get('key', key)}\n")
|
|
271
|
+
f.write(f"- **Name**: {result.get('name', name)}\n")
|
|
272
|
+
f.write(f"- **Description**: {result.get('description', description)}\n")
|
|
273
|
+
|
|
274
|
+
log.info("project_created_reported", path=str(output_path), key=key)
|
|
275
|
+
|
|
276
|
+
_emit_json(result)
|
|
277
|
+
|
|
278
|
+
except BitbucketClientError as exc:
|
|
279
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
280
|
+
raise typer.Exit(1) from None
|
|
281
|
+
except Exception as exc:
|
|
282
|
+
log.error("unexpected_error", message=str(exc))
|
|
283
|
+
raise typer.Exit(1) from None
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
@app.command()
|
|
287
|
+
def repositories(
|
|
288
|
+
project_key: str = typer.Argument(..., help="Project key"),
|
|
289
|
+
limit: int = typer.Option(25, "--limit", help="Maximum number of repositories to list"),
|
|
290
|
+
) -> None:
|
|
291
|
+
"""List repositories in a project."""
|
|
292
|
+
log = structlog.get_logger(__name__)
|
|
293
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
294
|
+
markdown = _cli_state.get("markdown", True)
|
|
295
|
+
reports = _cli_state.get("reports", True)
|
|
296
|
+
|
|
297
|
+
try:
|
|
298
|
+
settings = load_settings(strict=True)
|
|
299
|
+
with _build_client(settings) as client:
|
|
300
|
+
repos = client.list_repositories(project_key, limit=limit)
|
|
301
|
+
|
|
302
|
+
if reports and markdown:
|
|
303
|
+
output_path = report_dir / f"{project_key}_repositories.md"
|
|
304
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
305
|
+
|
|
306
|
+
with output_path.open("w") as f:
|
|
307
|
+
f.write(f"# {project_key} Repositories\n\n")
|
|
308
|
+
f.write(f"Found {len(repos)} repositories:\n\n")
|
|
309
|
+
for repo in repos:
|
|
310
|
+
name = repo.get("name", "N/A")
|
|
311
|
+
slug = repo.get("slug", "N/A")
|
|
312
|
+
f.write(f"- **{slug}**: {name}\n")
|
|
313
|
+
|
|
314
|
+
log.info("repositories_reported", path=str(output_path), project_key=project_key, count=len(repos))
|
|
315
|
+
|
|
316
|
+
_emit_json({"repositories": repos})
|
|
317
|
+
|
|
318
|
+
except BitbucketClientError as exc:
|
|
319
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
320
|
+
raise typer.Exit(1) from None
|
|
321
|
+
except Exception as exc:
|
|
322
|
+
log.error("unexpected_error", message=str(exc))
|
|
323
|
+
raise typer.Exit(1) from None
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
@app.command()
|
|
327
|
+
def pull_requests(
|
|
328
|
+
project_key: str = typer.Argument(..., help="Project key"),
|
|
329
|
+
repository_slug: str = typer.Argument(..., help="Repository slug"),
|
|
330
|
+
state: str = typer.Option("OPEN", "--state", help="PR state (OPEN, MERGED, DECLINED)"),
|
|
331
|
+
order: str = typer.Option("newest", "--order", help="Sort order (newest, oldest)"),
|
|
332
|
+
limit: int = typer.Option(100, "--limit", help="Maximum number of PRs to list"),
|
|
333
|
+
start: int = typer.Option(0, "--start", help="Start index for pagination"),
|
|
334
|
+
) -> None:
|
|
335
|
+
"""List pull requests for a repository."""
|
|
336
|
+
log = structlog.get_logger(__name__)
|
|
337
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
338
|
+
markdown = _cli_state.get("markdown", True)
|
|
339
|
+
reports = _cli_state.get("reports", True)
|
|
340
|
+
|
|
341
|
+
try:
|
|
342
|
+
settings = load_settings(strict=True)
|
|
343
|
+
with _build_client(settings) as client:
|
|
344
|
+
prs = client.list_pull_requests(
|
|
345
|
+
project_key, repository_slug, state=state, order=order, limit=limit, start=start
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
if reports and markdown:
|
|
349
|
+
output_path = report_dir / f"{project_key}_{repository_slug}_pull_requests.md"
|
|
350
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
351
|
+
|
|
352
|
+
with output_path.open("w") as f:
|
|
353
|
+
f.write(f"# Pull Requests - {project_key}/{repository_slug}\n\n")
|
|
354
|
+
f.write(f"Found {len(prs)} {state.lower()} pull requests:\n\n")
|
|
355
|
+
for pr in prs:
|
|
356
|
+
title = pr.get("title", "N/A")
|
|
357
|
+
pr_id = pr.get("id", "N/A")
|
|
358
|
+
author = pr.get("author", {}).get("displayName", "N/A")
|
|
359
|
+
f.write(f"- **#{pr_id}**: {title} (by {author})\n")
|
|
360
|
+
|
|
361
|
+
log.info(
|
|
362
|
+
"pull_requests_reported",
|
|
363
|
+
path=str(output_path),
|
|
364
|
+
project_key=project_key,
|
|
365
|
+
repository=repository_slug,
|
|
366
|
+
count=len(prs),
|
|
367
|
+
)
|
|
368
|
+
|
|
369
|
+
_emit_json({"pull_requests": prs})
|
|
370
|
+
|
|
371
|
+
except BitbucketClientError as exc:
|
|
372
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
373
|
+
raise typer.Exit(1) from None
|
|
374
|
+
except Exception as exc:
|
|
375
|
+
log.error("unexpected_error", message=str(exc))
|
|
376
|
+
raise typer.Exit(1) from None
|
|
377
|
+
|
|
378
|
+
|
|
379
|
+
@app.command()
|
|
380
|
+
def pipelines(
|
|
381
|
+
workspace: str = typer.Argument(..., help="Workspace (Bitbucket Cloud only)"),
|
|
382
|
+
repository: str = typer.Argument(..., help="Repository name"),
|
|
383
|
+
limit: int = typer.Option(10, "--limit", help="Maximum number of pipelines to list"),
|
|
384
|
+
) -> None:
|
|
385
|
+
"""List pipelines (Bitbucket Cloud only)."""
|
|
386
|
+
log = structlog.get_logger(__name__)
|
|
387
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
388
|
+
markdown = _cli_state.get("markdown", True)
|
|
389
|
+
reports = _cli_state.get("reports", True)
|
|
390
|
+
|
|
391
|
+
try:
|
|
392
|
+
settings = load_settings(strict=True)
|
|
393
|
+
if not settings.is_cloud_mode:
|
|
394
|
+
log.error("cloud_mode_required", message="Pipelines are only available in Bitbucket Cloud mode")
|
|
395
|
+
raise typer.Exit(1) from None
|
|
396
|
+
|
|
397
|
+
with _build_client(settings) as client:
|
|
398
|
+
pipelines = client.list_pipelines(workspace, repository, limit=limit)
|
|
399
|
+
|
|
400
|
+
if reports and markdown:
|
|
401
|
+
output_path = report_dir / f"{workspace}_{repository}_pipelines.md"
|
|
402
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
403
|
+
|
|
404
|
+
with output_path.open("w") as f:
|
|
405
|
+
f.write(f"# Pipelines - {workspace}/{repository}\n\n")
|
|
406
|
+
f.write(f"Found {len(pipelines)} pipelines:\n\n")
|
|
407
|
+
for pipeline in pipelines:
|
|
408
|
+
uuid = pipeline.get("uuid", "N/A")
|
|
409
|
+
state = pipeline.get("state", {}).get("name", "N/A")
|
|
410
|
+
f.write(f"- **{uuid}**: {state}\n")
|
|
411
|
+
|
|
412
|
+
log.info(
|
|
413
|
+
"pipelines_reported",
|
|
414
|
+
path=str(output_path),
|
|
415
|
+
workspace=workspace,
|
|
416
|
+
repository=repository,
|
|
417
|
+
count=len(pipelines),
|
|
418
|
+
)
|
|
419
|
+
|
|
420
|
+
_emit_json({"pipelines": pipelines})
|
|
421
|
+
|
|
422
|
+
except BitbucketClientError as exc:
|
|
423
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
424
|
+
raise typer.Exit(1) from None
|
|
425
|
+
except Exception as exc:
|
|
426
|
+
log.error("unexpected_error", message=str(exc))
|
|
427
|
+
raise typer.Exit(1) from None
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
@app.command()
|
|
431
|
+
def groups(
|
|
432
|
+
filter: str | None = typer.Option(None, "--filter", help="Filter string to search for specific groups"),
|
|
433
|
+
limit: int = typer.Option(50, "--limit", help="Maximum number of groups to list"),
|
|
434
|
+
) -> None:
|
|
435
|
+
"""List all available Bitbucket groups."""
|
|
436
|
+
log = structlog.get_logger(__name__)
|
|
437
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
438
|
+
markdown = _cli_state.get("markdown", True)
|
|
439
|
+
reports = _cli_state.get("reports", True)
|
|
440
|
+
|
|
441
|
+
try:
|
|
442
|
+
settings = load_settings(strict=True)
|
|
443
|
+
with _build_client(settings) as client:
|
|
444
|
+
groups = client.list_groups(group_filter=filter, limit=limit)
|
|
445
|
+
|
|
446
|
+
if reports and markdown:
|
|
447
|
+
output_path = report_dir / "groups.md"
|
|
448
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
449
|
+
|
|
450
|
+
with output_path.open("w") as f:
|
|
451
|
+
f.write("# Bitbucket Groups\n\n")
|
|
452
|
+
f.write(f"Found {len(groups)} groups:\n\n")
|
|
453
|
+
for group in groups:
|
|
454
|
+
f.write(f"- **{group}**\n")
|
|
455
|
+
|
|
456
|
+
log.info("groups_reported", path=str(output_path), count=len(groups))
|
|
457
|
+
|
|
458
|
+
_emit_json({"groups": groups})
|
|
459
|
+
|
|
460
|
+
except BitbucketClientError as exc:
|
|
461
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
462
|
+
raise typer.Exit(1) from None
|
|
463
|
+
except Exception as exc:
|
|
464
|
+
log.error("unexpected_error", message=str(exc))
|
|
465
|
+
raise typer.Exit(1) from None
|
|
466
|
+
|
|
467
|
+
|
|
468
|
+
@app.command()
|
|
469
|
+
def project_users(
|
|
470
|
+
project_key: str = typer.Argument(..., help="Project key"),
|
|
471
|
+
filter: str | None = typer.Option(None, "--filter", help="Filter string to search for specific users"),
|
|
472
|
+
limit: int = typer.Option(25, "--limit", help="Maximum number of users to list"),
|
|
473
|
+
) -> None:
|
|
474
|
+
"""List users with permissions for a project."""
|
|
475
|
+
log = structlog.get_logger(__name__)
|
|
476
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
477
|
+
markdown = _cli_state.get("markdown", True)
|
|
478
|
+
reports = _cli_state.get("reports", True)
|
|
479
|
+
|
|
480
|
+
try:
|
|
481
|
+
settings = load_settings(strict=True)
|
|
482
|
+
with _build_client(settings) as client:
|
|
483
|
+
users = client.list_project_users(project_key, filter_str=filter, limit=limit)
|
|
484
|
+
|
|
485
|
+
if reports and markdown:
|
|
486
|
+
output_path = report_dir / f"{project_key}_users.md"
|
|
487
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
488
|
+
|
|
489
|
+
with output_path.open("w") as f:
|
|
490
|
+
f.write(f"# {project_key} Project Users\n\n")
|
|
491
|
+
f.write(f"Found {len(users)} users:\n\n")
|
|
492
|
+
for user in users:
|
|
493
|
+
name = user.get("name", "N/A")
|
|
494
|
+
email = user.get("emailAddress", "N/A")
|
|
495
|
+
permission = user.get("permission", "N/A")
|
|
496
|
+
f.write(f"- **{name}** ({email}) - {permission}\n")
|
|
497
|
+
|
|
498
|
+
log.info("project_users_reported", path=str(output_path), project_key=project_key, count=len(users))
|
|
499
|
+
|
|
500
|
+
_emit_json({"users": users})
|
|
501
|
+
|
|
502
|
+
except BitbucketClientError as exc:
|
|
503
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
504
|
+
raise typer.Exit(1) from None
|
|
505
|
+
except Exception as exc:
|
|
506
|
+
log.error("unexpected_error", message=str(exc))
|
|
507
|
+
raise typer.Exit(1) from None
|
|
508
|
+
|
|
509
|
+
|
|
510
|
+
@app.command()
|
|
511
|
+
def project_groups(
|
|
512
|
+
project_key: str = typer.Argument(..., help="Project key"),
|
|
513
|
+
filter: str | None = typer.Option(None, "--filter", help="Filter string to search for specific groups"),
|
|
514
|
+
limit: int = typer.Option(25, "--limit", help="Maximum number of groups to list"),
|
|
515
|
+
) -> None:
|
|
516
|
+
"""List groups with permissions for a project."""
|
|
517
|
+
log = structlog.get_logger(__name__)
|
|
518
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
519
|
+
markdown = _cli_state.get("markdown", True)
|
|
520
|
+
reports = _cli_state.get("reports", True)
|
|
521
|
+
|
|
522
|
+
try:
|
|
523
|
+
settings = load_settings(strict=True)
|
|
524
|
+
with _build_client(settings) as client:
|
|
525
|
+
groups = client.list_project_groups(project_key, filter_str=filter, limit=limit)
|
|
526
|
+
|
|
527
|
+
if reports and markdown:
|
|
528
|
+
output_path = report_dir / f"{project_key}_groups.md"
|
|
529
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
530
|
+
|
|
531
|
+
with output_path.open("w") as f:
|
|
532
|
+
f.write(f"# {project_key} Project Groups\n\n")
|
|
533
|
+
f.write(f"Found {len(groups)} groups:\n\n")
|
|
534
|
+
for group in groups:
|
|
535
|
+
f.write(f"- **{group}**\n")
|
|
536
|
+
|
|
537
|
+
log.info("project_groups_reported", path=str(output_path), project_key=project_key, count=len(groups))
|
|
538
|
+
|
|
539
|
+
_emit_json({"groups": groups})
|
|
540
|
+
|
|
541
|
+
except BitbucketClientError as exc:
|
|
542
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
543
|
+
raise typer.Exit(1) from None
|
|
544
|
+
except Exception as exc:
|
|
545
|
+
log.error("unexpected_error", message=str(exc))
|
|
546
|
+
raise typer.Exit(1) from None
|
|
547
|
+
|
|
548
|
+
|
|
549
|
+
@app.command()
|
|
550
|
+
def grant_group_repo(
|
|
551
|
+
project_key: str = typer.Argument(..., help="Project key"),
|
|
552
|
+
repository_slug: str = typer.Argument(..., help="Repository slug"),
|
|
553
|
+
group_name: str = typer.Argument(..., help="Group name"),
|
|
554
|
+
permission: str = typer.Argument(..., help="Permission level (read, write, admin)"),
|
|
555
|
+
) -> None:
|
|
556
|
+
"""Grant group permissions to a repository."""
|
|
557
|
+
log = structlog.get_logger(__name__)
|
|
558
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
559
|
+
markdown = _cli_state.get("markdown", True)
|
|
560
|
+
|
|
561
|
+
try:
|
|
562
|
+
settings = load_settings(strict=True)
|
|
563
|
+
with _build_client(settings) as client:
|
|
564
|
+
result = client.grant_group_repository_permissions(project_key, repository_slug, group_name, permission)
|
|
565
|
+
|
|
566
|
+
if _cli_state.get("reports", True) and markdown:
|
|
567
|
+
output_path = report_dir / f"{project_key}_{repository_slug}_group_granted.md"
|
|
568
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
569
|
+
|
|
570
|
+
with output_path.open("w") as f:
|
|
571
|
+
f.write("# Group Permission Granted\n\n")
|
|
572
|
+
f.write(f"- **Project**: {project_key}\n")
|
|
573
|
+
f.write(f"- **Repository**: {repository_slug}\n")
|
|
574
|
+
f.write(f"- **Group**: {group_name}\n")
|
|
575
|
+
f.write(f"- **Permission**: {permission}\n")
|
|
576
|
+
f.write("- **Result**: Success\n")
|
|
577
|
+
|
|
578
|
+
log.info(
|
|
579
|
+
"group_permission_granted",
|
|
580
|
+
path=str(output_path),
|
|
581
|
+
project_key=project_key,
|
|
582
|
+
repository=repository_slug,
|
|
583
|
+
group=group_name,
|
|
584
|
+
permission=permission,
|
|
585
|
+
)
|
|
586
|
+
|
|
587
|
+
_emit_json(result)
|
|
588
|
+
|
|
589
|
+
except BitbucketClientError as exc:
|
|
590
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
591
|
+
raise typer.Exit(1) from None
|
|
592
|
+
except Exception as exc:
|
|
593
|
+
log.error("unexpected_error", message=str(exc))
|
|
594
|
+
raise typer.Exit(1) from None
|
|
595
|
+
|
|
596
|
+
|
|
597
|
+
@app.command()
|
|
598
|
+
def branch_permissions(
|
|
599
|
+
action: str = typer.Argument(..., help="Action: get, set, delete"),
|
|
600
|
+
project_key: str = typer.Argument(..., help="Project key"),
|
|
601
|
+
permission_id: int | None = typer.Option(None, "--permission-id", help="Permission ID (for get/delete)"),
|
|
602
|
+
repository_slug: str | None = typer.Option(None, "--repo", help="Repository slug"),
|
|
603
|
+
matcher_type: str | None = typer.Option(None, "--matcher-type", help="Matcher type (for set)"),
|
|
604
|
+
matcher_value: str | None = typer.Option(None, "--matcher-value", help="Matcher value (for set)"),
|
|
605
|
+
permission_type: str | None = typer.Option(None, "--permission-type", help="Permission type (for set)"),
|
|
606
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operation"),
|
|
607
|
+
) -> None:
|
|
608
|
+
"""Branch permissions management."""
|
|
609
|
+
log = structlog.get_logger(__name__)
|
|
610
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
611
|
+
markdown = _cli_state.get("markdown", True)
|
|
612
|
+
reports = _cli_state.get("reports", True)
|
|
613
|
+
|
|
614
|
+
try:
|
|
615
|
+
settings = load_settings(strict=True)
|
|
616
|
+
with _build_client(settings) as client:
|
|
617
|
+
if action == "get":
|
|
618
|
+
if not permission_id:
|
|
619
|
+
raise typer.BadParameter("--permission-id required for get")
|
|
620
|
+
result = client.get_branch_permission(project_key, permission_id, repository_slug=repository_slug)
|
|
621
|
+
elif action == "set":
|
|
622
|
+
if not yes:
|
|
623
|
+
raise typer.BadParameter("Refusing to set permissions without --yes")
|
|
624
|
+
if not matcher_type or not matcher_value or not permission_type:
|
|
625
|
+
raise typer.BadParameter("--matcher-type, --matcher-value, and --permission-type required for set")
|
|
626
|
+
result = client.set_branches_permissions(
|
|
627
|
+
project_key,
|
|
628
|
+
matcher_type=matcher_type,
|
|
629
|
+
matcher_value=matcher_value,
|
|
630
|
+
permission_type=permission_type,
|
|
631
|
+
repository_slug=repository_slug,
|
|
632
|
+
)
|
|
633
|
+
elif action == "delete":
|
|
634
|
+
if not yes:
|
|
635
|
+
raise typer.BadParameter("Refusing to delete without --yes")
|
|
636
|
+
if not permission_id:
|
|
637
|
+
raise typer.BadParameter("--permission-id required for delete")
|
|
638
|
+
client.delete_branch_permission(project_key, permission_id, repository_slug=repository_slug)
|
|
639
|
+
result = {"deleted": permission_id}
|
|
640
|
+
else:
|
|
641
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
642
|
+
|
|
643
|
+
if reports and markdown:
|
|
644
|
+
output_path = report_dir / f"{project_key}_branch_permissions_{action}.md"
|
|
645
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
646
|
+
with output_path.open("w") as f:
|
|
647
|
+
f.write(f"# Branch Permissions - {action}\n\n")
|
|
648
|
+
f.write(f"- **Project**: {project_key}\n")
|
|
649
|
+
if repository_slug:
|
|
650
|
+
f.write(f"- **Repository**: {repository_slug}\n")
|
|
651
|
+
f.write(f"- **Action**: {action}\n")
|
|
652
|
+
log.info("branch_permissions_reported", path=str(output_path), action=action)
|
|
653
|
+
|
|
654
|
+
_emit_json(result)
|
|
655
|
+
|
|
656
|
+
except BitbucketClientError as exc:
|
|
657
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
658
|
+
raise typer.Exit(1) from None
|
|
659
|
+
except Exception as exc:
|
|
660
|
+
log.error("unexpected_error", message=str(exc))
|
|
661
|
+
raise typer.Exit(1) from None
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
@app.command()
|
|
665
|
+
def conditions(
|
|
666
|
+
action: str = typer.Argument(..., help="Action: list, get, create, update, delete"),
|
|
667
|
+
project_key: str = typer.Argument(..., help="Project key"),
|
|
668
|
+
repository_slug: str | None = typer.Option(None, "--repo", help="Repository slug (for repo conditions)"),
|
|
669
|
+
condition_id: int | None = typer.Option(None, "--condition-id", help="Condition ID (for get/update/delete)"),
|
|
670
|
+
condition_file: Path | None = typer.Option(None, "--condition-file", help="JSON file with condition data (for create/update)"),
|
|
671
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operation"),
|
|
672
|
+
) -> None:
|
|
673
|
+
"""Conditions-Reviewers management."""
|
|
674
|
+
log = structlog.get_logger(__name__)
|
|
675
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
676
|
+
markdown = _cli_state.get("markdown", True)
|
|
677
|
+
reports = _cli_state.get("reports", True)
|
|
678
|
+
|
|
679
|
+
try:
|
|
680
|
+
import json
|
|
681
|
+
|
|
682
|
+
settings = load_settings(strict=True)
|
|
683
|
+
with _build_client(settings) as client:
|
|
684
|
+
if action == "list":
|
|
685
|
+
if repository_slug:
|
|
686
|
+
result = client.get_repo_conditions(project_key, repository_slug)
|
|
687
|
+
else:
|
|
688
|
+
result = client.get_project_conditions(project_key)
|
|
689
|
+
elif action == "get":
|
|
690
|
+
if not condition_id:
|
|
691
|
+
raise typer.BadParameter("--condition-id required for get")
|
|
692
|
+
if repository_slug:
|
|
693
|
+
result = client.get_repo_condition(project_key, repository_slug, condition_id)
|
|
694
|
+
else:
|
|
695
|
+
result = client.get_project_condition(project_key, condition_id)
|
|
696
|
+
elif action == "create":
|
|
697
|
+
if not yes:
|
|
698
|
+
raise typer.BadParameter("Refusing to create without --yes")
|
|
699
|
+
if not condition_file:
|
|
700
|
+
raise typer.BadParameter("--condition-file required for create")
|
|
701
|
+
with condition_file.open() as f:
|
|
702
|
+
condition_data = json.load(f)
|
|
703
|
+
if repository_slug:
|
|
704
|
+
result = client.create_repo_condition(project_key, repository_slug, condition_data)
|
|
705
|
+
else:
|
|
706
|
+
result = client.create_project_condition(project_key, condition_data)
|
|
707
|
+
elif action == "update":
|
|
708
|
+
if not yes:
|
|
709
|
+
raise typer.BadParameter("Refusing to update without --yes")
|
|
710
|
+
if not condition_id or not condition_file:
|
|
711
|
+
raise typer.BadParameter("--condition-id and --condition-file required for update")
|
|
712
|
+
with condition_file.open() as f:
|
|
713
|
+
condition_data = json.load(f)
|
|
714
|
+
if repository_slug:
|
|
715
|
+
result = client.update_repo_condition(project_key, repository_slug, condition_data, condition_id)
|
|
716
|
+
else:
|
|
717
|
+
result = client.update_project_condition(project_key, condition_data, condition_id)
|
|
718
|
+
elif action == "delete":
|
|
719
|
+
if not yes:
|
|
720
|
+
raise typer.BadParameter("Refusing to delete without --yes")
|
|
721
|
+
if not condition_id:
|
|
722
|
+
raise typer.BadParameter("--condition-id required for delete")
|
|
723
|
+
if repository_slug:
|
|
724
|
+
client.delete_repo_condition(project_key, repository_slug, condition_id)
|
|
725
|
+
else:
|
|
726
|
+
client.delete_project_condition(project_key, condition_id)
|
|
727
|
+
result = {"deleted": condition_id}
|
|
728
|
+
else:
|
|
729
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
730
|
+
|
|
731
|
+
if reports and markdown:
|
|
732
|
+
output_path = report_dir / f"{project_key}_conditions_{action}.md"
|
|
733
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
734
|
+
with output_path.open("w") as f:
|
|
735
|
+
f.write(f"# Conditions - {action}\n\n")
|
|
736
|
+
f.write(f"- **Project**: {project_key}\n")
|
|
737
|
+
if repository_slug:
|
|
738
|
+
f.write(f"- **Repository**: {repository_slug}\n")
|
|
739
|
+
f.write(f"- **Action**: {action}\n")
|
|
740
|
+
log.info("conditions_reported", path=str(output_path), action=action)
|
|
741
|
+
|
|
742
|
+
_emit_json(result)
|
|
743
|
+
|
|
744
|
+
except BitbucketClientError as exc:
|
|
745
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
746
|
+
raise typer.Exit(1) from None
|
|
747
|
+
except Exception as exc:
|
|
748
|
+
log.error("unexpected_error", message=str(exc))
|
|
749
|
+
raise typer.Exit(1) from None
|
|
750
|
+
|
|
751
|
+
|
|
752
|
+
@app.command()
|
|
753
|
+
def code(
|
|
754
|
+
action: str = typer.Argument(..., help="Action: file, commits, diff, changelog"),
|
|
755
|
+
project_key: str = typer.Argument(..., help="Project key"),
|
|
756
|
+
repository_slug: str = typer.Argument(..., help="Repository slug"),
|
|
757
|
+
filename: str | None = typer.Option(None, "--file", help="File path (for file/diff)"),
|
|
758
|
+
at: str | None = typer.Option(None, "--at", help="Revision/ref (for file)"),
|
|
759
|
+
hash_oldest: str | None = typer.Option(None, "--hash-oldest", help="Oldest commit hash (for commits/diff/changelog)"),
|
|
760
|
+
hash_newest: str | None = typer.Option(None, "--hash-newest", help="Newest commit hash (for commits/diff/changelog)"),
|
|
761
|
+
ref_from: str | None = typer.Option(None, "--ref-from", help="From ref (for changelog)"),
|
|
762
|
+
ref_to: str | None = typer.Option(None, "--ref-to", help="To ref (for changelog)"),
|
|
763
|
+
limit: int = typer.Option(100, "--limit", help="Maximum number of results"),
|
|
764
|
+
) -> None:
|
|
765
|
+
"""Code/file content operations."""
|
|
766
|
+
log = structlog.get_logger(__name__)
|
|
767
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
768
|
+
markdown = _cli_state.get("markdown", True)
|
|
769
|
+
|
|
770
|
+
try:
|
|
771
|
+
settings = load_settings(strict=True)
|
|
772
|
+
with _build_client(settings) as client:
|
|
773
|
+
if action == "file":
|
|
774
|
+
if not filename:
|
|
775
|
+
raise typer.BadParameter("--file required for file action")
|
|
776
|
+
result = client.get_content_of_file(project_key, repository_slug, filename, at=at)
|
|
777
|
+
# For file content, emit as text
|
|
778
|
+
if _cli_state.get("reports", True) and markdown:
|
|
779
|
+
output_path = report_dir / f"{project_key}_{repository_slug}_{filename.replace('/', '_')}.txt"
|
|
780
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
781
|
+
with output_path.open("w") as f:
|
|
782
|
+
f.write(result)
|
|
783
|
+
log.info("file_content_reported", path=str(output_path))
|
|
784
|
+
else:
|
|
785
|
+
sys.stdout.write(result)
|
|
786
|
+
return
|
|
787
|
+
elif action == "commits":
|
|
788
|
+
result = client.get_commits(project_key, repository_slug, hash_oldest, hash_newest, limit=limit)
|
|
789
|
+
elif action == "diff":
|
|
790
|
+
if not filename or not hash_oldest or not hash_newest:
|
|
791
|
+
raise typer.BadParameter("--file, --hash-oldest, and --hash-newest required for diff")
|
|
792
|
+
result = client.get_diff(project_key, repository_slug, filename, hash_oldest, hash_newest)
|
|
793
|
+
elif action == "changelog":
|
|
794
|
+
if not ref_from or not ref_to:
|
|
795
|
+
raise typer.BadParameter("--ref-from and --ref-to required for changelog")
|
|
796
|
+
result = client.get_changelog(project_key, repository_slug, ref_from, ref_to, limit=limit)
|
|
797
|
+
else:
|
|
798
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
799
|
+
|
|
800
|
+
if _cli_state.get("reports", True) and markdown:
|
|
801
|
+
output_path = report_dir / f"{project_key}_{repository_slug}_code_{action}.md"
|
|
802
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
803
|
+
with output_path.open("w") as f:
|
|
804
|
+
f.write(f"# Code Operations - {action}\n\n")
|
|
805
|
+
f.write(f"- **Project**: {project_key}\n")
|
|
806
|
+
f.write(f"- **Repository**: {repository_slug}\n")
|
|
807
|
+
f.write(f"- **Action**: {action}\n")
|
|
808
|
+
log.info("code_operation_reported", path=str(output_path), action=action)
|
|
809
|
+
else:
|
|
810
|
+
_emit_json(result)
|
|
811
|
+
|
|
812
|
+
except BitbucketClientError as exc:
|
|
813
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
814
|
+
raise typer.Exit(1) from None
|
|
815
|
+
except Exception as exc:
|
|
816
|
+
log.error("unexpected_error", message=str(exc))
|
|
817
|
+
raise typer.Exit(1) from None
|
|
818
|
+
|
|
819
|
+
|
|
820
|
+
@app.command()
|
|
821
|
+
def webhooks(
|
|
822
|
+
action: str = typer.Argument(..., help="Action: list, get, create, update, delete"),
|
|
823
|
+
project_key: str = typer.Argument(..., help="Project key"),
|
|
824
|
+
repository_slug: str = typer.Argument(..., help="Repository slug"),
|
|
825
|
+
webhook_id: int | None = typer.Option(None, "--webhook-id", help="Webhook ID (get/update/delete)"),
|
|
826
|
+
name: str | None = typer.Option(None, "--name", help="Webhook name (create/update)"),
|
|
827
|
+
url: str | None = typer.Option(None, "--url", help="Webhook URL (create/update)"),
|
|
828
|
+
events: list[str] = typer.Option(
|
|
829
|
+
[],
|
|
830
|
+
"--event",
|
|
831
|
+
help="Webhook event key (repeatable). Required for create; optional for update.",
|
|
832
|
+
),
|
|
833
|
+
filter_event: str | None = typer.Option(
|
|
834
|
+
None, "--filter-event", help="Filter by event when listing repository webhooks"
|
|
835
|
+
),
|
|
836
|
+
statistics: bool = typer.Option(False, "--statistics/--no-statistics", help="Include webhook statistics when listing"),
|
|
837
|
+
active_flag: bool = typer.Option(True, "--active/--inactive", help="Set active flag when creating a webhook"),
|
|
838
|
+
set_active: bool | None = typer.Option(
|
|
839
|
+
None, "--set-active/--set-inactive", help="Set active flag when updating a webhook"
|
|
840
|
+
),
|
|
841
|
+
secret: str | None = typer.Option(None, "--secret", help="Webhook secret (create/update)"),
|
|
842
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operation"),
|
|
843
|
+
) -> None:
|
|
844
|
+
"""Manage Bitbucket Cloud repository webhooks."""
|
|
845
|
+
log = structlog.get_logger(__name__)
|
|
846
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
847
|
+
markdown = _cli_state.get("markdown", True)
|
|
848
|
+
|
|
849
|
+
try:
|
|
850
|
+
settings = load_settings(strict=True)
|
|
851
|
+
if not settings.is_cloud_mode:
|
|
852
|
+
log.error("cloud_mode_required", message="Webhooks are only available in Bitbucket Cloud mode")
|
|
853
|
+
raise typer.Exit(1) from None
|
|
854
|
+
|
|
855
|
+
with _build_client(settings) as client:
|
|
856
|
+
result: dict | None = None
|
|
857
|
+
if action == "list":
|
|
858
|
+
if events:
|
|
859
|
+
raise typer.BadParameter("--event is not applicable for list; use --filter-event")
|
|
860
|
+
payload = client.list_webhooks(
|
|
861
|
+
project_key,
|
|
862
|
+
repository_slug,
|
|
863
|
+
event=filter_event,
|
|
864
|
+
statistics=statistics,
|
|
865
|
+
)
|
|
866
|
+
result = {"webhooks": payload}
|
|
867
|
+
elif action == "get":
|
|
868
|
+
if webhook_id is None:
|
|
869
|
+
raise typer.BadParameter("--webhook-id required for get")
|
|
870
|
+
payload = client.get_webhook(project_key, repository_slug, webhook_id)
|
|
871
|
+
result = {"webhook": payload}
|
|
872
|
+
elif action == "create":
|
|
873
|
+
if not yes:
|
|
874
|
+
raise typer.BadParameter("Refusing to create webhook without --yes")
|
|
875
|
+
if not name:
|
|
876
|
+
raise typer.BadParameter("--name required for create")
|
|
877
|
+
if not url:
|
|
878
|
+
raise typer.BadParameter("--url required for create")
|
|
879
|
+
if not events:
|
|
880
|
+
raise typer.BadParameter("--event required at least once for create")
|
|
881
|
+
payload = client.create_webhook(
|
|
882
|
+
project_key,
|
|
883
|
+
repository_slug,
|
|
884
|
+
name,
|
|
885
|
+
events,
|
|
886
|
+
url,
|
|
887
|
+
active=active_flag,
|
|
888
|
+
secret=secret,
|
|
889
|
+
)
|
|
890
|
+
result = {"webhook": payload}
|
|
891
|
+
elif action == "update":
|
|
892
|
+
if not yes:
|
|
893
|
+
raise typer.BadParameter("Refusing to update webhook without --yes")
|
|
894
|
+
if webhook_id is None:
|
|
895
|
+
raise typer.BadParameter("--webhook-id required for update")
|
|
896
|
+
has_changes = any(
|
|
897
|
+
[
|
|
898
|
+
name is not None,
|
|
899
|
+
url is not None,
|
|
900
|
+
bool(events),
|
|
901
|
+
set_active is not None,
|
|
902
|
+
secret is not None,
|
|
903
|
+
]
|
|
904
|
+
)
|
|
905
|
+
if not has_changes:
|
|
906
|
+
raise typer.BadParameter("Must provide at least one field to update (name/url/event/secret/active)")
|
|
907
|
+
payload = client.update_webhook(
|
|
908
|
+
project_key,
|
|
909
|
+
repository_slug,
|
|
910
|
+
webhook_id,
|
|
911
|
+
name=name,
|
|
912
|
+
webhook_url=url,
|
|
913
|
+
events=events or None,
|
|
914
|
+
active=set_active,
|
|
915
|
+
secret=secret,
|
|
916
|
+
)
|
|
917
|
+
result = {"webhook": payload}
|
|
918
|
+
elif action == "delete":
|
|
919
|
+
if not yes:
|
|
920
|
+
raise typer.BadParameter("Refusing to delete webhook without --yes")
|
|
921
|
+
if webhook_id is None:
|
|
922
|
+
raise typer.BadParameter("--webhook-id required for delete")
|
|
923
|
+
client.delete_webhook(project_key, repository_slug, webhook_id)
|
|
924
|
+
result = {"deleted": webhook_id}
|
|
925
|
+
else:
|
|
926
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
927
|
+
|
|
928
|
+
if _cli_state.get("reports", True) and markdown:
|
|
929
|
+
output_path = report_dir / f"{project_key}_{repository_slug}_webhooks_{action}.md"
|
|
930
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
931
|
+
with output_path.open("w") as f:
|
|
932
|
+
f.write(f"# Webhooks - {action}\n\n")
|
|
933
|
+
f.write(f"- **Project**: {project_key}\n")
|
|
934
|
+
f.write(f"- **Repository**: {repository_slug}\n")
|
|
935
|
+
f.write(f"- **Action**: {action}\n")
|
|
936
|
+
if action == "list":
|
|
937
|
+
hooks = result.get("webhooks", []) if result else []
|
|
938
|
+
f.write(f"- **Count**: {len(hooks)}\n")
|
|
939
|
+
elif action in {"get", "create", "update"}:
|
|
940
|
+
hook = result.get("webhook", {}) if result else {}
|
|
941
|
+
f.write(f"- **Webhook ID**: {hook.get('id', 'N/A')}\n")
|
|
942
|
+
f.write(f"- **Name**: {hook.get('name', name)}\n")
|
|
943
|
+
f.write(f"- **URL**: {hook.get('url', url)}\n")
|
|
944
|
+
elif action == "delete":
|
|
945
|
+
f.write(f"- **Deleted ID**: {webhook_id}\n")
|
|
946
|
+
log.info("webhooks_reported", path=str(output_path), action=action)
|
|
947
|
+
else:
|
|
948
|
+
_emit_json(result or {})
|
|
949
|
+
|
|
950
|
+
except BitbucketClientError as exc:
|
|
951
|
+
log.error("bitbucket_error", message=str(exc), context=exc.context)
|
|
952
|
+
raise typer.Exit(1) from None
|
|
953
|
+
except Exception as exc:
|
|
954
|
+
log.error("unexpected_error", message=str(exc))
|
|
955
|
+
raise typer.Exit(1) from None
|
|
956
|
+
|
|
957
|
+
|
|
958
|
+
@app.command()
|
|
959
|
+
def code_insights(
|
|
960
|
+
action: str = typer.Argument(..., help="Action: create, get, annotate, delete"),
|
|
961
|
+
project_key: str = typer.Argument(..., help="Project key"),
|
|
962
|
+
repository_slug: str = typer.Argument(..., help="Repository slug"),
|
|
963
|
+
commit_id: str = typer.Argument(..., help="Commit hash"),
|
|
964
|
+
report_key: str | None = typer.Option(None, "--report-key", help="Report key (required for all actions)"),
|
|
965
|
+
report_title: str | None = typer.Option(None, "--title", help="Report title (for create)"),
|
|
966
|
+
reporter: str | None = typer.Option(None, "--reporter", help="Reporter name (for create)"),
|
|
967
|
+
result: str | None = typer.Option(None, "--result", help="Report result: PASSED, FAILED, PENDING (for create)"),
|
|
968
|
+
data_file: Path | None = typer.Option(None, "--data-file", help="Path to JSON file with report data (for create)"),
|
|
969
|
+
annotations_file: Path | None = typer.Option(
|
|
970
|
+
None, "--annotations-file", help="Path to JSON file with annotations (for annotate)"
|
|
971
|
+
),
|
|
972
|
+
report_type: str | None = typer.Option(None, "--report-type", help="Report type (e.g., SECURITY, COVERAGE, TEST)"),
|
|
973
|
+
details: str | None = typer.Option(None, "--details", help="Report details text"),
|
|
974
|
+
link_text: str | None = typer.Option(None, "--link-text", help="External link text"),
|
|
975
|
+
link_url: str | None = typer.Option(None, "--link-url", help="External link URL"),
|
|
976
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operation"),
|
|
977
|
+
) -> None:
|
|
978
|
+
"""Manage Bitbucket Cloud Code Insights reports."""
|
|
979
|
+
log = structlog.get_logger(__name__)
|
|
980
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
981
|
+
markdown = _cli_state.get("markdown", True)
|
|
982
|
+
|
|
983
|
+
try:
|
|
984
|
+
settings = load_settings(strict=True)
|
|
985
|
+
if not settings.is_cloud_mode:
|
|
986
|
+
log.error("cloud_mode_required", message="Code Insights are only available in Bitbucket Cloud mode")
|
|
987
|
+
raise typer.Exit(1) from None
|
|
988
|
+
|
|
989
|
+
with _build_client(settings) as client:
|
|
990
|
+
result_obj: dict | None = None
|
|
991
|
+
|
|
992
|
+
# Validate report_key is provided
|
|
993
|
+
if not report_key:
|
|
994
|
+
raise typer.BadParameter("--report-key required for all actions")
|
|
995
|
+
|
|
996
|
+
if action == "create":
|
|
997
|
+
if not yes:
|
|
998
|
+
raise typer.BadParameter("Refusing to create report without --yes")
|
|
999
|
+
if not report_title:
|
|
1000
|
+
raise typer.BadParameter("--title required for create")
|
|
1001
|
+
if not reporter:
|
|
1002
|
+
raise typer.BadParameter("--reporter required for create")
|
|
1003
|
+
if not result:
|
|
1004
|
+
raise typer.BadParameter("--result required for create")
|
|
1005
|
+
|
|
1006
|
+
# Load data from file if provided
|
|
1007
|
+
data = None
|
|
1008
|
+
if data_file:
|
|
1009
|
+
if not data_file.exists():
|
|
1010
|
+
raise typer.BadParameter(f"Data file not found: {data_file}")
|
|
1011
|
+
with data_file.open() as f:
|
|
1012
|
+
data = json.load(f)
|
|
1013
|
+
if not isinstance(data, list):
|
|
1014
|
+
raise typer.BadParameter("Data file must contain a JSON array")
|
|
1015
|
+
|
|
1016
|
+
# Build report parameters
|
|
1017
|
+
report_params: dict = {}
|
|
1018
|
+
if report_type:
|
|
1019
|
+
report_params["report_type"] = report_type
|
|
1020
|
+
if details:
|
|
1021
|
+
report_params["details"] = details
|
|
1022
|
+
if link_text and link_url:
|
|
1023
|
+
report_params["link"] = {"text": link_text, "href": link_url}
|
|
1024
|
+
|
|
1025
|
+
payload = client.create_code_insights_report(
|
|
1026
|
+
project_key=project_key,
|
|
1027
|
+
repository_slug=repository_slug,
|
|
1028
|
+
commit_id=commit_id,
|
|
1029
|
+
report_key=report_key,
|
|
1030
|
+
report_title=report_title,
|
|
1031
|
+
reporter=reporter,
|
|
1032
|
+
result=result,
|
|
1033
|
+
data=data,
|
|
1034
|
+
**report_params,
|
|
1035
|
+
)
|
|
1036
|
+
result_obj = {"report": payload}
|
|
1037
|
+
|
|
1038
|
+
elif action == "get":
|
|
1039
|
+
payload = client.get_code_insights_report(project_key, repository_slug, commit_id, report_key)
|
|
1040
|
+
result_obj = {"report": payload}
|
|
1041
|
+
|
|
1042
|
+
elif action == "annotate":
|
|
1043
|
+
if not yes:
|
|
1044
|
+
raise typer.BadParameter("Refusing to add annotations without --yes")
|
|
1045
|
+
if not annotations_file:
|
|
1046
|
+
raise typer.BadParameter("--annotations-file required for annotate")
|
|
1047
|
+
if not annotations_file.exists():
|
|
1048
|
+
raise typer.BadParameter(f"Annotations file not found: {annotations_file}")
|
|
1049
|
+
|
|
1050
|
+
with annotations_file.open() as f:
|
|
1051
|
+
annotations = json.load(f)
|
|
1052
|
+
if not isinstance(annotations, list):
|
|
1053
|
+
raise typer.BadParameter("Annotations file must contain a JSON array")
|
|
1054
|
+
|
|
1055
|
+
payload = client.add_code_insights_annotations_to_report(
|
|
1056
|
+
project_key, repository_slug, commit_id, report_key, annotations
|
|
1057
|
+
)
|
|
1058
|
+
result_obj = {"annotations": payload}
|
|
1059
|
+
|
|
1060
|
+
elif action == "delete":
|
|
1061
|
+
if not yes:
|
|
1062
|
+
raise typer.BadParameter("Refusing to delete report without --yes")
|
|
1063
|
+
client.delete_code_insights_report(project_key, repository_slug, commit_id, report_key)
|
|
1064
|
+
result_obj = {"deleted": report_key}
|
|
1065
|
+
|
|
1066
|
+
else:
|
|
1067
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1068
|
+
|
|
1069
|
+
if _cli_state.get("reports", True) and markdown:
|
|
1070
|
+
output_path = report_dir / f"{project_key}_{repository_slug}_code_insights_{action}.md"
|
|
1071
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1072
|
+
with output_path.open("w") as f:
|
|
1073
|
+
f.write(f"# Code Insights - {action}\n\n")
|
|
1074
|
+
f.write(f"- **Project**: {project_key}\n")
|
|
1075
|
+
f.write(f"- **Repository**: {repository_slug}\n")
|
|
1076
|
+
f.write(f"- **Commit**: {commit_id}\n")
|
|
1077
|
+
f.write(f"- **Report Key**: {report_key}\n")
|
|
1078
|
+
f.write(f"- **Action**: {action}\n")
|
|
1079
|
+
if action == "create" and result_obj:
|
|
1080
|
+
report = result_obj.get("report", {})
|
|
1081
|
+
f.write(f"- **Title**: {report.get('title', report_title)}\n")
|
|
1082
|
+
f.write(f"- **Result**: {report.get('result', result)}\n")
|
|
1083
|
+
log.info("code_insights_reported", path=str(output_path), action=action)
|
|
1084
|
+
else:
|
|
1085
|
+
_emit_json(result_obj or {})
|
|
1086
|
+
|
|
1087
|
+
except BitbucketClientError as exc:
|
|
1088
|
+
log.error("bitbucket_error", message=str(exc), context=getattr(exc, "context", {}))
|
|
1089
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1090
|
+
raise typer.Exit(1) from None
|
|
1091
|
+
except typer.BadParameter as exc:
|
|
1092
|
+
log.error("parameter_error", message=str(exc))
|
|
1093
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1094
|
+
raise typer.Exit(1) from None
|
|
1095
|
+
except Exception as exc:
|
|
1096
|
+
log.error("unexpected_error", message=str(exc))
|
|
1097
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1098
|
+
raise typer.Exit(1) from None
|
|
1099
|
+
|
|
1100
|
+
|
|
1101
|
+
@app.command("repo-variables")
|
|
1102
|
+
def repo_variables(
|
|
1103
|
+
action: str = typer.Argument(..., help="Action: list, get, create, update, delete"),
|
|
1104
|
+
workspace: str = typer.Option(..., "--workspace", "-w", help="Workspace slug (Cloud only)"),
|
|
1105
|
+
repo_slug: str = typer.Option(..., "--repo-slug", "-r", help="Repository slug"),
|
|
1106
|
+
variable_uuid: str | None = typer.Option(None, "--uuid", help="Variable UUID (for get/update/delete)"),
|
|
1107
|
+
key: str | None = typer.Option(None, "--key", help="Variable key (for create/update)"),
|
|
1108
|
+
value: str | None = typer.Option(None, "--value", help="Variable value (for create/update)"),
|
|
1109
|
+
secured: str | None = typer.Option(None, "--secured", help="Set secured flag (true/false)"),
|
|
1110
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operations (create/update/delete)"),
|
|
1111
|
+
) -> None:
|
|
1112
|
+
"""Manage repository variables (Bitbucket Cloud only)."""
|
|
1113
|
+
log = structlog.get_logger(__name__)
|
|
1114
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
1115
|
+
markdown = _cli_state.get("markdown", True)
|
|
1116
|
+
|
|
1117
|
+
try:
|
|
1118
|
+
settings = load_settings(strict=True)
|
|
1119
|
+
if not settings.is_cloud_mode:
|
|
1120
|
+
log.error(
|
|
1121
|
+
"cloud_mode_required",
|
|
1122
|
+
message="Repository variable operations are only available in Bitbucket Cloud mode",
|
|
1123
|
+
)
|
|
1124
|
+
typer.echo("Error: Repository variable operations are only available in Bitbucket Cloud mode", err=True)
|
|
1125
|
+
raise typer.Exit(1) from None
|
|
1126
|
+
|
|
1127
|
+
with _build_client(settings) as client:
|
|
1128
|
+
def _coerce_secured(raw_value: str | None) -> bool | None:
|
|
1129
|
+
if raw_value is None:
|
|
1130
|
+
return None
|
|
1131
|
+
normalized = raw_value.strip().lower()
|
|
1132
|
+
if normalized in {"true", "1", "yes"}:
|
|
1133
|
+
return True
|
|
1134
|
+
if normalized in {"false", "0", "no"}:
|
|
1135
|
+
return False
|
|
1136
|
+
raise typer.BadParameter("--secured must be true/false")
|
|
1137
|
+
|
|
1138
|
+
if action == "list":
|
|
1139
|
+
variables = client.list_repository_variables(workspace, repo_slug)
|
|
1140
|
+
if _cli_state.get("reports", True) and markdown:
|
|
1141
|
+
output_path = report_dir / f"{workspace}_{repo_slug}_repository_variables.md"
|
|
1142
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1143
|
+
with output_path.open("w") as f:
|
|
1144
|
+
f.write(f"# Repository Variables - {workspace}/{repo_slug}\n\n")
|
|
1145
|
+
f.write(f"- **Count**: {len(variables)}\n\n")
|
|
1146
|
+
for variable in variables:
|
|
1147
|
+
key_value = variable.get("key", "unknown")
|
|
1148
|
+
uuid_value = variable.get("uuid", "unknown")
|
|
1149
|
+
secured_value = variable.get("secured", False)
|
|
1150
|
+
f.write(f"- `{key_value}` (uuid={uuid_value}, secured={secured_value})\n")
|
|
1151
|
+
log.info(
|
|
1152
|
+
"repo_variables_reported",
|
|
1153
|
+
workspace=workspace,
|
|
1154
|
+
repository_slug=repo_slug,
|
|
1155
|
+
count=len(variables),
|
|
1156
|
+
path=str(output_path),
|
|
1157
|
+
)
|
|
1158
|
+
_emit_json({"workspace": workspace, "repository": repo_slug, "count": len(variables), "variables": variables})
|
|
1159
|
+
|
|
1160
|
+
elif action == "get":
|
|
1161
|
+
if not variable_uuid:
|
|
1162
|
+
raise typer.BadParameter("--uuid required for get")
|
|
1163
|
+
variable = client.get_repository_variable(workspace, repo_slug, variable_uuid)
|
|
1164
|
+
_emit_json(variable)
|
|
1165
|
+
|
|
1166
|
+
elif action == "create":
|
|
1167
|
+
if not yes:
|
|
1168
|
+
raise typer.BadParameter("Refusing to create variable without --yes")
|
|
1169
|
+
if not key:
|
|
1170
|
+
raise typer.BadParameter("--key required for create")
|
|
1171
|
+
if value is None:
|
|
1172
|
+
raise typer.BadParameter("--value required for create")
|
|
1173
|
+
secured_value = _coerce_secured(secured)
|
|
1174
|
+
secured_flag = secured_value if secured_value is not None else False
|
|
1175
|
+
variable = client.create_repository_variable(workspace, repo_slug, key, value, secured=secured_flag)
|
|
1176
|
+
_emit_json(variable)
|
|
1177
|
+
|
|
1178
|
+
elif action == "update":
|
|
1179
|
+
if not yes:
|
|
1180
|
+
raise typer.BadParameter("Refusing to update variable without --yes")
|
|
1181
|
+
if not variable_uuid:
|
|
1182
|
+
raise typer.BadParameter("--uuid required for update")
|
|
1183
|
+
secured_value = _coerce_secured(secured)
|
|
1184
|
+
if key is None and value is None and secured_value is None:
|
|
1185
|
+
raise typer.BadParameter("Provide at least one of --key/--value/--secured to update")
|
|
1186
|
+
variable = client.update_repository_variable(
|
|
1187
|
+
workspace,
|
|
1188
|
+
repo_slug,
|
|
1189
|
+
variable_uuid,
|
|
1190
|
+
key=key,
|
|
1191
|
+
value=value,
|
|
1192
|
+
secured=secured_value,
|
|
1193
|
+
)
|
|
1194
|
+
_emit_json(variable)
|
|
1195
|
+
|
|
1196
|
+
elif action == "delete":
|
|
1197
|
+
if not yes:
|
|
1198
|
+
raise typer.BadParameter("Refusing to delete variable without --yes")
|
|
1199
|
+
if not variable_uuid:
|
|
1200
|
+
raise typer.BadParameter("--uuid required for delete")
|
|
1201
|
+
client.delete_repository_variable(workspace, repo_slug, variable_uuid)
|
|
1202
|
+
_emit_json({"status": "deleted", "uuid": variable_uuid})
|
|
1203
|
+
|
|
1204
|
+
else:
|
|
1205
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1206
|
+
|
|
1207
|
+
except BitbucketClientError as exc:
|
|
1208
|
+
log.error("bitbucket_error", message=str(exc), context=getattr(exc, "context", {}))
|
|
1209
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1210
|
+
raise typer.Exit(1) from None
|
|
1211
|
+
except typer.BadParameter as exc:
|
|
1212
|
+
log.error("parameter_error", message=str(exc))
|
|
1213
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1214
|
+
raise typer.Exit(1) from None
|
|
1215
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
1216
|
+
log.error("unexpected_error", message=str(exc))
|
|
1217
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1218
|
+
raise typer.Exit(1) from None
|
|
1219
|
+
|
|
1220
|
+
|
|
1221
|
+
@app.command("deployment-env")
|
|
1222
|
+
def deployment_env(
|
|
1223
|
+
action: str = typer.Argument(..., help="Action: list, get, vars"),
|
|
1224
|
+
sub_action: str | None = typer.Argument(None, help="Sub-action for vars: list, create, update, delete"),
|
|
1225
|
+
workspace: str = typer.Option(..., "--workspace", "-w", help="Workspace slug (Cloud only)"),
|
|
1226
|
+
repo_slug: str = typer.Option(..., "--repo-slug", "-r", help="Repository slug"),
|
|
1227
|
+
env_uuid: str | None = typer.Option(None, "--env-uuid", help="Deployment environment UUID"),
|
|
1228
|
+
variable_uuid: str | None = typer.Option(None, "--var-uuid", help="Deployment environment variable UUID"),
|
|
1229
|
+
key: str | None = typer.Option(None, "--key", help="Variable key (for create/update)"),
|
|
1230
|
+
value: str | None = typer.Option(None, "--value", help="Variable value (for create/update)"),
|
|
1231
|
+
secured: str | None = typer.Option(None, "--secured", help="Set secured flag (true/false)"),
|
|
1232
|
+
pagelen: int = typer.Option(10, "--pagelen", help="Pagelen for variable listing"),
|
|
1233
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operations (vars create/update/delete)"),
|
|
1234
|
+
) -> None:
|
|
1235
|
+
"""Manage deployment environments and their variables (Bitbucket Cloud only)."""
|
|
1236
|
+
log = structlog.get_logger(__name__)
|
|
1237
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
1238
|
+
markdown = _cli_state.get("markdown", True)
|
|
1239
|
+
|
|
1240
|
+
try:
|
|
1241
|
+
settings = load_settings(strict=True)
|
|
1242
|
+
if not settings.is_cloud_mode:
|
|
1243
|
+
log.error(
|
|
1244
|
+
"cloud_mode_required",
|
|
1245
|
+
message="Deployment environment operations are only available in Bitbucket Cloud mode",
|
|
1246
|
+
)
|
|
1247
|
+
typer.echo("Error: Deployment environment operations are only available in Bitbucket Cloud mode", err=True)
|
|
1248
|
+
raise typer.Exit(1) from None
|
|
1249
|
+
|
|
1250
|
+
with _build_client(settings) as client:
|
|
1251
|
+
def _coerce_secured(raw_value: str | None) -> bool | None:
|
|
1252
|
+
if raw_value is None:
|
|
1253
|
+
return None
|
|
1254
|
+
normalized = raw_value.strip().lower()
|
|
1255
|
+
if normalized in {"true", "1", "yes"}:
|
|
1256
|
+
return True
|
|
1257
|
+
if normalized in {"false", "0", "no"}:
|
|
1258
|
+
return False
|
|
1259
|
+
raise typer.BadParameter("--secured must be true/false")
|
|
1260
|
+
|
|
1261
|
+
if action == "list":
|
|
1262
|
+
environments = client.list_deployment_environments(workspace, repo_slug)
|
|
1263
|
+
if _cli_state.get("reports", True) and markdown:
|
|
1264
|
+
output_path = report_dir / f"{workspace}_{repo_slug}_deployment_environments.md"
|
|
1265
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1266
|
+
with output_path.open("w") as f:
|
|
1267
|
+
f.write(f"# Deployment Environments - {workspace}/{repo_slug}\n\n")
|
|
1268
|
+
f.write(f"- **Count**: {len(environments)}\n\n")
|
|
1269
|
+
for env in environments:
|
|
1270
|
+
f.write(
|
|
1271
|
+
f"- `{env.get('uuid', 'unknown')}` "
|
|
1272
|
+
f"(name={env.get('name', 'unknown')}, category={env.get('category', 'unknown')})\n"
|
|
1273
|
+
)
|
|
1274
|
+
log.info(
|
|
1275
|
+
"deployment_env_reported",
|
|
1276
|
+
workspace=workspace,
|
|
1277
|
+
repository_slug=repo_slug,
|
|
1278
|
+
count=len(environments),
|
|
1279
|
+
path=str(output_path),
|
|
1280
|
+
)
|
|
1281
|
+
_emit_json({"workspace": workspace, "repository": repo_slug, "environments": environments})
|
|
1282
|
+
|
|
1283
|
+
elif action == "get":
|
|
1284
|
+
if not env_uuid:
|
|
1285
|
+
raise typer.BadParameter("--env-uuid required for get")
|
|
1286
|
+
environment = client.get_deployment_environment(workspace, repo_slug, env_uuid)
|
|
1287
|
+
_emit_json(environment)
|
|
1288
|
+
|
|
1289
|
+
elif action == "vars":
|
|
1290
|
+
if sub_action is None:
|
|
1291
|
+
raise typer.BadParameter("Sub-action required for vars (list, create, update, delete)")
|
|
1292
|
+
if sub_action not in {"list", "create", "update", "delete"}:
|
|
1293
|
+
raise typer.BadParameter(f"Unknown vars sub-action: {sub_action}")
|
|
1294
|
+
if not env_uuid:
|
|
1295
|
+
raise typer.BadParameter("--env-uuid required for vars actions")
|
|
1296
|
+
|
|
1297
|
+
if sub_action == "list":
|
|
1298
|
+
variables = client.list_deployment_environment_variables(
|
|
1299
|
+
workspace, repo_slug, env_uuid, pagelen=pagelen
|
|
1300
|
+
)
|
|
1301
|
+
if _cli_state.get("reports", True) and markdown:
|
|
1302
|
+
output_path = (
|
|
1303
|
+
report_dir / f"{workspace}_{repo_slug}_{env_uuid}_deployment_env_variables.md"
|
|
1304
|
+
)
|
|
1305
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1306
|
+
with output_path.open("w") as f:
|
|
1307
|
+
f.write(f"# Deployment Environment Variables - {env_uuid}\n\n")
|
|
1308
|
+
f.write(f"- **Count**: {len(variables)}\n\n")
|
|
1309
|
+
for variable in variables:
|
|
1310
|
+
f.write(
|
|
1311
|
+
f"- `{variable.get('key', 'unknown')}` "
|
|
1312
|
+
f"(uuid={variable.get('uuid', 'unknown')}, secured={variable.get('secured', False)})\n"
|
|
1313
|
+
)
|
|
1314
|
+
log.info(
|
|
1315
|
+
"deployment_env_vars_reported",
|
|
1316
|
+
workspace=workspace,
|
|
1317
|
+
repository_slug=repo_slug,
|
|
1318
|
+
environment_uuid=env_uuid,
|
|
1319
|
+
count=len(variables),
|
|
1320
|
+
path=str(output_path),
|
|
1321
|
+
)
|
|
1322
|
+
_emit_json(
|
|
1323
|
+
{
|
|
1324
|
+
"workspace": workspace,
|
|
1325
|
+
"repository": repo_slug,
|
|
1326
|
+
"environment_uuid": env_uuid,
|
|
1327
|
+
"count": len(variables),
|
|
1328
|
+
"variables": variables,
|
|
1329
|
+
}
|
|
1330
|
+
)
|
|
1331
|
+
|
|
1332
|
+
elif sub_action == "create":
|
|
1333
|
+
if not yes:
|
|
1334
|
+
raise typer.BadParameter("Refusing to create variable without --yes")
|
|
1335
|
+
if not key:
|
|
1336
|
+
raise typer.BadParameter("--key required for vars create")
|
|
1337
|
+
if value is None:
|
|
1338
|
+
raise typer.BadParameter("--value required for vars create")
|
|
1339
|
+
secured_value = _coerce_secured(secured)
|
|
1340
|
+
secured_flag = secured_value if secured_value is not None else False
|
|
1341
|
+
variable = client.create_deployment_environment_variable(
|
|
1342
|
+
workspace, repo_slug, env_uuid, key, value, secured=secured_flag
|
|
1343
|
+
)
|
|
1344
|
+
_emit_json(variable)
|
|
1345
|
+
|
|
1346
|
+
elif sub_action == "update":
|
|
1347
|
+
if not yes:
|
|
1348
|
+
raise typer.BadParameter("Refusing to update variable without --yes")
|
|
1349
|
+
if not variable_uuid:
|
|
1350
|
+
raise typer.BadParameter("--var-uuid required for vars update")
|
|
1351
|
+
secured_value = _coerce_secured(secured)
|
|
1352
|
+
if key is None and value is None and secured_value is None:
|
|
1353
|
+
raise typer.BadParameter("Provide at least one of --key/--value/--secured to update")
|
|
1354
|
+
variable = client.update_deployment_environment_variable(
|
|
1355
|
+
workspace,
|
|
1356
|
+
repo_slug,
|
|
1357
|
+
env_uuid,
|
|
1358
|
+
variable_uuid,
|
|
1359
|
+
key=key,
|
|
1360
|
+
value=value,
|
|
1361
|
+
secured=secured_value,
|
|
1362
|
+
)
|
|
1363
|
+
_emit_json(variable)
|
|
1364
|
+
|
|
1365
|
+
elif sub_action == "delete":
|
|
1366
|
+
if not yes:
|
|
1367
|
+
raise typer.BadParameter("Refusing to delete variable without --yes")
|
|
1368
|
+
if not variable_uuid:
|
|
1369
|
+
raise typer.BadParameter("--var-uuid required for vars delete")
|
|
1370
|
+
client.delete_deployment_environment_variable(
|
|
1371
|
+
workspace, repo_slug, env_uuid, variable_uuid
|
|
1372
|
+
)
|
|
1373
|
+
_emit_json({"status": "deleted", "uuid": variable_uuid})
|
|
1374
|
+
|
|
1375
|
+
else:
|
|
1376
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1377
|
+
|
|
1378
|
+
except BitbucketClientError as exc:
|
|
1379
|
+
log.error("bitbucket_error", message=str(exc), context=getattr(exc, "context", {}))
|
|
1380
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1381
|
+
raise typer.Exit(1) from None
|
|
1382
|
+
except typer.BadParameter as exc:
|
|
1383
|
+
log.error("parameter_error", message=str(exc))
|
|
1384
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1385
|
+
raise typer.Exit(1) from None
|
|
1386
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
1387
|
+
log.error("unexpected_error", message=str(exc))
|
|
1388
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1389
|
+
raise typer.Exit(1) from None
|
|
1390
|
+
|
|
1391
|
+
|
|
1392
|
+
@app.command("pr-blocker")
|
|
1393
|
+
def pr_blocker(
|
|
1394
|
+
action: str = typer.Argument(..., help="Action: list, add, delete"),
|
|
1395
|
+
workspace: str = typer.Option(..., "--workspace", "-w", help="Workspace slug (Cloud only)"),
|
|
1396
|
+
repo_slug: str = typer.Option(..., "--repo-slug", "-r", help="Repository slug"),
|
|
1397
|
+
pull_request_id: str = typer.Option(..., "--pull-request-id", "-p", help="Pull request ID"),
|
|
1398
|
+
blocker_uuid: str | None = typer.Option(None, "--blocker-uuid", help="Blocker UUID (for delete)"),
|
|
1399
|
+
message: str | None = typer.Option(None, "--message", help="Blocker message (for add)"),
|
|
1400
|
+
reason: str | None = typer.Option(None, "--reason", help="Blocker reason (for add)"),
|
|
1401
|
+
severity: str | None = typer.Option(None, "--severity", help="Blocker severity (for add)"),
|
|
1402
|
+
search: str | None = typer.Option(None, "--search", help="Filter expression (for list)"),
|
|
1403
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm delete operation"),
|
|
1404
|
+
) -> None:
|
|
1405
|
+
"""Manage pull request blockers (Bitbucket Cloud only)."""
|
|
1406
|
+
log = structlog.get_logger(__name__)
|
|
1407
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
1408
|
+
markdown = _cli_state.get("markdown", True)
|
|
1409
|
+
|
|
1410
|
+
try:
|
|
1411
|
+
settings = load_settings(strict=True)
|
|
1412
|
+
if not settings.is_cloud_mode:
|
|
1413
|
+
log.error(
|
|
1414
|
+
"cloud_mode_required",
|
|
1415
|
+
message="Pull request blocker operations are only available in Bitbucket Cloud mode",
|
|
1416
|
+
)
|
|
1417
|
+
typer.echo("Error: Pull request blocker operations are only available in Bitbucket Cloud mode", err=True)
|
|
1418
|
+
raise typer.Exit(1) from None
|
|
1419
|
+
|
|
1420
|
+
with _build_client(settings) as client:
|
|
1421
|
+
if action == "list":
|
|
1422
|
+
blockers = client.list_pull_request_blockers(
|
|
1423
|
+
workspace,
|
|
1424
|
+
repo_slug,
|
|
1425
|
+
pull_request_id,
|
|
1426
|
+
query=search,
|
|
1427
|
+
)
|
|
1428
|
+
if _cli_state.get("reports", True) and markdown:
|
|
1429
|
+
output_path = report_dir / f"{workspace}_{repo_slug}_{pull_request_id}_pr_blockers.md"
|
|
1430
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1431
|
+
with output_path.open("w") as f:
|
|
1432
|
+
f.write(f"# Pull Request Blockers - {pull_request_id}\n\n")
|
|
1433
|
+
f.write(f"- **Count**: {len(blockers)}\n\n")
|
|
1434
|
+
for blocker in blockers:
|
|
1435
|
+
f.write(
|
|
1436
|
+
f"- `{blocker.get('uuid', 'unknown')}` "
|
|
1437
|
+
f"(message={blocker.get('message', '')}, reason={blocker.get('reason')})\n"
|
|
1438
|
+
)
|
|
1439
|
+
log.info(
|
|
1440
|
+
"pull_request_blockers_reported",
|
|
1441
|
+
workspace=workspace,
|
|
1442
|
+
repository_slug=repo_slug,
|
|
1443
|
+
pull_request_id=pull_request_id,
|
|
1444
|
+
count=len(blockers),
|
|
1445
|
+
path=str(output_path),
|
|
1446
|
+
)
|
|
1447
|
+
_emit_json({"blockers": blockers})
|
|
1448
|
+
else:
|
|
1449
|
+
_emit_json({"blockers": blockers})
|
|
1450
|
+
|
|
1451
|
+
elif action == "add":
|
|
1452
|
+
if not message:
|
|
1453
|
+
raise typer.BadParameter("--message required for add")
|
|
1454
|
+
blocker = client.add_pull_request_blocker(
|
|
1455
|
+
workspace,
|
|
1456
|
+
repo_slug,
|
|
1457
|
+
pull_request_id,
|
|
1458
|
+
message,
|
|
1459
|
+
reason=reason,
|
|
1460
|
+
severity=severity,
|
|
1461
|
+
)
|
|
1462
|
+
_emit_json(blocker)
|
|
1463
|
+
|
|
1464
|
+
elif action == "delete":
|
|
1465
|
+
if not yes:
|
|
1466
|
+
raise typer.BadParameter("Refusing to delete blocker without --yes")
|
|
1467
|
+
if not blocker_uuid:
|
|
1468
|
+
raise typer.BadParameter("--blocker-uuid required for delete")
|
|
1469
|
+
client.delete_pull_request_blocker(
|
|
1470
|
+
workspace,
|
|
1471
|
+
repo_slug,
|
|
1472
|
+
pull_request_id,
|
|
1473
|
+
blocker_uuid,
|
|
1474
|
+
)
|
|
1475
|
+
_emit_json({"status": "deleted", "uuid": blocker_uuid})
|
|
1476
|
+
|
|
1477
|
+
else:
|
|
1478
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1479
|
+
|
|
1480
|
+
except BitbucketClientError as exc:
|
|
1481
|
+
log.error("bitbucket_error", message=str(exc), context=getattr(exc, "context", {}))
|
|
1482
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1483
|
+
raise typer.Exit(1) from None
|
|
1484
|
+
except typer.BadParameter as exc:
|
|
1485
|
+
log.error("parameter_error", message=str(exc))
|
|
1486
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1487
|
+
raise typer.Exit(1) from None
|
|
1488
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
1489
|
+
log.error("unexpected_error", message=str(exc))
|
|
1490
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1491
|
+
raise typer.Exit(1) from None
|
|
1492
|
+
|
|
1493
|
+
|
|
1494
|
+
@app.command("issue")
|
|
1495
|
+
def issue(
|
|
1496
|
+
action: str = typer.Argument(..., help="Action: list, get, create, update, delete"),
|
|
1497
|
+
workspace: str = typer.Option(..., "--workspace", "-w", help="Workspace slug (Cloud only)"),
|
|
1498
|
+
repo_slug: str = typer.Option(..., "--repo-slug", "-r", help="Repository slug"),
|
|
1499
|
+
issue_id: str | None = typer.Option(None, "--issue-id", "-i", help="Issue ID (for get/update/delete)"),
|
|
1500
|
+
title: str | None = typer.Option(None, "--title", help="Issue title (for create/update)"),
|
|
1501
|
+
description: str | None = typer.Option(None, "--description", help="Issue description (for create/update)"),
|
|
1502
|
+
kind: str | None = typer.Option(None, "--kind", help="Issue kind (bug, enhancement, proposal, task)"),
|
|
1503
|
+
priority: str | None = typer.Option(None, "--priority", help="Issue priority (trivial, minor, major, critical, blocker)"),
|
|
1504
|
+
state: str | None = typer.Option(None, "--state", help="Issue state (for update)"),
|
|
1505
|
+
query: str | None = typer.Option(None, "--query", help="Filtering query (for list)"),
|
|
1506
|
+
sort: str | None = typer.Option(None, "--sort", help="Sort clause (for list)"),
|
|
1507
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm delete operation"),
|
|
1508
|
+
) -> None:
|
|
1509
|
+
"""Manage Bitbucket Cloud issues for repositories."""
|
|
1510
|
+
log = structlog.get_logger(__name__)
|
|
1511
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
1512
|
+
markdown = _cli_state.get("markdown", True)
|
|
1513
|
+
|
|
1514
|
+
try:
|
|
1515
|
+
settings = load_settings(strict=True)
|
|
1516
|
+
if not settings.is_cloud_mode:
|
|
1517
|
+
log.error(
|
|
1518
|
+
"cloud_mode_required",
|
|
1519
|
+
message="Repository issue operations are only available in Bitbucket Cloud mode",
|
|
1520
|
+
)
|
|
1521
|
+
typer.echo("Error: Repository issue operations are only available in Bitbucket Cloud mode", err=True)
|
|
1522
|
+
raise typer.Exit(1) from None
|
|
1523
|
+
|
|
1524
|
+
with _build_client(settings) as client:
|
|
1525
|
+
if action == "list":
|
|
1526
|
+
issues = client.list_repository_issues(workspace, repo_slug, query=query, sort=sort)
|
|
1527
|
+
if _cli_state.get("reports", True) and markdown:
|
|
1528
|
+
output_path = report_dir / f"{workspace}_{repo_slug}_issues.md"
|
|
1529
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1530
|
+
with output_path.open("w") as f:
|
|
1531
|
+
f.write(f"# Repository Issues - {workspace}/{repo_slug}\n\n")
|
|
1532
|
+
f.write(f"- **Count**: {len(issues)}\n\n")
|
|
1533
|
+
for issue_obj in issues:
|
|
1534
|
+
f.write(
|
|
1535
|
+
f"- `{issue_obj.get('id', 'unknown')}` "
|
|
1536
|
+
f"(title={issue_obj.get('title', '')}, state={issue_obj.get('state', '')})\n"
|
|
1537
|
+
)
|
|
1538
|
+
log.info(
|
|
1539
|
+
"repository_issues_reported",
|
|
1540
|
+
workspace=workspace,
|
|
1541
|
+
repository_slug=repo_slug,
|
|
1542
|
+
count=len(issues),
|
|
1543
|
+
query=query,
|
|
1544
|
+
sort=sort,
|
|
1545
|
+
path=str(output_path),
|
|
1546
|
+
)
|
|
1547
|
+
_emit_json({"issues": issues})
|
|
1548
|
+
|
|
1549
|
+
elif action == "get":
|
|
1550
|
+
if not issue_id:
|
|
1551
|
+
raise typer.BadParameter("--issue-id required for get")
|
|
1552
|
+
issue_obj = client.get_repository_issue(workspace, repo_slug, issue_id)
|
|
1553
|
+
_emit_json(issue_obj)
|
|
1554
|
+
|
|
1555
|
+
elif action == "create":
|
|
1556
|
+
if not title:
|
|
1557
|
+
raise typer.BadParameter("--title required for create")
|
|
1558
|
+
create_kind = kind or "task"
|
|
1559
|
+
create_priority = priority or "major"
|
|
1560
|
+
issue_obj = client.create_repository_issue(
|
|
1561
|
+
workspace,
|
|
1562
|
+
repo_slug,
|
|
1563
|
+
title,
|
|
1564
|
+
description or "",
|
|
1565
|
+
kind=create_kind,
|
|
1566
|
+
priority=create_priority,
|
|
1567
|
+
)
|
|
1568
|
+
_emit_json(issue_obj)
|
|
1569
|
+
|
|
1570
|
+
elif action == "update":
|
|
1571
|
+
if not issue_id:
|
|
1572
|
+
raise typer.BadParameter("--issue-id required for update")
|
|
1573
|
+
if (
|
|
1574
|
+
title is None
|
|
1575
|
+
and description is None
|
|
1576
|
+
and kind is None
|
|
1577
|
+
and priority is None
|
|
1578
|
+
and state is None
|
|
1579
|
+
):
|
|
1580
|
+
raise typer.BadParameter("Provide at least one field to update (--title/--description/--kind/--priority/--state)")
|
|
1581
|
+
issue_obj = client.update_repository_issue(
|
|
1582
|
+
workspace,
|
|
1583
|
+
repo_slug,
|
|
1584
|
+
issue_id,
|
|
1585
|
+
title=title,
|
|
1586
|
+
description=description,
|
|
1587
|
+
kind=kind,
|
|
1588
|
+
priority=priority,
|
|
1589
|
+
state=state,
|
|
1590
|
+
)
|
|
1591
|
+
_emit_json(issue_obj)
|
|
1592
|
+
|
|
1593
|
+
elif action == "delete":
|
|
1594
|
+
if not issue_id:
|
|
1595
|
+
raise typer.BadParameter("--issue-id required for delete")
|
|
1596
|
+
if not yes:
|
|
1597
|
+
raise typer.BadParameter("Refusing to delete issue without --yes")
|
|
1598
|
+
client.delete_repository_issue(workspace, repo_slug, issue_id)
|
|
1599
|
+
_emit_json({"status": "deleted", "issue_id": issue_id})
|
|
1600
|
+
|
|
1601
|
+
else:
|
|
1602
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1603
|
+
|
|
1604
|
+
except BitbucketClientError as exc:
|
|
1605
|
+
log.error("bitbucket_error", message=str(exc), context=getattr(exc, "context", {}))
|
|
1606
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1607
|
+
raise typer.Exit(1) from None
|
|
1608
|
+
except typer.BadParameter as exc:
|
|
1609
|
+
log.error("parameter_error", message=str(exc))
|
|
1610
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1611
|
+
raise typer.Exit(1) from None
|
|
1612
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
1613
|
+
log.error("unexpected_error", message=str(exc))
|
|
1614
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1615
|
+
raise typer.Exit(1) from None
|
|
1616
|
+
|
|
1617
|
+
|
|
1618
|
+
@app.command("pipeline-advanced")
|
|
1619
|
+
def pipeline_advanced(
|
|
1620
|
+
action: str = typer.Argument(..., help="Action: get, steps, step, log, stop"),
|
|
1621
|
+
workspace: str = typer.Option(..., "--workspace", "-w", help="Workspace slug (Cloud only)"),
|
|
1622
|
+
repo_slug: str = typer.Option(..., "--repo-slug", "-r", help="Repository slug"),
|
|
1623
|
+
pipeline_uuid: str | None = typer.Option(None, "--pipeline-uuid", help="Pipeline UUID"),
|
|
1624
|
+
step_uuid: str | None = typer.Option(None, "--step-uuid", help="Step UUID (for step/log)"),
|
|
1625
|
+
start: int | None = typer.Option(None, "--start", help="Start byte for log range"),
|
|
1626
|
+
end: int | None = typer.Option(None, "--end", help="End byte for log range"),
|
|
1627
|
+
output: Path | None = typer.Option(None, "--output", help="Optional file path to write log output"),
|
|
1628
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm stop operation"),
|
|
1629
|
+
) -> None:
|
|
1630
|
+
"""Advanced pipeline operations (Bitbucket Cloud only)."""
|
|
1631
|
+
log = structlog.get_logger(__name__)
|
|
1632
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
1633
|
+
markdown = _cli_state.get("markdown", True)
|
|
1634
|
+
|
|
1635
|
+
try:
|
|
1636
|
+
settings = load_settings(strict=True)
|
|
1637
|
+
if not settings.is_cloud_mode:
|
|
1638
|
+
log.error(
|
|
1639
|
+
"cloud_mode_required",
|
|
1640
|
+
message="Advanced pipeline operations are only available in Bitbucket Cloud mode",
|
|
1641
|
+
)
|
|
1642
|
+
typer.echo("Error: Advanced pipeline operations are only available in Bitbucket Cloud mode", err=True)
|
|
1643
|
+
raise typer.Exit(1) from None
|
|
1644
|
+
|
|
1645
|
+
with _build_client(settings) as client:
|
|
1646
|
+
if action == "get":
|
|
1647
|
+
if not pipeline_uuid:
|
|
1648
|
+
raise typer.BadParameter("--pipeline-uuid required for get")
|
|
1649
|
+
pipeline = client.get_pipeline(workspace, repo_slug, pipeline_uuid)
|
|
1650
|
+
_emit_json(pipeline)
|
|
1651
|
+
|
|
1652
|
+
elif action == "steps":
|
|
1653
|
+
if not pipeline_uuid:
|
|
1654
|
+
raise typer.BadParameter("--pipeline-uuid required for steps")
|
|
1655
|
+
steps = client.list_pipeline_steps(workspace, repo_slug, pipeline_uuid)
|
|
1656
|
+
if _cli_state.get("reports", True) and markdown:
|
|
1657
|
+
output_path = report_dir / f"{workspace}_{repo_slug}_{pipeline_uuid}_pipeline_steps.md"
|
|
1658
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1659
|
+
with output_path.open("w") as f:
|
|
1660
|
+
f.write(f"# Pipeline Steps - {pipeline_uuid}\n\n")
|
|
1661
|
+
f.write(f"- **Count**: {len(steps)}\n\n")
|
|
1662
|
+
for step in steps:
|
|
1663
|
+
f.write(
|
|
1664
|
+
f"- `{step.get('uuid', 'unknown')}` "
|
|
1665
|
+
f"(state={step.get('state', 'unknown')}, duration={step.get('duration_in_seconds')})\n"
|
|
1666
|
+
)
|
|
1667
|
+
log.info(
|
|
1668
|
+
"pipeline_steps_reported",
|
|
1669
|
+
workspace=workspace,
|
|
1670
|
+
repository_slug=repo_slug,
|
|
1671
|
+
pipeline_uuid=pipeline_uuid,
|
|
1672
|
+
count=len(steps),
|
|
1673
|
+
path=str(output_path),
|
|
1674
|
+
)
|
|
1675
|
+
_emit_json({"pipeline_uuid": pipeline_uuid, "steps": steps})
|
|
1676
|
+
|
|
1677
|
+
elif action == "step":
|
|
1678
|
+
if not pipeline_uuid:
|
|
1679
|
+
raise typer.BadParameter("--pipeline-uuid required for step")
|
|
1680
|
+
if not step_uuid:
|
|
1681
|
+
raise typer.BadParameter("--step-uuid required for step")
|
|
1682
|
+
step = client.get_pipeline_step(workspace, repo_slug, pipeline_uuid, step_uuid)
|
|
1683
|
+
_emit_json(step)
|
|
1684
|
+
|
|
1685
|
+
elif action == "log":
|
|
1686
|
+
if not pipeline_uuid:
|
|
1687
|
+
raise typer.BadParameter("--pipeline-uuid required for log")
|
|
1688
|
+
if not step_uuid:
|
|
1689
|
+
raise typer.BadParameter("--step-uuid required for log")
|
|
1690
|
+
payload = client.get_pipeline_step_log(
|
|
1691
|
+
workspace,
|
|
1692
|
+
repo_slug,
|
|
1693
|
+
pipeline_uuid,
|
|
1694
|
+
step_uuid,
|
|
1695
|
+
start=start,
|
|
1696
|
+
end=end,
|
|
1697
|
+
)
|
|
1698
|
+
content = payload.get("content", "")
|
|
1699
|
+
if output:
|
|
1700
|
+
output.parent.mkdir(parents=True, exist_ok=True)
|
|
1701
|
+
output.write_text(content)
|
|
1702
|
+
log.info(
|
|
1703
|
+
"pipeline_step_log_written",
|
|
1704
|
+
workspace=workspace,
|
|
1705
|
+
repository_slug=repo_slug,
|
|
1706
|
+
pipeline_uuid=pipeline_uuid,
|
|
1707
|
+
step_uuid=step_uuid,
|
|
1708
|
+
path=str(output),
|
|
1709
|
+
)
|
|
1710
|
+
_emit_json({"status": "written", "path": str(output), **payload})
|
|
1711
|
+
elif _cli_state.get("reports", True) and markdown:
|
|
1712
|
+
output_path = (
|
|
1713
|
+
report_dir / f"{workspace}_{repo_slug}_{pipeline_uuid}_{step_uuid}_pipeline_log.txt"
|
|
1714
|
+
)
|
|
1715
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1716
|
+
output_path.write_text(content)
|
|
1717
|
+
log.info(
|
|
1718
|
+
"pipeline_step_log_reported",
|
|
1719
|
+
workspace=workspace,
|
|
1720
|
+
repository_slug=repo_slug,
|
|
1721
|
+
pipeline_uuid=pipeline_uuid,
|
|
1722
|
+
step_uuid=step_uuid,
|
|
1723
|
+
path=str(output_path),
|
|
1724
|
+
)
|
|
1725
|
+
_emit_json({"status": "written", "path": str(output_path), **payload})
|
|
1726
|
+
else:
|
|
1727
|
+
_emit_json(payload)
|
|
1728
|
+
|
|
1729
|
+
elif action == "stop":
|
|
1730
|
+
if not pipeline_uuid:
|
|
1731
|
+
raise typer.BadParameter("--pipeline-uuid required for stop")
|
|
1732
|
+
if not yes:
|
|
1733
|
+
raise typer.BadParameter("Refusing to stop pipeline without --yes")
|
|
1734
|
+
payload = client.stop_pipeline(workspace, repo_slug, pipeline_uuid)
|
|
1735
|
+
if _cli_state.get("reports", True) and markdown:
|
|
1736
|
+
output_path = report_dir / f"{workspace}_{repo_slug}_{pipeline_uuid}_pipeline_stop.md"
|
|
1737
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1738
|
+
with output_path.open("w") as f:
|
|
1739
|
+
f.write(f"# Pipeline Stop - {pipeline_uuid}\n\n")
|
|
1740
|
+
f.write(f"- **Status**: {payload.get('status', 'stopped')}\n")
|
|
1741
|
+
log.info(
|
|
1742
|
+
"pipeline_stop_reported",
|
|
1743
|
+
workspace=workspace,
|
|
1744
|
+
repository_slug=repo_slug,
|
|
1745
|
+
pipeline_uuid=pipeline_uuid,
|
|
1746
|
+
path=str(output_path),
|
|
1747
|
+
)
|
|
1748
|
+
_emit_json(payload)
|
|
1749
|
+
|
|
1750
|
+
else:
|
|
1751
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1752
|
+
|
|
1753
|
+
except BitbucketClientError as exc:
|
|
1754
|
+
log.error("bitbucket_error", message=str(exc), context=getattr(exc, "context", {}))
|
|
1755
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1756
|
+
raise typer.Exit(1) from None
|
|
1757
|
+
except typer.BadParameter as exc:
|
|
1758
|
+
log.error("parameter_error", message=str(exc))
|
|
1759
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1760
|
+
raise typer.Exit(1) from None
|
|
1761
|
+
except Exception as exc: # pragma: no cover - unexpected errors
|
|
1762
|
+
log.error("unexpected_error", message=str(exc))
|
|
1763
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1764
|
+
raise typer.Exit(1) from None
|
|
1765
|
+
|
|
1766
|
+
|
|
1767
|
+
@app.command()
|
|
1768
|
+
def repo_settings(
|
|
1769
|
+
category: str = typer.Argument(..., help="Category: branching-model, default-branch, hooks, lfs"),
|
|
1770
|
+
action: str = typer.Argument(..., help="Action: get, set, enable, disable (varies by category)"),
|
|
1771
|
+
project_key: str = typer.Argument(..., help="Project key"),
|
|
1772
|
+
repository_slug: str = typer.Argument(..., help="Repository slug"),
|
|
1773
|
+
model_file: Path | None = typer.Option(None, "--model-file", help="Path to JSON file with branching model (for set)"),
|
|
1774
|
+
branch: str | None = typer.Option(None, "--branch", help="Branch name (for default-branch set)"),
|
|
1775
|
+
hook_key: str | None = typer.Option(None, "--hook-key", help="Hook key (for hooks enable/disable)"),
|
|
1776
|
+
enabled: bool | None = typer.Option(None, "--enabled/--disabled", help="Enable/disable LFS (for lfs set)"),
|
|
1777
|
+
start: int = typer.Option(0, "--start", help="Start index for pagination (for hooks get)"),
|
|
1778
|
+
limit: int | None = typer.Option(None, "--limit", help="Limit for pagination (for hooks get)"),
|
|
1779
|
+
filter_type: str | None = typer.Option(None, "--filter-type", help="Filter type (for hooks get)"),
|
|
1780
|
+
yes: bool = typer.Option(False, "--yes", help="Confirm write operation"),
|
|
1781
|
+
) -> None:
|
|
1782
|
+
"""Manage Bitbucket repository settings."""
|
|
1783
|
+
log = structlog.get_logger(__name__)
|
|
1784
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
1785
|
+
markdown = _cli_state.get("markdown", True)
|
|
1786
|
+
|
|
1787
|
+
try:
|
|
1788
|
+
settings = load_settings(strict=True)
|
|
1789
|
+
with _build_client(settings) as client:
|
|
1790
|
+
result_obj: dict | None = None
|
|
1791
|
+
|
|
1792
|
+
if category == "branching-model":
|
|
1793
|
+
if action == "get":
|
|
1794
|
+
payload = client.get_branching_model(project_key, repository_slug)
|
|
1795
|
+
result_obj = {"branching_model": payload}
|
|
1796
|
+
elif action == "set":
|
|
1797
|
+
if not yes:
|
|
1798
|
+
raise typer.BadParameter("Refusing to set branching model without --yes")
|
|
1799
|
+
if not model_file:
|
|
1800
|
+
raise typer.BadParameter("--model-file required for set")
|
|
1801
|
+
if not model_file.exists():
|
|
1802
|
+
raise typer.BadParameter(f"Model file not found: {model_file}")
|
|
1803
|
+
with model_file.open() as f:
|
|
1804
|
+
model = json.load(f)
|
|
1805
|
+
payload = client.set_branching_model(project_key, repository_slug, model)
|
|
1806
|
+
result_obj = {"branching_model": payload}
|
|
1807
|
+
elif action == "enable":
|
|
1808
|
+
if not yes:
|
|
1809
|
+
raise typer.BadParameter("Refusing to enable branching model without --yes")
|
|
1810
|
+
payload = client.enable_branching_model(project_key, repository_slug)
|
|
1811
|
+
result_obj = {"branching_model": payload}
|
|
1812
|
+
elif action == "disable":
|
|
1813
|
+
if not yes:
|
|
1814
|
+
raise typer.BadParameter("Refusing to disable branching model without --yes")
|
|
1815
|
+
payload = client.disable_branching_model(project_key, repository_slug)
|
|
1816
|
+
result_obj = {"branching_model": payload}
|
|
1817
|
+
else:
|
|
1818
|
+
raise typer.BadParameter(f"Unknown action for branching-model: {action}")
|
|
1819
|
+
|
|
1820
|
+
elif category == "default-branch":
|
|
1821
|
+
if action == "get":
|
|
1822
|
+
payload = client.get_default_branch(project_key, repository_slug)
|
|
1823
|
+
result_obj = {"default_branch": payload}
|
|
1824
|
+
elif action == "set":
|
|
1825
|
+
if not yes:
|
|
1826
|
+
raise typer.BadParameter("Refusing to set default branch without --yes")
|
|
1827
|
+
if not branch:
|
|
1828
|
+
raise typer.BadParameter("--branch required for set")
|
|
1829
|
+
payload = client.set_default_branch(project_key, repository_slug, branch)
|
|
1830
|
+
result_obj = {"default_branch": payload}
|
|
1831
|
+
else:
|
|
1832
|
+
raise typer.BadParameter(f"Unknown action for default-branch: {action}")
|
|
1833
|
+
|
|
1834
|
+
elif category == "hooks":
|
|
1835
|
+
if action == "get":
|
|
1836
|
+
payload = client.get_repo_hook_settings(project_key, repository_slug, start=start, limit=limit, filter_type=filter_type)
|
|
1837
|
+
result_obj = {"hooks": payload}
|
|
1838
|
+
elif action == "enable":
|
|
1839
|
+
if not yes:
|
|
1840
|
+
raise typer.BadParameter("Refusing to enable hook without --yes")
|
|
1841
|
+
if not hook_key:
|
|
1842
|
+
raise typer.BadParameter("--hook-key required for enable")
|
|
1843
|
+
payload = client.enable_repo_hook_settings(project_key, repository_slug, hook_key)
|
|
1844
|
+
result_obj = {"hook": payload}
|
|
1845
|
+
elif action == "disable":
|
|
1846
|
+
if not yes:
|
|
1847
|
+
raise typer.BadParameter("Refusing to disable hook without --yes")
|
|
1848
|
+
if not hook_key:
|
|
1849
|
+
raise typer.BadParameter("--hook-key required for disable")
|
|
1850
|
+
payload = client.disable_repo_hook_settings(project_key, repository_slug, hook_key)
|
|
1851
|
+
result_obj = {"hook": payload}
|
|
1852
|
+
else:
|
|
1853
|
+
raise typer.BadParameter(f"Unknown action for hooks: {action}")
|
|
1854
|
+
|
|
1855
|
+
elif category == "lfs":
|
|
1856
|
+
if action == "get":
|
|
1857
|
+
payload = client.get_lfs_repo_status(project_key, repository_slug)
|
|
1858
|
+
result_obj = {"lfs": payload}
|
|
1859
|
+
elif action == "set":
|
|
1860
|
+
if not yes:
|
|
1861
|
+
raise typer.BadParameter("Refusing to set LFS status without --yes")
|
|
1862
|
+
if enabled is None:
|
|
1863
|
+
raise typer.BadParameter("--enabled or --disabled required for set")
|
|
1864
|
+
payload = client.set_lfs_repo_status(project_key, repository_slug, enabled)
|
|
1865
|
+
result_obj = {"lfs": payload}
|
|
1866
|
+
else:
|
|
1867
|
+
raise typer.BadParameter(f"Unknown action for lfs: {action}")
|
|
1868
|
+
|
|
1869
|
+
else:
|
|
1870
|
+
raise typer.BadParameter(f"Unknown category: {category}")
|
|
1871
|
+
|
|
1872
|
+
if _cli_state.get("reports", True) and markdown:
|
|
1873
|
+
output_path = report_dir / f"{project_key}_{repository_slug}_repo_settings_{category}_{action}.md"
|
|
1874
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1875
|
+
with output_path.open("w") as f:
|
|
1876
|
+
f.write(f"# Repository Settings - {category} - {action}\n\n")
|
|
1877
|
+
f.write(f"- **Project**: {project_key}\n")
|
|
1878
|
+
f.write(f"- **Repository**: {repository_slug}\n")
|
|
1879
|
+
f.write(f"- **Category**: {category}\n")
|
|
1880
|
+
f.write(f"- **Action**: {action}\n")
|
|
1881
|
+
log.info("repo_settings_reported", path=str(output_path), category=category, action=action)
|
|
1882
|
+
else:
|
|
1883
|
+
_emit_json(result_obj or {})
|
|
1884
|
+
|
|
1885
|
+
except BitbucketClientError as exc:
|
|
1886
|
+
log.error("bitbucket_error", message=str(exc), context=getattr(exc, "context", {}))
|
|
1887
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1888
|
+
raise typer.Exit(1) from None
|
|
1889
|
+
except typer.BadParameter as exc:
|
|
1890
|
+
log.error("parameter_error", message=str(exc))
|
|
1891
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1892
|
+
raise typer.Exit(1) from None
|
|
1893
|
+
except Exception as exc:
|
|
1894
|
+
log.error("unexpected_error", message=str(exc))
|
|
1895
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
1896
|
+
raise typer.Exit(1) from None
|
|
1897
|
+
|
|
1898
|
+
|
|
1899
|
+
@app.command()
|
|
1900
|
+
def code_advanced(
|
|
1901
|
+
action: str = typer.Argument(..., help="Action: file-list, commit-info, commit-changes, search, search-advanced"),
|
|
1902
|
+
project_key: str | None = typer.Option(None, "--project-key", help="Project key (for file-list, commit-info, commit-changes)"),
|
|
1903
|
+
repository_slug: str | None = typer.Option(None, "--repository-slug", help="Repository slug (for file-list, commit-info, commit-changes)"),
|
|
1904
|
+
sub_folder: str | None = typer.Option(None, "--sub-folder", help="Subfolder path (for file-list)"),
|
|
1905
|
+
query: str | None = typer.Option(None, "--query", help="Query string for filtering (for file-list)"),
|
|
1906
|
+
commit: str | None = typer.Option(None, "--commit", help="Commit hash (for commit-info)"),
|
|
1907
|
+
commit_id: str | None = typer.Option(None, "--commit-id", help="Commit ID (for commit-changes)"),
|
|
1908
|
+
hash_newest: str | None = typer.Option(None, "--hash-newest", help="Newest commit hash (for commit-changes)"),
|
|
1909
|
+
path: str | None = typer.Option(None, "--path", help="File path (for commit-info)"),
|
|
1910
|
+
merges: str = typer.Option("include", "--merges", help="How to handle merges: include, exclude, only (for commit-changes)"),
|
|
1911
|
+
team: str | None = typer.Option(None, "--team", help="Team/workspace name (for search, Cloud-only)"),
|
|
1912
|
+
search_query: str | None = typer.Option(None, "--search-query", help="Search query string (for search)"),
|
|
1913
|
+
repository: str | None = typer.Option(None, "--repository", help="Repository slug filter (for search-advanced)"),
|
|
1914
|
+
start: int = typer.Option(0, "--start", help="Start index for pagination"),
|
|
1915
|
+
limit: int | None = typer.Option(None, "--limit", help="Limit for pagination"),
|
|
1916
|
+
page: int = typer.Option(1, "--page", help="Page number (for search)"),
|
|
1917
|
+
) -> None:
|
|
1918
|
+
"""Advanced code operations."""
|
|
1919
|
+
log = structlog.get_logger(__name__)
|
|
1920
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
1921
|
+
markdown = _cli_state.get("markdown", True)
|
|
1922
|
+
|
|
1923
|
+
try:
|
|
1924
|
+
settings = load_settings(strict=True)
|
|
1925
|
+
with _build_client(settings) as client:
|
|
1926
|
+
result_obj: dict | None = None
|
|
1927
|
+
|
|
1928
|
+
if action == "file-list":
|
|
1929
|
+
if not project_key:
|
|
1930
|
+
raise typer.BadParameter("--project-key required for file-list")
|
|
1931
|
+
if not repository_slug:
|
|
1932
|
+
raise typer.BadParameter("--repository-slug required for file-list")
|
|
1933
|
+
payload = client.get_file_list(
|
|
1934
|
+
project_key, repository_slug, sub_folder=sub_folder, query=query, start=start, limit=limit
|
|
1935
|
+
)
|
|
1936
|
+
result_obj = {"files": payload}
|
|
1937
|
+
|
|
1938
|
+
elif action == "commit-info":
|
|
1939
|
+
if not project_key:
|
|
1940
|
+
raise typer.BadParameter("--project-key required for commit-info")
|
|
1941
|
+
if not repository_slug:
|
|
1942
|
+
raise typer.BadParameter("--repository-slug required for commit-info")
|
|
1943
|
+
if not commit:
|
|
1944
|
+
raise typer.BadParameter("--commit required for commit-info")
|
|
1945
|
+
payload = client.get_commit_info(project_key, repository_slug, commit, path=path)
|
|
1946
|
+
result_obj = {"commit": payload}
|
|
1947
|
+
|
|
1948
|
+
elif action == "commit-changes":
|
|
1949
|
+
if not project_key:
|
|
1950
|
+
raise typer.BadParameter("--project-key required for commit-changes")
|
|
1951
|
+
if not repository_slug:
|
|
1952
|
+
raise typer.BadParameter("--repository-slug required for commit-changes")
|
|
1953
|
+
if not commit_id and not hash_newest:
|
|
1954
|
+
raise typer.BadParameter("--commit-id or --hash-newest required for commit-changes")
|
|
1955
|
+
payload = client.get_commit_changes(
|
|
1956
|
+
project_key, repository_slug, commit_id=commit_id, hash_newest=hash_newest, merges=merges
|
|
1957
|
+
)
|
|
1958
|
+
result_obj = {"changes": payload}
|
|
1959
|
+
|
|
1960
|
+
elif action == "search":
|
|
1961
|
+
if not settings.is_cloud_mode:
|
|
1962
|
+
log.error("cloud_mode_required", message="Code search is only available in Bitbucket Cloud mode")
|
|
1963
|
+
raise typer.Exit(1) from None
|
|
1964
|
+
if not team:
|
|
1965
|
+
raise typer.BadParameter("--team required for search")
|
|
1966
|
+
if not search_query:
|
|
1967
|
+
raise typer.BadParameter("--search-query required for search")
|
|
1968
|
+
payload = client.search_code(team, search_query, page=page, limit=limit or 10)
|
|
1969
|
+
result_obj = {"results": payload}
|
|
1970
|
+
elif action == "search-advanced":
|
|
1971
|
+
if not settings.is_cloud_mode:
|
|
1972
|
+
log.error("cloud_mode_required", message="Code search is only available in Bitbucket Cloud mode")
|
|
1973
|
+
raise typer.Exit(1) from None
|
|
1974
|
+
if not team:
|
|
1975
|
+
raise typer.BadParameter("--team required for search-advanced")
|
|
1976
|
+
if not search_query:
|
|
1977
|
+
raise typer.BadParameter("--search-query required for search-advanced")
|
|
1978
|
+
payload = client.search_code_advanced(
|
|
1979
|
+
team, search_query, page=page, limit=limit or 10, repository=repository
|
|
1980
|
+
)
|
|
1981
|
+
result_obj = {"results": payload, "repository_filter": repository}
|
|
1982
|
+
|
|
1983
|
+
else:
|
|
1984
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
1985
|
+
|
|
1986
|
+
if _cli_state.get("reports", True) and markdown:
|
|
1987
|
+
output_path = report_dir / f"{project_key or team}_code_advanced_{action}.md"
|
|
1988
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1989
|
+
with output_path.open("w") as f:
|
|
1990
|
+
f.write(f"# Advanced Code Operations - {action}\n\n")
|
|
1991
|
+
if project_key:
|
|
1992
|
+
f.write(f"- **Project**: {project_key}\n")
|
|
1993
|
+
if repository_slug:
|
|
1994
|
+
f.write(f"- **Repository**: {repository_slug}\n")
|
|
1995
|
+
if team:
|
|
1996
|
+
f.write(f"- **Team**: {team}\n")
|
|
1997
|
+
f.write(f"- **Action**: {action}\n")
|
|
1998
|
+
log.info("code_advanced_reported", path=str(output_path), action=action)
|
|
1999
|
+
else:
|
|
2000
|
+
_emit_json(result_obj or {})
|
|
2001
|
+
|
|
2002
|
+
except BitbucketClientError as exc:
|
|
2003
|
+
log.error("bitbucket_error", message=str(exc), context=getattr(exc, "context", {}))
|
|
2004
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2005
|
+
raise typer.Exit(1) from None
|
|
2006
|
+
except typer.BadParameter as exc:
|
|
2007
|
+
log.error("parameter_error", message=str(exc))
|
|
2008
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2009
|
+
raise typer.Exit(1) from None
|
|
2010
|
+
except Exception as exc:
|
|
2011
|
+
log.error("unexpected_error", message=str(exc))
|
|
2012
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2013
|
+
raise typer.Exit(1) from None
|
|
2014
|
+
|
|
2015
|
+
|
|
2016
|
+
@app.command()
|
|
2017
|
+
def workspace(
|
|
2018
|
+
action: str = typer.Argument(..., help="Action: list, get, permissions, projects, project"),
|
|
2019
|
+
workspace_slug: str | None = typer.Option(None, "--workspace-slug", help="Workspace slug (required for all actions except list)"),
|
|
2020
|
+
repo_slug: str | None = typer.Option(None, "--repo-slug", help="Repository slug (for permissions action)"),
|
|
2021
|
+
project_key: str | None = typer.Option(None, "--project-key", help="Project key (for project action)"),
|
|
2022
|
+
) -> None:
|
|
2023
|
+
"""Cloud workspace operations (Cloud-only)."""
|
|
2024
|
+
log = structlog.get_logger(__name__)
|
|
2025
|
+
report_dir = _cli_state.get("report_dir", _REPORT_DIR)
|
|
2026
|
+
markdown = _cli_state.get("markdown", True)
|
|
2027
|
+
|
|
2028
|
+
try:
|
|
2029
|
+
settings = load_settings(strict=True)
|
|
2030
|
+
|
|
2031
|
+
# Check Cloud mode requirement
|
|
2032
|
+
if not settings.is_cloud_mode:
|
|
2033
|
+
log.error("cloud_mode_required", message="Workspace operations are only available in Bitbucket Cloud mode")
|
|
2034
|
+
typer.echo("Error: Workspace operations are only available in Bitbucket Cloud mode", err=True)
|
|
2035
|
+
raise typer.Exit(1) from None
|
|
2036
|
+
|
|
2037
|
+
with _build_client(settings) as client:
|
|
2038
|
+
result_obj: dict | None = None
|
|
2039
|
+
|
|
2040
|
+
if action == "list":
|
|
2041
|
+
payload = client.list_workspaces()
|
|
2042
|
+
result_obj = {"workspaces": payload, "count": len(payload)}
|
|
2043
|
+
|
|
2044
|
+
elif action == "get":
|
|
2045
|
+
if not workspace_slug:
|
|
2046
|
+
raise typer.BadParameter("--workspace-slug required for get")
|
|
2047
|
+
payload = client.get_workspace(workspace_slug)
|
|
2048
|
+
result_obj = {"workspace": payload}
|
|
2049
|
+
|
|
2050
|
+
elif action == "permissions":
|
|
2051
|
+
if not workspace_slug:
|
|
2052
|
+
raise typer.BadParameter("--workspace-slug required for permissions")
|
|
2053
|
+
if repo_slug:
|
|
2054
|
+
payload = client.get_workspace_repository_permissions(workspace_slug, repo_slug=repo_slug)
|
|
2055
|
+
result_obj = {"repository_permissions": payload}
|
|
2056
|
+
else:
|
|
2057
|
+
payload = client.get_workspace_permissions(workspace_slug)
|
|
2058
|
+
result_obj = {"workspace_permissions": payload, "count": len(payload)}
|
|
2059
|
+
|
|
2060
|
+
elif action == "projects":
|
|
2061
|
+
if not workspace_slug:
|
|
2062
|
+
raise typer.BadParameter("--workspace-slug required for projects")
|
|
2063
|
+
payload = client.list_workspace_projects(workspace_slug)
|
|
2064
|
+
result_obj = {"projects": payload, "count": len(payload)}
|
|
2065
|
+
|
|
2066
|
+
elif action == "project":
|
|
2067
|
+
if not workspace_slug:
|
|
2068
|
+
raise typer.BadParameter("--workspace-slug required for project")
|
|
2069
|
+
if not project_key:
|
|
2070
|
+
raise typer.BadParameter("--project-key required for project")
|
|
2071
|
+
payload = client.get_workspace_project(workspace_slug, project_key)
|
|
2072
|
+
result_obj = {"project": payload}
|
|
2073
|
+
|
|
2074
|
+
else:
|
|
2075
|
+
raise typer.BadParameter(f"Unknown action: {action}")
|
|
2076
|
+
|
|
2077
|
+
if _cli_state.get("reports", True) and markdown:
|
|
2078
|
+
output_path = report_dir / f"{workspace_slug or 'all'}_workspace_{action}.md"
|
|
2079
|
+
output_path.parent.mkdir(parents=True, exist_ok=True)
|
|
2080
|
+
with output_path.open("w") as f:
|
|
2081
|
+
f.write(f"# Workspace Operations - {action}\n\n")
|
|
2082
|
+
if workspace_slug:
|
|
2083
|
+
f.write(f"- **Workspace**: {workspace_slug}\n")
|
|
2084
|
+
if repo_slug:
|
|
2085
|
+
f.write(f"- **Repository**: {repo_slug}\n")
|
|
2086
|
+
if project_key:
|
|
2087
|
+
f.write(f"- **Project**: {project_key}\n")
|
|
2088
|
+
f.write(f"- **Action**: {action}\n")
|
|
2089
|
+
log.info("workspace_reported", path=str(output_path), action=action, workspace_slug=workspace_slug)
|
|
2090
|
+
else:
|
|
2091
|
+
_emit_json(result_obj or {})
|
|
2092
|
+
|
|
2093
|
+
except BitbucketClientError as exc:
|
|
2094
|
+
log.error("bitbucket_error", message=str(exc), context=getattr(exc, "context", {}))
|
|
2095
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2096
|
+
raise typer.Exit(1) from None
|
|
2097
|
+
except typer.BadParameter as exc:
|
|
2098
|
+
log.error("parameter_error", message=str(exc))
|
|
2099
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2100
|
+
raise typer.Exit(1) from None
|
|
2101
|
+
except Exception as exc:
|
|
2102
|
+
log.error("unexpected_error", message=str(exc))
|
|
2103
|
+
typer.echo(f"Error: {exc}", err=True)
|
|
2104
|
+
raise typer.Exit(1) from None
|
|
2105
|
+
|
|
2106
|
+
|
|
2107
|
+
if __name__ == "__main__":
|
|
2108
|
+
app()
|