Laravel & Jenkins: Continuous integration

UPDATE: I have published an updated version of this post since this one is a bit outdated. The new post deals with PHP applications in general, and can easily be applied for continuous integration with Laravel & Jenkins. Read it instead.

This will be a hands on guide for setting up automated builds for a Laravel application using Jenkins. Pretty much that when you do a commit, Jenkins will automagically make a build and in that check code errors and syntax, run unit tests and provide visual code coverage for your code base. Achieve continous integration for PHP with Laravel & Jenkins, along with other goodies.

I’ve completely switched over to Laravel as my weapon of choice for PHP frameworks, I’m not going to get into details about why but the philosophy behind it is just as awesome as the code. One of the concepts is that the entire framework is unit tested with 100% code coverage, which makes it a perfect candidate for continuos integration.

Jenkins is an open source continuos integration server that has gotten a lot of attention recently as the #1 open source continuos integration server. Mostly because its vast amount of plugins, which currently is 600+ as I’m writing this. We’re going to use 10 of those to automate our test environment.

This guide will use the following:

  • Ubuntu 12.10
  • PHP 5.4.6
  • Git
  • Jenkins

Installing stuff

We’re going to start off by installing all the necessary software for this to be possible. We’ll start with PHP, Git and Jenkins.

sudo apt-get install php5 git-core curl jenkins

You will now be able to open Jenkins in your browser by going to http://localhost:8080 to see if the installation worked.

Then it’s time to setup a Laravel installation.

cd /var/www
git clone https://github.com/laravel/laravel.git laravel

And now you’ll a fresh installation of Laravel in /var/www/laravel.

Now we continue by installing all the PHP packages through PEAR and also the plugins Jenkins need. Most of this is based on jenkins-php. We also download the latest jenkins.war file since the one shipped with the standard package in Ubuntu has caused me nothing but problem.

sudo apt-get install php-pear
sudo pear config-set auto_discover 1
sudo pear install pear.phpqatools.org/phpqatools
curl -L http://updates.jenkins-ci.org/update-center.json | sed '1d;$d' | curl -X POST -H 'Accept: application/json' -d @- http://localhost:8080/updateCenter/byId/default/postBack
jenkins-cli -s http://localhost:8080 install-plugin checkstyle cloverphp dry htmlpublisher jdepend plot pmd violations xunit git
sudo wget -O /usr/share/jenkins/jenkins.war http://mirrors.jenkins-ci.org/war/latest/jenkins.war
sudo /etc/init.d/jenkins restart

Go to Jenkins web interface (when it has finished restarting) and go to Manage Jenkins -> Configure System, then scroll down until you find Git Plugin. Then fill in the git configuration for Jenkins, such as jenkins as [email protected] and hit Save.

Configure build

Now clone my github repository laravel-jenkins which is the boilerplate for all the config files and the Jenkins job.

