Build toko kini selesai dalam sembilan puluh detik, dan tab Actions berubah dari sumber frustasi menjadi tempat yang menyenangkan untuk dikunjungi. Namun kecepatan itu menutupi satu kebohongan kecil yang kita warisi dari pipeline lama: langkah docker scout pada Chapter Build, Test, dan Publish memang menjalankan pemindaian, tetapi hasilnya hanya dicetak ke log — job tetap hijau berapa pun banyaknya kerentanan kritis yang ditemukan. Pemindaian yang tidak pernah bisa menggagalkan apa pun bukanlah gerbang; pemindaian itu hanyalah laporan yang dibaca orang saat ingat. Chapter ini memberikan pemindaian daya untuk berkata “tidak”: kebijakan exit-code yang membuat job gagal saat temuan melampaui ambang, alur tiga job yang menahan tag cantik sampai gerbang lolos, kebijakan toleransi yang tertulis beserta pengecualiannya, serta SBOM yang dijadikan artefak rilis untuk audit antar versi.

Gerbang, Bukan Laporan #

Gerbang adalah pemeriksaan yang bisa menggagalkan pipeline, dan mekanismenya memakai kontrak tertua antara program dan sistem yang menjalankannya: exit code. Program keluar dengan 0 berarti sukses; keluar dengan nilai lain berarti gagal — dan GitHub Actions menerjemahkan nilai bukan nol menjadi job merah, persis kontrak yang kita bedah pada Chapter Memahami Exit Code. Pemindaian lama toko tidak punya daya menolak justru karena selalu keluar dengan 0: tabel temuan dicetak, lalu job ditandai sukses. Docker Scout memiliki flag khusus untuk mematahkan kebiasaan itu: --exit-code membuat perintah keluar dengan kode 2 saat ada temuan yang lolos filter, dan kode 2 itulah yang mengubah laporan menjadi gerbang.

Struktur workflow juga ikut berubah. Tag main dan latest tidak lagi lahir langsung dari job build; image terlebih dahulu menunggu di satu alamat netral — tag sha — seperti penumpang di ruang tunggu sebelum diberi boarding pass. Job scan memeriksa image di ruang tunggu itu; hanya image yang lulus yang berpindah ke job publish untuk menerima nama-nama manisnya. Alur lengkapnya tampak pada diagram berikut:

Mari kita telaah dua jalur pada diagram tersebut: jalur kiri adalah hari biasa — scan bersih, publish berjalan, tag main bergeser ke image baru. Jalur kanan adalah hari yang kita siapkan — scan menemukan kritis, job gagal, dan seluruh job yang bergantung padanya dilewati otomatis oleh GitHub Actions; image bermasalah tidak pernah menerima nama yang membuatnya terlihat seperti rilis resmi.

Workflow Tiga Job #

Bentuk lengkap workflow dengan tiga job yang dirantai berjalan sebagai berikut:

yaml
name: pipeline

on:
  push:
    branches: [main]
  pull_request:

jobs:
  build:
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    outputs:
      sha-pendek: ${{ steps.sha.outputs.pendek }}
    steps:
      - uses: actions/checkout@v4
      - id: sha
        run: echo "pendek=${GITHUB_SHA:0:7}" >> "$GITHUB_OUTPUT"
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Build dan dorong tag sha
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/budi/toko:sha-${{ steps.sha.outputs.pendek }}
          cache-from: type=registry,ref=ghcr.io/budi/toko:buildcache
          cache-to: type=registry,ref=ghcr.io/budi/toko:buildcache,mode=max

  scan:
    needs: build
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: read
    steps:
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Pindai CVE kritis
        run: |
          docker scout cves \
            --only-severity critical \
            --exit-code \
            ghcr.io/budi/toko:sha-${{ needs.build.outputs.sha-pendek }}

  publish:
    needs: [build, scan]
    if: github.ref == 'refs/heads/main' && github.event_name == 'push'
    runs-on: ubuntu-latest
    permissions:
      contents: read
      packages: write
    steps:
      - uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.actor }}
          password: ${{ secrets.GITHUB_TOKEN }}
      - name: Pasang tag cantik setelah gerbang lolos
        run: |
          docker pull ghcr.io/budi/toko:sha-${{ needs.build.outputs.sha-pendek }}
          docker tag ghcr.io/budi/toko:sha-${{ needs.build.outputs.sha-pendek }} ghcr.io/budi/toko:main
          docker push ghcr.io/budi/toko:main

