@@ -3,6 +3,24 @@ name: CI
33on :
44 push :
55 pull_request :
6+ workflow_dispatch :
7+ inputs :
8+ framework_ref :
9+ description : AIDEFEND framework branch, tag, or commit to stage
10+ required : false
11+ default : main
12+ type : string
13+ run_current_snapshot :
14+ description : Also run the exact 2026-07-24 snapshot assertions
15+ required : false
16+ default : false
17+ type : boolean
18+ repository_dispatch :
19+ types : [aidefend-framework-release]
20+ schedule :
21+ # Exercise the rolling upstream contract every day. Exact snapshot tests
22+ # remain an explicit manual gate and never pin this scheduled check.
23+ - cron : " 17 9 * * *"
624
725permissions :
826 contents : read
@@ -11,37 +29,201 @@ jobs:
1129 test :
1230 name : Test Suite
1331 runs-on : ubuntu-latest
14- timeout-minutes : 30
32+ timeout-minutes : 60
33+ env :
34+ GITHUB_BRANCH : ${{ github.event.client_payload.framework_ref || inputs.framework_ref || 'main' }}
1535
1636 steps :
1737 - name : Checkout code
18- uses : actions/checkout@v4
38+ uses : actions/checkout@v5
1939
2040 - name : Set up Python
21- uses : actions/setup-python@v5
41+ uses : actions/setup-python@v6
2242 with :
2343 python-version : " 3.11"
2444 cache : " pip"
2545
2646 - name : Set up Node.js
27- uses : actions/setup-node@v4
47+ uses : actions/setup-node@v5
2848 with :
29- node-version : " 20"
30- cache : " npm"
49+ node-version : " 22.7.0"
3150
3251 - name : Install Python dependencies
3352 run : |
3453 python -m pip install --upgrade pip
3554 python -m pip install -r requirements-dev.txt
3655
37- - name : Install Node dependencies
38- run : npm ci
39-
4056 - name : Build local knowledge base
4157 run : python __main__.py --resync
4258
43- - name : Run pytest
44- run : python -m pytest -q
59+ - name : Verify exact framework-to-index manifest
60+ run : python scripts/verify_index_manifest.py
61+
62+ - name : Run rolling compatibility pytest without silent skips
63+ run : |
64+ mkdir -p test-artifacts
65+ python -m pytest -q -m "not current_snapshot" --junitxml=test-artifacts/pytest.xml
66+ python - <<'PY'
67+ import xml.etree.ElementTree as ET
68+ from pathlib import Path
69+
70+ report = Path("test-artifacts/pytest.xml")
71+ root = ET.parse(report).getroot()
72+ skipped = sum(
73+ int(suite.attrib.get("skipped", 0))
74+ for suite in root.iter("testsuite")
75+ )
76+ if skipped:
77+ raise SystemExit(f"Release test suite skipped {skipped} test(s)")
78+ PY
79+
80+ - name : Run exact 2026-07-24 framework snapshot gate
81+ if : ${{ github.event_name == 'workflow_dispatch' && inputs.run_current_snapshot }}
82+ run : >-
83+ python -m pytest -q -m current_snapshot
84+ tests/test_current_framework_contract.py
85+
86+ - name : Exercise every MCP and REST tool
87+ run : python scripts/smoke_all_tools.py --data-path data --transport both --timeout 180
88+
89+ - name : Build and smoke-test clean release artifacts
90+ run : |
91+ python -m pip install build
92+ python scripts/build_release_artifacts.py --outdir dist
93+ python scripts/verify_distribution_inventory.py dist
94+ python -m pip install --force-reinstall --no-deps dist/*.whl
95+ mkdir -p /tmp/aidefend-wheel-smoke/scripts
96+ cp scripts/verify_index_manifest.py /tmp/aidefend-wheel-smoke/scripts/verify_index_manifest.py
97+ cp scripts/smoke_all_tools.py /tmp/aidefend-wheel-smoke/scripts/smoke_all_tools.py
98+ export PYTHONPATH=""
99+ export DATA_PATH="$GITHUB_WORKSPACE/data"
100+ cd /tmp/aidefend-wheel-smoke
101+ python - <<'PY'
102+ import json
103+ import os
104+ from pathlib import Path
105+ import app
106+ import app.tools
107+ import mcp_server
108+ from app.utils import NODE_PARSER_SCRIPT, parse_js_file_with_node
109+
110+ assert NODE_PARSER_SCRIPT.exists(), NODE_PARSER_SCRIPT
111+ workspace = Path(os.environ["GITHUB_WORKSPACE"]).resolve()
112+ assert not Path(app.__file__).resolve().is_relative_to(workspace)
113+ assert not Path(app.tools.__file__).resolve().is_relative_to(workspace)
114+ assert not Path(mcp_server.__file__).resolve().is_relative_to(workspace)
115+ data_path = workspace / "data"
116+ version = json.loads((data_path / "local_version.json").read_text(encoding="utf-8"))
117+ source_files = version.get("source_files")
118+ assert isinstance(source_files, list) and source_files
119+ tactic_files = [name for name in source_files if name != "aidefend-intro.js"]
120+ assert tactic_files
121+ assert all(
122+ isinstance(name, str)
123+ and Path(name).name == name
124+ and name.endswith(".js")
125+ for name in tactic_files
126+ )
127+ parsed = parse_js_file_with_node(data_path / "raw_content" / tactic_files[0])
128+ assert isinstance(parsed.get("name"), str) and parsed["name"].strip()
129+ assert isinstance(parsed.get("techniques"), list) and parsed["techniques"]
130+ PY
131+ python scripts/verify_index_manifest.py
132+ python scripts/smoke_all_tools.py \
133+ --data-path "$DATA_PATH" \
134+ --transport both \
135+ --timeout 180
136+
137+ clean-install :
138+ name : Clean wheel - ${{ matrix.os }} / Python ${{ matrix.python }}
139+ runs-on : ${{ matrix.os }}
140+ timeout-minutes : 35
141+ strategy :
142+ fail-fast : false
143+ matrix :
144+ os : [ubuntu-latest, windows-latest, macos-latest]
145+ python : ["3.10", "3.11", "3.12", "3.13"]
146+ env :
147+ PYTHONPATH : " "
148+
149+ steps :
150+ - name : Checkout code
151+ uses : actions/checkout@v5
152+
153+ - name : Set up Python
154+ uses : actions/setup-python@v6
155+ with :
156+ python-version : ${{ matrix.python }}
157+
158+ - name : Set up Node.js
159+ uses : actions/setup-node@v5
160+ with :
161+ node-version : " 22.7.0"
162+
163+ - name : Build wheel from a clean sdist extraction
164+ run : |
165+ python -m pip install --upgrade pip build
166+ python scripts/build_release_artifacts.py --outdir dist
167+ python scripts/verify_distribution_inventory.py dist
168+
169+ - name : Install wheel with all declared dependencies
170+ run : >-
171+ python -c "from pathlib import Path; import subprocess, sys;
172+ wheels=list(Path('dist').glob('*.whl'));
173+ assert len(wheels) == 1, wheels;
174+ subprocess.check_call([sys.executable, '-m', 'pip', 'install',
175+ '--force-reinstall', str(wheels[0])])"
176+
177+ - name : Verify dependency and installed-wheel contracts outside the repo
178+ working-directory : ${{ runner.temp }}
179+ run : |
180+ python -m pip check
181+ python "${{ github.workspace }}/scripts/verify_clean_install.py"
182+
183+ container :
184+ name : Container Contract
185+ runs-on : ubuntu-latest
186+ timeout-minutes : 30
187+ env :
188+ AIDEFEND_API_KEY : ci-container-contract-only
189+
190+ steps :
191+ - name : Checkout code
192+ uses : actions/checkout@v5
193+
194+ - name : Validate Dockerfile and Compose
195+ run : |
196+ docker compose config --quiet
197+ LOCAL_FRAMEWORK_PATH=/native/host/framework docker compose config | grep -q 'LOCAL_FRAMEWORK_PATH:'
198+ if LOCAL_FRAMEWORK_PATH=/native/host/framework docker compose config | grep -q /native/host/framework; then
199+ echo Native LOCAL_FRAMEWORK_PATH leaked into the Linux container configuration
200+ exit 1
201+ fi
202+ docker build --check .
203+
204+ - name : Build image
205+ run : docker build --tag aidefend-mcp:ci .
206+
207+ - name : Smoke-test the final runtime
208+ run : |
209+ test $(docker run --rm --entrypoint id aidefend-mcp:ci -u) -ne 0
210+ docker run --rm \
211+ --env AUTH_MODE=api_key \
212+ --env AIDEFEND_API_KEY=$AIDEFEND_API_KEY \
213+ --entrypoint python \
214+ aidefend-mcp:ci -c 'import app.main, mcp_server'
215+ docker run --rm \
216+ --mount type=bind,source=${{ github.workspace }}/tests/fixtures,target=/fixtures,readonly \
217+ --entrypoint node \
218+ aidefend-mcp:ci parse_js_module.mjs /fixtures/test_example.js \
219+ | grep -q 'Test Tactic'
220+
221+ - name : Verify persisted data volume is writable by the service user
222+ run : |
223+ docker run --rm \
224+ --mount type=volume,target=/app/data \
225+ --entrypoint touch \
226+ aidefend-mcp:ci /app/data/container-write-check
45227
46228 bandit :
47229 name : Bandit
@@ -50,10 +232,10 @@ jobs:
50232
51233 steps :
52234 - name : Checkout code
53- uses : actions/checkout@v4
235+ uses : actions/checkout@v5
54236
55237 - name : Set up Python
56- uses : actions/setup-python@v5
238+ uses : actions/setup-python@v6
57239 with :
58240 python-version : " 3.11"
59241 cache : " pip"
64246 python -m pip install bandit[toml]
65247
66248 - name : Run Bandit
67- run : python -m bandit -q -r app
249+ run : python -m bandit -q -r app mcp_server.py __main__.py
0 commit comments