Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/gradlew text eol=lf
*.bat text eol=crlf
*.jar binary
126 changes: 126 additions & 0 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
name: Deploy Prod Backend

concurrency:
group: be-prod
cancel-in-progress: true # Deploy 작업은 한번에 하나만 실행하도록 제한

on:
push:
tags:
- 'v*.*.*-be'

jobs:
build-and-push:
runs-on: ubuntu-latest

outputs:
version: ${{ steps.get_version.outputs.VERSION }}

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0 # 태그 검증을 위해 전체 Git 히스토리 필요

- name: tag가 be/prod branch에 있는지 검증
run: |
git fetch origin be/prod
if git merge-base --is-ancestor ${{ github.sha }} origin/be/prod; then
echo "이 태그는 be/prod branch에 포함되어 있습니다. 배포를 진행합니다."
else
echo "Error: 이 tag는 be/prod branch에 없습니다."
exit 1
fi

- name: Git tag에서 버전 정보 추출
id: get_version
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT"

- name: Docker Hub 로그인
run: echo "${{ secrets.DOCKER_ACCESS_TOKEN }}" | docker login -u "${{ secrets.DOCKER_USERNAME }}" --password-stdin

- name: QEMU 설정
uses: docker/setup-qemu-action@v3

- name: Docker Buildx 설정
uses: docker/setup-buildx-action@v3

- name: Docker Image Build & Push
uses: docker/build-push-action@v6
with:
context: ./
file: ./Dockerfile
push: true
platforms: linux/arm64
tags: |
${{ secrets.PROD_IMAGE_NAME }}:${{ steps.get_version.outputs.VERSION }}
${{ secrets.PROD_IMAGE_NAME }}:latest

deploy:
runs-on: [ self-hosted, recycle-study-server-prod ]
needs: build-and-push

steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: .env file 생성
run: |
cat <<EOF > .env
DOCKER_IMAGE=${{ secrets.PROD_IMAGE_NAME }}
DOCKER_TAG=${{ needs.build-and-push.outputs.version }}
${{ secrets.ENV_PROD }}
EOF

- name: log directory 준비
run: |
sudo mkdir -p /app/log
sudo chown -R 1001:1001 /app/log

- name: 배포 시작
run: |
docker compose -f docker-compose.prod.yaml pull
docker compose -f docker-compose.prod.yaml up -d

- name: 신규 deploy에 대한 health check
run: |
echo "=========================================="
echo "Health Check 시작"
echo "=========================================="

echo "애플리케이션 초기화 대기 중... (10초)"
sleep 10

# 최대 12번 시도 (총 60초)
for i in {1..12}; do
STATUS=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8080/actuator/health || echo "000")

if [ "$STATUS" -eq 200 ]; then
echo "=========================================="
echo "배포 성공!"
echo "=========================================="
docker ps --filter "name=recycle-study-server"
exit 0
fi

echo "[$i/12] 헬스체크 재시도... (응답 코드: $STATUS)"
sleep 5
done

# Health Check 실패 시
echo "=========================================="
echo "Health Check 실패"
echo "=========================================="
echo "컨테이너 상태:"
docker ps -a --filter "name=recycle-study-server"
echo ""
echo "컨테이너 로그:"
docker logs recycle-study-server --tail 100 || true

exit 1

- name: Docker resource 정리
if: always()
run: |
docker image prune -a -f
docker builder prune -a -f
67 changes: 67 additions & 0 deletions .github/workflows/test_coverage.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
name: Test Coverage Validation

on:
pull_request:
types:
[ opened, synchronize, reopened ]
branches:
- 'be/**'

jobs:
Build-And-Test:
runs-on: ubuntu-latest

steps:
- name: Github Repository 파일 불러오기
uses: actions/checkout@v6
with:
fetch-depth: 0

- name: Setup Docker
uses: docker/setup-buildx-action@v3

- name: JDK 버전 21 설치
uses: actions/setup-java@v5
with:
distribution: corretto
java-version: 21

- name: Gradle Wrapper 실행 권한 부여
run: chmod +x ./gradlew

- name: 전체 빌드 및 테스트
run: ./gradlew --info test

- name: JaCoCo 커버리지 리포트 생성
id: jacoco_report
uses: madrapps/jacoco-report@v1.7.2
with:
paths: ${{ github.workspace }}/build/reports/jacoco/test/jacocoTestReport.xml
token: ${{ secrets.GITHUB_TOKEN }}
min-coverage-overall: 70
min-coverage-changed-files: 70
title: "🧪 테스트 커버리지 리포트"
update-comment: true