Mari kita telaah lima poin alur pada workflow di atas:

  1. Job build mendorong tag sha saja — tujuh karakter pertama SHA commit menjadi alamat byte yang stabil; image menunggu di alamat itu seperti di ruang tunggu, tanpa nama yang menjanjikan apa pun kepada konsumen. Pull request pun mendapat perlakuan sama: diuji dan dipindai tanpa pernah menyentuh tag main.
  2. Job scan membaca image dari registry, bukan dari disk runner — docker scout cves menarik manifest dari GHCR berdasarkan tag sha, sehingga yang diperiksa persis byte yang diperiksa identitasnya; flag --only-severity critical membatasi hitungan ke temuan kritis, dan --exit-code mengubah keberadaan temuan menjadi keputusan.
  3. needs merangkai urutan dan menegakkan konsekuensi — scan menunggu build selesai; publish menunggu keduanya. Ketika scan gagal, GitHub Actions otomatis melewatkan seluruh job yang bergantung padanya, tanpa satu baris logika tambahan dari kita.
  4. Output sha-pendek melintas antar job lewat outputs — nilai yang dihitung pada step job build diekspos sebagai output job, lalu dibaca job lain melalui needs.build.outputs; pola ini menjaga satu sumber kebenaran untuk alamat image di seluruh workflow.
  5. Tag cantik menjadi milik job publish — main hanya lahir setelah gerbang lolos, lewat docker pull, docker tag, dan docker push pada image yang sama; inilah invarian yang dijaga seluruh struktur: nama yang manis hanya melekat pada image yang lulus pemeriksaan.

Saat Gerbang Menutup #

Suatu Senin, merge sebuah pull request membawa kenaikan dependensi transitif, dan untuk pertama kalinya sejak gerbang dipasang, jalur kanan diagram benar-benar dijalankan. Log job scan berbunyi:

Output yang Dihasilkan (dipotong):

text
### Overview

target:
  └─ ghcr.io/budi/toko:sha-4d8e1c9

    digest      sha256:6f2a9e1b7c4d8305f1a9c3e5b7d9f1a3c5e7b9d1f3a5c7e9b1d3f5a7c9e1b3d5
    os          alpine 3.20
    size        142MB
    packages    118
...
### CVEs found: 28 (1C 2H 8M 17L)

    ✗ 1C     2H     8M     17L

CVE ID          SEVERITY    AFFECTED PACKAGE    FIXED IN
CVE-2026-11875  CRITICAL    libssl3              3.3.2-r0
...
Error: Process completed with exit code 2.

Mari kita telaah isi kegagalan tersebut:

  1. ✗ 1C 2H 8M 17L — rekap temuan setelah seluruh isi image diinventarisasi: satu CRITICAL, dua HIGH, delapan MEDIUM, tujuh belas LOW; tanda silang mengawali baris karena ada kategori yang memicu keputusan.
  2. CVE-2026-11875 pada libssl3 dengan FIXED IN 3.3.2-r0 — temuan kritis dengan versi perbaikan yang tersedia; kolom FIXED IN mengubah temuan menjadi pekerjaan yang jelas, persis pembacaan tabel CVE dari Chapter Image Scanning.
  3. exit code 2 — bahasa Docker Scout untuk “ada temuan yang memenuhi filter”; nilai khusus ini membedakan “ditemukan kerentanan” (2) dari “perintah itu sendiri gagal” (1).
  4. Error: Process completed with exit code 2. — GitHub Actions menerjemahkan exit code bukan nol menjadi kegagalan job; baris penutup yang akrab itu adalah seluruh jembatan antara scanner dan sistem CI.
  5. Job publish tidak menghasilkan log sama sekali — tidak ada docker tag, tidak ada docker push; tag main tidak bergeser, dan siapa pun yang menarik ghcr.io/budi/toko:main malam itu masih menerima image yang lama dan sudah teruji.

Perbaikannya sendiri berlangsung singkat keesokan paginya: Budi menaikkan paket yang membawa libssl3 ke 3.3.2-r0, mendorong commit baru, dan pipeline hijau kembali — tag sha yang baru lolos gerbang, lalu menerima tag main. Siklus gagal-perbaiki-lolos inilah bentuk kerja harian yang sehat dari sebuah gerbang: merah sesaat, bukan merah selamanya.

Kebijakan yang Tertulis #

Exit-code hanya mengeksekusi kebijakan; kebijakannya sendiri harus tertulis agar keputusan bisa diaudit dan diperdebatkan tanpa membaca workflow. Tabel berikut adalah kebijakan toko, memperluas kerangka ambang dari Chapter Image Scanning:

TemuanAksi pipelineAlasan
CRITICAL dengan versi perbaikan tersediagagal — gerbang menutupperbaikan tersedia; menolak adalah murah
CRITICAL tanpa versi perbaikanpengecualian tercatat dengan alasan dan tanggal kedaluwarsatidak bisa diperbaiki dengan upgrade; keputusan diterima secara sadar dan sementara
HIGH, jumlah 5 atau kuranglulus dengan laporanwajib tiket perbaikan pada siklus berikutnya
MEDIUM dan LOWlapor sajadipantau, dibereskan bertahap

Baris pertama ditegakkan langsung oleh --only-severity critical --exit-code. Baris kedua tidak bisa ditegakkan scanner — cacat tanpa perbaikan tidak hilang dengan upgrade — sehingga jalurnya adalah dokumen pengecualian yang mencantumkan alasan dan tanggal kedaluwarsa, persis disiplin yang kita tanam di Chapter Image Scanning. Baris ketiga ditegakkan dengan menyaring keluaran JSON scanner lalu menghitungnya sebelum memutuskan; ambang HIGH 5 adalah pilihan toko, bukan hukum alam. Baris keempat sengaja tidak memblokir — gerbang yang memblokir segalanya akan dibongkar orang.

