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
131 changes: 131 additions & 0 deletions .github/workflows/e2e-nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
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@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.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
245 changes: 245 additions & 0 deletions INITIAL-14.md

Large diffs are not rendered by default.

45 changes: 45 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# ForecastLabAI — operator-friendly entry points.
#
# This Makefile is a thin wrapper around the existing CLI / docker-compose
# tooling. It exists so a reviewer can run the full end-to-end demo with
# one command: `make demo`. The heavy lifting happens in
# `scripts/run_demo.py` (PRP-15); the rules here just orchestrate the
# prerequisites.
#
# Conventions:
# * Tab indentation on recipe lines (`make` requires it).
# * Every target is `.PHONY` (no real file outputs).
# * `uv run` prefixes every Python invocation (CLAUDE.md "Commands").
#
# Quick reference:
# make demo — full e2e: docker compose + migrations + run_demo
# make demo-quick — re-run run_demo without re-seeding (fast iteration)
# make demo-clean — destructive: wipe DB first, then run demo
# make help — list available targets

.DEFAULT_GOAL := help
.PHONY: help demo demo-quick demo-clean

help: ## show this help and exit
@echo "ForecastLabAI Make targets:"
@echo " make demo run the full end-to-end demo (~90-180 s)"
@echo " make demo-quick re-run the demo without re-seeding"
@echo " make demo-clean wipe the DB, then run the full demo"
@echo ""
@echo "Preconditions for all targets:"
@echo " * docker compose Postgres+pgvector must be reachable on :5433"
@echo " * uvicorn must already be serving on http://localhost:8123"
@echo " (start with: uv run uvicorn app.main:app --port 8123)"

demo: ## full e2e — seed -> features -> train x3 -> backtest -> register -> agent
docker compose up -d
uv run alembic upgrade head
uv run python scripts/run_demo.py --seed 42

demo-quick: ## re-run the demo without re-seeding (fast iteration)
uv run python scripts/run_demo.py --seed 42 --skip-seed

demo-clean: ## destructive — wipe DB then run the full demo
docker compose up -d
uv run alembic upgrade head
uv run python scripts/run_demo.py --seed 42 --reset
Loading
Loading