Skip to main content
  1. Posts/
  2. Learning ArgoCD/
  3. Operations & Debugging/

GitHub Webhook for Instant Sync

·1141 words·6 mins
Ravi Singh
Author
Ravi Singh
Software engineer with 15+ years building backend systems and cloud platforms across fintech, automotive, and academia. I write about the things I build, debug, and learn — so I don’t forget them.
Learning ArgoCD - This article is part of a series.
Part 4: This Article

GitHub Webhook for Instant Sync
#

What This Covers
#

Why ArgoCD’s default polling is slow, how GitHub webhooks replace polling with event-driven sync, and how to wire it up for a local cluster using ngrok - including a shared HMAC secret so ArgoCD can verify requests really came from GitHub.


Why Polling Is Slow
#

By default ArgoCD polls your Git repo every 3 minutes. After a git push you wait up to 3 minutes before ArgoCD notices the change and triggers a sync. For rapid iteration this is painful.

The fix is webhooks. Instead of ArgoCD asking GitHub “anything new?”, GitHub calls ArgoCD the moment a push happens. Sync starts in seconds.


How It Works
#

ArgoCD exposes a built-in webhook endpoint:

1
POST /api/webhook

It understands GitHub, GitLab, Bitbucket, and generic payloads natively - no plugin required.

1
2
3
4
5
git push
  → GitHub fires POST /api/webhook to public URL
    → ngrok forwards to argocd-server on localhost
      → ArgoCD validates HMAC signature
        → triggers immediate refresh + sync

The Webhook Secret (HMAC-SHA256)
#

GitHub signs every webhook payload using your shared secret:

1
X-Hub-Signature-256: sha256=<HMAC-SHA256(secret, payload)>

ArgoCD reads webhook.github.secret from the argocd-secret Kubernetes Secret and validates this header on every incoming request. If the signature doesn’t match, the request is rejected with 401. This prevents anyone with your ngrok URL from spoofing a sync trigger.

Why ngrok?
#

ArgoCD runs inside Rancher Desktop - it has no public IP. GitHub can’t reach localhost. ngrok creates a temporary public HTTPS tunnel to your local port:

1
GitHub → https://abc123.ngrok-free.app → localhost:8080 → argocd-server

The ngrok URL changes every time you restart the tunnel (on the free tier without a static domain). You’ll need to update the GitHub webhook URL if that happens.


Setup
#

1. Generate a webhook secret
#

1
2
openssl rand -hex 20
# Save this value - you'll need it in steps 2 and 5

2. Store the secret in argocd-secret
#

1
2
3
kubectl patch secret argocd-secret -n argocd \
  --type merge \
  -p '{"stringData":{"webhook.github.secret":"<your-generated-secret>"}}'

ArgoCD watches this secret and picks up the change without a pod restart.

Verify it was written:

1
2
kubectl get secret argocd-secret -n argocd \
  -o jsonpath='{.data.webhook\.github\.secret}' | base64 -d && echo

3. Port-forward ArgoCD
#

ArgoCD is running in insecure (HTTP) mode - TLS is terminated at Traefik. Use port 80 on the service side:

1
kubectl port-forward svc/argocd-server -n argocd 8080:80

Keep this running in its own terminal.

4. Start ngrok
#

In a second terminal:

1
ngrok http 8080

Note the Forwarding HTTPS URL from the output - e.g.:

1
Forwarding  https://abc123.ngrok-free.app -> http://localhost:8080

5. Add the webhook in GitHub
#

Go to: github.com/ravikrs/learning-argocdSettings → Webhooks → Add webhook

FieldValue
Payload URLhttps://abc123.ngrok-free.app/api/webhook
Content typeapplication/json
Secretthe value from step 1
Which eventsJust the push event

Click Add webhook. GitHub immediately sends a ping event - you should see a green tick and a 200 response in the Recent Deliveries tab.


Verification
#

