jenkins:
[...]
basePlugins:
- name: job-dsl
version: "1.93"
- name: configuration-as-code
version: "2006.v001a_2ca_6b_574"
- name: workflow-job
version: "1559.va_a_533730b_ea_d"
- name: workflow-aggregator
version: "608.v67378e9d3db_1"
[...]
# plugins are plugins required by the user
plugins:
- name: generic-webhook-trigger
version: "2.4.1"
Radicle Garden CI with Jenkins
Wire Jenkins up to your Radicle Garden node so that every push and patch triggers a pipeline and the build result appears on the commit in your Radicle client.
Prerequisites
Before you start, ensure: - your Jenkins is reachable from your Garden node at its public URL, - you have the prerequisites already set up.
Step 1: Add the credential for the webhook shared secret
This shared secret is used to verify the authenticity of the webhook payload and ensures that your Jenkins only accepts requests to run builds from Radicle Garden. The secret protects against arbitrary requests from unauthorized sources.
From the web UI, go to: Manage Jenkins → Credentials → Add Credentials
-
Kind: Secret text
-
ID:
webhook-shared-secret(the references this exact ID) -
Secret: generate with
openssl rand -hex 32
Step 2: Plugins
-
Jenkins needs to have these plugins:
For example, this is the relevant sections for jenkins.basePlugins
and jenkins.plugins of the Jenkins Helm chart values:
With the webhooks trigger plugin installed, you’ll also need to
configure it to use the webhook-shared-secret credential that it
reads from the webhook x-radicle-signature header.
For example, here is the relevant Configuration As Code (CASC) config map:
apiVersion: v1
kind: ConfigMap
metadata:
name: jenkins-operator-casc
data:
1-webhooks-config.yaml: |
unclassified:
whitelist:
enabled: true
whitelistItems:
- hmacAlgorithm: "HmacSHA256"
hmacCredentialId: "webhook-shared-secret"
hmacEnabled: true
hmacHeader: "x-radicle-signature"
Step 3: Add some Jenkins jobs to your repo
Through the Job DSL plugin, you can tell Jenkins where to find the pipeline definition for your project. You can do that by creating 2 files:
-
the Job definition (which can also live inside your repo, for example:
.jenkins/jobs/test.groovy). You can use this example as a starting point, but remember to adapt for your environment — edit these lines:#!/usr/bin/env groovy folder('radicle-ci') { } pipelineJob('radicle-ci/jenkins-example') { description('Example pipeline') parameters { stringParam('repositoryId', '', 'Repository ID ($.repository.id)') stringParam('repositoryCloneUrl', '', 'Repository clone URL ($.repository.clone_url)') stringParam('repositoryName', '', 'Repository name ($.repository.name)') stringParam('repositoryHttpUrl', '', 'Repository HTTP URL ($.repository.http_url)') stringParam('nodeId', '', 'Radicle node ID ($.repository.seeder)') stringParam('webhookContext', '', 'Webhook context for commit status ($.context)') stringParam('commitStatusUrl', '', 'URL where commit status update should be posted back ($.commit_status_url)') stringParam('pushAfter', '', 'Commit SHA after push (PushPayload.after)') stringParam('pushBranch', '', 'Branch name (PushPayload.branch)') stringParam('patchId', '', 'Patch ID (PatchPayload.patch.id)') stringParam('patchAfter', '', 'Commit SHA from patch (PatchPayload.patch.after)') stringParam('patchTarget', '', 'Target branch of the patch (PatchPayload.patch.target)') } triggers { genericTrigger { genericVariables { genericVariable { key("repositoryId") value("\$.repository.id") expressionType("JSONPath") } genericVariable { key("repositoryCloneUrl") value("\$.repository.clone_url") expressionType("JSONPath") } genericVariable { key("repositoryName") value("\$.repository.name") expressionType("JSONPath") } genericVariable { key("repositoryHttpUrl") value("\$.repository.http_url") expressionType("JSONPath") } genericVariable { key("nodeId") value("\$.repository.seeder") expressionType("JSONPath") } genericVariable { key("webhookContext") value("\$.context") expressionType("JSONPath") } genericVariable { key("commitStatusUrl") value("\$.commit_status_url") expressionType("JSONPath") } genericVariable { key("pushAfter") value("\$.after") expressionType("JSONPath") } genericVariable { key("pushBranch") value("\$.branch") expressionType("JSONPath") } genericVariable { key("patchId") value("\$.patch.id") expressionType("JSONPath") } genericVariable { key("patchAfter") value("\$.patch.after") expressionType("JSONPath") } genericVariable { key("patchTarget") value("\$.patch.target") expressionType("JSONPath") } } genericHeaderVariables { genericHeaderVariable { key("x-radicle-event-type") regexpFilter("[^A-Za-z]") } } token('radicle-ci-jenkins-example') causeString('Triggered on repository $repositoryId (push: $pushAfter, patch: $patchId)') printContributedVariables(true) printPostContent(true) silentResponse(false) shouldNotFlatten(false) } } definition { cpsScm { scm { git { remote { // For a radicle.garden-seeded repo the clone URL has this shape: // https://index.radicle.garden/<rid-without-"rad:"-prefix>.git // e.g. rad:z2hCUNw2T1qU31LyGy7VPEiS7BkxW → // https://index.radicle.garden/z2hCUNw2T1qU31LyGy7VPEiS7BkxW.git url('https://index.radicle.garden/<your-rid>.git') } branches('*/main') } } scriptPath('.jenkins/pipelines/test.jenkinsfile') lightweight(false) } } }
| Line | Change to |
|---|---|
|
A random string unique to this
job. Must match the |
|
The clone URL
Jenkins should use to fetch the pipeline file at build time. For a
radicle.garden-seeded repo, it’s
|
-
the Pipeline definition (also inside your repo, e.g. in
.jenkins/pipelines/test.jenkinsfile). You can use this example as a starting point, but remember to adapt for your environment — edit these lines:#!/usr/bin/env groovy def commitSha = env.pushAfter ?: env.patchAfter def commitBranch = env.pushBranch ?: env.patchTarget def postCommitStatus(String state, String failureMessage = null) { if (!env.commitStatusUrl) { if (failureMessage) { echo failureMessage } return } def body = """{"node_id":"${env.nodeId}","repo_id":"${env.repositoryId}","sha":"${commitSha}","context":"${env.webhookContext}","state":"${state}","target_url":"${env.BUILD_URL ?: ''}"}""" echo "Reporting commit status: ${state}" echo "Body: ${body}" withCredentials([ string(credentialsId: 'webhook-shared-secret', variable: 'WEBHOOK_SECRET'), ]) { sh """ SIG=\$(printf '%s' '${body}' | openssl dgst -sha256 -hmac '${WEBHOOK_SECRET}' | cut -d' ' -f2) curl -sf -X POST '${env.commitStatusUrl}' \ -H 'Content-Type: application/json' \ -H "X-Garden-Signature-256: sha256=\${SIG}" \ -d '${body}' || echo "${failureMessage ?: 'Commit status POST failed (non-fatal)'}" """ } } pipeline { stages { stage('Checkout') { checkout scm } // It is important to have this first stage in your jenkins pipeline, so you can see the commit status updated // in Radicle. Technically speaking, it isn't part of the CI pipeline and this functionality (reporting // pipeline status)could later be moved to a dedicated Jenkins plugin. stage('Update status on Radicle') { steps { script { postCommitStatus('running', 'Running status POST failed (non-fatal)') } } } stage('Info') { steps { echo "Repository: ${env.repositoryName ?: 'N/A'}" echo "Branch: ${commitBranch ?: 'N/A'}" echo "Commit SHA: ${commitSha ?: 'N/A'}" echo "Context: ${env.webhookContext ?: 'N/A'}" echo "Commit Status URL: ${env.commitStatusUrl ?: 'N/A'}" } } stage('Lint') { steps { echo "Running lint checks..." sh 'sleep 2' echo "Lint passed. Ha! 8)" } } stage('Test') { steps { echo "Running tests..." sh 'sleep 3' echo "Tests passed. \(^ ^)/" } } } // this ensures the status is always updated on Radicle after the job has run. post { always { script { def state = currentBuild.currentResult == 'SUCCESS' ? 'success' : 'failure' postCommitStatus(state, 'Commit status POST failed (non-fatal)') } } } }
| Line | Change to |
|---|---|
|
The clone URL
Jenkins should use to fetch the pipeline file at build time. For a
radicle.garden-seeded repo, it’s
|
Commit and push.
Step 4: Create the Jenkins job
You can, of course, create the Jenkins job manually, through the web UI, but since we’re using the Job DSL plugin, it’s better to have that in code as well!
The Configuration As Code (CASC) plugin allows you to tell Jenkins where to pick up your Job definition like so:
jenkins:
[...]
seedJobs:
- id: your-project-name
targets: ".jenkins/jobs/*.jenkinsfile"
description: ""
repositoryBranch: main
repositoryUrl: https://index.radicle.garden/<your-rid>.git
You may need to restart Jenkins after adding this. But once you see the seed job in the Jenkins UI, and the seed job has ran, you should see your pipeline too. If you can see both of these, it’s finally time to wire the two up together!
Step 5: Register the webhook in Radicle
From the repo working copy:
rad webhooks add \
--name jenkins \
--url 'https://<your-jenkins>/generic-webhook-trigger/invoke?token=<your-token>' \
--secret '<the-same-secret-you-put-in-Jenkins>' \
--nid z6Mk... # your Garden node's NID
git add .radicle/webhooks/jenkins.yaml
git commit -m 'Enable Jenkins webhook'
git push rad
| Flag | Note |
|---|---|
|
Becomes the filename and the |
|
Must be reachable from your Garden node. Use the token from Step 3 above. |
|
Copy into your CI system’s credential store (the one you set in Step 1). |
|
Your Garden node’s NID. |
Step 6: Push a commit and watch it flow
git commit --allow-empty -m 'First Jenkins trigger'
git push rad
Within a few seconds:
-
Garden node POSTs the event to Jenkins.
-
Generic Webhook Trigger starts a build.
-
Pipeline POSTs
state: "running"tocommit_status_url. Opening the repo in radicle-desktop at this point shows the commit status as “running”. -
Infostage prints the event metadata. -
Lint+Teststages run (placeholders — replace with your own). -
post { always }POSTsstate: "success"or"failure". -
Garden updates the commit status. radicle-desktop now shows “success” or “failure”.
Support
If you’re having trouble, reach out to us for Support.