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
196 changes: 196 additions & 0 deletions .github/workflows/verify-examples.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,196 @@
name: Verify SDK Examples

on:
pull_request:
branches: [main]
push:
branches: [iml_verify_auto]
Comment thread
dIvYaNshhh marked this conversation as resolved.
Comment thread
dIvYaNshhh marked this conversation as resolved.
Comment thread
dIvYaNshhh marked this conversation as resolved.
workflow_dispatch:
inputs:
mode:
description: 'offline, live, or auto'
required: true
default: 'auto'
pypi_version:
description: 'PyPI version to verify'
required: true
default: 'latest'

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
verify-examples:
name: ${{ matrix.platform }}
runs-on: ${{ matrix.os }}
timeout-minutes: 30
env:
VERIFY_EXAMPLES_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.mode || 'auto' }}
VERIFY_PYPI_VERSION: ${{ github.event_name == 'workflow_dispatch' && inputs.pypi_version || 'latest' }}
strategy:
fail-fast: false
matrix:
include:
- platform: darwin-arm64
os: macos-15
native_module: gopher_mcp_python_native_darwin_arm64
- platform: linux-x64
os: ubuntu-22.04
native_module: gopher_mcp_python_native_linux_x64

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.11'

- name: Remove Homebrew on macOS
if: runner.os == 'macOS' && runner.environment == 'github-hosted'
shell: bash
run: |
echo "=== Removing Homebrew to simulate clean machine ==="
sudo rm -rf /opt/homebrew /usr/local/Homebrew /usr/local/Cellar
sudo rm -f /usr/local/bin/brew /opt/homebrew/bin/brew
echo "Homebrew removed"

- name: Install SDK package for native preflight
shell: bash
run: |
python -m venv native-preflight
source native-preflight/bin/activate
python -m pip install --upgrade pip
if [ "${{ github.event_name }}" = "pull_request" ]; then
python -m pip install -e . "gopher-mcp-python-native-${{ matrix.platform }}"
elif [ "$VERIFY_PYPI_VERSION" = "latest" ]; then
python -m pip install gopher-mcp-python gopher-mcp-python-native-${{ matrix.platform }}
Comment thread
dIvYaNshhh marked this conversation as resolved.
else
python -m pip install "gopher-mcp-python==${VERIFY_PYPI_VERSION}" "gopher-mcp-python-native-${{ matrix.platform }}==${VERIFY_PYPI_VERSION}"
fi

- name: Verify macOS native package
if: runner.os == 'macOS'
shell: bash
run: |
source native-preflight/bin/activate
native_lib_dir="$(
python -c 'import importlib; module = importlib.import_module("${{ matrix.native_module }}"); print(module.get_lib_path())'
)"
cd "$native_lib_dir"

echo "=== Code Signatures ==="
failed=0
for f in *.dylib; do
if codesign -v "$f" 2>&1; then
echo "OK $f"
else
echo "FAILED $f: $(codesign -v "$f" 2>&1)"
failed=1
fi
done
[ "$failed" -eq 0 ] || exit 1

echo "=== Homebrew Path Check ==="
for f in *.dylib; do
[ -L "$f" ] && continue
if otool -L "$f" | grep -qE '/usr/local/|/opt/homebrew/'; then
echo "FAILED $f has Homebrew paths:"
otool -L "$f" | grep -E '/usr/local/|/opt/homebrew/'
exit 1
fi
done

echo "=== Bundled @loader_path Dependencies ==="
missing=0
for f in *.dylib; do
[ -L "$f" ] && continue
while read -r dep rest; do
name="${dep#@loader_path/}"
if [ ! -f "$name" ]; then
echo "MISSING $name needed by $f"
missing=1
fi
done < <(otool -L "$f" | grep "@loader_path/" || true)
done
[ "$missing" -eq 0 ] || exit 1

- name: Verify Linux native package
Comment thread
dIvYaNshhh marked this conversation as resolved.
if: runner.os == 'Linux'
shell: bash
run: |
set -euo pipefail

source native-preflight/bin/activate
native_lib_dir="$(
python -c 'import importlib; module = importlib.import_module("${{ matrix.native_module }}"); print(module.get_lib_path())'
)"
cd "$native_lib_dir"

echo "=== Linux Native Dependencies ==="
found=0
for sofile in *.so *.so.*; do
[ -L "$sofile" ] && continue
[ -f "$sofile" ] || continue
found=1
echo "--- $sofile"
rpath="$(readelf -d "$sofile" | grep -E 'RUNPATH|RPATH' || true)"
if [ -n "$rpath" ]; then
echo "$rpath"
if ! grep -Fq '$ORIGIN' <<<"$rpath"; then
echo "Linux native library $sofile has RPATH/RUNPATH without \$ORIGIN"
exit 1
fi
else
echo "No RPATH/RUNPATH declared for $sofile"
fi
LD_LIBRARY_PATH="$native_lib_dir:${LD_LIBRARY_PATH:-}" ldd "$sofile" | tee /tmp/ldd.out
missing="$(awk '/not found/ {print $1}' /tmp/ldd.out | grep -Ev '^(libssl\.so|libcrypto\.so)' || true)"
if [ -n "$missing" ]; then
echo "$missing"
echo "Missing shared dependency for $sofile"
exit 1
fi
done

