Add structured TODO template for tracking ongoing and planned work #18
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: workflow-name # 워크플로 이름 | |
| on: | |
| push: | |
| branches: # 워크플로 대상 브랜치 | |
| # - main | |
| # - test | |
| env: | |
| PROJECT_NAME: my-project # 프로젝트 명 | |
| DOCKERHUB_REPO: ${{ secrets.DOCKERHUB_USERNAME }} | |
| APP_IMAGE_SUFFIX: "-back" | |
| MAIN_PORT: "8080" | |
| TEST_PORT: "8081" | |
| APP_EXPOSE_PORT: "8080" | |
| MOUNT_DIR: "/volume/project/..." | |
| SPRING_PROFILE_MAIN: "prod" | |
| SPRING_PROFILE_TEST: "prod" | |
| jobs: | |
| build: | |
| if: ${{ !startsWith(github.event.head_commit.message, 'version(') }} # 특정 커밋 메시지 워크플로 실행 제외 | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: 코드 체크아웃 | |
| uses: actions/checkout@v4 | |
| - name: JDK 설정 | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: '21' | |
| distribution: 'temurin' | |
| cache: 'gradle' | |
| - name: Gradle Wrapper 실행권한 부여 | |
| run: chmod +x gradlew | |
| # application-prod.yml 파일을 빌드 전에 생성 | |
| - name: Create application-prod.yml from secret | |
| run: | | |
| cat << 'EOF' > ./src/main/resources/application-prod.yml | |
| ${{ secrets.APPLICATION_PROD_YML }} | |
| EOF | |
| # 브랜치 별 active profile 설정 | |
| - name: Decide active profile | |
| id: profile | |
| run: | | |
| if [ "${GITHUB_REF_NAME}" = "deploy" ]; then | |
| echo "PROFILE=deploy" >> $GITHUB_OUTPUT | |
| else | |
| echo "PROFILE=prod" >> $GITHUB_OUTPUT | |
| fi | |
| - name: Build with Gradle | |
| run: ./gradlew clean build -x test -Dspring.profiles.active=${{ steps.profile.outputs.PROFILE }} | |
| - name: Docker 빌드환경 설정 | |
| uses: docker/setup-buildx-action@v3 | |
| - name: DockerHub 로그인 | |
| uses: docker/login-action@v3 | |
| with: | |
| username: ${{ secrets.DOCKERHUB_USERNAME }} | |
| password: ${{ secrets.DOCKERHUB_TOKEN }} | |
| - name: Cache Docker layers | |
| uses: actions/cache@v4 | |
| with: | |
| path: /tmp/.buildx-cache | |
| key: ${{ runner.os }}-buildx-${{ hashFiles('Dockerfile') }} | |
| restore-keys: | | |
| ${{ runner.os }}-buildx- | |
| - name: Docker 이미지 빌드 및 푸시 | |
| uses: docker/build-push-action@v5 | |
| with: | |
| context: . | |
| file: ./Dockerfile | |
| push: true | |
| tags: ${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}${{ env.APP_IMAGE_SUFFIX }}:${{ github.ref_name }} | |
| cache-from: type=registry,ref=${{ secrets.DOCKERHUB_USERNAME }}/${{ env.PROJECT_NAME }}${{ env.APP_IMAGE_SUFFIX }}:cache | |
| cache-to: type=inline | |
| deploy: | |
| needs: build | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Deploy | |
| uses: appleboy/ssh-action@v1.0.3 | |
| with: | |
| host: ${{ secrets.SERVER_HOST }} | |
| username: ${{ secrets.SERVER_USER }} | |
| password: ${{ secrets.SERVER_PASSWORD }} | |
| port: ${{ secrets.SERVER_PORT }} | |
| script: | | |
| set -e | |
| export PATH=$PATH:/usr/local/bin | |
| echo "환경변수 설정.." | |
| PW="${{ secrets.SERVER_PASSWORD }}" | |
| BRANCH="${{ github.ref_name }}" | |
| REPO="${{ env.DOCKERHUB_REPO }}" | |
| PROJECT="${{ env.PROJECT_NAME }}" | |
| IMG_SUFFIX="${{ env.APP_IMAGE_SUFFIX }}" | |
| IMAGE="${REPO}/${PROJECT}${IMG_SUFFIX}:${BRANCH}" | |
| CONTAINER_NAME="${PROJECT}${IMG_SUFFIX}" | |
| APP_PORT="${{ env.APP_EXPOSE_PORT }}" | |
| MOUNT_DIR="${{ env.MOUNT_DIR }}" | |
| MAIN_PORT="${{ env.MAIN_PORT }}" | |
| TEST_PORT="${{ env.TEST_PORT }}" | |
| PROFILE_MAIN="${{ env.SPRING_PROFILE_MAIN }}" | |
| PROFILE_TEST="${{ env.SPRING_PROFILE_TEST }}" | |
| echo "브랜치=${BRANCH}" | |
| echo "이미지=${IMAGE}" | |
| echo "MAIN_PORT=${MAIN_PORT} | TEST_PORT=${TEST_PORT}" | |
| if [ "${BRANCH}" = "main" ]; then | |
| PORT="${MAIN_PORT}" | |
| PROFILE="${PROFILE_MAIN}" | |
| elif [ "${BRANCH}" = "test" ]; then | |
| CONTAINER_NAME="${CONTAINER_NAME}"-test | |
| PORT=${TEST_PORT} | |
| PROFILE=${PROFILE_TEST} | |
| else | |
| echo "지원하지 않는 브랜치: ${BRANCH}" | |
| exit 1 | |
| fi | |
| echo "브랜치: ${BRANCH}" | |
| echo "컨테이너 이름: ${CONTAINER_NAME}" | |
| echo "포트: ${PORT}" | |
| echo "활성 프로필: ${PROFILE}" | |
| echo "도커 이미지 풀 : ${IMAGE}" | |
| echo $PW | sudo -S docker pull "${IMAGE}" | |
| echo "컨테이너 ${CONTAINER_NAME} 존재 여부 확인 중..." | |
| if sudo docker ps -a --format '{{.Names}}' | grep -Eq "^${CONTAINER_NAME}\$"; then | |
| echo "컨테이너 ${CONTAINER_NAME} 이(가) 존재합니다. 중지 및 삭제 중..." | |
| echo $PW | sudo -S docker rm -f ${CONTAINER_NAME} | |
| echo "컨테이너 ${CONTAINER_NAME} 이(가) 삭제되었습니다." | |
| else | |
| echo "존재하는 컨테이너 ${CONTAINER_NAME} 이(가) 없습니다." | |
| fi | |
| echo "새로운 컨테이너 ${CONTAINER_NAME} 실행 중..." | |
| echo $PW | sudo -S docker run -d -p "${PORT}":"${APP_PORT}" --name "${CONTAINER_NAME}" \ | |
| -e TZ=Asia/Seoul \ | |
| -e "SPRING_PROFILES_ACTIVE=${PROFILE}" \ | |
| -v /etc/localtime:/etc/localtime:ro \ | |
| -v "${MOUNT_DIR}":/app \ | |
| "${IMAGE}" | |
| echo "[main] 헬스체크 (최대 120회, 1초간격)" | |
| for i in $(seq 1 120); do | |
| if curl -fsS "http://127.0.0.1:${PORT}/actuator/health" >/dev/null 2>&1 || \ | |
| curl -fsS "http://127.0.0.1:${PORT}/healthz" >/dev/null 2>&1 || \ | |
| curl -fsS "http://127.0.0.1:${PORT}/" >/dev/null 2>&1 || \ | |
| curl -fsS "http://127.0.0.1:${PORT}/docs/swagger-ui/index.html" >/dev/null 2>&1 ; then | |
| echo "헬스체크 성공 (시도 ${i})" | |
| HEALTH_OK=1 | |
| break | |
| fi | |
| echo "헬스체크 진행중... (시도 ${i}/120)" | |
| sleep 1 | |
| done | |
| if [ "${HEALTH_OK:-0}" != "1" ]; then | |
| echo "[오류] 헬스체크 실패 -> 배포 중단, 로그 출력" | |
| echo "$PW" | sudo -S docker logs --tail=200 ${CONTAINER_NAME} | |
| exit 1 | |
| fi | |
| # <none> 태그로 남은 이미지 정리 | |
| echo "불필요한 dangling(<none>) 이미지 정리..." | |
| echo $PW | sudo -S docker image prune -af | |
| echo "배포가 성공적으로 완료되었습니다." |