Skip to content

Commit 3b5b5ce

Browse files
committed
ci: a per-test duration ceiling, the Detection Gate under full CPU load, and the Ecosystem Gate latest-release leg on every PR; the live-smoke driver clears only its own keys
1 parent 36c4a98 commit 3b5b5ce

4 files changed

Lines changed: 166 additions & 4 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,7 @@ jobs:
125125
126126
- name: Run tests
127127
run: |
128-
IPINFO_TOKEN='${{ secrets.IPINFO_TOKEN }}' REDIS_URL='redis://localhost:6379' uv run pytest -v --cov=guard_core --cov-fail-under=100 -W error
128+
IPINFO_TOKEN='${{ secrets.IPINFO_TOKEN }}' REDIS_URL='redis://localhost:6379' GUARD_TESTS_MAX_TEST_SECONDS=60 uv run pytest -v --cov=guard_core --cov-fail-under=100 -W error
129129
130130
- name: Notify Success on Slack Channel
131131
uses: rennf93/good-comms@master
@@ -202,7 +202,7 @@ jobs:
202202

203203
- name: Run tests in Docker
204204
run: |
205-
COMPOSE_BAKE=true PYTHON_VERSION=3.10 docker compose run --rm --build guard-core pytest -q -p no:cacheprovider -W error --ignore=tests/live_smoke --cov=guard_core --cov-branch --cov-fail-under=100
205+
COMPOSE_BAKE=true PYTHON_VERSION=3.10 docker compose run --rm --build -e GUARD_TESTS_MAX_TEST_SECONDS=60 guard-core pytest -q -p no:cacheprovider -W error --ignore=tests/live_smoke --cov=guard_core --cov-branch --cov-fail-under=100
206206
207207
- name: Tear down Docker Compose
208208
if: always()

.github/workflows/detection-gate.yml

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ jobs:
6666
shell: bash
6767
run: |
6868
REDIS_URL='redis://localhost:6379' \
69+
GUARD_TESTS_MAX_TEST_SECONDS=300 \
6970
uv run pytest -m 'redos_timing' -s -v --no-cov 2>&1 | tee detection-report.txt
7071
7172
- name: Build the PR comment body
@@ -95,6 +96,68 @@ jobs:
9596
done
9697
gh pr comment "$PR_NUMBER" --repo "$REPO" --body-file detection-comment.md
9798
99+
detection-gate-cpu-load:
100+
name: detection-gate-cpu-load
101+
runs-on: ubuntu-latest
102+
103+
services:
104+
redis:
105+
image: redis
106+
ports:
107+
- 6379:6379
108+
options: >-
109+
--health-cmd "redis-cli ping"
110+
--health-interval 10s
111+
--health-timeout 5s
112+
--health-retries 5
113+
114+
steps:
115+
- name: Checkout code
116+
uses: actions/checkout@v7
117+
118+
- name: Set up Python
119+
uses: actions/setup-python@v7
120+
with:
121+
python-version: '3.10'
122+
123+
- name: Install uv
124+
run: |
125+
pip install uv
126+
127+
- name: Install dependencies (same extras as the Dockerfile)
128+
run: |
129+
uv sync --extra dev --extra otel --extra logfire
130+
131+
- name: Start a CPU burner on every core
132+
run: |
133+
: > burner_pids.txt
134+
for _ in $(seq 1 "$(nproc)"); do
135+
python3 -c "while True: pass" &
136+
echo $! >> burner_pids.txt
137+
done
138+
139+
- name: Run the detection gate suite under CPU load
140+
shell: bash
141+
run: |
142+
REDIS_URL='redis://localhost:6379' \
143+
GUARD_TESTS_MAX_TEST_SECONDS=300 \
144+
uv run pytest -m 'redos_timing' -s -v --no-cov 2>&1 | tee detection-report-cpu-load.txt
145+
146+
- name: Stop the CPU burner
147+
if: always()
148+
run: |
149+
while read -r pid; do
150+
kill "$pid" 2>/dev/null || true
151+
done < burner_pids.txt
152+
153+
- name: Upload the CPU-load detection gate report
154+
if: always()
155+
uses: actions/upload-artifact@v7
156+
with:
157+
name: detection-report-cpu-load
158+
path: detection-report-cpu-load.txt
159+
if-no-files-found: ignore
160+
98161
detection-corpus:
99162
runs-on: ubuntu-latest
100163

.github/workflows/ecosystem-gate.yml

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,10 +145,102 @@ jobs:
145145
consumer/pytest-output-candidate.txt
146146
if-no-files-found: ignore
147147

