mcp-openstack-ops 3.5.5__tar.gz → 3.5.7__tar.gz

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.
Files changed (166) hide show
  1. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.github/workflows/pypi-publish.yml +36 -10
  2. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/PKG-INFO +1 -1
  3. mcp_openstack_ops-3.5.7/scripts/check_release.py +72 -0
  4. mcp_openstack_ops-3.5.7/scripts/publish_release.py +141 -0
  5. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/PKG-INFO +1 -1
  6. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/SOURCES.txt +5 -1
  7. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/scm_file_list.json +4 -0
  8. mcp_openstack_ops-3.5.7/src/mcp_openstack_ops.egg-info/scm_version.json +8 -0
  9. mcp_openstack_ops-3.5.7/tests/test_publish_release.py +153 -0
  10. mcp_openstack_ops-3.5.7/tests/test_release.py +57 -0
  11. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/uv.lock +1 -1
  12. mcp_openstack_ops-3.5.5/src/mcp_openstack_ops.egg-info/scm_version.json +0 -8
  13. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.env.example +0 -0
  14. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.env.test +0 -0
  15. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.github/FUNDING.yml +0 -0
  16. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.github/copilot-instructions.md +0 -0
  17. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.github/dependabot.yml +0 -0
  18. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.gitignore +0 -0
  19. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.gitleaks.toml +0 -0
  20. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.pre-commit-config.yaml +0 -0
  21. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.python-version +0 -0
  22. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/Dockerfile.MCP-Server +0 -0
  23. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/Dockerfile.MCPO-Proxy +0 -0
  24. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/LICENSE +0 -0
  25. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/MANIFEST.in +0 -0
  26. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/README.md +0 -0
  27. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/build-mcp-server-docker-image.sh +0 -0
  28. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/build-mcpo-server-docker-image.sh +0 -0
  29. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/diagnose_openstack.py +0 -0
  30. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/docker-compose.yml +0 -0
  31. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/MCP-Workflow-of-Quickstart-Tutorial.png +0 -0
  32. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/screenshot-claude-desktop.png +0 -0
  33. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/screenshot-claude-desktop.png:Zone.Identifier +0 -0
  34. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/screenshot-example-cluster-status.png +0 -0
  35. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/screenshot-openstack-dashboard.png +0 -0
  36. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/main.py +0 -0
  37. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/mcp-config.json.http +0 -0
  38. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/mcp-config.json.multi-project +0 -0
  39. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/mcp-config.json.stdio +0 -0
  40. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/pyproject.toml +0 -0
  41. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/scripts/mcp-server-docker-cmd.sh +0 -0
  42. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/scripts/run-mcp-inspector-local.sh +0 -0
  43. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/scripts/run-mcp-inspector-pypi.sh +0 -0
  44. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/setup.cfg +0 -0
  45. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/__init__.py +0 -0
  46. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/__main__.py +0 -0
  47. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/connection.py +0 -0
  48. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/functions.py +0 -0
  49. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/mcp_main.py +0 -0
  50. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/prompt_template.md +0 -0
  51. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/__init__.py +0 -0
  52. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/compute.py +0 -0
  53. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/core.py +0 -0
  54. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/identity.py +0 -0
  55. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/image.py +0 -0
  56. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/__init__.py +0 -0
  57. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/amphorae.py +0 -0
  58. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/core.py +0 -0
  59. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/health_monitors.py +0 -0
  60. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/l7_policies.py +0 -0
  61. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/listeners.py +0 -0
  62. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/management.py +0 -0
  63. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/pools.py +0 -0
  64. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/monitoring.py +0 -0
  65. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/network.py +0 -0
  66. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/orchestration.py +0 -0
  67. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/storage.py +0 -0
  68. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/__init__.py +0 -0
  69. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_availability_zones.py +0 -0
  70. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_floating_ip_pools.py +0 -0
  71. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_floating_ips.py +0 -0
  72. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_heat_stacks.py +0 -0
  73. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_hypervisor_details.py +0 -0
  74. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_image_detail_list.py +0 -0
  75. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_instance.py +0 -0
  76. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_instance_by_name.py +0 -0
  77. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_instance_details.py +0 -0
  78. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_instances_by_status.py +0 -0
  79. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_keypair_list.py +0 -0
  80. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_amphorae.py +0 -0
  81. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_availability_zones.py +0 -0
  82. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_details.py +0 -0
  83. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_flavors.py +0 -0
  84. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_health_monitors.py +0 -0
  85. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_l7_policies.py +0 -0
  86. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_l7_rules.py +0 -0
  87. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_list.py +0 -0
  88. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_listeners.py +0 -0
  89. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_pool_members.py +0 -0
  90. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_pools.py +0 -0
  91. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_providers.py +0 -0
  92. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_quotas.py +0 -0
  93. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_network_details.py +0 -0
  94. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_project_details.py +0 -0
  95. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_quota.py +0 -0
  96. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_resource_monitoring.py +0 -0
  97. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_role_assignments.py +0 -0
  98. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_routers.py +0 -0
  99. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_security_groups.py +0 -0
  100. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_server_events.py +0 -0
  101. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_server_groups.py +0 -0
  102. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_server_volumes.py +0 -0
  103. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_service_status.py +0 -0
  104. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_usage_statistics.py +0 -0
  105. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_user_list.py +0 -0
  106. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_volume_list.py +0 -0
  107. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_volume_snapshots.py +0 -0
  108. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_volume_types.py +0 -0
  109. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/search_instances.py +0 -0
  110. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_alarms.py +0 -0
  111. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_compute_agents.py +0 -0
  112. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_domains.py +0 -0
  113. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_flavor.py +0 -0
  114. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_floating_ip.py +0 -0
  115. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_floating_ip_port_forwarding.py +0 -0
  116. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_heat_stack.py +0 -0
  117. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_identity_groups.py +0 -0
  118. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_image.py +0 -0
  119. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_image_members.py +0 -0
  120. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_image_metadata.py +0 -0
  121. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_image_visibility.py +0 -0
  122. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_instance.py +0 -0
  123. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_keypair.py +0 -0
  124. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer.py +0 -0
  125. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_amphora.py +0 -0
  126. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_availability_zone.py +0 -0
  127. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_flavor.py +0 -0
  128. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_health_monitor.py +0 -0
  129. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_l7_policy.py +0 -0
  130. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_l7_rule.py +0 -0
  131. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_listener.py +0 -0
  132. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_pool.py +0 -0
  133. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_pool_member.py +0 -0
  134. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_quota.py +0 -0
  135. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_metrics.py +0 -0
  136. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_network_agents.py +0 -0
  137. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_network_ports.py +0 -0
  138. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_network_qos_policies.py +0 -0
  139. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_networks.py +0 -0
  140. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_project.py +0 -0
  141. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_quota.py +0 -0
  142. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_roles.py +0 -0
  143. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_backup.py +0 -0
  144. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_dump.py +0 -0
  145. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_fixed_ip.py +0 -0
  146. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_floating_ip.py +0 -0
  147. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_group.py +0 -0
  148. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_migration.py +0 -0
  149. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_network.py +0 -0
  150. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_properties.py +0 -0
  151. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_security_group.py +0 -0
  152. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_volume.py +0 -0
  153. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_service_logs.py +0 -0
  154. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_services.py +0 -0
  155. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_snapshot.py +0 -0
  156. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_subnets.py +0 -0
  157. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_volume.py +0 -0
  158. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_volume_backups.py +0 -0
  159. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_volume_groups.py +0 -0
  160. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_volume_qos.py +0 -0
  161. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/dependency_links.txt +0 -0
  162. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/entry_points.txt +0 -0
  163. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/requires.txt +0 -0
  164. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/top_level.txt +0 -0
  165. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/test_compatibility.sh +0 -0
  166. {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/test_project_isolation.py +0 -0
@@ -1,7 +1,4 @@
1
- # This workflow will install Python dependencies, run tests and lint with a variety of Python versions
2
- # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
3
-
4
- name: Publish to PyPI on tag
1
+ name: Publish Packages and GitHub Release on Tag
5
2
 
6
3
  on:
7
4
  push:
@@ -9,7 +6,6 @@ on:
9
6
 
10
7
  permissions:
11
8
  contents: read
12
- id-token: write
13
9
 
14
10
  jobs:
15
11
  build:
@@ -22,14 +18,21 @@ jobs:
22
18
  - uses: actions/setup-python@v7
23
19
  with:
24
20
  python-version: "3.12.x"
25
- - name: Install build tool
26
- run: pip install --upgrade build
21
+ - name: Install build and release test tools
22
+ run: pip install --upgrade build pytest PyYAML
23
+ - name: Test release automation
24
+ run: python -m pytest -q tests/test_publish_release.py tests/test_release.py
27
25
  - name: Build distributions
28
26
  run: python -m build
27
+ - name: Verify tag and distribution versions
28
+ env:
29
+ RELEASE_TAG: ${{ github.ref_name }}
30
+ run: python -m scripts.check_release "$RELEASE_TAG" dist
29
31
  - uses: actions/upload-artifact@v7
30
32
  with:
31
33
  name: dist
32
34
  path: dist/
35
+ if-no-files-found: error
33
36
 
34
37
  publish-to-testpypi:
35
38
  name: Publish to TestPyPI
@@ -55,8 +58,7 @@ jobs:
55
58
  name: Publish to PyPI
56
59
  if: startsWith(github.ref, 'refs/tags/')
57
60
  runs-on: ubuntu-latest
58
- needs: build
59
- #needs: publish-to-testpypi
61
+ needs: publish-to-testpypi
60
62
  permissions:
61
63
  id-token: write
62
64
  environment:
@@ -70,4 +72,28 @@ jobs:
70
72
  - name: Publish to PyPI
71
73
  uses: pypa/gh-action-pypi-publish@release/v1
72
74
  with:
73
- repository-url: https://upload.pypi.org/legacy/
75
+ repository-url: https://upload.pypi.org/legacy/
76
+
77
+ release:
78
+ name: Publish GitHub Release
79
+ needs: publish-to-pypi
80
+ runs-on: ubuntu-latest
81
+ timeout-minutes: 5
82
+ permissions:
83
+ contents: write
84
+ concurrency:
85
+ group: github-release-${{ github.repository }}
86
+ cancel-in-progress: false
87
+ queue: max
88
+ env:
89
+ GH_TOKEN: ${{ github.token }}
90
+ RELEASE_TAG: ${{ github.ref_name }}
91
+ steps:
92
+ - name: Check Out Tagged Repository
93
+ uses: actions/checkout@v7
94
+ with:
95
+ persist-credentials: false
96
+ fetch-depth: 0
97
+
98
+ - name: Publish Release with Commit Messages
99
+ run: python3 -m scripts.publish_release "$RELEASE_TAG"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-openstack-ops
3
- Version: 3.5.5
3
+ Version: 3.5.7
4
4
  Summary: OpenStack operations automation MCP server
5
5
  Author-email: JungJungIn <call518@gmail.com>
6
6
  Requires-Python: >=3.12
@@ -0,0 +1,72 @@
1
+ """Require both distribution metadata versions to match the release tag."""
2
+
3
+ import argparse
4
+ import re
5
+ import tarfile
6
+ import zipfile
7
+ from email.parser import BytesParser
8
+ from pathlib import Path
9
+
10
+
11
+ def release_version(tag: str) -> str:
12
+ """Validate a canonical release tag and return its package version."""
13
+ version = tag.removeprefix("v")
14
+ number = r"(?:0|[1-9][0-9]*)"
15
+ if not re.fullmatch(
16
+ rf"{number}\.{number}\.{number}(?:(?:a|b|rc){number})?", version
17
+ ):
18
+ raise ValueError("Use a tag such as 0.5.7, v0.5.7, or 0.5.7rc1")
19
+ return version
20
+
21
+
22
+ def check_release(tag: str, directory: Path) -> None:
23
+ """Validate a canonical release tag and the wheel/sdist prepared for upload."""
24
+ version = release_version(tag)
25
+ wheels = list(directory.glob("*.whl"))
26
+ sdists = list(directory.glob("*.tar.gz"))
27
+ if len(wheels) != 1 or len(sdists) != 1:
28
+ raise ValueError("Expected exactly one wheel and one source distribution")
29
+
30
+ with zipfile.ZipFile(wheels[0]) as archive:
31
+ metadata_files = [
32
+ name for name in archive.namelist() if name.endswith(".dist-info/METADATA")
33
+ ]
34
+ if len(metadata_files) != 1:
35
+ raise ValueError("Expected one wheel metadata file")
36
+ wheel_metadata = archive.read(metadata_files[0])
37
+ with tarfile.open(sdists[0], "r:gz") as archive:
38
+ metadata_files = [
39
+ item
40
+ for item in archive.getmembers()
41
+ if len(Path(item.name).parts) == 2 and item.name.endswith("/PKG-INFO")
42
+ ]
43
+ if len(metadata_files) != 1:
44
+ raise ValueError("Expected one source distribution metadata file")
45
+ stream = archive.extractfile(metadata_files[0])
46
+ if stream is None:
47
+ raise ValueError("Source distribution metadata is not a file")
48
+ with stream:
49
+ sdist_metadata = stream.read()
50
+
51
+ for path, raw in zip((wheels[0], sdists[0]), (wheel_metadata, sdist_metadata)):
52
+ metadata = BytesParser().parsebytes(raw)
53
+ if metadata.get("Name") != "mcp-openstack-ops" or metadata.get("Version") != version:
54
+ raise ValueError(f"{path.name}: expected mcp-openstack-ops {version}")
55
+ print(f"Verified {path.name}: mcp-openstack-ops {version}")
56
+
57
+
58
+ def main() -> None:
59
+ """Check release artifacts without uploading or modifying them."""
60
+ parser = argparse.ArgumentParser(description=__doc__)
61
+ parser.add_argument("tag")
62
+ parser.add_argument("directory", type=Path)
63
+ args = parser.parse_args()
64
+ try:
65
+ check_release(args.tag, args.directory)
66
+ except ValueError as exc:
67
+ parser.exit(1, f"Release validation failed: {exc}\n")
68
+
69
+
70
+ if __name__ == "__main__":
71
+ main()
72
+
@@ -0,0 +1,141 @@
1
+ """Publish a tag's GitHub Release after package publication has succeeded."""
2
+
3
+ import argparse
4
+ import json
5
+ import os
6
+ import re
7
+ import subprocess
8
+ import tempfile
9
+ from pathlib import Path
10
+ from typing import Any
11
+ from urllib.parse import quote
12
+
13
+ from scripts.check_release import release_version
14
+
15
+
16
+ def command(*args: str) -> str:
17
+ """Run an argument list without interpreting commit messages as shell code."""
18
+ return subprocess.run(
19
+ args, check=True, capture_output=True, text=True
20
+ ).stdout.strip()
21
+
22
+
23
+ def release_notes(repository: str, tag: str, previous: str | None) -> str:
24
+ """Collect full commit messages in oldest-first topological order."""
25
+ current = command("git", "rev-parse", "--verify", f"refs/tags/{tag}^{{commit}}")
26
+ revision = current
27
+ if previous:
28
+ base = command(
29
+ "git", "rev-parse", "--verify", f"refs/tags/{previous}^{{commit}}"
30
+ )
31
+ command("git", "merge-base", "--is-ancestor", base, current)
32
+ revision = f"{base}..{current}"
33
+ raw = command(
34
+ "git",
35
+ "log",
36
+ "--reverse",
37
+ "--topo-order",
38
+ "--format=%H%x00%B%x00",
39
+ revision,
40
+ "--",
41
+ )
42
+ parts = raw.split("\0")
43
+ notes = [
44
+ f"## {tag}",
45
+ f"Changes since {previous}." if previous else "Initial release.",
46
+ "",
47
+ ]
48
+ for index in range(0, len(parts) - 1, 2):
49
+ sha, message = parts[index].strip(), parts[index + 1].strip()
50
+ # A longer fence preserves messages containing Markdown code fences.
51
+ fence = "`" * max(
52
+ 3, max((len(m) + 1 for m in re.findall(r"`+", message)), default=3)
53
+ )
54
+ notes.extend(
55
+ [
56
+ f"### [{sha[:7]}](https://github.com/{repository}/commit/{sha})",
57
+ f"{fence}text",
58
+ message,
59
+ fence,
60
+ "",
61
+ ]
62
+ )
63
+ if previous:
64
+ notes.append(
65
+ f"[Full Changelog](https://github.com/{repository}/compare/{quote(previous, safe='')}...{quote(tag, safe='')})"
66
+ )
67
+ return "\n".join(notes) + "\n"
68
+
69
+
70
+ def publish_release(repository: str, tag: str) -> None:
71
+ """Create a release once, using the last published release as the baseline."""
72
+ version = release_version(tag)
73
+ current = command("git", "rev-parse", "--verify", f"refs/tags/{tag}^{{commit}}")
74
+ remote = command("gh", "api", f"repos/{repository}/commits/{tag}", "--jq", ".sha")
75
+ if current != remote or current != command("git", "rev-parse", "HEAD"):
76
+ raise ValueError(
77
+ "The remote tag, local tag, and checked-out commit must match."
78
+ )
79
+ pages = json.loads(
80
+ command(
81
+ "gh",
82
+ "api",
83
+ "--paginate",
84
+ "--slurp",
85
+ f"repos/{repository}/releases?per_page=100",
86
+ )
87
+ )
88
+ releases: list[dict[str, Any]] = [release for page in pages for release in page]
89
+ existing = next(
90
+ (release for release in releases if release["tag_name"] == tag), None
91
+ )
92
+ if existing:
93
+ if existing["draft"]:
94
+ raise ValueError("A draft already exists for this tag; review it manually.")
95
+ print(f"Release {tag} already exists; leaving its content unchanged.")
96
+ return
97
+ published = [release for release in releases if not release["draft"]]
98
+ previous = (
99
+ max(published, key=lambda release: release["published_at"])["tag_name"]
100
+ if published
101
+ else None
102
+ )
103
+ notes = release_notes(repository, tag, previous)
104
+ with tempfile.TemporaryDirectory(prefix="openstack-ops-release-") as temporary:
105
+ path = Path(temporary) / "release-notes.md"
106
+ path.write_text(notes, encoding="utf-8")
107
+ args = [
108
+ "gh",
109
+ "release",
110
+ "create",
111
+ tag,
112
+ "--repo",
113
+ repository,
114
+ "--verify-tag",
115
+ "--title",
116
+ tag,
117
+ "--notes-file",
118
+ str(path),
119
+ ]
120
+ if re.search(r"(?:a|b|rc)\d+$", version):
121
+ args.extend(["--prerelease", "--latest=false"])
122
+ command(*args)
123
+ print(f"Published GitHub Release {tag} (previous release: {previous or 'none'}).")
124
+
125
+
126
+ def main() -> None:
127
+ """Publish only when explicitly called by the final release job."""
128
+ parser = argparse.ArgumentParser(description=__doc__)
129
+ parser.add_argument("tag")
130
+ args = parser.parse_args()
131
+ try:
132
+ publish_release(os.environ["GITHUB_REPOSITORY"], args.tag)
133
+ except subprocess.CalledProcessError as error:
134
+ parser.exit(1, f"Release command failed: {error.stderr or error}\n")
135
+ except (ValueError, KeyError, OSError) as error:
136
+ parser.exit(1, f"GitHub Release failed: {error}\n")
137
+
138
+
139
+ if __name__ == "__main__":
140
+ main()
141
+
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mcp-openstack-ops
3
- Version: 3.5.5
3
+ Version: 3.5.7
4
4
  Summary: OpenStack operations automation MCP server
5
5
  Author-email: JungJungIn <call518@gmail.com>
6
6
  Requires-Python: >=3.12
@@ -30,7 +30,9 @@ img/screenshot-claude-desktop.png
30
30
  img/screenshot-claude-desktop.png:Zone.Identifier
31
31
  img/screenshot-example-cluster-status.png
32
32
  img/screenshot-openstack-dashboard.png
33
+ scripts/check_release.py
33
34
  scripts/mcp-server-docker-cmd.sh
35
+ scripts/publish_release.py
34
36
  scripts/run-mcp-inspector-local.sh
35
37
  scripts/run-mcp-inspector-pypi.sh
36
38
  src/mcp_openstack_ops/__init__.py
@@ -156,4 +158,6 @@ src/mcp_openstack_ops/tools/set_subnets.py
156
158
  src/mcp_openstack_ops/tools/set_volume.py
157
159
  src/mcp_openstack_ops/tools/set_volume_backups.py
158
160
  src/mcp_openstack_ops/tools/set_volume_groups.py
159
- src/mcp_openstack_ops/tools/set_volume_qos.py
161
+ src/mcp_openstack_ops/tools/set_volume_qos.py
162
+ tests/test_publish_release.py
163
+ tests/test_release.py
@@ -29,7 +29,9 @@
29
29
  "mcp-config.json.multi-project",
30
30
  "mcp-config.json.stdio",
31
31
  "pyproject.toml",
32
+ "scripts/check_release.py",
32
33
  "scripts/mcp-server-docker-cmd.sh",
34
+ "scripts/publish_release.py",
33
35
  "scripts/run-mcp-inspector-local.sh",
34
36
  "scripts/run-mcp-inspector-pypi.sh",
35
37
  "src/mcp_openstack_ops/__init__.py",
@@ -150,6 +152,8 @@
150
152
  "src/mcp_openstack_ops/tools/set_volume_qos.py",
151
153
  "test_compatibility.sh",
152
154
  "test_project_isolation.py",
155
+ "tests/test_publish_release.py",
156
+ "tests/test_release.py",
153
157
  "uv.lock"
154
158
  ]
