Skip to content

release: 0.6.12 (#286) #16

release: 0.6.12 (#286)

release: 0.6.12 (#286) #16

Workflow file for this run

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