Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 32 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,8 @@ jobs:
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for signing the image attestation (OIDC)
attestations: write # Required to store the attestation

steps:
- name: Checkout code
Expand Down Expand Up @@ -147,6 +149,7 @@ jobs:
type=raw,value=latest,enable=${{ env.PUBLISH_LATEST == 'true' }}

- name: Build and push Docker image
id: build
uses: docker/build-push-action@53b7df96c91f9c12dcc8a07bcb9ccacbed38856a # v7.3.0
with:
context: .
Expand All @@ -157,12 +160,27 @@ jobs:
cache-from: type=gha
cache-to: type=gha,mode=max

# Attest the pushed image digest so the Docker artifact is verifiable back
# to this workflow and commit, like the npm tarballs already are:
# gh attestation verify oci://index.docker.io/debugmcp/mcp-debugger:<tag> \
# --repo debugmcp/mcp-debugger
# Same composite-action approach as the provenance job below (see the note
# there for why slsa-github-generator's reusable workflows are not usable
# in this org).
- name: Attest Docker image provenance
uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
with:
subject-name: index.docker.io/debugmcp/mcp-debugger
subject-digest: ${{ steps.build.outputs.digest }}
push-to-registry: true

pypi-publish:
name: Publish Python Launcher to PyPI
needs: build-and-test
runs-on: ubuntu-latest
permissions:
contents: read
id-token: write # Required for PyPI trusted publishing (OIDC)

steps:
- name: Checkout code
Expand Down Expand Up @@ -219,14 +237,21 @@ jobs:
rm -rf dist build *.egg-info
python -m build