155
159
  }
@@ -0,0 +1,8 @@
1
+ {
2
+ "tag": "3.5.7",
3
+ "distance": 0,
4
+ "node": "gcf2349f49bdda7322083fcebde270dbb7f441672",
5
+ "dirty": false,
6
+ "branch": "HEAD",
7
+ "node_date": "2026-10-24"
8
+ }
@@ -0,0 +1,153 @@
1
+ """Verify release publication without writing Git state or calling GitHub."""
2
+
3
+ import json
4
+ import subprocess
5
+ from pathlib import Path
6
+ from typing import Any
7
+
8
+ import pytest
9
+ import yaml
10
+
11
+ from scripts import publish_release
12
+
13
+ REPOSITORY = "example/project"
14
+ CURRENT = "b" * 40
15
+ BASE = "a" * 40
16
+
17
+
18
+ @pytest.fixture
19
+ def commands(monkeypatch: pytest.MonkeyPatch) -> dict[str, Any]:
20
+ state: dict[str, Any] = {"calls": [], "created": [], "releases": [[]]}
21
+
22
+ def execute(*args: str) -> str:
23
+ state["calls"].append(args)
24
+ if args[:3] == ("gh", "release", "create"):
25
+ path = Path(args[args.index("--notes-file") + 1])
26
+ state["created"].append((args, path.read_text()))
27
+ return "https://github.com/example/project/releases/tag/0.6.6"
28
+ if args[:2] == ("gh", "api"):
29
+ if "--paginate" in args:
30
+ if state.get("api_failure"):
31
+ raise subprocess.CalledProcessError(1, args, stderr="API failed")
32
+ return json.dumps(state["releases"])
33
+ return state.get("remote", CURRENT)
34
+ if args == ("git", "rev-parse", "HEAD"):
35
+ return state.get("head", CURRENT)
36
+ if args[:3] == ("git", "rev-parse", "--verify"):
37
+ return (
38
+ CURRENT
39
+ if args[-1].endswith("0.6.6^{commit}")
40
+ or args[-1].endswith("0.6.6rc1^{commit}")
41
+ else BASE
42
+ )
43
+ if args[:3] == ("git", "merge-base", "--is-ancestor"):
44
+ if state.get("unrelated"):
45
+ raise subprocess.CalledProcessError(1, args)
46
+ return ""
47
+ if args[:2] == ("git", "log"):
48
+ return (
49
+ f"{'c' * 40}\0feat: first change\n\nBREAKING CHANGE: use start\n\0\n"
50
+ f"{CURRENT}\0fix: second change\n\n```sh\n$(touch marker)\n```\n\0"
51
+ )
52
+ raise AssertionError(args)
53
+
54
+ monkeypatch.setattr(publish_release, "command", execute)
55
+ return state
56
+
57
+
58
+ def release(tag: str, published_at: str, *, draft: bool = False) -> dict[str, Any]:
59
+ return {"tag_name": tag, "published_at": published_at, "draft": draft}
60
+
61
+
62
+ def test_uses_previous_published_release_across_pages(commands) -> None:
63
+ commands["releases"] = [
64
+ [release("0.6.5", "2026-10-24T12:00:00Z", draft=True)],
65
+ [release("0.6.4", "2026-10-23T12:00:00Z")],
66
+ [release("0.6.3", "2026-10-22T12:00:00Z")],
67
+ ]
68
+ publish_release.publish_release(REPOSITORY, "0.6.6")
69
+ args, notes = commands["created"][0]
70
+ assert "--verify-tag" in args
71
+ assert args[args.index("--title") + 1] == "0.6.6"
72
+ assert "Changes since 0.6.4." in notes
73
+ assert "0.6.4...0.6.6" in notes
74
+ assert notes.index("first change") < notes.index("second change")
75
+ assert "BREAKING CHANGE: use start" in notes
76
+ assert "$(touch marker)" in notes
77
+ assert "````text" in notes
78
+ assert ("git", "merge-base", "--is-ancestor", BASE, CURRENT) in commands["calls"]
79
+ log = next(call for call in commands["calls"] if call[:2] == ("git", "log"))
80
+ assert "--reverse" in log and "--topo-order" in log
81
+ assert f"{BASE}..{CURRENT}" in log
82
+ assert not Path(args[args.index("--notes-file") + 1]).exists()
83
+
84
+
85
+ def test_published_release_is_not_overwritten(commands) -> None:
86
+ commands["releases"] = [[release("0.6.6", "2026-10-24T12:00:00Z")]]
87
+ publish_release.publish_release(REPOSITORY, "0.6.6")
88
+ assert commands["created"] == []
89
+ assert not any(call[:2] == ("git", "log") for call in commands["calls"])
90
+
91
+
92
+ def test_existing_draft_requires_manual_review(commands) -> None:
93
+ commands["releases"] = [[release("0.6.6", "", draft=True)]]
94
+ with pytest.raises(ValueError, match="draft already exists"):
95
+ publish_release.publish_release(REPOSITORY, "0.6.6")
96
+ assert commands["created"] == []
97
+
98
+
99
+ def test_first_release_includes_all_history(commands) -> None:
100
+ publish_release.publish_release(REPOSITORY, "0.6.6")
101
+ _, notes = commands["created"][0]
102
+ assert "Initial release." in notes
103
+ assert "Full Changelog" not in notes
104
+ log = next(call for call in commands["calls"] if call[:2] == ("git", "log"))
105
+ assert log[-2:] == (CURRENT, "--")
106
+
107
+
108
+ def test_prerelease_is_not_marked_latest(commands) -> None:
109
+ publish_release.publish_release(REPOSITORY, "0.6.6rc1")
110
+ args, _ = commands["created"][0]
111
+ assert "--prerelease" in args and "--latest=false" in args
112
+
113
+
114
+ @pytest.mark.parametrize("field", ["remote", "head"])
115
+ def test_mismatched_commit_blocks_publication(commands, field) -> None:
116
+ commands[field] = "d" * 40
117
+ with pytest.raises(ValueError, match="must match"):
118
+ publish_release.publish_release(REPOSITORY, "0.6.6")
119
+ assert commands["created"] == []
120
+
121
+
122
+ @pytest.mark.parametrize("failure", ["api_failure", "unrelated"])
123
+ def test_api_or_ancestry_failure_does_not_publish(commands, failure) -> None:
124
+ commands["releases"] = [[release("0.6.4", "2026-10-23T12:00:00Z")]]
125
+ commands[failure] = True
126
+ with pytest.raises(subprocess.CalledProcessError):
127
+ publish_release.publish_release(REPOSITORY, "0.6.6")
128
+ assert commands["created"] == []
129
+
130
+
131
+ def test_invalid_tag_does_not_call_commands(commands) -> None:
132
+ with pytest.raises(ValueError):
133
+ publish_release.publish_release(REPOSITORY, "$(touch marker)")
134
+ assert commands["calls"] == []
135
+
136
+
137
+ def test_release_job_is_separate_and_uses_scoped_permissions() -> None:
138
+ root = Path(__file__).resolve().parents[1]
139
+ workflow = yaml.safe_load((root / ".github/workflows/pypi-publish.yml").read_text())
140
+ assert workflow["permissions"] == {"contents": "read"}
141
+ job = workflow["jobs"]["release"]
142
+ assert job["needs"] == "publish-to-pypi"
143
+ assert job["permissions"] == {"contents": "write"}
144
+ assert job["concurrency"]["cancel-in-progress"] is False
145
+ assert job["concurrency"]["queue"] == "max"
146
+ assert job["steps"][0]["with"]["fetch-depth"] == 0
147
+ assert job["env"]["GH_TOKEN"] == "${{ github.token }}"
148
+ assert (
149
+ job["steps"][-1]["run"] == 'python3 -m scripts.publish_release "$RELEASE_TAG"'
150
+ )
151
+ for name in ("publish-to-testpypi", "publish-to-pypi"):
152
+ assert workflow["jobs"][name]["permissions"] == {"id-token": "write"}
153
+
@@ -0,0 +1,57 @@
1
+ """Check that tag publication rejects incorrect or incomplete distributions."""
2
+
3
+ import io
4
+ import tarfile
5
+ import zipfile
6
+ from pathlib import Path
7
+
8
+ import pytest
9
+
10
+ from scripts.check_release import check_release
11
+
12
+
13
+ def write_distributions(directory: Path, version: str, name: str = "mcp-openstack-ops") -> None:
14
+ metadata = f"Name: {name}\nVersion: {version}\n".encode()
15
+ with zipfile.ZipFile(directory / "package.whl", "w") as archive:
16
+ archive.writestr("mcp-openstack-ops.dist-info/METADATA", metadata)
17
+ with tarfile.open(directory / "package.tar.gz", "w:gz") as archive:
18
+ member = tarfile.TarInfo("mcp-openstack-ops/PKG-INFO")
19
+ member.size = len(metadata)
20
+ archive.addfile(member, io.BytesIO(metadata))
21
+
22
+
23
+ @pytest.mark.parametrize("tag", ["0.5.7", "v0.5.7", "0.5.7rc1", "v0.5.7a1"])
24
+ def test_matching_release(tag: str, tmp_path: Path) -> None:
25
+ write_distributions(tmp_path, tag.removeprefix("v"))
26
+ check_release(tag, tmp_path)
27
+
28
+
29
+ @pytest.mark.parametrize("tag", ["latest", "0.5", "0.5.7+dirty", "01.5.7", "0.5.7-rc1"])
30
+ def test_invalid_release_tag(tag: str, tmp_path: Path) -> None:
31
+ with pytest.raises(ValueError, match="Use a tag"):
32
+ check_release(tag, tmp_path)
33
+
34
+
35
+ @pytest.mark.parametrize("version,name", [("0.5.8", "mcp-openstack-ops"), ("0.5.7", "other")])
36
+ def test_wrong_distribution(version: str, name: str, tmp_path: Path) -> None:
37
+ write_distributions(tmp_path, version, name)
38
+ with pytest.raises(ValueError, match="expected mcp-openstack-ops 0.5.7"):
39
+ check_release("0.5.7", tmp_path)
40
+
41
+
42
+ def test_sdist_version_must_also_match(tmp_path: Path) -> None:
43
+ write_distributions(tmp_path, "0.5.8")
44
+ with zipfile.ZipFile(tmp_path / "package.whl", "w") as archive:
45
+ archive.writestr(
46
+ "mcp-openstack-ops.dist-info/METADATA", "Name: mcp-openstack-ops\nVersion: 0.5.7\n"
47
+ )
48
+ with pytest.raises(ValueError, match="package.tar.gz"):
49
+ check_release("0.5.7", tmp_path)
50
+
51
+
52
+ def test_incomplete_distributions(tmp_path: Path) -> None:
53
+ write_distributions(tmp_path, "0.5.7")
54
+ (tmp_path / "package.tar.gz").unlink()
55
+ with pytest.raises(ValueError, match="exactly one"):
56
+ check_release("0.5.7", tmp_path)
57
+
@@ -752,7 +752,7 @@ dev = [
752
752
  [package.metadata]
753
753
  requires-dist = [
754
754
  { name = "fastmcp", specifier = ">=2.12.3" },
755
- { name = "openstacksdk", specifier = ">=4.1.0,<=4.20.0" },
755
+ { name = "openstacksdk", specifier = ">=4.1.0,<=4.21.0" },
756
756
  { name = "pre-commit", specifier = ">=4.6.0" },
757
757
  { name = "pytest", marker = "extra == 'dev'", specifier = ">=7.0.0" },
758
758
  { name = "pytest-asyncio", marker = "extra == 'dev'", specifier = ">=0.21.0" },
@@ -1,8 +0,0 @@
1
- {
2
- "tag": "3.5.5",
3
- "distance": 0,
4
- "node": "g84c7c43228409000711b9a0f3da9865e5e18df37",
5
- "dirty": false,
6
- "branch": "HEAD",
7
- "node_date": "2026-10-22"
8
- }