Skip to main content

Setup Git Bare Repository untuk CI/CD

·7 mins

Git Bare Repository adalah jenis repositori Git yang tidak memiliki direktori kerja aktif (working tree) dan hanya berisi metadata seperti branch, tag, log, dan riwayat commit. Jenis repositori ini cocok digunakan sebagai alternatif internal dari GitHub karena dapat diakses bersama oleh banyak developer serta mendukung otomatisasi deployment menggunakan Git Hooks.

Setup Git Bare Repository
#

Buat direktori repositori di server, misalnya di /srv/git atau /opt/git.

mkdir -p /srv/git/nama-proyek.git

Masuk ke direktori tersebut, lalu inisialisasi repositori bare menggunakan perintah berikut.

cd /srv/git/nama-proyek.git
git init --bare

Agar repositori dapat diakses dan dimodifikasi oleh sekelompok pengguna, gunakan opsi --shared.

git init --bare --shared=group /srv/git/nama-proyek.git

Opsional: sesuaikan konfigurasi Git Bare pada bagian receive dan gc seperti berikut.

[core]
    repositoryformatversion = 0
    filemode = true
    bare = true
    logallrefupdates = true
    sharedrepository = 1

[receive]
    denyCurrentBranch = updateInstead

[gc]
    auto = 0

Setelah itu, ubah kepemilikan dan izin direktori agar grup tertentu memiliki akses penuh.

sudo chgrp -R git /srv/git/nama-proyek.git
sudo chmod -R 775 /srv/git/nama-proyek.git

Lakukan pengujian dengan melakukan clone repositori.

git clone user@server:/srv/git/nama-proyek.git

Mengubah Repositori Lokal Menjadi Bare Repository
#

Jika Anda sudah memiliki repositori biasa di komputer atau server dan ingin mengubahnya menjadi repositori bare, gunakan perintah berikut.

git clone --bare /path/to/local/repo /srv/git/nama-proyek.git

Git Hooks
#

Salah satu keunggulan Git Bare Repository adalah kemampuannya menjalankan otomatisasi menggunakan Git Hooks. Fitur ini sangat berguna untuk kebutuhan auto deploy maupun proses Continuous Integration / Continuous Deployment (CI/CD), di mana kode akan otomatis diterapkan ke server setiap kali ada perubahan.

Beberapa hook yang umum digunakan:

  • pre-receive Berjalan sebelum server menerima perubahan. Umumnya digunakan untuk validasi kode, pemeriksaan format commit, atau mencegah force push.

  • post-receive Berjalan setelah server menerima perubahan. Biasanya digunakan untuk deployment otomatis, mengirim notifikasi, atau menjalankan workflow tertentu.

Masuk ke direktori hooks.

cd /srv/git/nama-proyek.git/hooks

Buat dan edit file post-receive.

nano post-receive

Contoh script post-receive untuk melakukan deploy otomatis ke direktori aplikasi.

#!/bin/bash

TARGET="/var/www/proyek"
GIT_DIR="/srv/git/nama-proyek.git"
BRANCH="main"

echo "=== POST-RECEIVE HOOK EXECUTED ==="
echo "Repository: $(basename "$(pwd)")"
echo "Time: $(date)"

while read oldrev newrev ref; do
    branch=$(git rev-parse --symbolic --abbrev-ref "$ref")

    echo "Branch: $branch"
    echo "Old revision: $oldrev"
    echo "New revision: $newrev"
    echo "-------------------"

    if [[ "$ref" == "refs/heads/$BRANCH" ]]; then
        echo "Deploying $BRANCH branch to production..."

        git \
            --work-tree="$TARGET" \
            --git-dir="$GIT_DIR" \
            checkout -f "$BRANCH"

        cd "$TARGET" || exit 1

        # Jalankan perintah tambahan di sini, misalnya:
        # composer install --no-dev --optimize-autoloader
        # docker compose up -d
        # npm run build
    fi
done

echo "=== POST-RECEIVE HOOK COMPLETED ==="

Berikan izin eksekusi pada file hook.

chmod +x post-receive

Script Hooks
#

Berikut beberapa contoh Git Hooks yang umum digunakan pada Git Bare Repository untuk validasi branch dan deployment otomatis.

Hook pre-receive
#

Hook pre-receive digunakan untuk memvalidasi perubahan sebelum push diterima oleh server. Contoh berikut membatasi push hanya ke branch main dan develop, serta memberikan peringatan jika commit message mengandung kata WIP.

#!/bin/bash

