Skip to content

Commit 041cf08

Browse files
Merge pull request #11 from NimbleBrainInc/chore/scanner-publish-workflow
Add automated release pipeline for scanner
2 parents e0bc6d6 + 0c9655c commit 041cf08

7 files changed

Lines changed: 184 additions & 21 deletions

File tree

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
name: Publish Scanner
2+
3+
on:
4+
push:
5+
tags:
6+
- 'scanner-v*'
7+
8+
env:
9+
REGISTRY: ghcr.io
10+
IMAGE_NAME: nimblebraininc/mpak-scanner
11+
12+
jobs:
13+
verify:
14+
runs-on: ubuntu-latest
15+
defaults:
16+
run:
17+
working-directory: apps/scanner
18+
steps:
19+
- uses: actions/checkout@v4
20+
- uses: actions/setup-python@v5
21+
with:
22+
python-version: '3.13'
23+
- uses: astral-sh/setup-uv@v4
24+
- run: uv sync --extra dev
25+
- run: uv run ruff check src/ tests/
26+
- run: uv run ruff format --check src/ tests/
27+
- run: uv run ty check src/
28+
- run: uv run pytest -m "not e2e"
29+
30+
publish-pypi:
31+
needs: verify
32+
runs-on: ubuntu-latest
33+
environment: pypi
34+
permissions:
35+
id-token: write
36+
defaults:
37+
run:
38+
working-directory: apps/scanner
39+
steps:
40+
- uses: actions/checkout@v4
41+
- uses: actions/setup-python@v5
42+
with:
43+
python-version: '3.13'
44+
- uses: astral-sh/setup-uv@v4
45+
46+
- name: Verify tag matches package version
47+
run: |
48+
TAG_VERSION="${GITHUB_REF#refs/tags/scanner-v}"
49+
PKG_VERSION=$(python -c "import tomllib; print(tomllib.load(open('pyproject.toml','rb'))['project']['version'])")
50+
if [ "$TAG_VERSION" != "$PKG_VERSION" ]; then
51+
echo "::error::Tag version ($TAG_VERSION) != pyproject.toml version ($PKG_VERSION)"
52+
exit 1
53+
fi
54+
55+
- name: Build
56+
run: uv build
57+
58+
- name: Publish to PyPI
59+
uses: pypa/gh-action-pypi-publish@release/v1
60+
with:
61+
packages-dir: apps/scanner/dist/
62+
63+
publish-docker:
64+
needs: [verify, publish-pypi]
65+
runs-on: ubuntu-latest
66+
permissions:
67+
contents: read
68+
packages: write
69+
steps:
70+
- uses: actions/checkout@v4
71+
72+
- name: Extract version from tag
73+
id: version
74+
run: echo "version=${GITHUB_REF#refs/tags/scanner-v}" >> "$GITHUB_OUTPUT"
75+
76+
- name: Log in to GHCR
77+
uses: docker/login-action@v3
78+
with:
79+
registry: ${{ env.REGISTRY }}
80+
username: ${{ github.actor }}
81+
password: ${{ secrets.GITHUB_TOKEN }}
82+
83+
- name: Build and push Docker image
84+
uses: docker/build-push-action@v6
85+
with:
86+
context: apps/scanner
87+
file: apps/scanner/Dockerfile
88+
push: true
89+
build-args: |
90+
SCANNER_VERSION=${{ steps.version.outputs.version }}
91+
tags: |
92+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ steps.version.outputs.version }}
93+
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:latest

apps/scanner/CLAUDE.md

Lines changed: 19 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -159,35 +159,38 @@ uv run pytest # all tests (unit + e2e)
159159

160160
## Releasing a New Version
161161

