Run pertama chapter sebelumnya meninggalkan image sha-3f9c... di ghcr.io — dibangun, didorong, selesai. Namun pipeline satu job itu justru melanggengkan kebiasaan yang ingin kita buang: image naik ke registry tanpa satu gerbang pun, karena test tidak dijalankan, scan bukan urusan job itu, dan persetujuan rilis tidak pernah diminta. Padahal checklist produksi kita menuntut semuanya berbaris disiplin: kode lulus test sebelum menjadi image, image lulus scan sebelum mendekati produksi, dan manusia memegang tombol rilis yang terakhir. Chapter ini merangkai keempatnya menjadi satu workflow multi-job pipeline-backend.yml — job test dengan database sidecar di dalam container, job build yang mendorong image ke registry, job scan dengan Docker Scout, dan job deploy yang menunggu persetujuan — lalu membedah filosofi di baliknya: mengapa registry, bukan berkas upload-artifact, yang menjadi jembatan antar job.

Workflow Utuh: pipeline-backend.yml #

Berkas baru ini hidup berdampingan dengan build-backend.yml di folder .github/workflows/, tetapi tidak lama: keduanya membaca pemicu yang sama, dan dua workflow dengan pemicu sama akan membangun image dua kali untuk satu commit. Budi menghapus build-backend.yml di commit yang sama dengan penambahan berkas berikut, sehingga pipeline-backend.yml menjadi satu-satunya pemilik ritual rilis toko:

yaml
name: Pipeline Backend

on:
  push:
    branches: [main]
    tags: ['v*']

env:
  REGISTRY: ghcr.io
  IMAGE_NAME: ${{ github.repository }}/backend

permissions:
  contents: read
  packages: write

jobs:
  test:
    runs-on: ubuntu-latest
    container:
      image: node:22-alpine
    services:
      postgres:
        image: postgres:18-alpine
        env:
          POSTGRES_USER: toko
          POSTGRES_PASSWORD: rahasia123
          POSTGRES_DB: toko_test
        ports:
          - 5432/tcp
        options: >-
          --health-cmd "pg_isready -U toko"
          --health-interval 5s
          --health-timeout 5s
          --health-retries 10
    env:
      DATABASE_URL: postgres://toko:rahasia123@postgres:5432/toko_test
    steps:
      - name: Ambil kode ke runner
        uses: actions/checkout@v4

      - name: Pasang dependensi
        working-directory: backend
        run: npm ci

      - name: Jalankan test
        working-directory: backend
        run: npm test

  build:
    needs: test
    runs-on: ubuntu-latest
    steps:
      - name: Ambil kode ke runner
        uses: actions/checkout@v4

      - name: Siapkan Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login ke GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Susun nama image dengan huruf kecil
        id: image
        run: |
          echo "name=${REGISTRY}/${IMAGE_NAME,,}" >> "$GITHUB_OUTPUT"

      - name: Build dan push image backend
        uses: docker/build-push-action@v6
        with:
          context: ./backend
          platforms: linux/amd64
          push: true
          tags: |
            ${{ steps.image.outputs.name }}:${{ github.ref_name }}
            ${{ steps.image.outputs.name }}:sha-${{ github.sha }}
          cache-from: type=gha
          cache-to: type=gha,mode=max

  scan:
    needs: build
    runs-on: ubuntu-latest
    steps:
      - name: Login ke GHCR
        uses: docker/login-action@v3
        with:
          registry: ${{ env.REGISTRY }}
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Pasang Docker Scout CLI
        run: |
          curl -fsSL https://raw.githubusercontent.com/docker/scout-cli/main/install.sh -o install-scout.sh
          sh install-scout.sh

      - name: Pindai CVE image hasil build
        run: |
          docker scout cves "${REGISTRY}/${IMAGE_NAME,,}:sha-${GITHUB_SHA}"

  deploy:
    needs: scan
    runs-on: ubuntu-latest
    environment: production
    steps:
      - name: Tampilkan info rilis
        run: echo "Rilis ${GITHUB_REF_NAME} - image sha-${GITHUB_SHA} siap dikerahkan"

      - name: Kerahkan ke server produksi
        run: |
          echo "Placeholder deploy: tarik image by digest lalu docker compose up -d"
          echo "Pola SSH, smoke test, dan rollback dibedah di chapter Deployment Automation"

