Skip to content

Commit e6577c7

Browse files
committed
ci: add slow e2e validation
1 parent 6ac8142 commit e6577c7

10 files changed

Lines changed: 1437 additions & 52 deletions

File tree

.github/INTEGRATION-TEST.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,10 @@ Release-specific sign-off still lives in [`.github/RELEASE.md`](RELEASE.md).
1616
- `uv run mcp-server-python-docs build-index --versions 3.12,3.13`
1717
- Doctor passes:
1818
- `uv run mcp-server-python-docs doctor`
19+
- Slow E2E workflow passes when preparing a release:
20+
- GitHub Actions: `Slow E2E`
21+
- Expected: Python 3.13 and 3.14 jobs both complete `build-index`, `doctor`,
22+
and `validate-corpus` from an installed wheel
1923
- If `uv` is not on `PATH`, use `python -m uv ...` instead
2024

2125
## Test 1: MCP Inspector quick loop
@@ -116,6 +120,24 @@ locked.
116120
- [ ] Follow the README from scratch
117121
- Expected: a new user can get to a working client configuration without using `.planning/`
118122

123+
## Test 5: Slow E2E workflow
124+
125+
Run this before a release and after changes to ingestion, publishing, packaging,
126+
or supported Python versions.
127+
128+
### Checks
129+
130+
- [ ] Start the `Slow E2E` workflow from GitHub Actions
131+
- Expected: both Python 3.13 and Python 3.14 jobs start
132+
- [ ] Confirm each job installs the built wheel into a clean virtual environment
133+
- Expected: the command path is the installed `mcp-server-python-docs`, not editable source
134+
- [ ] Confirm `build-index --versions 3.12,3.13` passes
135+
- Expected: both versions produce content, not symbol-only fallback
136+
- [ ] Confirm `doctor` and `validate-corpus` pass
137+
- Expected: corpus smoke checks include requested versions and the default version
138+
- [ ] Inspect uploaded logs if a job fails
139+
- Expected: Sphinx/build logs are available as workflow artifacts
140+
119141
## Evidence log
120142

121143
| Test | Pass/Fail | Tester | Date | Notes |
@@ -124,3 +146,4 @@ locked.
124146
| Claude Desktop | | | | |
125147
| Cursor | | | | |
126148
| Fresh install | | | | |
149+
| Slow E2E workflow | | | | |

