Add Forgejo CD workflow + Docker image build
Some checks failed
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 53s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Failing after 1s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Failing after 1s
Some checks failed
Deploy eCourt Note Web to Server / Validate Database Schema (Drizzle) (push) Successful in 3s
Deploy eCourt Note Web to Server / Build Web Docker Image (push) Successful in 53s
Deploy eCourt Note Web to Server / Deploy eCourt Note Web to Server (push) Failing after 1s
Deploy eCourt Note Web to Server / Notify Deployment Status (push) Failing after 1s
- .forgejo/workflows/build.yml: ดัดแปลงจาก e-Ticket — validate db/schema.ts (Drizzle) แทน Prisma migrations, image chamomind/ecourt-note, deploy ผ่าน SSH (ECOURT_NOTE_TAG), แก้ env ที่ใช้แต่ไม่ได้นิยาม (NEXT_PUBLIC_APP_URL) และ vars/secrets.SSH_DEPLOY_USER ไม่สอดคล้อง - docker/web.Dockerfile: Next.js standalone 3 stages (deps/build/runner) พร้อม build-time DATABASE_URL placeholder (db/index.ts throw ถ้าไม่มี env ตอนเก็บ page data) + COPY .next/static ที่ standalone ไม่รวม - .dockerignore กัน .git/node_modules/.next/.env เข้า build context - next.config.mjs: เปิด output: "standalone" หมายเหตุ: ยังตรวจ docker build ท้องถิ่นไม่ครบ — ค้างที่ดิสก์ C: เต็ม (Docker Desktop buildkit read-only) รอเคลียร์พื้นที่ก่อน rerun Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
parent
eddcec28c7
commit
83108dd16c
4 changed files with 301 additions and 1 deletions
11
.dockerignore
Normal file
11
.dockerignore
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
node_modules
|
||||||
|
.next
|
||||||
|
.git
|
||||||
|
.forgejo
|
||||||
|
.claude
|
||||||
|
drizzle
|
||||||
|
dev-server.log
|
||||||
|
npm-debug.log*
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
249
.forgejo/workflows/build.yml
Normal file
249
.forgejo/workflows/build.yml
Normal file
|
|
@ -0,0 +1,249 @@
|
||||||
|
name: Deploy eCourt Note Web to 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
|
||||||
|
DEPLOY_HOST: ${{ vars.SSH_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 main branch
|
||||||
|
run: |
|
||||||
|
echo "======================================================"
|
||||||
|
echo "Verifying tag is pushed to main branch"
|
||||||
|
echo "======================================================"
|
||||||
|
echo "Tag: ${{ github.ref_name }}"
|
||||||
|
echo "Commit: ${{ github.sha }}"
|
||||||
|
|
||||||
|
# Fetch main branch
|
||||||
|
git fetch origin main
|
||||||
|
|
||||||
|
# Check if the tag's commit is on main branch
|
||||||
|
if git branch -r --contains ${{ github.sha }} | grep -q "origin/main"; then
|
||||||
|
echo "✅ Tag is on main branch"
|
||||||
|
echo "test_branch=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "❌ Tag is NOT on main branch"
|
||||||
|
echo "This workflow only triggers for tags pushed to main 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/main...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 Server
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: build
|
||||||
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
|
steps:
|
||||||
|
- name: Deploy web on 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 Deployment Successful",
|
||||||
|
"description": "eCourt Note Web deployed to server",
|
||||||
|
"color": 3066993,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Environment",
|
||||||
|
"value": "Test",
|
||||||
|
"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 Deployment Failed",
|
||||||
|
"description": "Failed to deploy eCourt Note Web to server",
|
||||||
|
"color": 15158332,
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Environment",
|
||||||
|
"value": "Test",
|
||||||
|
"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
|
||||||
37
docker/web.Dockerfile
Normal file
37
docker/web.Dockerfile
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
# ─── Stage 1: ติดตั้ง dependencies (npm ci รวม postinstall คัดลอก pdf.worker) ───
|
||||||
|
FROM node:24-alpine AS deps
|
||||||
|
WORKDIR /app
|
||||||
|
COPY package.json package-lock.json ./
|
||||||
|
# postinstall เขียน public/pdf.worker.min.mjs — ต้องมีโฟลเดอร์ public/ ก่อน ไม่งั้น npm ci ล้ม
|
||||||
|
RUN mkdir -p public && npm ci
|
||||||
|
|
||||||
|
# ─── Stage 2: build Next.js (output: standalone) ───
|
||||||
|
FROM node:24-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY --from=deps /app/node_modules ./node_modules
|
||||||
|
COPY . .
|
||||||
|
# ประกันว่า pdf.worker.min.mjs ตรงกับเวอร์ชัน pdfjs-dist ใน node_modules เสมอ
|
||||||
|
# (postinstall รันใน stage deps ซึ่งยังไม่มี public/ ของ repo)
|
||||||
|
RUN node -e "require('fs').copyFileSync('node_modules/pdfjs-dist/build/pdf.worker.min.mjs','public/pdf.worker.min.mjs')"
|
||||||
|
ENV NEXT_TELEMETRY_DISABLED=1
|
||||||
|
# db/index.ts throw ถ้าไม่มี DATABASE_URL ตอน next build เก็บ page data —
|
||||||
|
# ใส่ค่า dummy (postgres.js เชื่อมแบบ lazy ไม่มี query ตอน build) ตัวจริงฉีดตอน docker run
|
||||||
|
ENV DATABASE_URL=postgresql://placeholder:placeholder@127.0.0.1:5432/placeholder
|
||||||
|
RUN npm run build
|
||||||
|
|
||||||
|
# ─── Stage 3: runtime ตัวเบา (แค่ standalone + public/) ───
|
||||||
|
FROM node:24-alpine AS runner
|
||||||
|
WORKDIR /app
|
||||||
|
ENV NODE_ENV=production \
|
||||||
|
NEXT_TELEMETRY_DISABLED=1 \
|
||||||
|
PORT=3000 \
|
||||||
|
HOSTNAME=0.0.0.0
|
||||||
|
RUN addgroup -S nodejs && adduser -S nextjs -G nodejs
|
||||||
|
# Next 14.2 รวม public/ (pdf.worker, ฟอนต์ Sarabun, samples PDF) ไว้ใน standalone แล้ว
|
||||||
|
# แต่ไม่รวม .next/static (JS chunks) — ต้องคัดลอกเพิ่ม ไม่งั้นหน้าเว็บไม่มี JS
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
|
||||||
|
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
|
||||||
|
USER nextjs
|
||||||
|
EXPOSE 3000
|
||||||
|
# DATABASE_URL ฉีดตอนรันผ่าน env ของ container (อย่าฝังใน image)
|
||||||
|
CMD ["node", "server.js"]
|
||||||
|
|
@ -1,4 +1,7 @@
|
||||||
/** @type {import('next').NextConfig} */
|
/** @type {import('next').NextConfig} */
|
||||||
const nextConfig = {};
|
const nextConfig = {
|
||||||
|
// สำหรับ Docker (docker/web.Dockerfile) — build แล้วได้ .next/standalone รันด้วย node server.js
|
||||||
|
output: "standalone",
|
||||||
|
};
|
||||||
|
|
||||||
export default nextConfig;
|
export default nextConfig;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue