# CI — Gitea Actions (push 시 자동 테스트) > push/PR마다 백엔드 테스트 + 프론트 빌드를 자동으로 돌려, 회귀를 배포 전에 잡는다. > 워크플로 정의: [`.gitea/workflows/ci.yml`](../.gitea/workflows/ci.yml) ## 구성 요소 | 요소 | 위치 | 역할 | |---|---|---| | Actions 활성화 | `docker-compose.prod.yml` gitea env `GITEA__actions__ENABLED=true` | Gitea에 Actions 기능 켜기 | | 러너(runner) | compose 서비스 `runner`(`gitea/act_runner`) | 일감을 받아 job 컨테이너를 띄우는 일꾼 | | 러너 설정·등록 | 호스트 `/opt/mirim-runner/` (`config.yaml`, `.runner`) | 최초 등록 정보·job 컨테이너 네트워크 설정 | | 워크플로 | `.gitea/workflows/ci.yml` | 무엇을 검사할지(백엔드 test, 프론트 build) | ## 동작 흐름 1. 누가 push하면 Gitea가 워크플로를 큐에 넣는다. 2. 러너가 job을 받아 `catthehacker/ubuntu:act-22.04` 이미지로 job 컨테이너를 띄운다. 3. job 컨테이너는 `deploy_default` 네트워크 + `--add-host edu.awesomedevapp.com:host-gateway`로 Gitea에서 코드를 clone한 뒤, 백엔드 `mvn test` / 프론트 `npm ci && npm run build`를 실행한다. 4. 하나라도 실패하면 커밋에 빨간 X, 다 통과하면 초록 체크가 붙는다. (Gitea 웹 → Actions 탭) ## 러너를 처음부터 다시 세워야 한다면 ```bash # 1) Actions 활성화 (compose env, 이미 되어 있음) 후 gitea 재기동 # 2) 등록 토큰 발급 sudo docker exec -u git mirim-gitea gitea --config /data/gitea/conf/app.ini actions generate-runner-token # 3) 설정 파일 (/opt/mirim-runner/config.yaml) # labels: ubuntu-latest → catthehacker/ubuntu:act-22.04 # container.network: deploy_default # container.options: --add-host=edu.awesomedevapp.com:host-gateway ← job이 Gitea에 clone하려면 필수 # 4) 최초 1회만 토큰으로 등록 (등록되면 .runner 생성, 이후 compose가 관리) sudo docker run --rm --network deploy_default \ -v /var/run/docker.sock:/var/run/docker.sock -v /opt/mirim-runner:/data -w /data \ -e GITEA_INSTANCE_URL=http://mirim-gitea:3000 \ -e GITEA_RUNNER_REGISTRATION_TOKEN=<토큰> -e CONFIG_FILE=/data/config.yaml \ gitea/act_runner:0.2.11 # 5) compose로 상시 실행 sudo docker compose -f deploy/docker-compose.prod.yml up -d runner ``` ## 함정 메모 (겪은 것) - **Maven 다운로드**: dlcdn 미러는 옛 버전을 지운다(3.9.9가 404로 사라져 CI가 깨졌었다). → 모든 버전을 영구 보관하는 `archive.apache.org`를 쓴다. - **clone 실패**: job 컨테이너가 Gitea(`edu.awesomedevapp.com:3000`)에 못 닿으면 checkout이 실패한다. → `--add-host edu.awesomedevapp.com:host-gateway`로 컨테이너에서 호스트(→ 게시된 3000)로 잇는다. - **actions/checkout은 node 필요**: job 이미지에 node가 있어야 한다(catthehacker 이미지에 포함). 그래서 백엔드 job도 이 이미지를 쓰고 Maven만 따로 내려받는다. ## 배포 스모크 테스트 (`deploy/smoke-test.sh`) CI는 "코드"를 배포 전에 검사한다. 하지만 실제로 났던 사고(보안헤더로 학습 문서 iframe이 안 뜬 것)는 코드가 아니라 **Caddy 설정** 문제라 CI 테스트로는 안 잡혔다. 스모크 테스트는 배포된 **살아있는 사이트**를 실제 사용자처럼 두드려, 설정·헤더·인프라까지 포함해 "정말 열리는가"를 확인한다. → CI(코드 그물) + 스모크(설정·운영 그물). ```bash # 배포 직후 반드시 실행 — 하나라도 실패하면 exit 1 bash deploy/smoke-test.sh ``` 검사 항목(11): 홈 200 · HTTP→HTTPS 리다이렉트 · 보안헤더 3종 · **학습 문서 200 + X-Frame-Options=SAMEORIGIN + no-cache + 한국어 본문**(그 회귀를 직접 감지) · 백엔드 API 401(Caddy→백엔드→DB 경로) · SPA 라우트 폴백. > **권장 배포 흐름**: 빌드 → 업로드 → `docker compose up` → **`bash deploy/smoke-test.sh`**. > 스모크가 빨간불이면 배포에 문제가 있는 것이니 바로 롤백/수정한다. ## 품질 게이트 (push/PR마다 자동 검사) CI가 통과해야 하는 관문이 아래처럼 늘었다. **로컬에서 먼저 돌려 보고 밀면** 빨간불을 피한다. | 게이트 | 무엇을 막나 | 로컬에서 고치는 법 | |---|---|---| | Spotless(백엔드) | 안 쓰는 import·줄끝 공백·파일끝 개행 | `cd backend && mvn spotless:apply` | | JaCoCo(백엔드) | 라인 커버리지 30% 미만으로 **후퇴** | 테스트 추가 (`mvn test` 후 `target/site/jacoco/index.html`) | | ESLint(프론트) | 안 쓰는 변수·없는 전역·React 훅 오용 | `cd frontend && npm run lint` | | 커버리지(프론트) | 채점 로직 `runSql.js` 40% 미만 | `npm run coverage` 로 확인 후 테스트 보강 | > 커버리지 바닥(30%/40%)은 "지금보다 나빠지지 마라"는 **래칫(ratchet)**이다. > 테스트를 늘리면 이 숫자도 함께 올려 바닥을 끌어올리는 게 건강한 흐름. ## 의존성 취약점 스캔 (라이브러리 CVE) 우리가 직접 쓴 코드뿐 아니라 "가져다 쓰는 라이브러리"에도 취약점이 생긴다. CI가 이를 훑는다. | 대상 | 도구 | 방식 | |---|---|---| | 프론트(npm) | `npm audit --omit=dev --audit-level=high` | 온디맨드(대용량 DB 없음) | | 백엔드(Maven) | OSV-Scanner v2.4.0, `scan source -r backend` | osv.dev 온디맨드 질의 | **둘 다 비차단(`continue-on-error`)으로 "보고 전용" 시작한다.** 이유: 이 스캔은 "코드"가 아니라 "외부 권고 DB 상태"에 결합돼, 우리가 한 줄도 안 고쳐도 새 CVE가 공개되면 차단형이면 무관한 PR까지 전부 CI가 붉어진다. 팀이 수용목록(allow-list) 운영에 익숙해지면 차단으로 승격한다. > ✅ **첫 스캔이 값어치를 증명하고, 바로 해결됐다**: 백엔드 OSV 스캔이 Spring Boot 3.4.2의 > 누적 CVE(tomcat-embed-core·spring-security-* 등, 일부 CVSS 9점대)를 다수 찾아냈다. > **Spring Boot 3.4.2 → 3.5.16 업그레이드로 전부 해결**(tomcat 10.1.34→10.1.55, > spring-security 6.4.2→6.5.11, spring-web 6.2.2→6.2.19 등 취약 의존성 모두 수정버전 이상). > "스캔으로 찾고 → 버전 상향으로 고치고 → 재스캔/테스트로 확인"의 한 사이클 예시. ## main 브랜치 보호 (PR 워크플로) `main`은 보호 규칙이 걸려 있다(Gitea → 저장소 설정 → 브랜치): - **CI 두 개(backend-test·frontend-build)가 초록불이어야** merge 가능 - **PR에 승인 1개 필요** (멘토가 학생 PR을 리뷰·승인) - 학생은 `main`에 직접 push 불가 → **브랜치 만들어 PR** 여는 흐름을 익힌다 - 운영·배포용으로 관리자 계정만 직접 push 허용(화이트리스트) 학생 실습 흐름: `git switch -c feat/내작업` → 커밋 → push → Gitea에서 PR 생성 → CI 초록불 + 멘토 승인 → merge. (실무 표준 흐름을 그대로 경험) ## E2E 테스트 (`deploy/e2e.sh`) 단위/컴포넌트 테스트가 "부품"을 본다면, E2E는 **다 조립된 앱을 진짜 브라우저로** 눌러 본다 — 프론트+백엔드+DB가 함께 도는 통합 경로를 태워 "연결 지점의 회귀"를 잡는다(Playwright). ```bash bash deploy/e2e.sh # 격리 임시 스택 기동 → 테스트 → down -v(정리) ``` 검사하는 여정(6): 인증 가드(비로그인→로그인) · 로그인 실패 안내 · 학생 로그인→대시보드 · 학습 센터→강좌 · **퀴즈 풀기→제출→채점** · 멘토 로그인→화면+감사로그. **설계 — 운영 무오염**: 별도 프로젝트(`mirime2e`)·네트워크·**휘발성 DB(tmpfs)** 로 완전히 격리된 임시 스택을 띄우고(호스트 포트 8899), 테스트 학생을 API로 만들고, 브라우저 내장 이미지 (`mcr.microsoft.com/playwright`)로 돌린 뒤 통째로 내린다. 운영 DB/컨테이너는 건드리지 않는다. > **왜 push마다 CI로 안 돌리나?** 러너가 운영과 같은 작은 인스턴스(3.7GB)에 있어, 매 push마다 > 두 번째 풀스택+브라우저를 띄우면 운영 메모리를 압박할 수 있다. 그래서 E2E는 **릴리스 전/필요 시 > 온디맨드**로 돌린다(백엔드 힙도 256MB로 제한). 전용 러너가 생기면 그때 CI 게이트로 승격한다. ## 다음 개선 (학생 티켓) - Maven·npm 캐시로 CI 실행 시간 단축 - 커버리지 바닥을 단계적으로 상향(30%→40%→…) - 스모크 테스트를 배포 스크립트에 자동 편입(deploy 후 자동 실행·실패 시 알림) - React 컴포넌트 테스트(Testing Library) — 프론트 커버리지 확장 - 전용 러너 확보 시 E2E를 CI 게이트로 승격(현재는 온디맨드 `deploy/e2e.sh`)