- name: Publish to PyPI
env:
TWINE_USERNAME: __token__
TWINE_PASSWORD: ${{ secrets.PYPI_TOKEN }}
- name: Check package metadata
run: |
cd mcp_debugger_launcher
python -m twine check dist/*
python -m twine upload --skip-existing dist/*

# PyPI trusted publishing (OIDC) - no long-lived token. The action also
# generates and uploads PEP 740 digital attestations for the dists.
# Requires the trusted publisher configured on pypi.org for
# debug-mcp-server-launcher (repo debugmcp/mcp-debugger, workflow
# release.yml).
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
with:
packages-dir: mcp_debugger_launcher/dist
skip-existing: true

npm-publish:
name: Publish to npm
Expand Down Expand Up @@ -645,8 +670,10 @@ jobs:
### 🔏 Verify this release
```bash
gh attestation verify <asset>.tgz --repo debugmcp/mcp-debugger
gh attestation verify oci://index.docker.io/debugmcp/mcp-debugger:${{ steps.changelog.outputs.VERSION }} --repo debugmcp/mcp-debugger
npm audit signatures # in a project that installs @debugmcp packages
```
The PyPI launcher carries PEP 740 attestations — see the "Verified details" on its [PyPI page](https://pypi.org/project/debug-mcp-server-launcher/${{ steps.changelog.outputs.VERSION }}/).
SBOMs (SPDX + CycloneDX) are attached as release assets.

### 📚 Documentation
Expand Down
55 changes: 4 additions & 51 deletions .github/workflows/validate-secrets.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,54 +44,7 @@ jobs:
exit 1
fi

validate-pypi:
name: "PyPI: token"
runs-on: ubuntu-latest
steps:
- name: Test token
env:
PYPI_TOKEN: ${{ secrets.PYPI_TOKEN }}
run: |
# Use the PyPI JSON API with token auth to check validity
# Requesting our own project metadata — 200 = good auth, 401/403 = bad token
HTTP_CODE=$(curl -s -o /tmp/pypi_response -w "%{http_code}" \
-H "Authorization: token $PYPI_TOKEN" \
"https://pypi.org/pypi/debug-mcp-server-launcher/json" 2>/dev/null) || true

case "$HTTP_CODE" in
200)
echo "✅ PYPI_TOKEN is valid"
;;
401|403)
echo "❌ PYPI_TOKEN is expired or revoked (HTTP $HTTP_CODE)"
echo " Fix: regenerate at pypi.org → Account Settings → API tokens"
exit 1
;;
404)
echo "⚠️ Package not found on PyPI (HTTP 404) — token may still be valid"
echo " (404 can mean the package hasn't been published yet)"
;;
*)
# Fallback: try twine with a real sdist from our project
pip install "twine==6.1.0" "build==1.2.2" > /dev/null 2>&1
mkdir -p /tmp/pypi-test && cd /tmp/pypi-test
echo '[project]' > pyproject.toml
echo 'name = "_validate-token-test"' >> pyproject.toml
echo 'version = "0.0.0"' >> pyproject.toml
python -m build --sdist > /dev/null 2>&1

TWINE_OUT=$(python -m twine upload --skip-existing \
--repository-url https://upload.pypi.org/legacy/ \
-u __token__ -p "$PYPI_TOKEN" \
dist/*.tar.gz 2>&1) || true

if echo "$TWINE_OUT" | grep -qiE "401|403|unauthorized|forbidden"; then
echo "❌ PYPI_TOKEN is expired or revoked"
echo " $TWINE_OUT"
exit 1
else
echo "✅ PYPI_TOKEN appears valid"
echo " (twine output: $TWINE_OUT)"
fi
;;
esac
# PyPI publishing uses trusted publishing (OIDC) — there is no PyPI token
# to validate. The trusted publisher is configured on pypi.org for
# debug-mcp-server-launcher (repo debugmcp/mcp-debugger, workflow
# release.yml).
19 changes: 14 additions & 5 deletions SUPPLY-CHAIN-SECURITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,15 @@ Note: the SBOMs attached to releases are generated from the source tree and enum

#### PyPI (`debug-mcp-server-launcher`)

- Currently uses token-based authentication via repository secret.
- Migration to OIDC trusted publishing (via `pypa/gh-action-pypi-publish`) is planned to eliminate the long-lived PyPI token.
- **OIDC trusted publishing** via `pypa/gh-action-pypi-publish`: CI exchanges its GitHub OIDC identity for a short-lived credential at publish time; no long-lived PyPI token is involved.
- **PEP 740 attestations**: the publish action generates and uploads digital attestations for every distribution, shown under "Verified details" on the PyPI project page.

#### Docker Hub (`debugmcp/mcp-debugger`)

- Multi-platform images (linux/amd64, linux/arm64) built in CI from digest-pinned base images.
- Published only after all tests pass.
- Credential-based authentication via repository secrets.
- Credential-based authentication via repository secrets (Docker Hub does not support OIDC publishing).
- **Build-provenance attestation**: the pushed image digest is attested with `actions/attest-build-provenance` and the attestation is pushed to the registry, so the image is verifiable back to the tagged commit and workflow (see [Verifying a Release](#verifying-a-release)).

### SBOMs

Expand All @@ -86,14 +87,22 @@ All tools installed during CI are pinned to specific versions to ensure reproduc

## Verifying a Release

What is attested: **the npm package tarballs** (the `.tgz` files published to npm and attached to the GitHub release). The Docker image and the PyPI launcher are built by the same tag-triggered workflow but do not yet carry their own attestations.
What is attested: **every distributed artifact** — the npm package tarballs (the `.tgz` files published to npm and attached to the GitHub release), the Docker image, and the PyPI launcher. All are built by the same tag-triggered workflow. (Applies to releases after v0.24.2; earlier releases attest only the npm tarballs.)

**Verify a release asset's build provenance** (proves the tarball was built by this repository's release workflow from the tagged commit):

```bash
gh attestation verify debugmcp-mcp-debugger-<version>.tgz --repo debugmcp/mcp-debugger
```

**Verify the Docker image** (proves the pushed image digest was built and attested by the same release workflow):

```bash
gh attestation verify oci://index.docker.io/debugmcp/mcp-debugger:<version> --repo debugmcp/mcp-debugger
```

**Verify the PyPI launcher**: distributions carry [PEP 740](https://peps.python.org/pep-0740/) attestations, generated at upload by trusted publishing — see "Verified details" on the [PyPI project page](https://pypi.org/project/debug-mcp-server-launcher/), or verify locally with `pypi-attestations verify pypi ...`.

**Verify installed npm packages** (checks registry signatures and provenance attestations for everything in your lockfile):

```bash
Expand Down Expand Up @@ -133,7 +142,7 @@ The project currently has a single lead maintainer (see [MAINTAINERS.md](MAINTAI
| npm (`@debugmcp` scope) | Trusted publishing bound to this GitHub repository's release workflow (not to individual accounts); scoped token only for first-publishes |
| Docker Hub (`debugmcp/mcp-debugger`) | Project maintainers via repository secrets |
| GitHub (admin) | Organization owners of `debugmcp` |
| PyPI (`debug-mcp-server-launcher`) | Project maintainers via repository secret (OIDC planned) |
| PyPI (`debug-mcp-server-launcher`) | Trusted publishing bound to this GitHub repository's release workflow (not to individual accounts) |

If the primary maintainer becomes unavailable, organization-level access on GitHub ensures continuity. npm trusted publishing is tied to the GitHub repository, not individual accounts.

Expand Down
2 changes: 1 addition & 1 deletion docs/release-checklist.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ Pre-release validation for mcp-debugger. Run `npm run release:dry-run` to automa
- [ ] **npm trusted publishing configured** — every *previously published* `@debugmcp/*` package must have a trusted publisher at npmjs.com → package Settings → Trusted Publisher (GitHub Actions; org/user: `debugmcp`, repo: `mcp-debugger`, workflow: `release.yml`, environment: blank). These packages publish token-free via OIDC; a publish without this config fails (404/permission error) — configure, then re-run via workflow_dispatch.
- [ ] **First-time packages** — any package that has never been on npm publishes via the `NPM_TOKEN` step in `release.yml` this once. After the release: configure its trusted publisher, then move it from the token step into the OIDC step. When no first-publishes remain, delete the token step and the `NPM_TOKEN` secret. (The five `@debugmcp/codelldb-<platform>` packages have their own token step and follow the same dance after their first release.)
- [ ] **Docker Hub credentials** — `DOCKER_USERNAME` and `DOCKER_PASSWORD` secrets are current
- [ ] **PyPI token** — `PYPI_TOKEN` secret is current
- [ ] **PyPI trusted publishing configured** — `debug-mcp-server-launcher` has a trusted publisher at pypi.org → project Settings → Publishing (GitHub Actions; owner: `debugmcp`, repo: `mcp-debugger`, workflow: `release.yml`, environment: blank). Publishing is token-free via OIDC with PEP 740 attestations; no `PYPI_TOKEN` secret is needed.
- [ ] `release.yml` default ref updated to current tag (for workflow_dispatch reruns)
- [ ] All new adapters have their toolchain in `release.yml` **both** `build-and-test` and `npm-publish` jobs
- [ ] New adapters intended for npm publishing have `publishConfig.access: "public"`, a `git+https` `repository.url` with `directory`, and appear in: `release.yml` (pack dry-run, publish, pack-artifacts) and `PUBLISHED_PKGS` in `scripts/release-dry-run.sh`. Bundle-only packages carry `"private": true`.
Expand Down
5 changes: 3 additions & 2 deletions scripts/release-dry-run.sh
Original file line number Diff line number Diff line change
Expand Up @@ -174,9 +174,10 @@ echo ""
echo "── Publishing credentials ──"

if command -v gh > /dev/null 2>&1; then
# Check secrets exist (NPM_TOKEN = granular access token for npm publish)
# Check secrets exist (NPM_TOKEN = granular access token for npm publish;
# PyPI needs no secret - trusted publishing via OIDC)
SECRETS_LIST=$(gh secret list 2>/dev/null || echo "")
for secret in NPM_TOKEN DOCKER_USERNAME DOCKER_PASSWORD PYPI_TOKEN; do
for secret in NPM_TOKEN DOCKER_USERNAME DOCKER_PASSWORD; do
if echo "$SECRETS_LIST" | grep -q "^${secret}"; then
pass "GitHub secret $secret exists"
else
Expand Down
Loading