Empat Job dalam Satu Barisan #

Kata kunci needs: menjahit keempat job menjadi satu barisan: job tidak lagi berjalan sendiri, melainkan menunggu pendahulunya lulus. Diagram berikut memetakan barisan lengkap beserta dua ujungnya — peristiwa pemicu di hulu dan image yang berjalan di hilir:

Diagram itu ringkas, tetapi peran tiap job pantas dibaca dalam satu tabel sebelum kita membedah masing-masing secara dalam:

JobBergantung padaPeranGerbang yang dijaga
test—menjalankan test di container node:22-alpine dengan sidecar postgreskode yang gagal test tidak berhak menjadi image
buildtestmembangun dan mendorong image ke ghcr.iohanya kode lulus test yang lahir sebagai image
scanbuildmemindai CVE image hasil build dengan Docker Scoutimage bermasalah tidak melaju ke tahap deploy
deployscanmenunggu persetujuan environment production lalu mengerahkanmanusia tetap memegang keputusan rilis terakhir

Barisan ini adalah checklist produksi yang kita audit secara manual pada section sebelumnya, diterjemahkan ke bentuk mesin: setiap baris checklist menjadi sebuah needs:, dan setiap temuan berhenti pada gerbang panggungnya masing-masing alih-alih ditemukan saat go-live.

Test Bercontainer dengan Sidecar Database #

Job test adalah job yang paling berbeda dari semua yang kita tulis sejauh ini, karena seluruh langkahnya dieksekusi di dalam container. Blok container: mengubah runner menjadi container node:22-alpine, dan blok services: menambahkan satu container pendamping di sebelahnya — sidecar postgres yang siap menerima koneksi dari langkah-langkah job.

Mari kita telaah blok services: dan kawan-kawannya pada job test:

  1. container: image: node:22-alpine — seluruh langkah job (npm ci, npm test) dieksekusi di dalam container ini, bukan di atas runner telanjang; Node.js versi 22 hadir bersama image, tanpa instalasi.
  2. services: postgres: — sidecar container dari image postgres:18-alpine, dibangunkan otomatis sebelum langkah pertama job; job dan sidecar berbagi satu jaringan internal runner.
  3. POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB — variabel inisialisasi yang membangkitkan database toko_test dengan pengguna toko, mekanisme yang sama dengan Chapter Inisialisasi Database; nilainya data uji, bukan rahasia produksi.
  4. options dengan --health-cmd "pg_isready -U toko" — job menunggu sampai postgres melaporkan diri siap lewat pg_isready sebelum langkah pertama dijalankan; gerbang kesiapan yang sama semangatnya dengan healthcheck pada Chapter Healthcheck dan Service Readiness.
  5. DATABASE_URL: postgres://toko:rahasia123@postgres:5432/toko_test — nama host pada URL adalah postgres, bukan alamat IP; label service menjadi alias jaringan yang diterjemahkan DNS, kebiasaan Chapter Container DNS dalam bentuk paling murni.
  6. working-directory: backend — langkah npm ci dan npm test dijalankan dari folder backend/, tempat package.json dan test suite berada; tidak ada cd manual yang bisa salah ketik.

Log langkah “Jalankan test” pada run berikutnya membuktikan barisan itu bekerja. Test runner bawaan Node 22 melaporkan hasilnya dalam format ringkas yang sudah biasa kita baca:

Output yang Dihasilkan (dipotong):

text
> [email protected] test
> node --test

▶ test/api-items.test.js
  ✔ membuat item baru lewat POST (12.4ms)
  ✔ menolak harga negatif (1.8ms)
...
ℹ tests 11
ℹ suites 4
ℹ pass 11
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1483.6

