From 3ec638353596be77fc1592efa3bdffd9b33c912d Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 27 Apr 2026 08:22:51 +0000 Subject: [PATCH 1/5] chore: sync workflows from base-template --- .github/workflows/publish.yml | 106 ++++++++++++++++++++++++ .github/workflows/re-pull.yml | 60 ++++++++++++++ .github/workflows/script/notify.sh | 26 ++++++ .github/workflows/script/re-pull.sh | 120 ++++++++++++++++++++++++++++ 4 files changed, 312 insertions(+) create mode 100644 .github/workflows/publish.yml create mode 100644 .github/workflows/re-pull.yml create mode 100644 .github/workflows/script/notify.sh create mode 100644 .github/workflows/script/re-pull.sh diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..088f877 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,106 @@ +name: Publish Docker to GHCR + +on: + workflow_dispatch: + inputs: + stack_env: + description: "stack env" + required: true + type: choice + default: "dev" + options: + - dev + - prod + - stg + tag: + description: "Image tag (e.g. 1.0.0)" + required: true + default: "1.0.0" + + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }} + +jobs: + publish: + name: Build & Push to GHCR ${{ github.repository }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }} + runs-on: ubuntu-latest + environment: ${{ vars.PORTAINER_ENV || 'portainer' }} + permissions: + contents: read + packages: write + steps: + - name: Free disk space + run: | + sudo rm -rf /usr/share/dotnet + sudo rm -rf /usr/local/lib/android + sudo rm -rf /opt/ghc + sudo rm -rf /opt/hostedtoolcache/CodeQL + sudo docker image prune --all --force + df -h + + - name: Checkout branch ${{ github.event.inputs.stack_env }} + uses: actions/checkout@v4 + with: + ref: ${{ github.event.inputs.stack_env }} + + - name: Checkout scripts from main + uses: actions/checkout@v4 + with: + ref: main + path: .ci + sparse-checkout: .github/workflows/script + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GitHub Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Generate image metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=raw,value=${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }} + type=raw,value=${{ github.event.inputs.stack_env }}-latest + + - name: Build and push Docker image + uses: docker/build-push-action@v6 + with: + context: . + file: ./Dockerfile + push: true + platforms: linux/amd64 + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + no-cache: true + + - name: Notify success + if: success() + run: bash ./.ci/.github/workflows/script/notify.sh + env: + TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} + TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + NOTIFY_STATUS: success + NOTIFY_WORKFLOW: "Publish Docker" + NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}" + + - name: Notify failure + if: failure() + run: bash ./.ci/.github/workflows/script/notify.sh + env: + TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} + TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + NOTIFY_STATUS: failure + NOTIFY_WORKFLOW: "Publish Docker" + NOTIFY_DETAIL: "Image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.event.inputs.stack_env }}-${{ github.event.inputs.tag }}" diff --git a/.github/workflows/re-pull.yml b/.github/workflows/re-pull.yml new file mode 100644 index 0000000..e2110e7 --- /dev/null +++ b/.github/workflows/re-pull.yml @@ -0,0 +1,60 @@ +name: Re-Pull Docker +on: + workflow_dispatch: + inputs: + stack_name: + description: "stack name" + required: true + type: string + stack_env: + description: "stack env" + required: true + type: choice + default: "dev" + options: + - dev + - stg + - prod + +jobs: + publish: + name: Re-Pull Docker ${{ github.event.inputs.stack_name }} + runs-on: ubuntu-latest + environment: ${{ vars.PORTAINER_ENV || 'portainer' }} + permissions: + contents: read + packages: write + steps: + - name: Checkout scripts from main + uses: actions/checkout@v4 + with: + ref: main + sparse-checkout: .github/workflows/script + + - name: Deploy ke Portainer + run: bash ./.github/workflows/script/re-pull.sh + env: + PORTAINER_USERNAME: ${{ secrets.PORTAINER_USERNAME }} + PORTAINER_PASSWORD: ${{ secrets.PORTAINER_PASSWORD }} + PORTAINER_URL: ${{ secrets.PORTAINER_URL }} + STACK_NAME: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }} + + - name: Notify success + if: success() + run: bash ./.github/workflows/script/notify.sh + env: + TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} + TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + NOTIFY_STATUS: success + NOTIFY_WORKFLOW: "Re-Pull Docker" + NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}" + + - name: Notify failure + if: failure() + run: bash ./.github/workflows/script/notify.sh + env: + TELEGRAM_TOKEN: ${{ secrets.TELEGRAM_TOKEN }} + TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }} + NOTIFY_STATUS: failure + NOTIFY_WORKFLOW: "Re-Pull Docker" + NOTIFY_DETAIL: "Stack: ${{ github.event.inputs.stack_name }}-${{ github.event.inputs.stack_env }}" diff --git a/.github/workflows/script/notify.sh b/.github/workflows/script/notify.sh new file mode 100644 index 0000000..22944bf --- /dev/null +++ b/.github/workflows/script/notify.sh @@ -0,0 +1,26 @@ +#!/bin/bash + +: "${TELEGRAM_TOKEN:?TELEGRAM_TOKEN tidak di-set}" +: "${TELEGRAM_CHAT_ID:?TELEGRAM_CHAT_ID tidak di-set}" +: "${NOTIFY_STATUS:?NOTIFY_STATUS tidak di-set}" +: "${NOTIFY_WORKFLOW:?NOTIFY_WORKFLOW tidak di-set}" + +if [ "$NOTIFY_STATUS" = "success" ]; then + ICON="โœ…" + TEXT="${ICON} *${NOTIFY_WORKFLOW}* berhasil!" +else + ICON="โŒ" + TEXT="${ICON} *${NOTIFY_WORKFLOW}* gagal!" +fi + +if [ -n "$NOTIFY_DETAIL" ]; then + TEXT="${TEXT} +${NOTIFY_DETAIL}" +fi + +curl -s -X POST "https://api.telegram.org/bot${TELEGRAM_TOKEN}/sendMessage" \ + -H "Content-Type: application/json" \ + -d "$(jq -n \ + --arg chat_id "$TELEGRAM_CHAT_ID" \ + --arg text "$TEXT" \ + '{chat_id: $chat_id, text: $text, parse_mode: "Markdown"}')" diff --git a/.github/workflows/script/re-pull.sh b/.github/workflows/script/re-pull.sh new file mode 100644 index 0000000..0aa3fa6 --- /dev/null +++ b/.github/workflows/script/re-pull.sh @@ -0,0 +1,120 @@ +#!/bin/bash + +: "${PORTAINER_URL:?PORTAINER_URL tidak di-set}" +: "${PORTAINER_USERNAME:?PORTAINER_USERNAME tidak di-set}" +: "${PORTAINER_PASSWORD:?PORTAINER_PASSWORD tidak di-set}" +: "${STACK_NAME:?STACK_NAME tidak di-set}" + +# Timeout total: MAX_RETRY * SLEEP_INTERVAL detik +MAX_RETRY=60 # 60 ร— 10s = 10 menit +SLEEP_INTERVAL=10 + +echo "๐Ÿ” Autentikasi ke Portainer..." +TOKEN=$(curl -s -X POST "https://${PORTAINER_URL}/api/auth" \ + -H "Content-Type: application/json" \ + -d "{\"username\": \"${PORTAINER_USERNAME}\", \"password\": \"${PORTAINER_PASSWORD}\"}" \ + | jq -r .jwt) + +if [ -z "$TOKEN" ] || [ "$TOKEN" = "null" ]; then + echo "โŒ Autentikasi gagal! Cek PORTAINER_URL, USERNAME, dan PASSWORD." + exit 1 +fi + +echo "๐Ÿ” Mencari stack: $STACK_NAME..." +STACK=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks" \ + -H "Authorization: Bearer ${TOKEN}" \ + | jq ".[] | select(.Name == \"$STACK_NAME\")") + +if [ -z "$STACK" ]; then + echo "โŒ Stack '$STACK_NAME' tidak ditemukan di Portainer!" + exit 1 +fi + +STACK_ID=$(echo "$STACK" | jq -r .Id) +ENDPOINT_ID=$(echo "$STACK" | jq -r .EndpointId) +ENV=$(echo "$STACK" | jq '.Env // []') + +# โ”€โ”€ Catat container ID lama sebelum redeploy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +echo "๐Ÿ“ธ Mencatat container aktif sebelum redeploy..." +CONTAINERS_BEFORE=$(curl -s -X GET \ + "https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \ + -H "Authorization: Bearer ${TOKEN}") + +OLD_IDS=$(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Id] | join(",")') +echo " Container lama: $(echo "$CONTAINERS_BEFORE" | jq -r '[.[] | .Names[0]] | join(", ")')" + +# โ”€โ”€ Ambil compose file lalu trigger redeploy โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ +echo "๐Ÿ“„ Mengambil compose file..." +STACK_FILE=$(curl -s -X GET "https://${PORTAINER_URL}/api/stacks/${STACK_ID}/file" \ + -H "Authorization: Bearer ${TOKEN}" \ + | jq -r .StackFileContent) + +PAYLOAD=$(jq -n \ + --arg content "$STACK_FILE" \ + --argjson env "$ENV" \ + '{stackFileContent: $content, env: $env, pullImage: true}') + +echo "๐Ÿš€ Triggering redeploy $STACK_NAME (pull latest image)..." +HTTP_STATUS=$(curl -s -o /tmp/portainer_response.json -w "%{http_code}" \ + -X PUT "https://${PORTAINER_URL}/api/stacks/${STACK_ID}?endpointId=${ENDPOINT_ID}" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d "$PAYLOAD") + +if [ "$HTTP_STATUS" != "200" ]; then + echo "โŒ Redeploy gagal! HTTP Status: $HTTP_STATUS" + cat /tmp/portainer_response.json | jq . + exit 1 +fi + +echo "โณ Menunggu image selesai di-pull dan container baru running..." +echo " (Timeout: $((MAX_RETRY * SLEEP_INTERVAL)) detik)" + +COUNT=0 +while [ $COUNT -lt $MAX_RETRY ]; do + sleep $SLEEP_INTERVAL + COUNT=$((COUNT + 1)) + + CONTAINERS=$(curl -s -X GET \ + "https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/containers/json?all=true&filters=%7B%22label%22%3A%5B%22com.docker.compose.project%3D${STACK_NAME}%22%5D%7D" \ + -H "Authorization: Bearer ${TOKEN}") + + # Container baru = ID tidak ada di daftar container lama + NEW_RUNNING=$(echo "$CONTAINERS" | jq \ + --arg old "$OLD_IDS" \ + '[.[] | select(.State == "running" and ((.Id) as $id | ($old | split(",") | index($id)) == null))] | length') + + FAILED=$(echo "$CONTAINERS" | jq \ + '[.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not))] | length') + + echo "๐Ÿ”„ [$((COUNT * SLEEP_INTERVAL))s / $((MAX_RETRY * SLEEP_INTERVAL))s] Container baru running: ${NEW_RUNNING} | Gagal: ${FAILED}" + echo "$CONTAINERS" | jq -r '.[] | " โ†’ \(.Names[0]) | \(.State) | \(.Status) | id: \(.Id[:12])"' + + if [ "$FAILED" -gt "0" ]; then + echo "" + echo "โŒ Ada container yang crash!" + echo "$CONTAINERS" | jq -r '.[] | select(.State == "exited" and (.Status | test("Exited \\(0\\)") | not) and (.Names[0] | test("seed") | not)) | " โ†’ \(.Names[0]) | \(.Status)"' + exit 1 + fi + + if [ "$NEW_RUNNING" -gt "0" ]; then + # Cleanup dangling images setelah redeploy sukses + echo "๐Ÿงน Membersihkan dangling images..." + curl -s -X POST "https://${PORTAINER_URL}/api/endpoints/${ENDPOINT_ID}/docker/images/prune" \ + -H "Authorization: Bearer ${TOKEN}" \ + -H "Content-Type: application/json" \ + -d '{"filters":{"dangling":["true"]}}' | jq -r '" Reclaimed: \(.SpaceReclaimed // 0 | . / 1073741824 | tostring | .[0:5]) GB"' + + echo "โœ… Cleanup selesai!" + echo "" + echo "โœ… Stack $STACK_NAME berhasil di-redeploy dengan image baru dan running!" + exit 0 + fi + + +done + +echo "" +echo "โŒ Timeout $((MAX_RETRY * SLEEP_INTERVAL))s! Container baru tidak kunjung running." +echo " Kemungkinan image masih dalam proses pull atau ada error di server." +exit 1 -- 2.49.1 From 786953054a55bf3a2ef40e511b5c9257b2caf360 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 27 Apr 2026 16:53:10 +0800 Subject: [PATCH 2/5] upd:dockerfile --- Dockerfile | 62 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 Dockerfile diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..5f41ba9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,62 @@ +# Stage 1: Build +FROM oven/bun:1.3 AS build + +# Install build dependencies for native modules +RUN apt-get update && apt-get install -y \ + python3 \ + make \ + g++ \ + && rm -rf /var/lib/apt/lists/* + +# Set the working directory +WORKDIR /app + +# Copy package files +COPY package.json bun.lock* ./ + +# Install dependencies +RUN bun install --frozen-lockfile + +# Copy the rest of the application code +COPY . . + +# Use .env.example as default env for build +RUN cp .env.example .env + +# Generate Prisma client +RUN bun x prisma generate + +# Generate API types +RUN bun run gen:api + +# Build the application frontend +RUN bun run build + +# Stage 2: Runtime +FROM oven/bun:1.3-slim AS runtime + +# Set environment variables +ENV NODE_ENV=production + +# Install runtime dependencies +RUN apt-get update && apt-get install -y \ + postgresql-client \ + && rm -rf /var/lib/apt/lists/* + +# Set the working directory +WORKDIR /app + +# Copy necessary files from build stage +COPY --from=build /app/package.json ./ +COPY --from=build /app/tsconfig.json ./ +COPY --from=build /app/dist ./dist +COPY --from=build /app/generated ./generated +COPY --from=build /app/src ./src +COPY --from=build /app/node_modules ./node_modules +COPY --from=build /app/prisma ./prisma + +# Expose the port +EXPOSE 3000 + +# Start the application +CMD ["bun", "start"] -- 2.49.1 From 1104217070398c1dd7f40b39004c38801dd80891 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 27 Apr 2026 16:58:44 +0800 Subject: [PATCH 3/5] upd:dockerfile --- Dockerfile | 66 +++++++++++++++--------------------------------------- 1 file changed, 18 insertions(+), 48 deletions(-) diff --git a/Dockerfile b/Dockerfile index 5f41ba9..7ae36c4 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,62 +1,32 @@ -# Stage 1: Build -FROM oven/bun:1.3 AS build - -# Install build dependencies for native modules -RUN apt-get update && apt-get install -y \ - python3 \ - make \ - g++ \ - && rm -rf /var/lib/apt/lists/* - -# Set the working directory +FROM oven/bun:1 AS base WORKDIR /app -# Copy package files -COPY package.json bun.lock* ./ - # Install dependencies +FROM base AS deps +COPY package.json bun.lock ./ RUN bun install --frozen-lockfile -# Copy the rest of the application code -COPY . . - -# Use .env.example as default env for build -RUN cp .env.example .env - # Generate Prisma client -RUN bun x prisma generate +FROM deps AS prisma +COPY prisma ./prisma +RUN bunx prisma generate -# Generate API types -RUN bun run gen:api - -# Build the application frontend +# Build frontend (Vite โ†’ dist/) +FROM prisma AS builder +COPY . . RUN bun run build -# Stage 2: Runtime -FROM oven/bun:1.3-slim AS runtime - -# Set environment variables -ENV NODE_ENV=production - -# Install runtime dependencies -RUN apt-get update && apt-get install -y \ - postgresql-client \ - && rm -rf /var/lib/apt/lists/* - -# Set the working directory +# Runtime +FROM base AS runner WORKDIR /app -# Copy necessary files from build stage -COPY --from=build /app/package.json ./ -COPY --from=build /app/tsconfig.json ./ -COPY --from=build /app/dist ./dist -COPY --from=build /app/generated ./generated -COPY --from=build /app/src ./src -COPY --from=build /app/node_modules ./node_modules -COPY --from=build /app/prisma ./prisma +COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/prisma ./prisma +COPY --from=builder /app/src ./src +COPY --from=builder /app/dist ./dist +COPY --from=builder /app/package.json ./ -# Expose the port +ENV NODE_ENV=production EXPOSE 3000 -# Start the application -CMD ["bun", "start"] +CMD ["bun", "src/index.tsx"] \ No newline at end of file -- 2.49.1 From 8527671f460cec3e244371f7e7c094645012f7b3 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 27 Apr 2026 17:16:36 +0800 Subject: [PATCH 4/5] upd:compose --- compose.yml | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 compose.yml diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..aaaca7d --- /dev/null +++ b/compose.yml @@ -0,0 +1,65 @@ +services: + monitoring-app: + image: ghcr.io/bipprojectbali/monitoring-app:stg-latest + container_name: monitoring-app-stg + restart: unless-stopped + environment: + # Database + - DATABASE_URL=${DATABASE_URL} + - DIRECT_URL=${DIRECT_URL} + # Google OAuth + - GOOGLE_CLIENT_ID=${GOOGLE_CLIENT_ID} + - GOOGLE_CLIENT_SECRET=${GOOGLE_CLIENT_SECRET} + # App + - PORT=${PORT:-3000} + - NODE_ENV=${NODE_ENV:-production} + # Admin (initial Super Admin emails, comma-separated) + - SUPER_ADMIN_EMAIL=${SUPER_ADMIN_EMAIL} + networks: + - public-net + - postgres-net-stg + depends_on: + migrate: + condition: service_completed_successfully + deploy: + resources: + limits: + cpus: '1.0' + memory: 1G + reservations: + memory: 512M + logging: + driver: json-file + options: + max-size: "10m" + max-file: "3" + labels: + - "traefik.enable=true" + - "traefik.docker.network=public-net" + - "traefik.http.routers.monitoring-app.rule=Host(`monitoring-stg.wibudev.com`)" + - "traefik.http.routers.monitoring-app.entrypoints=websecure" + - "traefik.http.routers.monitoring-app.tls=true" + - "traefik.http.routers.monitoring-app.tls.certresolver=letsencrypt" + - "traefik.http.services.monitoring-app.loadbalancer.server.port=3000" + + migrate: + image: ghcr.io/bipprojectbali/monitoring-app:stg-latest + container_name: monitoring-app-stg-migrate + restart: "no" + # `migrate deploy` only applies existing migrations from prisma/migrations/. + # Safer than `migrate dev --name auto` which auto-generates new migrations + # from schema diff (risk of drift in production). + # Seed runs only if SEED_ON_DEPLOY=true (idempotent โ€” wipe-and-reseed by + # design; recommend leaving false for production with real customer data). + entrypoint: ["sh", "-c", "bunx prisma migrate deploy && if [ \"$$SEED_ON_DEPLOY\" = \"true\" ]; then bun prisma/seed.ts; fi"] + environment: + - DATABASE_URL=${DIRECT_URL} + - SEED_ON_DEPLOY=${SEED_ON_DEPLOY:-false} + networks: + - postgres-net-stg + +networks: + public-net: + external: true + postgres-net-stg: + external: true -- 2.49.1 From 9d80eb3b852610daf8344f521bc0f260c7111153 Mon Sep 17 00:00:00 2001 From: amaliadwiy Date: Mon, 27 Apr 2026 17:33:19 +0800 Subject: [PATCH 5/5] upd --- Dockerfile | 1 + 1 file changed, 1 insertion(+) diff --git a/Dockerfile b/Dockerfile index 7ae36c4..923dcd9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -21,6 +21,7 @@ FROM base AS runner WORKDIR /app COPY --from=builder /app/node_modules ./node_modules +COPY --from=builder /app/generated ./generated COPY --from=builder /app/prisma ./prisma COPY --from=builder /app/src ./src COPY --from=builder /app/dist ./dist -- 2.49.1