if find "$native_lib_dir" -maxdepth 1 -type f \( -name 'libssl.so*' -o -name 'libcrypto.so*' \) | grep .; then
if [ "${{ github.event_name }}" = "pull_request" ]; then
echo "OpenSSL libraries must remain system-provided in PR-built packages."
exit 1
fi
echo "WARNING: OpenSSL libraries should remain system-provided in newly published packages."
Comment thread
dIvYaNshhh marked this conversation as resolved.
fi

if [ "$found" -ne 1 ]; then
echo "No Linux shared libraries found in $native_lib_dir"
exit 1
fi

- name: Verify native loading
shell: bash
run: |
source native-preflight/bin/activate
python scripts/verify-example-native-probe.py

- name: Verify examples
shell: bash
env:
LLM_PROVIDER: ${{ secrets.LLM_PROVIDER || 'AnthropicProvider' }}
LLM_MODEL: ${{ secrets.LLM_MODEL }}
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
GOPHER_API_KEY: ${{ secrets.GOPHER_API_KEY }}
GOPHER_MCP_URL: ${{ secrets.GOPHER_MCP_URL }}
GOPHER_SDK_TEST: true
SDK_INSTALL_SPEC: ${{ github.event_name == 'pull_request' && github.workspace || '' }}
run: |
VERIFY_LIVE_PROMPT="list my draft mails" \
VERIFY_EXPECTED_ANSWER="r-2553040815323886578" \
scripts/verify-examples.sh --mode "$VERIFY_EXAMPLES_MODE" --only create_by_api_key
VERIFY_LIVE_PROMPT="list my draft mails" \
VERIFY_EXPECTED_ANSWER="r-2553040815323886578" \
scripts/verify-examples.sh --mode "$VERIFY_EXAMPLES_MODE" --only create_by_url
2 changes: 2 additions & 0 deletions examples/api/create_by_api_key.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@

API_KEY_PLACEHOLDER = "{YOUR_GOPHER_API_KEY}"
MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}"
MISSING_ENV_MARKER = "ERROR: missing-required-env: GOPHER_API_KEY,LLM_MODEL"


def env_or(name: str, fallback: str) -> str:
Expand Down Expand Up @@ -67,6 +68,7 @@ def main() -> None:
print(f"Queries: {len(queries)}")

if model == MODEL_PLACEHOLDER or api_key == API_KEY_PLACEHOLDER:
print(MISSING_ENV_MARKER, file=sys.stderr)
print(
"\nError: LLM_MODEL and GOPHER_API_KEY must both be set.",
file=sys.stderr,
Expand Down
4 changes: 4 additions & 0 deletions examples/api/create_by_gateway_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
API_KEY_PLACEHOLDER = "{YOUR_GOPHER_API_KEY}"
GATEWAY_ID_PLACEHOLDER = "{YOUR_MCP_GATEWAY_ID}"
MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}"
MISSING_ENV_MARKER = (
"ERROR: missing-required-env: GOPHER_API_KEY,GOPHER_MCP_GATEWAY_ID,LLM_MODEL"
)


def env_or(name: str, fallback: str) -> str:
Expand Down Expand Up @@ -82,6 +85,7 @@ def main() -> None:
or api_key == API_KEY_PLACEHOLDER
or gateway_id == GATEWAY_ID_PLACEHOLDER
):
print(MISSING_ENV_MARKER, file=sys.stderr)
print(
"\nError: LLM_MODEL, GOPHER_API_KEY, and GOPHER_MCP_GATEWAY_ID "
"must all be set.",
Expand Down
4 changes: 4 additions & 0 deletions examples/api/create_by_gateway_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
API_KEY_PLACEHOLDER = "{YOUR_GOPHER_API_KEY}"
GATEWAY_NAME_PLACEHOLDER = "{YOUR_MCP_GATEWAY_NAME}"
MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}"
MISSING_ENV_MARKER = (
"ERROR: missing-required-env: GOPHER_API_KEY,GOPHER_MCP_GATEWAY_NAME,LLM_MODEL"
)


