58

TL;DR Is there a way to import code into the Jenkinsfile from the local repository (other than the load step)?

Why?

I've experienced that for complex builds the Jenkinsfile gets kind of bulky and not very maintainable.
Now that the build job is code, it would be wonderful to have the same means as for other code. That is, I would like to divide it into smaller (more maintainable) units and unit test them.

What I tried

  • shared libraries: allow for dividing our Jenkins Pipeline logic into smaller files in a separate module and even unit test it.
    However, they need to be in different repository and (if not on GitHub) must be configured into Jenkins.
  • load Step: Allow for loading groovy scripts from the repository.
    However, the files must be scripts and not "full" groovy classes, making it difficult to have multiple files or classes that depend on each other. For example inheritance is impossible.
    In addition, theses files are not displayed when doing a replay on a Jenkins job, which makes them hard to develop and debug.

My Questions

  • Is there a way (or workaround) to create a shared library in the same repository as the Jenkinsfile and import this library into the Jenkinsfile?
  • Or is there even another way I haven't tried, yet?

Example directory structure

Similar to the directory structure described for shared libs I would like to have the following in a single repository.

(root)
+- someModule
|   +- ...
+- jenkins           # Classes/Scripts used by Jenkins in a separate module
|   +- src                       # Groovy source files
|      +- org
|          +- foo
|              +- Bar.groovy     # for org.foo.Bar class
|   +- test                      # Groovy test files
|      +- org
|          +- foo
|              +- BarTest.groovy # Test for org.foo.Bar class
|   +- pom.xml or build.groovy   # Build for local library
+- Jenkinsfile     # Build "someModule", uses classes from "jenkins" module
schnatterer
  • 7,525
  • 7
  • 61
  • 80
  • 1
    "shared libraries... need to be in different repository and (if not on GitHub) must be configured into Jenkins." Why is this a problem? Shared libraries are the standard way to reuse code across pipelines, you can have pure groovy code, test it with Spock, it works well for me. – Boris Lopez Jan 26 '18 at 11:52
  • 2
    @Boris My use case is actually not to share it between projects, but to divide one `Jenkinfile` into smaller (more maintainable) units and unit test them *within one project*, i.e. "next" to the `Jenkinsfile`. – schnatterer Jan 26 '18 at 15:15
  • 1
    If you want you can have your Jenkinsfile and your shared library in the same repo and just import it. I have done this, you still need to configure the shared library in Jenkins (I don't see why that is a problem either). – Boris Lopez Jan 26 '18 at 19:27
  • 8
    @Boris Configuring the shared library is just one more configuration step that should not be necessary. Not everybody has administrative rights on his Jenkins instances. See also [icoanel's answer](https://stackoverflow.com/a/46220886/1845976) – schnatterer Jan 27 '18 at 10:27
  • 2
    Other case for this is when you want to test this features without _polluting_ common and shared repository. – lucasvc Feb 07 '20 at 10:17

9 Answers9

13

Workaround:

library identifier: 'shared-library@version', retriever: legacySCM(scm)

The approach currently taken in PR 37 will not work properly with build agents, and anyway will only work for scripts using the library step, not the @Library annotation.

By the way files loaded from the load step do appear in Replay. But it is true that your script cannot statically refer to types defined in such files. In other words, you could simulate library vars/*.groovy but not src/**/*.groovy—the same limitation as the current PR 37.

schnatterer
  • 7,525
  • 7
  • 61
  • 80
Jesse Glick
  • 24,539
  • 10
  • 90
  • 112
  • I can confirm this works for shared library repos (that are not within a sub directory). Note that we have to pass a library version as part of the `identifier`. Example: https://github.com/cloudogu/ces-build-lib/blob/b1f01a1ec4ec1dc581870d4b8d71b3e3d8ba87af/Jenkinsfile#L71 Any ideas how to use a sub directory of the current repo, e.g. `jenkins` in the folder structure shown in the question? – schnatterer Mar 06 '18 at 19:44
  • Subdirectories are tracked in [JENKINS-46721](https://issues.jenkins-ci.org/browse/JENKINS-46721). – Jesse Glick Mar 12 '18 at 13:27
  • Also FYI: [`workflow-cps-global-lib` PR 43](https://github.com/jenkinsci/workflow-cps-global-lib-plugin/pull/43) – Jesse Glick Mar 12 '18 at 14:24
  • 1
    Any ideas on how to do this if the library is normally loaded implicitly? Loading with the same name doesn't work because the library [can only be loaded once](https://github.com/jenkinsci/workflow-cps-global-lib-plugin/blob/aaa7ed1e04ce2ef751b2a770e71f0286c509ddc6/src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryAdder.java#L114), and loading it with a different name doesn't [work either](https://github.com/jenkinsci/workflow-cps-global-lib-plugin/blob/aaa7ed1e04ce2ef751b2a770e71f0286c509ddc6/src/main/java/org/jenkinsci/plugins/workflow/libs/LibraryStep.java#L291). – Deiwin Apr 20 '18 at 15:36
  • Perhaps there's a way to disable loading implicit libraries for a particular job. Or to disable loading a particular implicitly loaded library for a particular job. – Deiwin Apr 20 '18 at 15:57
  • If your repo requires credentials, you may need to use `retriever: modernSCM` with the `credentialsId` argument instead. See https://jenkins.io/doc/book/pipeline/shared-libraries/#dynamic-retrieval – cowlinator Apr 03 '20 at 21:21
  • @cowlinator no, for purposes of this question you must use `legacySCM(scm)` as typed above. The `scm` object includes repository location, credentials, commit identifier, etc., just as it would have been from a `checkout scm` step. – Jesse Glick Dec 17 '21 at 21:46
9

I guess that proper way to do that is to implement a custom SCMRetriever.

However, you can use the following hack:

Assuming jenkins/vars/log.groovy in your local repo contains:

def info(message) {
    echo "INFO: ${message}"
}

Your Jenkinsfile can load that shared library from the jenkins/ directory using library step:

node('node1') { // load library
    checkout scm
    // create new git repo inside jenkins subdirectory
    sh('cd jenkins && git init && git add --all . && git commit -m init &> /dev/null') 
    def repoPath = sh(returnStdout: true, script: 'pwd').trim() + "/jenkins"
    library identifier: 'local-lib@master', retriever: modernSCM([$class: 'GitSCMSource', remote: repoPath])
}

node('node2') {
    stage('Build') {
        log.info("called shared lib") // use the loaded library
    }
}
mkobit
  • 43,979
  • 12
  • 156
  • 150
Pawel Wiejacha
  • 704
  • 7
  • 5
  • Hi @Pawel, thanks! This looks promissing. However, I get an error saying: `stderr: fatal: '/home/jenkins/workspace/.../localLibrary' does not appear to be a git repository fatal: Could not read from remote repository`. Any ideas? – schnatterer Dec 22 '17 at 17:03
  • 1
    @schnatterer, this command is hiding the errors. Remove `&>/dev/null` so you can debug the issue that is preventing it from working: `sh('cd jenkins && git init && git add --all . && git commit -m init &> /dev/null')`. I suspect you did not change the path to the subdir in the `sh` command so the git repo was never initialized – Brandon Oct 02 '18 at 19:45
  • 1
    @Brandon thanks. Your hint helped me fixing the error in the `sh` step. I needed to `git config user.email`. Unfortunately, the build fails in the `library` step with the same error as in my first comment ( `does not appear to be a git repository`). Here's what I tried: https://github.com/schnatterer/jenkins-load-local-lib/blob/94cf097e3f8d1485ed1cb884da01c4033c80e554/Jenkinsfile#L80 – schnatterer Oct 26 '18 at 11:02
  • 2
    Your answer inspired me to write a blog that explains this solution more in-depth as well as covering some pitfalls. https://www.code-held.com/2020/01/22/jenkins-local-shared-library/ I also abstracted the solution into a shared library for others to use: https://github.com/mld-ger/jenkins-library-subfolder-loader – Marcus Held Jan 22 '20 at 09:39
  • @schnatterer. did you figure out what the issue was? I have the same issue `hudson.plugins.git.GitException: Command "git ls-remote -- /home/jenkins/workspace/.../library" returned status code 128: stdout: stderr: fatal: '/home/jenkins/workspace/.../library' does not appear to be a git repository fatal: Could not read from remote repository.` – MarMan Dec 13 '21 at 16:04
  • @MarMan I never managed to continue working on this topic – schnatterer Dec 13 '21 at 22:18
  • I was expecting that loading a library in such a way would solve the `Scripts not permitted to use method java.xyz. Administrators can decide whether to approve or reject this signature.` issue, same as when loading an external library. Is there any way to workaround this without having to approve from the methods from the Jenkins admin console? – MarMan Apr 26 '22 at 10:49
  • The reason you get `does not appear to be a git repository` might be separation of controller and agent machines. That was the case for me at least. The repo is cloned to jenkins agent, but the library is loaded from filesystem of controller – Vladislav Ivanov Jun 22 '22 at 11:21
5

You may take a look at plugin I wrote, that enables using subdirectories of repo where your pipeline is as shared libraries: https://github.com/karolgil/SharedLibrary

After building and installing it, you can simply put following in your pipeline:

@SharedLibrary('dir/in/repo') _

To start using dir/in/repo as shared library for your pipelines.

Karol Gil
  • 95
  • 3
  • 8
2

Wanted to do the same and ended up creating this:

https://github.com/jenkinsci/workflow-cps-global-lib-plugin/pull/37

and here is how I use it:

https://github.com/syndesisio/syndesis-pipeline-library/blob/master/Jenkinsfile#L3

In my case I wanted to create a Jenkinsfile that actually tests the pipeline library that the repository contains.

Let me know what you think and feel free to add your comments on the PR too.

iocanel
  • 570
  • 2
  • 2
  • Thanks. Looks like exactly what I am looking for! If only they hard already merged and released it :-) I added my use case to the PR, maybe it can help boosting it. – schnatterer Sep 14 '17 at 15:15
  • Regarding testing a shared library, there are other ways to do this that don't require pushing up to a running Jenkins. I think this feature is good for other use cases as well. – mkobit Jan 26 '18 at 16:29
0

I found the version from Pawel has problems if you are checking out the pipeline from SCM (not embedded in Jenkins job UI config). This is my version for those cases:

node {
    def scriptFolder = sh(script: "echo \$(pwd | sed 's,\\(.*\\)\\(@[0-9]*\$\\),\\1,')@script/\$(sed -n '2,\$p' ../config.xml | xmllint --xpath '(//scriptPath/text())' - | xargs dirname)", returnStdout: true).trim()
    sh("cd ${scriptFolder} && ls -l vars/ && if [ -d .git ]; then rm -rf .git; fi; git init && git add --all . && git commit -m init &> /dev/null")
    library identifier: 'local-lib@master', retriever: modernSCM([$class: 'GitSCMSource', remote: scriptFolder])

    stage('Build') {
        log.info("called shared lib") // use the loaded library
    }
}

For these cases, the pipeline itself is checkout out in a different workspace (same directory name but with @script in the name) than what the pipeline itself defines.

lqbweb
  • 1,684
  • 3
  • 19
  • 33
0

I've successfully tested a simple workaround for GIT repos having both protocols - HTTPS and SSH (I'm using BitBucket) - just configure the Jenkins job as follows (pointing to the same repository but forcing different fetch methods):

  • In the Branch Sources add "Checkout over SSH" option

  • In the Pipeline Libraries -> Source Code Management -> Project Repository use HTTPS protocol - e.g. something like https://...

0

Is there a way to import code into the Jenkinsfile from the local repository (other than the load step)?

Is there a way (or workaround) to create a shared library in the same repository as the Jenkinsfile and import this library into the Jenkinsfile?

Yes. Provided that the "directory structure of a Shared Library repository" is observed according to specification, then it's absolutely feasible and no workaround is required to get it working. Basically, your directory structure would require an adjustment along the following lines:

+- src                     # Groovy source files
|   +- org
|       +- foo
|           +- Bar.groovy  # for org.foo.Bar class
+- vars
|   +- foo.groovy          # for global 'foo' variable
|   +- foo.txt             # help for 'foo' variable
+- resources               # resource files (external libraries only)
|   +- org
|       +- foo
|           +- bar.json    # static helper data for org.foo.Bar
+- someModule
|   +- ...
|
|
+- Jenkinsfile

This answer is not based on a conjecture. Although it's not documented, I have applied this pattern on multiple projects and training, and it works. You do not need to tinker with your job configuration in anyway than is usual.

Igwe Kalu
  • 14,286
  • 2
  • 29
  • 39
  • What I was observing was: 1) If the Jenkinfile was written in 'declarative pipeline' syntax, then this would work, as long as the call to the foo.echo() method was encased within "script { ... }". 2) If the Jenkinsfile was written in 'scripted pipeline' syntax, then I'd get a "No such property: foo" error Gives me the impression that the mechanism you've made use of in this case only works for the former, not the latter. Alas, the codebase I'm working with is stuck with scripted-pipeline syntax for now. – Gurce Mar 31 '20 at 13:16
  • I finally gave this a try, which resulted in `unable to resolve class com.cloudogu.ces.cesbuildlib.Git` for https://github.com/cloudogu/ces-build-lib. Do I have to load the "library" in the Jenkinsfile? – schnatterer Apr 09 '20 at 07:13
  • That depends on whether you selected the 'load implicitly' option or not during setup of the shared lib. Ordinary, it's a good approach to load libs _explicitly_: `@Library( 'lib-name-as-defined-during-setup' ) _`. – Igwe Kalu Apr 09 '20 at 23:45
0

My shared library pipeline can test itself. It's a bit clunky but it works.

All I do is import the library itself into the Jenkinsfile in the library's repo. The actual pipeline is defined in /vars as myPipeline.groovy - it's in Declarative syntax, uses the Delegate pattern provided by Jenkins to ingest parameters, and every effort is made to put all actual code into library steps, leaving the pipeline DSL as clean as possible.

// repo's Jenkinsfile
library 'my-shared-library'

myPipeline {
  myParam1 = 'value1'
  myParam2 = 'value2'
}

It's important to remember to pull the branch you're working on when you're testing something new:

// repo's Jenkinsfile
library 'my-shared-library@my-new-branch`
...

You can write actual unit tests into the /test/com/myOrg/ dir; I use the plain JenkinsPipelineUnit framework, it gets the job done, but it's clunky. I landed on using Gradle to perform the build and tests. It's slow, but gets the job done.

Or, you can test your /vars steps in the actual Jenkinsfile in an ad-hoc pipeline. This is sometimes easier than dealing with mocking everything out in units, especially if you end up mocking what you want to be testing, which defeats the purpose.

load() should definitely be providing the loaded scripts for editing in Replay. If not, something else is going on.

Lastly, I highly recommend setting up a Jenkins instance on your dev machine, so you can test changes without pushing them. Especially if you create an ad-hoc pipe to test your steps, you can then edit the pipeline in Replay, creating the shortest feedback loop for yourself. To get even faster, get familiar with testing groovy in the Script Console, or using Gradle run to actually run more complex code from the command line. Anything to avoid waiting for code to push and pipelines to run will make your development go faster.

note of warning on the script console, readJson/readYaml don't work right, and I'm sure others too. When i need to test stuff like that, i tend to use ad-hoc pipes to quickly iterate on the code.

Max Cascone
  • 648
  • 9
  • 25
0

Wrote a small plugin(a custom retriever) to solve this issue. Need to paste the library files inside jenkins agent file system and mention the library path while loading the library in pipeline script like below.

For testing create logger.groovy under C:\test-library\vars\ directory with below content

#!/groovy
def info(message) {
    echo message
}

Jenkins pipeline script:

pipeline {
    agent any
    stages {
        stage("Test FileSystemRetriever"){
            steps{
                script{
                    library identifier: 'test-library@0.0.1', retriever: fileSystemRetriever(
                    libraryPath: 'C:\\test-library')
                    log.info("info message")
                }
            }
        }
    }
}

Github repository for this plugin can be found here - https://github.com/Velan987/FileSystemRetriever

Velan
  • 21
  • 1
  • 3