Otomatisasi Deployment Tanpa Ribet Menggunakan GitHub Actions

I Nyoman Donostia 835 kata
Otomatisasi Deployment Tanpa Ribet Menggunakan GitHub Actions

Kenapa GitHub Actions?

GitHub Actions terintegrasi langsung dengan repo GitHub Anda. Tidak perlu setupCircleCI, Travis, atau Jenkins terpisah. Gratis untuk 2000 menit/bulan di akun publik — cukup untuk proyek small-medium.

Dalam project saya, GitHub Actions menghemat 4-6 jam per minggu pekerjaan manual deploy. Build -> test -> deploy -> notif Slack, semuanya otomatis.

Konsep Dasar Workflow

File .github/workflows/*.yml mendefinisikan workflow. Setiap workflow punya:

  • Triggers: kapan jalan (push, pull_request, schedule, workflow_dispatch)
  • Jobs: grup langkah yang berjalan di runner (Ubuntu, Windows, macOS)
  • Steps: perintah individual dalam job

1. Workflow CI: Lint, Test, Build

# .github/workflows/ci.yml
name: CI

on:
  push:
    branches: [main, develop]
  pull_request:
    branches: [main]

jobs:
  quality:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup Node.js
        uses: actions/setup-node@v4
        with:
          node-version: '20'
          cache: 'npm'
      
      - name: Install dependencies
        run: npm ci
      
      - name: Lint
        run: npm run lint
      
      - name: Type check
        run: npm run typecheck
      
      - name: Test
        run: npm test -- --coverage
      
      - name: Build
        run: npm run build
      
      - name: Upload coverage
        uses: codecov/codecov-action@v4
        with:
          file: ./coverage/coverage-final.json

2. Deploy ke VPS tanpa Downtime

Strategi: pull kode baru, build lokal di VPS, restart service via systemd dengan zero downtime.

Server-side Setup

# /etc/systemd/system/myapp.service
[Unit]
Description=My App
After=network.target

[Service]
Type=notify
User=deployer
WorkingDirectory=/var/www/myapp
Environment=NODE_ENV=production
ExecStart=/usr/bin/node dist/server.js
Restart=always
RestartSec=3
KillSignal=SIGTERM
TimeoutStopSec=10

[Install]
WantedBy=multi-user.target

Workflow Deployment

# .github/workflows/deploy.yml
name: Deploy

on:
  push:
    branches: [main]
  workflow_dispatch:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: '20', cache: 'npm' }
      - run: npm ci
      - run: npm test
      - run: npm run build
  
  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup SSH
        run: |
          mkdir -p ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa
          chmod 600 ~/.ssh/id_rsa
          ssh-keyscan -H ${{ secrets.SERVER_IP }} >> ~/.ssh/known_hosts
      
      - name: Deploy to VPS
        run: |
          ssh ${{ secrets.SSH_USER }}@${{ secrets.SERVER_IP }} << 'EOF'
            set -e
            cd /var/www/myapp
            git pull origin main
            npm ci --production=false
            npm run build
            npm prune --production
            sudo systemctl restart myapp
            sleep 3
            curl -f http://localhost:3000/health || exit 1
          EOF
      
      - name: Notify Slack
        if: always()
        uses: slackapi/slack-github-action@v1
        with:
          webhook-type: incoming-webhook
          webhook-url: ${{ secrets.SLACK_WEBHOOK }}
          payload: |
            {
              "text": "Deployment ${{ job.status == 'success' && '✅' || '❌' }} ${{ github.repository }}"
            }

3. Secrets dan Environment Variables

Di GitHub repo → Settings → Secrets and variables → Actions:

DEPLOY_SSH_KEY       (SSH private key)
SERVER_IP            (VPS IP address)
SSH_USER             (deployer)
SLACK_WEBHOOK        (Slack webhook URL)
DATABASE_URL         (production database URL)
JWT_SECRET           (production JWT secret)

⚠️ Jangan commit secrets ke repo! Selaluvia GitHub secrets.

4. Deploy Composer PHP dengan SSH

name: Deploy PHP

on:
  push:
    branches: [main]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Setup PHP
        uses: shivammathur/setup-php@v2
        with:
          php-version: '8.3'
          extensions: mbstring, pdo_mysql, intl
          coverage: none
      
      - name: Install Composer Dependencies
        run: composer install --no-dev --optimize-autoloader
      
      - name: Setup SSH
        uses: webfactory/ssh-agent@v0.9.0
        with:
          ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
      
      - name: Deploy via rsync
        run: |
          rsync -avz --delete \
            --exclude='.git*' \
            --exclude='node_modules' \
            --exclude='.env' \
            ./ ${{ secrets.SSH_USER }}@${{ secrets.SERVER_IP }}:/var/www/laravel-app/
      
      - name: Run migrations on server
        run: |
          ssh ${{ secrets.SSH_USER }}@${{ secrets.SERVER_IP }} << 'EOF'
            cd /var/www/laravel-app
            php artisan migrate --force
            php artisan config:cache
            php artisan route:cache
            php artisan octane:reload
          EOF

5. Parallel Jobs dan Conditional

jobs:
  build:
    strategy:
      matrix:
        node: [18, 20, 22]
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: ${{ matrix.node }}, cache: 'npm' }
      - run: npm ci && npm test
  
  notify:
    needs: build
    if: always()
    runs-on: ubuntu-latest
    steps:
      - run: echo "All builds completed. Result: ${{ needs.build.result }}"

6. Caching Dependencies

- name: Cache node modules
  uses: actions/cache@v3
  with:
    path: ~/.npm
    key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }}
    restore-keys: |
      ${{ runner.os }}-node-

- name: Cache Docker layers
  uses: actions/cache@v3
  with:
    path: /tmp/.buildx-cache
    key: ${{ runner.os }}-buildx-${{ github.sha }}
    restore-keys: |
      ${{ runner.os }}-buildx-

7. Build & Push Docker Image

- name: Login to Docker Hub
  uses: docker/login-action@v3
  with:
    username: ${{ secrets.DOCKERHUB_USERNAME }}
    password: ${{ secrets.DOCKERHUB_TOKEN }}

- name: Build and push
  uses: docker/build-push-action@v5
  with:
    context: .
    push: true
    tags: |
      myorg/myapp:latest
      myorg/myapp:${{ github.sha }}
    cache-from: type=gha
    cache-to: type=gha,mode=max

8. Health Check Setelah Deploy

- name: Wait for rollout
  run: |
    for i in {1..30}; do
      if curl -sf https://myapp.com/health; then
        echo "Health check passed"
        exit 0
      fi
      sleep 5
    done
    echo "Health check failed"
    exit 1

9. Rollback Otomatis

- name: Rollback on failure
  if: failure()
  run: |
    ssh ${{ secrets.SSH_USER }}@${{ secrets.SERVER_IP }} << 'EOF'
      cd /var/www/myapp
      git rollback ${{ github.event.before }}
      sudo systemctl restart myapp
    EOF

Kesimpulan

GitHub Actions memungkinkan setup CI/CD lengkap dalam satu file YAML. Setup awal mungkin butuh 1-2 jam, tapi setelah itu, deploy jadi 0 menit pekerjaan manual. Push → test → deploy → notif → selesai.

Mulai dari workflow sederhana: lint + test saja. Tambah deploy setelah stabil. Tambah notification setelah diperlukan. Iterative improvement adalah kunci.

I Nyoman Donostia

I Nyoman Donostia

Fullstack Developer - Bali, Indonesia

Artikel Terkait