Skip to content

fix(cicd): make a server's version a single source of truth - #4604

Open
dihannahdi wants to merge 1 commit into
awslabs:mainfrom
dihannahdi:fix/version-single-source-of-truth
Open

fix(cicd): make a server's version a single source of truth#4604
dihannahdi wants to merge 1 commit into
awslabs:mainfrom
dihannahdi:fix/version-single-source-of-truth

Conversation

@dihannahdi

Copy link
Copy Markdown

Fixes #4476, fixes #4477, fixes #425

Summary

pyproject.toml is the only version that reaches PyPI — python.yml's uv build reads nothing else. Every hardcoded version literal inside a package is therefore a second copy that something has to keep in sync, and the thing meant to keep it in sync fails open.

PyPiPackage.bump_version() in .github/workflows/release.py always bumps pyproject.toml, then makes a best-effort attempt to patch a literal in awslabs/<module>/__init__.py. It fails silently three ways:

  1. The directory name was derived, not discovered. module_name = package_name[8:].replace('-', '_') produces aws_location_mcp_server, but aws-location-mcp-server's directory is aws_location_server. init_file.exists() is False, so it warned and returned success. That constant froze at 1.0.0 while pyproject.toml reached 2.1.1.
  2. The regex only matches a quoted literal. amazon-translate's __init__.py is __version__ = MCP_SERVER_VERSION, an identifier, so the real constant in consts.py was never touched.
  3. It only ever opens that one file. A literal in server.py or a utilities/ module is invisible to it.

All three paths click.echo a warning and still return the new version, and the commit step only checks git diff --cached --quiet over src/, which is always dirty because pyproject.toml did change. So the release PR opens green.

Rather than teach the release script to chase every literal, this removes the duplicate. The version is read from the installed distribution with importlib.metadata, following the pattern already used by postgres, mssql and oracle. There is then nothing to sync.

Changes

Six servers were drifting. Three fed the stale value straight into botocore's user_agent_extra, so every AWS API call reported the wrong version:

server stale constant pyproject.toml consumed at
amazon-translate consts.py 1.0.0 1.0.7 aws_client.py:93
billing-cost-management utilities/aws_service_base.py 1.0.0 0.0.37 same file, :99
stepfunctions-tool server.py 0.1.5 0.2.1 aws_helper.py:55
aws-location __init__.py 1.0.0 2.1.1 not consumed
valkey version.py 0.2.0 1.1.1 not consumed, file removed
sagemaker-ai sagemaker_hyperpod/__init__.py 1.0.0 1.1.0 not consumed, constant removed

