|
| 1 | +name: Onboarding Smoke Test |
| 2 | + |
| 3 | +# Verifies the contributor setup path: |
| 4 | +# fresh clone → ./setup.sh → ./start.sh → services respond |
| 5 | +# Catches onboarding regressions before they hit contributors. |
| 6 | + |
| 7 | +on: |
| 8 | + push: |
| 9 | + branches: [main] |
| 10 | + paths: |
| 11 | + - "setup.sh" |
| 12 | + - "start.sh" |
| 13 | + - "backend/**" |
| 14 | + - "frontend/**" |
| 15 | + - "backend/requirements.txt" |
| 16 | + - "backend/requirements-dev.txt" |
| 17 | + - ".github/workflows/smoke-test.yml" |
| 18 | + pull_request: |
| 19 | + branches: [main] |
| 20 | + paths: |
| 21 | + - "setup.sh" |
| 22 | + - "start.sh" |
| 23 | + - "backend/**" |
| 24 | + - "frontend/**" |
| 25 | + - "backend/requirements.txt" |
| 26 | + - "backend/requirements-dev.txt" |
| 27 | + - ".github/workflows/smoke-test.yml" |
| 28 | + workflow_dispatch: |
| 29 | + |
| 30 | +jobs: |
| 31 | + smoke-test: |
| 32 | + name: Fresh-clone smoke test (Ubuntu) |
| 33 | + runs-on: ubuntu-latest |
| 34 | + timeout-minutes: 15 |
| 35 | + |
| 36 | + steps: |
| 37 | + # ── 1. Checkout ──────────────────────────────────────────────────────── |
| 38 | + - name: Checkout repository |
| 39 | + uses: actions/checkout@v4 |
| 40 | + |
| 41 | + # ── 2. Set up Python 3.11 ────────────────────────────────────────────── |
| 42 | + # setup.sh requires Python 3.11+. We pin it explicitly so the runner |
| 43 | + # never silently falls back to an older system Python. |
| 44 | + - name: Set up Python 3.11 |
| 45 | + uses: actions/setup-python@v5 |
| 46 | + with: |
| 47 | + python-version: "3.11" |
| 48 | + |
| 49 | + # ── 3. Set up Node.js ────────────────────────────────────────────────── |
| 50 | + # setup.sh checks for node + npm; frontend uses Vite on port 5173. |
| 51 | + - name: Set up Node.js |
| 52 | + uses: actions/setup-node@v4 |
| 53 | + with: |
| 54 | + node-version: "20" |
| 55 | + |
| 56 | + # ── 4. Make scripts executable ───────────────────────────────────────── |
| 57 | + # Git on some clients strips execute bits — enforce them explicitly. |
| 58 | + - name: Make scripts executable |
| 59 | + run: chmod +x setup.sh start.sh |
| 60 | + |
| 61 | + # ── 5. Run setup.sh ──────────────────────────────────────────────────── |
| 62 | + # setup.sh: creates venv, pip installs backend/requirements.txt + |
| 63 | + # httpx[cli], npm installs frontend, writes .env, creates data/logs dirs. |
| 64 | + # lsof is used by start.sh; install it here so setup.sh can't fail on it. |
| 65 | + - name: Install system dependencies |
| 66 | + run: sudo apt-get install -y lsof libcairo2-dev pkg-config python3-dev |
| 67 | + |
| 68 | + - name: Run setup.sh |
| 69 | + run: | |
| 70 | + echo "::group::setup.sh output" |
| 71 | + bash setup.sh |
| 72 | + echo "::endgroup::" |
| 73 | +
|
| 74 | + # ── 6. Verify setup produced expected artifacts ──────────────────────── |
| 75 | + # Fails fast with a clear message if setup.sh silently skipped something. |
| 76 | + - name: Verify setup artifacts |
| 77 | + run: | |
| 78 | + echo "Checking venv..." |
| 79 | + test -f venv/bin/python3 || { echo "FAIL: venv/bin/python3 missing"; exit 1; } |
| 80 | + test -f venv/bin/activate || { echo "FAIL: venv/bin/activate missing"; exit 1; } |
| 81 | +
|
| 82 | + echo "Checking backend deps..." |
| 83 | + source venv/bin/activate |
| 84 | + python3 -c "import fastapi" || { echo "FAIL: fastapi not installed"; exit 1; } |
| 85 | + python3 -c "import uvicorn" || { echo "FAIL: uvicorn not installed"; exit 1; } |
| 86 | + python3 -c "import httpx" || { echo "FAIL: httpx not installed"; exit 1; } |
| 87 | + deactivate |
| 88 | +
|
| 89 | + echo "Checking frontend node_modules..." |
| 90 | + test -f frontend/node_modules/.bin/vite \ |
| 91 | + || { echo "FAIL: frontend/node_modules/.bin/vite missing"; exit 1; } |
| 92 | +
|
| 93 | + echo "Checking directories..." |
| 94 | + for d in data data/raw data/reports logs wordlists; do |
| 95 | + test -d "$d" || { echo "FAIL: directory '$d' missing"; exit 1; } |
| 96 | + done |
| 97 | +
|
| 98 | + echo "Checking .env..." |
| 99 | + test -f .env || { echo "FAIL: .env not created"; exit 1; } |
| 100 | + grep -q "SECUSCAN_BIND_PORT=8000" .env \ |
| 101 | + || { echo "FAIL: expected port config missing from .env"; exit 1; } |
| 102 | +
|
| 103 | + echo "All artifact checks passed." |
| 104 | +
|
| 105 | + # ── 7. Start services via start.sh ───────────────────────────────────── |
| 106 | + # start.sh launches uvicorn on 127.0.0.1:8000 and Vite on 127.0.0.1:5173. |
| 107 | + # We background the whole script and capture its PID for cleanup. |
| 108 | + - name: Start services via start.sh |
| 109 | + run: | |
| 110 | + bash start.sh & |
| 111 | + echo "START_SH_PID=$!" >> "$GITHUB_ENV" |
| 112 | +
|
| 113 | + # ── 8. Wait for backend (uvicorn on :8000) ───────────────────────────── |
| 114 | + # start.sh starts uvicorn on 127.0.0.1:8000. |
| 115 | + # /openapi.json is always present in FastAPI without any auth — safer |
| 116 | + # than /health which may not exist. |
| 117 | + - name: Wait for backend to be ready |
| 118 | + run: | |
| 119 | + MAX_WAIT=60 |
| 120 | + INTERVAL=3 |
| 121 | + ELAPSED=0 |
| 122 | + URL="http://127.0.0.1:8000/openapi.json" |
| 123 | +
|
| 124 | + echo "Polling $URL ..." |
| 125 | + until curl --silent --fail --max-time 2 "$URL" > /dev/null 2>&1; do |
| 126 | + if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then |
| 127 | + echo "ERROR: Backend did not become ready within ${MAX_WAIT}s" |
| 128 | + echo "--- Active processes ---" |
| 129 | + ps aux | grep -E "uvicorn|python" || true |
| 130 | + echo "--- Port 8000 status ---" |
| 131 | + lsof -i :8000 || true |
| 132 | + exit 1 |
| 133 | + fi |
| 134 | + echo " Not ready (${ELAPSED}s elapsed) — retrying in ${INTERVAL}s ..." |
| 135 | + sleep "$INTERVAL" |
| 136 | + ELAPSED=$((ELAPSED + INTERVAL)) |
| 137 | + done |
| 138 | + echo "Backend ready after ${ELAPSED}s." |
| 139 | +
|
| 140 | + # ── 9. Wait for frontend (Vite on :5173) ────────────────────────────── |
| 141 | + - name: Wait for frontend to be ready |
| 142 | + run: | |
| 143 | + MAX_WAIT=60 |
| 144 | + INTERVAL=3 |
| 145 | + ELAPSED=0 |
| 146 | + URL="http://127.0.0.1:5173" |
| 147 | +
|
| 148 | + echo "Polling $URL ..." |
| 149 | + until curl --silent --fail --max-time 2 "$URL" > /dev/null 2>&1; do |
| 150 | + if [ "$ELAPSED" -ge "$MAX_WAIT" ]; then |
| 151 | + echo "ERROR: Frontend did not become ready within ${MAX_WAIT}s" |
| 152 | + echo "--- Active processes ---" |
| 153 | + ps aux | grep -E "vite|node" || true |
| 154 | + echo "--- Port 5173 status ---" |
| 155 | + lsof -i :5173 || true |
| 156 | + exit 1 |
| 157 | + fi |
| 158 | + echo " Not ready (${ELAPSED}s elapsed) — retrying in ${INTERVAL}s ..." |
| 159 | + sleep "$INTERVAL" |
| 160 | + ELAPSED=$((ELAPSED + INTERVAL)) |
| 161 | + done |
| 162 | + echo "Frontend ready after ${ELAPSED}s." |
| 163 | +
|
| 164 | + # ── 10. Smoke-check backend API ──────────────────────────────────────── |
| 165 | + # /openapi.json must contain "openapi" and "SecuScan" (the app title). |
| 166 | + # /docs must return HTTP 200 (Swagger UI). |
| 167 | + # These require zero auth and prove the real app stack loaded correctly. |
| 168 | + - name: Smoke-check backend API |
| 169 | + run: | |
| 170 | + echo "--- GET /openapi.json ---" |
| 171 | + OAS=$(curl --silent --fail --max-time 5 "http://127.0.0.1:8000/openapi.json") |
| 172 | + echo "$OAS" | python3 -c "import sys, json; d=json.load(sys.stdin); assert 'openapi' in d, 'missing openapi key'" \ |
| 173 | + || { echo "FAIL: /openapi.json invalid JSON or missing openapi key"; exit 1; } |
| 174 | + echo "openapi.json OK" |
| 175 | +
|
| 176 | + echo "--- GET /docs ---" |
| 177 | + curl --silent --fail --max-time 5 "http://127.0.0.1:8000/docs" > /dev/null \ |
| 178 | + || { echo "FAIL: /docs did not return 200"; exit 1; } |
| 179 | + echo "/docs OK" |
| 180 | +
|
| 181 | + echo "Backend smoke checks passed." |
| 182 | +
|
| 183 | + # ── 11. Smoke-check frontend ─────────────────────────────────────────── |
| 184 | + - name: Smoke-check frontend |
| 185 | + run: | |
| 186 | + echo "--- GET http://127.0.0.1:5173 ---" |
| 187 | + BODY=$(curl --silent --fail --max-time 5 "http://127.0.0.1:5173") |
| 188 | + echo "$BODY" | grep -qi "html" \ |
| 189 | + || { echo "FAIL: frontend did not return an HTML page"; exit 1; } |
| 190 | + echo "Frontend smoke check passed." |
| 191 | +
|
| 192 | + # ── 12. Teardown ─────────────────────────────────────────────────────── |
| 193 | + - name: Stop services |
| 194 | + if: always() |
| 195 | + run: | |
| 196 | + [ -n "${START_SH_PID:-}" ] && kill "$START_SH_PID" 2>/dev/null || true |
| 197 | + pkill -f "uvicorn" 2>/dev/null || true |
| 198 | + pkill -f "vite" 2>/dev/null || true |
| 199 | + pkill -f "npm" 2>/dev/null || true |
| 200 | + echo "Teardown complete." |
| 201 | +
|
| 202 | + # ── 13. Upload logs on failure ───────────────────────────────────────── |
| 203 | + - name: Upload logs on failure |
| 204 | + if: failure() |
| 205 | + uses: actions/upload-artifact@v4 |
| 206 | + with: |
| 207 | + name: smoke-test-logs |
| 208 | + path: | |
| 209 | + logs/ |
| 210 | + **/*.log |
| 211 | + nohup.out |
| 212 | + if-no-files-found: ignore |
| 213 | + retention-days: 7 |
0 commit comments