/
Help
CI & Webhooks
Jenkins
Jenkins

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

  1. 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:

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"

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:

  1. 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

token('radicle-ci-jenkins-example')

A random string unique to this job. Must match the token= query param in the webhook URL you register with rad webhooks add.

url('https://index.radicle.garden/<your-rid>.git')

The clone URL Jenkins should use to fetch the pipeline file at build time. For a radicle.garden-seeded repo, it’s https://index.radicle.garden/<rid-without-"rad:"-prefix>.git.

  1. 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

url('https://index.radicle.garden/<your-rid>.git')

The clone URL Jenkins should use to fetch the pipeline file at build time. For a radicle.garden-seeded repo, it’s https://index.radicle.garden/<rid-without-"rad:"-prefix>.git.

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

--name

Becomes the filename and the context on commit status (jenkins, woodpecker, …). If you have multiple jobs on Jenkins, use a distinct name per job.

--url

Must be reachable from your Garden node. Use the token from Step 3 above.

--secret

Copy into your CI system’s credential store (the one you set in Step 1).

--nid

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:

  1. Garden node POSTs the event to Jenkins.

  2. Generic Webhook Trigger starts a build.

  3. Pipeline POSTs state: "running" to commit_status_url. Opening the repo in radicle-desktop at this point shows the commit status as “running”.

  4. Info stage prints the event metadata.

  5. Lint + Test stages run (placeholders — replace with your own).

  6. post { always } POSTs state: "success" or "failure".

  7. Garden updates the commit status. radicle-desktop now shows “success” or “failure”.

Support

If you’re having trouble, reach out to us for Support.

Help