Skip to content

E2E Demo (nightly)

E2E Demo (nightly) #23

Workflow file for this run

name: E2E Demo (nightly)
# Nightly run of `scripts/run_demo.py` against a fresh Postgres+pgvector
# service to catch regressions in the documented end-to-end pipeline
# (seed -> features -> train -> backtest -> register -> verify).
#
# Per PRP-15, this workflow is intentionally NOT a required status check
# on `dev` or `main` -- it is informational only. Flake-budget lives here,
# not in the per-PR `ci.yml` gate.
#
# Trigger options:
# * Daily schedule at 07:00 UTC (cron `0 7 * * *`)
# * On-demand via `workflow_dispatch` (with optional ref override)
on:
schedule:
- cron: '0 7 * * *'
workflow_dispatch:
inputs:
ref:
description: 'Branch or ref to run the nightly demo on (default: github.ref)'
required: false
type: string
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ inputs.ref || github.ref }}
cancel-in-progress: true
env:
PYTHON_VERSION: "3.12"
UV_VERSION: "0.5"
CHECKOUT_REF: ${{ inputs.ref || github.ref }}
# API the script will hit. Bound to localhost because the runner ports
# this nightly job. Mirrors scripts/run_demo.py default.
DEMO_API_URL: "http://127.0.0.1:8123"
jobs:
e2e-demo:
name: Run end-to-end demo
runs-on: ubuntu-latest
services:
postgres:
image: pgvector/pgvector:pg16
env:
POSTGRES_USER: forecastlab
POSTGRES_PASSWORD: forecastlab
POSTGRES_DB: forecastlab_e2e
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
env:
DATABASE_URL: postgresql+asyncpg://forecastlab:forecastlab@localhost:5432/forecastlab_e2e
APP_ENV: testing
# The agent step in run_demo.py auto-skips when neither key is set;
# nightly CI runs without LLM keys so the step short-circuits with
# `⏭️ [SKIP]`. Do NOT add OPENAI_API_KEY / ANTHROPIC_API_KEY here.
steps:
- uses: actions/checkout@v6
with:
ref: ${{ env.CHECKOUT_REF }}
- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: ${{ env.UV_VERSION }}
enable-cache: true
- name: Set up Python
run: uv python install ${{ env.PYTHON_VERSION }}
- name: Install dependencies
run: uv sync --frozen --all-extras --dev
- name: Apply migrations
run: uv run --frozen alembic upgrade head
- name: Start uvicorn in background
# We bind to 127.0.0.1:8123 (the script's default) and write logs
# to a file so the artifact upload below can capture them on
# failure for forensics.
run: |
mkdir -p .ci-logs
nohup uv run --frozen uvicorn app.main:app \
--host 127.0.0.1 --port 8123 --log-level warning \
> .ci-logs/uvicorn.log 2>&1 &
echo $! > .ci-logs/uvicorn.pid
- name: Wait for uvicorn /health
run: |
for i in $(seq 1 30); do
if curl -fsS "${DEMO_API_URL}/health" > /dev/null; then
echo "uvicorn ready after ${i}s"
exit 0
fi
sleep 1
done
echo "uvicorn did not become healthy within 30s"
cat .ci-logs/uvicorn.log || true
exit 1
- name: Run demo pipeline
run: |
uv run --frozen python scripts/run_demo.py \
--seed 42 \
--api-url "${DEMO_API_URL}" \
--timeout 60
- name: Stop uvicorn
if: always()
run: |
if [ -f .ci-logs/uvicorn.pid ]; then
kill "$(cat .ci-logs/uvicorn.pid)" || true
fi
- name: Upload uvicorn logs on failure
if: failure()
uses: actions/upload-artifact@v7
with:
name: uvicorn-logs
path: .ci-logs/
retention-days: 7