162-
The scanner is distributed via PyPI. The Docker image installs from PyPI, not local source.
162+
Releases are automated via GitHub Actions and PyPI trusted publishing. Pushing a tag triggers: verify → publish to PyPI → build + push Docker image to GHCR.
163+
164+
**Version is in one place:** `pyproject.toml`. Runtime version (`__version__`, `SCANNER_VERSION`) is derived via `importlib.metadata`.
163165

164166
### Steps
165167

166-
1. **Bump version** in four files (must all match):
167-
- `pyproject.toml` (`version = "X.Y.Z"`)
168-
- `src/mpak_scanner/__init__.py` (`__version__ = "X.Y.Z"`)
169-
- `src/mpak_scanner/scanner.py` (`SCANNER_VERSION = "X.Y.Z"`)
170-
- `Dockerfile` (`mpak-scanner[job]==X.Y.Z`)
168+
1. **Bump version** in `pyproject.toml` (the only place)
171169

172170
2. **Run verification**:
173171
```bash
174172
uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/ && uv run ty check src/ && uv run pytest
175173
```
176174

177-
3. **Commit and push** in `apps/mpak`
178-
179-
4. **Publish to PyPI** (from `apps/mpak/apps/scanner/`):
175+
3. **Commit, tag, and push:**
180176
```bash
181-
uv build && uv publish
177+
git commit -am "scanner: bump to X.Y.Z"
178+
git tag scanner-vX.Y.Z
179+
git push origin main --tags
182180
```
183181

184-
5. **Build + push Docker image** (from `hq/deployments/mpak/`):
185-
```bash
186-
make deploy-scanner ENV=production
187-
make apply-scanner-infra ENV=production # only if RBAC/secrets changed
188-
```
182+
CI handles PyPI publish and Docker build/push to `ghcr.io/nimblebraininc/mpak-scanner`. See `.github/workflows/scanner-publish.yml`.
183+
184+
### Production Deployment (ECR/K8s)
185+
186+
After the PyPI release, deploy the scanner to production K8s (from `hq/deployments/mpak/`):
187+
188+
```bash
189+
make deploy-scanner ENV=production
190+
make apply-scanner-infra ENV=production # only if RBAC/secrets changed
191+
```
189192

190-
The Makefile pushes both the git commit tag and `latest`. The mpak-api references `latest`, so deploying automatically updates what production uses.
193+
The Makefile builds from the Dockerfile which pulls the version from PyPI.
191194

192195
### Schemas and Rules
193196

apps/scanner/Dockerfile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
FROM python:3.13-slim
2+
ARG SCANNER_VERSION=0.2.6
23

34
# System deps for external security tools
45
RUN apt-get update && apt-get install -y --no-install-recommends \
@@ -20,7 +21,7 @@ RUN curl -sSfL https://raw.githubusercontent.com/trufflesecurity/trufflehog/main
2021
RUN npm install -g eslint eslint-plugin-security --no-fund --no-audit
2122

2223
# mpak-scanner + Python security tools (bandit, guarddog)
23-
RUN pip install --no-cache-dir "mpak-scanner[job]==0.2.4" bandit guarddog
24+
RUN pip install --no-cache-dir "mpak-scanner[job]==${SCANNER_VERSION}" bandit guarddog
2425

2526
ENTRYPOINT ["mpak-scanner"]
2627
CMD ["job"]

