Skip to content

Commit 627a6fc

Browse files
Merge pull request #23 from heroku/fix_ci_workflow_yaml
Improved github CI
2 parents bb3b0e6 + df406c6 commit 627a6fc

File tree

5 files changed

+103
-32
lines changed

5 files changed

+103
-32
lines changed

.github/workflows/test.yml

Lines changed: 85 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,30 @@
11
name: MCP Tests
22

33
on:
4-
push:
5-
branches:
64
pull_request:
7-
85
jobs:
96
###########################################################################
107
# 1 - Local integration tests (always run)
118
###########################################################################
129
local:
1310
runs-on: ubuntu-latest
14-
1511
# Dummy key lets the clients authenticate against the local servers.
1612
env:
1713
API_KEY: ci-test-key
1814

1915
steps:
2016
- uses: actions/checkout@v4
2117

18+
- name: Read Python version from .python-version
19+
id: python-version
20+
run: |
21+
PY_VERSION=$(cat .python-version)
22+
echo "version=$PY_VERSION" >> $GITHUB_OUTPUT
23+
2224
- uses: actions/setup-python@v5
2325
with:
24-
python-version: "3.11"
26+
python-version: ${{ steps.python-version.outputs.version }}
2527

26-
# Optional: cache wheels to speed up numpy / scipy installs
2728
- uses: actions/cache@v4
2829
with:
2930
path: ~/.cache/pip
@@ -33,47 +34,106 @@ jobs:
3334
run: |
3435
python -m pip install --upgrade pip
3536
pip install -r requirements.txt
36-
pip install -r requirements-dev.txt
3737
3838
- name: Run pytest (local transports)
3939
run: pytest -q
4040

41-
4241
###########################################################################
43-
# 2 - Remote smoke-test (only when secrets are set)
44-
#
45-
# • Put MCP_SERVER_URL → “https://<app>.herokuapp.com”
46-
# • Put API_KEY → same key you set as Heroku config var
47-
#
48-
# The fixture auto-skips the remote case if these are missing, so
49-
# the job is conditionally *created* only when both secrets exist.
42+
# 2 - Deploy this PR to a temp Heroku app and run tests against deployed app (in addition to 'local')
5043
###########################################################################
5144
remote:
52-
if: ${{ secrets.MCP_SERVER_URL != '' && secrets.API_KEY != '' }}
5345
runs-on: ubuntu-latest
54-
5546
env:
56-
MCP_SERVER_URL: ${{ secrets.MCP_SERVER_URL }}
57-
API_KEY: ${{ secrets.API_KEY }}
47+
HEROKU_API_KEY: ${{ secrets.HEROKU_API_KEY }}
48+
API_KEY: ci-test-key
49+
# also note that github CI doesn't have access to your app's config vars, so here we're setting the remote
50+
# server type to streamable HTTP. Folks using SSE would need to change this line for their e2e remote integration
51+
# tests to test SSE instead of streamable HTTP.
52+
REMOTE_SERVER_TRANSPORT_MODULE: streamable_http_server
53+
# $APP_NAME is set below because we need to shorten the repo owner's name, as a precaution
5854

5955
steps:
6056
- uses: actions/checkout@v4
57+
with:
58+
fetch-depth: 0 # <-- disables shallow clone, which heroku is upset by when running git push heroku later on
59+
60+
# Setting a short $APP_NAME that will be unique even if folks choose to fork this repo --> avoids clashes.
61+
# Needs to be shortened if the github repo owner has a long name (max 30 char app name heroku limit).
62+
- name: Generate short APP_NAME
63+
id: appname
64+
run: |
65+
OWNER_SHORT=${GITHUB_REPOSITORY_OWNER:0:5}
66+
REPO_NAME=$(basename "$GITHUB_REPOSITORY")
67+
PR_NUMBER=$(jq .number "$GITHUB_EVENT_PATH")
68+
APP_NAME="${OWNER_SHORT}-${REPO_NAME}-${PR_NUMBER}"
69+
echo "APP_NAME=$APP_NAME"
70+
echo "APP_NAME=$APP_NAME" >> $GITHUB_ENV
71+
72+
- name: Read Python version from .python-version
73+
id: python-version
74+
run: |
75+
PY_VERSION=$(cat .python-version)
76+
echo "version=$PY_VERSION" >> $GITHUB_OUTPUT
6177
6278
- uses: actions/setup-python@v5
6379
with:
64-
python-version: "3.11"
80+
python-version: ${{ steps.python-version.outputs.version }}
81+
82+
- name: Install Heroku CLI
83+
run: |
84+
curl https://cli-assets.heroku.com/install.sh | sh
85+
86+
- name: Log in to Heroku
87+
run: |
88+
echo "$HEROKU_API_KEY" | heroku auth:token
89+
90+
- name: Pre-cleanup (destroy app if it exists)
91+
continue-on-error: true
92+
run: |
93+
heroku apps:destroy --app $APP_NAME --confirm $APP_NAME
94+
95+
# github CI can't use our app.json, so the config etc bits must be set manually.
96+
# note WEB_CONCURRENCY is important! You get non-deterministic errors w/out it.
97+
- name: Create temp Heroku app for this PR
98+
run: |
99+
heroku create $APP_NAME
100+
heroku buildpacks:set heroku/python -a $APP_NAME
101+
heroku config:set API_KEY=$API_KEY --app $APP_NAME
102+
heroku config:set STDIO_MODE_ONLY=false
103+
heroku config:set REMOTE_SERVER_TRANSPORT_MODULE=$REMOTE_SERVER_TRANSPORT_MODULE --app $APP_NAME
104+
heroku config:set WEB_CONCURRENCY=1 --app $APP_NAME
105+
106+
- name: Deploy this branch to Heroku
107+
run: |
108+
git push https://heroku:[email protected]/$APP_NAME.git HEAD:refs/heads/main --force
65109
66110
- uses: actions/cache@v4
67111
with:
68112
path: ~/.cache/pip
69113
key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements*.txt') }}
70114