148+
consumer-test-latest-release:
149+
name: consumer-test-latest-release
150+
if: github.event_name == 'pull_request'
151+
needs: build-wheel
152+
runs-on: ubuntu-latest
153+
154+
services:
155+
redis:
156+
image: redis
157+
ports:
158+
- 6379:6379
159+
options: >-
160+
--health-cmd "redis-cli ping"
161+
--health-interval 10s
162+
--health-timeout 5s
163+
--health-retries 5
164+
165+
strategy:
166+
fail-fast: false
167+
matrix:
168+
include:
169+
- name: fastapi-guard
170+
repo: rennf93/fastapi-guard
171+
sync_extra: '--extra dev'
172+
pytest_args: '-v --cov=guard --cov-branch'
173+
redis_prefix: 'test:fastapi_guard:'
174+
- name: flaskapi-guard
175+
repo: rennf93/flaskapi-guard
176+
sync_extra: '--extra dev'
177+
pytest_args: '-v --cov=flaskapi_guard'
178+
redis_prefix: 'test:flaskapi_guard:'
179+
- name: djapi-guard
180+
repo: rennf93/djapi-guard
181+
sync_extra: '--extra dev'
182+
pytest_args: '-v --cov=djangoapi_guard'
183+
redis_prefix: 'test:djangoapi_guard:'
184+
- name: guard-agent
185+
repo: rennf93/guard-agent
186+
sync_extra: '--extra dev'
187+
pytest_args: '-v --cov=guard_agent'
188+
redis_prefix: ''
189+
- name: guard-core-mcp
190+
repo: rennf93/guard-core-mcp
191+
sync_extra: '--extra dev'
192+
pytest_args: '-v --cov=guard_core_mcp --cov-branch'
193+
redis_prefix: ''
194+
195+
env:
196+
IPINFO_TOKEN: test_token
197+
REDIS_URL: redis://localhost:6379
198+
GH_TOKEN: ${{ github.token }}
199+
200+
steps:
201+
- name: Checkout code
202+
uses: actions/checkout@v7
203+
204+
- name: Download the PR wheel
205+
uses: actions/download-artifact@v8
206+
with:
207+
name: guard-core-wheel
208+
path: dist
209+
210+
- name: Set up Python
211+
uses: actions/setup-python@v7
212+
with:
213+
python-version: '3.10'
214+
215+
- name: Install uv
216+
run: |
217+
pip install uv
218+
219+
- name: Test ${{ matrix.name }} against the PR wheel (latest-release)
220+
run: |
221+
.github/scripts/ecosystem_gate.sh \
222+
"${{ matrix.repo }}" \
223+
"${{ matrix.sync_extra }}" \
224+
"${{ matrix.pytest_args }}" \
225+
"$PWD/dist" \
226+
"${{ needs.build-wheel.outputs.version }}" \
227+
"${{ matrix.redis_prefix }}" \
228+
"latest-release"
229+
230+
- name: Upload pytest output
231+
if: failure()
232+
uses: actions/upload-artifact@v7
233+
with:
234+
name: ${{ matrix.name }}-latest-release-pytest-output
235+
path: |
236+
consumer/pytest-output-base.txt
237+
consumer/pytest-output-candidate.txt
238+
if-no-files-found: ignore
239+
148240
ecosystem-gate:
149241
name: ecosystem-gate
150242
runs-on: ubuntu-latest
151-
needs: [consumer-test]
243+
needs: [consumer-test, consumer-test-latest-release]
152244
if: always()
153245
steps:
154246
- name: Fail when any consumer job failed
@@ -157,4 +249,8 @@ jobs:
157249
echo "::error::consumer-test result: ${{ needs.consumer-test.result }}"
158250
exit 1
159251
fi
252+
if [ "${{ github.event_name }}" = "pull_request" ] && [ "${{ needs.consumer-test-latest-release.result }}" != "success" ]; then
253+
echo "::error::consumer-test-latest-release result: ${{ needs.consumer-test-latest-release.result }}"
254+
exit 1
255+
fi
160256
echo "all consumer jobs passed"

tests/live_smoke/driver.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
NGINX_PORT = int(os.environ.get("LIVE_SMOKE_NGINX_PORT", "8089"))
2424
AGENT_PORT = int(os.environ.get("LIVE_SMOKE_AGENT_PORT", "8091"))
2525
REDIS_PORT = int(os.environ.get("LIVE_SMOKE_REDIS_PORT", "16379"))
26+
REDIS_PREFIX = "smoke:"
2627
OTLP_STUB_PORT = int(os.environ.get("LIVE_SMOKE_OTLP_STUB_PORT", "8092"))
2728

2829
BASE_URL = f"http://localhost:{NGINX_PORT}"
@@ -219,7 +220,9 @@ def _wait_healthy(self, expected_nonce: str, timeout: float = 60.0) -> None:
219220
def _flush_smoke_state(self) -> None:
220221
client = make_redis_client()
221222
try:
222-
client.flushdb()
223+
keys = list(client.scan_iter(f"{REDIS_PREFIX}*"))
224+
for batch_start in range(0, len(keys), 500):
225+
client.delete(*keys[batch_start : batch_start + 500])
223226
finally:
224227
client.close()
225228

0 commit comments

Comments
 (0)