Mari kita telaah ringkasan tersebut:

  1. ▶ test/api-items.test.js dan tanda ✔ — suite dan test yang lulus, masing-masing satu baris dengan durasinya; test yang gagal akan tampil dengan tanda ✖ beserta alasan kegagalannya.
  2. ℹ pass 11 dan ℹ fail 0 — sebelas test lulus tanpa satu pun gagal; angka fail 0 inilah yang membuat step dianggap sukses oleh runner.
  3. Angka fail yang bukan nol membuat step gagal — step gagal menjatuhkan job test, dan karena job lain bergantung lewat needs:, seluruh barisan berhenti: tidak ada image yang dibangun hari itu.

Job scan: Docker Scout di Pipeline #

Job scan mewarisi image dari job build dan memeriksanya sebelum image itu boleh mendekati produksi. Tiga langkahnya sederhana: login ke GHCR (karena image dianalisis langsung dari registry), pemasangan CLI Docker Scout ke runner, lalu pemindaian itu sendiri. Log langkah “Pindai CVE image hasil build” pada run yang sama terbaca sebagai berikut:

Output yang Dihasilkan (dipotong):

text
i ghcr.io/budi/toko/backend:sha-3f9c2e8a1b7d5c4e6f8a0b1c2d3e4f5a6b7c8d9e
    0C 0H 4M 12L

Mari kita telaah dua baris tersebut:

  1. Baris i — image yang dianalisis, ditunjuk lewat tag sha-3f9c... yang menunjuk satu commit — satu digest — sehingga laporan ini terikat pada objek yang persis sama dengan yang diuji job test dan yang akan dikerahkan job deploy.
  2. 0C 0H — nol temuan critical dan nol high: kenaikan semver ke 5.7.2 pada rilis v1.6.0 menutup temuan CVE-2026-3928 yang dicatat audit bulanan pada Chapter Checklist Production; empat medium dan dua belas low tersisa sebagai pekerjaan siklus berikutnya, sesuai kebijakan toleransi dari Chapter Image Scanning.
  3. Belum ada gerbang tolak — docker scout cves di sini baru melapor, belum menolak; memasang ambang yang menjatuhkan pipeline secara otomatis (exit-code policy) adalah pekerjaan Chapter Image Scanning di CI/CD.

Job deploy: Gerbang Persetujuan Manusia #

Job terakhir adalah yang paling pendek sekaligus paling berkuasa, karena baris environment: production membawa sesuatu yang tidak dimiliki tiga job sebelumnya: seorang penjaga manusia. Environment dengan nama production diaktifkan di pengaturan repositori dengan required reviewer, sehingga job deploy tidak mulai sebelum salah satu reviewer menekan tombol persetujuan di halaman run — pipeline berhenti dengan status menunggu, boleh selama sepuluh menit boleh selama dua hari, dan keputusannya tercatat: siapa menyetujui, kapan, untuk run yang mana. Isi langkahnya masih placeholder echo yang menampilkan info rilis; pekerjaan sesungguhnya — menarik image by digest ke server lewat SSH, menjalankan smoke test, dan jalur rollback — adalah materi Chapter Deployment Automation. Pola gerbangnya saja yang penting sekarang: keempat job menuntaskan otomasi yang bisa dipercaya, dan manusia tetap memegang kunci pintu terakhir.

Registry sebagai Rumah Artifact #

Ada satu pertanyaan struktural yang belum terjawab: bagaimana image berpindah dari job build ke job scan dan job deploy, padahal setiap job berjalan di runner yang segar — mesin baru yang tidak menyimpan apa pun dari job sebelumnya? Jawaban klasik CI adalah upload artifact: berkas diarsipkan oleh satu job lalu diunduh job berikutnya. Jawaban untuk pipeline container berbeda, dan lebih elegan: registry adalah rumah artifact itu sendiri.

