ci.yml 9.0 KB

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