- name: Close PR, if build fail
if: ${{ failure() }}
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const pull_number = ${{ github.event.pull_request.number }}
const updated_title = `[VALIDATION FAIL] ${{ github.event.pull_request.title }}`
await github.rest.pulls.createReview({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pull_number,
body: '빌드 또는 테스트 커버리지 검증에 실패했습니다.',
event: 'REQUEST_CHANGES'
})
await github.rest.pulls.update({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: pull_number,
title: updated_title,
state: 'closed'
})
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -38,3 +38,6 @@ out/

### .env ###
.env

# Generated Swagger UI
src/main/resources/static/docs/
34 changes: 34 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Build stage
FROM amazoncorretto:21-alpine3.19-jdk AS builder

WORKDIR /app

COPY gradlew .
COPY gradle gradle
COPY build.gradle .
COPY settings.gradle .
COPY src src

RUN chmod +x ./gradlew
RUN ./gradlew bootJar --no-daemon

# Runtime stage
FROM amazoncorretto:21-alpine3.19

WORKDIR /app

RUN apk add --no-cache curl tzdata && \
cp /usr/share/zoneinfo/Asia/Seoul /etc/localtime && \
echo "Asia/Seoul" > /etc/timezone && \
apk del tzdata

RUN addgroup -g 1001 appgroup && adduser -u 1001 -G appgroup -D appuser
RUN mkdir -p /app/log && chown -R appuser:appgroup /app

COPY --from=builder --chown=appuser:appgroup /app/build/libs/*.jar app.jar

USER appuser

EXPOSE 8080

ENTRYPOINT ["java", "-jar", "app.jar"]
125 changes: 125 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
plugins {
id 'java'
id 'jacoco'
id 'org.springframework.boot' version '3.5.9'
id 'io.spring.dependency-management' version '1.1.7'
id 'com.epages.restdocs-api-spec' version '0.19.4'
id 'org.hidetake.swagger.generator' version '2.19.2'
}

group = 'com.recyclestudy'
version = '0.0.1-SNAPSHOT'
description = 'Periodic Review Assistant'

java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}

configurations {
compileOnly {
extendsFrom annotationProcessor
}
}

repositories {
mavenCentral()
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-mail'
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
runtimeOnly 'com.mysql:mysql-connector-j'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation 'org.flywaydb:flyway-core'
implementation 'org.flywaydb:flyway-mysql'

// monitoring
implementation 'org.springframework.boot:spring-boot-starter-actuator'
runtimeOnly 'io.micrometer:micrometer-registry-prometheus'

testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'io.rest-assured:rest-assured'
testRuntimeOnly 'com.h2database:h2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

// api docs
testImplementation 'org.springframework.restdocs:spring-restdocs-restassured'
testImplementation 'org.springframework.restdocs:spring-restdocs-mockmvc'
testImplementation 'com.epages:restdocs-api-spec-restassured:0.19.4'
implementation 'com.epages:restdocs-api-spec-mockmvc:0.19.4'

swaggerUI 'org.webjars:swagger-ui:5.30.3'
}

tasks.named('test') {
useJUnitPlatform()
finalizedBy jacocoTestReport
}

jacoco {
toolVersion = "0.8.14"
}

jacocoTestReport {
reports {
html.required = true
xml.required = true
csv.required = true
}

afterEvaluate {
classDirectories.setFrom(files(classDirectories.files.collect {
fileTree(dir: it, exclude: [
"**/*Application*",
"**/common/**",
"**/exception/**",
"**/request/**",
"**/response/**",
"**/input/**",
"**/output/**",
"**/dto/**"
])
}))
}
dependsOn test
}

openapi3 {
servers = [
{ url = "http://localhost:8080" }
]
title = "Recycle-Study Server API Document"
description = "응답 테스트 검증이 완료된 명세서입니다."
version = "0.0.1"
format = "yaml"
}

swaggerSources {
sample {
setInputFile(file("${project.buildDir}/api-spec/openapi3.yaml"))
}
}

tasks.withType(GenerateSwaggerUI).configureEach {
dependsOn 'openapi3'
}

tasks.register('copySwaggerDocument', Copy) {
dependsOn generateSwaggerUI

from file("build/swagger-ui-sample/")
into file("src/main/resources/static/docs")
}

jar {
enabled = false
}

bootJar {
dependsOn copySwaggerDocument
}
11 changes: 11 additions & 0 deletions docker-compose.prod.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
services:
app:
container_name: recycle-study-server
image: ${DOCKER_IMAGE}:${DOCKER_TAG}
restart: unless-stopped
ports:
- "8080:8080"
env_file:
- .env
volumes:
- /app/log:/app/log
Loading