From f507377a01f2e716b133c85156088f3dee1e1140 Mon Sep 17 00:00:00 2001 From: Mathew Goldsborough <1759329+mgoldsborough@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:44:54 -1000 Subject: [PATCH 1/4] Add automated release pipeline for scanner - Consolidate version to single source of truth (pyproject.toml) - __init__.py and scanner.py now derive version via importlib.metadata - Dockerfile uses SCANNER_VERSION build arg - Add scanner-publish.yml: tag-triggered pipeline - Verify (ruff, ty, pytest) - Publish to PyPI via trusted publishing (OIDC) - Build and push Docker image to ghcr.io/nimblebraininc/mpak-scanner - Document release process in README and CLAUDE.md - Add mpak.dev badge to README --- .github/workflows/scanner-publish.yml | 93 +++++++++++++++++++++++ apps/scanner/CLAUDE.md | 35 +++++---- apps/scanner/Dockerfile | 3 +- apps/scanner/README.md | 55 ++++++++++++++ apps/scanner/src/mpak_scanner/__init__.py | 8 +- apps/scanner/src/mpak_scanner/scanner.py | 9 ++- 6 files changed, 183 insertions(+), 20 deletions(-) create mode 100644 .github/workflows/scanner-publish.yml diff --git a/.github/workflows/scanner-publish.yml b/.github/workflows/scanner-publish.yml new file mode 100644 index 0000000..666e92b --- /dev/null +++ b/.github/workflows/scanner-publish.yml @@ -0,0 +1,93 @@ +name: Publish Scanner + +on: + push: + tags: + - 'scanner-v*' + +env: + REGISTRY: ghcr.io + IMAGE_NAME: nimblebraininc/mpak-scanner + +jobs: + verify: + runs-on: ubuntu-latest + defaults: + run: + working-directory: apps/scanner + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - uses: astral-sh/setup-uv@v4 + - run: uv sync --extra dev + - run: uv run ruff check src/ tests/ + - run: uv run ruff format --check src/ tests/ + - run: uv run ty check src/ + - run: uv run pytest -m "not e2e" + + publish-pypi: + needs: verify + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + defaults: + run: + working-directory: apps/scanner + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + - uses: astral-sh/setup-uv@v4 + + - name: Verify tag matches package version + run: | + TAG_VERSION="${GITHUB_REF#refs/tags/scanner-v}" + PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])") + if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then + echo "::error::Tag version ($TAG_VERSION) != pyproject.toml version ($PKG_VERSION)" + exit 1 + fi + + - name: Build + run: uv build + + - name: Publish to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: apps/scanner/dist/ + + publish-docker: + needs: [verify, publish-pypi] + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + steps: + - uses: actions/checkout@v4 + + - name: Extract version from tag + id: version + run: echo "version=${GITHUB_REF#refs/tags/scanner-v}" >> "$GITHUB_OUTPUT" + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: apps/scanner + file: apps/scanner/Dockerfile + push: true + build-args: | + SCANNER_VERSION=${{ steps.version.outputs.version }} + tags: | + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }} + ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest diff --git a/apps/scanner/CLAUDE.md b/apps/scanner/CLAUDE.md index fa7cc56..e4cfd4c 100644 --- a/apps/scanner/CLAUDE.md +++ b/apps/scanner/CLAUDE.md @@ -159,35 +159,38 @@ uv run pytest # all tests (unit + e2e) ## Releasing a New Version -The scanner is distributed via PyPI. The Docker image installs from PyPI, not local source. +Releases are automated via GitHub Actions and PyPI trusted publishing. Pushing a tag triggers: verify → publish to PyPI → build + push Docker image to GHCR. + +**Version is in one place:** `pyproject.toml`. Runtime version (`__version__`, `SCANNER_VERSION`) is derived via `importlib.metadata`. ### Steps -1. **Bump version** in four files (must all match): - - `pyproject.toml` (`version = "X.Y.Z"`) - - `src/mpak_scanner/__init__.py` (`__version__ = "X.Y.Z"`) - - `src/mpak_scanner/scanner.py` (`SCANNER_VERSION = "X.Y.Z"`) - - `Dockerfile` (`mpak-scanner[job]==X.Y.Z`) +1. **Bump version** in `pyproject.toml` (the only place) 2. **Run verification**: ```bash uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/ && uv run ty check src/ && uv run pytest ``` -3. **Commit and push** in `apps/mpak` - -4. **Publish to PyPI** (from `apps/mpak/apps/scanner/`): +3. **Commit, tag, and push:** ```bash - uv build && uv publish + git commit -am "scanner: bump to X.Y.Z" + git tag scanner-vX.Y.Z + git push origin main --tags ``` -5. **Build + push Docker image** (from `hq/deployments/mpak/`): - ```bash - make deploy-scanner ENV=production - make apply-scanner-infra ENV=production # only if RBAC/secrets changed - ``` +CI handles PyPI publish and Docker build/push to `ghcr.io/nimblebraininc/mpak-scanner`. See `.github/workflows/scanner-publish.yml`. + +### Production Deployment (ECR/K8s) + +After the PyPI release, deploy the scanner to production K8s (from `hq/deployments/mpak/`): + +```bash +make deploy-scanner ENV=production +make apply-scanner-infra ENV=production # only if RBAC/secrets changed +``` -The Makefile pushes both the git commit tag and `latest`. The mpak-api references `latest`, so deploying automatically updates what production uses. +The Makefile builds from the Dockerfile which pulls the version from PyPI. ### Schemas and Rules diff --git a/apps/scanner/Dockerfile b/apps/scanner/Dockerfile index 5199eec..6139a99 100644 --- a/apps/scanner/Dockerfile +++ b/apps/scanner/Dockerfile @@ -1,3 +1,4 @@ +ARG SCANNER_VERSION=0.2.4 FROM python:3.13-slim # System deps for external security tools @@ -20,7 +21,7 @@ RUN curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main RUN npm install -g eslint eslint-plugin-security --no-fund --no-audit # mpak-scanner + Python security tools (bandit, guarddog) -RUN pip install --no-cache-dir "mpak-scanner[job]==0.2.4" bandit guarddog +RUN pip install --no-cache-dir "mpak-scanner[job]==${SCANNER_VERSION}" bandit guarddog ENTRYPOINT ["mpak-scanner"] CMD ["job"] diff --git a/apps/scanner/README.md b/apps/scanner/README.md index 7940264..31e3563 100644 --- a/apps/scanner/README.md +++ b/apps/scanner/README.md @@ -4,6 +4,7 @@ [![PyPI](https://img.shields.io/pypi/v/mpak-scanner)](https://pypi.org/project/mpak-scanner/) [![Python](https://img.shields.io/pypi/pyversions/mpak-scanner)](https://pypi.org/project/mpak-scanner/) [![License](https://img.shields.io/pypi/l/mpak-scanner)](https://github.com/NimbleBrainInc/mpak/blob/main/apps/scanner/LICENSE) +[![mpak.dev](https://mpak.dev/badge.svg)](https://mpak.dev) Security scanner for [MCP](https://modelcontextprotocol.io/) bundles (.mcpb). Reference implementation of the [mpak Trust Framework (MTF)](https://mpaktrust.org), an open security standard for MCP server packaging. @@ -121,6 +122,60 @@ The scanner ships with test fixtures for validation: See [tests/fixtures/README.md](tests/fixtures/README.md) for details. +## Releasing + +Releases are automated via GitHub Actions. Pushing a tag triggers the full pipeline: verify, publish to PyPI (via [trusted publishing](https://docs.pypi.org/trusted-publishers/)), and build + push Docker image to GHCR. + +**Version is defined in one place:** `pyproject.toml`. The runtime version (`mpak_scanner.__version__`, `SCANNER_VERSION`) is derived automatically via `importlib.metadata`. + +### Steps + +1. **Bump version** in `pyproject.toml`: + ```bash + # Edit pyproject.toml version field, or use hatch: + hatch version patch # 0.2.4 → 0.2.5 + hatch version minor # 0.2.4 → 0.3.0 + ``` + +2. **Run verification:** + ```bash + uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/ && uv run ty check src/ && uv run pytest + ``` + +3. **Commit and push:** + ```bash + git commit -am "scanner: bump to X.Y.Z" + git push + ``` + +4. **Tag and push** (this triggers the publish): + ```bash + git tag scanner-vX.Y.Z + git push origin scanner-vX.Y.Z + ``` + +CI will: +- Run lint, format, type check, and unit tests +- Verify the tag matches `pyproject.toml` +- Build and publish to [PyPI](https://pypi.org/project/mpak-scanner/) +- Build and push Docker image to `ghcr.io/nimblebraininc/mpak-scanner:{version}` and `:latest` + +See [`scanner-publish.yml`](../../.github/workflows/scanner-publish.yml). + +### Docker Image + +The Docker image includes all external security tools (Syft, Grype, TruffleHog, ESLint, Bandit, GuardDog) and installs `mpak-scanner` from PyPI. + +```bash +# Pull from GHCR +docker pull ghcr.io/nimblebraininc/mpak-scanner:latest + +# Run a scan +docker run --rm -v /path/to/bundle.mcpb:/bundle.mcpb ghcr.io/nimblebraininc/mpak-scanner scan /bundle.mcpb +``` + +For production deployment to ECR/K8s, see `deployments/mpak/`. + ## Related Projects - [mpak registry](https://mpak.dev) - Search, download, and publish MCP bundles diff --git a/apps/scanner/src/mpak_scanner/__init__.py b/apps/scanner/src/mpak_scanner/__init__.py index e745f3a..ab433ab 100644 --- a/apps/scanner/src/mpak_scanner/__init__.py +++ b/apps/scanner/src/mpak_scanner/__init__.py @@ -3,5 +3,11 @@ from mpak_scanner.models import ComplianceLevel, ControlResult, SecurityReport from mpak_scanner.scanner import scan_bundle -__version__ = "0.2.4" +try: + from importlib.metadata import version as _get_version + + __version__ = _get_version("mpak-scanner") +except Exception: + __version__ = "0.0.0" + __all__ = ["scan_bundle", "SecurityReport", "ControlResult", "ComplianceLevel"] diff --git a/apps/scanner/src/mpak_scanner/scanner.py b/apps/scanner/src/mpak_scanner/scanner.py index 9da8c5b..8fd6488 100644 --- a/apps/scanner/src/mpak_scanner/scanner.py +++ b/apps/scanner/src/mpak_scanner/scanner.py @@ -69,8 +69,13 @@ logger = logging.getLogger(__name__) -# Version of the scanner -SCANNER_VERSION = "0.2.4" +# Version of the scanner (derived from pyproject.toml via importlib.metadata) +try: + from importlib.metadata import version as _get_version + + SCANNER_VERSION = _get_version("mpak-scanner") +except Exception: + SCANNER_VERSION = "0.0.0" # Domain groupings for controls (matches MTF v0.1 spec) DOMAINS = { From d0263eed0576823a9793cde4b60221f5e1518aae Mon Sep 17 00:00:00 2001 From: Mathew Goldsborough <1759329+mgoldsborough@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:49:48 -1000 Subject: [PATCH 2/4] scanner: bump to 0.2.5 --- apps/scanner/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/scanner/pyproject.toml b/apps/scanner/pyproject.toml index 071040c..de3c257 100644 --- a/apps/scanner/pyproject.toml +++ b/apps/scanner/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mpak-scanner" -version = "0.2.4" +version = "0.2.5" description = "Security scanner for MCP bundles. Powers mpak Certified verification." readme = "README.md" license = "Apache-2.0" From 2c9bbf450965d4d649b5b2ab02fc9d2d4eed7665 Mon Sep 17 00:00:00 2001 From: Mathew Goldsborough <1759329+mgoldsborough@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:52:47 -1000 Subject: [PATCH 3/4] Fix Dockerfile: move ARG after FROM so build arg is available --- apps/scanner/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/scanner/Dockerfile b/apps/scanner/Dockerfile index 6139a99..3399c6a 100644 --- a/apps/scanner/Dockerfile +++ b/apps/scanner/Dockerfile @@ -1,5 +1,5 @@ -ARG SCANNER_VERSION=0.2.4 FROM python:3.13-slim +ARG SCANNER_VERSION=0.2.5 # System deps for external security tools RUN apt-get update && apt-get install -y --no-install-recommends \ From 0c9655cacf8a6501fe61487acfb81e0af60431a6 Mon Sep 17 00:00:00 2001 From: Mathew Goldsborough <1759329+mgoldsborough@users.noreply.github.com> Date: Wed, 18 Feb 2026 20:53:06 -1000 Subject: [PATCH 4/4] scanner: bump to 0.2.6 --- apps/scanner/Dockerfile | 2 +- apps/scanner/pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/scanner/Dockerfile b/apps/scanner/Dockerfile index 3399c6a..047ec40 100644 --- a/apps/scanner/Dockerfile +++ b/apps/scanner/Dockerfile @@ -1,5 +1,5 @@ FROM python:3.13-slim -ARG SCANNER_VERSION=0.2.5 +ARG SCANNER_VERSION=0.2.6 # System deps for external security tools RUN apt-get update && apt-get install -y --no-install-recommends \ diff --git a/apps/scanner/pyproject.toml b/apps/scanner/pyproject.toml index de3c257..5a8edd1 100644 --- a/apps/scanner/pyproject.toml +++ b/apps/scanner/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "mpak-scanner" -version = "0.2.5" +version = "0.2.6" description = "Security scanner for MCP bundles. Powers mpak Certified verification." readme = "README.md" license = "Apache-2.0"