Rilis pertama ke production terasa seperti petualangan; rilis kesepuluh harus terasa seperti menekan satu tombol. Jarak antara keduanya adalah workflow: urutan langkah yang sama, dieksekusi dengan cara yang sama, dengan hasil yang bisa diverifikasi setiap saat. Pada chapter ini kita susun urutan lengkap sebuah rilis Laravel — dari maintenance mode hingga smoke test — lalu membuktikannya sebagai skrip yang benar-benar kita jalankan, bukan sekadar daftar saran. Seluruh blok bangunan sudah kita kuasai satu per satu di chapter-chapter sebelumnya; tugas sekarang adalah merangkainya.

Anatomi Satu Rilis #

Urutan berikut adalah kerangka yang dipakai hampir semua aplikasi Laravel. Delapan langkahnya sengaja berurutan — beberapa di antaranya hanya aman dilakukan pada posisinya:

#LangkahPerintah intiMengapa di posisi itu
1Aktifkan maintenancephp artisan downPengguna tidak melihat setengah versi
2Ambil kode barugit pull origin mainKode sebelum migrasi, karena migrasi ikut kode baru
3Pasang dependencycomposer install --no-dev --optimize-autoloaderAutoloader baru untuk kode baru
4Build aset frontendnpm ci && npm run buildAset baru tersedia sebelum halaman ditampilkan
5Migrasi databasephp artisan migrate --forceSkema baru siap sebelum kode aktif
6Bangun ulang cacheoptimize:clear + optimizeCache lama tidak boleh menyembunyikan kode baru
7Restart workerphp artisan queue:restartWorker memuat kode lama di memori
8Buka aplikasi + verifikasiphp artisan up + smoke testBukti akhir bahwa rilis sehat

Langkah 5 dan 6 adalah jantung keamanan rilis: migrasi produksi dengan strategi kolom baru sudah dibahas di Database Migration di Production , sementara alasan membangun ulang cache sudah dibuktikan di Caching di Production . Langkah 7 tanpa queue:restart membuat job dieksekusi memakai kode yang sudah tidak ada di disk — sinyal restart-nya sudah kita lihat bekerja di Queue Worker di Production .

Skrip Mini yang Benar-Benar Dijalankan #

Daftar di atas hanya berguna bila menjadi skrip. Versi mini berikut kita jalankan utuh di sandbox; di server, tiga langkah pembuka disisipkan sebelum baris php artisan down — git pull origin main, composer install --no-dev --optimize-autoloader --no-interaction, dan npm ci && npm run build — karena sandbox tidak punya remote Git dan Node:

bash
#!/usr/bin/env bash
set -euo pipefail
APP_DIR="/var/www/tokobudi"
cd "$APP_DIR"

echo "== 1. maintenance mode ON =="
php artisan down

echo "== 2. migrasi database =="
php artisan migrate --force

echo "== 3. bangun ulang cache =="
php artisan optimize:clear
php artisan optimize

echo "== 4. restart queue worker =="
php artisan queue:restart

echo "== 5. pastikan storage:link =="
php artisan storage:link

echo "== 6. maintenance mode OFF =="
php artisan up

Baris set -euo pipefail adalah pengaman skrip: -e menghentikan eksekusi begitu satu langkah gagal (migrasi gagal → aplikasi tidak dibuka), -u menolak variabel kosong, dan -o pipefail menangkap kegagalan di tengah pipe. Hasil eksekusinya di sandbox:

text
== 1. maintenance mode ON ==

   INFO  Application is now in maintenance mode.  

== 2. migrasi database ==

   INFO  Nothing to migrate.  

== 3. bangun ulang cache ==

   INFO  Clearing cached bootstrap files.  

  config ......................................................... 0.89ms DONE
  cache ........................................................ 10.14ms DONE
  compiled ....................................................... 0.76ms DONE
  events ......................................................... 0.48ms DONE
  routes ......................................................... 0.49ms DONE
  views .......................................................... 2.80ms DONE


   INFO  Caching framework bootstrap, configuration, and metadata.  

  config ......................................................... 7.10ms DONE
  events ......................................................... 0.94ms DONE
  routes ......................................................... 8.63ms DONE
  views .......................................................... 15.64ms DONE

== 4. restart queue worker ==

   INFO  Broadcasting queue restart signal.  

== 5. pastikan storage:link ==

   ERROR  The [public/storage] link already exists.  

== 6. maintenance mode OFF ==

   INFO  Application is now live.  

Dua detail dari output layak dijelaskan. Nothing to migrate bukan skrip gagal — migrasi memang hanya berjalan bila ada migration baru, dan sisanya adalah keadaan normal rilis biasa. Lalu baris ERROR The [public/storage] link already exists tampak menakutkan padahal jinak: tautan sudah dibuat rilis sebelumnya, storage:link bersifat idempoten, dan exit code tetap 0 sehingga skrip lanjut ke langkah penutup.

