Skip to content

Commit c30b372

Browse files
committed
ci(storyboard): extract reusable harness script for seller_agent.py storyboard
Closes #815 Moves the inline storyboard recipe from `.github/workflows/ci.yml` into `scripts/ci/run_storyboard_reference_seller.sh` so that cross-repo release-gating (adcp-client#1916) can call the script from a tagged checkout instead of duplicating shell steps. Key improvements over the previous inline approach: - Isolated venv (base package only) avoids polluting the caller's environment with dev extras - `trap ... EXIT` ensures the seller agent process is cleaned up on any exit path - Explicit `ADCP_SDK_VERSION` / `ADCP_SDK_TARBALL` interface with input validation (mutual exclusivity, absolute-path check, extension guard) - `ADCP_PORT` and `STORYBOARD_RESULT_PATH` are configurable; omitting the latter writes to stdout for local iteration CI passes `ADCP_SDK_VERSION=latest` to preserve the existing drift-detection behaviour. Cross-repo callers can substitute a pinned version or tarball path. https://claude.ai/code/session_01BdEqT6X8riNXLLCZUxUTMx
1 parent 0b30743 commit c30b372

3 files changed

Lines changed: 147 additions & 72 deletions

File tree

.github/workflows/ci.yml

Lines changed: 9 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -381,78 +381,15 @@ jobs:
381381
restore-keys: |
382382
${{ runner.os }}-npm-
383383
384-
- name: Pre-install @adcp/sdk (once, then call binary directly)
385-
# Single install step at the top of the job; subsequent runner
386-
# calls invoke the already-installed binary instead of paying
387-
# the ``npx -y -p ...`` per-invocation extract+link tax.
388-
# ``@adcp/sdk@latest`` is intentionally unpinned: this is AdCP's
389-
# own CI running AdCP's own canonical runner — tracking latest
390-
# surfaces protocol drift as soon as it ships, which is the
391-
# point of this job.
392-
run: |
393-
npm install -g @adcp/sdk@latest
394-
adcp --version
395-
396-
- name: Install dependencies
397-
run: |
398-
python -m pip install --upgrade pip
399-
pip install -e ".[dev]"
400-
401-
- name: Start seller agent
402-
run: |
403-
ADCP_PORT=3001 python examples/seller_agent.py &
404-
AGENT_PID=$!
405-
for i in $(seq 1 60); do
406-
# Any HTTP response (including 405 on GET to a POST-only endpoint)
407-
# means the server is up and accepting connections.
408-
# ``||`` runs on the assignment so curl's "000" stdout and the
409-
# fallback don't concatenate when the connection is refused.
410-
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 1 \
411-
http://127.0.0.1:3001/mcp 2>/dev/null) || HTTP_CODE="000"
412-
if [ "$HTTP_CODE" != "000" ]; then
413-
echo "Seller agent ready (HTTP ${HTTP_CODE}, pid ${AGENT_PID})"
414-
break
415-
fi
416-
if ! kill -0 "$AGENT_PID" 2>/dev/null; then
417-
echo "Seller agent process died during startup"
418-
exit 1
419-
fi
420-
if [ "$i" -eq 60 ]; then
421-
echo "Seller agent failed to start within 30s"
422-
kill "$AGENT_PID" 2>/dev/null || true
423-
exit 1
424-
fi
425-
sleep 0.5
426-
done
427-
428-
- name: Run storyboard suite
429-
timeout-minutes: 5
430-
# ``adcp`` was installed once at job start (see "Pre-install"
431-
# step) — call the binary directly to skip per-invocation
432-
# ``npx`` extract+link overhead.
433-
run: |
434-
adcp storyboard run \
435-
http://127.0.0.1:3001/mcp media_buy_seller \
436-
--json --allow-http \
437-
> storyboard-result.json
438-
439-
- name: Assert pass
440-
run: |
441-
python -c "
442-
import json, sys, pathlib
443-
p = pathlib.Path('storyboard-result.json')
444-
if not p.exists() or p.stat().st_size == 0:
445-
print('storyboard-result.json missing or empty — runner produced no output')
446-
sys.exit(1)
447-
with p.open() as f:
448-
d = json.load(f)
449-
if d.get('overall_status') != 'passing':
450-
print(json.dumps(d, indent=2))
451-
sys.exit(1)
452-
if not d.get('controller_detected'):
453-
print('controller_detected was false; check DemoStore overrides (see #304)')
454-
sys.exit(1)
455-
"
384+
- name: Run storyboard reference seller
385+
timeout-minutes: 10
386+
# scripts/ci/run_storyboard_reference_seller.sh owns the full
387+
# recipe: SDK install + isolated venv + agent boot + storyboard
388+
# run + assertions. See #815.
389+
env:
390+
ADCP_SDK_VERSION: latest
391+
STORYBOARD_RESULT_PATH: storyboard-result.json
392+
run: ./scripts/ci/run_storyboard_reference_seller.sh
456393

