From 83f6b25d85ba1982c475fea2e3e9f4c98957ff50 Mon Sep 17 00:00:00 2001 From: Mohammad Saddam Hossain Date: Sat, 22 Aug 2026 14:38:20 +0530 Subject: [PATCH] ci: smoke-test the published package on a schedule MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The mcp 2.0.0 breakage (#2) was not caught by CI, by a release check, or by the maintainer. It was caught by a user, 25 days after it started, because nothing anywhere installed the published package and tried to start it. This does that daily: pip install from PyPI, import, assert every registered tool has a handler and vice versa, then run the entry point and require a clean exit. It deliberately does not check out the repo. The artifact under test is the one on PyPI, resolved against whatever its declared dependencies pull today — a checkout would put src/ on sys.path and could pass while the release is broken. Verified both directions before landing: passes against 0.4.2 (85 tools, mcp 1.29.0), and fails with exit 1 against 0.4.1 + mcp 2.0.0, reproducing the AttributeError from #2. Co-Authored-By: Claude Opus 5 (1M context) --- .github/workflows/smoke.yml | 81 +++++++++++++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 .github/workflows/smoke.yml diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml new file mode 100644 index 0000000..84debad --- /dev/null +++ b/.github/workflows/smoke.yml @@ -0,0 +1,81 @@ +name: Smoke test (published package) + +# Installs seleniumboot-mcp from PyPI exactly as a user would and checks it can +# actually start. This exists because mcp 2.0.0 (2026-07-28) removed the +# decorators server.py registers its tools with, and the unbounded dependency +# meant every fresh install crashed at import — for 25 days, unnoticed, until a +# user reported it (#2). +# +# Deliberately does NOT check out the repo: the point is to test the published +# artifact and its real dependency resolution. A checkout would put src/ on the +# path and could mask a broken release. + +on: + schedule: + - cron: '17 6 * * *' + workflow_dispatch: + pull_request: + paths: + - '.github/workflows/smoke.yml' + +jobs: + smoke: + name: py${{ matrix.python-version }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + python-version: ['3.10', '3.12', '3.13'] + + steps: + - uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install from PyPI + run: pip install --no-cache-dir seleniumboot-mcp + + - name: Report resolved versions + run: pip list | grep -Ei '^(mcp|selenium|seleniumboot)' + + - name: Import and register tools + run: | + python - <<'PY' + import asyncio, sys + from importlib.metadata import version + + from selenium_mcp import server + + print("seleniumboot-mcp:", version("seleniumboot-mcp")) + print("mcp:", version("mcp")) + + tools = asyncio.run(server.list_tools()) + handlers = server.TOOL_HANDLERS + names = {t.name for t in tools} + + print(f"tools={len(tools)} handlers={len(handlers)}") + + failures = [] + if not tools: + failures.append("no tools registered") + unhandled = sorted(names - handlers.keys()) + if unhandled: + failures.append(f"tools with no handler: {unhandled}") + orphaned = sorted(handlers.keys() - names) + if orphaned: + failures.append(f"handlers with no tool: {orphaned}") + + if failures: + for f in failures: + print("FAIL:", f, file=sys.stderr) + sys.exit(1) + + print("OK") + PY + + - name: Entry point starts + run: | + # stdio server: EOF on stdin is a clean shutdown. A crash (e.g. the + # AttributeError from #2) exits non-zero and fails here. + seleniumboot-mcp < /dev/null + echo "entry point exited cleanly"