Filosofinya layak dibedah perlahan. Pertama, artifact kita memang bukan berkas biasa — artifact kita adalah image, dan registry adalah sistem penyimpanan yang dirancang khusus untuknya: identitas immutable lewat digest, distribusi lintas mesin lewat pull, dan penghematan lewat layer yang terdeduplikasi. Kedua, job berikutnya tidak perlu salinan — job berikutnya cukup menunjuk: tag sha-${GITHUB_SHA} pada job scan menunjuk objek yang sama dengan yang barusan di-push job build, dan job deploy dapat menunjuk lebih dalam lagi dengan digest penuh ghcr.io/budi/toko/backend@sha256:... — byte yang sama yang diuji, discan, dan dikerahkan. Ketiga, audit trail hidup di registry, bukan di arsip runner yang hangus: kapan pun seseorang bertanya “image apa yang berjalan di produksi hari itu”, jawabannya adalah satu digest yang dapat ditarik ulang dan diperiksa ulang.

Dengan kata lain: upload-artifact menyimpan salinan untuk dikirim antar job, sedangkan registry menyimpan identitas yang bisa dirujuk siapa saja — dan untuk objek yang sudah punya sistem identitas sebaik content-addressable storage, membuat salinan kedua hanyalah pekerjaan ganda.

Variasi: compose.ci.yaml untuk Stack yang Lebih Besar #

Sidecar postgres menjawab kebutuhan job test toko hari ini, tetapi saat test tumbuh membutuhkan lebih banyak tetangga — cache Redis, proxy, worker — menulis semuanya sebagai blok services: mulai terasa seperti menyalin compose.yaml ke bahasa lain. Kebiasaan yang lebih rapi untuk kasus itu adalah berkas compose khusus pipeline: compose.ci.yaml di repositori, dipanggil eksplisit dari langkah run:

bash
$ docker compose -f compose.ci.yaml up --wait
$ docker compose -f compose.ci.yaml run --rm backend npm test

Nama compose.ci.yaml tidak termasuk nama yang dicari otomatis oleh plugin compose, sehingga flag -f yang eksplisit justru menyatakan niat: berkas ini khusus lingkungan CI dengan profil dan variabelnya sendiri. Flag --wait menahan perintah sampai seluruh service melaporkan diri sehat lewat healthcheck yang dideklarasikan di dalam berkas — gerbang kesiapan yang sama dengan pg_isready, hanya saja dikelola compose. Kedua perintah di atas berjalan di runner yang menyediakan Docker, bukan di dalam job container, sehingga pola ini menggantikan services: saat kebutuhan melebihi satu-dua sidecar.

Lencana Status di README #

Pipeline sebaik apa pun kehilangan separuh nilainya bila tidak terlihat. README repositori toko kini membuka satu baris baru di bagian atas — lencana status yang menampilkan warna run terakhir dari pipeline-backend.yml:

text
![status pipeline backend](https://github.com/budi/toko/actions/workflows/pipeline-backend.yml/badge.svg)

Lencana hijau berarti seluruh gerbang — test, build, scan, persetujuan deploy — lolos pada commit terbaru cabang utama; lencana merah berarti salah satu gerbang berhenti dan main sedang tidak sehat. Yang menarik dari kebiasaan ini bukan kebanggaan warna, melainkan kejujurannya: setiap pengunjung repositori, termasuk calon kontributor, langsung tahu keadaan main hari ini tanpa membuka tab Actions, dan tim tidak bisa bersembunyi dari pipeline merah di balik README yang rapi.

Empat job kini berbaris di belakang satu push: test menjaga pintu masuk dengan database sidecar, build melahirkan image di registry, scan memeriksa isinya, dan deploy menunggu persetujuan manusia — seluruh checklist produksi yang dulu diaudit dengan lembar cetak kini diaudit oleh mesin pada setiap commit. Namun ada satu angka yang belum kita periksa: durasi. Setiap job berjalan di runner kosong tanpa satu byte cache BuildKit, sehingga npm ci dan seluruh layer mahal dibayar penuh pada setiap run, dan type=gha dari chapter sebelumnya baru menyimpan sebagian jawabannya. Bagaimana cache menyeberang dari mesin ke mesin lewat registry, seberapa besar penghematannya, dan risiko apa yang menyertainya, adalah pekerjaan Chapter Build Cache di CI/CD.

Referensi #