# pre-receive:
# Memvalidasi branch dan commit message sebelum push diterima.

while read oldrev newrev refname; do
    # Ambil nama branch dari referensi Git
    branch="${refname#refs/heads/}"

    # Hanya izinkan branch tertentu
    if [[ "$branch" != "main" && "$branch" != "develop" ]]; then
        echo "ERROR: Push ke branch '$branch' tidak diizinkan."
        echo "Hanya branch 'main' dan 'develop' yang diperbolehkan."
        exit 1
    fi

    # Periksa commit message terbaru
    if git log -1 --pretty=format:"%s" "$newrev" | grep -qi "WIP"; then
        echo "WARNING: Commit dengan message 'WIP' terdeteksi."
        echo "Push tetap diizinkan."
    fi
done

echo "Pre-receive validation passed."
exit 0

Hook post-receive Multi-Branch Deployment
#

Hook post-receive berikut digunakan untuk deployment otomatis berdasarkan branch yang di-push.

  • main → production
  • develop → staging
  • feature/* → environment khusus feature branch
#!/bin/bash

# post-receive:
# Multi-branch automatic deployment

BASE_DIR="/var/www"
GIT_DIR="$(pwd)"
REPO_NAME="$(basename "$GIT_DIR" .git)"

echo "=== Starting multi-branch deployment ==="

while read oldrev newrev refname; do
    branch="${refname#refs/heads/}"

    # Tentukan direktori target berdasarkan branch
    case "$branch" in
        main)
            TARGET="$BASE_DIR/$REPO_NAME/production"
            echo "Deploying PRODUCTION environment"
            ;;

        develop)
            TARGET="$BASE_DIR/$REPO_NAME/staging"
            echo "Deploying STAGING environment"
            ;;

        feature/*)
            feature_name="${branch#feature/}"
            TARGET="$BASE_DIR/$REPO_NAME/features/$feature_name"

            echo "Deploying FEATURE branch: $feature_name"

            mkdir -p "$TARGET"
            ;;

        *)
            TARGET="$BASE_DIR/$REPO_NAME/$branch"

            echo "Deploying branch: $branch"

            mkdir -p "$TARGET"
            ;;
    esac

    # Pastikan direktori target tersedia
    mkdir -p "$TARGET"

    # Deploy branch ke target directory
    git \
        --work-tree="$TARGET" \
        --git-dir="$GIT_DIR" \
        checkout -f "$branch"

    # Simpan informasi deployment
    {
        echo "Deployed : $(date)"
        echo "Branch    : $branch"
        echo "Commit    : $newrev"
    } > "$TARGET/deploy-info.txt"

done

echo "=== Multi-branch deployment completed ==="

Hook post-receive dengan Logging dan Notifikasi Slack
#

Berikut contoh hook post-receive yang lebih production-ready dengan beberapa praktik terbaik seperti:

  • atomic deployment menggunakan symbolic link
  • logging terpusat
  • cleanup release lama
  • notifikasi ke Slack
  • error handling yang lebih aman
  • deployment berbasis commit hash

Struktur deployment yang dihasilkan kurang lebih seperti berikut:

/home/devops/proyek/
├── main -> releases/main-a1b2c3d
├── develop -> releases/develop-e4f5g6h
└── releases/
    ├── main-a1b2c3d/
    ├── main-x7y8z9a/
    └── develop-e4f5g6h/

Script hook:

#!/bin/bash

# post-receive:
# Production-ready deployment hook dengan logging,
# rollback-friendly release management,
# dan notifikasi Slack.

# =========================================================
# Konfigurasi
# =========================================================

# Base directory deployment
: "${DEPLOY_BASE_DIR:=/home/devops}"

# Nama repository
REPO_NAME=$(basename "$(pwd)")
[[ "$REPO_NAME" == *.git ]] && REPO_NAME="${REPO_NAME%.git}"

# File log
: "${LOG_FILE:=/home/devops/logs/git-deploy.log}"

# Slack webhook URL
: "${SLACK_WEBHOOK:=}"

# Email admin (opsional)
: "${ADMIN_EMAIL:=}"

# =========================================================
# Logging dan shell safety
# =========================================================

mkdir -p "$(dirname "$LOG_FILE")"

exec >> "$LOG_FILE" 2>&1

echo "[$(date)] Hook triggered"

set -euo pipefail

# =========================================================
# Fungsi notifikasi
# =========================================================

notify() {
    local level="$1"
    local msg="$2"

    echo "[$level] $msg"

    if [[ -n "$SLACK_WEBHOOK" ]]; then
        curl -s \
            -X POST \
            -H 'Content-type: application/json' \
            --data "{\"text\":\"[$level] $msg\"}" \
            "$SLACK_WEBHOOK" >/dev/null || true
    fi
}

# =========================================================
# Cleanup release lama
# =========================================================

cleanup_old_releases() {
    local branch="$1"

    local releases_dir="$DEPLOY_BASE_DIR/$REPO_NAME/releases"
    local keep=2

    [[ -d "$releases_dir" ]] || return

    echo "[INFO] Cleaning old releases (keep=$keep)"

    local target_link="$DEPLOY_BASE_DIR/$REPO_NAME/$branch"

    local current_active=""
    if [[ -L "$target_link" ]]; then
        current_active=$(readlink -f "$target_link")
    fi

    (
        cd "$releases_dir" || exit 0

        shopt -s nullglob

        local dirs=()

        while IFS= read -r dir; do
            dirs+=("$dir")
        done < <(
            for d in "${branch}-"*/; do
                echo "${d%/}"
            done \
                | xargs -I{} stat --format="%Y {}" {} 2>/dev/null \
                | sort -rn \
                | awk '{print $2}'
        )

        [[ ${#dirs[@]} -gt 0 ]] || {
            echo "[INFO] No releases found for branch: $branch"
            return
        }

        local count=0

        for dir in "${dirs[@]}"; do
            count=$((count + 1))

            # Simpan release terbaru
            if [[ $count -le $keep ]]; then
                echo "[KEEP] $dir"
                continue
            fi

            local abs_dir
            abs_dir=$(readlink -f "$dir" 2>/dev/null || echo "")

            [[ -n "$abs_dir" ]] || {
                echo "[WARN] Failed resolving path: $dir"
                continue
            }

            # Hindari menghapus release aktif
            if [[ -n "$current_active" && "$abs_dir" == "$current_active" ]]; then
                echo "[WARN] Skipping active release: $dir"
                continue
            fi

            echo "[DELETE] Removing old release: $dir"
            rm -rf "$dir"
        done
    )
}