billing-cost-management and stepfunctions-tool each had a correct constant and a stale shadow, and the consumer had picked the shadow.

  • release.py now discovers the subpackage directory instead of deriving its name, raises on a genuinely structural problem instead of warning, and treats the absence of a literal as correct rather than as a failure. validate_path_security is preserved, now called only for a file that exists — previously it raised for a missing file, was caught by the local except ValueError, and was misreported as "Cannot update __init__.py safely", which made the "not found" branch unreachable.
  • scripts/verify_version_sync.py walks the AST of every module under awslabs/ and fails when a version literal disagrees with pyproject.toml. It catches all three of the blind spots above because it does not care which file the literal lives in, and it parses rather than regex-matches, since the regex is what failed. Wired into the existing per-package gate in python.yml, blocking (no || true).
  • scripts/tests/test_verify_version_sync.py covers the matching case, the disagreeing case, the indirect-assignment case, a literal in a nested non-__init__ module, a package with no awslabs/, and a pyproject.toml with no version key. There is currently no job that runs tests for repo tooling — the matrix in python.yml is built from src/*/pyproject.toml, and the pytest hook in .pre-commit-config.yaml is stages: [pre-push] with files: src\/.*\/pyproject.toml, so it never runs in CI — so a small verify-scripts job was added rather than leaving a test nothing executes.
  • stepfunctions-tool's tests/test_server.py carried a commented-out test_version_matches_pyproject() pointing at RFC: Package Bumping #167 and chore(cicd): bump version in all related files #425. RFC: Package Bumping #167 proposed commitizen and was closed by the stale bot without a decision, which is why 53 servers still carry [tool.commitizen] config that nothing invokes. chore(cicd): bump version in all related files #425 asked for exactly this and was closed with no comments. The new check covers all 62 servers, so the dead block is removed.

User experience

Before: awslabs.amazon-translate-mcp-server 1.0.7 identified itself to AWS as md/awslabs#mcp#amazon-translate-mcp-server/1.0.0 on every Translate call. Nothing failed, nothing warned, and the same was true for Cost Explorer and Step Functions traffic. A release could not fix it, because the release is what caused it.

After: the version reported at runtime is the version that was published, and a literal that disagrees with pyproject.toml fails the PR that introduces it.

Checklist

If your change doesn't seem to apply, please leave them unchecked.

  • I have reviewed the contributing guidelines
  • I have performed a self-review of this change
  • Changes have been tested
  • Changes are documented

Is this a breaking change? (N)

RFC issue number:

Checklist:

  • Migration process documented
  • Implement warnings (if it can live side by side)

How this was verified

  • scripts/verify_version_sync.py across all 62 servers: 6 violations before, 0 after.
  • importlib.metadata.version() resolves the real published version for all four rewired distributions in a live venv (0.2.1, 1.0.7, 0.0.37, 2.1.1) — not just correct on paper.
  • release.py exercised against fixtures for all four cases: derived-name mismatch now patches the right file; indirect assignment succeeds without patching; two subpackages raises; a normal package with a literal still gets patched.
  • Every touched server's suite compared against unmodified code. stepfunctions-tool 61 passed / 100% coverage. aws-location 53 passed. amazon-translate and billing-cost-management have pre-existing failures on main (1 and 43 respectively) and the counts are identical before and after. sagemaker-ai's 2 failures are Linux-only path assertions failing on Windows, also identical. valkey could not be run — uv sync fails building valkey-glide v2.3.1 in this environment, unrelated to this change; that server's edit is the deletion of an unreferenced file.
  • pre-commit run over the diff: 21 hooks pass. gitleaks could not be installed here (its Go modules failed to download) and check-license-header, pyright and pytest were skipped — the last two are pre-push-staged and do not run in CI.

Acknowledgment

By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of the project license.

🤖 Generated with Claude Code

pyproject.toml is the only version that reaches PyPI, because python.yml's
`uv build` reads nothing else. Every hardcoded version literal in a package is
therefore a second copy that something has to keep in sync, and the thing meant
to keep it in sync fails open.

PyPiPackage.bump_version() in .github/workflows/release.py always bumps
pyproject.toml, then makes a best-effort attempt to patch a literal in
awslabs/<module>/__init__.py. It derived that directory name from the package
name instead of discovering it, so the one server whose directory name is
irregular (aws-location-mcp-server: aws_location_server, not
aws_location_mcp_server) never matched. Its literal froze at 1.0.0 while
pyproject reached 2.1.1. The regex it uses only matches a quoted literal, so
amazon-translate's `__version__ = MCP_SERVER_VERSION` was missed and the real
constant in consts.py never moved. And it only ever opens that one file, so a
literal anywhere else is invisible. All three paths echoed a warning and
returned success, and the commit step only checks `git diff --cached --quiet`
over src/, which is always dirty because pyproject.toml did change.

Rather than teach the release script to chase every literal, this removes the
duplicate: the version is read from the installed distribution with
importlib.metadata, following the pattern already used by postgres, mssql and
oracle. Nothing left to sync.

Six servers were drifting. Three of them fed the stale value straight into
botocore's user_agent_extra, so every AWS API call reported the wrong version:

  amazon-translate         consts.py 1.0.0 vs 1.0.7   (used in aws_client.py)
  billing-cost-management  aws_service_base.py 1.0.0 vs 0.0.37  (used there)
  stepfunctions-tool       server.py 0.1.5 vs 0.2.1   (used in aws_helper.py)
  aws-location             __init__.py 1.0.0 vs 2.1.1 (unused)
  valkey                   version.py 0.2.0 vs 1.1.1  (unused, removed)
  sagemaker-ai             sagemaker_hyperpod 1.0.0 vs 1.1.0 (unused, removed)

billing-cost-management and stepfunctions-tool each had a correct constant and
a stale shadow, and the consumer had picked the shadow.

release.py now discovers the subpackage directory instead of deriving its name,
raises on a genuinely structural problem instead of warning, and treats the
absence of a literal as correct rather than as a failure. scripts/
verify_version_sync.py walks the AST of every module under awslabs/ and fails
when a version literal disagrees with pyproject.toml, wired into the existing
per-package gate in python.yml. It catches the release script's blind spots
because it does not care which file the literal lives in.

tests/test_server.py carried a commented-out test_version_matches_pyproject()
pointing at awslabs#167 and awslabs#425. awslabs#167 proposed commitizen and was closed by the stale
bot without a decision, which is why 53 servers still carry [tool.commitizen]
config that nothing invokes. awslabs#425 asked for exactly this and was closed with no
comments. The check now covers all 62 servers, so the dead block is removed.

Closes awslabs#4476
Closes awslabs#4477
Closes awslabs#425

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: To triage

1 participant