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
11 changes: 7 additions & 4 deletions .coderabbit.yaml
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json
language: "ko-KR"
reviews:
profile: "assertive" # 깐깐하게 로직 검토
request_changes_workflow: false # AI가 승인을 막지 않도록 설정
profile: "assertive"
request_changes_workflow: false
high_level_summary: true
review_status: true
review_details: false
poem: false # 불필요한 기능 제거
poem: false
pre_merge_checks:
docstrings:
mode: "off"
auto_review:
enabled: true
drafts: false
base_branches:
- "develop"
- "main"
chat:
auto_reply: true
auto_reply: true
31 changes: 31 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE/release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## 📌 작업 요약
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

최상위 제목 레벨을 H1으로 맞춰 주세요.

Line 1은 ##로 시작해서 markdownlint MD041 경고를 유발합니다. #로 올리는 게 맞습니다.

🧰 Tools
🪛 markdownlint-cli2 (0.22.1)

[warning] 1-1: First line in a file should be a top-level heading

(MD041, first-line-heading, first-line-h1)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/PULL_REQUEST_TEMPLATE/release.md at line 1, 현재 템플릿의 최상위 제목이 '## 📌
작업 요약'으로 되어 있어 markdownlint MD041 경고를 발생시키므로 상단 헤더 토큰을 '##'에서 H1인 '#'로 변경하세요; 즉
파일의 최상위 제목 문자열 '## 📌 작업 요약'을 '# 📌 작업 요약'으로 바꾸고 변경 후 markdownlint/CI에서 MD041
경고가 사라지는지 확인하세요.


- 요약:
- develop 브랜치의 누적 변경사항을 main으로 릴리즈 배포
- 관련 이슈: closes #

## 🌿 브랜치 정보

- **Source**: `develop` (기본)
- **Target**: `main` (릴리즈)

## ✅ 체크리스트

- [ ] 브랜치 컨벤션 준수 (`feat/refac/hotfix/chore/design/bugfix`)
- [ ] 커밋 컨벤션 준수 (`feat/fix/refactor/docs/style/chore`)
- [ ] self-review 완료
- [ ] 테스트 및 로컬 실행 확인 완료

## 🧪 테스트 결과

- GitHub Actions `Deploy AI to EC2` 실행 확인 (`workflow_dispatch`, ref: `main`)
- 결과:
- 스크린샷: ![ssm-send-step]()

- 원격 배포 순서/재기동 확인
- 결과:
- 스크린샷: ![ssm-order]()

- 배포 후 컨테이너 상태 확인
- 결과:
- 스크린샷: ![compose-ps]()
16 changes: 6 additions & 10 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
## 📌 작업 요약
- 요약:

- 요약:
- 관련 이슈: closes #

## 🌿 브랜치 정보

- **Source**: `feat/#이슈번호-기능명`
- **Target**: `develop` (기본) / `main` (릴리즈, 핫픽스)

## 🧩 변경 타입
- [ ] feat: 새로운 기능 추가
- [ ] fix: 버그 수정
- [ ] refactor: 코드 리팩토링
- [ ] docs: 문서 수정
- [ ] style: 코드 포맷팅, 세미콜론 누락 등
- [ ] chore: 빌드 업무, 패키지 매니저 설정 등

## ✅ 체크리스트

- [ ] 브랜치 컨벤션 준수 (`feat/refac/hotfix/chore/design/bugfix`)
- [ ] 커밋 컨벤션 준수 (`feat/fix/refactor/docs/style/chore`)
- [ ] self-review 완료
- [ ] 테스트 및 로컬 실행 확인 완료

## 🧪 테스트 결과
- (테스트 코드 실행 결과 스크린샷이나 로그, 또는 테스트 방법)

- (테스트 코드 실행 결과 스크린샷이나 로그, 또는 테스트 방법)
203 changes: 203 additions & 0 deletions .github/workflows/deploy-ai-ec2.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
name: Deploy AI to EC2

"on":
workflow_run:
workflows: ["AI Docker CI"]
types: [completed]
branches: ["main"]
workflow_dispatch:

permissions:
contents: read
id-token: write

env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

jobs:
deploy:
if: github.event_name == 'workflow_dispatch' || github.event.workflow_run.conclusion == 'success'
runs-on: ubuntu-latest
concurrency:
group: deploy-ai-ec2-ssm
cancel-in-progress: false

steps:
- name: Resolve deploy path
id: deploy-path
env:
EC2_DEPLOY_PATH_SECRET: ${{ secrets.EC2_DEPLOY_PATH }}
run: |
VALUE="${EC2_DEPLOY_PATH_SECRET:-}"
VALUE="${VALUE%$'\r'}"
VALUE="$(printf '%s' "$VALUE" | sed 's/[[:space:]]*$//')"
if [ -n "$VALUE" ]; then
echo "value=$VALUE" >> "$GITHUB_OUTPUT"
else
echo "value=/home/ubuntu/GACHI-BE/deploy" >> "$GITHUB_OUTPUT"
fi