Alternatif: trivy-action #

Docker Scout bukan satu-satunya penjaga pintu. Trivy — pemindai yang kita kenal di Chapter Image Scanning — memiliki action resmi di marketplace yang membungkus pola yang sama dalam satu langkah:

yaml
      - name: Pindai dengan Trivy
        uses: aquasecurity/trivy-action@master
        with:
          image-ref: ghcr.io/budi/toko:sha-${{ needs.build.outputs.sha-pendek }}
          severity: CRITICAL,HIGH
          ignore-unfixed: true
          exit-code: 1
          format: table

Mari kita telaah empat input yang membentuk kebijakan pada blok tersebut: severity: CRITICAL,HIGH memperluas filter menjadi dua tingkat sekaligus; ignore-unfixed: true mengecualikan temuan tanpa versi perbaikan dari hitungan, sehingga baris kedua tabel kebijakan ditegakkan oleh alatnya sendiri; exit-code: 1 memilih kode kegagalan ketika ambang tersentuh; dan format: table menjaga laporan tetap terbaca manusia di log. Prinsip kerjanya identik dengan Scout — scanner, filter, ambang, exit code — sehingga mengganti alat tidak mengubah bentuk gerbang.

SBOM sebagai Artefak Rilis #

Gerbang menjawab pertanyaan “boleh lewat atau tidak”; SBOM menjawab pertanyaan yang berbeda — “image ini sebenarnya berisi apa”. Inventaris paket yang menjadi fondasi pemindaian pada Chapter Image Scanning paling murah dilahirkan sebagai attestation bersama build: satu input tambahan pada job build, dan builder menyusun inventarisnya sendiri lalu menyimpannya menempel pada manifest image — mekanisme attestation yang kita bedah pada Chapter Image Provenance.

yaml
      - name: Build dan dorong tag sha
        uses: docker/build-push-action@v6
        with:
          context: .
          push: true
          tags: ghcr.io/budi/toko:sha-${{ steps.sha.outputs.pendek }}
          sbom: true
          cache-from: type=registry,ref=ghcr.io/budi/toko:buildcache
          cache-to: type=registry,ref=ghcr.io/budi/toko:buildcache,mode=max

SBOM juga bisa dikeluarkan sebagai berkas lepas untuk diarsipkan. Trivy mengerjakannya dengan satu perintah, lalu workflow mengunggah hasilnya sebagai artefak:

bash
$ trivy image --format cyclonedx --output sbom.json ghcr.io/budi/toko:sha-b62d914
yaml
      - uses: actions/upload-artifact@v4
        with:
          name: sbom-rilis
          path: sbom.json

Arsip itu baru menunjukkan taringnya saat dibandingkan antar rilis — kebiasaan diff SBOM yang kita tanam pada Chapter Supply Chain Security:

bash
$ diff <(jq -r '.components[].name' sbom-v1.4.0.json | sort) \
       <(jq -r '.components[].name' sbom-v1.5.0.json | sort)

Output yang Dihasilkan:

text
14a15
> sharp

Mari kita telaah dua baris output tersebut: format 14a15 menyatakan satu entri baru ditambahkan setelah baris keempat belas, dan entri itu bernama sharp. Paket pemroses gambar itu tidak disebut satu pun berkas pada pull request rilis 1.5.0 — ia datang sebagai tamu transitif dari kenaikan dependensi. Ditemukan sebelum rilis, tamu seperti ini cukup ditanyakan satu kalimat pada penulis pull request; ditemukan setelah insiden, ia menjadi pertanyaan yang jauh lebih mahal. Diff SBOM menjadikan pertanyaan itu rutin dan murah.

Anatomi Sebuah Gerbang #

Seluruh bagian chapter ini menyusun satu gerbang dengan anatomi yang bisa dibaca ulang pada tabel berikut:

ElemenPerannyaWujud pada pipeline toko
Scannermenghitung temuan dari isi imagedocker scout cves membaca image di registry
Filtermemilih temuan yang ikut dihitung--only-severity critical
Ambangmemutuskan lolos atau gagal--exit-code — satu temuan kritis cukup menutup
Sinyal gagalmenerjemahkan keputusan ke sistem CIexit code 2 menjadi job merah
Rantaimenjaga keputusan sampai ke hilirpublish bergantung pada scan lewat needs
Pengecualianlubang yang disengaja, tercatat, dan berkedaluwarsadokumen kebijakan di luar workflow

Anatomi ini sengaja dibuat generik: ganti scanner, ganti filter, ganti ambang — keenam elemen tetap berdiri dengan peran yang sama. Gerbang toko kini berdiri tegak di tengah pipeline, dan image yang melewatinya layak menerima nama. Justru di titik itu masalah berikutnya muncul: nama yang dijanjikan job publish — main, latest, angka versi — masih diketik oleh tangan manusia pada baris-baris workflow, dan tangan manusia adalah mesin tagging yang paling tidak andal. Chapter Version Tagging menghapus ketikan itu: tag dihitung langsung dari keadaan Git, bukan dari ingatan orang yang sedang menekan tombol merge.

Referensi #