ci.yml 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. name: CI
  2. on:
  3. push:
  4. branches: [main]
  5. pull_request:
  6. # Run on all PRs, but skip for repo owner (runs local tests)
  7. # Skip CI for repo owner's PRs (they run tests locally)
  8. # This check is applied to all jobs below
  9. env:
  10. PYTHON_VERSION: '3.11'
  11. NODE_VERSION: '20'
  12. # Cancel in-progress runs for the same branch
  13. concurrency:
  14. group: ${{ github.workflow }}-${{ github.ref }}
  15. cancel-in-progress: true
  16. jobs:
  17. # ============================================================================
  18. # Backend Checks
  19. # ============================================================================
  20. backend-lint:
  21. name: Backend Lint
  22. runs-on: ubuntu-latest
  23. if: github.event_name == 'push' || github.actor != github.repository_owner
  24. steps:
  25. - uses: actions/checkout@v4
  26. - name: Set up Python
  27. uses: actions/setup-python@v5
  28. with:
  29. python-version: ${{ env.PYTHON_VERSION }}
  30. - name: Install ruff
  31. run: pip install ruff
  32. - name: Run ruff check
  33. run: ruff check backend/
  34. - name: Run ruff format check
  35. run: ruff format --check backend/
  36. backend-security:
  37. name: Backend Security
  38. runs-on: ubuntu-latest
  39. if: github.event_name == 'push' || github.actor != github.repository_owner
  40. continue-on-error: true
  41. steps:
  42. - uses: actions/checkout@v4
  43. - name: Set up Python
  44. uses: actions/setup-python@v5
  45. with:
  46. python-version: ${{ env.PYTHON_VERSION }}
  47. - name: Install dependencies
  48. run: |
  49. python -m pip install --upgrade pip
  50. pip install -r requirements.txt
  51. pip install pip-audit
  52. - name: Run pip-audit
  53. run: pip-audit --desc on
  54. backend-tests:
  55. name: Backend Tests
  56. runs-on: ubuntu-latest
  57. if: github.event_name == 'push' || github.actor != github.repository_owner
  58. needs: backend-lint
  59. steps:
  60. - uses: actions/checkout@v4
  61. - name: Set up Python
  62. uses: actions/setup-python@v5
  63. with:
  64. python-version: ${{ env.PYTHON_VERSION }}
  65. - name: Cache pip
  66. uses: actions/cache@v4
  67. with:
  68. path: ~/.cache/pip
  69. key: ${{ runner.os }}-pip-${{ hashFiles('requirements.txt') }}
  70. restore-keys: |
  71. ${{ runner.os }}-pip-
  72. - name: Install dependencies
  73. run: |
  74. python -m pip install --upgrade pip
  75. pip install -r requirements.txt
  76. pip install pytest pytest-asyncio pytest-cov pytest-timeout
  77. - name: Run tests
  78. timeout-minutes: 10
  79. run: |
  80. cd backend
  81. python -m pytest tests/ -v --tb=short --timeout=60 --timeout-method=thread
  82. # ============================================================================
  83. # Frontend Checks
  84. # ============================================================================
  85. frontend-lint:
  86. name: Frontend Lint
  87. runs-on: ubuntu-latest
  88. if: github.event_name == 'push' || github.actor != github.repository_owner
  89. steps:
  90. - uses: actions/checkout@v4
  91. - name: Set up Node.js
  92. uses: actions/setup-node@v4
  93. with:
  94. node-version: ${{ env.NODE_VERSION }}
  95. cache: 'npm'
  96. cache-dependency-path: frontend/package-lock.json
  97. - name: Install dependencies
  98. working-directory: frontend
  99. run: npm ci
  100. - name: Run ESLint
  101. working-directory: frontend
  102. run: npm run lint
  103. frontend-security:
  104. name: Frontend Security
  105. runs-on: ubuntu-latest
  106. if: github.event_name == 'push' || github.actor != github.repository_owner
  107. continue-on-error: true
  108. steps:
  109. - uses: actions/checkout@v4
  110. - name: Set up Node.js
  111. uses: actions/setup-node@v4
  112. with:
  113. node-version: ${{ env.NODE_VERSION }}
  114. cache: 'npm'
  115. cache-dependency-path: frontend/package-lock.json
  116. - name: Install dependencies
  117. working-directory: frontend
  118. run: npm ci
  119. - name: Run npm audit
  120. working-directory: frontend
  121. run: npm audit --audit-level=moderate
  122. frontend-typecheck:
  123. name: Frontend Type Check
  124. runs-on: ubuntu-latest
  125. if: github.event_name == 'push' || github.actor != github.repository_owner
  126. steps:
  127. - uses: actions/checkout@v4
  128. - name: Set up Node.js
  129. uses: actions/setup-node@v4
  130. with:
  131. node-version: ${{ env.NODE_VERSION }}
  132. cache: 'npm'
  133. cache-dependency-path: frontend/package-lock.json
  134. - name: Install dependencies
  135. working-directory: frontend
  136. run: npm ci
  137. - name: Run TypeScript check
  138. working-directory: frontend
  139. run: npx tsc --noEmit
  140. frontend-tests:
  141. name: Frontend Tests
  142. runs-on: ubuntu-latest
  143. if: github.event_name == 'push' || github.actor != github.repository_owner
  144. needs: [frontend-lint, frontend-typecheck]
  145. steps:
  146. - uses: actions/checkout@v4
  147. - name: Set up Node.js
  148. uses: actions/setup-node@v4
  149. with:
  150. node-version: ${{ env.NODE_VERSION }}
  151. cache: 'npm'
  152. cache-dependency-path: frontend/package-lock.json
  153. - name: Install dependencies
  154. working-directory: frontend
  155. run: npm ci
  156. - name: Run tests
  157. timeout-minutes: 10
  158. working-directory: frontend
  159. run: npm run test:run
  160. frontend-build:
  161. name: Frontend Build
  162. runs-on: ubuntu-latest
  163. if: github.event_name == 'push' || github.actor != github.repository_owner
  164. needs: [frontend-tests]
  165. steps:
  166. - uses: actions/checkout@v4
  167. - name: Set up Node.js
  168. uses: actions/setup-node@v4
  169. with:
  170. node-version: ${{ env.NODE_VERSION }}
  171. cache: 'npm'
  172. cache-dependency-path: frontend/package-lock.json
  173. - name: Install dependencies
  174. working-directory: frontend
  175. run: npm ci
  176. - name: Build
  177. working-directory: frontend
  178. run: npm run build
  179. # ============================================================================
  180. # Docker Tests (matches test_docker.sh)
  181. # ============================================================================
  182. docker-test:
  183. name: Docker Build
  184. runs-on: ubuntu-latest
  185. if: github.event_name == 'push' || github.actor != github.repository_owner
  186. timeout-minutes: 20
  187. needs: [backend-tests, frontend-build]
  188. steps:
  189. - uses: actions/checkout@v4
  190. # Test 1: Docker Build
  191. - name: Build production image
  192. run: docker build -t bambuddy:test .
  193. - name: Verify backend imports
  194. run: docker run --rm bambuddy:test python -c "import backend.app.main; print('Backend imports OK')"
  195. - name: Verify static files exist
  196. run: docker run --rm bambuddy:test test -d /app/static
  197. # Test 2: Backend Unit Tests in Docker
  198. - name: Build backend test image
  199. run: docker compose -f docker-compose.test.yml build backend-test
  200. - name: Run backend tests in Docker
  201. run: docker compose -f docker-compose.test.yml run --rm backend-test
  202. # Test 3: Frontend Unit Tests in Docker
  203. - name: Build frontend test image
  204. run: docker compose -f docker-compose.test.yml build frontend-test
  205. - name: Run frontend tests in Docker
  206. run: docker compose -f docker-compose.test.yml run --rm frontend-test
  207. # Test 4: Integration Tests
  208. - name: Build integration container
  209. run: docker compose -f docker-compose.test.yml build integration
  210. - name: Start integration container
  211. run: |
  212. docker compose -f docker-compose.test.yml up -d integration
  213. echo "Waiting for container to be healthy..."
  214. for i in {1..30}; do
  215. if docker compose -f docker-compose.test.yml ps integration | grep -q "healthy"; then
  216. echo "Container is healthy"
  217. break
  218. fi
  219. sleep 2
  220. done
  221. - name: Test health endpoint
  222. run: |
  223. HEALTH=$(docker compose -f docker-compose.test.yml exec -T integration curl -s http://localhost:8000/health)
  224. echo "$HEALTH"
  225. echo "$HEALTH" | grep -q "healthy"
  226. - name: Test API endpoint
  227. run: |
  228. docker compose -f docker-compose.test.yml exec -T integration curl -s http://localhost:8000/api/v1/settings
  229. - name: Test static files served
  230. run: |
  231. STATUS=$(docker compose -f docker-compose.test.yml exec -T integration curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/)
  232. echo "Static files HTTP status: $STATUS"
  233. [ "$STATUS" = "200" ]
  234. - name: Show logs on failure
  235. if: failure()
  236. run: docker compose -f docker-compose.test.yml logs
  237. - name: Cleanup
  238. if: always()
  239. run: docker compose -f docker-compose.test.yml down -v --remove-orphans