71-
- name: Install dependencies
115+
- name: Install test dependencies
72116
run: |
73117
python -m pip install --upgrade pip
74118
pip install -r requirements.txt
75119
76-
# We reuse the *same* test-suite; the fixture detects MCP_SERVER_URL
77-
# and adds the “remote” parameter automatically.
78-
- name: Run pytest (remote smoke)
79-
run: pytest -q
120+
- name: Get Heroku env vars
121+
id: heroku_env
122+
run: |
123+
url=$(heroku info -s -a $APP_NAME | grep web_url | cut -d= -f2 | tr -d '\n')
124+
echo "url=$url" >> "$GITHUB_OUTPUT"
125+
126+
- name: Run pytest against deployed app
127+
env:
128+
MCP_SERVER_URL: ${{ steps.heroku_env.outputs.url }}
129+
run: |
130+
echo "APP_NAME = $APP_NAME"
131+
echo "MCP_SERVER_URL = $MCP_SERVER_URL"
132+
echo "REMOTE_SERVER_TRANSPORT_MODULE = $REMOTE_SERVER_TRANSPORT_MODULE"
133+
echo "API_KEY is ${API_KEY:+set}" # won't print the key, just confirms it's non-empty
134+
pytest -q
135+
136+
- name: Destroy Heroku app after test
137+
if: always()
138+
run: |
139+
heroku apps:destroy --app $APP_NAME --confirm $APP_NAME

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ uvicorn src.streamable_http_server:app --reload
8787
*Running with `--reload` is optional, but great for local development*
8888

8989
Next, in a new pane, you can try running some queries against your server:
90+
9091
#### Local Streamable HTTP, SSE - Example Requests
9192
First run:
9293
```bash

tests/README.md

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,20 @@ pip install -r requirements.txt
3131

3232
## 2 · Run local transports only
3333
```bash
34+
git push heroku <your-branch>:main
35+
```
36+
37+
## 2 · Run local & one-off-dyno (STDIO) deployed transports
38+
```bash
3439
pytest tests -q
3540
```
3641

37-
## 3 - Run local & remote transports
42+
## 3 - Run local & all deployed transports
3843
```bash
44+
REMOTE_SERVER_TYPE=$(heroku config:get REMOTE_SERVER_TYPE) \
3945
MCP_SERVER_URL=$(heroku info -s -a "$APP_NAME" | grep web_url | cut -d= -f2 | tr -d '\n') \
4046
API_KEY=$(heroku config:get API_KEY -a "$APP_NAME") \
4147
pytest tests -q
42-
```
48+
```
49+
50+
*NOTE: if your `REMOTE_SERVER_TYPE` is set to `sse_server` and not the default `streamable_http_server`, you'll need to change the `REMOTE_SERVER_TRANSPORT_MODULE` declaration line in `.github/workflows/test.yml` to make sure that the end to end integration tests against the temporary deployed remote server are using the appropriate client code.*

tests/conftest.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,11 +73,14 @@ async def _ctx_stdio_local() -> AsyncGenerator[Dict, None]:
7373

7474
# ---------------------------------------------------------------- remote HTTP / SSE
7575
async def _ctx_remote() -> AsyncGenerator[Dict, None]:
76-
url = os.getenv("MCP_SERVER_URL"); key = os.getenv("API_KEY")
76+
url = os.getenv("MCP_SERVER_URL")
77+
key = os.getenv("API_KEY")
78+
server_type = os.getenv("REMOTE_SERVER_TRANSPORT_MODULE")
79+
7780
if not url or not key:
7881
pytest.skip("remote env-vars missing")
79-
yield {"client": "streamable_http_client",
80-
"extra_env": {"API_KEY": key, "MCP_SERVER_URL": url.rstrip("/")}}
82+
yield {"client": server_type.replace("server", "client"),
83+
"extra_env": {"API_KEY": key, "MCP_SERVER_URL": url, "REMOTE_SERVER_TRANSPORT_MODULE": server_type}}
8184

8285
# ---------------------------------------------------------------- remote STDIO ctx
8386
async def _ctx_remote_stdio() -> AsyncGenerator[Dict, None]:

tests/test_mcp_e2e.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -54,7 +54,6 @@ async def test_list_tools(ctx):
5454

5555

5656
async def test_code_exec(ctx):
57-
payload = json.dumps({"name": "code_exec_python",
58-
"arguments": {"code": "print(2+2)"}})
57+
payload = json.dumps({"name": "code_exec_python", "arguments": {"code": "print(2+2)"}})
5958
data = await _safe_call(ctx, "call_tool", "--args", payload)
6059
assert _extract_stdout(data) == "4"

0 commit comments

Comments
 (0)