457394
- if: always()
458395
uses: actions/upload-artifact@v4

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -113,6 +113,7 @@ venv/
113113
ENV/
114114
env.bak/
115115
venv.bak/
116+
.ci-venv/
116117

117118
# Spyder project settings
118119
.spyderproject
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
#!/usr/bin/env bash
2+
# scripts/ci/run_storyboard_reference_seller.sh
3+
#
4+
# Boot examples/seller_agent.py and run the media_buy_seller storyboard
5+
# against it. Callable from Python CI and from cross-repo release-gating
6+
# (adcp-client#1916) without duplicating the recipe.
7+
#
8+
# Required — set exactly one of:
9+
# ADCP_SDK_VERSION @adcp/sdk version to install from npm ("latest" or
10+
# a specific version such as "7.10.2")
11+
# ADCP_SDK_TARBALL Absolute path to a candidate @adcp/sdk .tgz/.tar.gz
12+
#
13+
# Optional:
14+
# ADCP_PORT Port for the seller agent (default: 3001)
15+
# STORYBOARD_RESULT_PATH Path for JSON result output; omit to write to stdout
16+
#
17+
# Run from the root of an adcp-client-python checkout.
18+
# Node.js (npm + adcp binary) and Python must be on PATH.
19+
20+
set -euo pipefail
21+
22+
# --- Validate inputs ---
23+
24+
if [[ -z "${ADCP_SDK_VERSION:-}" && -z "${ADCP_SDK_TARBALL:-}" ]]; then
25+
echo "Error: set exactly one of ADCP_SDK_VERSION or ADCP_SDK_TARBALL" >&2
26+
echo " ADCP_SDK_VERSION=latest ${0}" >&2
27+
echo " ADCP_SDK_VERSION=7.10.2 ${0}" >&2
28+
echo " ADCP_SDK_TARBALL=/absolute/path/adcp-sdk.tgz ${0}" >&2
29+
exit 1
30+
fi
31+
if [[ -n "${ADCP_SDK_VERSION:-}" && -n "${ADCP_SDK_TARBALL:-}" ]]; then
32+
echo "Error: set only one of ADCP_SDK_VERSION or ADCP_SDK_TARBALL, not both" >&2
33+
exit 1
34+
fi
35+
36+
ADCP_PORT="${ADCP_PORT:-3001}"
37+
38+
if [[ -n "${ADCP_SDK_TARBALL:-}" ]]; then
39+
if [[ "${ADCP_SDK_TARBALL}" != /* ]]; then
40+
echo "Error: ADCP_SDK_TARBALL must be an absolute path (got: ${ADCP_SDK_TARBALL})" >&2
41+
exit 1
42+
fi
43+
if [[ ! -f "${ADCP_SDK_TARBALL}" ]]; then
44+
echo "Error: ADCP_SDK_TARBALL not found: ${ADCP_SDK_TARBALL}" >&2
45+
exit 1
46+
fi
47+
case "${ADCP_SDK_TARBALL}" in
48+
*.tgz|*.tar.gz) ;;
49+
*) echo "Error: ADCP_SDK_TARBALL must be a .tgz or .tar.gz file" >&2; exit 1 ;;
50+
esac
51+
fi
52+
53+
# --- Install @adcp/sdk ---
54+
55+
if [[ -n "${ADCP_SDK_VERSION:-}" ]]; then
56+
npm install -g "@adcp/sdk@${ADCP_SDK_VERSION}"
57+
else
58+
npm install -g "${ADCP_SDK_TARBALL}"
59+
fi
60+
adcp --version
61+
62+
# --- Install Python deps in an isolated venv ---
63+
# Uses the base package only — seller_agent.py does not need dev extras.
64+
65+
VENV_DIR=".ci-venv"
66+
python -m venv "${VENV_DIR}"
67+
"${VENV_DIR}/bin/pip" install --quiet --upgrade pip
68+
"${VENV_DIR}/bin/pip" install --quiet -e .
69+
70+
# --- Boot seller agent with guaranteed cleanup on exit ---
71+
72+
AGENT_PID=""
73+
_cleanup() {
74+
[[ -n "${AGENT_PID}" ]] && kill "${AGENT_PID}" 2>/dev/null || true
75+
}
76+
trap _cleanup EXIT
77+
78+
ADCP_PORT="${ADCP_PORT}" "${VENV_DIR}/bin/python" examples/seller_agent.py &
79+
AGENT_PID=$!
80+
81+
echo "Waiting for seller agent (pid ${AGENT_PID}) on port ${ADCP_PORT}..."
82+
for i in $(seq 1 60); do
83+
# Any HTTP response (including 405 on GET to a POST-only endpoint) means
84+
# the server is up and accepting connections.
85+
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" --max-time 1 \
86+
"http://127.0.0.1:${ADCP_PORT}/mcp" 2>/dev/null) || HTTP_CODE="000"
87+
if [[ "${HTTP_CODE}" != "000" ]]; then
88+
echo "Seller agent ready (HTTP ${HTTP_CODE}, pid ${AGENT_PID})"
89+
break
90+
fi
91+
if ! kill -0 "${AGENT_PID}" 2>/dev/null; then
92+
echo "Seller agent process died during startup" >&2
93+
exit 1
94+
fi
95+
if [[ "${i}" -eq 60 ]]; then
96+
echo "Seller agent failed to start within 30s" >&2
97+
exit 1
98+
fi
99+
sleep 0.5
100+
done
101+
102+
# --- Run storyboard ---
103+
104+
_RESULT_DEST="${STORYBOARD_RESULT_PATH:-}"
105+
if [[ -n "${_RESULT_DEST}" ]]; then
106+
adcp storyboard run \
107+
"http://127.0.0.1:${ADCP_PORT}/mcp" media_buy_seller \
108+
--json --allow-http \
109+
> "${_RESULT_DEST}"
110+
_ASSERT_FILE="${_RESULT_DEST}"
111+
else
112+
_TMPFILE="$(mktemp)"
113+
adcp storyboard run \
114+
"http://127.0.0.1:${ADCP_PORT}/mcp" media_buy_seller \
115+
--json --allow-http \
116+
| tee "${_TMPFILE}"
117+
_ASSERT_FILE="${_TMPFILE}"
118+
fi
119+
120+
# --- Assert result ---
121+
122+
"${VENV_DIR}/bin/python" - "${_ASSERT_FILE}" <<'PYEOF'
123+
import json, sys, pathlib
124+
p = pathlib.Path(sys.argv[1])
125+
if not p.exists() or p.stat().st_size == 0:
126+
print("storyboard result missing or empty — runner produced no output")
127+
sys.exit(1)
128+
with p.open() as f:
129+
d = json.load(f)
130+
if d.get("overall_status") != "passing":
131+
print(json.dumps(d, indent=2))
132+
sys.exit(1)
133+
if not d.get("controller_detected"):
134+
print("controller_detected was false; check DemoStore overrides (see #304)")
135+
sys.exit(1)
136+
print("Storyboard passed.")
137+
PYEOF

0 commit comments

Comments
 (0)