I have been working on Weldr for some time now. It is a multi-component software with several layers built on top of each other as seen on the image below.

One of the risks that we face is introducing changes in downstream components which are going to break something up the stack! In this post I am going to show you how I have configured Jenkins to trigger dependent rebuilds and report all of the statuses back to the original GitHub PR. All of the code below is Jenkins Job Builder yaml.
bdcs is the first layer of our software stack. It provides command line
utilities. codec-rpm is a library component that facilitates working
with RPM packages (in Haskell). bdcs links to codec-rpm when it is compiled,
bdcs uses some functions and data types from codec-rpm.
When a pull request is opened against codec-rpm and testing completes successfully
I want to reuse that particular version of the codec-rpm library and
rebuild/test bdcs with that.
YAML configuration
All jobs have the following structure: -trigger -> -provision -> -runtest -> -teardown. This means that Jenkins will start executing a new job when it gets triggered by an event in GitHub (commit to master branch or new pull request), then it will provision a slave VM in OpenStack, execute the test suite on the slave and destroy all of the resources at the end. This is repeated twice: for master branch and for pull requests! Here's how the -runtest jobs look:
- job-template:
name: '{name}-provision'
node: master
parameters:
- string:
name: PROVIDER
scm:
- git:
url: 'https://github.com/weldr/{repo_name}.git'
refspec: ${{git_refspec}}
branches:
- ${{git_branch}}
builders:
- github-notifier
- shell: |
#!/bin/bash -ex
# do the openstack provisioning here
# NB: runtest_job is passed to us via the -trigger job
- trigger-builds:
- project: '${{runtest_job}}'
block: true
current-parameters: true
condition: 'SUCCESS'
fail-on-missing: true
- job-template:
name: '{name}-master-runtest'
node: cinch-slave
project-type: freestyle
description: 'Build master branch of {name}!'
scm:
- git:
url: 'https://github.com/weldr/{repo_name}.git'
branches:
- master
builders:
- github-notifier
- conditional-step:
condition-kind: regex-match
regex: "^.+$"
label: '${{UPSTREAM_BUILD}}'
on-evaluation-failure: dont-run
steps:
- copyartifact:
project: ${{UPSTREAM_BUILD}}
which-build: specific-build
build-number: ${{UPSTREAM_BUILD_NUMBER}}
filter: ${{UPSTREAM_ARTIFACT}}
flatten: true
- shell: |
#!/bin/bash -ex
make ci
publishers:
- trigger-parameterized-builds:
- project: '{name}-teardown'
current-parameters: true
- github-notifier
- job-template:
name: '{name}-PR-runtest'
node: cinch-slave
description: 'Build PRs for {name}!'
scm:
- git:
url: 'https://github.com/weldr/{repo_name}.git'
refspec: +refs/pull/*:refs/remotes/origin/pr/*
branches:
# builds the commit hash instead of a branch
- ${{ghprbActualCommit}}
builders:
- github-notifier
- shell: |
#!/bin/bash -ex
make ci
- conditional-step:
condition-kind: current-status
condition-worst: SUCCESS
condition-best: SUCCESS
on-evaluation-failure: dont-run
steps:
- shell: |
#!/bin/bash -ex
make after_success
publishers:
- archive:
artifacts: '{artifacts_path}'
allow-empty: '{artifacts_empty}'
- conditional-publisher:
- condition-kind: '{execute_dependent_job}'
on-evaluation-failure: dont-run
action:
- trigger-parameterized-builds:
- project: '{dependent_job}'
current-parameters: true
predefined-parameters: |
UPSTREAM_ARTIFACT={artifacts_path}
UPSTREAM_BUILD=${{JOB_NAME}}
UPSTREAM_BUILD_NUMBER=${{build_number}}
condition: 'SUCCESS'
- trigger-parameterized-builds:
- project: '{name}-teardown'
current-parameters: true
- github-notifier
- job-group:
name: '{name}-tests'
jobs:
- '{name}-provision'
- '{name}-teardown'
- '{name}-master-trigger'
- '{name}-master-runtest'
- '{name}-PR-trigger'
- '{name}-PR-runtest'
- job:
name: 'codec-rpm-rebuild-bdcs'
node: master
project-type: freestyle
description: 'Rebuild bdcs after codec-rpm PR!'
scm:
- git:
url: 'https://github.com/weldr/codec-rpm.git'
refspec: +refs/pull/*:refs/remotes/origin/pr/*
branches:
# builds the commit hash instead of a branch
- ${ghprbActualCommit}
builders:
- github-notifier
- trigger-builds:
- project: 'bdcs-master-trigger'
block: true
predefined-parameters: |
UPSTREAM_ARTIFACT=${UPSTREAM_ARTIFACT}
UPSTREAM_BUILD=${UPSTREAM_BUILD}
UPSTREAM_BUILD_NUMBER=${UPSTREAM_BUILD_NUMBER}
publishers:
- github-notifier
- project:
name: codec-rpm
dependent_job: '{name}-rebuild-bdcs'
execute_dependent_job: always
artifacts_path: 'dist/{name}-latest.tar.gz'
artifacts_empty: false
jobs:
- '{name}-tests'
Publishing artifacts
make after_success is responsible for creating a tarball if codec-rpm test suite
passed. This tarball gets uploaded as artifact into Jenkins and we can make use of it later!
Inside -master-runtest I have a conditional-step inside the builders section which
will copy the artifacts from the previous build if they are present. Notice that I copy
artifacts for a particular job number, which is the job for codec-rpm PR.
Making use of local artifacts is handled inside bdcs' make ci because it is
per-project specific and because I'd like to reuse my YAML templates.
Reporting statuses to GitHub
For github-notifier to be able to report statuses back to the pull request
the job needs to be configured with the git repository this pull request came from.
This is done by specifying the same scm section for all jobs that are related and
current-parameters: true to pass the revision information to the other jobs.
This also means that if I want to report status from codec-rpm-rebuild-bdcs then
it needs to be configured for the codec-rpm repository (see yaml) but somehow
it should trigger jobs for another repository!
When jobs are started via trigger-parameterized-builds their statuses are reported
separately to GitHub. When they are started via trigger-builds there should be only
one status reported.
Trigger chain for dependency rebuilds
With all of the above info we can now look at the codec-rpm-rebuild-bdcs job.
- It is configured for the codec-rpm repository so it will report its status to the PR
- It is conditionally started after
codec-rpm-PR-runtestfinishes successfully - It triggers
bdcs-master-triggerwhich in turn will rebuild & retest the bdcs component. Additional parameters specify whether we're going to use locally built artifacts or attempt to download then from Hackage - It uses
block: trueso that the status ofcodec-rpm-rebuild-bdcsis dependent on the status ofbdcs-master-runtest(everything in the job chain usesblock: truebecause of this)
How this looks like in practice
I have opened codec-rpm #39 to validate my configuration. The chain of jobs that gets executed in Jenkins is:
--- console.log for bdcs-master-runtest ---
Started by upstream project "bdcs-jslave-1-provision" build number 267
originally caused by:
Started by upstream project "bdcs-master-trigger" build number 133
originally caused by:
Started by upstream project "codec-rpm-rebuild-bdcs" build number 25
originally caused by:
Started by upstream project "codec-rpm-PR-runtest" build number 77
originally caused by:
Started by upstream project "codec-rpm-jslave-1-provision" build number 178
originally caused by:
Started by upstream project "codec-rpm-PR-trigger" build number 118
originally caused by:
GitHub pull request #39 of commit b00c923065e367afd5b7a7cc068b049bb1ed25e1, no merge conflicts.
Statuses are reported on GitHub as follows:

default is coming from the provisioning step and I think this is some sort of a bug
or misconfiguration of the provisioning job. We don't really care about this.
On the picture you can see that codec-rpm-PR-runtest was successful but
codec-rpm-rebuild-bdcs was not. The actual error when compiling bdcs is:
src/BDCS/Import/RPM.hs:110:24: error:
* Couldn't match type `Entry' with `C8.ByteString'
Expected type: conduit-1.2.13.1:Data.Conduit.Internal.Conduit.ConduitM
C8.ByteString
Data.Void.Void
Data.ContentStore.CsMonad
([T.Text], [Maybe ObjectDigest])
Actual type: conduit-1.2.13.1:Data.Conduit.Internal.Conduit.ConduitM
Entry
Data.Void.Void
Data.ContentStore.CsMonad
([T.Text], [Maybe ObjectDigest])
* In the second argument of `(.|)', namely
`getZipConduit
((,) <$> ZipConduit filenames <*> ZipConduit digests)'
In the second argument of `($)', namely
`src
.|
getZipConduit
((,) <$> ZipConduit filenames <*> ZipConduit digests)'
In the second argument of `($)', namely
`runConduit
$ src
.|
getZipConduit
((,) <$> ZipConduit filenames <*> ZipConduit digests)'
|
110 | .| getZipConduit ((,) <$> ZipConduit filenames
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^...
That is because PR #39 changes the return type of Codec.RPM.Conduit::payloadContentsC
from Entry to C8.ByteString.
Thanks for reading and happy testing!
social image CC by https://pxhere.com/en/photo/226978
Comments !