//BAGIAN 6 DARI 10
Bagian 6: Ruleset yang Memblokir Merge, dan Lima Demo PR yang Harus Gagal
· 18 menit baca
English//TUTORIAL · 6 DARI 10 — Secure CI/CD Pipeline dengan Go, GitHub Actions, Vault, dan Dokploy
- 01Bagian 1: Kenapa Saya Bikin Secure CI/CD Pipeline, dan Persiapan Tools-nya
- 02Bagian 2: Vault, Secret, Policy, dan AppRole per Environment
- 03Bagian 3: Aplikasi Go Kecil yang Membaca Secret dari File dan Menolak Jalan Kalau Config Salah
- 04Bagian 4: Vault Agent Sidecar dan Docker Compose, Secret Masuk Tanpa App Tahu Vault Ada
- 05Bagian 5: Repo GitHub dan Workflow PR sebagai Security Gate
- 06Bagian 6: Ruleset yang Memblokir Merge, dan Lima Demo PR yang Harus Gagal
- 07Bagian 7: Menyiapkan Environment Dev dan Production di Dokploy
- 08Bagian 8: Workflow Deploy, Build Sekali, Scan, SBOM, GHCR, lalu Promosi ke Production
- 09Bagian 9: Proses Exception untuk Temuan Security yang Belum Bisa Diperbaiki
- 10Bagian 10: CI Mengambil Token Dokploy dari Vault lewat GitHub OIDC, dan Penutup Seri
Di Bagian 5, workflow pr-checks.yml dan source-branch.yml sudah jalan di setiap PR. Masalahnya, check yang merah itu belum memaksa apa-apa. Tombol Merge tetap hijau dan tetap bisa ditekan. Jadi gate-nya baru setengah jadi.
Di bagian ini saya menutup celah itu dengan ruleset GitHub di development dan main. Setelah ruleset aktif, PR dengan check merah nggak bisa di-merge, push langsung ditolak, dan saya sendiri sebagai admin juga nggak bisa melewatinya.
Lalu bagian yang paling saya suka: membuktikannya. Saya membuat lima PR yang sengaja jahat (secret bocor, command injection, dependency rentan, container root, dan PR yang melompati development). Kelimanya harus diblokir oleh gate yang berbeda.
Catatan
Analogi yang saya pakai: workflow di Bagian 5 itu detektor logam. Ruleset itu satpam yang benar-benar menahan orang kalau detektornya bunyi. Detektor tanpa satpam cuma bunyi, lalu orangnya tetap lewat.
Ruleset itu apa, dan kenapa bukan branch protection
Ruleset adalah kumpulan aturan yang GitHub tegakkan di server saat ada push atau merge ke branch tertentu. Karena dicek di server, aturannya nggak bisa diakali dari laptop.
GitHub punya dua fitur yang mirip. Saya pilih ruleset karena beberapa alasan:
Branch protection (lama) | Ruleset (baru) | |
|---|---|---|
Jumlah aturan per branch | Satu aturan per pola branch | Bisa berlapis |
Siapa yang bisa melihat | Admin | Semua yang punya akses baca repo (transparan) |
Riwayat perubahan | Nggak ada | Ada, plus Rule Insights (log push yang ditolak/diizinkan) |
Ekspor/impor JSON | Nggak | Bisa, jadi bisa disimpan di git |
Bypass admin | Satu centang | Diatur eksplisit lewat |
Aturan yang saya pakai cuma lima jenis:
Aturan ( | Artinya | Kenapa dipakai |
|---|---|---|
| Branch nggak boleh dihapus |
|
| Force push dilarang | Force push bisa menulis ulang history dan menghapus commit yang sudah di-review |
| Semua perubahan wajib lewat PR | Nggak ada jalan masuk selain lewat check |
| Check tertentu wajib hijau | Inilah yang menyambungkan ruleset ke workflow Bagian 5 |
| Nggak ada yang boleh melewati, termasuk admin | Anti-goal dari Bagian 1: "pipeline bisa dilewati admin" |
Poin terakhir itu yang paling penting. Kalau admin boleh lewat, gate-nya nggak bermakna. Satu kali "ah, darurat, merge saja" dan semua jaminan hilang.
Langkah 0: workflow harus jalan sekali dulu
Ruleset mewajibkan check berdasarkan nama. Kalau nama di ruleset nggak sama persis dengan nama yang dilaporkan workflow, PR akan menunggu selamanya dengan status "Expected — Waiting for status to be reported".
Karena itu urutannya penting: workflow dari Bagian 5 harus sudah jalan minimal sekali di sebuah PR. Setelah itu nama check-nya sudah ada di GitHub, muncul di pilihan UI ruleset, dan bisa saya baca lewat API.
Saya ambil daftar check dari PR workflow di Bagian 5:
R=dimss113/secure-cicd-goSHA=$(gh api repos/$R/pulls/2 --jq .head.sha)gh api repos/$R/commits/$SHA/check-runs \ --jq '.check_runs[] | .name + " " + .conclusion + " app=" + (.app.id|tostring) + " " + .app.slug'SHA adalah commit terakhir PR itu. Endpoint check-runs mengembalikan semua check yang pernah dilaporkan untuk commit tersebut, lengkap dengan app yang mengirimnya.
Cek hasil:
Semgrep OSS success app=57789 github-advanced-securityTrivy success app=57789 github-advanced-securitygitleaks success app=57789 github-advanced-securitydependency-review success app=15368 github-actionstrivy-image success app=15368 github-actionsgovulncheck success app=15368 github-actionsgitleaks success app=15368 github-actionssemgrep success app=15368 github-actionstest success app=15368 github-actionstrivy-config success app=15368 github-actionsTujuh check dengan app=15368 adalah job saya di pr-checks.yml (nama check = ID job). Tiga lainnya datang dari Code scanning, dibuat otomatis karena workflow meng-upload SARIF.
Yang sempat bikin saya bingung: gitleaks muncul dua kali, dari dua app berbeda.
Bahaya
Kunci check ke app GitHub Actions
Kalau ruleset cuma bilang "wajib ada check gitleaks yang hijau", check bernama gitleaks dari mana pun dianggap sah. Itu membuka dua masalah. Pertama, tabrakan nama: check dari Code scanning bisa ikut terhitung. Kedua, pemalsuan: siapa pun yang punya token dengan hak menulis status commit bisa mengirim test = success lewat API tanpa test benar-benar jalan. Dengan "integration_id": 15368, GitHub hanya menerima check yang dikirim oleh GitHub Actions. Angka 15368 itu ID app GitHub Actions di github.com, sama untuk semua repo.
Langkah 1: uji gate dulu, ternyata ada dua lubang
Sebelum memasang ruleset, saya coba dulu kelima demo di salinan repo. Gate yang belum pernah diuji dengan kasus buruk belum tentu bekerja. Dan memang ketemu dua masalah.
Lubang 1: salah ketik di trivy-config. Saya menulis limit-se-for-sarif, padahal input yang benar limit-severities-for-sarif. GitHub sebenarnya sudah memperingatkan lewat anotasi kuning di ringkasan run, tapi job-nya tetap hijau, jadi gampang terlewat. Akibatnya laporan SARIF memuat semua severity (LOW dan MEDIUM juga), jadi tab Security berisik. Gate-nya sendiri nggak terpengaruh. Pelajarannya: baca anotasi kuning di ringkasan run.
Lubang 2: Semgrep nggak menangkap command injection. Ini yang lebih serius. Kode demo exec.Command("sh", "-c", <input dari URL>) saya scan dengan beberapa ruleset:
Ruleset Semgrep | Hasil |
|---|---|
| Nggak ada temuan |
| Nggak ada temuan |
|
|
p/golang isinya lebih banyak aturan best practice. Aturan keamanan dangerous-exec-command ada di p/default. Saya cek juga p/default di kode yang sekarang: nol temuan, jadi nggak bikin PR lain merah tanpa alasan.
Perbaikannya dua baris di pr-checks.yml. Ini versi akhirnya di repo:
semgrep: # ... semgrep scan --config p/golang --config p/default --config p/secrets \ --severity ERROR --error --metrics=off \ # ... trivy-config: # ... severity: HIGH,CRITICAL limit-severities-for-sarif: true(Dipotong, hanya baris yang berubah.) --config boleh lebih dari satu, Semgrep menggabungkan semua aturannya.
Cek hasil:
grep -n "limit-severities-for-sarif" .github/workflows/pr-checks.yml # 2 baris: trivy-config dan trivy-imagesemgrep scan --config p/golang --config p/default --config p/secrets \ --severity ERROR --error --metrics=off --quiet .echo "exit=$?" # exit=0Perubahan ini belum saya commit. Nanti ikut di PR pertama yang jalan di bawah ruleset.
Langkah 2: ruleset sebagai kode
Ruleset bisa dibuat dengan klik-klik di Settings → Rules. Saya pilih file JSON + API, dengan prinsip yang sama seperti vault/policies/ di Bagian 2: bisa di-review lewat PR, bisa dipasang ulang persis sama, dan nggak ada centang yang terlewat.
Ini protect-development:
{ "name": "protect-development", "target": "branch", "enforcement": "active", "bypass_actors": [], "conditions": { "ref_name": { "include": ["refs/heads/development"], "exclude": [] } }, "rules": [ { "type": "deletion" }, { "type": "non_fast_forward" }, { "type": "pull_request", "parameters": { "required_approving_review_count": 0, "dismiss_stale_reviews_on_push": false, "require_code_owner_review": false, "require_last_push_approval": false, "required_review_thread_resolution": false, "allowed_merge_methods": ["merge"] } }, { "type": "required_status_checks", "parameters": { "strict_required_status_checks_policy": true, "do_not_enforce_on_create": false, "required_status_checks": [ { "context": "test", "integration_id": 15368 }, { "context": "gitleaks", "integration_id": 15368 }, { "context": "semgrep", "integration_id": 15368 }, { "context": "govulncheck", "integration_id": 15368 }, { "context": "dependency-review", "integration_id": 15368 }, { "context": "trivy-config", "integration_id": 15368 }, { "context": "trivy-image", "integration_id": 15368 } ] } } ]}protect-main isinya sama, dengan tiga perbedaan:
"conditions": { "ref_name": { "include": ["refs/heads/main"], "exclude": [] } }, ... "strict_required_status_checks_policy": false, ... { "context": "trivy-image", "integration_id": 15368 }, { "context": "source-branch", "integration_id": 15368 }(Dipotong, sisanya identik dengan protect-development.)
Catatan
File di repo punya satu check lagi
Kalau kamu buka docs/rulesets/ di repo, kedua file juga mewajibkan check exceptions. Job itu baru dibuat di Bagian 9 (proses exception). Di titik ini job-nya belum ada, jadi jangan diwajibkan dulu, nanti PR menunggu selamanya.
Kunci yang perlu dijelaskan:
Kunci | Artinya |
|---|---|
| Langsung ditegakkan. |
| Kosong: nggak ada siapa pun yang boleh lewat, termasuk admin |
| Branch yang dikenai aturan, dalam bentuk ref lengkap |
| Saya kerja sendiri, dan GitHub nggak mengizinkan approve PR milik sendiri. Di tim nyata minimal 1 |
| Hanya merge commit. Squash dan rebase dimatikan |
| "Require branches to be up to date". |
| Aturan tetap berlaku untuk branch yang baru dibuat |
| Nama check, sama dengan ID job |
| Check harus dari GitHub Actions |
Kenapa hanya merge commit
Ini bukan soal selera. Di Bagian 8, workflow deploy ke production nggak build ulang. Ia mencari commit head dari PR development → main lewat merge commit di main, lalu mencari image sha-<commit> yang sudah jalan di dev. Squash atau rebase membuat commit baru dengan SHA berbeda, dan jejak itu putus. Jadi metode merge saya kunci dari sekarang, di kedua branch supaya konsisten.
Kenapa strict beda di dua branch
Up to date artinya branch PR harus sudah berisi commit terbaru dari branch tujuan. Contoh: PR A dan PR B dibuat dari development yang sama. A di-merge duluan. B sekarang ketinggalan, dan check-nya diuji tanpa perubahan A. Bisa saja masing-masing hijau tapi gabungannya rusak. Dengan strict: true, B harus di-update dulu dan check jalan ulang. Itu yang saya mau di development.
Di main lain cerita. Setiap merge development → main membuat merge commit baru di main yang nggak ada di development. Kalau strict nyala, PR rilis berikutnya selalu dianggap ketinggalan, dan main harus digabung balik ke development setiap kali. Merepotkan dan mengotori history. Karena PR ke main selalu dari development yang sudah lolos semua check, strict di main saya matikan.
Kenapa source-branch hanya di main
source-branch.yml hanya jalan untuk PR ke main. Kalau saya wajibkan juga di development, check itu nggak pernah muncul di PR ke development, dan PR-nya menunggu selamanya.
Cek hasil:
jq -e . docs/rulesets/protect-development.json > /dev/null && echo OK-devjq -e . docs/rulesets/protect-main.json > /dev/null && echo OK-mainjq -e . gagal dengan exit code bukan nol kalau ada koma atau kurung yang salah. Harus keluar OK-dev dan OK-main.
Langkah 3: pasang lewat API
gh api -X POST repos/$R/rulesets --input docs/rulesets/protect-development.json --jq '.id, .name, .enforcement'gh api -X POST repos/$R/rulesets --input docs/rulesets/protect-main.json --jq '.id, .name, .enforcement'POST repos/$R/rulesets membuat ruleset baru, isinya diambil dari file JSON. Output-nya ID (angka), nama, dan active. Catat kedua ID.
Perhatian
POST cuma sekali
Menjalankan POST lagi membuat ruleset kedua dengan nama sama. Untuk mengubah, edit JSON lalu gh api -X PUT repos/$R/rulesets/<ID> --input <file>. Kalau sudah terlanjur dobel, hapus satu dengan gh api -X DELETE repos/$R/rulesets/<ID>.
Cek hasil:
gh api repos/$R/rulesets --jq '.[] | "\(.id) \(.name) \(.enforcement)"'gh api repos/$R/rules/branches/development --jq '.[].type'gh api repos/$R/rules/branches/main --jq '.[].type'Endpoint rules/branches/<branch> menampilkan aturan yang benar-benar berlaku di branch itu, gabungan dari semua ruleset. Untuk kedua branch harus keluar deletion, non_fast_forward, pull_request, required_status_checks.
Di UI, buka Settings → Rules → Rulesets → protect-development. Yang harus kamu lihat: Enforcement status: Active, Bypass list kosong, dan di bagian Require status checks to pass ada tujuh check, masing-masing berlabel GitHub Actions.
Langkah 4: push langsung harus ditolak
Pastikan dulu kedua ruleset active. Kalau belum, push uji ini berhasil dan commit kosongnya masuk ke development.
git stashgit switch developmentgit commit --allow-empty -m "test: direct push must be rejected"git push origin developmentgit stash menyimpan perubahan Langkah 1 dan 2 sementara. --allow-empty membuat commit tanpa perubahan file, cukup untuk menguji push.
Cek hasil (kurang lebih):
remote: error: GH013: Repository rule violations found for refs/heads/development.remote: - Changes must be made through a pull request.remote: - Required status checks "test", "gitleaks", ... are expected. ! [remote rejected] development -> development (push declined due to repository rule violations)Ini ditolak walaupun saya admin repo. Bersihkan commit uji dan kembali ke branch kerja:
git reset --hard origin/developmentgit switch ci/harden-gatesgit stash popreset --hard aman di sini karena satu-satunya isinya commit kosong tadi.
Langkah 5: PR pertama di bawah ruleset, dan coba --admin
git add .github/workflows/pr-checks.yml docs/rulesets/git commit -m "ci: harden gates and record branch rulesets"git push -u origin ci/harden-gatesgh pr create --base development \ --title "ci: harden gates and record branch rulesets" \ --body "Fixes limit-severities-for-sarif typo, adds Semgrep p/default, records the rulesets applied via the API."Selagi check masih jalan, saya langsung coba melewatinya dengan hak admin:
gh pr merge --merge --admin--admin artinya "merge dengan hak admin, abaikan aturan". Karena bypass_actors kosong, ini ditolak dengan pesan Repository rule violations found ... required status checks ... are expected.
Setelah check hijau, merge dengan cara normal:
gh pr checks --watchgh pr merge --merge --delete-branchgit switch development && git pullTips
"This branch is out-of-date with the base branch"
Itu aturan strict sedang bekerja. Jalankan gh pr update-branch, check jalan ulang, lalu merge.
Semua demo di bawah harus dibuat setelah PR ini di-merge. Demo command injection bergantung pada p/default.
Lima demo PR yang harus gagal
Semua demo mengikuti pola yang sama:
development terbaru → branch demo/... → ubah satu hal "buruk" → commit → push → PR→ check merah → "Merging is blocked" → TUTUP PR (jangan merge) → hapus branchPerintah yang dipakai berulang:
Perintah | Fungsinya |
|---|---|
| Mulai dari |
| Tunggu semua check. Keluar dengan exit code bukan nol kalau ada yang gagal, dan itu memang yang diharapkan |
| Harus |
| Tutup tanpa merge, hapus branch |
Di setiap PR demo, yang harus kamu lihat di bagian bawah halaman PR: daftar check dengan satu (atau dua) tanda silang merah, sisanya centang hijau, lalu kotak abu-abu bertuliskan "Merging is blocked" dengan keterangan bahwa required status check gagal. Tombol merge nonaktif. Klik Details di check yang merah untuk melihat log alasannya.
Demo 1: secret bocor, ditangkap gitleaks
git switch development && git pullgit switch -c demo/leak-secretmkdir -p internal/demoKEY=$(openssl rand -hex 20)printf 'package demo\n\n// Demo only: fake credential that the secret scanner must catch.\nconst apiKey = "%s"\n\nfunc Key() string { return apiKey }\n' "$KEY" > internal/demo/leak.goopenssl rand -hex 20 menghasilkan 40 karakter hex acak. Bentuknya mirip API key, tapi nggak berlaku di mana pun. Nama variabel apiKey ditambah string acak panjang adalah pola yang dikenali aturan generic-api-key milik Gitleaks.
Dua hal yang saya pelajari waktu memilih secret palsu. Contoh AWS key yang berakhiran EXAMPLE justru diabaikan Gitleaks. Dan kalau pakai pola token GitHub asli, push protection GitHub sudah menolak di git push, jadi CI nggak pernah jalan (itu juga bukti, lapisan pertama, tapi bukan yang mau saya tunjukkan di sini).
Cek dulu di laptop:
git add internal/demo/leak.gogit commit -m "demo: hardcode an api key"gitleaks git . --log-opts="HEAD~1..HEAD" --redact --no-bannerecho "exit=$?"Finding: const apiKey = "REDACTED"RuleID: generic-api-keyFile: internal/demo/leak.goWRN leaks found: 1exit=1Push, buka PR ke development, tunggu check.
Cek hasil: gitleaks merah, enam check lain hijau, status BLOCKED. Di tab Security → Code scanning muncul alert generic-api-key untuk PR ini.
Perhatian
Commit-nya tetap tersimpan di GitHub
Setelah PR ditutup, commit-nya masih bisa diakses lewat refs/pull/<n>/head. Karena itu hanya pakai nilai acak. Kalau yang bocor secret asli, menutup PR nggak cukup: langsung rotasi secret-nya.
Demo 2: command injection, ditangkap semgrep
package httpapi import ( "net/http" "os/exec") // Demo only: runs a shell command taken straight from the query string.func debugExec(w http.ResponseWriter, r *http.Request) { out, _ := exec.Command("sh", "-c", r.URL.Query().Get("cmd")).CombinedOutput() _, _ = w.Write(out)}r.URL.Query().Get("cmd") sepenuhnya dikendalikan pengunjung, dan exec.Command("sh", "-c", ...) menjalankannya sebagai perintah shell. GET /debug?cmd=cat /run/config/config.json sudah cukup untuk membocorkan secret JWT dan AES dari Bagian 4. Itu Remote Code Execution. Fungsinya bahkan belum dipasang ke router, tapi Semgrep tetap menandainya karena ia membaca kode, bukan menjalankan aplikasi.
Cek di laptop:
semgrep scan --config p/golang --config p/default --config p/secrets --severity ERROR --error --metrics=off .echo "exit=$?"Harus muncul temuan go.lang.security.audit.dangerous-exec-command.dangerous-exec-command di internal/httpapi/debug.go, lalu exit=1.
Cek hasil: semgrep merah, status BLOCKED. Di tab Files changed, baris exec.Command diberi anotasi dari Code scanning.
Kalau demo ini ternyata hijau, hampir pasti branch-nya dibuat sebelum PR Langkah 5 di-merge. Sebelum perbaikan itu, demo ini lolos. Justru itu poinnya: saya menguji gate saya sendiri, bukan cuma memasang tool.
Demo 3: dependency rentan, ditangkap govulncheck dan dependency-review
git switch development && git pullgit switch -c demo/vuln-depgo get github.com/golang-jwt/jwt/v5@v5.2.1grep jwt go.mod # require github.com/golang-jwt/jwt/v5 v5.2.1Repo memakai v5.3.1. Saya turunkan ke v5.2.1, yang punya CVE-2025-30204 (GO-2025-3553, GHSA-mh63-6h87-95cp, severity High): header token yang dibuat khusus membuat parser mengalokasikan memori berlebihan, jadi server bisa kehabisan memori. Diperbaiki di v5.2.2.
Cek di laptop:
go build ./... && go test ./...go run golang.org/x/vuln/cmd/govulncheck@v1.9.0 ./...Build dan test tetap ok. Kodenya tetap jalan, dan justru itu bahayanya. Lalu govulncheck:
Vulnerability #1: GO-2025-3553 Found in: github.com/golang-jwt/jwt/v5@v5.2.1 Fixed in: github.com/golang-jwt/jwt/v5@v5.2.2 #1: internal/token/token.go:52:31: token.Issuer.Verify calls jwt.ParseWithClaims, which eventually calls jwt.Parser.ParseUnverifiedTrace #1 membuktikan kode kita benar-benar memanggil bagian yang rentan: Verify dipakai endpoint GET /v1/me.
Cek hasil:
govulncheckmerah (vuln yang reachable).dependency-reviewmerah, karena PR menambahkan versi dengan advisory High (fail-on-severity: high). Ringkasan job menampilkanGHSA-mh63-6h87-95cp.trivy-imagehijau, tapi laporannya mencatat CVE-2025-30204. Severity-nya HIGH, bukan CRITICAL, jadi nggak memblokir.Status
BLOCKED.
Dua alat berbeda menangkap masalah yang sama dari sudut berbeda. Itu yang saya maksud defense in depth. Kalau dependency-review ternyata hijau, cek apakah Dependency graph sudah aktif di Settings → Advanced Security.
Demo 4: container jalan sebagai root, ditangkap trivy-config
Di Dockerfile, saya ganti baris user:
# sebelumUSER 65532:65532# sesudah (demo)USER rootProses di container jadi jalan sebagai UID 0. Kalau aplikasinya dibobol, misalnya lewat command injection seperti demo 2, penyerang langsung punya root di container, dan jalan ke host lewat celah container escape jadi lebih pendek.
Cek di laptop:
trivy config --severity HIGH,CRITICAL --exit-code 1 .echo "exit=$?"Dockerfile (dockerfile)Failures: 1 (HIGH: 1, CRITICAL: 0)DS-0002 (HIGH): Last USER command in Dockerfile should not be 'root'exit=1Cek hasil: trivy-config merah dengan temuan DS-0002, status BLOCKED.
Demo 5: PR feature langsung ke main, ditangkap source-branch
Demo terakhir ini beda. Kodenya bersih. Yang salah adalah jalurnya.
git switch development && git pullgit switch -c demo/main-from-featureecho "Demo: this branch tries to skip development." > docs/demo-skip-development.mdgit add docs/demo-skip-development.mdgit commit -m "demo: try to reach main without development"git push -u origin demo/main-from-featuregh pr create --base main --title "demo: feature straight to main" --body "Must be blocked by source-branch. Do not merge."Yang diuji adalah --base main. Ini job yang menangkapnya, dari Bagian 5:
- name: Only development may open a PR into main env: HEAD_REF: ${{ github.head_ref }} run: | if [ "$HEAD_REF" != "development" ]; then echo "::error::PR into main must come from development, not '$HEAD_REF'" exit 1 fiCek hasil: source-branch merah dengan pesan PR into main must come from development, not 'demo/main-from-feature'. Tujuh check pr-checks hijau. Status BLOCKED.
Artinya kode yang bersih pun nggak boleh masuk production tanpa lewat development, tempat ia di-deploy dan diuji dulu.
Ringkasan demo
PR demo | Yang diubah | Check yang merah | Kenapa |
|---|---|---|---|
|
|
| Pola |
|
|
|
|
|
|
| GO-2025-3553, reachable lewat |
|
|
| DS-0002 (HIGH) |
| PR feature → |
| Head branch bukan |
Untuk bukti, dua perintah ini berguna:
gh pr list --state closed --search "demo in:title" --json number,title,state \ --jq '.[] | "#\(.number) \(.title) \(.state)"'gh api repos/$R/rulesets/rule-suites --jq '.[] | "\(.pushed_at) \(.ref) \(.result) \(.actor_name)"'Yang pertama mendaftar semua PR demo yang ditutup. Yang kedua membaca Rule Insights: setiap push yang dievaluasi ruleset beserta hasilnya. Push langsung di Langkah 4 harus tercatat sebagai fail. Di UI ada di Settings → Rules → Insights.
Batasan yang harus jujur ditulis
Ruleset menahan kesalahan dan jalan pintas. Ia nggak menahan semuanya.
Batasan | Penjelasan dan mitigasi |
|---|---|
Admin bisa mengubah atau menghapus ruleset | Saya satu-satunya admin, jadi secara teknis saya bisa mematikan ruleset lalu push. Perubahannya tercatat di riwayat ruleset dan di JSON yang di-commit. Di perusahaan, ruleset dipasang di level organisasi oleh tim security, dan admin repo nggak bisa mengubahnya |
0 approval | Kerja sendiri. Di tim: minimal 1 approval plus |
PR bisa mengubah workflow gate-nya sendiri | Untuk event |
Gate bergantung pada kualitas aturan scanner | Terbukti di Langkah 1: |
Celah ketiga itu menurut saya pelajaran paling penting di bagian ini: gate harus dikelola oleh pihak yang berbeda dari yang dijaga.
Rangkuman
Dua ruleset aktif,
protect-developmentdanprotect-main, disimpan sebagai JSON didocs/rulesets/dan dipasang lewat API.Bypass list kosong. Push langsung ditolak
GH013, dangh pr merge --adminjuga ditolak.Tujuh check
pr-checkswajib hijau, dikunci ke GitHub Actions lewatintegration_id: 15368.mainjuga mewajibkansource-branch.Hanya merge commit yang boleh, supaya Bagian 8 bisa mempromosikan image tanpa build ulang.
Dua lubang gate ditemukan dan ditambal sebelum ruleset dipasang: typo
limit-severities-for-sarifdan Semgrep yang butuhp/default.Lima demo PR, lima gate berbeda, semuanya
BLOCKED.
Sekarang kode nggak bisa masuk development atau main tanpa lolos gate. Tapi kode yang lolos itu belum jalan di mana pun. Di Bagian 7, saya menyiapkan Dokploy dengan dua environment, dev dan production, lengkap dengan Vault Agent sidecar dan auto-deploy yang dimatikan, supaya nanti hanya CI yang boleh memicu deploy.