def env_or(name: str, fallback: str) -> str:
Expand Down Expand Up @@ -82,6 +85,7 @@ def main() -> None:
or api_key == API_KEY_PLACEHOLDER
or gateway_name == GATEWAY_NAME_PLACEHOLDER
):
print(MISSING_ENV_MARKER, file=sys.stderr)
print(
"\nError: LLM_MODEL, GOPHER_API_KEY, and GOPHER_MCP_GATEWAY_NAME "
"must all be set.",
Expand Down
2 changes: 2 additions & 0 deletions examples/api/create_by_json.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
from gopher_mcp_python import GopherAgent

MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}"
MISSING_ENV_MARKER = "ERROR: missing-required-env: LLM_MODEL"

SERVER_CONFIG = json.dumps(
{
Expand Down Expand Up @@ -82,6 +83,7 @@ def main() -> None:
print(f"Queries: {len(queries)}")

if model == MODEL_PLACEHOLDER:
print(MISSING_ENV_MARKER, file=sys.stderr)
print("\nError: LLM_MODEL must be set.", file=sys.stderr)
sys.exit(1)

Expand Down
4 changes: 4 additions & 0 deletions examples/api/create_by_server_id.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
API_KEY_PLACEHOLDER = "{YOUR_GOPHER_API_KEY}"
SERVER_ID_PLACEHOLDER = "{YOUR_MCP_SERVER_ID}"
MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}"
MISSING_ENV_MARKER = (
"ERROR: missing-required-env: GOPHER_API_KEY,GOPHER_MCP_SERVER_ID,LLM_MODEL"
)


def env_or(name: str, fallback: str) -> str:
Expand Down Expand Up @@ -80,6 +83,7 @@ def main() -> None:
or api_key == API_KEY_PLACEHOLDER
or server_id == SERVER_ID_PLACEHOLDER
):
print(MISSING_ENV_MARKER, file=sys.stderr)
print(
"\nError: LLM_MODEL, GOPHER_API_KEY, and GOPHER_MCP_SERVER_ID "
"must all be set.",
Expand Down
4 changes: 4 additions & 0 deletions examples/api/create_by_server_name.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@
API_KEY_PLACEHOLDER = "{YOUR_GOPHER_API_KEY}"
SERVER_NAME_PLACEHOLDER = "{YOUR_MCP_SERVER_NAME}"
MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}"
MISSING_ENV_MARKER = (
"ERROR: missing-required-env: GOPHER_API_KEY,GOPHER_MCP_SERVER_NAME,LLM_MODEL"
)


def env_or(name: str, fallback: str) -> str:
Expand Down Expand Up @@ -82,6 +85,7 @@ def main() -> None:
or api_key == API_KEY_PLACEHOLDER
or server_name == SERVER_NAME_PLACEHOLDER
):
print(MISSING_ENV_MARKER, file=sys.stderr)
print(
"\nError: LLM_MODEL, GOPHER_API_KEY, and GOPHER_MCP_SERVER_NAME "
"must all be set.",
Expand Down
2 changes: 2 additions & 0 deletions examples/api/create_by_url.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@

URL_PLACEHOLDER = "{YOUR_MCP_URL}"
MODEL_PLACEHOLDER = "{YOUR_LLM_MODEL}"
MISSING_ENV_MARKER = "ERROR: missing-required-env: GOPHER_MCP_URL,LLM_MODEL"


def env_or(name: str, fallback: str) -> str:
Expand Down Expand Up @@ -64,6 +65,7 @@ def main() -> None:
print(f"Queries: {len(queries)}")

if model == MODEL_PLACEHOLDER or url == URL_PLACEHOLDER:
print(MISSING_ENV_MARKER, file=sys.stderr)
print(
"\nError: LLM_MODEL and GOPHER_MCP_URL must both be set.",
file=sys.stderr,
Expand Down
44 changes: 44 additions & 0 deletions scripts/verify-example-native-probe.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
#!/usr/bin/env python3
"""Verify that the installed Python SDK can load the native gopher-orch library."""

import sys

from gopher_mcp_python.ffi.library import GopherOrchLibrary


def fail(message: str) -> None:
print(f"[verify-native] error: {message}", file=sys.stderr)
sys.exit(1)


def main() -> None:
library = GopherOrchLibrary.get_instance()
if library is None:
fail(GopherOrchLibrary.get_load_error_message())

print("[verify-native] native library loaded")

required_symbols = (
"gopher_orch_agent_create_by_json",
"gopher_orch_agent_create_by_api_key",
"gopher_orch_agent_run",
"gopher_orch_agent_release",
"gopher_orch_api_fetch_servers",
"gopher_orch_last_error",
"gopher_orch_clear_error",
"gopher_orch_free",
)
native_library = getattr(library, "_lib", None)
missing = [
symbol
for symbol in required_symbols
if native_library is None or not hasattr(native_library, symbol)
]
if missing:
fail(f"native library missing required symbols: {', '.join(missing)}")

print("[verify-native] required symbols present")


if __name__ == "__main__":
main()
Loading
Loading