Tightly Coupled CI/CD: Integrating OS Builds with Labgrid Testing in Jenkins

Contact Us

This article outlines a robust Jenkins pipeline architecture designed for operating system (OS) development, where an Upstream Pipeline is responsible for building the OS image and a Downstream Pipeline, typically running functional validation (like Labgrid tests), securely retrieves the exact artifact generated by the preceding build.

The Upstream Build Pipeline (The Producer)

The upstream pipeline’s primary role is to compile the OS source, create the final deployable image artifact (e.g., a .mender or .img file), archive it, and then trigger the test suite.

The critical step here is passing the current build’s unique identifier (currentBuild.number) to the downstream job.

#!groovy

import com.amarula.build.Build
import com.amarula.changelog.Changelog
import com.amarula.git.Git
import com.amarula.ui.Ui
import org.jenkinsci.plugins.pipeline.modeldefinition.Utils

...

throttle(['heavy_job']) { node('acme-node') {
  def credentials = ['c4f9fa8c-45d7-459e-a645-203f1245d914', '9af8a985-9516-467e-b9cb-0174692fe8c0']
  def manifestUrl = "${GITEA_SSH_URL}/acme/meta-acme.git"

  /* Set job description */
  currentBuild.displayName = 'acme_' + (env.GERRIT_TOPIC ? "${env.GERRIT_TOPIC}_" : '') + env.BUILD_NUMBER

  def ui = new Ui.Builder(this)
    .addStringParameter("BRANCH", 'master', "The branch to start from in order to build. Tag are valid too")
    .addStringParameter("GERRIT_TOPIC", '', "Gerrit Topic to cherry-pick, separated by ,")
    .addBooleanParameter("CLEAN_STATE_CACHE", false, "Force of the clean of state cache.")
    .addBooleanParameter("KEEP_BUILD", false, "If you want to keep build select it. It's used ONLY for production")
    .build()

  def buildCode = [
    'Clean state cache' : {
      if (CLEAN_STATE_CACHE.toBoolean() == true) {
        runYoctoTargetBuild("-fc cleansstate acmebsp-image-weston", "kas/bunny.yaml")
      } else {
        Utils.markStageSkippedForConditional(env.STAGE_NAME)
      }
    },
    'Yocto image Bunny' : {
      String mendPath = "build/tmp-glibc/log/mend"
      /* Clean up old report if any */
      dir (mendPath) {
        deleteDir()
      }
      runYoctoTargetBuild("acmebsp-image-weston", "kas/bunny.yaml:kas/security.yaml", KEEP_BUILD.toBoolean())
      recordIssues sourceCodeRetention: 'LAST_BUILD',
                   tools: [yoctoScanner(pattern: "build/tmp-glibc/log/cve/cve-summary.json")]
      if (KEEP_BUILD.toBoolean()) {
        archiveArtifacts "build/tmp-glibc/log/mend/mend-report-*.json"
        archiveArtifacts "build/tmp-glibc/log/mend/mend-report-*.zip"
      }
    },
    'Yocto sdk' : {
      runYoctoTargetBuild("-fc populate_sdk acmebsp-image-weston", "kas/bunny.yaml")
    },
    'Build Documentation' : {
      runContainerCmd("make BUILDDIR=/work/build/_build -C /work/acme-doc/ latexpdf", "kas/bunny.yaml")
    },
    'Release notes' : {
      def changelogString
      dir('meta-acme') {
        def status = sh(returnStatus: true, script: 'git describe --tags')
        def fromRef
        if (status != 0) {
          status = ""
          fromRef = """git rev-list --max-parents=0 HEAD"""
        } else {
          status = "_" + sh(returnStdout: true, script: 'git describe --tags').trim();
          fromRef = """git describe --abbrev=0 --always --tags HEAD^ --match='v[0-9].[0-9].[0-9]'"""
        }
        def options = [from: fromRef, to: "HEAD"]
        changelogString = new Changelog().generate(this, options)
        currentBuild.description = changelogString
      }
      writeFile(file: 'build/version-release-notes.html', text: changelogString)
    },
    'Archive artifacts' : {
      def out = 'build/tmp-glibc/deploy'
      archiveArtifacts "${out}/sdk/acmebsp-*.sh"
      archiveArtifacts "${out}/images/bunny-3588/acmebsp-image*-bunny-3588-*.*.*.wic.gz"
      archiveArtifacts "${out}/images/bunny-3588/acmebsp-image*-bunny-3588-*.*.*.update.img"
      archiveArtifacts "${out}/images/bunny-3588/acmebsp-image*-bunny-3588-*.*.*.mender"

      /* Save documentation */
      archiveArtifacts 'build/_build/latex/acme-bsp.pdf'
      archiveArtifacts 'build/version-release-notes.html'
    },
    'Labgrid Testing' : {
      build(job: 'labgrid-test-acme',
            parameters: [ string(name: 'ACMEBSP_BUILD_NUMBER', value: "${currentBuild.number}") ],
            wait: true)
    }
  ]

  try {
    def ver = new Build(this, env, credentials)
    def options = ['branch': BRANCH, history: true, 'gerritProject': 'acme/meta-acme']
    ver.setSyncMethod(Build.CHERRYPICK)
    ver.build(manifestUrl, buildCode, options)

    if (KEEP_BUILD.toBoolean()) {
      currentBuild.setKeepLog(true)
    }
  } finally {
    /* Clean up workspace */
    dir ("${WORKSPACE}/meta-acme/build/tmp-glibc") {
      deleteDir()
    }
  }
}}