1. GitHub - confirm delivery succeeded
#

In the GitHub webhook settings (Settings → Webhooks → your webhook → Recent Deliveries), each delivery shows the HTTP status ArgoCD returned. A green tick with 200 means the request was delivered and ArgoCD accepted it. This confirms the network path works but not that ArgoCD acted on it - GitHub has no visibility into what ArgoCD does after the 200.

2. ngrok terminal - confirm the request arrived locally
#

After a git push, you should see a line appear within a couple of seconds:

1
POST /api/webhook   200 OK

If you see 200 here as well as on GitHub, the full network path is confirmed: GitHub → ngrok → argocd-server.

3. argocd-server logs - confirm ArgoCD received and processed the webhook
#

This is the most direct confirmation on the ArgoCD side:

1
kubectl logs -n argocd deploy/argocd-server --since=5m | grep -i webhook

A successful receipt looks like:

1
level=info msg="Received push event" namespace=argocd repo=https://github.com/ravikrs/learning-argocd

If you want more context lines around the match:

1
kubectl logs -n argocd deploy/argocd-server --since=5m | grep -i -A2 webhook

If GitHub and ngrok show 200 but this log line is absent, the request was likely rejected silently due to an HMAC signature mismatch. Check with:

1
kubectl logs -n argocd deploy/argocd-server --since=5m | grep -i -E "webhook|signature|invalid"

4. ArgoCD CLI - confirm the app synced as a result
#

1
argocd app get <app-name>

Look for Operation: Sync with a timestamp matching your push. Webhook-triggered syncs appear identical to automated syncs - what you’re checking is that the timestamp is within seconds of the push rather than up to 3 minutes later.

Alternatively via kubectl:

1
2
kubectl get application -n argocd <app-name> \
  -o jsonpath='{.status.operationState.finishedAt}' && echo

5. Kubernetes events - confirm the operation was started
#

1
2
3
kubectl get events -n argocd \
  --field-selector reason=OperationStarted \
  --sort-by='.lastTimestamp'

Smoke test: make a trivial change and push
#

1
2
3
4
5
# Add a blank line to a values file, commit, and push
echo "" >> environments/eu-dev-rancher/services/svc1.yaml
git add environments/eu-dev-rancher/services/svc1.yaml
git commit -m "test: webhook smoke test"
git push

Watch steps 2–4 in sequence: ngrok shows 200, argocd-server log shows the push event, and the app’s sync timestamp updates within seconds.


Gotchas
#

ngrok URL changes on every restart

Free ngrok accounts without a static domain get a new random subdomain each session. You must update the GitHub webhook Payload URL after restarting ngrok. A free ngrok account gives you one permanent static domain - worth using once you’ve verified the setup works.

Port-forward must use port 80, not 443

Because argocd-server runs in insecure (HTTP) mode, the correct service port is 80. Port-forwarding to 443 still works technically (both listeners map to the same pod port) but sends HTTP traffic over what looks like an HTTPS port, which can confuse tools. Be explicit with port 80.

argocd-secret is not hot-reloaded on older ArgoCD versions

On ArgoCD v2.4 and earlier, argocd-server reads argocd-secret only at startup. Patching the secret has no effect until you restart the pod. On v2.5+ the server watches the secret dynamically. If validation keeps failing with 401, try:

1
kubectl rollout restart deployment argocd-server -n argocd

GitHub sends a ping on webhook creation - that’s expected

The first delivery is a ping event with {"zen":"..."}. ArgoCD responds with 200 but does not trigger a sync. This is correct behaviour. Subsequent push events trigger the sync.

Webhook delivers to ArgoCD, not to individual Applications

ArgoCD matches the webhook’s repository URL against all registered Applications and refreshes every Application that points to that repo. If you have multiple apps from the same repo (as this setup does), all of them get refreshed on every push - which is exactly what you want.

Learning ArgoCD - This article is part of a series.
Part 4: This Article