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
15 changes: 11 additions & 4 deletions .github/ISSUE_TEMPLATE/autonomous-agent.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ body:
- [ ] `<exact command>` <expected result>
validations:
required: true
- type: textarea
id: product-evidence
attributes:
label: Product evidence
description: For features include dated sources, a relevant alternative, baseline and target, non-goals, review date, and a revisit/removal condition. For repairs provide the reproduction or maintenance rationale.
validations:
required: true
- type: textarea
id: scope
attributes:
Expand Down Expand Up @@ -68,10 +75,10 @@ body:
description: The exact canonical gate, in order, plus any change-type-specific gates. Must pass before any PR is opened.
value: |
```bash
uv run ruff check src/ tests/
uv run pyright src/
uv run pytest --tb=short -q
uv run python-docs-mcp-server doctor
uv run --locked ruff check src/ tests/ benchmarks/ ops/ .github/scripts/
uv run --locked pyright src/ benchmarks/
uv run --locked pytest --tb=short -q
uv run --locked python-docs-mcp-server doctor
```
validations:
required: true
Expand Down
22 changes: 18 additions & 4 deletions .github/PULL_REQUEST_TEMPLATE/agent.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,10 @@ Closes #<issue-number>
## Validation gate output
<!-- Paste the tail of each gate command. All must be green before opening this PR. -->
```text
$ uv run ruff check src/ tests/
$ uv run pyright src/
$ uv run pytest --tb=short -q
$ uv run python-docs-mcp-server doctor
$ uv run --locked ruff check src/ tests/ benchmarks/ ops/ .github/scripts/
$ uv run --locked pyright src/ benchmarks/
$ uv run --locked pytest --tb=short -q
$ uv run --locked python-docs-mcp-server doctor
```
<!-- Plus any change-type-specific gates from pipeline §5 (stdio smoke,
validate-corpus, uv lock --check) that applied to this change. -->
Expand All @@ -42,3 +42,17 @@ Pending.
If any fired: apply `supervisor-review` for Vision to decide. Vision can
resolve it and merge after separate verification; delegates cannot self-merge. -->
None.


## Product decision and independent evidence

- Kind: feature / bugfix / maintenance / policy
- User problem or reproducible failure:
- Dated research and alternative/market comparison (features):
- Baseline → target; acceptance evidence:
- Non-goals; compatibility/security impact:
- Outcome review date and revisit/removal condition (features):
- Exact head/base SHA; independent verifier check; unresolved limitations:

<!-- Keep the corresponding pdctl decision JSON with this evidence. A label or
another agent's summary does not replace the independent check. -->
150 changes: 35 additions & 115 deletions .github/RELEASE.md
Original file line number Diff line number Diff line change
@@ -1,115 +1,35 @@
# Release Process

## One-Time Setup: PyPI Trusted Publishing

Already configured for this project — these steps are kept for reference when forking or recovering the publisher state.

1. Go to https://pypi.org/manage/account/publishing/
2. Add a publisher (pending or active):
- **PyPI project name**: `python-docs-mcp-server`
- **Owner**: your GitHub username or org (this repo: `ayhammouda`)
- **Repository**: `python-docs-mcp-server`
- **Workflow name**: `release.yml`
- **Environment name**: `pypi`
3. In the GitHub repo, go to Settings > Environments
4. Create an environment named `pypi`
5. (Optional) Add environment protection rules:
- Required reviewers (recommended)
- Deployment branches: only `main` tags

## Coverage notes

**Runtime coverage:** the release workflow builds and tests against Python 3.13 only. Python 3.12 is covered by the CI workflow (`ci.yml`), which runs a 2×2 matrix (3.12/3.13 × ubuntu/macos) on every push to `main`. Since tags are created from commits that have already passed CI, 3.12 compatibility is verified before the release workflow runs. This is an accepted trade-off to keep the release artifact pipeline simple (single Python version produces the wheel).

**Documentation coverage:** the full docs index target is Python documentation versions 3.10 through 3.14.

---

## Release Checklist

Replace `X.Y.Z` below with the version you are releasing (e.g. `0.1.7`). Complete each step in order; do not skip ahead. The release workflow at `.github/workflows/release.yml` runs four jobs in sequence on every `v*` tag push: `build` → `publish` (PyPI) → `publish-mcp-registry` → `github-release`.

### Pre-Release Verification

