Automasi developer berbasis Git tidak hanya datang dari server CI/CD atau platform hosting repository. Tool lokal juga bisa menjadi pemicu alur kerja tertentu. Sebagai konteks, rilis Magit 4.6 menunjukkan bagaimana tooling di sekitar Git terus berkembang dan makin terintegrasi dengan workflow harian developer. Namun inti masalahnya bukan pada tool tertentu, melainkan pada desain backend saat menerima event Git seperti push, tag, merge, atau release.
Jika Anda sedang merancang kontrak API untuk Git Hook, fokus utamanya adalah: payload harus stabil, request harus bisa diverifikasi, event boleh di-retry tanpa efek samping ganda, dan consumer tidak boleh rusak saat schema berkembang. Backend yang menerima webhook Git harus mengasumsikan bahwa event bisa datang ganda, terlambat, tidak berurutan, atau bahkan mewakili kondisi repository yang sudah berubah lagi.
Mengapa kontrak API webhook Git harus dirancang ketat
Webhook sering terlihat sederhana: kirim HTTP POST lalu proses payload. Masalahnya, event Git membawa karakteristik yang membuat implementasi naif mudah gagal:
- Delivery tidak selalu tepat satu kali. Pengirim bisa melakukan retry jika timeout atau menerima 5xx.
- Urutan tidak terjamin. Event release bisa tiba sebelum event push yang berkaitan, atau dua push ke branch yang sama datang terbalik.
- State repository bisa berubah cepat. Force-push dapat membuat commit yang sebelumnya valid menjadi tidak lagi berada di branch yang sama.
- Payload berevolusi. Producer bisa menambah field baru, mengubah format opsional, atau mengirim event type baru.
Karena itu, desain yang benar bukan hanya “endpoint menerima JSON”, tetapi kontrak yang jelas untuk identitas event, integritas request, aturan retry, dan semantik respons.
Struktur kontrak API untuk Git Hook
Field minimum yang sebaiknya ada
Untuk event Git, kontrak payload sebaiknya memisahkan metadata delivery dari data domain. Ini membantu verifikasi, deduplikasi, observability, dan kompatibilitas schema.
{
"spec_version": "2026-01",
"event_id": "01JZ8Y7LQ7K8Y9JQ3M4N5P6R7S",
"event_type": "git.push",
"occurred_at": "2026-07-01T10:15:30Z",
"delivery_attempt": 1,
"source": {
"provider": "internal-git-client",
"installation_id": "team-alpha",
"repository_id": "repo_12345",
"repository": "org/example-app"
},
"actor": {
"id": "user_789",
"username": "nina"
},
"data": {
"ref": "refs/heads/main",
"before": "3b1c4d5e",
"after": "8f9a0b1c",
"forced": false,
"commits": [
{ "id": "7a7b7c7d", "message": "Fix webhook dedup" },
{ "id": "8f9a0b1c", "message": "Add retry metrics" }
]
}
}Penjelasan field penting:
- spec_version: versi kontrak payload, bukan versi aplikasi. Ini memudahkan evolusi schema.
- event_id: ID unik per event untuk deduplikasi dan tracing.
- event_type: misalnya
git.push,git.tag.created,git.merge,git.release.published. - occurred_at: waktu event terjadi pada sisi producer, bukan waktu diterima server.
- delivery_attempt: nomor percobaan pengiriman. Berguna untuk observability, tetapi jangan dipakai sebagai identitas event.
- source: identitas asal event agar multi-tenant atau multi-provider lebih aman.
- data: isi domain spesifik event.
Pisahkan metadata delivery dari payload bisnis
Jangan menaruh semua field secara datar. Struktur yang memisahkan metadata dan data domain memberi beberapa keuntungan:
- consumer bisa membaca field umum tanpa tahu semua detail setiap event;
- penambahan field baru pada
datatidak mudah memecahkan parser lama; - logging dan tracing lebih konsisten.
Gunakan schema yang toleran terhadap penambahan field
Consumer sebaiknya mengabaikan field yang tidak dikenali, bukan gagal total. Prinsip ini penting agar producer bisa menambah metadata baru tanpa merusak integrasi lama. Sebaliknya, hindari perubahan yang memutus kompatibilitas seperti:
- mengganti arti field yang sudah ada;
- mengubah tipe field dari string ke object tanpa versi baru;
- menghapus field wajib tanpa masa transisi.
Praktik aman: tambahkan field baru sebagai opsional, dokumentasikan
spec_version, dan siapkan periode kompatibilitas jika ingin melakukan perubahan mayor.
Keamanan request: signature, timestamp, dan replay protection
Verifikasi signature HMAC
Webhook tanpa verifikasi signature pada dasarnya hanya endpoint publik yang menerima JSON. Minimal, gunakan shared secret dan tanda tangani raw request body dengan HMAC-SHA256. Jangan menandatangani JSON yang sudah di-parse, karena perbedaan whitespace atau serialisasi bisa membuat verifikasi salah.
POST /webhooks/git HTTP/1.1
Content-Type: application/json
X-Webhook-Id: 01JZ8Y7LQ7K8Y9JQ3M4N5P6R7S
X-Webhook-Timestamp: 2026-07-01T10:15:31Z
X-Webhook-Signature: sha256=4f7c2e...Alur verifikasi yang aman:
- baca raw body apa adanya;
- ambil timestamp dari header;
- buat string yang ditandatangani, misalnya
timestamp + "." + raw_body; - hitung HMAC dengan secret yang sesuai tenant atau source;
- bandingkan dengan constant-time comparison untuk menghindari timing attack.
Gunakan timestamp untuk membatasi request lama
Signature saja tidak cukup. Jika attacker memperoleh request valid, ia bisa mencoba mengirim ulang. Karena itu, sertakan timestamp header dan tolak request yang terlalu lama atau terlalu jauh di masa depan. Jendela toleransi umum biasanya beberapa menit, tetapi nilai pastinya bergantung pada karakteristik jaringan dan retry producer Anda.
Yang penting, validasi timestamp harus mempertimbangkan:
- sinkronisasi jam antar sistem;
- kemungkinan delivery terlambat yang masih sah;
- trade-off antara keamanan dan reliabilitas.
Replay protection tidak sama dengan idempotensi
Dua konsep ini sering tercampur:
- Replay protection mencegah request yang sama dikirim ulang secara berbahaya atau tidak sah.
- Idempotensi memastikan event yang sama tidak menghasilkan efek samping ganda saat dikirim ulang secara sah.
Replay protection biasanya memakai kombinasi event_id, signature, timestamp, dan penyimpanan nonce atau delivery ID untuk jangka waktu tertentu. Idempotensi fokus pada sisi pemrosesan bisnis.
Idempotensi, deduplikasi, dan retry policy
Gunakan idempotency key yang stabil
Untuk webhook Git, kandidat idempotency key terbaik biasanya bukan kombinasi branch dan commit terakhir, melainkan event_id yang memang dikeluarkan producer dan unik per event. Jika producer tidak punya event ID, Anda perlu mendefinisikan aturan deterministik sendiri, misalnya hash dari provider, repository, event_type, ref, before, after, dan occurred_at. Namun ini kurang ideal karena lebih rentan bentrok atau berubah.
Simpan status pemrosesan berdasarkan idempotency key:
- received: request lolos verifikasi dasar;
- processing: sedang dikerjakan worker;
- processed: selesai sukses;
- failed_retryable: gagal sementara, boleh dicoba lagi;
- failed_permanent: gagal permanen, perlu tindakan manual atau ditolak.
Deduplikasi harus terjadi sebelum efek samping
Kesalahan umum adalah baru mengecek duplikasi setelah menjalankan job, mengirim notifikasi, atau menulis deployment record. Deduplikasi yang benar harus dilakukan sedini mungkin, idealnya dalam transaksi yang juga mencatat status event. Dengan begitu, dua request paralel dengan event_id sama tidak sama-sama mengeksekusi efek samping.
-- Contoh konsep, bukan SQL vendor-spesifik
CREATE TABLE webhook_events (
event_id VARCHAR(64) PRIMARY KEY,
event_type VARCHAR(64) NOT NULL,
source_repository_id VARCHAR(128) NOT NULL,
occurred_at TIMESTAMP NOT NULL,
status VARCHAR(32) NOT NULL,
payload_json TEXT NOT NULL,
created_at TIMESTAMP NOT NULL,
updated_at TIMESTAMP NOT NULL
);Jika database mendukung unique constraint, jadikan event_id unik lalu tangani konflik insert sebagai sinyal event duplikat.
Retry policy: kapan producer harus retry?
Producer sebaiknya hanya me-retry kegagalan yang kemungkinan sementara, misalnya timeout, koneksi gagal, atau respons 5xx. Jangan me-retry tanpa batas. Gunakan exponential backoff dengan jitter agar tidak membanjiri receiver saat ada gangguan.
Pada sisi receiver, respons HTTP harus mencerminkan apakah producer sebaiknya mengirim ulang:
- 2xx: event diterima. Tidak perlu retry.
- 4xx: request salah atau tidak sah. Biasanya jangan retry kecuali kasus tertentu seperti rate limit yang jelas didokumentasikan.
- 5xx: receiver gagal memproses atau tidak siap. Producer boleh retry.
2xx tidak harus berarti proses selesai
Untuk webhook, pola paling aman biasanya verify fast, persist fast, ack fast. Artinya:
- verifikasi signature dan validasi minimum;
- simpan event ke durable storage atau queue internal;
- kembalikan
202 Acceptedatau200 OKdengan cepat; - proses bisnis dilakukan async oleh worker.
Pendekatan ini mengurangi timeout dan membuat retry lebih mudah dikendalikan. Namun konsekuensinya, producer hanya tahu bahwa event diterima, bukan bahwa seluruh workflow bisnis berhasil. Itu biasanya trade-off yang tepat untuk webhook.
Ordering issue dan edge case integrasi Git
Event ganda dan delivery terlambat
Event yang sama bisa terkirim dua kali karena network timeout, retry dari producer, atau failover pada sisi pengirim. Selain itu, event lama bisa tiba setelah event baru sudah diproses. Karena itu, jangan berasumsi bahwa event terakhir yang diterima adalah state terbaru repository.
Untuk operasi yang sensitif terhadap urutan, simpan juga informasi versi state domain, misalnya:
- ref yang terdampak;
- commit
beforedanafter; - waktu event;
- revision atau sequence jika producer menyediakannya.
Force-push pada branch
Force-push adalah salah satu edge case terpenting. Jika backend Anda memicu build, sinkronisasi metadata, atau deployment dari event push, Anda harus mempertimbangkan bahwa:
- rantai commit sebelumnya bisa tidak lagi ada pada branch yang sama;
- event lama yang baru tiba mungkin merujuk pada sejarah branch yang sudah ditulis ulang;
- consumer yang hanya melihat
aftertanpabeforebisa salah menafsirkan perubahan.
Karena itu, untuk event push sertakan field seperti ref, before, after, dan forced. Saat forced=true, consumer sebaiknya memperlakukan event sebagai perubahan state yang berpotensi non-linear, bukan sekadar tambahan commit biasa.
Tag, merge, dan release tidak selalu identik
Jangan paksa semua event memakai schema domain yang sama. Misalnya:
- git.tag.created perlu nama tag, target object, dan tipe target.
- git.merge perlu informasi source branch, target branch, merge commit, dan mungkin strategy.
- git.release.published perlu release identifier, tag terkait, nama release, dan artefak atau URL yang relevan.
Gunakan envelope umum yang sama, tetapi biarkan data berbeda sesuai event type.
Skema event per jenis
{
"spec_version": "2026-01",
"event_id": "01JZ8Z123456789ABCDEFGHJK",
"event_type": "git.release.published",
"occurred_at": "2026-07-01T10:20:00Z",
"source": {
"provider": "internal-git-client",
"repository_id": "repo_12345",
"repository": "org/example-app"
},
"data": {
"release_id": "rel_2026_07_01_001",
"tag_name": "v2.4.0",
"target_commit": "8f9a0b1c",
"name": "v2.4.0",
"notes": "Bugfix and retry hardening"
}
}Respons HTTP yang tepat: 2xx vs 4xx vs 5xx
Respons webhook harus punya semantik operasional yang jelas. Ini bukan sekadar formalitas HTTP, karena producer akan memutuskan retry dari sini.
| Kondisi | Status | Arti untuk producer | Catatan implementasi |
|---|---|---|---|
| Signature valid, payload diterima untuk diproses async | 202 Accepted | Jangan retry | Paling aman untuk arsitektur queue-based |
| Signature valid, sinkron dan selesai diproses | 200 OK | Jangan retry | Cocok jika proses cepat dan deterministik |
| Payload JSON rusak atau field wajib hilang | 400 Bad Request | Jangan retry tanpa perbaikan payload | Log detail validasi |
| Signature salah atau secret tidak cocok | 401/403 | Jangan retry sampai konfigurasi diperbaiki | Jangan bocorkan detail sensitif |
| Event type tidak didukung | 400 atau 422 | Jangan retry | Tergantung kontrak API Anda |
| Rate limit sementara | 429 Too Many Requests | Retry sesuai kebijakan yang didokumentasikan | Gunakan jika memang sengaja menahan traffic |
| Database/queue internal gagal sementara | 500/503 | Boleh retry | Pastikan idempotensi siap menangani duplikasi |
Satu hal penting: jika event duplikat datang lagi dan Anda sudah pernah memprosesnya, umumnya tetap aman mengembalikan 2xx. Dari perspektif producer, pengiriman sudah berhasil, dan receiver cukup mengabaikan efek samping ulang.
Contoh alur implementasi backend yang aman
Pola receiver sederhana
1. Terima request HTTP
2. Baca raw body
3. Verifikasi timestamp dan signature
4. Parse JSON dan validasi field minimum
5. Simpan event dengan event_id unik
6. Jika duplikat: return 200/202
7. Publish job internal ke queue
8. Return 202 secepat mungkin
9. Worker memproses event secara idempotent
10. Update status processed / failed_retryable / failed_permanentPseudocode verifikasi dan deduplikasi
def handle_webhook(headers, raw_body):
timestamp = headers.get("X-Webhook-Timestamp")
signature = headers.get("X-Webhook-Signature")
if not is_fresh_timestamp(timestamp):
return 401, "stale request"
if not verify_hmac(timestamp, raw_body, signature, secret_lookup(headers)):
return 401, "invalid signature"
payload = parse_json(raw_body)
validate_minimum_fields(payload)
event_id = payload["event_id"]
inserted = insert_event_if_absent(
event_id=event_id,
event_type=payload["event_type"],
payload_json=raw_body,
status="received"
)
if not inserted:
return 202, "duplicate ignored"
enqueue_job(event_id)
return 202, "accepted"Pseudocode di atas sengaja sederhana, tetapi memuat prinsip penting: verifikasi sebelum parsing bisnis, deduplikasi sebelum efek samping, dan respons cepat setelah event aman disimpan.
Failure mode yang paling sering muncul
| Failure mode | Dampak | Penyebab umum | Mitigasi |
|---|---|---|---|
| Event diproses dua kali | Build/deploy/notifikasi ganda | Retry, timeout, race condition | Unique event_id, status event, idempotent worker |
| Signature selalu gagal | Semua webhook ditolak | Body diubah middleware, secret salah, canonicalization keliru | Verifikasi raw body, rotasi secret yang rapi, log hash input |
| Event terlambat memicu state lama | Workflow salah sasaran | Queue lambat, retry panjang, gangguan jaringan | Bandingkan state repo/ref saat ini, cek before/after, toleransi ordering |
| Force-push membuat consumer bingung | Diff atau sinkronisasi salah | Consumer mengasumsikan history linear | Sertakan forced, before, after; perlakukan sebagai state rewrite |
| Perubahan schema merusak consumer | Parsing gagal, integrasi putus | Field dihapus/diubah tipe tanpa versi | Versioning, additive changes, ignore unknown fields |
| 5xx memicu badai retry | Beban server meningkat | Producer retry agresif, receiver lambat | Backoff + jitter, queue internal, observability, 429/503 yang jelas |
| Duplikasi karena paralel request | Race condition pada insert/proses | Tidak ada constraint unik | Unique constraint, lock ringan, transaksi atomik |
Perubahan schema tanpa merusak consumer
Gunakan versioning dengan disiplin
spec_version tidak harus berubah setiap kali ada field baru. Untuk perubahan aditif yang kompatibel, Anda bisa mempertahankan versi yang sama jika kontrak memang menyatakan bahwa consumer harus mengabaikan field tak dikenal. Naikkan versi jika ada perubahan mayor yang memengaruhi parsing atau semantik.
Dokumentasikan field wajib dan opsional
Sumber masalah integrasi paling sering bukan algoritme, melainkan asumsi yang tidak didokumentasikan. Untuk tiap event_type, jelaskan:
- field wajib;
- field opsional;
- arti null atau field kosong;
- apakah urutan event dijamin atau tidak;
- apakah event bisa dikirim ulang;
- apa arti 2xx, 4xx, dan 5xx.
Hindari semantik yang ambigu
Misalnya, field timestamp tanpa konteks bisa membingungkan: apakah itu waktu event terjadi, waktu request dibuat, atau waktu request dikirim ulang? Lebih baik gunakan nama eksplisit seperti occurred_at dan delivered_at bila keduanya memang diperlukan.
Debugging dan observability
Agar kontrak API untuk Git Hook mudah dioperasikan, siapkan observability sejak awal:
- log event_id, event_type, repository, dan hasil verifikasi;
- simpan payload mentah secara aman untuk debugging, dengan kontrol akses yang ketat;
- catat alasan penolakan: signature invalid, timestamp stale, schema invalid, unsupported event;
- buat metrik untuk jumlah accepted, duplicate, invalid, retryable failure, dan processing latency;
- hubungkan
event_idke trace atau job ID internal.
Saat debugging kegagalan signature, tiga penyebab yang paling sering adalah:
- server memverifikasi body yang sudah diubah parser atau middleware;
- secret yang dipakai tidak sesuai source atau tenant;
- format string yang ditandatangani tidak konsisten antara producer dan consumer.
Checklist implementasi
- Tentukan envelope event yang konsisten:
spec_version,event_id,event_type,occurred_at,source,data. - Gunakan HTTPS dan verifikasi signature HMAC pada raw body.
- Sertakan timestamp request dan tolak request yang terlalu lama.
- Terapkan replay protection untuk delivery yang dicurigai diulang.
- Jadikan
event_idsebagai idempotency key dengan constraint unik. - Simpan event sebelum memicu efek samping atau job downstream.
- Kembalikan
202 Accepteduntuk pola async yang cepat dan tahan timeout. - Gunakan retry dengan exponential backoff dan jitter pada sisi producer.
- Rancang consumer agar toleran terhadap event ganda, terlambat, dan tidak berurutan.
- Untuk event push, sertakan
ref,before,after, danforced. - Dokumentasikan semantik 2xx, 4xx, 5xx secara eksplisit.
- Pastikan schema berevolusi secara aditif dan consumer mengabaikan field yang tidak dikenali.
- Siapkan logging, metrics, dan tracing berbasis
event_id.
Penutup
Saat tool developer makin sering memicu automasi berbasis Git, kualitas integrasi backend ditentukan oleh kontrak webhook yang disiplin. Kontrak API untuk Git Hook yang baik bukan hanya mendefinisikan payload, tetapi juga cara memverifikasi request, mencegah replay, menangani retry, menjaga idempotensi, dan bertahan terhadap event ganda maupun out-of-order.
Jika Anda mulai dari satu prinsip, pilih ini: anggap setiap event bisa datang lebih dari sekali, bisa datang terlambat, dan bisa mewakili state Git yang sudah berubah lagi. Dari asumsi itu, keputusan desain seperti event ID unik, deduplikasi awal, respons 202, serta schema yang kompatibel akan terasa jauh lebih masuk akal.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!