diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 2937a926..6ef50775 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 @@ -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: . @@ -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: \ + # --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 @@ -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 @@ -645,8 +670,10 @@ jobs: ### 🔏 Verify this release ```bash gh attestation verify .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 diff --git a/.github/workflows/validate-secrets.yml b/.github/workflows/validate-secrets.yml index be10888c..378dea7a 100644 --- a/.github/workflows/validate-secrets.yml +++ b/.github/workflows/validate-secrets.yml @@ -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). diff --git a/SUPPLY-CHAIN-SECURITY.md b/SUPPLY-CHAIN-SECURITY.md index 6916e5d2..159884d0 100644 --- a/SUPPLY-CHAIN-SECURITY.md +++ b/SUPPLY-CHAIN-SECURITY.md @@ -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 @@ -86,7 +87,7 @@ 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): @@ -94,6 +95,14 @@ What is attested: **the npm package tarballs** (the `.tgz` files published to np gh attestation verify debugmcp-mcp-debugger-.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: --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 @@ -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. diff --git a/docs/release-checklist.md b/docs/release-checklist.md index 83a66dfd..ac6d373d 100644 --- a/docs/release-checklist.md +++ b/docs/release-checklist.md @@ -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-` 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`. diff --git a/scripts/release-dry-run.sh b/scripts/release-dry-run.sh index 2e980309..baf9f3b6 100644 --- a/scripts/release-dry-run.sh +++ b/scripts/release-dry-run.sh @@ -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