- name: Resolve AWS region
id: aws-region
env:
AWS_REGION_SECRET: ${{ secrets.AWS_REGION }}
run: |
VALUE="${AWS_REGION_SECRET:-}"
VALUE="${VALUE%$'\r'}"
VALUE="$(printf '%s' "$VALUE" | sed 's/[[:space:]]*$//')"
if [ -n "$VALUE" ]; then
echo "value=$VALUE" >> "$GITHUB_OUTPUT"
else
echo "value=ap-northeast-2" >> "$GITHUB_OUTPUT"
fi

- name: Validate deploy inputs
id: auth-check
env:
EC2_INSTANCE_ID: ${{ secrets.EC2_INSTANCE_ID }}
AWS_OIDC_ROLE_ARN: ${{ secrets.AWS_OIDC_ROLE_ARN }}
AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }}
AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
AWS_SESSION_TOKEN: ${{ secrets.AWS_SESSION_TOKEN }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
run: |
set -Eeuo pipefail
if [ -z "${EC2_INSTANCE_ID:-}" ]; then
echo "::error::EC2_INSTANCE_ID secret is required."
exit 1
fi
if [ -z "${DOCKERHUB_USERNAME:-}" ]; then
echo "::error::DOCKERHUB_USERNAME secret is required."
exit 1
fi
if [ -n "${AWS_OIDC_ROLE_ARN:-}" ]; then
echo "auth_mode=oidc" >> "$GITHUB_OUTPUT"
echo "has_session_token=false" >> "$GITHUB_OUTPUT"
else
if [ -z "${AWS_ACCESS_KEY_ID:-}" ] || [ -z "${AWS_SECRET_ACCESS_KEY:-}" ]; then
echo "::error::Set AWS_OIDC_ROLE_ARN or AWS_ACCESS_KEY_ID/AWS_SECRET_ACCESS_KEY."
exit 1
fi
echo "auth_mode=access_key" >> "$GITHUB_OUTPUT"
if [ -n "${AWS_SESSION_TOKEN:-}" ]; then
echo "has_session_token=true" >> "$GITHUB_OUTPUT"
else
echo "has_session_token=false" >> "$GITHUB_OUTPUT"
fi
fi

- name: Configure AWS credentials with OIDC
if: ${{ steps.auth-check.outputs.auth_mode == 'oidc' }}
uses: aws-actions/configure-aws-credentials@v6
with:
aws-region: ${{ steps.aws-region.outputs.value }}
role-to-assume: ${{ secrets.AWS_OIDC_ROLE_ARN }}
role-session-name: gachi-ai-deploy-ssm

- name: Configure AWS credentials with access key
if: ${{ steps.auth-check.outputs.auth_mode == 'access_key' && steps.auth-check.outputs.has_session_token != 'true' }}
uses: aws-actions/configure-aws-credentials@v6
with:
aws-region: ${{ steps.aws-region.outputs.value }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}

- name: Configure AWS credentials with access key session
if: ${{ steps.auth-check.outputs.auth_mode == 'access_key' && steps.auth-check.outputs.has_session_token == 'true' }}
uses: aws-actions/configure-aws-credentials@v6
with:
aws-region: ${{ steps.aws-region.outputs.value }}
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-session-token: ${{ secrets.AWS_SESSION_TOKEN }}

- name: Send AI deploy command via SSM
id: ssm-send
env:
AWS_REGION: ${{ steps.aws-region.outputs.value }}
EC2_INSTANCE_ID: ${{ secrets.EC2_INSTANCE_ID }}
EC2_DEPLOY_PATH: ${{ steps.deploy-path.outputs.value }}
DOCKERHUB_USERNAME: ${{ secrets.DOCKERHUB_USERNAME }}
run: |
set -Eeuo pipefail
COMMANDS="$(jq -cn \
--arg deployPath "$EC2_DEPLOY_PATH" \
--arg image "$DOCKERHUB_USERNAME/gachi-ai:latest" \
'{
commands: [[
"set -Eeuo pipefail",
"cd " + ($deployPath | @sh),
"test -f .env",
"if grep -q ^AI_IMAGE= .env; then sed -i " + ("s|^AI_IMAGE=.*|AI_IMAGE=" + $image + "|" | @sh) + " .env; else echo " + ("AI_IMAGE=" + $image | @sh) + " >> .env; fi",
"docker compose --env-file .env pull ai",
"docker compose --env-file .env up -d --remove-orphans --force-recreate ai",
"AI_CID=$(docker compose --env-file .env ps -q ai)",
"test -n \"$AI_CID\"",
"for i in $(seq 1 24); do AI_HEALTH=$(docker inspect --format '\''{{if .State.Health}}{{.State.Health.Status}}{{else}}{{.State.Status}}{{end}}'\'' \"$AI_CID\" 2>/dev/null || echo missing); echo \"ai health: $AI_HEALTH\"; [ \"$AI_HEALTH\" = healthy ] && break; sleep 5; done",
"test \"$AI_HEALTH\" = healthy",
"docker compose --env-file .env up -d --remove-orphans --force-recreate --no-deps nginx",
"docker compose --env-file .env ps",
"docker compose --env-file .env logs --tail=80 ai nginx || true"
] | join(" && ")],
executionTimeout: ["900"]
}')"
COMMAND_ID="$(aws ssm send-command \
--region "$AWS_REGION" \
--document-name "AWS-RunShellScript" \
--instance-ids "$EC2_INSTANCE_ID" \
--comment "deploy gachi-ai (run=${GITHUB_RUN_ID})" \
--max-concurrency "1" \
--max-errors "0" \
--parameters "$COMMANDS" \
--query "Command.CommandId" \
--output text)"
echo "command_id=$COMMAND_ID" >> "$GITHUB_OUTPUT"