- [ ] All CI tests green on `main`: https://github.com/ayhammouda/python-docs-mcp-server/actions/workflows/ci.yml
- [ ] Local test suite passes:
```bash
uv run pytest --tb=short -q
uv run ruff check src/ tests/
uv run pyright src/
```
- [ ] All four version-bearing files agree on `X.Y.Z` (the release workflow's `Verify tag matches package version` step enforces this; matching here saves a CI round-trip):
```bash
grep '^version' pyproject.toml
python3 -c 'import json; d = json.load(open("server.json")); print("server.json:", d["version"], "/ packages[0]:", d["packages"][0]["version"])'
grep '^name = "python-docs-mcp-server"' -A1 uv.lock | head -2
```
- [ ] `server.json` validates against the live MCP Registry schema. MCP Registry enforces stricter limits than PyPI (e.g. `description ≤ 100 chars`); validating before the tag prevents a half-published release (PyPI succeeds while MCP Registry rejects, as happened on v0.1.5 → recovered in v0.1.6):
```bash
curl -L "https://github.com/modelcontextprotocol/registry/releases/latest/download/mcp-publisher_$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/').tar.gz" | tar xz mcp-publisher
./mcp-publisher validate server.json
```
Must report `✅ server.json is valid`. If validation fails, shorten `server.json` `description` on `main` before tagging (the `pyproject.toml` description is unbounded by this constraint and can stay longer).
- [ ] Integration tests from `.github/INTEGRATION-TEST.md` are complete and signed off
- [ ] Refresh README `## Tools`, `glama.json`, and registry/version badges to match the current tool surface.
- [ ] `Doctor` subcommand passes:
```bash
uv run python-docs-mcp-server doctor
```
- [ ] `CHANGELOG.md` has a dated entry for `[X.Y.Z]`

### Tag and Release

- [ ] Create the annotated tag (replace `X.Y.Z` everywhere):
```bash
git tag -a vX.Y.Z -m "Release vX.Y.Z

<one-line summary of what shipped>

See CHANGELOG.md for the full entry."
```
- [ ] Push the tag to trigger the release workflow:
```bash
git push origin vX.Y.Z
```
- [ ] Watch the workflow run to completion:
```bash
gh run watch $(gh run list --workflow=release.yml --limit 1 --json databaseId -q '.[0].databaseId') --exit-status
```
- [ ] All four jobs green: `build` → `publish` → `publish-mcp-registry` → `github-release`

### Post-Release Verification

- [ ] PyPI listing exists with attestation badge: `https://pypi.org/project/python-docs-mcp-server/X.Y.Z/`
- [ ] Fresh install test from a clean shell (cache-busted so the new version actually resolves):
```bash
uv cache clean python-docs-mcp-server 2>/dev/null || true
uvx --refresh python-docs-mcp-server@X.Y.Z --version
# Expected: python-docs-mcp-server X.Y.Z
```
- [ ] Full install flow:
```bash
uvx python-docs-mcp-server build-index --versions 3.10,3.11,3.12,3.13,3.14
uvx python-docs-mcp-server doctor
```
Both must complete successfully.
- [ ] MCP Registry shows `X.Y.Z` as the `isLatest` entry:
```bash
curl -s 'https://registry.modelcontextprotocol.io/v0.1/servers?search=python-docs-mcp-server' | \
python3 -c "import json,sys; d=json.load(sys.stdin); print(next((e['server']['version'] for e in d['servers'] if e['_meta']['io.modelcontextprotocol.registry/official']['isLatest']), 'no latest'))"
```
- [ ] GitHub Release exists at `https://github.com/ayhammouda/python-docs-mcp-server/releases/tag/vX.Y.Z` with wheel + sdist attached and auto-generated notes
- [ ] **Slow E2E workflow** passes (manually triggered after publish):
- Run GitHub Actions workflow `Slow E2E` (`.github/workflows/e2e.yml`)
- Python 3.13 and 3.14 jobs both pass
- Each job installs the built wheel and runs `build-index --versions 3.10,3.11,3.12,3.13,3.14`, `doctor`, and `validate-corpus`
- [ ] Claude Desktop manual smoke test:
Configure `mcpServers` with `uvx python-docs-mcp-server` and verify a known query (e.g. "what is asyncio.TaskGroup") returns a correct hit.

### Release Complete

- [ ] All four `release.yml` jobs green for `vX.Y.Z`
- [ ] PyPI page shows `X.Y.Z` with the "Provenance" attestation badge
- [ ] MCP Registry `isLatest` entry points at `X.Y.Z`
- [ ] GitHub Release `vX.Y.Z` exists with both dist artifacts attached
- [ ] `CHANGELOG.md` `[X.Y.Z]` entry is committed on `main`
# Release process

Vision releases through the project broker after the scoped GitHub Apps are
activated (`ops/vision/README.md`). While setup is pending, release eligibility
fails closed. Do not substitute personal tokens or bypass checks.

1. Merge the version/changelog PR after independent current-head verification
and all required CI. `pyproject.toml`, `uv.lock`, `server.json` and its package
entry must agree. CI covers Python 3.12–3.14 on Ubuntu and macOS, the real
benchmark corpus, offline full-index regression, and installed-wheel stdio.
2. Wait for the required checks on the resulting **main commit** to pass.
3. Run `pdctl release EXACT_MAIN_COMMIT vX.Y.Z`. The broker confirms main
ancestry, the merged PR's verifier App check, main CI and manifest versions
before creating the tag. Release tags cannot be moved or deleted.
4. The release workflow runs eligibility → build/test/manifest validation →
installed-wheel integration on Python 3.13 and 3.14 → PyPI → MCP Registry →
GitHub release. Integration builds documentation 3.10–3.14 before publishing.
PyPI receives the same wheel that passed integration, with provenance.
5. Confirm every job succeeded, the exact version is visible on PyPI and in the
MCP Registry, the GitHub release has both artifacts, and a clean
`uvx --refresh python-docs-mcp-server@X.Y.Z --version` reports that version.
Confirm a representative real MCP client query before claiming client
compatibility; automated stdio tests are a distinct proof.

The Linux publisher helper `.github/scripts/install_publisher.sh` pins the
release URL and verifies its SHA-256 before extracting the executable. The
manifest is validated **before** PyPI publication. Do not use an unverified
`curl | tar` download or a moving `latest` URL.

PyPI Trusted Publishing remains bound to this repository, `release.yml` and the
`pypi` environment. Activation restricts that environment to `v*` tags and tag
creation to the owner App; the separate immutable-tag rule has no bypass actors.
No routine human reviewer is required. If publication partially fails, record
which registries succeeded and diagnose the failure; never overwrite a published
version, move a tag, or claim a release complete from a partial result.
8 changes: 8 additions & 0 deletions .github/scripts/install_publisher.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/usr/bin/env bash
set -euo pipefail
# Linux amd64 release runners only. Deliberate version/digest update required.
curl --fail --location --proto '=https' --tlsv1.2 \
https://github.com/modelcontextprotocol/registry/releases/download/v1.8.1/mcp-publisher_linux_amd64.tar.gz \
--output "${RUNNER_TEMP}/mcp-publisher.tar.gz"
echo "a06c9096dcb9727c13555b6be26c7effa707b01f06a4c561ba7a3635443cf2cc ${RUNNER_TEMP}/mcp-publisher.tar.gz" | sha256sum --check
tar -xzf "${RUNNER_TEMP}/mcp-publisher.tar.gz" -C "${RUNNER_TEMP}" mcp-publisher
85 changes: 85 additions & 0 deletions .github/scripts/release_gate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
"""Fail closed unless this tag names an independently verified main commit."""

import json
import os
import re
import urllib.request

REQUIRED = {
"Test (Python 3.12, ubuntu-latest)",
"Test (Python 3.13, ubuntu-latest)",
"Test (Python 3.14, ubuntu-latest)",
"Test (Python 3.12, macos-latest)",
"Test (Python 3.13, macos-latest)",
"Test (Python 3.14, macos-latest)",
"Dependency audit",
"Analyze",
"Installed package smoke",
"Product regression",
}


def require_eligible(api, sha: str, verifier_app_id: int) -> None:
if not re.fullmatch(r"[0-9a-f]{40}", sha) or verifier_app_id <= 0:
raise ValueError("Exact commit and configured verifier App ID are required")
if api(f"compare/{sha}...main")["status"] not in {"ahead", "identical"}:
raise ValueError("Release commit is not on main")
prs = [
p
for p in api(f"commits/{sha}/pulls?per_page=100")
if p["merged_at"] and p["merge_commit_sha"] == sha and p["base"]["ref"] == "main"
]
if len(prs) != 1:
raise ValueError("Release must be a uniquely identified merged PR")
head = prs[0]["head"]["sha"]
head_checks = api(f"commits/{head}/check-runs?per_page=100")["check_runs"]
codeql = [c for c in head_checks if c["name"] == "CodeQL" and c["app"]["id"] == 57789]
if not codeql or max(codeql, key=lambda c: c["id"])["conclusion"] != "success":
raise ValueError("Missing or failing CodeQL findings check on merged PR head")
reviews = [
c
for c in head_checks
if c["name"] == "Independent verification" and c["app"]["id"] == verifier_app_id
]
parents = api(f"git/commits/{sha}")["parents"]
latest = max(reviews, key=lambda c: c["id"], default={})
if (
len(parents) != 1
or latest.get("conclusion") != "success"
or latest.get("external_id") != f"{parents[0]['sha']}:{head}"
):
raise ValueError("Missing successful independent verification for merged PR head")
checks = api(f"commits/{sha}/check-runs?per_page=100")["check_runs"]
latest = {}
for check in sorted(checks, key=lambda c: c["id"]):
if check["app"]["id"] == 15368:
latest[check["name"]] = check
failed = sorted(
name for name in REQUIRED if latest.get(name, {}).get("conclusion") != "success"
)
if failed:
raise ValueError(f"Missing or failing main checks: {', '.join(failed)}")


def main() -> None:
repo = os.environ["GITHUB_REPOSITORY"]
token = os.environ["GH_TOKEN"]

def api(path):
request = urllib.request.Request(
f"https://api.github.com/repos/{repo}/{path}",
headers={
"Authorization": f"Bearer {token}",
"Accept": "application/vnd.github+json",
"User-Agent": "release-eligibility",
},
)
with urllib.request.urlopen(request, timeout=30) as response:
return json.load(response)

require_eligible(api, os.environ["GITHUB_SHA"], int(os.environ.get("VERIFIER_APP_ID") or "0"))
print("Verified main commit, independent review and required main checks.")


if __name__ == "__main__":
main()
27 changes: 19 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,44 +6,55 @@ on:
pull_request:
branches: [main]

permissions:
contents: read

jobs:
test:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
python-version: ["3.12", "3.13"]
python-version: ["3.12", "3.13", "3.14"]

timeout-minutes: 20
env:
UV_PYTHON: ${{ matrix.python-version }}
runs-on: ${{ matrix.os }}
name: Test (Python ${{ matrix.python-version }}, ${{ matrix.os }})

steps:
- uses: actions/checkout@v7
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@v10.2.0
uses: astral-sh/setup-uv@c18668ad3cf93ea998bef934396af7bb5c839dc7 # v10.2.0
with:
cache-suffix: py${{ matrix.python-version }}

- name: Set up Python ${{ matrix.python-version }}
run: uv python install ${{ matrix.python-version }}

- name: Install dependencies
run: uv sync --dev
run: uv sync --locked --dev

- name: Run linter
run: uv run ruff check src/ tests/ benchmarks/
run: uv run --locked ruff check src/ tests/ benchmarks/ ops/ .github/scripts/

- name: Run type checker
run: uv run pyright src/ benchmarks/
run: uv run --locked pyright src/ benchmarks/

- name: Validate frozen benchmark corpus
run: uv run --locked python -m benchmarks validate-corpus --corpus docs/benchmarks/corpus.yml --schema docs/benchmarks/corpus.schema.json

- name: Run tests
run: uv run pytest --tb=short -q
run: uv run --locked pytest --tb=short -q

- name: Verify wheel contents
run: |
uv build
uv run python -c "
uv run --locked python -c "
import zipfile, glob, sys
wheels = glob.glob('dist/*.whl')
if not wheels:
Expand Down
8 changes: 5 additions & 3 deletions .github/workflows/codeql.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,15 @@ jobs:

steps:
- name: Checkout repository
uses: actions/checkout@v7
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7
with:
persist-credentials: false

- name: Initialize CodeQL
uses: github/codeql-action/init@v4
uses: github/codeql-action/init@7999b86c43a865dc79d8923397f35af22de63401 # v4
with:
languages: python
queries: security-extended,security-and-quality

- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@v4
uses: github/codeql-action/analyze@7999b86c43a865dc79d8923397f35af22de63401 # v4
Loading
Loading