name: Release
on:
push:
tags:
- 'v*.*.*'
workflow_dispatch:
inputs:
ref:
description: 'Git ref (tag or branch) to release from'
required: true
default: 'refs/tags/v0.15.3'
jobs:
build-and-test:
name: Build and Test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
cache: 'pnpm'
- name: Setup Python 3.11
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Python dependencies
run: |
python -m pip install --upgrade pip
pip install debugpy
- name: Sanity versions
run: |
node -v
npm -v
pnpm -v
- name: Approve build scripts (esbuild)
run: pnpm approve-builds esbuild
continue-on-error: true
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Build project
run: pnpm run build
- name: Run tests
run: pnpm run test:ci-no-python
docker-publish:
name: Build and Push Docker Image
needs: build-and-test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Determine Docker latest tag strategy
shell: bash
env:
GITHUB_REF_TYPE: ${{ github.ref_type }}
GITHUB_REF_NAME: ${{ github.ref_name }}
DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
run: |
if [[ "${GITHUB_REF_TYPE}" == "branch" && "${GITHUB_REF_NAME}" == "${DEFAULT_BRANCH}" ]]; then
echo "PUBLISH_LATEST=true" >> "$GITHUB_ENV"
echo "latest will track default branch build (${GITHUB_REF_NAME})"
elif [[ "${GITHUB_REF_TYPE}" == "tag" && "${GITHUB_REF_NAME}" != *"-alpha"* && "${GITHUB_REF_NAME}" != *"-beta"* && "${GITHUB_REF_NAME}" != *"-rc"* ]]; then
echo "PUBLISH_LATEST=true" >> "$GITHUB_ENV"
echo "latest will point to release ${GITHUB_REF_NAME}"
else
echo "PUBLISH_LATEST=false" >> "$GITHUB_ENV"
echo "latest tag update skipped for ref ${GITHUB_REF_NAME}"
fi
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: debugmcp/mcp-debugger
tags: |
type=ref,event=tag
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=raw,value=latest,enable=${{ env.PUBLISH_LATEST == 'true' }}
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64,linux/arm64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
pypi-publish:
name: Publish Python Launcher to PyPI
needs: build-and-test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install Python build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine tomlkit
- name: Resolve release ref
run: |
if [ -n "${{ github.event.inputs.ref }}" ]; then
echo "RELEASE_REF=${{ github.event.inputs.ref }}" >> $GITHUB_ENV
else
echo "RELEASE_REF=${GITHUB_REF}" >> $GITHUB_ENV
fi
- name: Set launcher version from tag
run: |
VERSION="${RELEASE_REF#refs/tags/v}"
echo "VERSION=$VERSION" >> $GITHUB_ENV
- name: Sync version into pyproject.toml
run: |
python - <<'PY'
import os
from tomlkit import parse, dumps
p = os.path.join('mcp_debugger_launcher','pyproject.toml')
with open(p,'r',encoding='utf-8') as f:
doc = parse(f.read())
ver = os.environ.get('VERSION','0.0.0')
if 'project' in doc and 'version' in doc['project']:
doc['project']['version'] = ver
elif 'tool' in doc and 'poetry' in doc['tool'] and 'version' in doc['tool']['poetry']:
doc['tool']['poetry']['version'] = ver
else:
doc.setdefault('project', {})['version'] = ver
with open(p,'w',encoding='utf-8') as f:
f.write(dumps(doc))
print(f"Set pyproject version to {ver}")
PY
- name: Build Python package
run: |
cd mcp_debugger_launcher
rm -rf dist build *.egg-info
python -m build
- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
run: |
cd mcp_debugger_launcher
python -m twine check dist/*
python -m twine upload --skip-existing dist/*
npm-publish:
name: Publish to npm
needs: build-and-test
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Setup pnpm
uses: pnpm/action-setup@v2
with:
version: 10
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '22.x'
registry-url: 'https://registry.npmjs.org'
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scripts
- name: Sanity versions
run: |
node -v
npm -v
pnpm -v
- name: Resolve release ref
run: |
if [ -n "${{ github.event.inputs.ref }}" ]; then
echo "RELEASE_REF=${{ github.event.inputs.ref }}" >> $GITHUB_ENV
else
echo "RELEASE_REF=${GITHUB_REF}" >> $GITHUB_ENV
fi
- name: Set CLI package version from tag (monorepo-safe)
run: |
VERSION="${RELEASE_REF#refs/tags/v}"
echo "Setting CLI package version to $VERSION"
VERSION_STRIPPED="$VERSION" node -e "const fs=require('fs');const p='packages/mcp-debugger/package.json';const pkg=JSON.parse(fs.readFileSync(p,'utf8'));const ver=process.env.VERSION_STRIPPED; if(!/^[0-9]+\\.[0-9]+\\.[0-9]+(-.+)?$/.test(ver)){console.error('Invalid semver:',ver);process.exit(1);} pkg.version=ver; fs.writeFileSync(p,JSON.stringify(pkg,null,2)+'\\n');console.log('Updated',p,'to',pkg.version)"
- name: Capture workspace package versions (robust)
run: |
node -e 'console.log("SHARED_VERSION="+require("./packages/shared/package.json").version)' >> $GITHUB_ENV
node -e 'console.log("ADAPTER_MOCK_VERSION="+require("./packages/adapter-mock/package.json").version)' >> $GITHUB_ENV
node -e 'console.log("ADAPTER_PYTHON_VERSION="+require("./packages/adapter-python/package.json").version)' >> $GITHUB_ENV
node -e 'console.log("CLI_VERSION="+require("./packages/mcp-debugger/package.json").version)' >> $GITHUB_ENV
- name: Set npm dist-tag
run: |
if [[ "${CLI_VERSION}" == *"-beta"* ]] || [[ "${CLI_VERSION}" == *"-alpha"* ]]; then
echo "NPM_TAG=beta" >> $GITHUB_ENV
else
echo "NPM_TAG=latest" >> $GITHUB_ENV
fi
- name: Build project
run: pnpm run build
- name: Pack npm tarballs (dry-run)
run: |
npm pack --dry-run -w @debugmcp/shared
npm pack --dry-run -w @debugmcp/adapter-mock
npm pack --dry-run -w @debugmcp/adapter-python
npm pack --dry-run -w @debugmcp/mcp-debugger
- name: Publish to npm (workspaces)
run: |
# Authenticate npm
npm config set //registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}
# Publish packages in dependency order if the target version does NOT yet exist
if npm view @debugmcp/shared@${SHARED_VERSION} version >/dev/null 2>&1; then
echo "@debugmcp/shared@${SHARED_VERSION} already exists, skipping"
else
npm publish -w @debugmcp/shared --access public
fi
if npm view @debugmcp/adapter-mock@${ADAPTER_MOCK_VERSION} version >/dev/null 2>&1; then
echo "@debugmcp/adapter-mock@${ADAPTER_MOCK_VERSION} already exists, skipping"
else
npm publish -w @debugmcp/adapter-mock --access public
fi
if npm view @debugmcp/adapter-python@${ADAPTER_PYTHON_VERSION} version >/dev/null 2>&1; then
echo "@debugmcp/adapter-python@${ADAPTER_PYTHON_VERSION} already exists, skipping"
else
npm publish -w @debugmcp/adapter-python --access public
fi
if npm view @debugmcp/mcp-debugger@${CLI_VERSION} version >/dev/null 2>&1; then
echo "@debugmcp/mcp-debugger@${CLI_VERSION} already exists, skipping"
else
npm publish -w @debugmcp/mcp-debugger --access public --tag ${NPM_TAG}
fi
env:
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
create-release:
name: Create GitHub Release
needs: [docker-publish, pypi-publish, npm-publish]
runs-on: ubuntu-latest
permissions:
contents: write # Grant permission to create releases
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
ref: ${{ github.event.inputs.ref || github.ref }}
- name: Resolve release ref
run: |
if [ -n "${{ github.event.inputs.ref }}" ]; then
echo "RELEASE_REF=${{ github.event.inputs.ref }}" >> $GITHUB_ENV
else
echo "RELEASE_REF=${GITHUB_REF}" >> $GITHUB_ENV
fi
- name: Generate changelog
id: changelog
run: |
# Extract version from tag
VERSION=${RELEASE_REF#refs/tags/}
echo "VERSION=$VERSION" >> $GITHUB_OUTPUT
# Get changelog for this version
CHANGELOG=$(sed -n "/^## \[$VERSION\]/,/^## \[/p" CHANGELOG.md | sed '$ d')
echo "CHANGELOG<<EOF" >> $GITHUB_OUTPUT
echo "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Create Release with GitHub CLI
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
# Create the release body in a file
cat << 'EOF' > release_notes.md
## 🎉 Release ${{ steps.changelog.outputs.VERSION }}
${{ steps.changelog.outputs.CHANGELOG }}
### 📦 Installation
**Docker:**
```bash
docker pull debugmcp/mcp-debugger:${{ steps.changelog.outputs.VERSION }}
```
**npm (global install):**
```bash
npm install -g @debugmcp/mcp-debugger@${{ steps.changelog.outputs.VERSION }}
```
**npx (no install):**
```bash
npx @debugmcp/mcp-debugger@${{ steps.changelog.outputs.VERSION }} stdio
```
**PyPI:**
```bash
pip install debug-mcp-server-launcher==${{ steps.changelog.outputs.VERSION }}
```
**Optional adapters:**
```bash
npm install -g @debugmcp/adapter-python
npm install -g @debugmcp/adapter-mock
```
### 📚 Documentation
See the [README](https://github.com/debugmcp/mcp-debugger#readme) for usage instructions.
EOF
# Determine if this is a prerelease
if [[ "${{ github.ref_name }}" == *"-beta"* ]] || [[ "${{ github.ref_name }}" == *"-alpha"* ]]; then
PRERELEASE_FLAG="--prerelease"
else
PRERELEASE_FLAG=""
fi
# Create the release using GitHub CLI
gh release create "${{ github.ref_name }}" \
--title "Release ${{ steps.changelog.outputs.VERSION }}" \
--notes-file release_notes.md \
$PRERELEASE_FLAG