release: 0.6.12 (#286) #16
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Publish | |
| on: | |
| push: | |
| tags: ["v*.*.*"] | |
| # OIDC token exchange with PyPI Trusted Publishing. ``id-token: write`` | |
| # is the privilege the action trades for a one-time PyPI upload | |
| # credential; ``contents: read`` is the source checkout. | |
| permissions: | |
| id-token: write | |
| contents: read | |
| jobs: | |
| build: | |
| name: Build sdist + wheel | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: astral-sh/setup-uv@v7 | |
| with: | |
| python-version: "3.14" | |
| enable-cache: true | |
| - run: uv sync --locked | |
| - name: Sanity check — tag matches package version | |
| # Tag is the source of truth for the release; the version is | |
| # single-sourced in src/mcpg/__init__.py (__version__) and read | |
| # dynamically by hatchling. Validate the tag against that one | |
| # source (the freshly-synced package) to catch the very common | |
| # "tagged v0.5.1 but forgot to bump" footgun. | |
| run: | | |
| TAG_VERSION="${GITHUB_REF_NAME#v}" | |
| PKG_VERSION="$(uv run python -c 'import mcpg; print(mcpg.__version__)')" | |
| if [ -z "$PKG_VERSION" ]; then | |
| echo "::error::Could not read mcpg.__version__" | |
| exit 1 | |
| fi | |
| if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then | |
| echo "::error::Tag $GITHUB_REF_NAME (=$TAG_VERSION) does not match mcpg.__version__ $PKG_VERSION" | |
| exit 1 | |
| fi | |
| - name: Build | |
| run: uv run python -m build | |
| - name: Inspect rendered metadata | |
| run: uv run twine check dist/* | |
| - uses: actions/upload-artifact@v7 | |
| with: | |
| name: dist | |
| path: dist/ | |
| publish-testpypi: | |
| name: Publish → TestPyPI | |
| needs: build | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: testpypi | |
| url: https://test.pypi.org/project/mcpg/ | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - uses: pypa/gh-action-pypi-publish@release/v1 | |
| with: | |
| repository-url: https://test.pypi.org/legacy/ | |
| smoke-testpypi: | |
| name: Smoke-test TestPyPI install | |
| needs: publish-testpypi | |
| runs-on: ubuntu-latest | |
| steps: | |
| # ``uv export`` needs the source tree; the smoke install | |
| # itself runs against the published artifact. | |
| - uses: actions/checkout@v7 | |
| - uses: astral-sh/setup-uv@v7 | |
| with: | |
| python-version: "3.14" | |
| enable-cache: true | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.14" | |
| - name: Install runtime deps from real PyPI (anti-confusion) | |
| # TestPyPI is a public sandbox; combining its index with | |
| # ``--extra-index-url=pypi`` lets an attacker shadow our | |
| # deps with a higher-numbered fake (dependency confusion). | |
| # Step 1 pins real PyPI only, dep list canonically derived | |
| # from pyproject.toml. | |
| run: | | |
| uv export --no-dev --no-emit-project --no-hashes \ | |
| --format requirements-txt > /tmp/mcpg-deps.txt | |
| python -m pip install -r /tmp/mcpg-deps.txt | |
| - name: Wait for TestPyPI to index the upload | |
| # Trusted Publishing returns 200 the moment the file lands, but the | |
| # PEP 503 *simple index* pip actually installs from is a SEPARATE, | |
| # independently-consistent system from the JSON API. Polling the | |
| # JSON API (as this step used to) can report "ready" before the | |
| # simple index has caught up — observed on the v0.6.6 release: the | |
| # JSON check passed on its very first attempt, then the immediately | |
| # following pip install 404'd because 0.6.6 wasn't in the simple | |
| # index yet. Poll the simple index itself instead. | |
| run: | | |
| VER="${GITHUB_REF_NAME#v}" | |
| for i in $(seq 1 18); do | |
| if curl -fsSL "https://test.pypi.org/simple/mcpg/" 2>/dev/null | grep -q "mcpg-${VER}"; then | |
| echo "TestPyPI simple index sees mcpg==${VER}" | |
| exit 0 | |
| fi | |
| echo "TestPyPI simple index not ready (attempt $i/18)..." | |
| sleep 10 | |
| done | |
| echo "::error::TestPyPI simple index never listed mcpg==${VER}" | |
| exit 1 | |
| - name: Install mcpg from TestPyPI (no deps) | |
| # A short retry on the install itself too: even after the simple | |
| # index origin has the version, a CDN edge cache in front of it can | |
| # lag a few more seconds. | |
| run: | | |
| VER="${GITHUB_REF_NAME#v}" | |
| for i in 1 2 3; do | |
| if python -m pip install --no-deps \ | |
| --index-url https://test.pypi.org/simple/ \ | |
| "mcpg==${VER}"; then | |
| exit 0 | |
| fi | |
| echo "pip install attempt $i/3 failed; retrying in 10s..." | |
| sleep 10 | |
| done | |
| echo "::error::pip install mcpg==${VER} from TestPyPI failed after 3 attempts" | |
| exit 1 | |
| - name: Import + CLI smoke | |
| run: | | |
| python -c "import mcpg; print(mcpg.__version__)" | |
| mcpg --version | |
| publish-pypi: | |
| name: Publish → PyPI (gated on maintainer approval) | |
| needs: smoke-testpypi | |
| runs-on: ubuntu-latest | |
| # The ``pypi`` environment is configured with a required | |
| # reviewer (the maintainer). The workflow pauses on this job | |
| # until that approval click lands; that's the last chance to | |
| # cancel a bad release after TestPyPI smoke passed. | |
| environment: | |
| name: pypi | |
| url: https://pypi.org/project/mcpg/ | |
| steps: | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - uses: pypa/gh-action-pypi-publish@release/v1 | |
| github-release: | |
| name: Create GitHub Release | |
| needs: publish-pypi | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/download-artifact@v8 | |
| with: | |
| name: dist | |
| path: dist/ | |
| - name: Build the Claude Desktop extension (.mcpb) | |
| # One-click-install bundle for Claude Desktop. server.type "uv" | |
| # keeps it tiny (~2 kB): the host resolves the mcpg==VER pin | |
| # from PyPI at install time, so one bundle covers every | |
| # platform/arch. Versions are synced from the release tag the | |
| # same way server.json is — the checked-in files carry the | |
| # previous release's pin and are patched here, so the bundle | |
| # can never ship stale. | |
| run: | | |
| VER="${GITHUB_REF_NAME#v}" | |
| python3 packaging/mcpb/sync_version.py "$VER" | |
| npx --yes @anthropic-ai/mcpb validate packaging/mcpb/manifest.json | |
| npx --yes @anthropic-ai/mcpb pack packaging/mcpb "dist/mcpg-${VER}.mcpb" | |
| - name: Cut release | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VER="${GITHUB_REF_NAME#v}" | |
| # Pull the matching section out of CHANGELOG.md as the | |
| # release body. Falls back to a stub if the section is | |
| # missing so the release still ships — the maintainer can | |
| # edit the body afterwards on the GitHub side. | |
| # | |
| # NOT an awk range pattern: GNU awk treats a line matching | |
| # both endpoints as a single-line range, and "## [X.Y.Z]" | |
| # matches both /^## \[X.Y.Z\]/ and /^## \[/ — so the old | |
| # range-based extraction returned only the header line and | |
| # every release body silently fell back to the stub | |
| # (observed on v0.6.7 and all earlier releases). A flag | |
| # variable sidesteps that; index() avoids regex-escaping | |
| # the dots in the version. | |
| awk -v ver="$VER" ' | |
| index($0, "## [" ver "]") == 1 { in_section = 1; next } | |
| in_section && /^## \[/ { exit } | |
| in_section | |
| ' CHANGELOG.md > /tmp/notes.md | |
| [ -s /tmp/notes.md ] || echo "See CHANGELOG.md" > /tmp/notes.md | |
| gh release create "$GITHUB_REF_NAME" dist/* \ | |
| --title "MCPg ${GITHUB_REF_NAME}" \ | |
| --notes-file /tmp/notes.md | |
| publish-mcp-registry: | |
| name: Publish to MCP Registry | |
| needs: publish-pypi | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v7 | |
| - name: Sync server.json version to the release tag | |
| # server.json is checked in with a fixed version (bumped manually at | |
| # the 0.6.1 registry launch and never since — every subsequent | |
| # release silently re-submitted "0.6.1" and the registry rejected it | |
| # as a duplicate). Deriving the version from the tag here means this | |
| # step can never go stale again, the same way the sdist/wheel build | |
| # derives its version from pyproject.toml rather than a hand-maintained | |
| # constant. | |
| run: | | |
| VER="${GITHUB_REF_NAME#v}" | |
| python3 -c " | |
| import json | |
| path = 'server.json' | |
| with open(path) as f: | |
| data = json.load(f) | |
| data['version'] = '$VER' | |
| for pkg in data.get('packages', []): | |
| pkg['version'] = '$VER' | |
| with open(path, 'w') as f: | |
| json.dump(data, f, indent=2) | |
| f.write('\n') | |
| " | |
| cat server.json | |
| - name: Install mcp-publisher | |
| run: | | |
| curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher | |
| sudo mv mcp-publisher /usr/local/bin/ | |
| - name: Authenticate to MCP Registry | |
| run: mcp-publisher login github-oidc | |
| - name: Publish server to MCP Registry | |
| run: mcp-publisher publish | |
| publish-ghcr: | |
| name: Publish container → GHCR | |
| # Runs only after the human-approved PyPI publish succeeds, so the | |
| # container release tracks the approved PyPI release exactly — if the | |
| # maintainer cancels at the PyPI approval gate, no image is pushed. | |
| needs: publish-pypi | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - name: Log in to GHCR | |
| uses: docker/login-action@v4 | |
| with: | |
| registry: ghcr.io | |
| username: ${{ github.actor }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: Compute image tags | |
| id: tags | |
| # ``:<version>`` (e.g. 0.6.5) plus ``:latest``. The image name is | |
| # lowercased because GHCR rejects uppercase path components. | |
| run: | | |
| VER="${GITHUB_REF_NAME#v}" | |
| IMAGE="ghcr.io/devopam/mcpg" | |
| echo "tags=${IMAGE}:${VER},${IMAGE}:latest" >> "$GITHUB_OUTPUT" | |
| - name: Set up Buildx | |
| uses: docker/setup-buildx-action@v4 | |
| - name: Build and push | |
| uses: docker/build-push-action@v7 | |
| with: | |
| context: . | |
| push: true | |
| tags: ${{ steps.tags.outputs.tags }} | |
| # OCI labels so the package page links back to the repo + release. | |
| labels: | | |
| org.opencontainers.image.source=https://github.com/devopam/MCPg | |
| org.opencontainers.image.version=${{ github.ref_name }} | |
| org.opencontainers.image.licenses=MIT | |
| org.opencontainers.image.description=Production-grade PostgreSQL MCP server | |
| # Skip the buildkit provenance attestation so the package page | |
| # shows a single clean manifest rather than an unknown/unknown pair. | |
| provenance: false | |
| publish-smithery: | |
| name: Publish → Smithery | |
| # Opt-in: inert until the maintainer adds a SMITHERY_API_KEY repo | |
| # secret. Smithery lists MCPg as a local stdio server that launches | |
| # via `uvx mcpg`, so the listing already tracks the latest PyPI | |
| # release regardless — re-publishing here keeps the listing's | |
| # declared version/metadata current with each release. Non-gating. | |
| needs: publish-pypi | |
| if: ${{ vars.PUBLISH_SMITHERY == 'true' }} | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/checkout@v7 | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.14" | |
| - name: Build the Smithery bundle | |
| # Smithery's bundle parser doesn't understand the canonical | |
| # bundle's `uv` server type, so we derive a python/uvx variant | |
| # from the same source of truth (packaging/mcpb) — see | |
| # packaging/smithery/build.py. Sync the version first, exactly | |
| # like the .mcpb and server.json steps. | |
| run: | | |
| VER="${GITHUB_REF_NAME#v}" | |
| python packaging/mcpb/sync_version.py "$VER" | |
| python packaging/smithery/build.py smithery-bundle | |
| npx --yes @anthropic-ai/mcpb validate smithery-bundle/manifest.json | |
| npx --yes @anthropic-ai/mcpb pack smithery-bundle "mcpg-${VER}-smithery.mcpb" | |
| - name: Publish to Smithery | |
| env: | |
| SMITHERY_API_KEY: ${{ secrets.SMITHERY_API_KEY }} | |
| run: | | |
| VER="${GITHUB_REF_NAME#v}" | |
| npx --yes @smithery/cli@latest namespace use devopam | |
| npx --yes @smithery/cli@latest mcp publish "mcpg-${VER}-smithery.mcpb" -n devopam/mcpg | |
| publish-hf-space: | |
| name: Refresh HF demo Space | |
| # Opt-in: inert until the maintainer adds an HF_TOKEN repo secret | |
| # (a write token for the devopam/mcpg-demo Space) and sets the | |
| # REFRESH_HF_SPACE variable. Runs after the GHCR image is pushed so | |
| # the Space's factory rebuild pulls the new :latest. HF Docker Spaces | |
| # resolve `FROM …:latest` at build time and cache it, so a new GHCR | |
| # push does not auto-repull — this triggers the rebuild. Non-gating. | |
| needs: publish-ghcr | |
| if: ${{ vars.REFRESH_HF_SPACE == 'true' }} | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| steps: | |
| - uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.14" | |
| - run: pip install huggingface_hub | |
| - name: Factory-rebuild the Space (pull new :latest) | |
| env: | |
| HF_TOKEN: ${{ secrets.HF_TOKEN }} | |
| run: | | |
| python - <<'PY' | |
| import os | |
| from huggingface_hub import HfApi | |
| HfApi(token=os.environ["HF_TOKEN"]).restart_space( | |
| "devopam/mcpg-demo", factory_reboot=True | |
| ) | |
| print("HF demo Space factory reboot triggered") | |
| PY |