cd /var/www
git clone git://github.com/modess/laravel-jenkins.git
mv laravel-jenkins/* laravel/
cd /var/www/laravel

Now you should have these files in your Laravel directory as well:

build/
    - code-browser/
    - coverage/
    - logs/
    - pdepend/
    - phpcs.xml (PHP Code Sniffer config)
    - phpmd.xml (PHP Mess Detector config)
build.xml (build config)
config.xml (Jenkins job config)
phpunit-bootstrap.php (PHPUnit bootstrap script)
phpunit.xml.dist (PHPUnit config)

Configure Jenkins

Now we have to setup a job in Jenkins for building your application on commit. How this will work is that you add a hook to git which will trigger Jenkins to pull the code and start the automated build. Lucky you I have just like a TV chef prepared that for you. First we start by setting up the Job by moving it to Jenkins folder for jobs and reloading the configuration.

cat config.xml | jenkins-cli -s http://localhost:8080/ create-job laravel-job
sudo chown -R jenkins:jenkins /var/lib/jenkins/jobs/laravel-job  
jenkins-cli -s http://localhost:8080 reload-configuration

Then you must (but it’s optional..) add a post-commit git hook, that will trigger every time you do a commit. What it does is notify the Jenkins that a commit has been made and that it should fetch the code and do an automated build.

vim .git/hooks/post-commit 

Then add this to that file and save

#!/bin/sh
curl http://localhost:8080/git/notifyCommit?url=/var/www/laravel

And we also need to make the git executable

chmod +x .git/hooks/post-commit

Try out your new Laravel & Jenkins combo!

All you have to do now is make a commit.

git add .
git commit -m "Test autobuild in Jenkins"

After your commit you should see this in Jenkins, and the build should pass. Since Laravel ships with an example test (that just asserts true is true) you should have one passed test as well.

Congratulations, you have learned PHP continous integration for a Laravel application leveraging Jenkins and Git! Laravel & Jenkins go very smoothly hand in hand.

  • Glenn Plas

    Tx for the headsup on the ubuntu war file causing problems. timesaver!

    • That one did cause me a lot of headaches at first before I resolved the issue 🙂

      Stay tuned for an updated version targeting Laravel 4!

  • Thanks for this great guide! Im not sure if you can help or not but Im having an issue (im new to CI in general).

    The build from ANT is successful, but it then fails later on due to junit.xml being empty

    phpcb:
    [exec] [Warning] Could not read file ‘/var/lib/jenkins/jobs/laravel-job/workspace/build/logs/junit.xml’. Make sure it contains valid xml.

    build:

    BUILD SUCCESSFUL

    [xUnit] [INFO] – Processing PHPUnit-3.x (default)
    [xUnit] [INFO] – [PHPUnit-3.x (default)] – 1 test report file(s) were found with the pattern ‘build/logs/junit.xml’ relative to ‘/var/lib/jenkins/jobs/laravel-job/workspace’ for the testing framework ‘PHPUnit-3.x (default)’.
    [xUnit] [ERROR] – The result file ‘/var/lib/jenkins/jobs/laravel-job/workspace/build/logs/junit.xml’ for the metric ‘PHPUnit’ is empty. The result file has been skipped.
    [xUnit] [INFO] – Fail BUILD because ‘set build failed if errors’ option is activated.

    Any help greatly apreciated! Thanks 🙂

    • Hey, thanks! I recently merged a pull request to the repository for making it compatible with Laravel 4. I haven’t really had time to make sure everything works as it should. My plan is to revise it and update this post to make sure everything works with Laravel 4.

      I’ll let you know when I do it, will hopefully be this week.

      • Hi Niklas,

        Thanks for getting back to me! After much trial and error it seems the problem was PHP / Composer related. It was a fresh box and PHP wasn’t quite setup right. After that the build ran successfully on my Laravel 4 site. So I can confirm that your config files work great on L4.

        Thanks, Adam 🙂

  • David

    Hi, thanks for the guide. How did you come up with the content of the config.xml file?

    • Hi, the config.xml file was generated through Jenkins. What I did was to setup a complete job and then exporting the file from that.

  • L

    Started by user anonymous
    Building in workspace /var/lib/jenkins/jobs/laravel-job/workspace
    Checkout:workspace / /var/lib/jenkins/jobs/laravel-job/workspace – [email protected]
    Using strategy: Default
    FATAL: com/cloudbees/plugins/credentials/common/StandardCredentials
    java.lang.NoClassDefFoundError: com/cloudbees/plugins/credentials/common/StandardCredentials
    at org.jenkinsci.plugins.gitclient.Git$1.invoke(Git.java:62)
    at org.jenkinsci.plugins.gitclient.Git$1.invoke(Git.java:53)
    at hudson.FilePath.act(FilePath.java:916)
    at hudson.FilePath.act(FilePath.java:889)
    at org.jenkinsci.plugins.gitclient.Git.getClient(Git.java:65)
    at hudson.plugins.git.GitSCM$2.invoke(GitSCM.java:956)
    at hudson.plugins.git.GitSCM$2.invoke(GitSCM.java:948)
    at hudson.FilePath.act(FilePath.java:916)
    at hudson.FilePath.act(FilePath.java:889)
    at hudson.plugins.git.GitSCM.determineRevisionToBuild(GitSCM.java:948)
    at hudson.plugins.git.GitSCM.checkout(GitSCM.java:1114)
    at hudson.model.AbstractProject.checkout(AbstractProject.java:1411)
    at hudson.model.AbstractBuild$AbstractBuildExecution.defaultCheckout(AbstractBuild.java:657)
    at jenkins.scm.SCMCheckoutStrategy.checkout(SCMCheckoutStrategy.java:88)
    at hudson.model.AbstractBuild$AbstractBuildExecution.run(AbstractBuild.java:562)
    at hudson.model.Run.execute(Run.java:1665)
    at hudson.model.FreeStyleBuild.run(FreeStyleBuild.java:46)
    at hudson.model.ResourceController.execute(ResourceController.java:88)
    at hudson.model.Executor.run(Executor.java:246)
    Caused by: java.lang.ClassNotFoundException: com.cloudbees.plugins.credentials.common.StandardCredentials
    at org.apache.tools.ant.AntClassLoader.findClassInComponents(AntClassLoader.java:1365)
    at org.apache.tools.ant.AntClassLoader.findClass(AntClassLoader.java:1315)
    at org.apache.tools.ant.AntClassLoader.loadClass(AntClassLoader.java:1068)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:266)
    … 19 more

  • L

    curl http://localhost:8080/git/notifyCommit?url=/var/www/laravel
    gives me:
    Status Code: 500Exception:
    Stacktrace:
    java.lang.NoSuchMethodError: hudson.security.ACL.impersonate(Lorg/acegisecurity/Authentication;)Lorg/acegisecurity/context/SecurityContext;
    at hudson.plugins.git.GitStatus$JenkinsAbstractProjectListener.onNotifyCommit(GitStatus.java:201)
    at hudson.plugins.git.GitStatus.doNotifyCommit(GitStatus.java:76)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:616)
    at org.kohsuke.stapler.Function$InstanceFunction.invoke(Function.java:282)
    at org.kohsuke.stapler.Function.bindAndInvoke(Function.java:149)
    at org.kohsuke.stapler.Function.bindAndInvokeAndServeResponse(Function.java:88)
    at org.kohsuke.stapler.MetaClass$1.doDispatch(MetaClass.java:104)
    at org.kohsuke.stapler.NameBasedDispatcher.dispatch(NameBasedDispatcher.java:53)
    at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:561)
    at org.kohsuke.stapler.Stapler.invoke(Stapler.java:646)
    at org.kohsuke.stapler.MetaClass$12.dispatch(MetaClass.java:377)
    at org.kohsuke.stapler.Stapler.tryInvoke(Stapler.java:561)
    at org.kohsuke.stapler.Stapler.invoke(Stapler.java:646)
    at org.kohsuke.stapler.Stapler.invoke(Stapler.java:477)
    at org.kohsuke.stapler.Stapler.service(Stapler.java:159)
    at javax.servlet.http.HttpServlet.service(HttpServlet.java:45)
    at winstone.ServletConfiguration.execute(ServletConfiguration.java:249)
    at winstone.RequestDispatcher.forward(RequestDispatcher.java:335)
    at winstone.RequestDispatcher.doFilter(RequestDispatcher.java:378)
    at hudson.util.PluginServletFilter$1.doFilter(PluginServletFilter.java:94)
    at hudson.util.PluginServletFilter.doFilter(PluginServletFilter.java:86)
    at winstone.FilterConfiguration.execute(FilterConfiguration.java:195)
    at winstone.RequestDispatcher.doFilter(RequestDispatcher.java:368)
    at hudson.security.csrf.CrumbFilter.doFilter(CrumbFilter.java:47)
    at winstone.FilterConfiguration.execute(FilterConfiguration.java:195)
    at winstone.RequestDispatcher.doFilter(RequestDispatcher.java:368)
    at hudson.security.ChainedServletFilter$1.doFilter(ChainedServletFilter.java:84)
    at hudson.security.ChainedServletFilter.doFilter(ChainedServletFilter.java:76)
    at hudson.security.HudsonFilter.doFilter(HudsonFilter.java:164)
    at winstone.FilterConfiguration.execute(FilterConfiguration.java:195)
    at winstone.RequestDispatcher.doFilter(RequestDispatcher.java:368)
    at hudson.util.CharacterEncodingFilter.doFilter(CharacterEncodingFilter.java:81)
    at winstone.FilterConfiguration.execute(FilterConfiguration.java:195)
    at winstone.RequestDispatcher.doFilter(RequestDispatcher.java:368)
    at winstone.RequestDispatcher.forward(RequestDispatcher.java:333)
    at winstone.RequestHandlerThread.processRequest(RequestHandlerThread.java:244)
    at winstone.RequestHandlerThread.run(RequestHandlerThread.java:150)
    at java.lang.Thread.run(Thread.java:679)

    • Seems to be some issue with Jenkins, can’t really help you with that unfortunately 🙁

  • Installation didn’t work, had to go to the site for clarification, before running apt-get install jenkins I had to run the following code:

    wget -q -O – http://pkg.jenkins-ci.org/debian/jenkins-ci.org.key | sudo apt-key add –
    sudo sh -c ‘echo deb http://pkg.jenkins-ci.org/debian binary/ > /etc/apt/sources.list.d/jenkins.list’
    sudo apt-get update
    sudo apt-get install jenkins

    Hope that helps someone else!

    • Which version of Ubuntu are you running? I’ll look into it and update the post, thanks!

  • Syrgak Turgumbaev

    Thanks bunch for the article! It’s definitely of great help for those who new to CI.
    I have one question though regarding its expansion. In your example the system listens to commit hook in order to trigger the build. As I understand it the commit must be local i.e. must be made from the same machine as the CI. Is there a way to set it up so it can listen to commit to the Git-repository from other places as well? Let’s say one of the developers is working remotely and pushes changes directly to Git-repo. Thanks in advance.

    • Yes this is something I’m aware of and been meaning to update the post about. That is how to use an external Jenkins server, since that is usually they case when working with CI. Hopefully I’ll get that done soon!

  • Pablo Ezequiel Leone Signetti

    I used a similar tool with a similar interface but with a different name. Is very useful if you write tests 🙂

    Would be brilliant if exists a laravel version with this built-in and ready to use it.

    Thank you, nice post!

  • chollie

    Flippit, why is their no info about how to add test coverage etc

    • With the provided templates for Jenkins and the phpunit.dist.xml it will automatically generate test coverage. However this post is slightly outdated and I intend to update it soon.

      • Jim

        Quick question, if the git repo is hosted on a remote server, this guide wouldn’t work correct? It seems this is for a local git repository.

        • Niklas Modess

          Not not entirely, it could of course be modified slightly for that to work. I’m working on a new and updated post for Laravel 5 and a remote server using a remote repository.

          • Jim

            Gotcha, I’m trying to make this work with a remote git server and in Laravel 5, so far no luck. I look forward to your update post! Thanks for the post and taking your time to help others! I appreciate it.

          • Sean

            Excellent, I was looking for the comment asking about Laravel 5. Look forward to the new post! Thanks for the great detail and thoughtfulness.

          • Niklas Modess

            Thanks! I will let you know once it’s published 🙂

          • Robert

            any update on this?

          • Niklas Modess

            Not yet, sorry. I’m currently travelling and writing is not my main priority at the moment. But I can promise you that it is coming sometime.

          • Benito

            so it should work properly with L5.2???

          • Niklas Modess

            I’m not sure actually since I haven’t tried it. But it should work.

          • Benito

            its really cool but what if my repo is in codecommit from aws ? i only should change the /var/www/laravel to my ssh address?? I already configured the keys

          • Niklas Modess

            This is one of the things I’m going to address in the new post, since this post only deals with a local repository. I’m not familiar with Codecommit, but I assume it works as Github or Bitbucket, and in that case it will be covered.

  • John Faber Marin

    It’s don’t work me, I install everything but jenkins not update the commit, what’s matther?

    • Really hard to tell without any error messages or anything. However this is an old post, and I have written a new and updated version that should work better. Take a look at that instead and try: https://modess.io/jenkins-php/