diff --git a/.github/workflows/deploy-docker.yml b/.github/workflows/deploy-docker.yml new file mode 100644 index 0000000..1a2f55c --- /dev/null +++ b/.github/workflows/deploy-docker.yml @@ -0,0 +1,134 @@ +name: Deploy to VPS + +on: + push: + branches: + - master + workflow_dispatch: + +# Prevent concurrent deployments from racing +concurrency: + group: deploy-vps-${{ github.ref }} + cancel-in-progress: false + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io + OWNER: mytheclipse + + +jobs: + build-and-push: + runs-on: ubuntu-latest + strategy: + fail-fast: false + max-parallel: 2 + matrix: + service: [frontend, backend, discord-gateway, proxy] + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v4 + + - name: Log in to GHCR + uses: docker/login-action@v4 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Build and push ${{ matrix.service }} + uses: docker/build-push-action@v7 + with: + context: . + file: infra/docker/Dockerfile.${{ matrix.service }} + push: true + tags: | + ${{ env.REGISTRY }}/${{ env.OWNER }}/bete-${{ matrix.service }}:latest + ${{ env.REGISTRY }}/${{ env.OWNER }}/bete-${{ matrix.service }}:${{ github.sha }} + cache-from: type=gha,scope=bete-${{ matrix.service }} + cache-to: type=gha,mode=max,scope=bete-${{ matrix.service }} + build-args: | + VITE_BE_API_URL=https://imphnen.asepharyana.my.id + VITE_BE_WS_URL=wss://imphnen.asepharyana.my.id + + deploy: + needs: build-and-push + runs-on: ubuntu-latest + if: github.ref == 'refs/heads/master' + steps: + - name: Deploy to VPS + uses: appleboy/ssh-action@v1.2.5 + env: + GHCR_USERNAME: ${{ github.actor }} + GHCR_TOKEN: ${{ secrets.GITHUB_TOKEN }} + ENV_FILE: ${{ secrets.ENV_FILE }} + with: + host: ${{ secrets.VPS_HOST }} + username: ${{ secrets.VPS_USERNAME }} + key: ${{ secrets.VPS_SSH_KEY }} + envs: GHCR_USERNAME,GHCR_TOKEN,ENV_FILE + script: | + set -eu + + APP_DIR=/opt/imphenbot + REPO_URL=https://github.com/MythEclipse/GMW.git + + if [ -d "$APP_DIR/.git" ]; then + ORIGIN=$(git -C "$APP_DIR" remote get-url origin 2>/dev/null || true) + if [ "$ORIGIN" != "$REPO_URL" ]; then + rm -rf "$APP_DIR" + fi + fi + + if [ ! -d "$APP_DIR/.git" ]; then + mkdir -p "$APP_DIR" + git clone --depth 1 --branch master "$REPO_URL" "$APP_DIR" + else + git -C "$APP_DIR" fetch --depth 1 origin master + git -C "$APP_DIR" checkout master + git -C "$APP_DIR" reset --hard origin/master + fi + + cd "$APP_DIR" + + mkdir -p infra/docker/recordings + # Set permissions for recordings directory (writable by container app user UID 100) + chmod -R 777 infra/docker/recordings + + # Write env file — strip \r to avoid configuration issues + printf '%s\n' "$ENV_FILE" | tr -d '\r' > infra/docker/.env + + echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin + + # Force stop any stale containers from previous deployments + docker rm -f imphenbot-proxy imphenbot-backend imphenbot-frontend imphenbot-discord-gateway 2>/dev/null || true + + # Retry docker pull up to 3 times on transient network errors + RETRIES=3 + for i in $(seq 1 $RETRIES); do + echo "docker compose pull (attempt $i/$RETRIES)" + if docker compose -f infra/docker/docker-compose.yml pull; then + echo "Pull succeeded" + break + else + echo "Pull failed (attempt $i/$RETRIES)" + if [ "$i" -eq "$RETRIES" ]; then + echo "All pull attempts failed" >&2 + exit 1 + fi + sleep 5 + fi + done + + # Remove orphan containers but don't block on healthchecks — + # containers have restart: unless-stopped and will recover on their own + docker compose -f infra/docker/docker-compose.yml up -d --remove-orphans + docker image prune -f diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 0ffeaa5..0000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,159 +0,0 @@ -# ─── BETE GitLab CI/CD Pipeline ─────────────────────────────────────────────── -# Builds 4 Docker images (frontend, backend, discord-gateway, proxy), -# pushes to GitLab Container Registry, then deploys to VPS via SSH. -# -# Required CI/CD Variables (set in GitLab → Settings → CI/CD → Variables): -# -# VPS_HOST - VPS IP/hostname -# VPS_USERNAME - SSH user -# VPS_SSH_KEY - SSH private key (type: file) -# ENV_FILE - Full .env file content (type: env_var) -# -# Notes: -# - VPS_SSH_KEY must be "File" type so GitLab writes it to disk -# - ENV_FILE is "Variable" type — contains full multi-line .env content -# -# ────────────────────────────────────────────────────────────────────────────── - -stages: - - build - - deploy - -variables: - # Use Docker-in-Docker for building with BuildKit - DOCKER_HOST: tcp://docker:2376 - DOCKER_TLS_CERTDIR: "/certs" - DOCKER_DRIVER: overlay2 - DOCKER_BUILDKIT: 1 - - # Image naming - REGISTRY: $CI_REGISTRY - IMAGE_TAG_LATEST: latest - IMAGE_TAG_COMMIT: $CI_COMMIT_SHA - - # Frontend build args - VITE_BE_API_URL: https://imphnen.asepharyana.my.id - VITE_BE_WS_URL: wss://imphnen.asepharyana.my.id - - # Deploy defaults - SSH_HOST: "${VPS_USERNAME:-root}@${VPS_HOST:-45.127.35.244}" - APP_DIR: /opt/imphenbot - -# ── Build stage ─────────────────────────────────────────────────────────────── -.docker-build: - stage: build - image: docker:27-cli - tags: - - docker - services: - - name: docker:27-dind - command: ["--mtu=1400"] - cache: - key: docker-$CI_COMMIT_REF_SLUG-$SERVICE_NAME - paths: - - /caches/$SERVICE_NAME/ - before_script: - # Login to GitLab Container Registry - - echo "$CI_JOB_TOKEN" | docker login "$CI_REGISTRY" -u "$CI_REGISTRY_USER" --password-stdin - # Ensure cache directory exists - - mkdir -p /caches/$SERVICE_NAME - script: - # Build with cache - - | - docker build \ - --file infra/docker/Dockerfile.$SERVICE_NAME \ - --tag $REGISTRY/$CI_PROJECT_PATH/bete-$SERVICE_NAME:$IMAGE_TAG_COMMIT \ - --tag $REGISTRY/$CI_PROJECT_PATH/bete-$SERVICE_NAME:$IMAGE_TAG_LATEST \ - --build-arg BUILDKIT_INLINE_CACHE=1 \ - --build-arg VITE_BE_API_URL=$VITE_BE_API_URL \ - --build-arg VITE_BE_WS_URL=$VITE_BE_WS_URL \ - --cache-from $REGISTRY/$CI_PROJECT_PATH/bete-$SERVICE_NAME:latest \ - . - # Push images - - docker push $REGISTRY/$CI_PROJECT_PATH/bete-$SERVICE_NAME:$IMAGE_TAG_COMMIT - - docker push $REGISTRY/$CI_PROJECT_PATH/bete-$SERVICE_NAME:$IMAGE_TAG_LATEST - -build-frontend: - extends: .docker-build - variables: - SERVICE_NAME: frontend - only: - - master - -build-backend: - extends: .docker-build - variables: - SERVICE_NAME: backend - only: - - master - -build-discord-gateway: - extends: .docker-build - variables: - SERVICE_NAME: discord-gateway - only: - - master - -build-proxy: - extends: .docker-build - variables: - SERVICE_NAME: proxy - only: - - master - -# ── Deploy stage ────────────────────────────────────────────────────────────── -deploy-vps: - stage: deploy - image: alpine:latest - tags: - - docker - only: - - master - needs: - - build-frontend - - build-backend - - build-discord-gateway - - build-proxy - before_script: - # Install SSH client and tools - - apk add --no-cache openssh-client docker-compose bash - # Set up SSH key (file-type variable: value is a path) - - mkdir -p ~/.ssh - - cp "$VPS_SSH_KEY" ~/.ssh/id_rsa - - chmod 600 ~/.ssh/id_rsa - # Write .env content to temp file for scp - - printf '%s' "$ENV_FILE" > /tmp/.env.prod - # Add VPS to known_hosts - - ssh-keyscan -H "${VPS_HOST}" >> ~/.ssh/known_hosts 2>/dev/null - script: - # Copy .env and docker-compose to VPS - - scp /tmp/.env.prod "$SSH_HOST:$APP_DIR/.env" - - scp infra/docker/docker-compose.yml "$SSH_HOST:$APP_DIR/docker-compose.yml" - # Deploy via SSH - - | - ssh "$SSH_HOST" " - set -eu - - cd $APP_DIR - - # Login to GitLab Container Registry - echo '$CI_JOB_TOKEN' | docker login $CI_REGISTRY -u '$CI_REGISTRY_USER' --password-stdin - - # Update image tags in docker-compose.yml to use GitLab registry - sed -i 's|ghcr.io/\${OWNER:-mytheclipse}|$CI_REGISTRY/$CI_PROJECT_PATH|g' docker-compose.yml - sed -i 's|\${OWNER:-mytheclipse}|$CI_PROJECT_NAMESPACE|g' docker-compose.yml - - # Pull latest images - docker compose pull - - # Stop and remove old containers - docker rm -f imphenbot-proxy imphenbot-backend imphenbot-frontend imphenbot-discord-gateway 2>/dev/null || true - - # Start fresh - docker compose up -d --remove-orphans - - # Cleanup old images - docker image prune -f - " - after_script: - - rm -f ~/.ssh/id_rsa /tmp/.env.prod