removed if (applicationForm.value.questions.length >= 5) return #71
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Docker Setup Integration Test | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened, ready_for_review] | |
| push: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: docker-setup-integration-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| integration: | |
| name: Simulate new-user setup (setup.sh → docker compose up) | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 20 | |
| steps: | |
| # ── 1. Clone ───────────────────────────────────────────────────────────── | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| # ── 2. setup.sh ────────────────────────────────────────────────────────── | |
| - name: Run setup.sh (new-user step 1) | |
| run: | | |
| chmod +x ./setup.sh | |
| ./setup.sh | |
| - name: Verify .env was generated with all required keys | |
| run: | | |
| set -euo pipefail | |
| required=( | |
| BETTER_AUTH_SECRET | |
| BETTER_AUTH_URL | |
| DATABASE_URL | |
| DB_USER | |
| DB_PASSWORD | |
| DB_NAME | |
| S3_ENDPOINT | |
| S3_ACCESS_KEY | |
| S3_SECRET_KEY | |
| S3_BUCKET | |
| STORAGE_USER | |
| STORAGE_PASSWORD | |
| ) | |
| missing=() | |
| for key in "${required[@]}"; do | |
| if ! grep -q "^${key}=" .env; then | |
| missing+=("$key") | |
| fi | |
| done | |
| if [ ${#missing[@]} -gt 0 ]; then | |
| echo "❌ Missing keys in .env: ${missing[*]}" | |
| exit 1 | |
| fi | |
| echo "✅ All required keys present in .env" | |
| - name: Verify STORAGE_PASSWORD and S3_SECRET_KEY match (prevents credential mismatch bug) | |
| run: | | |
| set -euo pipefail | |
| storage_pass="$(grep '^STORAGE_PASSWORD=' .env | cut -d= -f2-)" | |
| s3_key="$(grep '^S3_SECRET_KEY=' .env | cut -d= -f2-)" | |
| if [ "$storage_pass" != "$s3_key" ]; then | |
| echo "❌ STORAGE_PASSWORD and S3_SECRET_KEY do not match — MinIO uploads will fail" | |
| exit 1 | |
| fi | |
| echo "✅ MinIO credentials are consistent" | |
| - name: Verify BETTER_AUTH_SECRET is at least 32 characters | |
| run: | | |
| set -euo pipefail | |
| secret="$(grep '^BETTER_AUTH_SECRET=' .env | cut -d= -f2-)" | |
| len="${#secret}" | |
| if [ "$len" -lt 32 ]; then | |
| echo "❌ BETTER_AUTH_SECRET is only ${len} chars (minimum 32)" | |
| exit 1 | |
| fi | |
| echo "✅ BETTER_AUTH_SECRET is ${len} chars" | |
| - name: Verify setup.sh refuses to overwrite existing .env | |
| run: | | |
| set -euo pipefail | |
| if ./setup.sh 2>&1; then | |
| echo "❌ setup.sh should have refused to overwrite .env but exited 0" | |
| exit 1 | |
| fi | |
| echo "✅ setup.sh correctly refused to overwrite existing .env" | |
| # ── 3. docker compose up --build ───────────────────────────────────────── | |
| - name: Build image and start all services (new-user step 2) | |
| run: docker compose up --build -d | |
| - name: Wait for db to be healthy | |
| run: | | |
| set -euo pipefail | |
| echo "Waiting for db..." | |
| for i in $(seq 60); do | |
| state="$(docker inspect --format='{{.State.Health.Status}}' reqcore_db 2>/dev/null || echo 'not-started')" | |
| echo " db: $state" | |
| [ "$state" = "healthy" ] && break | |
| if [ "$i" -eq 60 ]; then | |
| echo "❌ db did not become healthy in time" | |
| docker compose logs db --tail=50 | |
| exit 1 | |
| fi | |
| sleep 3 | |
| done | |
| echo "✅ db is healthy" | |
| - name: Wait for minio to be healthy | |
| run: | | |
| set -euo pipefail | |
| echo "Waiting for minio..." | |
| for i in $(seq 60); do | |
| state="$(docker inspect --format='{{.State.Health.Status}}' reqcore_minio 2>/dev/null || echo 'not-started')" | |
| echo " minio: $state" | |
| [ "$state" = "healthy" ] && break | |
| if [ "$i" -eq 60 ]; then | |
| echo "❌ minio did not become healthy in time" | |
| docker compose logs minio --tail=50 | |
| exit 1 | |
| fi | |
| sleep 3 | |
| done | |
| echo "✅ minio is healthy" | |
| - name: Wait for app to be reachable on :3000 | |
| run: | | |
| set -euo pipefail | |
| echo "Waiting for app..." | |
| for i in $(seq 60); do | |
| if curl -fs http://localhost:3000 > /dev/null 2>&1; then | |
| echo "✅ App is reachable on http://localhost:3000" | |
| exit 0 | |
| fi | |
| state="$(docker inspect --format='{{.State.Status}}' reqcore_app 2>/dev/null || echo 'missing')" | |
| if [ "$state" = "exited" ] || [ "$state" = "dead" ]; then | |
| echo "❌ App container exited unexpectedly" | |
| docker compose logs app --tail=100 | |
| exit 1 | |
| fi | |
| echo " attempt $i/60 — waiting..." | |
| sleep 3 | |
| done | |
| echo "❌ App did not become reachable in time" | |
| docker compose logs app --tail=100 | |
| exit 1 | |
| # ── 4. Startup log assertions ───────────────────────────────────────────── | |
| - name: Assert DB migrations ran successfully | |
| run: | | |
| set -euo pipefail | |
| if ! docker compose logs app | grep -q "Database migrations applied successfully"; then | |
| echo "❌ Migration success message not found in app logs" | |
| docker compose logs app | |
| exit 1 | |
| fi | |
| echo "✅ Migrations applied successfully" | |
| - name: Assert S3 bucket is ready | |
| run: | | |
| set -euo pipefail | |
| if ! docker compose logs app | grep -q 'S3 bucket "reqcore" is ready'; then | |
| echo "❌ S3 bucket ready message not found in app logs" | |
| docker compose logs app | |
| exit 1 | |
| fi | |
| echo "✅ S3 bucket is ready" | |
| # ── 5. HTTP smoke tests ─────────────────────────────────────────────────── | |
| - name: HTTP smoke tests | |
| run: | | |
| set -euo pipefail | |
| fail=0 | |
| check() { | |
| local label="$1" url="$2" expected="$3" | |
| local actual | |
| actual="$(curl -s -o /dev/null -w "%{http_code}" "$url")" | |
| if [ "$actual" = "$expected" ]; then | |
| echo "✅ $label → $actual" | |
| else | |
| echo "❌ $label → expected $expected, got $actual" | |
| fail=1 | |
| fi | |
| } | |
| check "Home page" "http://localhost:3000" "200" | |
| check "Sign-in page" "http://localhost:3000/auth/sign-in" "200" | |
| check "Sign-up page" "http://localhost:3000/auth/sign-up" "200" | |
| check "Public job board" "http://localhost:3000/jobs" "200" | |
| check "Blog index" "http://localhost:3000/blog" "200" | |
| check "API/jobs (no auth→401)" "http://localhost:3000/api/jobs" "401" | |
| check "API/candidates (no auth)" "http://localhost:3000/api/candidates" "401" | |
| exit $fail | |
| # ── 6. Seed command (optional step from README) ─────────────────────────── | |
| - name: Seed demo data (docker compose exec app npm run db:seed) | |
| run: | | |
| set -euo pipefail | |
| output="$(docker compose exec app npm run db:seed 2>&1)" | |
| echo "$output" | |
| if ! echo "$output" | grep -q "Seed complete"; then | |
| echo "❌ Seed did not complete successfully" | |
| exit 1 | |
| fi | |
| echo "✅ Seed completed" | |
| - name: Sign in with seeded demo account (auth smoke test) | |
| run: | | |
| set -euo pipefail | |
| response="$(curl -s -X POST http://localhost:3000/api/auth/sign-in/email \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"email":"demo@reqcore.com","password":"demo1234"}' \ | |
| -w "\n%{http_code}")" | |
| body="$(echo "$response" | head -n -1)" | |
| status="$(echo "$response" | tail -n 1)" | |
| echo "Status: $status" | |
| echo "Body: $body" | |
| if [ "$status" != "200" ]; then | |
| echo "❌ Sign-in failed — expected 200, got $status" | |
| exit 1 | |
| fi | |
| if ! echo "$body" | grep -q "demo@reqcore.com"; then | |
| echo "❌ Response body does not contain expected email" | |
| exit 1 | |
| fi | |
| echo "✅ Demo account sign-in succeeded" | |
| - name: Seed idempotency — re-running seed must not crash | |
| run: | | |
| set -euo pipefail | |
| output="$(docker compose exec app npm run db:seed 2>&1)" | |
| echo "$output" | |
| if echo "$output" | grep -qi "^npm error\|unhandledRejection\|UnhandledPromiseRejection"; then | |
| echo "❌ Seed second run produced an error" | |
| exit 1 | |
| fi | |
| echo "✅ Seed is idempotent" | |
| # ── 7. Adminer --profile tools ──────────────────────────────────────────── | |
| - name: Start Adminer via --profile tools | |
| run: docker compose --profile tools up -d adminer | |
| - name: Wait for Adminer to respond on :8080 | |
| run: | | |
| set -euo pipefail | |
| for i in $(seq 20); do | |
| if curl -fs http://localhost:8080 > /dev/null 2>&1; then | |
| echo "✅ Adminer is reachable on http://localhost:8080" | |
| exit 0 | |
| fi | |
| sleep 2 | |
| done | |
| echo "❌ Adminer did not become reachable" | |
| docker compose logs adminer --tail=30 | |
| exit 1 | |
| # ── 8. Restart resilience (migrations must be idempotent) ──────────────── | |
| - name: Restart app and verify it comes back clean | |
| run: | | |
| set -euo pipefail | |
| docker compose restart app | |
| for i in $(seq 30); do | |
| if curl -fs http://localhost:3000 > /dev/null 2>&1; then | |
| echo "✅ App reachable again after restart" | |
| break | |
| fi | |
| if [ "$i" -eq 30 ]; then | |
| echo "❌ App did not come back after restart" | |
| docker compose logs app --tail=50 | |
| exit 1 | |
| fi | |
| sleep 3 | |
| done | |
| if docker compose logs app | grep -q "Migration failed"; then | |
| echo "❌ Migration error found in logs after restart" | |
| docker compose logs app | |
| exit 1 | |
| fi | |
| echo "✅ Restart handled cleanly — no migration errors" | |
| # ── Always: dump logs on failure ────────────────────────────────────────── | |
| - name: Dump all service logs | |
| if: always() | |
| run: | | |
| echo "=== docker compose ps ===" | |
| docker compose --profile tools ps || true | |
| echo "" | |
| echo "=== app ===" | |
| docker compose logs app --no-color --tail=200 || true | |
| echo "" | |
| echo "=== db ===" | |
| docker compose logs db --no-color --tail=50 || true | |
| echo "" | |
| echo "=== minio ===" | |
| docker compose logs minio --no-color --tail=50 || true | |
| - name: Tear down all services and volumes | |
| if: always() | |
| run: docker compose --profile tools down -v || true |