apps/scanner/README.md

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
[![PyPI](https://img.shields.io/pypi/v/mpak-scanner)](https://pypi.org/project/mpak-scanner/)
55
[![Python](https://img.shields.io/pypi/pyversions/mpak-scanner)](https://pypi.org/project/mpak-scanner/)
66
[![License](https://img.shields.io/pypi/l/mpak-scanner)](https://github.com/NimbleBrainInc/mpak/blob/main/apps/scanner/LICENSE)
7+
[![mpak.dev](https://mpak.dev/badge.svg)](https://mpak.dev)
78

89
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.
910

@@ -121,6 +122,60 @@ The scanner ships with test fixtures for validation:
121122

122123
See [tests/fixtures/README.md](tests/fixtures/README.md) for details.
123124

125+
## Releasing
126+
127+
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.
128+
129+
**Version is defined in one place:** `pyproject.toml`. The runtime version (`mpak_scanner.__version__`, `SCANNER_VERSION`) is derived automatically via `importlib.metadata`.
130+
131+
### Steps
132+
133+
1. **Bump version** in `pyproject.toml`:
134+
```bash
135+
# Edit pyproject.toml version field, or use hatch:
136+
hatch version patch # 0.2.4 → 0.2.5
137+
hatch version minor # 0.2.4 → 0.3.0
138+
```
139+
140+
2. **Run verification:**
141+
```bash
142+
uv run ruff check src/ tests/ && uv run ruff format --check src/ tests/ && uv run ty check src/ && uv run pytest
143+
```
144+
145+
3. **Commit and push:**
146+
```bash
147+
git commit -am "scanner: bump to X.Y.Z"
148+
git push
149+
```
150+
151+
4. **Tag and push** (this triggers the publish):
152+
```bash
153+
git tag scanner-vX.Y.Z
154+
git push origin scanner-vX.Y.Z
155+
```
156+
157+
CI will:
158+
- Run lint, format, type check, and unit tests
159+
- Verify the tag matches `pyproject.toml`
160+
- Build and publish to [PyPI](https://pypi.org/project/mpak-scanner/)
161+
- Build and push Docker image to `ghcr.io/nimblebraininc/mpak-scanner:{version}` and `:latest`
162+
163+
See [`scanner-publish.yml`](../../.github/workflows/scanner-publish.yml).
164+
165+
### Docker Image
166+
167+
The Docker image includes all external security tools (Syft, Grype, TruffleHog, ESLint, Bandit, GuardDog) and installs `mpak-scanner` from PyPI.
168+
169+
```bash
170+
# Pull from GHCR
171+
docker pull ghcr.io/nimblebraininc/mpak-scanner:latest
172+
173+
# Run a scan
174+
docker run --rm -v /path/to/bundle.mcpb:/bundle.mcpb ghcr.io/nimblebraininc/mpak-scanner scan /bundle.mcpb
175+
```
176+
177+
For production deployment to ECR/K8s, see `deployments/mpak/`.
178+
124179
## Related Projects
125180

126181
- [mpak registry](https://mpak.dev) - Search, download, and publish MCP bundles

apps/scanner/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "mpak-scanner"
7-
version = "0.2.4"
7+
version = "0.2.6"
88
description = "Security scanner for MCP bundles. Powers mpak Certified verification."
99
readme = "README.md"
1010
license = "Apache-2.0"

apps/scanner/src/mpak_scanner/__init__.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,11 @@
33
from mpak_scanner.models import ComplianceLevel, ControlResult, SecurityReport
44
from mpak_scanner.scanner import scan_bundle
55

6-
__version__ = "0.2.4"
6+
try:
7+
from importlib.metadata import version as _get_version
8+
9+
__version__ = _get_version("mpak-scanner")
10+
except Exception:
11+
__version__ = "0.0.0"
12+
713
__all__ = ["scan_bundle", "SecurityReport", "ControlResult", "ComplianceLevel"]

apps/scanner/src/mpak_scanner/scanner.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,8 +69,13 @@
6969

7070
logger = logging.getLogger(__name__)
7171

72-
# Version of the scanner
73-
SCANNER_VERSION = "0.2.4"
72+
# Version of the scanner (derived from pyproject.toml via importlib.metadata)
73+
try:
74+
from importlib.metadata import version as _get_version
75+
76+
SCANNER_VERSION = _get_version("mpak-scanner")
77+
except Exception:
78+
SCANNER_VERSION = "0.0.0"
7479

7580
# Domain groupings for controls (matches MTF v0.1 spec)
7681
DOMAINS = {

0 commit comments

Comments
 (0)