From 9e3d2a541bc6447ada99ba309d4225dc398796f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=91=EB=AF=BC?= Date: Sat, 10 Jan 2026 01:48:16 -0700 Subject: [PATCH 01/11] fix: Update entrypoint script and Jenkinsfile for improved Docker commands and server startup --- Jenkinsfile | 15 +++++++-------- entrypoint.sh | 4 ++-- 2 files changed, 9 insertions(+), 10 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 2f5e518..6ce3b62 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -61,14 +61,13 @@ pipeline { steps { echo "Determining colors.." script { - def blueRunning = sh( - script: "docker ps -aq -f 'name=workinkorea-server-blue'", + script: "docker ps -q -f 'name=workinkorea-server-blue'", returnStdout: true ).trim() def greenRunning = sh( - script: "docker ps -aq -f 'name=workinkorea-server-green'", + script: "docker ps -q -f 'name=workinkorea-server-green'", returnStdout: true ).trim() @@ -157,7 +156,7 @@ pipeline { echo "Health checking.." script { def healthCheck = sh( - script: "docker inspect -f '{{.State.Running}}' ${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR}", + script: "docker inspect -f '{{.State.Running}}' ${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR} 2>/dev/null || echo 'false'", returnStdout: true ).trim() @@ -285,8 +284,8 @@ pipeline { script { sh """ docker stop ${env.DOCKER_IMAGE_NAME}-${env.COLOR} || true - docker container prune -f || true - docker image prune -f || true + docker rm ${env.DOCKER_IMAGE_NAME}-${env.COLOR} || true + docker rmi ${env.DOCKER_IMAGE_NAME}-${env.COLOR} || true """ } discordSend description: "${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR} deployed successfully", @@ -301,8 +300,8 @@ pipeline { try{ sh """ docker stop ${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR} || true - docker container prune -f || true - docker image prune -f || true + docker rm ${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR} || true + docker rmi ${env.DOCKER_IMAGE_NAME}-${env.NEW_COLOR} || true """ if (env.COLOR != "none") { echo "Rolling back to ${env.COLOR} container..." diff --git a/entrypoint.sh b/entrypoint.sh index 7b9361b..4c9e8cd 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -2,7 +2,7 @@ set -e # database migration -uv run alembic upgrade head +# uv run alembic upgrade head # start server -exec uv run uvicorn app.main:app --host 0.0.0.0 --port "$PORT" \ No newline at end of file +uv run uvicorn app.main:app --host 0.0.0.0 --port "${PORT:-8000}" \ No newline at end of file From 6c9986c45389ffc15baad580d6a94d4a11c92ee5 Mon Sep 17 00:00:00 2001 From: mhd329 Date: Thu, 15 Jan 2026 22:59:08 +0900 Subject: [PATCH 02/11] =?UTF-8?q?feat:=20=EC=BF=A0=ED=82=A4=EB=A5=BC=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EC=97=AC=20=EC=95=A1=EC=84=B8?= =?UTF-8?q?=EC=8A=A4=20=ED=86=A0=ED=81=B0=20=EB=B0=8F=20=EB=A6=AC=ED=94=84?= =?UTF-8?q?=EB=A0=88=EC=8B=9C=20=ED=86=A0=ED=81=B0=20=EC=A0=84=EB=8B=AC=20?= =?UTF-8?q?=EB=B0=A9=EC=8B=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/auth/router.py | 40 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/app/auth/router.py b/app/auth/router.py index 2d3aa33..a4687f3 100644 --- a/app/auth/router.py +++ b/app/auth/router.py @@ -135,7 +135,7 @@ async def login_google_callback( # 파라미터 user name, access token 저장 status_massage_dict["user_id"] = user.id status_massage_dict["name"] = user.profile.name - status_massage_dict["token"] = access_token + # status_massage_dict["token"] = access_token -> 토큰은 이제 set cookie 로 전달댐 # jwt refresh token redis 저장 refresh_token_obj = await auth_redis_service.set_refresh_token(refresh_token, user.email) @@ -148,6 +148,15 @@ async def login_google_callback( # jwt token 쿠키에 저장 success_url = f"{SETTINGS.CLIENT_URL}/auth/callback?{urlencode(status_massage_dict)}" response = RedirectResponse(url=success_url) + response.set_cookie( + key="access_token", + value=access_token, + httponly=True, + secure=False, # 개발 환경에서는 secure=False + max_age=SETTINGS.ACCESS_TOKEN_EXPIRE_MINUTES, + samesite="lax", + domain=SETTINGS.COOKIE_DOMAIN + ) response.set_cookie( key="refresh_token", value=refresh_token, @@ -236,6 +245,10 @@ async def logout(request: Request, auth_redis_service: AuthRedisService = Depend key="refresh_token", domain=SETTINGS.COOKIE_DOMAIN ) + response.delete_cookie( + key="access_token", + domain=SETTINGS.COOKIE_DOMAIN + ) return response except Exception as e: return {"error": str(e)} @@ -305,7 +318,19 @@ async def refresh(request: Request, if not access_token: return JSONResponse(content={"message": "Failed to create access token"}, status_code=500) - return JSONResponse(content={"access_token": access_token, "token_type": token_type}) + # return JSONResponse(content={"access_token": access_token, "token_type": token_type}) + response = JSONResponse(content={"success": True}, status_code=200) + response.set_cookie( + key="access_token", + value=access_token, + httponly=True, + secure=False, + max_age=SETTINGS.ACCESS_TOKEN_EXPIRE_MINUTES, + samesite="lax", + domain=SETTINGS.COOKIE_DOMAIN + ) + return response + except Exception as e: return JSONResponse(content={"error": str(e)}, status_code=500) @@ -435,11 +460,20 @@ async def company_login(form_data: OAuth2PasswordRequestForm = Depends(), status_massage_dict = { "user_id": company_user.id, "company_id": company_user.company_id, - "token": access_company_token, + # "token": access_company_token, -> 이제 쿠키로 감 } url = f"{SETTINGS.CLIENT_URL}/company?{urlencode(status_massage_dict)}" response = JSONResponse(content={"url": url}) + response.set_cookie( + key="access_token", + value=access_company_token, + httponly=True, + secure=False, # 개발 환경에서는 secure=False + max_age=SETTINGS.ACCESS_TOKEN_EXPIRE_MINUTES, + samesite="lax", + domain=SETTINGS.COOKIE_DOMAIN + ) response.set_cookie( key="refresh_token", value=refresh_company_token, From b06f83b272f6b1d9c4f07e3020ad026459ce461b Mon Sep 17 00:00:00 2001 From: mhd329 Date: Thu, 15 Jan 2026 23:46:09 +0900 Subject: [PATCH 03/11] fixed invalid param --- app/auth/router.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/auth/router.py b/app/auth/router.py index a4687f3..55a4139 100644 --- a/app/auth/router.py +++ b/app/auth/router.py @@ -135,7 +135,7 @@ async def login_google_callback( # 파라미터 user name, access token 저장 status_massage_dict["user_id"] = user.id status_massage_dict["name"] = user.profile.name - # status_massage_dict["token"] = access_token -> 토큰은 이제 set cookie 로 전달댐 + status_massage_dict["token"] = access_token # jwt refresh token redis 저장 refresh_token_obj = await auth_redis_service.set_refresh_token(refresh_token, user.email) From 2fb41301f32d3c7a74fcb523e96ffa32834c6e6b Mon Sep 17 00:00:00 2001 From: mhd329 Date: Sat, 17 Jan 2026 05:11:10 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EC=9A=94=EC=B2=AD=EC=97=90?= =?UTF-8?q?=EC=84=9C=20=EC=BF=A0=ED=82=A4=EB=A5=BC=20=ED=86=B5=ED=95=B4=20?= =?UTF-8?q?=EC=95=A1=EC=84=B8=EC=8A=A4=20=ED=86=A0=ED=81=B0=EC=9D=84=20?= =?UTF-8?q?=EA=B0=80=EC=A0=B8=EC=98=A4=EB=8A=94=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/dependencies.py | 13 ++++++++++--- app/auth/dependencies.py | 11 +++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/app/admin/dependencies.py b/app/admin/dependencies.py index 464e1bd..aa2cd77 100644 --- a/app/admin/dependencies.py +++ b/app/admin/dependencies.py @@ -1,6 +1,6 @@ import jwt from app.auth.models import User -from fastapi import Depends, HTTPException, status +from fastapi import Depends, HTTPException, status, Request from app.core.settings import SETTINGS from app.database import get_async_session from sqlalchemy.ext.asyncio import AsyncSession @@ -22,7 +22,8 @@ def get_auth_repository( async def get_admin_user( - credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer()), + request: Request, + credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False)), auth_repository: AuthRepository = Depends(get_auth_repository) ) -> User: """ @@ -36,7 +37,13 @@ async def get_admin_user( raises: HTTPException """ - access_token = credentials.credentials + access_token = None + if credentials: + access_token = credentials.credentials + + if not access_token: + access_token = request.cookies.get("access_token") + env_admin_emails: str = SETTINGS.ADMIN_EMAILS if not access_token: diff --git a/app/auth/dependencies.py b/app/auth/dependencies.py index a8f79d1..282ca7e 100644 --- a/app/auth/dependencies.py +++ b/app/auth/dependencies.py @@ -26,7 +26,8 @@ def get_company_repository( async def get_current_user( - credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer()), + request: Request, + credentials: HTTPAuthorizationCredentials = Depends(HTTPBearer(auto_error=False)), auth_repository: AuthRepository = Depends(get_auth_repository) ) -> User: """ @@ -34,7 +35,13 @@ async def get_current_user( args: credentials: HTTPAuthorizationCredentials """ - access_token = credentials.credentials + + access_token = None + if credentials: + access_token = credentials.credentials + + if not access_token: + access_token = request.cookies.get("access_token") if not access_token: raise HTTPException( From 76faf266a7afc368de60188f32d5c3d40d60804c Mon Sep 17 00:00:00 2001 From: mhd329 Date: Sat, 17 Jan 2026 05:20:37 +0900 Subject: [PATCH 05/11] =?UTF-8?q?fix:=20=EC=98=88=EC=99=B8=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=EC=97=90=EC=84=9C=20=EC=98=A4=EB=A5=98=20=EB=A9=94?= =?UTF-8?q?=EC=8B=9C=EC=A7=80=EC=97=90=20=EC=98=88=EC=99=B8=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=EB=A5=BC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/auth/dependencies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/auth/dependencies.py b/app/auth/dependencies.py index 282ca7e..5492ff3 100644 --- a/app/auth/dependencies.py +++ b/app/auth/dependencies.py @@ -66,10 +66,10 @@ async def get_current_user( status_code=status.HTTP_401_UNAUTHORIZED, detail="Access token expired" ) - except Exception: + except Exception as e: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token" + detail=f"Invalid token : {e}" ) user = await auth_repository.get_user_by_email(email) From 24c2499219274068a302cd399cb7e6bfa46246a7 Mon Sep 17 00:00:00 2001 From: mhd329 Date: Sat, 17 Jan 2026 05:29:04 +0900 Subject: [PATCH 06/11] =?UTF-8?q?fix:=20JWT=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20?= =?UTF-8?q?=EB=B0=8F=20=EC=98=88=EC=99=B8=20=EB=A9=94=EC=8B=9C=EC=A7=80=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/admin/dependencies.py | 7 ------- app/auth/dependencies.py | 33 ++++++++++++++++++++------------- 2 files changed, 20 insertions(+), 20 deletions(-) diff --git a/app/admin/dependencies.py b/app/admin/dependencies.py index aa2cd77..a011780 100644 --- a/app/admin/dependencies.py +++ b/app/admin/dependencies.py @@ -58,7 +58,6 @@ async def get_admin_user( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Admin JWT configuration not found" ) - try: payload = jwt.decode( access_token, @@ -73,7 +72,6 @@ async def get_admin_user( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token" ) - # 어드민 토큰 타입 체크 if token_type != "admin_access": raise HTTPException( @@ -90,33 +88,28 @@ async def get_admin_user( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token" ) - user = await auth_repository.get_user_by_email(email) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found" ) - # user_gubun이 'admin'인지 체크 if user.user_gubun != "admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required - invalid user type" ) - # 어드민 이메일 리스트에 있는지 체크 (이중 체크) if not SETTINGS.ADMIN_EMAILS: raise HTTPException( status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail="Admin emails not configured" ) - admin_emails = [email.strip() for email in env_admin_emails.split(",")] if user.email not in admin_emails: raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required - email not authorized" ) - return user diff --git a/app/auth/dependencies.py b/app/auth/dependencies.py index 5492ff3..cd122c0 100644 --- a/app/auth/dependencies.py +++ b/app/auth/dependencies.py @@ -35,50 +35,57 @@ async def get_current_user( args: credentials: HTTPAuthorizationCredentials """ - access_token = None if credentials: access_token = credentials.credentials - if not access_token: access_token = request.cookies.get("access_token") - if not access_token: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated" ) - try: payload = jwt.decode( access_token, SETTINGS.JWT_SECRET, algorithms=[SETTINGS.JWT_ALGORITHM] ) - email: str = payload.get("sub") - if not email: - raise HTTPException( - status_code=status.HTTP_401_UNAUTHORIZED, - detail="Invalid token" - ) except jwt.ExpiredSignatureError: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Access token expired" ) + except jwt.InvalidSignatureError: # 어드민 토큰으로 일반 유저 api 쓰려고 할 때 발생할 수도 있음 + try: # 어드민 시크릿으로 재검증 + payload = jwt.decode( + access_token, + SETTINGS.ADMIN_JWT_SECRET, + algorithms=[SETTINGS.ADMIN_JWT_ALGORITHM] + ) + except Exception: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token signature" + ) except Exception as e: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, - detail=f"Invalid token : {e}" + # detail=f"Invalid token: {e}" -> 프로덕션 환경에서는 예외 상세메세지를 숨기는 편이 좋음 + detail=f"Invalid token" + ) + email: str = payload.get("sub") + if not email: + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Invalid token" ) - user = await auth_repository.get_user_by_email(email) if not user: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found" ) - return user From 1876de4e44fe661df208f2a022e585e0d2cb29b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=91=EB=AF=BC?= Date: Fri, 16 Jan 2026 16:40:24 -0700 Subject: [PATCH 07/11] feat: Add GitHub Actions workflow for deploying Workinkorea development server --- .github/workflows/deploy-dev.yml | 126 +++++++++++++++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 .github/workflows/deploy-dev.yml diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml new file mode 100644 index 0000000..c494f65 --- /dev/null +++ b/.github/workflows/deploy-dev.yml @@ -0,0 +1,126 @@ +name: Deploy Workinkorea Development Server +on: + push: + branches: [dev] + +env: + BASE_URL: byeong98.xyz + DOCKER_IMAGE_NAME: workinkorea-server + PORT: 8000 + +jobs: + development-build-and-deploy: + runs-on: ubuntu-latest + environment: development + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Build Docker image + run: | + docker build -t ${{ env.DOCKER_IMAGE_NAME }}:${{ github.sha }} . + docker save ${{ env.DOCKER_IMAGE_NAME }}:${{ github.sha }} | gzip > image.tar.gz + + - name: Setup SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + + - name: Add SSH known hosts + run: | + mkdir -p ~/.ssh + echo "${{ secrets.SSH_KNOWN_HOSTS }}" >> ~/.ssh/known_hosts + + - name: Copy image to remote server + run: | + scp -P ${{ secrets.SSH_PORT }} image.tar.gz ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }}:~/ + + - name: Deploy on remote server + run: | + ssh -p ${{ secrets.SSH_PORT }} ${{ secrets.SSH_USER }}@${{ secrets.SSH_HOST }} bash << 'ENDSSH' + set -e + + IMAGE_NAME="${{ env.DOCKER_IMAGE_NAME }}" + IMAGE_TAG="${{ github.sha }}" + BASE_URL="${{ env.BASE_URL }}" + PORT="${{ env.PORT }}" + + echo "Loading Docker image..." + docker load < ~/image.tar.gz + + echo "Stopping existing container..." + docker stop ${IMAGE_NAME} 2>/dev/null || true + docker rm ${IMAGE_NAME} 2>/dev/null || true + + echo "Starting new container..." + docker run -d \ + --name ${IMAGE_NAME} \ + --network core_network \ + --restart unless-stopped \ + --label "traefik.enable=true" \ + --label "traefik.http.routers.${IMAGE_NAME}.rule=Host(\`arw.${BASE_URL}\`)" \ + --label "traefik.http.routers.${IMAGE_NAME}.entrypoints=websecure" \ + --label "traefik.http.routers.${IMAGE_NAME}.tls.certresolver=le" \ + --label "traefik.http.services.${IMAGE_NAME}.loadbalancer.server.port=${PORT}" \ + -e COOKIE_DOMAIN="${{ secrets.COOKIE_DOMAIN }}" \ + -e CLIENT_URL="${{ secrets.CLIENT_URL }}" \ + -e ORIGINS_URLS="${{ secrets.ORIGINS_URLS }}" \ + -e DATABASE_SYNC_URL="${{ secrets.DATABASE_SYNC_URL }}" \ + -e DATABASE_ASYNC_URL="${{ secrets.DATABASE_ASYNC_URL }}" \ + -e REDIS_HOST="${{ secrets.REDIS_HOST }}" \ + -e REDIS_PORT="${{ secrets.REDIS_PORT }}" \ + -e REDIS_DB="${{ secrets.REDIS_DB }}" \ + -e GOOGLE_CLIENT_ID="${{ secrets.GOOGLE_CLIENT_ID }}" \ + -e GOOGLE_CLIENT_SECRET="${{ secrets.GOOGLE_CLIENT_SECRET }}" \ + -e GOOGLE_REDIRECT_URI="${{ secrets.GOOGLE_REDIRECT_URI }}" \ + -e GOOGLE_AUTHORIZATION_URL="${{ secrets.GOOGLE_AUTHORIZATION_URL }}" \ + -e GOOGLE_TOKEN_URL="${{ secrets.GOOGLE_TOKEN_URL }}" \ + -e GOOGLE_USER_INFO_URL="${{ secrets.GOOGLE_USER_INFO_URL }}" \ + -e JWT_SECRET="${{ secrets.JWT_SECRET }}" \ + -e JWT_ALGORITHM="${{ secrets.JWT_ALGORITHM }}" \ + -e ACCESS_TOKEN_EXPIRE_MINUTES="${{ secrets.ACCESS_TOKEN_EXPIRE_MINUTES }}" \ + -e REFRESH_TOKEN_EXPIRE_MINUTES="${{ secrets.REFRESH_TOKEN_EXPIRE_MINUTES }}" \ + -e MAIL_USERNAME="${{ secrets.MAIL_USERNAME }}" \ + -e MAIL_PASSWORD="${{ secrets.MAIL_PASSWORD }}" \ + -e MAIL_FROM_NAME="${{ secrets.MAIL_FROM_NAME }}" \ + -e MAIL_FROM="${{ secrets.MAIL_FROM }}" \ + -e MAIL_PORT="${{ secrets.MAIL_PORT }}" \ + -e MAIL_SERVER="${{ secrets.MAIL_SERVER }}" \ + -e MINIO_ENDPOINT="${{ secrets.MINIO_ENDPOINT }}" \ + -e MINIO_ACCESS_KEY="${{ secrets.MINIO_ACCESS_KEY }}" \ + -e MINIO_SECRET_KEY="${{ secrets.MINIO_SECRET_KEY }}" \ + -e MINIO_BUCKET_NAME="${{ secrets.MINIO_BUCKET_NAME }}" \ + -e ADMIN_JWT_SECRET="${{ secrets.ADMIN_JWT_SECRET }}" \ + -e ADMIN_JWT_ALGORITHM="${{ secrets.ADMIN_JWT_ALGORITHM }}" \ + -e ADMIN_ACCESS_TOKEN_EXPIRE_MINUTES="${{ secrets.ADMIN_ACCESS_TOKEN_EXPIRE_MINUTES }}" \ + -e ADMIN_REFRESH_TOKEN_EXPIRE_MINUTES="${{ secrets.ADMIN_REFRESH_TOKEN_EXPIRE_MINUTES }}" \ + -e ADMIN_EMAILS="${{ secrets.ADMIN_EMAILS }}" \ + ${IMAGE_NAME}:${IMAGE_TAG} + + echo "Cleaning up old images..." + docker image prune -f + + echo "Removing temporary files..." + rm -f ~/image.tar.gz + + echo "Deployment completed successfully!" + ENDSSH + + - name: Send Discord Deployment Notification + if: always() + uses: sarisia/actions-status-discord@v1 + with: + webhook: ${{ secrets.DISCORD_WEBHOOK_URL_DEV }} + status: ${{ job.status }} + title: "${{ job.status == 'success' && 'Success' || 'Failed' }}: Workinkorea-Server" + description: | + **Deployment ${{ job.status == 'success' && 'Successful' || 'Failed' }}** + + • **Branch**: `${{ github.ref_name }}` + • **Commit**: `${{ github.sha }}` + • **Message**: ${{ github.event.head_commit.message }} + • **Environment**: Development + • **URL**: https://arw.${{ env.BASE_URL }} \ No newline at end of file From aa3f7c0e7e4de68a3a7ba38a592a77b422f828e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=91=EB=AF=BC?= Date: Fri, 16 Jan 2026 17:00:33 -0700 Subject: [PATCH 08/11] fix: Update Docker image build and deployment process in GitHub Actions workflow --- .github/workflows/deploy-dev.yml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/deploy-dev.yml b/.github/workflows/deploy-dev.yml index c494f65..dca2646 100644 --- a/.github/workflows/deploy-dev.yml +++ b/.github/workflows/deploy-dev.yml @@ -21,8 +21,8 @@ jobs: - name: Build Docker image run: | - docker build -t ${{ env.DOCKER_IMAGE_NAME }}:${{ github.sha }} . - docker save ${{ env.DOCKER_IMAGE_NAME }}:${{ github.sha }} | gzip > image.tar.gz + docker build -t ${{ env.DOCKER_IMAGE_NAME }} . + docker save ${{ env.DOCKER_IMAGE_NAME }} | gzip > image.tar.gz - name: Setup SSH uses: webfactory/ssh-agent@v0.9.0 @@ -44,7 +44,6 @@ jobs: set -e IMAGE_NAME="${{ env.DOCKER_IMAGE_NAME }}" - IMAGE_TAG="${{ github.sha }}" BASE_URL="${{ env.BASE_URL }}" PORT="${{ env.PORT }}" @@ -98,7 +97,7 @@ jobs: -e ADMIN_ACCESS_TOKEN_EXPIRE_MINUTES="${{ secrets.ADMIN_ACCESS_TOKEN_EXPIRE_MINUTES }}" \ -e ADMIN_REFRESH_TOKEN_EXPIRE_MINUTES="${{ secrets.ADMIN_REFRESH_TOKEN_EXPIRE_MINUTES }}" \ -e ADMIN_EMAILS="${{ secrets.ADMIN_EMAILS }}" \ - ${IMAGE_NAME}:${IMAGE_TAG} + ${IMAGE_NAME} echo "Cleaning up old images..." docker image prune -f @@ -115,7 +114,7 @@ jobs: with: webhook: ${{ secrets.DISCORD_WEBHOOK_URL_DEV }} status: ${{ job.status }} - title: "${{ job.status == 'success' && 'Success' || 'Failed' }}: Workinkorea-Server" + title: "Workinkorea-Server / Development" description: | **Deployment ${{ job.status == 'success' && 'Successful' || 'Failed' }}** From 30fcf83399ef25097ec1b6be3e866a2c32e141e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=91=EB=AF=BC?= Date: Fri, 16 Jan 2026 17:16:54 -0700 Subject: [PATCH 09/11] feat: Install and configure Redis in Dockerfile for local access --- Dockerfile | 10 ++++++++++ dockerfile | 10 ++++++++++ 2 files changed, 20 insertions(+) diff --git a/Dockerfile b/Dockerfile index cd82c01..f3a135f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -2,6 +2,16 @@ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim WORKDIR /app +# Redis 설치 +RUN apt-get update && \ + apt-get install -y redis-server && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Redis 설정 (localhost만 접근 가능하도록) +RUN sed -i 's/bind 127.0.0.1 ::1/bind 127.0.0.1/' /etc/redis/redis.conf && \ + sed -i 's/protected-mode yes/protected-mode no/' /etc/redis/redis.conf + # 의존성 파일 복사 COPY pyproject.toml uv.lock ./ diff --git a/dockerfile b/dockerfile index cd82c01..f3a135f 100644 --- a/dockerfile +++ b/dockerfile @@ -2,6 +2,16 @@ FROM ghcr.io/astral-sh/uv:python3.13-bookworm-slim WORKDIR /app +# Redis 설치 +RUN apt-get update && \ + apt-get install -y redis-server && \ + apt-get clean && \ + rm -rf /var/lib/apt/lists/* + +# Redis 설정 (localhost만 접근 가능하도록) +RUN sed -i 's/bind 127.0.0.1 ::1/bind 127.0.0.1/' /etc/redis/redis.conf && \ + sed -i 's/protected-mode yes/protected-mode no/' /etc/redis/redis.conf + # 의존성 파일 복사 COPY pyproject.toml uv.lock ./ From 5880187968e589957e149f5d3eb51c16e9b631e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=91=EB=AF=BC?= Date: Fri, 16 Jan 2026 17:17:00 -0700 Subject: [PATCH 10/11] feat: Start Redis server in entrypoint script for improved service initialization --- entrypoint.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/entrypoint.sh b/entrypoint.sh index 4c9e8cd..eeb954e 100644 --- a/entrypoint.sh +++ b/entrypoint.sh @@ -1,6 +1,9 @@ #!/bin/sh set -e +# start redis +redis-server /etc/redis/redis.conf --daemonize yes + # database migration # uv run alembic upgrade head From ef56bdefb583c71f0181601706ea7af8627cac1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B9=80=EB=B3=91=EB=AF=BC?= Date: Fri, 16 Jan 2026 17:17:07 -0700 Subject: [PATCH 11/11] refactor: Remove database migration steps and enhance Discord notification in deployment workflow --- .github/workflows/deploy-pro.yml | 41 ++++++++------------------------ 1 file changed, 10 insertions(+), 31 deletions(-) diff --git a/.github/workflows/deploy-pro.yml b/.github/workflows/deploy-pro.yml index 56664a2..29d697d 100644 --- a/.github/workflows/deploy-pro.yml +++ b/.github/workflows/deploy-pro.yml @@ -21,23 +21,6 @@ jobs: uses: 'google-github-actions/auth@v2' with: credentials_json: '${{ secrets.GCP_SA_KEY }}' - - - name: database migration - run: | - pip install --upgrade pip - pip install alembic asyncpg psycopg2-binary sqlalchemy python-dotenv pydantic-settings pydantic redis - wget https://storage.googleapis.com/cloud-sql-connectors/cloud-sql-proxy/v2.8.0/cloud-sql-proxy.linux.amd64 -O cloud-sql-proxy - chmod +x cloud-sql-proxy - ./cloud-sql-proxy workinkorea-main:asia-northeast3:workinkorea-postgresql & - sleep 10 - export DATABASE_SYNC_URL="${{ secrets.DATABASE_SYNC_URL }}" - export DATABASE_ASYNC_URL="${{ secrets.DATABASE_ASYNC_URL }}" - export REDIS_HOST="${{ secrets.REDIS_HOST }}" - export REDIS_PORT="${{ secrets.REDIS_PORT }}" - export REDIS_DB="${{ secrets.REDIS_DB }}" - alembic upgrade head - pkill cloud-sql-proxy - - name: Build and Push Container run: | @@ -97,22 +80,18 @@ jobs: --env-vars-file env.yaml # discord notification - - name: send success message - if: success() - uses: sarisia/actions-status-discord@v1 - with: - webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} - status: ${{ job.status }} - title: "Workinkorea-Server-Production" - description: | - **Image: ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO_NAME }}/${{ env.IMAGE_NAME }}:${{ github.sha }}** - - - name: send failure message - if: failure() + - name: Send Discord Deployment Notification + if: always() uses: sarisia/actions-status-discord@v1 with: webhook: ${{ secrets.DISCORD_WEBHOOK_URL }} status: ${{ job.status }} - title: "Workinkorea-Server-Production" + title: "Workinkorea-Server / Production" description: | - **Image: ${{ env.REGION }}-docker.pkg.dev/${{ env.PROJECT_ID }}/${{ env.REPO_NAME }}/${{ env.IMAGE_NAME }}:${{ github.sha }}** \ No newline at end of file + **Deployment ${{ job.status == 'success' && 'Successful' || 'Failed' }}** + + • **Branch**: `${{ github.ref_name }}` + • **Commit**: `${{ github.sha }}` + • **Message**: ${{ github.event.head_commit.message }} + • **Environment**: Production + • **URL**: https://workinkorea.com \ No newline at end of file