From 2f260e0f3b1cdad544f5b69f4a94eb62efc28095 Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 17 Sep 2026 14:06:19 +0700 Subject: [PATCH] =?UTF-8?q?CD:=20=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88?= =?UTF-8?q?=E0=B8=A1=20workflow=20uat.yml=20deploy=20=E0=B9=80=E0=B8=84?= =?UTF-8?q?=E0=B8=A3=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B8=87=20UAT=20=E2=80=94?= =?UTF-8?q?=20mirror=20build.yml=20=E0=B8=97=E0=B8=B8=E0=B8=81=E0=B8=AD?= =?UTF-8?q?=E0=B8=A2=E0=B9=88=E0=B8=B2=E0=B8=87=20=E0=B9=80=E0=B8=9B?= =?UTF-8?q?=E0=B8=A5=E0=B8=B5=E0=B9=88=E0=B8=A2=E0=B8=99=E0=B8=95=E0=B8=B1?= =?UTF-8?q?=E0=B8=A7=E0=B9=80=E0=B8=94=E0=B8=B5=E0=B8=A2=E0=B8=A7:=20DEPLO?= =?UTF-8?q?Y=5FHOST=20=E0=B8=AD=E0=B9=88=E0=B8=B2=E0=B8=99=E0=B8=88?= =?UTF-8?q?=E0=B8=B2=E0=B8=81=20vars.SSH=5FUAT=5FDEPLOY=5FHOST=20=E0=B9=83?= =?UTF-8?q?=E0=B8=AB=E0=B8=A1=E0=B9=88=20(=E0=B8=95=E0=B8=B1=E0=B9=89?= =?UTF-8?q?=E0=B8=87=E0=B9=80=E0=B8=9E=E0=B8=B4=E0=B9=88=E0=B8=A1=E0=B9=83?= =?UTF-8?q?=E0=B8=99=20Forgejo=20=E0=B8=81=E0=B9=88=E0=B8=AD=E0=B8=99?= =?UTF-8?q?=E0=B9=83=E0=B8=8A=E0=B9=89=20=E0=B9=84=E0=B8=A1=E0=B9=88?= =?UTF-8?q?=E0=B8=87=E0=B8=B1=E0=B9=89=E0=B8=99=20deploy=20job=20SSH=20?= =?UTF-8?q?=E0=B9=84=E0=B8=9B=20string=20=E0=B8=A7=E0=B9=88=E0=B8=B2?= =?UTF-8?q?=E0=B8=87)=20=E2=80=94=20trigger=20tag=20v*.*.*=20=E0=B9=80?= =?UTF-8?q?=E0=B8=94=E0=B8=B4=E0=B8=A1=20(push=20tag=20=E0=B9=80=E0=B8=94?= =?UTF-8?q?=E0=B8=B5=E0=B8=A2=E0=B8=A7=20deploy=20Test=20+=20UAT=20?= =?UTF-8?q?=E0=B8=9E=E0=B8=A3=E0=B9=89=E0=B8=AD=E0=B8=A1=E0=B8=81=E0=B8=B1?= =?UTF-8?q?=E0=B8=99),=20registry/image=20=E0=B9=80=E0=B8=94=E0=B8=B4?= =?UTF-8?q?=E0=B8=A1=20push=20:v*=20=E0=B9=81=E0=B8=A5=E0=B8=B0=20:latest?= =?UTF-8?q?=20=E0=B8=8B=E0=B9=89=E0=B8=B3=E0=B8=81=E0=B8=B1=E0=B8=99=20(co?= =?UTF-8?q?mmit=20=E0=B9=80=E0=B8=94=E0=B8=B5=E0=B8=A2=E0=B8=A7=E0=B8=81?= =?UTF-8?q?=E0=B8=B1=E0=B8=99=20=E0=B9=80=E0=B8=99=E0=B8=B7=E0=B9=89?= =?UTF-8?q?=E0=B8=AD=E0=B8=AB=E0=B8=B2=E0=B9=80=E0=B8=AB=E0=B8=A1=E0=B8=B7?= =?UTF-8?q?=E0=B8=AD=E0=B8=99=20=E0=B9=84=E0=B8=A1=E0=B9=88=E0=B8=8A?= =?UTF-8?q?=E0=B8=99=E0=B8=81=E0=B8=B1=E0=B8=99),=20SSH=20user/port/passwo?= =?UTF-8?q?rd=20=E0=B8=8A=E0=B8=B8=E0=B8=94=E0=B9=80=E0=B8=94=E0=B8=B4?= =?UTF-8?q?=E0=B8=A1,=20script=20~/repo/replace-env.sh=20+=20deploy.sh=20?= =?UTF-8?q?=E0=B9=80=E0=B8=94=E0=B8=B4=E0=B8=A1=20(=E0=B9=80=E0=B8=84?= =?UTF-8?q?=E0=B8=A3=E0=B8=B7=E0=B9=88=E0=B8=AD=E0=B8=87=20UAT=20=E0=B8=95?= =?UTF-8?q?=E0=B9=89=E0=B8=AD=E0=B8=87=E0=B8=A1=E0=B8=B5=E0=B9=84=E0=B8=9F?= =?UTF-8?q?=E0=B8=A5=E0=B9=8C=E0=B8=AA=E0=B8=AD=E0=B8=87=E0=B8=95=E0=B8=B1?= =?UTF-8?q?=E0=B8=A7=E0=B8=99=E0=B8=B5=E0=B9=89=20+=20=E0=B8=A3=E0=B8=B1?= =?UTF-8?q?=E0=B8=9A=20credentials=20=E0=B9=80=E0=B8=94=E0=B8=B4=E0=B8=A1)?= =?UTF-8?q?,=20Discord=20webhook/Web=20URL=20=E0=B8=95=E0=B8=B1=E0=B8=A7?= =?UTF-8?q?=E0=B9=80=E0=B8=94=E0=B8=B4=E0=B8=A1=20=E2=80=94=20=E0=B9=80?= =?UTF-8?q?=E0=B8=9B=E0=B8=A5=E0=B8=B5=E0=B9=88=E0=B8=A2=E0=B8=99=E0=B9=80?= =?UTF-8?q?=E0=B8=89=E0=B8=9E=E0=B8=B2=E0=B8=B0=E0=B8=82=E0=B9=89=E0=B8=AD?= =?UTF-8?q?=E0=B8=84=E0=B8=A7=E0=B8=B2=E0=B8=A1=20workflow/job/notificatio?= =?UTF-8?q?n=20=E0=B9=80=E0=B8=9B=E0=B9=87=E0=B8=99=20"UAT"=20=E0=B9=83?= =?UTF-8?q?=E0=B8=AB=E0=B9=89=E0=B9=81=E0=B8=A2=E0=B8=81=20notification=20?= =?UTF-8?q?=E0=B8=AA=E0=B8=AD=E0=B8=87=E0=B9=80=E0=B8=84=E0=B8=A3=E0=B8=B7?= =?UTF-8?q?=E0=B9=88=E0=B8=AD=E0=B8=87=E0=B9=84=E0=B8=94=E0=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- .forgejo/workflows/uat.yml | 249 +++++++++++++++++++++++++++++++++++++ 1 file changed, 249 insertions(+) create mode 100644 .forgejo/workflows/uat.yml diff --git a/.forgejo/workflows/uat.yml b/.forgejo/workflows/uat.yml new file mode 100644 index 0000000..772bcfe --- /dev/null +++ b/.forgejo/workflows/uat.yml @@ -0,0 +1,249 @@ +name: Deploy eCourt Note Web to UAT Server + +on: + push: + tags: + - "v*.*.*" + workflow_dispatch: + +env: + # Container Registry + REGISTRY_URL: ${{ vars.CONTAINER_REGISTRY }} + REGISTRY_USERNAME: ${{ vars.CONTAINER_REGISTRY_USERNAME }} + REGISTRY_PASSWORD: ${{ secrets.CONTAINER_REGISTRY_PASSWORD }} + + # Docker Image + WEB_IMAGE_NAME: chamomind/ecourt-note + IMAGE_TAG: ${{ github.ref_name }} + + # SSH Deployment — ต่างจาก build.yml ตัวเดียว: ชี้เครื่อง UAT (ตั้ง vars.SSH_UAT_DEPLOY_HOST เพิ่มใน Forgejo) + DEPLOY_HOST: ${{ vars.SSH_UAT_DEPLOY_HOST }} + DEPLOY_USER: ${{ vars.SSH_DEPLOY_USER }} + + # Notifications + DISCORD_WEBHOOK_URL: ${{ vars.DISCORD_WEBHOOK_URL }} + NEXT_PUBLIC_APP_URL: ${{ vars.NEXT_PUBLIC_APP_URL }} + +jobs: + validate-schema: + name: Validate Database Schema (Drizzle) + runs-on: ubuntu-latest + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Verify tag is on dev branch + run: | + echo "======================================================" + echo "Verifying tag is pushed to dev branch" + echo "======================================================" + echo "Tag: ${{ github.ref_name }}" + echo "Commit: ${{ github.sha }}" + + # Fetch dev branch + git fetch origin dev + + # Check if the tag's commit is on dev branch + if git branch -r --contains ${{ github.sha }} | grep -q "origin/dev"; then + echo "✅ Tag is on dev branch" + echo "test_branch=true" >> $GITHUB_ENV + else + echo "❌ Tag is NOT on dev branch" + echo "This workflow only triggers for tags pushed to dev branch" + echo "" + echo "Current branches containing this commit:" + git branch -r --contains ${{ github.sha }} + exit 1 + fi + + - name: Check for schema changes + id: schema-check + run: | + # โปรเจกต์นี้ใช้ Drizzle (drizzle-kit push) ไม่มีโฟลเดอร์ migrations — ดูการเปลี่ยนแปลงที่ db/schema.ts + if git diff --name-only origin/dev...HEAD | grep -qE "^db/schema\.ts$"; then + echo "has_schema_changes=true" >> $GITHUB_OUTPUT + echo "✅ Schema changes detected" + else + echo "has_schema_changes=false" >> $GITHUB_OUTPUT + echo "ℹ️ No schema changes" + fi + + - name: Schema change summary + if: steps.schema-check.outputs.has_schema_changes == 'true' + run: | + echo "=== Schema Change Summary ===" + echo "" + echo "Database schema (db/schema.ts) has been modified in this deployment." + echo "This project uses Drizzle push (no migration files)." + echo "" + echo "⚠️ REMINDERS:" + echo " 1. Run 'npx drizzle-kit push' on the target database (staging first)" + echo " 2. Monitor logs after deployment" + echo "" + echo "📚 Reference: README.md / docs/PLAN.md §3" + + build: + name: Build Web Docker Image + runs-on: ubuntu-latest + needs: validate-schema + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY_URL }} + username: ${{ env.REGISTRY_USERNAME }} + password: ${{ env.REGISTRY_PASSWORD }} + + - name: Build Docker image (no buildx) + run: | + # Build using standard docker build (not buildx) + git reset --hard HEAD + cd ${{ github.workspace }} + docker build \ + --no-cache \ + --pull \ + --file ./docker/web.Dockerfile \ + --tag ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:${{ env.IMAGE_TAG }} \ + --tag ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:latest \ + . + + echo "✅ Build successful!" + docker images | grep ${{ env.WEB_IMAGE_NAME }} + + - name: Push Docker image + run: | + docker push ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:${{ env.IMAGE_TAG }} + docker push ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:latest + echo "✅ Image pushed to registry" + + - name: Test image can run + run: | + # Quick test that image can start + docker run --rm ${{ env.REGISTRY_URL }}/${{ env.WEB_IMAGE_NAME }}:${{ env.IMAGE_TAG }} node --version + + deploy: + name: Deploy eCourt Note Web to UAT Server + runs-on: ubuntu-latest + needs: build + if: startsWith(github.ref, 'refs/tags/') + steps: + - name: Deploy web on UAT server + uses: appleboy/ssh-action@v1.2.1 + with: + host: ${{ env.DEPLOY_HOST }} + port: ${{ vars.SSH_DEPLOY_PORT }} + username: ${{ env.DEPLOY_USER }} + password: ${{ secrets.SSH_DEPLOY_PASSWORD }} + script: | + cd ~/repo + + chmod +x ./replace-env.sh ./deploy.sh + ./replace-env.sh ECOURT_NOTE_TAG "${{ env.IMAGE_TAG }}" + ./deploy.sh + + notify: + name: Notify Deployment Status + runs-on: ubuntu-latest + needs: [build, deploy] + if: always() + steps: + - name: Send Discord notification - Success + if: ${{ needs.deploy.result == 'success' }} + run: | + curl -H "Content-Type: application/json" \ + -d '{ + "embeds": [{ + "title": "✅ eCourt Note UAT Deployment Successful", + "description": "eCourt Note Web deployed to UAT server", + "color": 3066993, + "fields": [ + { + "name": "Environment", + "value": "UAT", + "inline": true + }, + { + "name": "Tag", + "value": "${{ github.ref_name }}", + "inline": true + }, + { + "name": "Commit", + "value": "`${{ github.sha }}`", + "inline": false + }, + { + "name": "Image Tag", + "value": "`${{ env.IMAGE_TAG }}`", + "inline": true + }, + { + "name": "Web URL", + "value": "${{ env.NEXT_PUBLIC_APP_URL }}", + "inline": false + }, + { + "name": "Deployed By", + "value": "${{ github.actor }}", + "inline": true + } + ], + "footer": { + "text": "eCourt Note CD Pipeline" + }, + "timestamp": "'"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"'" + }] + }' \ + ${{ env.DISCORD_WEBHOOK_URL }} + + - name: Send Discord notification - Failure + if: ${{ needs.deploy.result == 'failure' || needs.build.result == 'failure' }} + run: | + curl -H "Content-Type: application/json" \ + -d '{ + "embeds": [{ + "title": "❌ eCourt Note UAT Deployment Failed", + "description": "Failed to deploy eCourt Note Web to UAT server", + "color": 15158332, + "fields": [ + { + "name": "Environment", + "value": "UAT", + "inline": true + }, + { + "name": "Tag", + "value": "${{ github.ref_name }}", + "inline": true + }, + { + "name": "Commit", + "value": "`${{ github.sha }}`", + "inline": false + }, + { + "name": "Failed Job", + "value": "${{ needs.build.result == 'failure' && 'Build' || 'Deploy' }}", + "inline": true + }, + { + "name": "Deployed By", + "value": "${{ github.actor }}", + "inline": true + } + ], + "footer": { + "text": "eCourt Note CD Pipeline" + }, + "timestamp": "'"$(date -u +%Y-%m-%dT%H:%M:%S.000Z)"'" + }] + }' \ + ${{ env.DISCORD_WEBHOOK_URL }} + exit 1