Skrip yang sama juga menjawab pertanyaan klasik “apa yang dilihat pengguna selama maintenance?”. Selama mode aktif kita ukur dua endpoint:

text
GET /:    HTTP 503
GET /up: HTTP 200

Pengguna melihat halaman 503 dengan status maintenance, sementara health check tetap hijau — kombinasi yang sudah dibedah di Logging dan Monitoring .

Smoke Test Pasca-Deploy #

Langkah terakhir tabel tadi menuntut bukti, bukan kepercayaan. Smoke test adalah tiga-empat curl yang memastikan halaman-halaman inti menjawab sebagaimana mestinya; URL dasar dikirim sebagai argumen pertama:

bash
#!/usr/bin/env bash
BASE_URL="${1:-https://tokobudi.example.com}"
GAGAL=0

periksa() {
    local nama="$1" url="$2" diharapkan="$3"
    local status
    status=$(curl -s -o /dev/null -w '%{http_code}' "$url")
    if [ "$status" = "$diharapkan" ]; then
        echo "LULUS  $nama ($url) -> $status"
    else
        echo "GAGAL  $nama ($url) -> $status (diharapkan $diharapkan)"
        GAGAL=1
    fi
}

periksa "health check" "$BASE_URL/up" 200
periksa "halaman utama" "$BASE_URL/" 200
periksa "halaman tidak ditemukan" "$BASE_URL/tidak-ada" 404

exit $GAGAL

Kita jalankan terhadap server sandbox:

bash
bash smoke.sh http://127.0.0.1:8018

Output yang Dihasilkan:

text
LULUS  health check (http://127.0.0.1:8018/up) -> 200
LULUS  halaman utama (http://127.0.0.1:8018/) -> 200
LULUS  halaman tidak ditemukan (http://127.0.0.1:8018/tidak-ada) -> 404

Exit code 0 berarti seluruh pemeriksaan lulus. Karena skrip berbicara lewat exit code, smoke test ini bisa langsung disandarkan di akhir skrip deploy maupun pipeline CI — rilis yang gagal smoke test otomatis ditandai gagal.

Tier Otomasi: Manual, Skrip, Pipeline #

Skrip di atas disimpan sebagai deploy.sh di server dan dieksekusi lewat SSH — tier pertama otomasi. Tier berikutnya memindahkan pemicunya dari tangan ke Git: pipeline CI/CD menjalankan skrip yang sama setiap kali tag rilis didorong. Contoh tampilan pipeline GitHub Actions:

yaml
name: Deploy
on:
  push:
    tags: ["v*"]
jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - name: Jalankan deploy di server
        uses: appleboy/ssh-action@v4
        with:
          host: ${{ secrets.DEPLOY_HOST }}
          username: deploy
          key: ${{ secrets.DEPLOY_SSH_KEY }}
          script: bash /var/www/tokobudi/deploy.sh

Kredensial server hidup di secrets repository, bukan di skrip — prinsip yang sama dengan Secret Management . Rekomendasi praktisnya: jangan lompat ke tier pipeline sebelum skrip manual terbukti stabil berpuluh kali.

Deploy Atomik: Tanpa Jendela Maintenance #

Skrip kita memakai maintenance mode, sehingga ada jendela waktu (detik hingga menit) ketika pengguna ditolak. Pola atomic deploy menghapus jendela itu lewat struktur direktori — contoh tampilan:

text
/var/www/tokobudi/
├── releases/
│   ├── 2026-10-13-0900/
│   └── 2026-10-14-1130/
├── shared/
│   ├── .env
│   └── storage/
└── current -> releases/2026-10-14-1130

Rilis baru dibangun utuh di dalam releases/<versi>/ — kode, dependency, aset, migrasi — sementara current yang ditunjuk document root Nginx masih menunjuk rilis lama yang terus melayani request. Saat semuanya siap, pergantian hanyalah satu perintah: ln -sfn releases/2026-10-14-1130 current — pembuatan symlink bersifat atomik di level filesystem, sehingga tidak ada request yang menangkap setengah versi. .env dan storage/ ditaruh di shared/ dan di-symlink ke tiap rilis agar upload dan konfigurasi tidak ikut terganti. Manfaat sampingannya besar: direktori rilis lama tetap utuh, dan “rollback” tinggal membalik arah symlink — cerita lengkapnya di Rollback dan Recovery .

Alur rilis kini berulang dan teruji. Pertanyaan berikutnya adalah rencana B: apa yang kita lakukan ketika rilis yang baru saja dianggap sehat itu ternyata bermasalah — dibahas tuntas di Rollback dan Recovery .