- name: Wait for SSM command completion
env:
AWS_REGION: ${{ steps.aws-region.outputs.value }}
EC2_INSTANCE_ID: ${{ secrets.EC2_INSTANCE_ID }}
COMMAND_ID: ${{ steps.ssm-send.outputs.command_id }}
run: |
set -Eeuo pipefail
for attempt in $(seq 1 90); do
STATUS="$(aws ssm get-command-invocation \
--region "$AWS_REGION" \
--command-id "$COMMAND_ID" \
--instance-id "$EC2_INSTANCE_ID" \
--query "Status" \
--output text 2>/dev/null || true)"
echo "[$attempt/90] status: ${STATUS:-not-ready}"
case "$STATUS" in
Success|Failed|TimedOut|Cancelled|Cancelling)
break
;;
*)
sleep 10
;;
esac
done

INVOCATION_JSON="$(aws ssm get-command-invocation \
--region "$AWS_REGION" \
--command-id "$COMMAND_ID" \
--instance-id "$EC2_INSTANCE_ID" \
--output json)"
FINAL_STATUS="$(printf '%s' "$INVOCATION_JSON" | jq -r '.Status // "Unknown"')"
STDOUT_CONTENT="$(printf '%s' "$INVOCATION_JSON" | jq -r '.StandardOutputContent // ""')"
STDERR_CONTENT="$(printf '%s' "$INVOCATION_JSON" | jq -r '.StandardErrorContent // ""')"

echo "::group::SSM stdout"
printf '%s\n' "$STDOUT_CONTENT"
echo "::endgroup::"

if [ -n "$STDERR_CONTENT" ]; then
echo "::group::SSM stderr"
printf '%s\n' "$STDERR_CONTENT"
echo "::endgroup::"
fi

if [ "$FINAL_STATUS" != "Success" ]; then
echo "::error::SSM command failed. status=$FINAL_STATUS"
exit 1
fi
47 changes: 34 additions & 13 deletions .github/workflows/docker-ai.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
name: AI Docker CI

on:
"on":
pull_request:
branches: ["develop", "main"]
push:
branches: [ "develop", "main" ]
branches: ["develop", "main"]
workflow_dispatch:

permissions:
Expand All @@ -12,7 +14,7 @@ env:
FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: true

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

steps:
Expand All @@ -24,13 +26,16 @@ jobs:
with:
python-version: "3.11"

- name: Install deps
run: pip install -r requirements.txt
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt

- name: Syntax check
run: python -m py_compile app/main.py
- name: Compile app
run: python -m compileall app

- name: Login Docker Hub
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref_name == 'main' || github.ref_name == 'develop')
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
Expand All @@ -39,18 +44,34 @@ jobs:
- name: Set image tags
id: tags
run: |
IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/gachi-ai"
SHORT_SHA="${GITHUB_SHA::7}"
if [ "${GITHUB_REF_NAME}" = "main" ]; then
echo "tags=${IMAGE}:latest,${IMAGE}:sha-${SHORT_SHA}" >> $GITHUB_OUTPUT
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
echo "tags=gachi-ai:pr-${{ github.event.pull_request.number }}" >> "$GITHUB_OUTPUT"
elif [ "${GITHUB_REF_NAME}" = "main" ]; then
IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/gachi-ai"
echo "tags=${IMAGE}:latest,${IMAGE}:sha-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
elif [ "${GITHUB_REF_NAME}" = "develop" ]; then
IMAGE="${{ secrets.DOCKERHUB_USERNAME }}/gachi-ai"
echo "tags=${IMAGE}:develop,${IMAGE}:sha-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
else
echo "tags=${IMAGE}:develop,${IMAGE}:sha-${SHORT_SHA}" >> $GITHUB_OUTPUT
SAFE_REF="$(echo "${GITHUB_REF_NAME}" | tr '/' '-')"
echo "tags=gachi-ai:${SAFE_REF},gachi-ai:sha-${SHORT_SHA}" >> "$GITHUB_OUTPUT"
fi

- name: Build and Push
- name: Build image without push
if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.ref_name != 'main' && github.ref_name != 'develop')
uses: docker/build-push-action@v7
with:
context: .
push: false
tags: ${{ steps.tags.outputs.tags }}
platforms: linux/amd64

- name: Build and push image
if: (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && (github.ref_name == 'main' || github.ref_name == 'develop')
uses: docker/build-push-action@v7
with:
context: .
push: true
tags: ${{ steps.tags.outputs.tags }}
platforms: linux/amd64
platforms: linux/amd64
Loading