.github/RELEASE.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,11 @@ Complete these steps in order. Each step has a checkbox -- do not skip ahead.
144144
uvx mcp-server-python-docs doctor
145145
# All checks should PASS
146146
```
147+
- [ ] Slow E2E workflow passes:
148+
- Run GitHub Actions workflow `Slow E2E`
149+
- Confirm Python 3.13 and Python 3.14 jobs both pass
150+
- Confirm each job installs the built wheel, runs
151+
`build-index --versions 3.12,3.13`, `doctor`, and `validate-corpus`
147152
- [ ] Claude Desktop test with published package:
148153
Configure `mcpServers` with `uvx mcp-server-python-docs` and verify
149154
"what is asyncio.TaskGroup" returns a correct hit
@@ -153,6 +158,7 @@ Complete these steps in order. Each step has a checkbox -- do not skip ahead.
153158
- [ ] GitHub Release exists with attached artifacts
154159
- [ ] PyPI page shows 0.1.0 with attestation
155160
- [ ] README install instructions verified end-to-end
161+
- [ ] Slow E2E workflow passed for the release candidate
156162
- [ ] Tag v0.1.0 exists in git
157163

158164
**Release date**: _______________

.github/workflows/e2e.yml

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
name: Slow E2E
2+
3+
on:
4+
workflow_dispatch:
5+
schedule:
6+
- cron: "17 4 * * 1"
7+
8+
jobs:
9+
installed-build-index:
10+
name: Installed build-index (Python ${{ matrix.python-version }})
11+
runs-on: ubuntu-latest
12+
timeout-minutes: 60
13+
14+
strategy:
15+
fail-fast: false
16+
matrix:
17+
python-version: ["3.13", "3.14"]
18+
19+
env:
20+
HOME: ${{ runner.temp }}/mcp-python-docs-home
21+
XDG_CACHE_HOME: ${{ runner.temp }}/mcp-python-docs-cache
22+
23+
steps:
24+
- uses: actions/checkout@v4
25+
26+
- name: Install uv
27+
uses: astral-sh/setup-uv@v4
28+
29+
- name: Set up Python ${{ matrix.python-version }}
30+
run: uv python install ${{ matrix.python-version }}
31+
32+
- name: Build package
33+
run: uv build
34+
35+
- name: Install wheel in clean virtual environment
36+
run: |
37+
uv run --python ${{ matrix.python-version }} python -m venv .e2e-venv
38+
.e2e-venv/bin/python -m pip install --upgrade pip
39+
.e2e-venv/bin/python -m pip install dist/*.whl
40+
.e2e-venv/bin/mcp-server-python-docs --version
41+
42+
- name: Build and validate full docs index
43+
run: |
44+
set -o pipefail
45+
.e2e-venv/bin/mcp-server-python-docs build-index --versions 3.12,3.13 \
46+
2>&1 | tee "${RUNNER_TEMP}/build-index-${{ matrix.python-version }}.log"
47+
.e2e-venv/bin/mcp-server-python-docs doctor
48+
.e2e-venv/bin/mcp-server-python-docs validate-corpus
49+
50+
- name: Upload E2E logs and cache artifacts
51+
if: always()
52+
uses: actions/upload-artifact@v4
53+
with:
54+
name: e2e-python-${{ matrix.python-version }}
55+
path: |
56+
${{ runner.temp }}/build-index-${{ matrix.python-version }}.log
57+
${{ runner.temp }}/mcp-python-docs-cache
58+
retention-days: 7

src/mcp_server_python_docs/__main__.py

Lines changed: 26 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,15 @@ def build_index(versions: str, skip_content: bool) -> None:
122122

123123
from mcp_server_python_docs.ingestion.inventory import ingest_inventory
124124
from mcp_server_python_docs.ingestion.publish import (
125+
_version_sort_key,
125126
generate_build_path,
127+
parse_expected_versions,
126128
publish_index,
127129
)
128130
from mcp_server_python_docs.ingestion.sphinx_json import (
131+
build_sphinx_json_command,
129132
ingest_sphinx_json_dir,
133+
make_sphinx_json_env,
130134
populate_synonyms,
131135
rebuild_fts_indexes,
132136
write_json_build_requirements,
@@ -144,7 +148,7 @@ def build_index(versions: str, skip_content: bool) -> None:
144148
"3.13": {"tag": "v3.13.12", "sphinx_pin": "sphinx<9.0.0"},
145149
}
146150

147-
version_list = [v.strip() for v in versions.split(",") if v.strip()]
151+
version_list = parse_expected_versions(versions)
148152
if not version_list:
149153
logger.error("No valid versions specified. Example: --versions 3.13")
150154
raise SystemExit(1)
@@ -159,8 +163,7 @@ def build_index(versions: str, skip_content: bool) -> None:
159163
raise SystemExit(1)
160164

161165
# Determine default version: highest version number (MVER-02)
162-
sorted_versions = sorted(version_list, key=lambda v: [int(x) for x in v.split(".")])
163-
default_version = sorted_versions[-1]
166+
default_version = max(version_list, key=_version_sort_key)
164167

165168
# Build into a timestamped artifact, not directly to index.db (PUBL-01)
166169
build_db_path = generate_build_path()
@@ -259,25 +262,15 @@ def build_index(versions: str, skip_content: bool) -> None:
259262
json_out = os.path.join(doc_dir, "build", "json")
260263
sphinx_compat_dir = Path(clone_dir) / "_sphinx_json_compat"
261264
write_sphinx_json_sitecustomize(sphinx_compat_dir)
262-
sphinx_env = os.environ.copy()
263-
sphinx_env["PYTHONPATH"] = (
264-
str(sphinx_compat_dir)
265-
if not sphinx_env.get("PYTHONPATH")
266-
else f"{sphinx_compat_dir}{os.pathsep}{sphinx_env['PYTHONPATH']}"
267-
)
265+
sphinx_env = make_sphinx_json_env(sphinx_compat_dir)
268266

269267
logger.info(
270268
"Running sphinx-build -b json for Python %s "
271269
"(this may take 3-8 minutes)...",
272270
version,
273271
)
274272
result = subprocess.run(
275-
[
276-
sphinx_build, "-b", "json",
277-
"-D", "html_theme=classic",
278-
"-j", "auto",
279-
doc_dir, json_out,
280-
],
273+
build_sphinx_json_command(sphinx_build, doc_dir, json_out),
281274
capture_output=True,
282275
text=True,
283276
cwd=doc_dir,
@@ -375,7 +368,10 @@ def validate_corpus(db_path: str | None) -> None:
375368
"""
376369
from pathlib import Path
377370

378-
from mcp_server_python_docs.ingestion.publish import run_smoke_tests
371+
from mcp_server_python_docs.ingestion.publish import (
372+
parse_expected_versions,
373+
run_smoke_tests,
374+
)
379375
from mcp_server_python_docs.storage.db import get_index_path, get_readonly_connection
380376

381377
if db_path is not None:
@@ -392,20 +388,28 @@ def validate_corpus(db_path: str | None) -> None:
392388

393389
# Auto-detect symbol-only builds from the last published ingestion run
394390
require_content = True
391+
expected_versions: list[str] | None = None
395392
try:
396393
ro_conn = get_readonly_connection(target)
397394
row = ro_conn.execute(
398-
"SELECT notes FROM ingestion_runs "
395+
"SELECT version, notes FROM ingestion_runs "
399396
"WHERE status = 'published' ORDER BY id DESC LIMIT 1"
400397
).fetchone()
401398
ro_conn.close()
402-
if row and row[0] and "build_mode=symbol_only" in row[0]:
399+
if row and row[0]:
400+
expected_versions = parse_expected_versions(row[0])
401+
if row and row[1] and "build_mode=symbol_only" in row[1]:
403402
require_content = False
404403
logger.info("Detected symbol-only build — skipping content checks")
405-
except Exception:
406-
pass # If we can't read the metadata, default to full validation
407-
408-
passed, messages = run_smoke_tests(target, require_content=require_content)
404+
except Exception as e:
405+
# If we can't read the metadata, default to full validation
406+
logger.debug("Could not read ingestion_runs metadata: %s", e)
407+
408+
passed, messages = run_smoke_tests(
409+
target,
410+
require_content=require_content,
411+
expected_versions=expected_versions,
412+
)
409413

410414
for msg in messages:
411415
if msg.startswith("OK:"):

0 commit comments

Comments
 (0)