The Labgrid Testing step will trigger the testing pipeline, wait for it and propagate the error to this pipeline

../../_images/jenkins-upstream-build.png

The Downstream Testing Pipeline (The Consumer)

The downstream pipeline, designated for the Labgrid testing environment, must first receive the parameter and then use the powerful copyArtifacts step to retrieve the image.

The key to enforcing the “exact build” requirement is the selector: specific() mechanism.

pipeline {
    agent {
        node {
            label 'maratona'
        }
    }

    options {
        skipDefaultCheckout(true)
    }

    parameters {
        string(name: 'ACMEBSP_BUILD_NUMBER', defaultValue: '',
               description: 'The specific build number of the producer job to retrieve artifacts from.')
    }

    environment {
        unitTestDirectory = 'output/acme'
        labgridArgs = '--lg-env local.yaml --lg-log --lg-colored-steps'
        reportPrefix = '../output/acme/results-'
        labgridPlace = 'bunny'
        labgridCoordinatorHost = '10.105.6.4'
        isJenkinsCI = '1'
    }

    stages {
        stage('Clean workspace')
        {
            steps {
                cleanWs()
                checkout scm
            }
        }

        stage('Verify Parameter') {
            steps {
                echo "Consumer build #${currentBuild.number} received upstream build number: ${params.ACMEBSP_BUILD_NUMBER}"
                script {
                    if (!params.ACMEBSP_BUILD_NUMBER) {
                        error 'The ACMEBSP_BUILD_NUMBER parameter is missing. Cannot copy artifacts.'
                    }
                }
            }
        }

        stage('Copy Artifact') {
            steps {
                copyArtifacts(
                    projectName: 'amarula/acme/acmebsp-yocto-bsp',
                    filter: 'build/tmp-glibc/deploy/images/bunny-3588/*.mender, build/tmp-glibc/deploy/images/bunny-3588/*.update.img',
                    selector: specific(params.ACMEBSP_BUILD_NUMBER),
                    target: '.'
                )
            }
        }

        stage('Acquire resources')
        {
            steps {
                sh """
                   . /opt/labgrid/labgrid-venv/bin/activate
                   labgrid-client -p "${labgridPlace}" acquire
                """
            }
        }
        stage('Shell Basic tests') {
            steps {
                dir('acme') {
                    sh """
                        . /opt/labgrid/labgrid-venv/bin/activate
                        pytest --junitxml "${reportPrefix}basic.xml" ${labgridArgs} tests/test_shell.py
                    """
                }
            }
        }
    }

    post {
        always {
            junit allowEmptyResults: true, keepProperties: true, testResults: "${unitTestDirectory}/*.xml"
            sh """
               . /opt/labgrid/labgrid-venv/bin/activate
               labgrid-client -p "${labgridPlace}" release
            """
        }
    }
}
../../_images/jenkins-downstream-testing.png

Benefits of Specific Artifact Copying

Using the build step with a specific artifact selector offers several crucial benefits for integrated CI/CD:

  • Immutability and Traceability: Every test run is permanently linked to the exact source code and build process that generated the OS image. If a test fails, you know exactly which artifact caused it.

  • Concurrency Safety: If multiple upstream builds run concurrently, the downstream pipelines triggered by them will never mistakenly pick up an artifact from a sibling build.

  • Reproducibility: You can re-run the downstream testing pipeline at any time by manually providing a specific, successful ACMEBSP_BUILD_NUMBER, guaranteeing the exact same test setup.