[{"data":1,"prerenderedAt":537},["ShallowReactive",2],{"content-query-j1qZKb2DrH":3},{"_path":4,"_dir":5,"_draft":6,"_partial":6,"_locale":7,"title":8,"description":9,"layout":10,"author":11,"tags":12,"categories":19,"date":21,"faq":22,"image":35,"body":36,"_type":530,"_id":531,"_source":532,"_file":533,"_stem":534,"_extension":535,"sitemap":536},"/blog/webhooks-in-ci-cd","blog",false,"","Receiving Webhooks in CI/CD: GitHub Actions, Jenkins & Harness (2026)","CI/CD runners are ephemeral and private, so providers can't reach them at a stable URL. Learn how to receive webhooks in GitHub Actions, Jenkins and Harness — behind a firewall, with a persistent URL and no open ports — using Webhook Relay.","post","Karolis Rusenas",[13,14,15,16,17,18],"ci/cd","github actions","jenkins","harness","webhooks","tunnels",[20],"guides","2026-07-18 10:00:00",[23,26,29,32],{"q":24,"a":25},"How do I receive a webhook in GitHub Actions?","Two patterns. To start a workflow from an external event, have the sender call GitHub's repository_dispatch or workflow_dispatch API — Webhook Relay can transform a provider webhook into that call. To receive a webhook inside a job or on a self-hosted runner behind a firewall, run the relay agent so the event is delivered to the runner over an outbound connection with no public IP.",{"q":27,"a":28},"How do I get GitHub or Bitbucket webhooks to a Jenkins server behind a firewall?","Point the provider at a persistent Webhook Relay bucket URL, then run the relay agent next to Jenkins. The agent connects outbound and forwards each webhook to the Jenkins endpoint (for example /github-webhook/), so no inbound firewall ports or public IP are required.",{"q":30,"a":31},"Can Harness receive webhooks through a self-hosted delegate behind a firewall?","Yes. Run the relay agent inside the same network as your self-managed Harness delegate or webhook endpoint and Webhook Relay delivers inbound trigger webhooks to it over an outbound connection. Verify Harness's current webhook-trigger configuration as of 2026 before wiring it up.",{"q":33,"a":34},"Why not just expose my CI server with a tunnel like ngrok?","A plain tunnel hands one URL to one process and drops events when the receiver is down — a problem for ephemeral runners. Webhook Relay gives a persistent URL that never changes as runners come and go, lets multiple agents share one bucket, and retries delivery for up to 30 days so a webhook fired while a runner was spinning up still lands.","/images/blog/heroes/route.jpg",{"type":37,"children":38,"toc":518},"root",[39,49,71,99,106,111,153,159,193,230,236,241,282,299,304,309,353,358,388,393,398,404,424,430,441,447,499,512],{"type":40,"tag":41,"props":42,"children":43},"element","p",{},[44],{"type":40,"tag":45,"props":46,"children":48},"img",{"alt":47,"src":35},"Receiving webhooks in CI/CD: GitHub Actions, Jenkins and Harness",[],{"type":40,"tag":41,"props":50,"children":51},{},[52,55,61,63,69],{"type":53,"value":54},"text","Continuous integration and delivery runs on webhooks — a push, a merged PR, a released tag, a provider event kicks off a build or a deploy. But CI/CD infrastructure is the ",{"type":40,"tag":56,"props":57,"children":58},"em",{},[59],{"type":53,"value":60},"worst",{"type":53,"value":62}," place to receive a webhook: runners are ",{"type":40,"tag":64,"props":65,"children":66},"strong",{},[67],{"type":53,"value":68},"ephemeral and usually private",{"type":53,"value":70},", so the sender has nothing stable to point at, and anything that arrives while a runner is spinning up is simply gone.",{"type":40,"tag":41,"props":72,"children":73},{},[74,76,83,85,90,92,97],{"type":53,"value":75},"This is the same problem people hit with ",{"type":40,"tag":77,"props":78,"children":80},"a",{"href":79},"/blog/ngrok-alternative",[81],{"type":53,"value":82},"ngrok",{"type":53,"value":84},", amplified. The fix is the same shape: a ",{"type":40,"tag":64,"props":86,"children":87},{},[88],{"type":53,"value":89},"persistent URL",{"type":53,"value":91}," the provider is configured with once, plus an ",{"type":40,"tag":64,"props":93,"children":94},{},[95],{"type":53,"value":96},"outbound agent",{"type":53,"value":98}," that delivers into your private network — so no runner ever needs a public IP.",{"type":40,"tag":100,"props":101,"children":103},"h2",{"id":102},"the-problem-cicd-runners-have-no-stable-public-address",[104],{"type":53,"value":105},"The problem: CI/CD runners have no stable public address",{"type":40,"tag":41,"props":107,"children":108},{},[109],{"type":53,"value":110},"Three things make webhooks hard in CI/CD:",{"type":40,"tag":112,"props":113,"children":114},"ul",{},[115,126,136],{"type":40,"tag":116,"props":117,"children":118},"li",{},[119,124],{"type":40,"tag":64,"props":120,"children":121},{},[122],{"type":53,"value":123},"No public IP.",{"type":53,"value":125}," Self-hosted GitHub Actions runners, on-prem Jenkins controllers and self-managed Harness delegates all sit behind a firewall. Opening inbound ports to them is exactly what your security team doesn't want.",{"type":40,"tag":116,"props":127,"children":128},{},[129,134],{"type":40,"tag":64,"props":130,"children":131},{},[132],{"type":53,"value":133},"Ephemerality.",{"type":53,"value":135}," A fresh runner comes up with a new address and disappears when the job ends. A URL you paste into a provider today is meaningless tomorrow.",{"type":40,"tag":116,"props":137,"children":138},{},[139,144,146,151],{"type":40,"tag":64,"props":140,"children":141},{},[142],{"type":53,"value":143},"Timing.",{"type":53,"value":145}," The webhook that triggers a job often fires ",{"type":40,"tag":56,"props":147,"children":148},{},[149],{"type":53,"value":150},"before",{"type":53,"value":152}," the environment is ready. Miss it and the pipeline silently never runs.",{"type":40,"tag":100,"props":154,"children":156},{"id":155},"the-pattern-a-persistent-bucket-the-relay-agent",[157],{"type":53,"value":158},"The pattern: a persistent bucket + the relay agent",{"type":40,"tag":41,"props":160,"children":161},{},[162,164,169,171,176,178,184,186,191],{"type":53,"value":163},"Webhook Relay gives you a ",{"type":40,"tag":64,"props":165,"children":166},{},[167],{"type":53,"value":168},"bucket",{"type":53,"value":170}," with a URL that never changes. You configure the provider (GitHub, GitLab, Bitbucket, Stripe, a partner API) with that URL ",{"type":40,"tag":64,"props":172,"children":173},{},[174],{"type":53,"value":175},"once",{"type":53,"value":177},". Inside your network, the lightweight ",{"type":40,"tag":77,"props":179,"children":181},{"href":180},"/features/webhook-to-internal-server",[182],{"type":53,"value":183},"relay agent",{"type":53,"value":185}," connects ",{"type":40,"tag":56,"props":187,"children":188},{},[189],{"type":53,"value":190},"outbound",{"type":53,"value":192}," to Webhook Relay and forwards each event to wherever it needs to land — a runner, a controller, an internal deploy hook. No inbound ports, no public IP, and the provider config survives every runner that comes and goes.",{"type":40,"tag":41,"props":194,"children":195},{},[196,198,204,206,212,214,220,222,228],{"type":53,"value":197},"Because it's a full ",{"type":40,"tag":77,"props":199,"children":201},{"href":200},"/webhook-gateway",[202],{"type":53,"value":203},"webhook gateway",{"type":53,"value":205},", you also get signature verification, ",{"type":40,"tag":77,"props":207,"children":209},{"href":208},"/features/transform-webhooks",[210],{"type":53,"value":211},"payload transformation",{"type":53,"value":213},", ",{"type":40,"tag":77,"props":215,"children":217},{"href":216},"/features/webhook-multiple-destinations",[218],{"type":53,"value":219},"fan-out",{"type":53,"value":221}," and ",{"type":40,"tag":77,"props":223,"children":225},{"href":224},"/features/durable-retries",[226],{"type":53,"value":227},"durable retries",{"type":53,"value":229}," on top of the tunnel.",{"type":40,"tag":100,"props":231,"children":233},{"id":232},"github-actions",[234],{"type":53,"value":235},"GitHub Actions",{"type":40,"tag":41,"props":237,"children":238},{},[239],{"type":53,"value":240},"There are two distinct jobs people mean by \"webhooks in GitHub Actions\":",{"type":40,"tag":41,"props":242,"children":243},{},[244,249,251,258,260,266,268,273,275,280],{"type":40,"tag":64,"props":245,"children":246},{},[247],{"type":53,"value":248},"1. Trigger a workflow from an external event.",{"type":53,"value":250}," GitHub starts workflows from its own ",{"type":40,"tag":252,"props":253,"children":255},"code",{"className":254},[],[256],{"type":53,"value":257},"repository_dispatch",{"type":53,"value":259}," / ",{"type":40,"tag":252,"props":261,"children":263},{"className":262},[],[264],{"type":53,"value":265},"workflow_dispatch",{"type":53,"value":267}," API. Point a provider at a Webhook Relay bucket, use a ",{"type":40,"tag":77,"props":269,"children":270},{"href":208},[271],{"type":53,"value":272},"transformation",{"type":53,"value":274}," to reshape the incoming payload into a ",{"type":40,"tag":252,"props":276,"children":278},{"className":277},[],[279],{"type":53,"value":257},{"type":53,"value":281}," call, and forward it to GitHub — a clean way to kick off a pipeline from a system that doesn't speak GitHub's API natively.",{"type":40,"tag":41,"props":283,"children":284},{},[285,290,292,297],{"type":40,"tag":64,"props":286,"children":287},{},[288],{"type":53,"value":289},"2. Receive a webhook on a self-hosted runner.",{"type":53,"value":291}," When a job needs to ",{"type":40,"tag":56,"props":293,"children":294},{},[295],{"type":53,"value":296},"receive",{"type":53,"value":298}," an external callback — a deploy approval, a test event from a third-party sandbox, a partner's webhook — run the relay agent so the event is delivered to the runner's local endpoint over the outbound tunnel. A shared bucket means the same event can reach every runner in a matrix at once.",{"type":40,"tag":100,"props":300,"children":301},{"id":15},[302],{"type":53,"value":303},"Jenkins",{"type":40,"tag":41,"props":305,"children":306},{},[307],{"type":53,"value":308},"Jenkins is the classic case: GitHub, GitLab or Bitbucket needs to notify Jenkins to start a build, but Jenkins lives on-prem or inside a cluster. Point the provider at a persistent bucket URL and run the agent next to Jenkins:",{"type":40,"tag":310,"props":311,"children":315},"pre",{"className":312,"code":313,"language":314,"meta":7,"style":7},"language-bash shiki shiki-themes github-dark","relay forward --bucket acme-ci http://localhost:8080/github-webhook/\n","bash",[316],{"type":40,"tag":252,"props":317,"children":318},{"__ignoreMap":7},[319],{"type":40,"tag":320,"props":321,"children":324},"span",{"class":322,"line":323},"line",1,[325,331,337,343,348],{"type":40,"tag":320,"props":326,"children":328},{"style":327},"--shiki-default:#B392F0",[329],{"type":53,"value":330},"relay",{"type":40,"tag":320,"props":332,"children":334},{"style":333},"--shiki-default:#9ECBFF",[335],{"type":53,"value":336}," forward",{"type":40,"tag":320,"props":338,"children":340},{"style":339},"--shiki-default:#79B8FF",[341],{"type":53,"value":342}," --bucket",{"type":40,"tag":320,"props":344,"children":345},{"style":333},[346],{"type":53,"value":347}," acme-ci",{"type":40,"tag":320,"props":349,"children":350},{"style":333},[351],{"type":53,"value":352}," http://localhost:8080/github-webhook/\n",{"type":40,"tag":41,"props":354,"children":355},{},[356],{"type":53,"value":357},"The agent connects outbound, so there are no firewall changes. We've documented the common setups in depth:",{"type":40,"tag":112,"props":359,"children":360},{},[361,370,379],{"type":40,"tag":116,"props":362,"children":363},{},[364],{"type":40,"tag":77,"props":365,"children":367},{"href":366},"/blog/github-jenkins-guide",[368],{"type":53,"value":369},"GitHub → Jenkins: the complete webhook guide",{"type":40,"tag":116,"props":371,"children":372},{},[373],{"type":40,"tag":77,"props":374,"children":376},{"href":375},"/blog/webhooks-to-jenkins-on-kubernetes",[377],{"type":53,"value":378},"Webhooks to Jenkins running on Kubernetes",{"type":40,"tag":116,"props":380,"children":381},{},[382],{"type":40,"tag":77,"props":383,"children":385},{"href":384},"/blog/automated-github-pull-request-builds-on-jenkins",[386],{"type":53,"value":387},"Automated GitHub pull-request builds on Jenkins",{"type":40,"tag":100,"props":389,"children":390},{"id":16},[391],{"type":53,"value":392},"Harness",{"type":40,"tag":41,"props":394,"children":395},{},[396],{"type":53,"value":397},"Harness triggers pipelines from webhooks too, and its self-managed delegates run inside your network. Run the relay agent alongside the delegate (or the internal endpoint you use for custom webhook triggers) and Webhook Relay delivers inbound trigger webhooks to it over an outbound connection — no public exposure of your Harness install. As of 2026, check Harness's current webhook-trigger and delegate configuration and point the agent at the endpoint it expects.",{"type":40,"tag":100,"props":399,"children":401},{"id":400},"share-one-endpoint-across-the-team-and-the-pipeline",[402],{"type":53,"value":403},"Share one endpoint across the team and the pipeline",{"type":40,"tag":41,"props":405,"children":406},{},[407,409,414,416,422],{"type":53,"value":408},"The broadcast model that makes Webhook Relay useful for teams is just as useful across a pipeline. ",{"type":40,"tag":64,"props":410,"children":411},{},[412],{"type":53,"value":413},"Multiple agents can connect to the same bucket, and each one receives every webhook",{"type":53,"value":415}," — so a single provider event can reach a developer's laptop, a staging deploy hook and a CI runner at the same time, from one URL you configured once. It's the piece a plain tunnel can't do; see the ",{"type":40,"tag":77,"props":417,"children":419},{"href":418},"/blog/ngrok-alternative#2-one-bucket-your-whole-team",[420],{"type":53,"value":421},"ngrok alternative writeup",{"type":53,"value":423}," for the full pattern.",{"type":40,"tag":100,"props":425,"children":427},{"id":426},"dont-lose-events-while-a-runner-spins-up",[428],{"type":53,"value":429},"Don't lose events while a runner spins up",{"type":40,"tag":41,"props":431,"children":432},{},[433,435,439],{"type":53,"value":434},"The webhook that starts a pipeline often arrives before the environment is ready. With ",{"type":40,"tag":77,"props":436,"children":437},{"href":224},[438],{"type":53,"value":227},{"type":53,"value":440}," the event is persisted the moment it reaches Webhook Relay and retried — with exponential backoff, for up to 30 days — so a webhook that fired while a runner was booting or a controller was restarting still gets delivered instead of vanishing.",{"type":40,"tag":100,"props":442,"children":444},{"id":443},"get-started",[445],{"type":53,"value":446},"Get started",{"type":40,"tag":448,"props":449,"children":450},"ol",{},[451,464,469,482],{"type":40,"tag":116,"props":452,"children":453},{},[454,462],{"type":40,"tag":77,"props":455,"children":459},{"href":456,"rel":457},"https://my.webhookrelay.com/register",[458],"nofollow",[460],{"type":53,"value":461},"Create a free account",{"type":53,"value":463}," and create a bucket — that's your persistent URL.",{"type":40,"tag":116,"props":465,"children":466},{},[467],{"type":53,"value":468},"Point your provider (GitHub, GitLab, Bitbucket, Harness…) at the bucket URL.",{"type":40,"tag":116,"props":470,"children":471},{},[472,474,480],{"type":53,"value":473},"Run ",{"type":40,"tag":252,"props":475,"children":477},{"className":476},[],[478],{"type":53,"value":479},"relay forward --bucket \u003Cname> \u003Cyour-ci-endpoint>",{"type":53,"value":481}," next to your runner, Jenkins controller or delegate.",{"type":40,"tag":116,"props":483,"children":484},{},[485,487,491,493,497],{"type":53,"value":486},"Optionally add a ",{"type":40,"tag":77,"props":488,"children":489},{"href":208},[490],{"type":53,"value":272},{"type":53,"value":492}," or ",{"type":40,"tag":77,"props":494,"children":495},{"href":216},[496],{"type":53,"value":219},{"type":53,"value":498}," to route the same event to more than one target.",{"type":40,"tag":41,"props":500,"children":501},{},[502,504,510],{"type":53,"value":503},"Want to inspect what a provider is actually sending first? Open a free ",{"type":40,"tag":77,"props":505,"children":507},{"href":506},"/webhook-bin",[508],{"type":53,"value":509},"Webhook Bin",{"type":53,"value":511}," and watch the requests live before you wire anything up.",{"type":40,"tag":513,"props":514,"children":515},"style",{},[516],{"type":53,"value":517},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":7,"searchDepth":519,"depth":519,"links":520},3,[521,523,524,525,526,527,528,529],{"id":102,"depth":522,"text":105},2,{"id":155,"depth":522,"text":158},{"id":232,"depth":522,"text":235},{"id":15,"depth":522,"text":303},{"id":16,"depth":522,"text":392},{"id":400,"depth":522,"text":403},{"id":426,"depth":522,"text":429},{"id":443,"depth":522,"text":446},"markdown","content:blog:webhooks-in-ci-cd.md","content","blog/webhooks-in-ci-cd.md","blog/webhooks-in-ci-cd","md",{"loc":4},1784407349439]