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.
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.github/workflows/pypi-publish.yml +36 -10
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/PKG-INFO +1 -1
- mcp_openstack_ops-3.5.7/scripts/check_release.py +72 -0
- mcp_openstack_ops-3.5.7/scripts/publish_release.py +141 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/PKG-INFO +1 -1
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/SOURCES.txt +5 -1
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/scm_file_list.json +4 -0
- mcp_openstack_ops-3.5.7/src/mcp_openstack_ops.egg-info/scm_version.json +8 -0
- mcp_openstack_ops-3.5.7/tests/test_publish_release.py +153 -0
- mcp_openstack_ops-3.5.7/tests/test_release.py +57 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/uv.lock +1 -1
- mcp_openstack_ops-3.5.5/src/mcp_openstack_ops.egg-info/scm_version.json +0 -8
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.env.example +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.env.test +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.github/FUNDING.yml +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.github/copilot-instructions.md +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.github/dependabot.yml +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.gitignore +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.gitleaks.toml +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.pre-commit-config.yaml +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/.python-version +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/Dockerfile.MCP-Server +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/Dockerfile.MCPO-Proxy +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/LICENSE +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/MANIFEST.in +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/README.md +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/build-mcp-server-docker-image.sh +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/build-mcpo-server-docker-image.sh +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/diagnose_openstack.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/docker-compose.yml +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/MCP-Workflow-of-Quickstart-Tutorial.png +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/screenshot-claude-desktop.png +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/screenshot-claude-desktop.png:Zone.Identifier +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/screenshot-example-cluster-status.png +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/screenshot-openstack-dashboard.png +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/main.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/mcp-config.json.http +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/mcp-config.json.multi-project +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/mcp-config.json.stdio +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/pyproject.toml +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/scripts/mcp-server-docker-cmd.sh +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/scripts/run-mcp-inspector-local.sh +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/scripts/run-mcp-inspector-pypi.sh +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/setup.cfg +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/__init__.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/__main__.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/connection.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/functions.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/mcp_main.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/prompt_template.md +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/__init__.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/compute.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/core.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/identity.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/image.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/__init__.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/amphorae.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/core.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/health_monitors.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/l7_policies.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/listeners.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/management.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/load_balancer/pools.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/monitoring.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/network.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/orchestration.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/services/storage.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/__init__.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_availability_zones.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_floating_ip_pools.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_floating_ips.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_heat_stacks.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_hypervisor_details.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_image_detail_list.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_instance.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_instance_by_name.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_instance_details.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_instances_by_status.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_keypair_list.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_amphorae.py +0 -0
- {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
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_details.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_flavors.py +0 -0
- {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
- {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
- {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
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_list.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_listeners.py +0 -0
- {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
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_pools.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_providers.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_load_balancer_quotas.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_network_details.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_project_details.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_quota.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_resource_monitoring.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_role_assignments.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_routers.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_security_groups.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_server_events.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_server_groups.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_server_volumes.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_service_status.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_usage_statistics.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_user_list.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_volume_list.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_volume_snapshots.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/get_volume_types.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/search_instances.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_alarms.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_compute_agents.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_domains.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_flavor.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_floating_ip.py +0 -0
- {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
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_heat_stack.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_identity_groups.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_image.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_image_members.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_image_metadata.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_image_visibility.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_instance.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_keypair.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_amphora.py +0 -0
- {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
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_flavor.py +0 -0
- {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
- {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
- {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
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_listener.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_pool.py +0 -0
- {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
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_load_balancer_quota.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_metrics.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_network_agents.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_network_ports.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_network_qos_policies.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_networks.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_project.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_quota.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_roles.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_backup.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_dump.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_fixed_ip.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_floating_ip.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_group.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_migration.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_network.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_properties.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_security_group.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_server_volume.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_service_logs.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_services.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_snapshot.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_subnets.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_volume.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_volume_backups.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_volume_groups.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops/tools/set_volume_qos.py +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/dependency_links.txt +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/entry_points.txt +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/requires.txt +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/src/mcp_openstack_ops.egg-info/top_level.txt +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/test_compatibility.sh +0 -0
- {mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/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"
|
|
@@ -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
|
+
|
{mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/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
|
+
|
|
@@ -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.
|
|
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" },
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/MCP-Workflow-of-Quickstart-Tutorial.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
{mcp_openstack_ops-3.5.5 → mcp_openstack_ops-3.5.7}/img/screenshot-example-cluster-status.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|