From c55736ee86578e4159a0fcbf6cfa028ad14eae6d Mon Sep 17 00:00:00 2001 From: ISMAIL MASSERAN Date: Thu, 23 Jul 2026 12:25:11 +0800 Subject: [PATCH] DONE: cicd pipeline for frontend that uses ip first --- .gitea/workflows/build.yml | 123 +++++++++++++++++++++-------- .gitea/workflows/fe-deploy.yml | 122 ++++++++++++++++++++++++++++ fe/web/.env.example | 12 +++ fe/web/.env.production | 13 +++ fe/web/.gitignore | 3 + fe/web/admin-app/.dockerignore | 8 ++ fe/web/admin-app/Dockerfile | 23 ++++-- fe/web/admin-app/nginx.conf | 10 +++ fe/web/admin-app/src/constants.js | 5 +- fe/web/docker-compose.yml | 25 ++++++ fe/web/teller-app/.dockerignore | 8 ++ fe/web/teller-app/Dockerfile | 23 ++++-- fe/web/teller-app/nginx.conf | 10 +++ fe/web/teller-app/src/constants.js | 3 +- 14 files changed, 339 insertions(+), 49 deletions(-) create mode 100644 .gitea/workflows/fe-deploy.yml create mode 100644 fe/web/.env.example create mode 100644 fe/web/.env.production create mode 100644 fe/web/.gitignore create mode 100644 fe/web/admin-app/.dockerignore create mode 100644 fe/web/admin-app/nginx.conf create mode 100644 fe/web/docker-compose.yml create mode 100644 fe/web/teller-app/.dockerignore create mode 100644 fe/web/teller-app/nginx.conf diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml index b14895a..d675a07 100644 --- a/.gitea/workflows/build.yml +++ b/.gitea/workflows/build.yml @@ -43,38 +43,93 @@ jobs: --push \ be - # --- Frontend builds (enable when FE Dockerfiles are ready) --- - # build-frontend: + build-admin: + runs-on: docker + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to Docker registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login git.koppkb.com \ + -u "${{ secrets.REGISTRY_USERNAME }}" \ + --password-stdin + + - name: Build & push admin frontend image + run: | + set -e + + IMAGE="git.koppkb.com/kopkb/qms-fe-admin" + TAGS="-t ${IMAGE}:${{ gitea.sha }}" + API_URL="${{ secrets.VITE_API_URL }}" + CUSTOMER_URL="${{ secrets.VITE_CUSTOMER_APP_URL }}" + + if [ -z "${API_URL}" ]; then + echo "ERROR: Set Gitea secret VITE_API_URL (e.g. http://YOUR_SERVER_IP:8080)" + exit 1 + fi + + if [ "${{ gitea.ref_type }}" = "tag" ] && echo "${{ gitea.ref_name }}" | grep -q '^v'; then + TAGS="${TAGS} -t ${IMAGE}:${{ gitea.ref_name }}" + fi + + echo "Building and pushing admin: ${TAGS}" + docker buildx build \ + --platform linux/amd64 \ + --build-arg VITE_API_URL="${API_URL}" \ + --build-arg VITE_CUSTOMER_APP_URL="${CUSTOMER_URL:-http://localhost:3000}" \ + --cache-from type=registry,ref=${IMAGE}:buildcache,ignore-error=true \ + --cache-to type=registry,ref=${IMAGE}:buildcache,mode=max \ + -f fe/web/admin-app/Dockerfile \ + ${TAGS} \ + --push \ + fe/web/admin-app + + build-teller: + runs-on: docker + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Login to Docker registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login git.koppkb.com \ + -u "${{ secrets.REGISTRY_USERNAME }}" \ + --password-stdin + + - name: Build & push teller frontend image + run: | + set -e + + IMAGE="git.koppkb.com/kopkb/qms-fe-teller" + TAGS="-t ${IMAGE}:${{ gitea.sha }}" + API_URL="${{ secrets.VITE_API_URL }}" + + if [ -z "${API_URL}" ]; then + echo "ERROR: Set Gitea secret VITE_API_URL (e.g. http://YOUR_SERVER_IP:8080)" + exit 1 + fi + + if [ "${{ gitea.ref_type }}" = "tag" ] && echo "${{ gitea.ref_name }}" | grep -q '^v'; then + TAGS="${TAGS} -t ${IMAGE}:${{ gitea.ref_name }}" + fi + + echo "Building and pushing teller: ${TAGS}" + docker buildx build \ + --platform linux/amd64 \ + --build-arg VITE_API_URL="${API_URL}" \ + --cache-from type=registry,ref=${IMAGE}:buildcache,ignore-error=true \ + --cache-to type=registry,ref=${IMAGE}:buildcache,mode=max \ + -f fe/web/teller-app/Dockerfile \ + ${TAGS} \ + --push \ + fe/web/teller-app + + # --- Customer (Next.js) — enable when ready --- + # build-customer: # runs-on: docker - # - # steps: - # - name: Checkout repository - # uses: actions/checkout@v4 - # - # - name: Login to Docker registry - # run: | - # echo "${{ secrets.REGISTRY_PASSWORD }}" | \ - # docker login git.koppkb.com \ - # -u "${{ secrets.REGISTRY_USERNAME }}" \ - # --password-stdin - # - # - name: Build & push frontend image - # run: | - # set -e - # - # IMAGE="git.koppkb.com/kopkb/qms-fe" - # TAGS="-t ${IMAGE}:${{ gitea.sha }}" - # - # if [ "${{ gitea.ref_type }}" = "tag" ] && echo "${{ gitea.ref_name }}" | grep -q '^v'; then - # TAGS="${TAGS} -t ${IMAGE}:${{ gitea.ref_name }}" - # fi - # - # echo "Building and pushing frontend: ${TAGS}" - # docker buildx build \ - # --platform linux/amd64 \ - # --cache-from type=registry,ref=${IMAGE}:buildcache,ignore-error=true \ - # --cache-to type=registry,ref=${IMAGE}:buildcache,mode=max \ - # -f fe/docker/Dockerfile \ - # --build-arg VITE_API_BASE_URL=https://api.example.com \ - # ${TAGS} \ - # --push . + # ... diff --git a/.gitea/workflows/fe-deploy.yml b/.gitea/workflows/fe-deploy.yml new file mode 100644 index 0000000..0f1a56b --- /dev/null +++ b/.gitea/workflows/fe-deploy.yml @@ -0,0 +1,122 @@ +name: Deploy Frontend to Production + +# Manual production deploy — pulls pre-built admin + teller images from the registry +on: + workflow_dispatch: + inputs: + image_tag: + description: "Docker image tag to deploy (commit SHA or version tag, e.g. v1.0.0)" + required: true + type: string + +env: + ADMIN_IMAGE: git.koppkb.com/kopkb/qms-fe-admin + TELLER_IMAGE: git.koppkb.com/kopkb/qms-fe-teller + DEPLOY_DIR: /home/arrahn/Project/qms-fe + +jobs: + deploy: + runs-on: host + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + ref: ${{ inputs.image_tag }} + + - name: Login to Docker registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | \ + docker login git.koppkb.com \ + -u "${{ secrets.REGISTRY_USERNAME }}" \ + --password-stdin + + - name: Verify frontend images exist in registry + run: | + set -e + IMAGE_TAG="${{ inputs.image_tag }}" + + for IMAGE in "${ADMIN_IMAGE}:${IMAGE_TAG}" "${TELLER_IMAGE}:${IMAGE_TAG}"; do + echo "Checking if image exists: ${IMAGE}" + if ! docker manifest inspect "${IMAGE}" > /dev/null 2>&1; then + echo "ERROR: Image ${IMAGE} does not exist in registry!" + exit 1 + fi + echo "✓ ${IMAGE} found" + done + + - name: Pull frontend images + run: | + set -e + IMAGE_TAG="${{ inputs.image_tag }}" + docker pull "${ADMIN_IMAGE}:${IMAGE_TAG}" + docker pull "${TELLER_IMAGE}:${IMAGE_TAG}" + echo "✓ Images pulled successfully" + + - name: Sync compose file and deploy + run: | + set -e + IMAGE_TAG="${{ inputs.image_tag }}" + + mkdir -p "${DEPLOY_DIR}" + cp fe/web/docker-compose.yml "${DEPLOY_DIR}/docker-compose.yml" + + cd "${DEPLOY_DIR}" + + if [ ! -f .env ]; then + echo "ERROR: ${DEPLOY_DIR}/.env is missing on the server" + echo "Create it from fe/web/.env.example (or fe/web/.env.production) before deploying." + exit 1 + fi + + export IMAGE_TAG + export ADMIN_IMAGE + export TELLER_IMAGE + + docker compose -f docker-compose.yml pull admin teller + docker compose -f docker-compose.yml up -d --remove-orphans + + echo "✓ Deployed IMAGE_TAG=${IMAGE_TAG}" + + - name: Verify deployment + run: | + set -e + cd "${DEPLOY_DIR}" + + echo "Waiting for services to start..." + sleep 5 + + for SERVICE in admin teller; do + STATUS=$(docker compose -f docker-compose.yml ps --status running --format '{{.Name}}' "$SERVICE" 2>/dev/null || true) + if [ -z "$STATUS" ]; then + echo "ERROR: ${SERVICE} is not running" + docker compose -f docker-compose.yml ps + docker compose -f docker-compose.yml logs --tail=50 "$SERVICE" || true + exit 1 + fi + echo "✓ ${SERVICE} is running (${STATUS})" + done + + ADMIN_HOST_PORT=$(grep -E '^ADMIN_HOST_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true) + TELLER_HOST_PORT=$(grep -E '^TELLER_HOST_PORT=' .env 2>/dev/null | cut -d= -f2- | tr -d '"' || true) + ADMIN_HOST_PORT=${ADMIN_HOST_PORT:-5000} + TELLER_HOST_PORT=${TELLER_HOST_PORT:-3001} + + check_http() { + NAME=$1 + PORT=$2 + for i in 1 2 3 4 5 6; do + CODE=$(curl -s -o /dev/null -w "%{http_code}" "http://127.0.0.1:${PORT}/" || echo "000") + if [ "${CODE}" != "000" ]; then + echo "✓ ${NAME} is responding on port ${PORT} (HTTP ${CODE})" + return 0 + fi + echo "Waiting for ${NAME}... attempt ${i}/6" + sleep 3 + done + echo "ERROR: ${NAME} is not responding on port ${PORT}" + return 1 + } + + check_http "admin" "${ADMIN_HOST_PORT}" + check_http "teller" "${TELLER_HOST_PORT}" diff --git a/fe/web/.env.example b/fe/web/.env.example new file mode 100644 index 0000000..acc4eff --- /dev/null +++ b/fe/web/.env.example @@ -0,0 +1,12 @@ +# Host ports (change if they collide on the single server) +ADMIN_HOST_PORT=5000 +TELLER_HOST_PORT=3001 + +# Baked into the Vite images at build time (set in Gitea secret/variable VITE_API_URL too) +VITE_API_URL=http://YOUR_SERVER_IP:8080 +VITE_CUSTOMER_APP_URL=http://YOUR_SERVER_IP:3000 + +# Registry images (used by compose / deploy) +ADMIN_IMAGE=git.koppkb.com/kopkb/qms-fe-admin +TELLER_IMAGE=git.koppkb.com/kopkb/qms-fe-teller +IMAGE_TAG=local diff --git a/fe/web/.env.production b/fe/web/.env.production new file mode 100644 index 0000000..ed02fa8 --- /dev/null +++ b/fe/web/.env.production @@ -0,0 +1,13 @@ +# Host ports (change if they collide on the single server) +ADMIN_HOST_PORT=5000 +TELLER_HOST_PORT=3001 + +# Replace YOUR_SERVER_IP with the production server IP (API is on :8080) +# Also set the same value as Gitea secret/variable VITE_API_URL for CI image builds +VITE_API_URL=http://172.16.6.200:8080 +VITE_CUSTOMER_APP_URL=http://172.16.6.200:3000 + +# Registry images (used by compose / deploy) +ADMIN_IMAGE=git.koppkb.com/kopkb/qms-fe-admin +TELLER_IMAGE=git.koppkb.com/kopkb/qms-fe-teller +IMAGE_TAG=local diff --git a/fe/web/.gitignore b/fe/web/.gitignore new file mode 100644 index 0000000..b5da3fb --- /dev/null +++ b/fe/web/.gitignore @@ -0,0 +1,3 @@ +.env +!.env.example +!.env.production diff --git a/fe/web/admin-app/.dockerignore b/fe/web/admin-app/.dockerignore new file mode 100644 index 0000000..167eb8e --- /dev/null +++ b/fe/web/admin-app/.dockerignore @@ -0,0 +1,8 @@ +node_modules +dist +.git +.gitignore +*.md +.env +.env.* +.DS_Store diff --git a/fe/web/admin-app/Dockerfile b/fe/web/admin-app/Dockerfile index 6e157b4..35ea182 100644 --- a/fe/web/admin-app/Dockerfile +++ b/fe/web/admin-app/Dockerfile @@ -1,10 +1,21 @@ -FROM node:21-alpine AS build -WORKDIR /admin-app -COPY package*.json . -RUN npm install +# Build stage +FROM node:20-bookworm AS build +WORKDIR /app + +ARG VITE_API_URL=http://localhost:8080 +ARG VITE_CUSTOMER_APP_URL=http://localhost:3000 +ENV VITE_API_URL=$VITE_API_URL +ENV VITE_CUSTOMER_APP_URL=$VITE_CUSTOMER_APP_URL + +COPY package.json package-lock.json ./ +RUN npm ci COPY . . RUN npm run build -EXPOSE 5001 -CMD ["npm", "run", "preview"] +# Runtime stage — static SPA +FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/fe/web/admin-app/nginx.conf b/fe/web/admin-app/nginx.conf new file mode 100644 index 0000000..3c25a98 --- /dev/null +++ b/fe/web/admin-app/nginx.conf @@ -0,0 +1,10 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/fe/web/admin-app/src/constants.js b/fe/web/admin-app/src/constants.js index 7b6387b..5fb7548 100644 --- a/fe/web/admin-app/src/constants.js +++ b/fe/web/admin-app/src/constants.js @@ -1,4 +1,5 @@ -export const SERVER_URL = 'http://localhost:8080'; +export const SERVER_URL = + import.meta.env.VITE_API_URL ?? 'http://localhost:8080'; export const CUSTOMER_APP_URL = import.meta.env.VITE_CUSTOMER_APP_URL ?? 'http://localhost:3000'; @@ -6,4 +7,4 @@ export const CUSTOMER_APP_URL = export const ROLES = { ROLE_SUPER_ADMIN : "ROLE_SUPER_ADMIN", ROLE_BRANCH_ADMIN : "ROLE_BRANCH_ADMIN" -} \ No newline at end of file +} diff --git a/fe/web/docker-compose.yml b/fe/web/docker-compose.yml new file mode 100644 index 0000000..39f315b --- /dev/null +++ b/fe/web/docker-compose.yml @@ -0,0 +1,25 @@ +services: + admin: + image: ${ADMIN_IMAGE:-qms-fe-admin}:${IMAGE_TAG:-local} + build: + context: ./admin-app + dockerfile: Dockerfile + args: + VITE_API_URL: ${VITE_API_URL:-http://localhost:8080} + VITE_CUSTOMER_APP_URL: ${VITE_CUSTOMER_APP_URL:-http://localhost:3000} + container_name: qms-fe-admin + restart: unless-stopped + ports: + - "${ADMIN_HOST_PORT:-5000}:80" + + teller: + image: ${TELLER_IMAGE:-qms-fe-teller}:${IMAGE_TAG:-local} + build: + context: ./teller-app + dockerfile: Dockerfile + args: + VITE_API_URL: ${VITE_API_URL:-http://localhost:8080} + container_name: qms-fe-teller + restart: unless-stopped + ports: + - "${TELLER_HOST_PORT:-3001}:80" diff --git a/fe/web/teller-app/.dockerignore b/fe/web/teller-app/.dockerignore new file mode 100644 index 0000000..167eb8e --- /dev/null +++ b/fe/web/teller-app/.dockerignore @@ -0,0 +1,8 @@ +node_modules +dist +.git +.gitignore +*.md +.env +.env.* +.DS_Store diff --git a/fe/web/teller-app/Dockerfile b/fe/web/teller-app/Dockerfile index 5a47a1e..1ee5326 100644 --- a/fe/web/teller-app/Dockerfile +++ b/fe/web/teller-app/Dockerfile @@ -1,10 +1,21 @@ -FROM node:21-alpine AS build -WORKDIR /teller-app -COPY package*.json . -RUN npm install +# Build stage +FROM node:20-bookworm AS build +WORKDIR /app + +ARG VITE_API_URL=http://localhost:8080 +ARG VITE_GOLD_PRICE_URL +ENV VITE_API_URL=$VITE_API_URL +ENV VITE_GOLD_PRICE_URL=$VITE_GOLD_PRICE_URL + +COPY package.json package-lock.json ./ +RUN npm ci COPY . . RUN npm run build -EXPOSE 3000 -CMD ["npm", "run", "preview"] +# Runtime stage — static SPA +FROM nginx:alpine +COPY nginx.conf /etc/nginx/conf.d/default.conf +COPY --from=build /app/dist /usr/share/nginx/html +EXPOSE 80 +CMD ["nginx", "-g", "daemon off;"] diff --git a/fe/web/teller-app/nginx.conf b/fe/web/teller-app/nginx.conf new file mode 100644 index 0000000..3c25a98 --- /dev/null +++ b/fe/web/teller-app/nginx.conf @@ -0,0 +1,10 @@ +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + location / { + try_files $uri $uri/ /index.html; + } +} diff --git a/fe/web/teller-app/src/constants.js b/fe/web/teller-app/src/constants.js index e8863df..3826367 100644 --- a/fe/web/teller-app/src/constants.js +++ b/fe/web/teller-app/src/constants.js @@ -1,4 +1,5 @@ -export const SERVER_URL = 'http://localhost:8080'; +export const SERVER_URL = + import.meta.env.VITE_API_URL ?? 'http://localhost:8080'; export const GOLD_PRICE_URL = import.meta.env.VITE_GOLD_PRICE_URL ??