Gerbang keamanan toko kini berdiri tegak: image dengan temuan kritis tidak pernah lagi menerima nama manis, dan job publish hanya bekerja untuk byte yang lulus pemeriksaan. Namun di dalam job publish itu sendiri masih bersembunyi satu titik kelemahan yang tidak menyentuh kode sama sekali: nama-nama tag diketik oleh tangan manusia. Kisahnya selalu mirip — Jumat sore lalu seseorang lupa mendorong tag main sehingga latest di registry masih menunjuk build Kamis; dua minggu sebelumnya ada yang salah mengetik 1.50 untuk rilis 1.5.0; dan sekali waktu tag 1.5.0 terdorong dua kali dari dua commit berbeda karena jempol licin. Kesalahan ketik adalah kelas bug yang tidak bisa di-review karena tidak menyisakan baris kode untuk direview. Chapter ini menutup kelas itu dengan satu aturan emas: tag dihitung dari keadaan Git, bukan diketik dari ingatan manusia.
Tag Dihitung dari Keadaan Git #
Setiap event yang memicu workflow membawa keadaan Git yang lengkap: SHA commit yang dibangun, jenis ref (cabang atau tag), dan nama ref. Ketiga informasi itu sudah cukup untuk menghitung seluruh himpunan tag yang benar — tanpa satu pun masukan tambahan dari manusia. Kebijakan tagging toko terangkum dalam tabel berikut:
| Keadaan Git | Tag yang dihitung | Dipakai untuk |
|---|---|---|
push ke cabang main | sha-b62d914, main, latest | jejak audit per commit; kanal harian |
push tag v1.5.0 | 1.5.0, 1.5, sha-b62d914 | rilis resmi; manifest produksi |
| pull request nomor 42 | pr-42 | image uji untuk review; dibuang setelah merge |
push cabang fitur-keranjang | fitur-keranjang | eksperimen pembangun; tidak pernah menyentuh latest |
Mari kita telaah logika di balik keempat barisnya: tag sha menautkan image langsung ke commit yang melahirkannya — satu SHA, satu image, bisa ditelusuri dua arah, sifat yang kita kenal dari Chapter Image Digest dan Strategi Versioning Image. Tag main dan latest adalah penunjuk bergerak yang hanya boleh bergeser melalui proses rilis. Tag semver 1.5.0 adalah identitas resmi rilis, ditemani tag mengapung 1.5 yang mengikuti patch terbaru di jalur minor — konvensi dari Chapter Strategi Versioning Image. Tag pr-42 membuat reviewer bisa menjalankan image dari pull request yang sedang dibaca tanpa mencemari kumpulan tag resmi. Satu aturan menyatukan semuanya: tidak ada satu pun tag di tabel yang perlu diingat manusia, karena semuanya bisa dihitung ulang dari Git kapan pun.
Skrip Kecil Penghitung Tag #
Seluruh keadaan Git tersedia sebagai environment variable GITHUB_* pada runner, sehingga kebijakan tabel di atas bisa diterjemahkan menjadi skrip bash yang pendek dan bisa dibaca baris demi baris:
#!/usr/bin/env bash
set -euo pipefail
IMAGE="ghcr.io/budi/toko"
SHA="${GITHUB_SHA:0:7}"
tags=()
if [ "${GITHUB_EVENT_NAME}" = "pull_request" ]; then
tags+=("pr-${GITHUB_REF_NAME%%/*}")
elif [ "${GITHUB_REF_TYPE}" = "tag" ]; then
versi="${GITHUB_REF_NAME#v}"
tags+=("${versi}" "${versi%.*}" "sha-${SHA}")
elif [ "${GITHUB_REF_NAME}" = "main" ]; then
tags+=("main" "latest" "sha-${SHA}")
else
tags+=("${GITHUB_REF_NAME//\//-}")
fi
args=()
for tag in "${tags[@]}"; do
args+=(-t "${IMAGE}:${tag}")
done
docker buildx build "${args[@]}" --load .
for tag in "${tags[@]}"; do
docker push "${IMAGE}:${tag}"
echo "terdorong: ${IMAGE}:${tag}"
doneMari kita telaah bagian-bagian penghitungannya:
SHA="${GITHUB_SHA:0:7}"— memotong SHA commit penuh menjadi tujuh karakter pertama; panjang itu cukup untuk penelusuran karena repositori tetap menyimpan SHA utuhnya.GITHUB_REF_TYPEdanGITHUB_REF_NAME— jenis ref (cabang atau tag) beserta namanya; inilah keadaan Git yang menjadi sumber seluruh keputusan.${GITHUB_REF_NAME#v}— membuang awalanvdari nama tag Git sehinggav1.5.0menjadi1.5.0; image memakai semver polos, Git memakai prefiksv, keduanya tetap rapi di alamnya masing-masing.${versi%.*}— membuang segmen terakhir:1.5.0menjadi1.5; ekspansi parameter bash yang kecil ini adalah seluruh mesin tag mengapung kita.pr-${GITHUB_REF_NAME%%/*}— nama ref pull request berbentuk42/merge, dan%%/*memotong semuanya sebelum garis miring pertama, menyisakanpr-42.- Banyak
-tpada SATU build, lalu loopdocker push— image dibangun sekali dengan seluruh tag menempel, lalu tiap tag didorong bergiliran; layer diunggah pada push pertama, dan push berikutnya hanya mengirim manifest.
Saat skrip itu berjalan pada peristiwa tag v1.5.0 — rilis pesanan bernomor yang dikerjakan Siti — keluarannya berbunyi:
Output yang Dihasilkan (dipotong):
The push refers to repository [ghcr.io/budi/toko]
a1b2c3d4e5f6: Pushed
...
1.5.0: digest: sha256:7a2e9c4b1d6f83025a7c9e1b3d5f7092a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2d6 size: 2201
The push refers to repository [ghcr.io/budi/toko]
a1b2c3d4e5f6: Layer already exists
...
1.5: digest: sha256:7a2e9c4b1d6f83025a7c9e1b3d5f7092a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2d6 size: 2201
The push refers to repository [ghcr.io/budi/toko]
a1b2c3d4e5f6: Layer already exists
...
sha-b62d914: digest: sha256:7a2e9c4b1d6f83025a7c9e1b3d5f7092a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2d6 size: 2201
terdorong: ghcr.io/budi/toko:1.5.0
terdorong: ghcr.io/budi/toko:1.5
terdorong: ghcr.io/budi/toko:sha-b62d914Tiga nama, satu digest — baris Layer already exists membuktikan bahwa push kedua dan ketiga tidak memindahkan satu byte layer pun, dan ketiga baris digest yang identik adalah janji bahwa 1.5.0, 1.5, dan sha-b62d914 menunjuk image yang persis sama. Digest inilah yang kelak dicatat pada CHANGELOG, menyambung jejak rilis dari Chapter Update dan Rollback.
metadata-action: Hitungan yang Sama Tanpa Skrip #
Skrip di atas benar, tetapi production toko memakai cara yang lebih deklaratif: docker/metadata-action@v5 menghitung tag sekaligus label OCI dari keadaan Git berdasarkan aturan yang ditulis sebagai konfigurasi, bukan sebagai percabangan if-then:
jobs:
meta:
runs-on: ubuntu-latest
outputs:
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
steps:
- uses: actions/checkout@v4
- id: meta
uses: docker/metadata-action@v5
with:
images: ghcr.io/budi/toko
tags: |
type=ref,event=branch
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=sha,format=short
flavor: |
latest=auto
- name: Perlihatkan hasil hitungan
run: |
echo "Tags:"
echo "${{ steps.meta.outputs.tags }}"
echo "Labels:"
echo "${{ steps.meta.outputs.labels }}"Pada push commit b62d914 ke cabang main, langkah terakhir mencetak hasil hitungannya:
Output yang Dihasilkan:
Tags:
ghcr.io/budi/toko:main
ghcr.io/budi/toko:latest
ghcr.io/budi/toko:sha-b62d914
Labels:
org.opencontainers.image.title=toko
org.opencontainers.image.source=https://github.com/budi/toko
org.opencontainers.image.revision=b62d914a3f8e2d1c6b9a4e7f2d8c5a1b9e3f7d2c8
org.opencontainers.image.version=mainMari kita telaah aturan-aturan yang menghasilkan keluaran tersebut:
type=ref,event=branch— menghasilkan tag bernama cabang untuk push cabang; pada contoh di atas:main.type=semverdengan dua pola — pola{{version}}menghasilkan1.5.0dan pola{{major}}.{{minor}}menghasilkan1.5ketika event-nya tagv1.5.0; pasangan ini menggantikan dua baris ekspansi parameter pada skrip bash tadi.type=sha,format=short— menghasilkansha-b62d914, sama persis dengan potongan tujuh karakter pada skrip.type=ref,event=pr— menghasilkanpr-42untuk pull request, menutup baris ketiga tabel kebijakan.flavor: latest=auto— menempelkanlatestsecara otomatis pada konteks rilis seperti push ke cabang default, dan tidak pernah menempelkannya pada pull request maupun cabang fitur.- Label OCI
org.opencontainers.image.*— action ikut menyusun label baku: sumber repositori, revisi commit, dan versi; label ini menempel pada image sebagai metadata yang bisa dibaca mesin — fondasi yang sama dengan attestation pada Chapter Image Provenance.
Hasil hitungan itu kemudian dikonsumsi build-push-action langsung dari output job meta, sehingga tidak ada satu pun nama tag yang ditulis dua kali:
build:
needs: meta
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 }}
- uses: docker/build-push-action@v6
with:
context: .
push: true
tags: ${{ needs.meta.outputs.tags }}
labels: ${{ needs.meta.outputs.labels }}
cache-from: type=registry,ref=ghcr.io/budi/toko:buildcache
cache-to: type=registry,ref=ghcr.io/budi/toko:buildcache,mode=maxPerhatikan satu hal yang menarik pada contoh keluaran di atas: commit b62d914 menerima main, latest, dan sha-b62d914 ketika didorong ke cabang, lalu — beberapa hari kemudian, ketika commit yang sama ditandai v1.5.0 — commit yang sama itu menerima 1.5.0, 1.5, dan sha-b62d914. Tag berbeda per event, SHA tetap sama; SHA-lah yang menjahit kedua peristiwa menjadi satu riwayat, dan SHA pula yang dicari orang saat audit.
Gerbang Tetap Dihormati #
Bagaimana pola ini berdamai dengan gerbang scan dari Chapter Image Scanning di CI/CD? Job meta boleh berjalan sejak awal — menghitung nama bukanlah tindakan berisiko — tetapi job build tetap mendorong tag sha saja ke ruang tunggunya, dan seluruh tag cantik hasil hitungan dipindahkan ke job publish yang baru berjalan setelah gerbang lolos. Job publish bahkan tidak perlu membangun ulang: docker buildx imagetools create menempel tag baru pada manifest image yang sudah ada, tanpa menarik satu layer pun:
publish:
needs: [meta, scan]
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Pasang tag hasil hitungan pada image sha
run: |
for tag in $(echo "${{ needs.meta.outputs.tags }}"); do
docker buildx imagetools create -t "${tag}" \
"ghcr.io/budi/toko:sha-${GITHUB_SHA:0:7}"
doneMari kita telaah loop kecil tersebut: setiap tag pada keluaran meta diteruskan sebagai -t kepada imagetools create, yang menulis manifest baru di registry menunjuk blob-blob yang sudah tersimpan — digest image tidak berubah karena isinya memang tidak berubah. Nama dihitung oleh meta, byte tetap milik image sha, dan gerbang tetap menjadi satu-satunya jalan menuju nama manis. Tiga kepentingan, tiga job, tidak ada yang saling mencuri peran.
Changelog dari Riwayat Git #
Rilis yang tercatat baik lahir dari Git yang tertulis baik. Karena setiap rilis toko ditandai tag berannotasi — kebiasaan dari Chapter Update dan Rollback — daftar perubahan antar rilis bisa dihitung dari rentang tag:
$ git log v1.4.0..v1.5.0 --onelineOutput yang Dihasilkan:
b62d914 pesanan: nomor pesanan + trace_id di log
6d1a2f9 db: migrasi kolom nomor_pesanan (nullable)
b40e7c2 deps: perbarui package-lock untuk expressRentang v1.4.0..v1.5.0 berarti “commit yang dicapai v1.5.0 tetapi belum dicapai v1.4.0” — persis isi rilis itu. Tiga baris oneline di atas adalah bibit CHANGELOG: Siti tinggal memberi judul dan tanggal, lalu menempelkan digest rilis seperti yang kita lakukan pada Chapter Update dan Rollback. Ketika disiplin pesan commit mengikuti konvensi (fitur, perbaikan, perubahan merusak), langkah penyusunan ini pun bisa diserahkan ke alat seperti git-cliff atau semantic-release — keduanya merakit changelog dan bahkan menomori rilis berikutnya secara otomatis; bagi toko, tiga baris git log masih lebih murah daripada memelihara alat tambahan.
Rilis untuk Registry Privat #
Produksi toko tidak menarik image dari GHCR. Server srv-toko mengambil dari registry privat perusahaan — registry.example.com — yang hanya terjangkau dari jaringan internal, persis pola Chapter Private Registry. Manifest produksi karenanya menyebut alamat itu, dan pada rilis 1.5.0 bentuknya:
services:
backend:
image: registry.example.com/toko/backend:1.5.0Pertanyaannya: image sudah ada di GHCR — bagaimana ia pindah ke registry privat tanpa dibangun ulang? Jawabannya adalah job mirror yang berjalan setelah gerbang scan, memakai imagetools create untuk menyalin manifest beserta seluruh blob-nya:
mirror:
needs: scan
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/login-action@v3
with:
registry: registry.example.com
username: ${{ secrets.REGISTRY_USER }}
password: ${{ secrets.REGISTRY_PASSWORD }}
- name: Tiru image ke registry privat
run: |
VERSI="${GITHUB_REF_NAME#v}"
docker buildx imagetools create \
-t "registry.example.com/toko/backend:${VERSI}" \
"ghcr.io/budi/toko:${VERSI}"Output yang Dihasilkan (dipotong):
2026/10/24 04:11:57 pushed blob: sha256:1f4a6c8e0b2d4f6a8c0e2d4f6a8c0e2d4f6a8c0e2d4f6a8c0e2d4f6a8c0e2d4f6
2026/10/24 04:11:58 pushed blob: sha256:9c2e4d6f8a0b3c5d7e9f1a3b5c7d9e1f3a5b7c9d1e3f5a7b9c1d3e5f7a9b1c3d5
2026/10/24 04:11:59 created manifest: sha256:7a2e9c4b1d6f83025a7c9e1b3d5f7092a4c6e8b0d2f4a6c8e0b2d4f6a8c0e2d6Mari kita telaah tiga baris keluaran itu: blob disalin dari GHCR ke registry privat satu per satu, lalu manifest diciptakan di alamat barunya — dan digest-nya, sha256:7a2e9c4b..., identik dengan digest image 1.5.0 di GHCR. Identitas lintas registry ini bukan kebetulan melainkan sifat content-addressable yang kita buktikan pada Chapter Private Registry: digest dihitung dari isi, bukan dari lokasi penyimpanan, sehingga image yang sama berdigest sama di registry mana pun. Verifikasinya tinggal membandingkan docker buildx imagetools inspect pada kedua alamat — dua nama, satu identitas. Sepasang login pada job juga perlu dicatat: GHCR dibayar GITHUB_TOKEN, registry privat dibayar kredensial REGISTRY_USER dan REGISTRY_PASSWORD dari secrets — dua rumah, dua kunci, tidak ada yang dipakai bergantian.
Rilis toko kini terhitung penuh dari keadaan Git: tag ditentukan event, label menyusun diri, changelog lahir dari rentang tag, dan image termirror ke registry privat dengan identitas yang terbukti identik. Satu langkah terakhir dari seluruh rantai ini masih dikerjakan tangan manusia: setelah job mirror hijau, Budi membuka terminal, masuk SSH ke srv-toko, dan mengetik ritual git checkout, docker compose pull, docker compose up -d persis seperti chapter Update dan Rollback. Tangan itu adalah mata rantai terakhir yang belum diaudit, belum digerbangi, dan belum bisa diulang dengan sama persis setiap kali. Chapter Deployment Automation mengotomatiskan langkah penutup itu: pola deploy SSH dengan persetujuan manual, smoke test yang menguji rantai produksi dari ujung ke ujung, dan rollback otomatis yang bangun tengah malam supaya Budi tidak perlu lagi.
Referensi #
- Docker di GitHub Actions — action resmi Docker untuk workflow build, termasuk pola tagging.
- Events that trigger workflows — event push, tag, dan pull request yang menjadi sumber keadaan Git.
- docker buildx imagetools create — menempel tag pada manifest yang sudah ada tanpa membangun ulang image.