mcp-openstack-ops 3.5.4__tar.gz → 3.5.6__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.
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/.github/workflows/pypi-publish.yml +36 -10
- {mcp_openstack_ops-3.5.4/src/mcp_openstack_ops.egg-info → mcp_openstack_ops-3.5.6}/PKG-INFO +15 -2
- mcp_openstack_ops-3.5.4/PKG-INFO → mcp_openstack_ops-3.5.6/README.md +13 -18
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/pyproject.toml +1 -1
- mcp_openstack_ops-3.5.6/scripts/check_release.py +72 -0
- mcp_openstack_ops-3.5.6/scripts/publish_release.py +141 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/connection.py +6 -3
- mcp_openstack_ops-3.5.4/README.md → mcp_openstack_ops-3.5.6/src/mcp_openstack_ops.egg-info/PKG-INFO +31 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops.egg-info/SOURCES.txt +5 -1
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops.egg-info/requires.txt +1 -1
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops.egg-info/scm_file_list.json +4 -0
- mcp_openstack_ops-3.5.6/src/mcp_openstack_ops.egg-info/scm_version.json +8 -0
- mcp_openstack_ops-3.5.6/tests/test_publish_release.py +153 -0
- mcp_openstack_ops-3.5.6/tests/test_release.py +57 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/uv.lock +4 -4
- mcp_openstack_ops-3.5.4/src/mcp_openstack_ops.egg-info/scm_version.json +0 -8
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/.env.example +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/.env.test +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/.github/FUNDING.yml +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/.github/copilot-instructions.md +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/.github/dependabot.yml +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/.gitignore +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/.gitleaks.toml +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/.pre-commit-config.yaml +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/.python-version +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/Dockerfile.MCP-Server +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/Dockerfile.MCPO-Proxy +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/LICENSE +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/MANIFEST.in +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/build-mcp-server-docker-image.sh +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/build-mcpo-server-docker-image.sh +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/diagnose_openstack.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/docker-compose.yml +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/img/MCP-Workflow-of-Quickstart-Tutorial.png +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/img/screenshot-claude-desktop.png +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/img/screenshot-claude-desktop.png:Zone.Identifier +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/img/screenshot-example-cluster-status.png +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/img/screenshot-openstack-dashboard.png +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/main.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/mcp-config.json.http +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/mcp-config.json.multi-project +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/mcp-config.json.stdio +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/scripts/mcp-server-docker-cmd.sh +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/scripts/run-mcp-inspector-local.sh +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/scripts/run-mcp-inspector-pypi.sh +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/setup.cfg +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/__init__.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/__main__.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/functions.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/mcp_main.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/prompt_template.md +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/__init__.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/compute.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/core.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/identity.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/image.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/load_balancer/__init__.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/load_balancer/amphorae.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/load_balancer/core.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/load_balancer/health_monitors.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/load_balancer/l7_policies.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/load_balancer/listeners.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/load_balancer/management.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/load_balancer/pools.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/monitoring.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/network.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/orchestration.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/services/storage.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/__init__.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_availability_zones.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_floating_ip_pools.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_floating_ips.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_heat_stacks.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_hypervisor_details.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_image_detail_list.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_instance.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_instance_by_name.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_instance_details.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_instances_by_status.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_keypair_list.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_amphorae.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_availability_zones.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_details.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_flavors.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_health_monitors.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_l7_policies.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_l7_rules.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_list.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_listeners.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_pool_members.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_pools.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_providers.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_load_balancer_quotas.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_network_details.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_project_details.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_quota.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_resource_monitoring.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_role_assignments.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_routers.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_security_groups.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_server_events.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_server_groups.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_server_volumes.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_service_status.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_usage_statistics.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_user_list.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_volume_list.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_volume_snapshots.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/get_volume_types.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/search_instances.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_alarms.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_compute_agents.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_domains.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_flavor.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_floating_ip.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_floating_ip_port_forwarding.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_heat_stack.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_identity_groups.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_image.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_image_members.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_image_metadata.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_image_visibility.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_instance.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_keypair.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_load_balancer.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_load_balancer_amphora.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_load_balancer_availability_zone.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_load_balancer_flavor.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_load_balancer_health_monitor.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_load_balancer_l7_policy.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_load_balancer_l7_rule.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_load_balancer_listener.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_load_balancer_pool.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_load_balancer_pool_member.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_load_balancer_quota.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_metrics.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_network_agents.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_network_ports.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_network_qos_policies.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_networks.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_project.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_quota.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_roles.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_server_backup.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_server_dump.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_server_fixed_ip.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_server_floating_ip.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_server_group.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_server_migration.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_server_network.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_server_properties.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_server_security_group.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_server_volume.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_service_logs.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_services.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_snapshot.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_subnets.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_volume.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_volume_backups.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_volume_groups.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops/tools/set_volume_qos.py +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops.egg-info/dependency_links.txt +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops.egg-info/entry_points.txt +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops.egg-info/top_level.txt +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/test_compatibility.sh +0 -0
- {mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/test_project_isolation.py +0 -0
|
@@ -1,7 +1,4 @@
|
|
|
1
|
-
|
|
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
|
|
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:
|
|
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,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: mcp-openstack-ops
|
|
3
|
-
Version: 3.5.
|
|
3
|
+
Version: 3.5.6
|
|
4
4
|
Summary: OpenStack operations automation MCP server
|
|
5
5
|
Author-email: JungJungIn <call518@gmail.com>
|
|
6
6
|
Requires-Python: >=3.12
|
|
7
7
|
Description-Content-Type: text/markdown
|
|
8
8
|
License-File: LICENSE
|
|
9
9
|
Requires-Dist: fastmcp>=2.12.3
|
|
10
|
-
Requires-Dist: openstacksdk<=4.
|
|
10
|
+
Requires-Dist: openstacksdk<=4.21.0,>=4.1.0
|
|
11
11
|
Requires-Dist: python-dotenv>=1.1.1
|
|
12
12
|
Requires-Dist: setuptools-scm>=10.0.5
|
|
13
13
|
Requires-Dist: pre-commit>=4.6.0
|
|
@@ -35,6 +35,19 @@ Dynamic: license-file
|
|
|
35
35
|
|
|
36
36
|
[](https://deepwiki.com/call518/MCP-OpenStack-Ops)
|
|
37
37
|
|
|
38
|
+
## Package and GitHub Releases
|
|
39
|
+
|
|
40
|
+
Pushing a release tag runs the GitHub Actions publication workflow: build and
|
|
41
|
+
validate distributions, publish to TestPyPI, publish to PyPI, then create a GitHub
|
|
42
|
+
Release named after the tag. Tags must use `X.Y.Z` or `vX.Y.Z`, optionally followed
|
|
43
|
+
by `aN`, `bN`, or `rcN`; both distribution versions must match the tag.
|
|
44
|
+
|
|
45
|
+
Release notes include full commit messages since the most recently published
|
|
46
|
+
non-draft GitHub Release, in oldest-first topological order. The first Release
|
|
47
|
+
includes all commit history. Existing published Releases are left unchanged;
|
|
48
|
+
an existing draft or a previous Release outside the tag's ancestry stops release
|
|
49
|
+
creation for manual review. Prereleases are marked as such and are not made latest.
|
|
50
|
+
|
|
38
51
|
---
|
|
39
52
|
|
|
40
53
|
## Features
|
|
@@ -1,21 +1,3 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: mcp-openstack-ops
|
|
3
|
-
Version: 3.5.4
|
|
4
|
-
Summary: OpenStack operations automation MCP server
|
|
5
|
-
Author-email: JungJungIn <call518@gmail.com>
|
|
6
|
-
Requires-Python: >=3.12
|
|
7
|
-
Description-Content-Type: text/markdown
|
|
8
|
-
License-File: LICENSE
|
|
9
|
-
Requires-Dist: fastmcp>=2.12.3
|
|
10
|
-
Requires-Dist: openstacksdk<=4.20.0,>=4.1.0
|
|
11
|
-
Requires-Dist: python-dotenv>=1.1.1
|
|
12
|
-
Requires-Dist: setuptools-scm>=10.0.5
|
|
13
|
-
Requires-Dist: pre-commit>=4.6.0
|
|
14
|
-
Provides-Extra: dev
|
|
15
|
-
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
16
|
-
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
17
|
-
Dynamic: license-file
|
|
18
|
-
|
|
19
1
|
# MCP-OpenStack-Ops
|
|
20
2
|
|
|
21
3
|
> **MCP OpenStack Operations Server**: A comprehensive MCP (Model Context Protocol) server providing OpenStack project management and monitoring capabilities with built-in safety controls and single-project scope.
|
|
@@ -35,6 +17,19 @@ Dynamic: license-file
|
|
|
35
17
|
|
|
36
18
|
[](https://deepwiki.com/call518/MCP-OpenStack-Ops)
|
|
37
19
|
|
|
20
|
+
## Package and GitHub Releases
|
|
21
|
+
|
|
22
|
+
Pushing a release tag runs the GitHub Actions publication workflow: build and
|
|
23
|
+
validate distributions, publish to TestPyPI, publish to PyPI, then create a GitHub
|
|
24
|
+
Release named after the tag. Tags must use `X.Y.Z` or `vX.Y.Z`, optionally followed
|
|
25
|
+
by `aN`, `bN`, or `rcN`; both distribution versions must match the tag.
|
|
26
|
+
|
|
27
|
+
Release notes include full commit messages since the most recently published
|
|
28
|
+
non-draft GitHub Release, in oldest-first topological order. The first Release
|
|
29
|
+
includes all commit history. Existing published Releases are left unchanged;
|
|
30
|
+
an existing draft or a previous Release outside the tag's ancestry stops release
|
|
31
|
+
creation for manual review. Prereleases are marked as such and are not made latest.
|
|
32
|
+
|
|
38
33
|
---
|
|
39
34
|
|
|
40
35
|
## Features
|
|
@@ -7,7 +7,7 @@ requires-python = ">=3.12"
|
|
|
7
7
|
authors = [{ name = "JungJungIn", email = "call518@gmail.com" }]
|
|
8
8
|
dependencies = [
|
|
9
9
|
"fastmcp>=2.12.3",
|
|
10
|
-
"openstacksdk>=4.1.0,<=4.
|
|
10
|
+
"openstacksdk>=4.1.0,<=4.21.0",
|
|
11
11
|
"python-dotenv>=1.1.1",
|
|
12
12
|
"setuptools-scm>=10.0.5",
|
|
13
13
|
"pre-commit>=4.6.0",
|
|
@@ -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
|
+
|
|
@@ -152,10 +152,13 @@ def get_current_project_id() -> str:
|
|
|
152
152
|
"""
|
|
153
153
|
try:
|
|
154
154
|
conn = get_openstack_connection()
|
|
155
|
-
# Get project ID from the token
|
|
155
|
+
# Get project ID from the token scope. conn.auth only holds the
|
|
156
|
+
# configured auth args, so it has no project_id when the connection
|
|
157
|
+
# is scoped by project name.
|
|
156
158
|
token = conn.identity.get_token()
|
|
157
|
-
project_id = conn.
|
|
158
|
-
|
|
159
|
+
project_id = conn.current_project_id or \
|
|
160
|
+
conn.auth.get('project_id') or conn.auth.get('tenant_id')
|
|
161
|
+
|
|
159
162
|
if not project_id:
|
|
160
163
|
# Fallback: get from project name
|
|
161
164
|
project_name = os.environ.get("OS_PROJECT_NAME")
|
mcp_openstack_ops-3.5.4/README.md → mcp_openstack_ops-3.5.6/src/mcp_openstack_ops.egg-info/PKG-INFO
RENAMED
|
@@ -1,3 +1,21 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mcp-openstack-ops
|
|
3
|
+
Version: 3.5.6
|
|
4
|
+
Summary: OpenStack operations automation MCP server
|
|
5
|
+
Author-email: JungJungIn <call518@gmail.com>
|
|
6
|
+
Requires-Python: >=3.12
|
|
7
|
+
Description-Content-Type: text/markdown
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Requires-Dist: fastmcp>=2.12.3
|
|
10
|
+
Requires-Dist: openstacksdk<=4.21.0,>=4.1.0
|
|
11
|
+
Requires-Dist: python-dotenv>=1.1.1
|
|
12
|
+
Requires-Dist: setuptools-scm>=10.0.5
|
|
13
|
+
Requires-Dist: pre-commit>=4.6.0
|
|
14
|
+
Provides-Extra: dev
|
|
15
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
16
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
17
|
+
Dynamic: license-file
|
|
18
|
+
|
|
1
19
|
# MCP-OpenStack-Ops
|
|
2
20
|
|
|
3
21
|
> **MCP OpenStack Operations Server**: A comprehensive MCP (Model Context Protocol) server providing OpenStack project management and monitoring capabilities with built-in safety controls and single-project scope.
|
|
@@ -17,6 +35,19 @@
|
|
|
17
35
|
|
|
18
36
|
[](https://deepwiki.com/call518/MCP-OpenStack-Ops)
|
|
19
37
|
|
|
38
|
+
## Package and GitHub Releases
|
|
39
|
+
|
|
40
|
+
Pushing a release tag runs the GitHub Actions publication workflow: build and
|
|
41
|
+
validate distributions, publish to TestPyPI, publish to PyPI, then create a GitHub
|
|
42
|
+
Release named after the tag. Tags must use `X.Y.Z` or `vX.Y.Z`, optionally followed
|
|
43
|
+
by `aN`, `bN`, or `rcN`; both distribution versions must match the tag.
|
|
44
|
+
|
|
45
|
+
Release notes include full commit messages since the most recently published
|
|
46
|
+
non-draft GitHub Release, in oldest-first topological order. The first Release
|
|
47
|
+
includes all commit history. Existing published Releases are left unchanged;
|
|
48
|
+
an existing draft or a previous Release outside the tag's ancestry stops release
|
|
49
|
+
creation for manual review. Prereleases are marked as such and are not made latest.
|
|
50
|
+
|
|
20
51
|
---
|
|
21
52
|
|
|
22
53
|
## Features
|
{mcp_openstack_ops-3.5.4 → mcp_openstack_ops-3.5.6}/src/mcp_openstack_ops.egg-info/SOURCES.txt
RENAMED
|
@@ -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,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
|
+
|