# =========================================================
# Atomic deployment
# =========================================================

deploy_atomic() {
    local branch="$1"
    local commit_hash="$2"

    local target_link="$DEPLOY_BASE_DIR/$REPO_NAME/$branch"

    local new_dir="$DEPLOY_BASE_DIR/$REPO_NAME/releases/${branch}-${commit_hash:0:7}"

    mkdir -p "$new_dir"

    # Extract repository content
    git \
        --git-dir="$(pwd)" \
        archive "$commit_hash" \
        | tar -x -C "$new_dir"

    # Deployment metadata
    {
        echo "Repository : $REPO_NAME"
        echo "Branch     : $branch"
        echo "Commit     : $(git rev-parse "$commit_hash")"
        echo "Deployed   : $(date)"
    } > "$new_dir/deploy-info.txt"

    # Update symlink secara atomic
    mkdir -p "$(dirname "$target_link")"

    ln -sfn "$new_dir" "$target_link"

    cleanup_old_releases "$branch"

    echo "$new_dir"
}

# =========================================================
# Main process
# =========================================================

while read oldrev newrev ref; do
    branch="${ref#refs/heads/}"

    echo "[INFO] Processing branch=$branch old=$oldrev new=$newrev"

    # Skip HEAD
    [[ "$branch" == "HEAD" ]] && continue

    case "$branch" in
        main|master|develop)

            notify \
                "INFO" \
                "Starting deployment: branch=$branch commit=${newrev:0:7}"

            if deploy_dir=$(deploy_atomic "$branch" "$newrev"); then

                # =================================================
                # Post-deployment commands
                # =================================================

                # (
                #     cd "$deploy_dir"
                #
                #     if [[ -f composer.json ]]; then
                #         composer install \
                #             --no-dev \
                #             --no-interaction \
                #             --optimize-autoloader
                #     fi
                #
                #     if [[ -f package.json ]]; then
                #         npm install --production
                #     fi
                #
                #     if [[ -f artisan ]]; then
                #         php artisan migrate --force
                #     fi
                # )

                notify \
                    "SUCCESS" \
                    "Deployment completed: branch=$branch dir=$deploy_dir"

            else
                notify \
                    "ERROR" \
                    "Deployment failed for branch=$branch"
            fi
            ;;

        *)
            echo "[INFO] Branch '$branch' ignored"
            ;;
    esac
done

echo "[$(date)] Hook finished"

exit 0

Related