Avatar
😀

Organizations

13 results for Jenkins
  • This is a repost of my post on the CloudBees Developers blog

    TL;DR Source control injection attacks are a bigger worry than build tool injection attacks, and if you cannot trust your local filesystem, then you cannot trust anything.

    A few exchanges on twitter have prompted me to write a fuller blog post on the subject of Cross-Build Injection (XBI) Attacks.

    The idea of XBI is that you trick the developer and replace parts of their code with your code, thereby getting your code to be trusted by the developer.

    Maven Jenkins Created Mon, 01 Oct 2012 00:00:00 +0000
  • This is a tail about Jenkins (nĂŠe Hudson) and Kohsuke’s policy of maintaining backwards compatibility…

    Back in 2006 I started working for my previous employer, just a month or two after Peter Reilly started. Initially we were working on the same team. This was a team who’s CI system was a nightly cron job that emailed off a list of failing tests to everyone… obviously Peter and I had many a WTF over that old system… so I convinced our boss that we should put some effort into setting up a proper CI system… Initially this was CruiseControl (as he thought Hudson at version 1.64 was too new and unheard of… go with the old reliable)… but after a couple of pains with the CruiseControl system (monolithic xml config file), we convinced him to switch to Hudson… (I don’t think we ever looked back!)

    Jenkins CloudBees Created Sun, 01 May 2011 00:00:00 +0000
  • We recently lost our hudson server due to a multiple disk failure in the RAID array storing our hudson configuration. [5 of the 15 disks died]

    So I’ve been looking into a backup script that will allow us to keep a backup of the configuration.  We use Maven for most of our builds, so the released artifacts are in our Maven Repository (which is hosted on two servers each with RAID arrays and using DRBD to mirror between the pair, with an rsync to a NAS in another cabinet and we are trying to get an rsych to an off-site storage going as well).

    Jenkins Created Thu, 01 Jul 2010 00:00:00 +0000
  • Life gets in the way… but we’re back with our final installment! So where to start, let’s start with a publisher for freestyle builds, then we’ll add a publisher for Maven 2 builds… These will both require some reports to display results, and then finally we’ll need the plugin entry point. But before we get into all that, perhaps I should briefly explain structured form submission supportDataBoundConstructorsHudson uses Stapler as it’s web framework. One of the things that Stapler provides is support for constructing objects from a JSON data model. Basically, if you have a class with a public constructor annotated with @DataBoundConstructor, Stapler will bind fields from a JSON object by matching the field name to the constructor parameter name. If a parameter also has a @DataBoundConstructor, then Stapler will recurse to construct this child object from the child JSON object.Note: The only hole in this (at the moment) is if you want to inject a variable class, i.e. it does not support the case where there are three ChildImpl classes all implementing Child, and all with @DataBoundConstructor and Parent’s constructor has a parameter which takes Child… However, plans are afoot to fix this!JavaNCSSPublisherPublishers in Hudson must have a Descriptor, this will be registered with Hudson and allows Hudson to create Publisher instances which have the details for the project they are publishing. Descriptors are normally implemented as an inner class called DescriptorImpl and there is normally a static field of the publisher DESCRIPTOR that holds the Descriptor singleton. 99.995% of the time, you will want your publisher to have a @DataBoundConstructor, so without further delay, here is the publisher:package hudson.plugins.javancss;import hudson.maven.MavenModule;import hudson.maven.MavenModuleSet;import hudson.model.AbstractProject;import hudson.model.Action; import hudson.model.Descriptor;import hudson.plugins.helpers.AbstractPublisherImpl;import hudson.plugins.helpers.Ghostwriter;import hudson.tasks.BuildStepDescriptor;import hudson.tasks.Publisher;import net.sf.json.JSONObject;import org.kohsuke.stapler.DataBoundConstructor;import org.kohsuke.stapler.StaplerRequest;public class JavaNCSSPublisher extends AbstractPublisherImpl { private String reportFilenamePattern; @DataBoundConstructor public JavaNCSSPublisher(String reportFilenamePattern) { reportFilenamePattern.getClass(); this.reportFilenamePattern = reportFilenamePattern; } public String getReportFilenamePattern() { return reportFilenamePattern; } public boolean needsToRunAfterFinalized() { return false; } public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); public Descriptor getDescriptor() { return DESCRIPTOR; } public Action getProjectAction(AbstractProject project) { return new JavaNCSSProjectIndividualReport(project); } protected Ghostwriter newGhostwriter() { return new JavaNCSSGhostwriter(reportFilenamePattern); } public static final class DescriptorImpl extends BuildStepDescriptor { private DescriptorImpl() { super(JavaNCSSPublisher.class); } public String getDisplayName() { return “Publish " + PluginImpl.DISPLAY_NAME; } public boolean isApplicable(Class> extends AbstractBuildAction { private final Collection results; private final Statistic totals; public AbstractBuildReport(Collection results) { this.results = results; this.totals = Statistic.total(results); } public Collection getResults() { return results; } public Statistic getTotals() { return totals; } public String getSummary() { AbstractBuild prevBuild = getBuild().getPreviousBuild(); while (prevBuild != null && prevBuild.getAction(getClass()) == null) { prevBuild = prevBuild.getPreviousBuild(); } if (prevBuild == null) { return totals.toSummary(); } else { AbstractBuildReport action = prevBuild.getAction(getClass()); return totals.toSummary(action.getTotals()); } } public String getIconFileName() { return PluginImpl.ICON_FILE_NAME; } public String getDisplayName() { return PluginImpl.DISPLAY_NAME; } public String getUrlName() { return PluginImpl.URL; } public boolean isGraphActive() { return false; }}Similarly, we have AbstractProjectReport which will be used for project reports:package hudson.plugins.javancss;import java.io.IOException;import java.util.Collection;import java.util.Collections;import hudson.model.AbstractBuild;import hudson.model.AbstractProject;import hudson.model.ProminentProjectAction;import hudson.plugins.helpers.AbstractProjectAction;import hudson.plugins.javancss.parser.Statistic;import org.kohsuke.stapler.StaplerRequest;import org.kohsuke.stapler.StaplerResponse;public abstract class AbstractProjectReport<T extends AbstractProject> extends AbstractProjectAction implements ProminentProjectAction { public AbstractProjectReport(T project) { super(project); } public String getIconFileName() { for (AbstractBuild build = getProject().getLastBuild(); build != null; build = build.getPreviousBuild()) { final AbstractBuildReport action = build.getAction(getBuildActionClass()); if (action != null) { return PluginImpl.ICON_FILE_NAME; } } return null; } public String getDisplayName() { for (AbstractBuild build = getProject().getLastBuild(); build != null; build = build.getPreviousBuild()) { final AbstractBuildReport action = build.getAction(getBuildActionClass()); if (action != null) { return PluginImpl.DISPLAY_NAME; } } return null; } public String getUrlName() { for (AbstractBuild build = getProject().getLastBuild(); build != null; build = build.getPreviousBuild()) { final AbstractBuildReport action = build.getAction(getBuildActionClass()); if (action != null) { return PluginImpl.URL; } } return null; } public String getSearchUrl() { return PluginImpl.URL; } public boolean isGraphActive() { return false; } public Collection getResults() { for (AbstractBuild build = getProject().getLastBuild(); build != null; build = build.getPreviousBuild()) { final AbstractBuildReport action = build.getAction(getBuildActionClass()); if (action != null) { return action.getResults(); } } return Collections.emptySet(); } public Statistic getTotals() { for (AbstractBuild build = getProject().getLastBuild(); build != null; build = build.getPreviousBuild()) { final AbstractBuildReport action = build.getAction(getBuildActionClass()); if (action != null) { return action.getTotals(); } } return null; } protected abstract Class> implements AggregatableAction { public JavaNCSSBuildIndividualReport(Collection results) { super(results); } @Override public synchronized void setBuild(AbstractBuild build) { super.setBuild(build); if (this.getBuild() != null) { for (Statistic r : getResults()) { r.setOwner(this.getBuild()); } } } public MavenAggregatedReport createAggregatedAction(MavenModuleSetBuild build, Map<MavenModule, List> moduleBuilds) { return new JavaNCSSBuildAggregatedReport(build, moduleBuilds); }}That was fairly painless… Note that we interfaces for both the freestyle and maven2 project types, this is OK as the freestyle projects will ignore the Maven2 stuff and vice-versa while the common code is shared by both. Next we need the aggregated build report:package hudson.plugins.javancss;import hudson.maven.*;import hudson.model.Action;import hudson.plugins.javancss.parser.Statistic;import java.util.ArrayList;import java.util.Collection;import java.util.List;import java.util.Map;public class JavaNCSSBuildAggregatedReport extends AbstractBuildReport implements MavenAggregatedReport { public JavaNCSSBuildAggregatedReport(MavenModuleSetBuild build, Map<MavenModule, List> moduleBuilds) { super(new ArrayList()); setBuild(build); } public synchronized void update(Map<MavenModule, List> moduleBuilds, MavenBuild newBuild) { JavaNCSSBuildIndividualReport report = newBuild.getAction(JavaNCSSBuildIndividualReport.class); if (report != null) { Collection u = Statistic.merge(report.getResults(), getResults()); getResults().clear(); getResults().addAll(u); getTotals().add(report.getTotals()); } } public Class> implements ProminentProjectAction { public JavaNCSSProjectIndividualReport(AbstractProject project) { super(project); } protected Class<? extends AbstractBuildReport> getBuildActionClass() { return JavaNCSSBuildIndividualReport.class; }}Don’t repeat ourselves comes in handy here as essentially all the work has been done for us!. The project aggregated report:package hudson.plugins.javancss;import hudson.model.Actionable;import hudson.model.ProminentProjectAction;import hudson.model.AbstractBuild;import hudson.model.Action;import hudson.maven.MavenModuleSet;import hudson.maven.MavenModuleSetBuild;import hudson.plugins.javancss.parser.Statistic;public class JavaNCSSProjectAggregatedReport extends AbstractProjectReport implements ProminentProjectAction { public JavaNCSSProjectAggregatedReport(MavenModuleSet project) { super(project); } protected Class<? extends AbstractBuildReport> getBuildActionClass() { return JavaNCSSBuildAggregatedReport.class; }}Again DRY to the rescue… At this point all that remains is to present the reports from these backing objects… so on with the jelly views. The helper classes and our inheritance makes this easy… all we need is two jelly files: hudson/plugins/javancss/AbstractBuildReport/reportDetail.jelly and hudson/plugins/javancss/AbstractProjectReport/reportDetail.jelly. Here they are:<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout” xmlns:t="/lib/hudson" xmlns:f="/lib/form"> Results Package Classes Functions Javadocs NCSS JLC SLCLC MLCLC Totals ${it.totals.classes} ${it.totals.functions} ${it.totals.javadocs} ${it.totals.ncss} ${it.totals.javadocLines} ${it.totals.singleCommentLines} ${it.totals.multiCommentLines} <j:forEach var=“r” items="${it.results}"> ${r.name} ${r.classes} ${r.functions} ${r.javadocs} ${r.ncss} ${r.javadocLines} ${r.singleCommentLines} ${r.multiCommentLines} </j:forEach> </j:jelly>Yep, the two files are identical! Other plugins may not be quite so lucky… but in general the project level report should be the same as the report for the latest buildMaking a pluginNow we are ready to make our plugin…. for this we need a class that extends hudson.Plugin and registers our publisher’s descriptors with the appropriate lists… here it is:package hudson.plugins.javancss;import hudson.Plugin;import hudson.maven.MavenReporters;import hudson.tasks.BuildStep;public class PluginImpl extends Plugin { public void start() throws Exception { BuildStep.PUBLISHERS.add(JavaNCSSPublisher.DESCRIPTOR); MavenReporters.LIST.add(JavaNCSSMavenPublisher.DESCRIPTOR); } public static String DISPLAY_NAME = “Java NCSS Report”; public static String GRAPH_NAME = “Java NCSS Trend”; public static String URL = “javancss”; public static String ICON_FILE_NAME = “graph.gif”;}And that’s pretty much it… we should have a working pluginFinishing touchesOK, so the plugin does not have health reports (i.e. the weather icons) and it does not show a trend graph… I think I’m going to need a part 8 :-(

    Jenkins Created Sun, 01 Jun 2008 00:00:00 +0000
  • Over on the Maven2 Users list a recent poster asked what CI server was best, and Hudson was the only answer.Then Jason van Zyl posted this praise for Hudson:I know from my vantage point Hudson is the only system I will provide commercial support for at Sonatype because the battle is over. Hudson won by making developers lives’ easier. Kohsuke will go to no end to make things easier for users. …At any rate I guarantee you that inside 3 months Hudson will have the best Maven integration of any CI/Build Server there is.I’m personally biased towards Hudson, but I have to agree that it has won the war of the CI servers. I don’t see anything coming close in ease of use or speed of startup.

    Jenkins Created Tue, 01 Apr 2008 00:00:00 +0000
  • In some ways parsing the JavaNCSS results is the least interesting part of developing a Hudson plugin, as once I have implemented the parser, it is available for everyone. For that reason I will focus more on:best practice techniques for parsing resultscommon gotchasdesigning for extensionGetting startedFirst off, we need to analyse the results file format. In the case of JavaNCSS there are multiple ways that the results file can be generated: from the JavaNCSS program directly, from ANT or from Maven. This leads us onto gotcha #1Gotcha #1:Never assume that a build tool generates the same format of output when run from the command line, ANT or Maven.A case in point for Gotcha #1 is Findbugs which generates one XML format from the command line and ANT, and generates a different format that appears similar at first glance when run from Maven (mail thread). In this case it turns out that Maven 1 used the different format output, and it is feared that some people came to depend on this Maven 1 format, so when the plugin for Maven 2 was developed, they kept the Maven 1 format. In any case, the moral is don’t assume, check!So we use the sample projects from Part 1 and generate an ANT and a Maven 2 XML report. First off, here is the report from ANT:2008-04-1211:22:30 com.onedash.common 1 3 10 3 <javadoc_lines>12</javadoc_lines> <single_comment_lines>0</single_comment_lines> <multi_comment_lines>0</multi_comment_lines> … … 5 8 46 9 <javadoc_lines>37</javadoc_lines> <single_comment_lines>0</single_comment_lines> <multi_comment_lines>0</multi_comment_lines> PackagesClassesFunctionsNCSSJavadocsper 4.005.008.0046.009.00Project 1.252.0011.502.25Package 1.609.201.80Class 5.751.13Function com.onedash.common.Factory 7 3 0 3 … … 6.60 1.60 0.00 1.80 46.00 com.onedash.common.Factory.Factory() 1 1 1 … … 46.00OK, first off, for those following the tutorial exactly, I have cheated a little. I added some more source files into the project to make sure that I have multiple classes is different packages. You can see the source code I built from here. Additionally, I have trimmed the output somewhat to highlight the interesting bits, removing the duplicate entries.From this report file we can see a basic XML structure:The root element is and has child elements: , , , , and The and elements are the timestamp when the report was generated with the date in YYYY-MM-DD format and the time in HH:MM:SS formatThe element has child elements: , , and . There are multiple ; elements, but only one and element.The elements have child elements: , , , , , <javadoc_lines>, <single_comment_lines> and <multi_comment_lines>. The element contains the name of the package as a String and the other elements contain totals as Integers.The element has child elements: , , , , <javadoc_lines>, <single_comment_lines> and <multi_comment_lines>. These elements are the sum of all the corresponding children inside the parentThe element seems to be a HTML table.The element has child elements: , and . There are multiple elements, the element contains the average results for all the elements and the element providing some form of total or average.The element has child elements: and . Again there are multiple elements with the element providing some form of total or average (interestingly the result appears to be the same as from ).Now, let’s take a look at what Maven 2 gives us:2008-04-1211:43:06 com.onedash.common 1 3 10 3 <javadoc_lines>12</javadoc_lines> <single_comment_lines>0</single_comment_lines> <multi_comment_lines>0</multi_comment_lines> … … 5 8 46 10 <javadoc_lines>42</javadoc_lines> <single_comment_lines>3</single_comment_lines> <multi_comment_lines>3</multi_comment_lines> PackagesClassesFunctionsNCSSJava 4.005.008.0046.0010.00P 1.252.0011.502.50Packag 1.609.202.00Class< 5.751.25Function</ com.onedash.common.api.Namer 2 1 0 1 … … 6.60 1.60 0.00 2.00 46.00 com.onedash.common.api.Namer.newName() 1 1 0 … 46.00Thankfully, this is the same format as for ANT. You will also be relieved to know that this is the format generated by the JavaNCSS program directly. Thus we only have to write one parser, and we do not have to detect what format we are parsing. But before I forget:Best Practice #1:When there are multiple formats of a report generated by different tools, make sure that your Hudson plugin can detect the different formats and can handle them appropriately (by either delegating to a different parser implementation, or by handling the differences on the fly).One of the goals of Hudson is to minimise configuration. So when a plugin can detect an configuration option automatically, it should detect it automatically (possibly providing an “Advanced” option button to let users override the detection if Hudson gets it wrong)Start smallLooking at the JavaNCSS output, I see that there is a lot of information… and I only have one more Part left in this series! So I am not going to parse everything. I am sure that in the future I will extend the Hudson plugin to parse all of the file, but for now I am just going to concentrate on the element. This gives users something useful and it’s better than nothing.But what happens when I do get around to parsing the and elements? People may have lots of old builds and they will want to see the trends of the and results. I have two choices:Tell them “Sorry, out of luck”Save the results with the build, and then the newer parser can extract the results when people want the trend.Choosing between these two options can be difficult. My preference is to go with option two, as long as the results are not a really big file.Best Practice #2:If you are not parsing everything in the results file, and the results file is not too big, and it can be parsed without reference to the source code, copy the results file to Hudson so that future versions of your plugin can read the information you are not currently parsing.Don’t over parseThe results that we parse are going to be placed into an Action object. This Action object will be serialized. When Hudson starts up, it reads all the results of all the builds. If we place too much information in our Action object, this can have a detrimental effect on Hudson’s performance. When users have 50+ projects each with a couple of hundred builds, they will thank you for keeping your Action objects small.Gotcha #2:Don’t store too much in your Action objects.Don’t under parseOK, so I have just given out about storing too much in your Action objects. There is a second problem… not storing enough! Most reporting plugins try to present a trend graph to show progress over a number of builds. If we don’t store the information required to generate this trend graph inside our Action objects, then displaying the trend graph will require parsing all the result files for all builds of a project. This can have a detrimental effect on Hudson’s performance. When users have projects with a couple of hundred builds, they will thank you for keeping the information to generate the main trend graph inside your Action objects.Gotcha #3:Store the information for generating the Project level trend graph in your Action objects.A case in point for Gotach #3 is the cobertura plugin, which at the time of writing, does not store the information for the main trend graph in the Action object. I fully intend to fix this situation once I have finished this series!How to parseMost of the result files that you will encounter are XML based. We are writing our plugins in Java, so that gives us a range of parsers to choose from, e.g.SAXDOMStAXRoll your ownEtc.Given that report files can end up very big for very big projects, we need to be careful how we parse the results:Gotcha #4:Don’t parse XML results using DOM, as this will require reading the entire report file into memory.I am going to stick my neck out and make a recommendation:Best Practice #3:Use an XML pull parser to parse XML report files.They are generally faster, use less memory, and are better suited to a “hit-and-run” style of result extraction.Be able to aggregate parsing resultsYou may think that there will only ever be one result file that you need to parse. Maven 2 usually throws a spanner into that model, and everyone has their own ANT build script, so:Gotcha #5:Don’t assume you only have to parse one report file for each project.This gotcha arrives from the code coverage plugins (emma, clover, cobertura). Initially, you would think that people are only interested in one code coverage result, i.e. the coverage for the project… so they will only have one result file that we need to parse, right? Wrong! Some tools/build scripts generate a report for each module but only generate a summary report in non-conforming HTML. Some tools / build scripts generate a report for unit tests and integration tests separately. It’s a mess, and don’t get me started on using different tools for different test types…The parsing engineOk, so here is the parsing engine:package hudson.plugins.javancss.parser;import hudson.model.AbstractBuild;import hudson.util.IOException2;import org.xmlpull.v1.XmlPullParser;import org.xmlpull.v1.XmlPullParserException;import org.xmlpull.v1.XmlPullParserFactory;import java.io.;import java.util.;public class Statistic implements Serializable { private AbstractBuild owner; private String name; private long classes; private long functions; private long ncss; private long javadocs; private long javadocLines; private long singleCommentLines; private long multiCommentLines; public static Collection parse(File inFile) throws IOException, XmlPullParserException { Collection results = new ArrayList(); FileInputStream fis = null; BufferedInputStream bis = null; try { fis = new FileInputStream(inFile); bis = new BufferedInputStream(fis); XmlPullParserFactory factory = XmlPullParserFactory.newInstance(); factory.setNamespaceAware(true); factory.setValidating(false); XmlPullParser parser = factory.newPullParser(); parser.setInput(bis, null); // check that the first tag is expectNextTag(parser, “javancss”); // skip until we get to the tag while (parser.getDepth() > 0 && (parser.getEventType() != XmlPullParser.START_TAG || !“packages”.equals(parser.getName()))) { parser.next(); } while (parser.getDepth() > 0 && (parser.getEventType() != XmlPullParser.START_TAG || !“package”.equals(parser.getName()))) { parser.next(); } while (parser.getDepth() >= 2 && parser.getEventType() == XmlPullParser.START_TAG && “package”.equals(parser.getName())) { Map<String, String> data = new HashMap<String, String>(); String lastTag = null; String lastText = null; int depth = parser.getDepth(); while (parser.getDepth() >= depth) { parser.next(); switch (parser.getEventType()) { case XmlPullParser.START_TAG: lastTag = parser.getName(); break; case XmlPullParser.TEXT: lastText = parser.getText(); break; case XmlPullParser.END_TAG: if (parser.getDepth() == 4 && lastTag != null && lastText != null) { data.put(lastTag, lastText); } lastTag = null; lastText = null; break; } } if (data.containsKey(“name”)) { Statistic s = new Statistic(data.get(“name”)); s.setClasses(Long.valueOf(data.get(“classes”))); s.setFunctions(Long.valueOf(data.get(“functions”))); s.setNcss(Long.valueOf(data.get(“ncss”))); s.setJavadocs(Long.valueOf(data.get(“javadocs”))); s.setJavadocLines(Long.valueOf(data.get(“javadoc_lines”))); s.setSingleCommentLines(Long.valueOf(data.get(“single_comment_lines”))); s.setMultiCommentLines(Long.valueOf(data.get(“multi_comment_lines”))); results.add(s); } parser.next(); } } catch (XmlPullParserException e) { throw new IOException2(e); } finally { if (bis != null) { bis.close(); } if (fis != null) { fis.close(); } } return results; } private static void skipTag(XmlPullParser parser) throws IOException, XmlPullParserException { parser.next(); endElement(parser); } private static void expectNextTag(XmlPullParser parser, String tag) throws IOException, XmlPullParserException { while (true) { if (parser.getEventType() != XmlPullParser.START_TAG) { parser.next(); continue; } if (parser.getName().equals(tag)) { return; } throw new IOException(“Expecting tag " + tag); } } private static void endElement(XmlPullParser parser) throws IOException, XmlPullParserException { int depth = parser.getDepth(); while (parser.getDepth() >= depth) { parser.next(); } } public Statistic(String name) { this.name = name; } … // Simple getters and setters for all the private fields … // equals based on all private fields, hashCode based on // name and owner. … // toString …}Essentially the main work is done in the static parse method. It takes a File and tries to parse it. We get an XML Pull Parser for the stream and ensure that it is neither namespace aware nor validating as the file format does not use namespaces and we will be forgiving on the XML format.The first tag should be and after that we skip until we get a tag. Once we have found the tag we skip until we hit the first tag.We are reverse engineering the JavaNCSS file format, so we will not make any assumptions about the order of the child elements in the element. We put all the child elements into a Map keyed by the element name, and then when we reach the end of the element we pull out the information we were after from the Map and put it into a Statistic object and add that to the collection of results that we will return.As soon as we hit the end of the element, we stop parsing.Supporting aggregationIn order to support aggregation of multiple results, we’ll add some utility methods to the Statistic class, first we need methods that allow us to calculate totals:package hudson.plugins.javancss.parser;…public class Statistic implements Serializable { … public static Statistic total(Collection… results) { Collection merged = merge(results); Statistic total = new Statistic(”"); for (Statistic individual : merged) { total.add(individual); } return total; } public void add(Statistic r) { classes += r.classes; functions += r.functions; ncss += r.ncss; javadocs += r.javadocs; javadocLines += r.javadocLines; singleCommentLines += r.singleCommentLines; multiCommentLines += r.multiCommentLines; } …}The total method just calculates the total of all the statistics in a collection of statistics. We will also need to be able to merge different result sets. This should aggregate totals for each package separately and return a collection with one total statistic for each package:package hudson.plugins.javancss.parser;…public class Statistic implements Serializable { … public static Collection merge( Collection… results) { if (results.length == 0) { return Collections.emptySet(); } else if (results.length == 1) { return results[0]; } else { Map<String, Statistic> merged = new HashMap<String, Statistic>(); for (Collection result : results) { for (Statistic individual : result) { if (!merged.containsKey(individual.name)) { merged.put(individual.name, new Statistic(individual.name)); } merged.get(individual.name).add(individual); } } return merged.values(); } } …}That is pretty much it for the parser engine.The GhostwriterNow we need to hook the engine into our publisher. We will need to configure the UI elements and the Actions… all tasks for the final part, but for now, we’ll just hook it up. We want to run the parsing on the slave side so we implement Ghostwriter.SlaveGhostwriter.package hudson.plugins.javancss;import hudson.FilePath;import hudson.model.AbstractBuild;import hudson.model.BuildListener;import hudson.plugins.helpers.BuildProxy;import hudson.plugins.helpers.Ghostwriter;import hudson.plugins.javancss.parser.Statistic;import org.xmlpull.v1.XmlPullParserException;import java.io.File;import java.io.IOException;import java.util.Collection;import java.util.HashSet;import java.util.Set;public class JavaNCSSGhostwriter implements Ghostwriter, Ghostwriter.SlaveGhostwriter { private final String reportFilenamePattern; public JavaNCSSGhostwriter(String reportFilenamePattern) { this.reportFilenamePattern = reportFilenamePattern; } public boolean performFromSlave( BuildProxy build, BuildListener listener) throws InterruptedException, IOException { FilePath[] paths = build.getExecutionRootDir() .list(reportFilenamePattern); Collection results = null; Set parsedFiles = new HashSet(); for (FilePath path : paths) { final String pathStr = path.getRemote(); if (!parsedFiles.contains(pathStr)) { parsedFiles.add(pathStr); try { Collection result = Statistic.parse(new File(pathStr)); if (results == null) { results = result; } else { results = Statistic.merge(results, result); } // TODO copy the parsed file to the master } catch (XmlPullParserException e) { e.printStackTrace(listener.getLogger()); } } } // TODO add the results into an Action an attach it to the // build. return true; }}Basically, we search the supplied wildcard-path for report files and merge all the results together into a collection of results. In the final part of this series we will create our Action to hold the results and wire everything together.

    Jenkins Created Tue, 01 Apr 2008 00:00:00 +0000
  • Ok, life has got in the way and I have not been able to update this series as quickly as I originally intended. In any case, there were a number of bugs in Hudson that I discovered and are now fixed, and there was a good deal of tidy-up needed in order for me to figure out what to do for parts 6 and 7. The good news is that I am getting closer to finishing writing these final two parts. The bad news is that there are a number of corrections to the previous posts (I have put some corrections in-line for parts 4 and 5). This post aims to ensure that, for parts 6 and 7, everyone is able to follow from the same code!pom.xmlThis needs to be updated to reference a Hudson version of at least 1.200 in order to have the bugs I identified fixed.src/main/java/hudson/plugins/helpers/AbstractBuildAction.javaAFAIK I escaped all the HTML entities and this file is the same as constructed from the previous posts.package hudson.plugins.helpers;import hudson.model.AbstractBuild;import hudson.model.HealthReportingAction;public abstract class AbstractBuildAction<BUILD extends AbstractBuild> implements HealthReportingAction { private BUILD build = null; protected AbstractBuildAction() { } public synchronized BUILD getBuild() { return build; } public synchronized void setBuild(BUILD build) { if (this.build == null && this.build != build) { this.build = build; } } public boolean isFloatingBoxActive() { return false; } public boolean isGraphActive() { return false; } public String getGraphName() { return getDisplayName(); } public String getSummary() { return “”; }}src/main/java/hudson/plugins/helpers/AbstractMavenReporterImpl.javaAFAIK I escaped all the HTML entities and this file is the same as constructed from the previous posts.package hudson.plugins.helpers;import hudson.maven.MavenBuildProxy;import hudson.maven.MavenReporter;import hudson.maven.MojoInfo;import hudson.model.BuildListener;import hudson.model.Result;import org.apache.maven.project.MavenProject;import java.io.IOException;public abstract class AbstractMavenReporterImpl extends MavenReporter { protected MojoExecutionReportingMode getExecutionMode() { return MojoExecutionReportingMode.ONLY_REPORT_ON_SUCCESS; } public boolean postExecute(MavenBuildProxy build, MavenProject pom, MojoInfo mojo, BuildListener listener, Throwable error) throws InterruptedException, IOException { if (!isExecutingMojo(mojo)) { // not a mojo who’s result we are interested in return true; } final Boolean okToContinue = getExecutionMode().isOkToContinue(this, build, listener, error); if (okToContinue != null) { return okToContinue; } build.registerAsProjectAction(this); return BuildProxy.doPerform(newGhostwriter(pom, mojo), build, pom, listener); } protected abstract boolean isExecutingMojo(MojoInfo mojo); protected abstract Ghostwriter newGhostwriter(MavenProject pom, MojoInfo mojo); public boolean preExecute(MavenBuildProxy build, MavenProject pom, MojoInfo mojo, BuildListener listener) throws InterruptedException, IOException { return !isAutoconfMojo(mojo) || autoconfPom(build, pom, mojo, listener); } protected boolean autoconfPom(MavenBuildProxy build, MavenProject pom, MojoInfo mojo, BuildListener listener) { return true; } protected boolean isAutoconfMojo(MojoInfo mojo) { return false; } protected enum MojoExecutionReportingMode { ONLY_REPORT_ON_SUCCESS { Boolean isOkToContinue(MavenReporter reporter, MavenBuildProxy build, BuildListener listener, Throwable error) { return error == null ? null : Boolean.TRUE; } }, ALWAYS_REPORT_STABLE { Boolean isOkToContinue(MavenReporter reporter, MavenBuildProxy build, BuildListener listener, Throwable error) { return null; }}, REPORT_UNSTABLE_ON_ERROR { Boolean isOkToContinue(MavenReporter reporter, MavenBuildProxy build, BuildListener listener, Throwable error) { if (error != null) { listener.getLogger().println("[HUDSON] " + reporter.getDescriptor().getDisplayName() + " setting build to UNSTABLE"); build.setResult(Result.UNSTABLE); } return null; } }; abstract Boolean isOkToContinue(MavenReporter reporter, MavenBuildProxy build, BuildListener listener, Throwable error); }}src/main/java/hudson/plugins/helpers/AbstractProjectAction.javaAFAIK I escaped all the HTML entities and this file is the same as constructed from the previous posts.package hudson.plugins.helpers;import hudson.model.AbstractProject;import hudson.model.Actionable;abstract public class AbstractProjectAction<PROJECT extends AbstractProject> extends Actionable { private final PROJECT project; protected AbstractProjectAction(PROJECT project) { this.project = project; } public PROJECT getProject() { return project; } public boolean isFloatingBoxActive() { return true; } public boolean isGraphActive() { return false; } public String getGraphName() { return getDisplayName(); }}src/main/java/hudson/plugins/helpers/AbstractPublisherImpl.javaSome of the HTML entities were not properly escaped.package hudson.plugins.helpers;import hudson.tasks.Publisher;import hudson.model.AbstractBuild;import hudson.model.BuildListener;import hudson.Launcher;import java.io.IOException;public abstract class AbstractPublisherImpl extends Publisher { protected abstract Ghostwriter newGhostwriter(); public boolean perform(AbstractBuild build, Launcher launcher, final BuildListener listener) throws InterruptedException, IOException { return BuildProxy.doPerform(newGhostwriter(), build, listener); } public boolean prebuild(AbstractBuild build, BuildListener listener) { return true; }}src/main/java/hudson/plugins/helpers/BuildProxy.javaAgain, some of the HTML entities were not properly escaped. Additionally, this is the file that has had the most updates during this tutorial, so here it is in full:package hudson.plugins.helpers;import hudson.FilePath;import hudson.maven.MavenBuildProxy;import hudson.util.IOException2;import hudson.model.Action;import hudson.model.Result;import hudson.model.AbstractBuild;import hudson.model.BuildListener;import java.util.Calendar;import java.util.List;import java.util.ArrayList;import java.io.IOException;import java.io.Serializable;import org.apache.maven.project.MavenProject;public final class BuildProxy implements Serializable { private final FilePath artifactsDir; private final FilePath projectRootDir; private final FilePath buildRootDir; private final FilePath executionRootDir; private final Calendar timestamp; private final List<AbstractBuildAction<AbstractBuild» actions = new ArrayList<AbstractBuildAction<AbstractBuild»(); private Result result = null; private boolean continueBuild = true; public static boolean doPerform(Ghostwriter ghostwriter, AbstractBuild build, BuildListener listener) throws IOException, InterruptedException { // first, do we need to do anything on the slave if (ghostwriter instanceof Ghostwriter.SlaveGhostwriter) { // construct the BuildProxy instance that we will use BuildProxy buildProxy = new BuildProxy( new FilePath(build.getArtifactsDir()), new FilePath(build.getProject().getRootDir()), new FilePath(build.getRootDir()), build.getProject().getModuleRoot(), build.getTimestamp()); BuildProxyCallableHelper callableHelper = new BuildProxyCallableHelper( buildProxy, ghostwriter, listener); try { buildProxy = buildProxy.getExecutionRootDir().act(callableHelper); buildProxy.updateBuild(build); // terminate the build if necessary if (!buildProxy.isContinueBuild()) { return false; } } catch (Exception e) { throw unwrapException(e, listener); } } // finally, on to the master final Ghostwriter.MasterGhostwriter masterGhostwriter = Ghostwriter.MasterGhostwriter.class.cast(ghostwriter); return masterGhostwriter == null || masterGhostwriter.performFromMaster(build, build.getProject().getModuleRoot(), listener); } private static RuntimeException unwrapException(Exception e, BuildListener listener) throws IOException, InterruptedException { if (e.getCause() instanceof IOException) { throw new IOException2(e.getCause().getMessage(), e); } if (e.getCause() instanceof InterruptedException) { e.getCause().printStackTrace(listener.getLogger()); throw new InterruptedException(e.getCause().getMessage()); } if (e.getCause() instanceof RuntimeException) { throw new RuntimeException(e.getCause()); } // How on earth do we get this far down the branch e.printStackTrace(listener.getLogger()); throw new RuntimeException(“Unexpected exception”, e); } public void updateBuild(AbstractBuild build) { for (AbstractBuildAction<AbstractBuild> action : actions) { if (!build.getActions().contains(action)) { action.setBuild(build); build.getActions().add(action); } } if (result != null && result.isWorseThan(build.getResult())) { build.setResult(result); } } public static boolean doPerform(Ghostwriter ghostwriter, MavenBuildProxy mavenBuildProxy, MavenProject pom, final BuildListener listener) throws InterruptedException, IOException { // first, construct the BuildProxy instance that we will use BuildProxy buildProxy = new BuildProxy( mavenBuildProxy.getArtifactsDir(), mavenBuildProxy.getProjectRootDir(), mavenBuildProxy.getRootDir(), new FilePath(pom.getBasedir()), mavenBuildProxy.getTimestamp()); // do we need to do anything on the slave if (ghostwriter instanceof Ghostwriter.SlaveGhostwriter) { final Ghostwriter.SlaveGhostwriter slaveGhostwriter = (Ghostwriter.SlaveGhostwriter) ghostwriter; // terminate the build if necessary if (!slaveGhostwriter.performFromSlave(buildProxy, listener)) { return false; } } // finally, on to the master try { return mavenBuildProxy.execute(new BuildProxyCallableHelper( buildProxy, ghostwriter, listener)); } catch (Exception e) { throw unwrapException(e, listener); } } private BuildProxy(FilePath artifactsDir, FilePath projectRootDir, FilePath buildRootDir, FilePath executionRootDir, Calendar timestamp) { this.artifactsDir = artifactsDir; this.projectRootDir = projectRootDir; this.buildRootDir = buildRootDir; this.executionRootDir = executionRootDir; this.timestamp = timestamp; } public List<AbstractBuildAction<AbstractBuild» getActions() { return actions; } public FilePath getArtifactsDir() { return artifactsDir; } public FilePath getBuildRootDir() { return buildRootDir; } public FilePath getExecutionRootDir() { return executionRootDir; } public FilePath getProjectRootDir() { return projectRootDir; } public Calendar getTimestamp() { return timestamp; } public Result getResult() { return result; } public void setResult(Result result) { this.result = result; } public boolean isContinueBuild() { return continueBuild; } public void setContinueBuild(boolean continueBuild) { this.continueBuild = continueBuild; }}src/main/java/hudson/plugins/helpers/BuildProxyCallableHelper.javaLost some HTML entities (again!)package hudson.plugins.helpers;import hudson.remoting.Callable;import hudson.maven.MavenBuildProxy;import hudson.maven.MavenBuild;import hudson.model.BuildListener;import java.io.IOException;class BuildProxyCallableHelper implements Callable<BuildProxy, Exception>, MavenBuildProxy.BuildCallable<Boolean, Exception> { private final BuildProxy buildProxy; private final Ghostwriter ghostwriter; private final BuildListener listener; BuildProxyCallableHelper(BuildProxy buildProxy, Ghostwriter ghostwriter, BuildListener listener) { this.buildProxy = buildProxy; this.ghostwriter = ghostwriter; this.listener = listener; } public Boolean call(MavenBuild mavenBuild) throws Exception { buildProxy.updateBuild(mavenBuild); if (ghostwriter instanceof Ghostwriter.MasterGhostwriter) { final Ghostwriter.MasterGhostwriter masterBuildStep = (Ghostwriter.MasterGhostwriter) ghostwriter; return masterBuildStep.performFromMaster(mavenBuild, buildProxy.getExecutionRootDir(), listener); } return true; } public BuildProxy call() throws Exception { if (ghostwriter instanceof Ghostwriter.SlaveGhostwriter) { final Ghostwriter.SlaveGhostwriter slaveBuildStep = (Ghostwriter.SlaveGhostwriter) ghostwriter; try { buildProxy.setContinueBuild( slaveBuildStep.performFromSlave(buildProxy, listener)); return buildProxy; } catch (IOException e) { throw new Exception(e); } catch (InterruptedException e) { throw new Exception(e); } } return buildProxy; }}src/main/java/hudson/plugins/helpers/Ghostwriter.javaJava Generics are a real gotcha for HTML entitiespackage hudson.plugins.helpers;import hudson.model.BuildListener;import hudson.model.AbstractBuild;import hudson.FilePath;import java.io.Serializable;import java.io.IOException;public interface Ghostwriter extends Serializable { public static interface SlaveGhostwriter extends Ghostwriter { boolean performFromSlave(BuildProxy build, BuildListener listener) throws InterruptedException, IOException; } public static interface MasterGhostwriter extends Ghostwriter { boolean performFromMaster(AbstractBuild build, FilePath executionRoot, BuildListener listener) throws InterruptedException, IOException; }}src/main/resources/hudson/plugins/helpers/AbstractBuildAction/enlargedGraph.jellyI left a “css” attribute in the <l:layout> tag.<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <l:layout xmlns:plugin="/hudson/plugins/javancss/tags"> <st:include it="${it.build}" page=“sidepanel.jelly”/> <l:main-panel> <j:if test="${it.graphActive}"> ${it.graphName} <st:include page=“largeGraph.jelly”/> </j:if> </l:main-panel> </l:layout></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractBuildAction/floatingBox.jellyThe <j:if> should be based on the from variable and not it<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <j:if test="${from.graphActive}"> ${from.graphName} <st:include page=“normalGraph.jelly”/> enlarge </j:if></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractBuildAction/index.jellyI left a “css” attribute in the <l:layout> tag.<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <l:layout xmlns:plugin="/hudson/plugins/javancss/tags"> <st:include it="${it.build}" page=“sidepanel.jelly”/> <l:main-panel> ${it.displayName} <j:if test="${it.graphActive}"> ${it.graphName} <st:include page=“normalGraph.jelly”/> </j:if> <st:include page=“reportDetail.jelly”/> </l:main-panel> </l:layout></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractBuildAction/largeGraph.jelly, src/main/resources/hudson/plugins/helpers/AbstractBuildAction/normalGraph.jelly, and src/main/resources/hudson/plugins/helpers/AbstractBuildAction/reportDetail.jellyThese are all the same content, and are basically empty placeholders to be overrided in classes that extend AbstractBuildAction. I do not think there are any corrections.<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractBuildAction/summary.jellyThe <j:if> had an extra } in the expression<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i=“jelly:fmt”> <t:summary icon="${it.iconFileName}"> ${it.displayName} ${it.summary} </t:summary> <j:if test="${it.floatingBoxActive}"> <st:include page=“floatingBox.jelly”/> </j:if></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractProjectAction/enlargedGraph.jellyI left a “css” attribute in the <l:layout> tag.<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <l:layout xmlns:plugin="/hudson/plugins/javancss/tags"> <st:include it="${it.project}" page=“sidepanel.jelly”/> <l:main-panel> <j:if test="${it.graphActive}"> ${it.graphName} <st:include page=“largeGraph.jelly”/> </j:if> </l:main-panel> </l:layout></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractProjectAction/floatingBox.jellyThe <j:if> should be based on the from variable and not it<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i=“jelly:fmt” xmlns:local=“local”> <j:if test="${from.graphActive}"> ${from.graphName} <st:include page=“normalGraph.jelly”/> enlarge </j:if></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractProjectAction/index.jellyI left a “css” attribute in the <l:layout> tag.<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <l:layout xmlns:plugin="/hudson/plugins/javancss/tags"> <st:include it="${it.project}" page=“sidepanel.jelly”/> <l:main-panel> ${it.displayName} <j:if test="${it.graphActive}"> ${it.graphName} <st:include page=“normalGraph.jelly”/> </j:if> <st:include page=“reportDetail.jelly”/> </l:main-panel> </l:layout></j:jelly>src/main/resources/hudson/plugins/helpers/AbstractProjectAction/largeGraph.jelly, src/main/resources/hudson/plugins/helpers/AbstractProjectAction/normalGraph.jelly, and src/main/resources/hudson/plugins/helpers/AbstractProjectAction/reportDetail.jellyThese are all the same content, and are basically empty placeholders to be overrided in classes that extend AbstractProjectAction. I do not think there are any corrections.<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"></j:jelly>

    Jenkins Created Sat, 01 Mar 2008 00:00:00 +0000
  • This is the fourth installment, which covers the two abstract classes that call the Ghostwriter for us. When we implement our plugin we will extend from these two classes. These classes perform the work of calling the Ghostwriter at the appropriate points in the build, the concrete classes that we will develop in Part 5, however have the responsibility of creating a configured Ghostwriter and providing it to the superclass for execution.AbstractPublisherImplIn a way, this is the simplest of the two classes:package hudson.plugins.helpers;import hudson.tasks.Publisher;import hudson.model.AbstractBuild;import hudson.model.BuildListener;import hudson.Launcher;import java.io.IOException;public abstract class AbstractPublisherImpl extends Publisher { protected abstract Ghostwriter newGhostwriter(); public boolean perform(AbstractBuild build, Launcher launcher, final BuildListener listener) throws InterruptedException, IOException { return BuildProxy.doPerform(newGhostwriter(), build, listener); } public boolean prebuild(AbstractBuild build, BuildListener listener) { return true; }}Fairly simple, we just ask the BuildProxy to invoke the new Ghostwriter created by the concrete class.AbstractMavenReporterImplThis class needs to do a bit more work. First I’ll present the abstract methods and protected methods that can be overrided by implementing classes:package hudson.plugins.helpers;import hudson.maven.MavenReporter;import hudson.maven.MojoInfo;import hudson.maven.MavenBuildProxy;import hudson.model.BuildListener;import hudson.model.Result;import hudson.plugins.helpers.BuildProxy;import hudson.plugins.javancss.PluginImpl;import org.apache.maven.project.MavenProject;import java.io.IOException;public abstract class AbstractMavenReporterImpl extends MavenReporter { protected abstract boolean isExecutingMojo(MojoInfo mojo); protected abstract Ghostwriter newGhostwriter(MavenProject pom, MojoInfo mojo); protected boolean autoconfPom(MavenBuildProxy build, MavenProject pom, MojoInfo mojo, BuildListener listener) { return true; } protected boolean isAutoconfMojo(MojoInfo mojo) { return false; }}Maven project execution consists of invocations of various golas of the different mojos that are bound to the Maven lifecycle. Hudson inserts hooks that allow us to intercept mojos before they execute and after they have executed. Intercepting prior to execution can be useful to ensure that the plugin has been configured with the options we require (e.g. XML output enabled). Post execution is usually when we want to invoke our Ghostwriter. If the implementing class wants to be able to tweak the mojo configuration prior to execution it needs to override isAutoconfMojo in order to identify the mojo executions that need to be tweaked, and override autoconfPom to do the actual configuration.The implementing class needs to provide:a isExecutingMojo method to identify execution of the mojo that this publisher reports on.a newGhostwriter method that constructs the Ghostwriter which will do the work for us. The new Ghostwriter can either be configured manually on the build page, and/or can be configured based on information in the pom.The final thing that we need to decide is when to execute the publisher:package hudson.plugins.helpers;…public abstract class AbstractMavenReporterImpl extends MavenReporter { … protected MojoExecutionReportingMode getExecutionMode() { return MojoExecutionReportingMode.ONLY_REPORT_ON_SUCCESS; } … protected enum MojoExecutionReportingMode { ONLY_REPORT_ON_SUCCESS { Boolean isOkToContinue(MavenReporter reporter, MavenBuildProxy build, BuildListener listener, Throwable error) { return error == null ? null : Boolean.TRUE; } }, ALWAYS_REPORT_STABLE { Boolean isOkToContinue(MavenReporter reporter, MavenBuildProxy build, BuildListener listener, Throwable error) { return null; }}, REPORT_UNSTABLE_ON_ERROR { Boolean isOkToContinue(MavenReporter reporter, MavenBuildProxy build, BuildListener listener, Throwable error) { if (error != null) { listener.getLogger().println("[HUDSON] " + reporter.getDescriptor().getDisplayName() + " setting build to UNSTABLE"); build.setResult(Result.UNSTABLE); } return null; } }; abstract Boolean isOkToContinue(MavenReporter reporter, MavenBuildProxy build, BuildListener listener, Throwable error); }}We define three different execution modes:ONLY_REPORT_ON_SUCCESS - this ensures that we only run the publisher if the mojo executed without error.ALWAYS_REPORT_STABLE - runs the publisher even if the mojo had an execution error, never marks the build as failed or unstable (note that Maven will most likely mark the build as failed or unstable)REPORT_UNSTABLE_ON_ERROR - runs the publisher even if the mojo had an execution error. In the event of a mojo execution error the build will be marked UNSTABLEAll that’s left is are the methods to tie these all together:package hudson.plugins.helpers;…public abstract class AbstractMavenReporterImpl extends MavenReporter { …. public boolean preExecute(MavenBuildProxy build, MavenProject pom, MojoInfo mojo, BuildListener listener) throws InterruptedException, IOException { return !isAutoconfMojo(mojo) || autoconfPom(build, pom, mojo, listener); } public boolean postExecute(MavenBuildProxy build, MavenProject pom, MojoInfo mojo, BuildListener listener, Throwable error) throws InterruptedException, IOException { if (!isExecutingMojo(mojo)) { // not a mojo who’s result we are interested in return true; } final Boolean okToContinue = getExecutionMode() .isOkToContinue(this, build, listener, error); if (okToContinue != null) { return okToContinue; } build.registerAsProjectAction(this); return BuildProxy.doPerform(newGhostwriter(pom, mojo), build, pom, listener); }}The preExecute method checks if this is an mojo execution that we want to tweak, executing the tweaks if necessary. The postExecute method checks if this the the mojo we want to report on. Then it checks if there were execution errors, following the execution mode reported by getExecutionMode. We register this MavenReporter as a project action (thus signalling Hudson that it should call the getProjectAction() method of our reporter. We will have this method return null if we actually don’t want a project action. This is safer than trying to be smart and only calling register if we want a project action!). Finally we invoke the Ghostwriter!SummaryWell that’s it for Part 4. In the next installment, I will introduce a DRY set of classes for the reports that the publisher will generate

    Jenkins Created Fri, 01 Feb 2008 00:00:00 +0000
  • Until now we have been generating classes that collect the results we want to display. We have not hooked into Hudson’s methods of displaying reports. There are essentially four places that we could want to generate reports:In each individual build (we’ll call this a Single Build Report),In the project (we’ll call this a Single Project Report),In the summary of all the Maven 2 module builds associated with a multi-module Maven 2 build (we’ll call this an Aggregate Build Report), and finallyIn the Maven 2 project itself (we’ll call this an Aggregate Project Report).To make matters more confusing, each of these reports usually to implement different classes and need to implement different interfaces:Project Reports usually inherit from ActionableBuild Reports usually inherit from HealthReportingActionSingle Build Reports in Maven 2 projects need to implement AggregatableActionWhat we need is to put some common framework around these reports to ensure that we are not repeating ourselves all the time. In general, each of these four reports will be doing mostly the same things.The Build Reports will be displaying the results for a specific buildThe Project Reports will be displaying the results for the latest build (and possibly a trend chart if that makes sense)The Aggregate Reports will be displaying the aggregate of all the Single ReportsSound’s like a job for multiple inheritance! Fortune would have it that Java does not support multiple inheritance, so we will have to use some form of composition to get the same effect, and generics can help reduce the repetition too. But enough! On to the solution.AbstractBuildActionWe’ll start with a parent class for all our Build Reports. We’ll make this class generic, parameterised by the Build class that it operates on, so that we can use the same core code for all the Build Reports:package hudson.plugins.helpers;import hudson.model.AbstractBuild;import hudson.model.HealthReportingAction;public abstract class AbstractBuildAction<BUILD extends AbstractBuild> implements HealthReportingAction { private BUILD build = null; protected AbstractBuildAction() { } public synchronized BUILD getBuild() { return build; } public synchronized void setBuild(BUILD build) { if (this.build == null && this.build != build) { this.build = build; } }}We store a reference to the build in a local variable and provide a getter for the build. Ideally we would like to the build variable to be final and initialize it in the constructor. However, Hudson’s remoting interface will get in the way of this for any actions created on the slave. The solution is to use a setter to set the build. Additionally, we have some logic that makes this setter write-once. This is just a safety net to stop us from accidentally confusing Hudson. Strictly speaking, if you are careful the setter can be justa a simple setter and not write once.[Correction] We also need to modify BuildProxy so that it calls our setter for us for actions added from slave side executions:package hudson.plugsin.helpers;…public final class BuildProxy implements Serializable { … private final List<AbstractBuildAction<AbstractBuild» actions = new ArrayList<AbstractBuildAction»(); … public void updateBuild(AbstractBuild<,?> build) { for (AbstractBuildAction<AbstractBuild> action: actions) { if (!build.getActions().contains(action)) { action.setBuild(build); build.getActions().add(action); } } if (result != null && result.isWorseThan(build.getResult())) { build.setResult(result); } } … public List<AbstractBuildAction<AbstractBuild» getActions() { return actions; } …}[/Correction]In addition to these simple getters and setters, we want to provide a framework for displaying the report detail. Each different type of report has different ways of fitting into the Hudson UI. We are going to attempt to standardise these by using wrapper .jelly files to call a standard set which implementing classes can override. Jelly files are stored as resources in the hudson plugin, so with the Maven2 project structure for a Hudson plugin, the java source is /src/main/java/hudson/plugins/helpers/AbstractBuildAction.java and the jelly files for this java class are in /src/main/resources/hudson/plugins/helpers/AbstractBuildAction/. The basic things that all build reports want to do are as follows:A main report detail page (e.g. package level summary of the number of lines of code)A graph of some sort, with the option to enlarge it. (e.g. trend graph of the number of lines of code)A simple summary of the report for the main page (i.e. “17,345 lines of code (+1,534)")We will implement this functionality with some properties of the AbstractBuildAction, and some default .jelly files. First the additional properties:…public abstract class AbstractBuildAction<BUILD extends AbstractBuild> implements HealthReportingAction { … public boolean isFloatingBoxActive() { return false; } public boolean isGraphActive() { return false; } public String getGraphName() { return getDisplayName(); } public String getSummary() { return “”; }}These four properties will be used to control how the action appears. isFloatingBoxActive allows us to enable the floating box on the build summary page. isGraphActive allows us to activate the graph inside the floating box. getGraphName should return the title that will be displayed above the graph. And finally, getSummary will control the summary text to display beside the build report icon on the build summary page.Next we have some empty default place-holder jelly files. These will be the jelly files that we can override in our actual reports:reportDetail.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout” xmlns:t="/lib/hudson" xmlns:f="/lib/form"></j:jelly>normalGraph.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"></j:jelly>largeGraph.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"></j:jelly>The reportDetail.jelly page will be used for the main report detail page, the normalGraph.jelly page will be used for the floating trend graph, while the largeGraph.jelly page will be used for the enlarged graph. To hook these pages into the Hudson framework, we need the following jelly pages:index.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <l:layout xmlns:plugin="/hudson/plugins/javancss/tags" css="/plugin/javancss/css/style.css"> <st:include it="${it.build}" page=“sidepanel.jelly”/> <l:main-panel> ${it.displayName} <j:if test="${it.graphActive}"> ${it.graphName} <st:include page=“normalGraph.jelly”/> </j:if> <st:include page=“reportDetail.jelly”/> </l:main-panel> </l:layout></j:jelly>This page will also include the trend graph if it is activefloatingBox.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <j:if test="${it.graphActive}"> ${from.graphName} <st:include page=“normalGraph.jelly”/> enlarge </j:if></j:jelly>summary.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i=“jelly:fmt”> <t:summary icon="${it.iconFileName}"> ${it.displayName} ${it.summary} </t:summary> <j:if test="${it.floatingBoxVisible}}"> <st:include page=“floatingBox.jelly”/> </j:if></j:jelly>Note that the build main page does not support floating boxes, so we have to hack it in via the summary.jelly pageenlargedGraph.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <l:layout xmlns:plugin="/hudson/plugins/javancss/tags" css="/plugin/javancss/css/style.css"> <st:include it="${it.build}" page=“sidepanel.jelly”/> <l:main-panel> <j:if test="${it.graphActive}"> ${it.graphName} <st:include page=“largeGraph.jelly”/> </j:if> </l:main-panel> </l:layout></j:jelly>AbstractProjectActionThis is similar to AbstractBuildAction, however, we don’t have to deal with the write-once setter problem.package hudson.plugins.helpers;import hudson.model.AbstractProject;import hudson.model.Actionable;abstract public class AbstractProjectAction<PROJECT extends AbstractProject> extends Actionable { private final PROJECT project; protected AbstractProjectAction(PROJECT project) { this.project = project; } public PROJECT getProject() { return project; } public boolean isFloatingBoxActive() { return true; } public boolean isGraphActive() { return false; } public String getGraphName() { return getDisplayName(); }}Again we have a set of jelly files, we’ll repeat the same placeholders:reportDetail.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"></j:jelly>normalGraph.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"></j:jelly>largeGraph.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"></j:jelly>The other link in pages are similar, only changing from a reference for ${it.build} to ${it.project}index.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <l:layout xmlns:plugin="/hudson/plugins/javancss/tags" css="/plugin/javancss/css/style.css"> <st:include it="${it.project}" page=“sidepanel.jelly”/> <l:main-panel> ${it.displayName} <j:if test="${it.graphActive}"> ${it.graphName} <st:include page=“normalGraph.jelly”/> </j:if> <st:include page=“reportDetail.jelly”/> </l:main-panel> </l:layout></j:jelly>floatingBox.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form" xmlns:i=“jelly:fmt” xmlns:local=“local”> <j:if test="${it.graphActive}"> ${from.graphName} <st:include page=“normalGraph.jelly”/> enlarge </j:if></j:jelly>The project page automatically includes the floating box, so we don;t require the summary hackenlargedGraph.jelly<j:jelly xmlns:j=“jelly:core” xmlns:st=“jelly:stapler” xmlns:d=“jelly:define” xmlns:l="/lib/layout" xmlns:t="/lib/hudson" xmlns:f="/lib/form"> <l:layout xmlns:plugin="/hudson/plugins/javancss/tags" css="/plugin/javancss/css/style.css"> <st:include it="${it.project}" page=“sidepanel.jelly”/> <l:main-panel> <j:if test="${it.graphActive}"> ${it.graphName} <st:include page=“largeGraph.jelly”/> </j:if> </l:main-panel> </l:layout></j:jelly>SummaryThat’s it for part 5, next time we will implement the javancss parser and get the results for a build. Part 7 will finish off the plugin with the reports based on these two base classes.

    Jenkins Created Fri, 01 Feb 2008 00:00:00 +0000
  • Writing a Hudson plug-in (Part 1 – Preparation)This is the first part of a tutorial on writing a Hudson plug-in. The aim of this tutorial is to develop a Publisher for JavaNCSS. This publisher will work with both Hudson Freestyle projects and Hudson Maven2 projects. The publisher will also include a trend graph.Before we can start plug-in development, we need to have a set of working projects to throw at our plugin. Ideally these projects should be simple and quick to build, but the most essential thing is that they build and run the tool that we want to publish the results of! We need a minimum of three projects:Ant-based projectMaven2-based single module projectMaven2-based multiple module projectSo we will develop these three basic projects, get them running the JavaNCSS tool, and then ensure they build in Hudson.The Ant-based projectWe’ll start with the Ant based project. First we’ll need a directory structure:project/ src/ main/ test/ lib/ tools/ javancss/Next we need to download the library dependencies to the lib folder (for example junit.jar) and the custom ant tasks to the appropriate sub-folder of tools.You can get JUnit.jar from http://www.junit.org.The JavaNCSS jars are available at http://www.kclee.de/clemens/java/javancss/ (Note this is the .de domain, not the .com domain). JavaNCSS comes as a zip file, so we’ll just extract the three jar files that we need: ccl.jar, javancss.jar and jhbasic.jarAt this point we should have the followingproject/ src/ main/ test/ lib/ junit.jar tools/ javancss/ ccl.jar javancss.jar jhbasic.jarNext we need a build file, and some sample java files to build. Here is a sample build.xml that will do most of what we want: <pathelement location="${basedir}/tools/javancss/jhbasic.jar" We can then write some simple java classes and test our build. We’ll start with a simple HelloWorld.javapackage com.onedash.hello;/*** A basic hello world application.** @author Stephen Connolly*/public class Hello { public String sayHello(String name) { return “Hello " + name; } public static void main(String[] args) { Hello instance = new Hello(); if (args.length == 1) { System.out.println(instance.sayHello(args[0])); } else { System.out.println(instance.sayHello(“world”)); } }}At this point we should have the following project structure:project/ build.xml src/ main/ com/ onedash/ hello/ Hello.java test/ lib/ junit.jar tools/ javancss/ ccl.jar javancss.jar jhbasic.jarAnd running ANT should give output like:C:\local\project>ant clean distBuildfile: build.xmlclean:[delete] Deleting directory C:\local\project\build[delete] Deleting directory C:\local\project\distcompile: [mkdir] Created dir: C:\local\project\build\classes [javac] Compiling 5 source files to C:\local\project\build\classescompile-tests: [mkdir] Created dir: C:\local\project\build\testcases [javac] Compiling 1 source file to C:\local\project\build\testcasestest: [mkdir] Created dir: C:\local\project\build\testcases\reportsjavancss:[javancss] Generating reportdist: [mkdir] Created dir: C:\local\project\dist [jar] Building jar: C:\local\project\dist\SimpleAntProject.jarBUILD SUCCESSFULTotal time: 3 secondsAnd there should be a javancss-report.xml file in the build directory. That should be enough for an ANT project. Some extra finesse would be to add some more source code so we can examine the structure of the xml report.The Maven2 based single module projectFirst we’ll need a directory structure:project/ src/ main/ java/ test/ java/Next we need a pom.xml to describe the project for Maven2. Here is a simple pom.xml: xsi:schemaLocation=“http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd"> 4.0.0 com.onedash.hudson SimpleM2Project SimpleM2Project 1.0-SNAPSHOT jar junit junit 4.4 test package org.codehaus.mojo javancss-maven-plugin process-sources report We can reuse the Hello.java from the Ant project, so at this point we should have the following directory structure:project/ pom.xml src/ main/ java/ com/ onedash/ hello/ Hello.java test/ java/And running maven should give the following output:C:\local\project>mvn clean package[INFO] Scanning for projects…[INFO] —————————————————————————-[INFO] Building SimpleM2Project[INFO] task-segment: [clean, package][INFO] —————————————————————————-[INFO] [clean:clean][INFO] Deleting directory C:\local\project\target[INFO] Deleting directory C:\local\project\target\classes[INFO] Deleting directory C:\local\project\target\test-classes[INFO] Deleting directory C:\local\project\target\site[INFO] [javancss:report {execution: default}][WARNING] Unable to locate Source XRef to link to - DISABLED[INFO] [resources:resources][INFO] Using default encoding to copy filtered resources.[INFO] [compiler:compile][INFO] Compiling 1 source files to C:\local\project\target\classes[INFO] [resources:testResources][INFO] Using default encoding to copy filtered resources.[INFO] [compiler:testCompile][INFO] Nothing to compile - all classes are up to date[INFO] [surefire:test][INFO] No tests to run.[INFO] [jar:jar][INFO] Building jar: C:\local\project\target\SimpleM2Project-1.0-SNAPSHOT.jar[INFO] ————————————————————————[INFO] BUILD SUCCESSFUL[INFO] ————————————————————————[INFO] Total time: 5 seconds[INFO] Finished at: Sun Jan 13 22:06:20 GMT 2008[INFO] Final Memory: 5M/10M[INFO] ————————————————————————And there should be a javancss-raw-report.xml file in the target directory.The Maven2 based multi-module projectWe can generate this project in its simplest form just by adding the simple project as a child of a project with pom. We’ll start off with the following directory structure:project/ SimpleM2Project pom.xml src/ main/ java/ com/ onedash/ hello/ Hello.java test/ java/Where the SimpleM2Project subtree is a copy of the previous project. All we now need is a pom.xml to nest this project in: 4.0.0 com.onedash.mvn.hudson MultiM2Project MultiM2Project 1.0-SNAPSHOT pom SimpleM2Project And running maven should give the following output:C:\local\project>mvn clean package[INFO] Scanning for projects…[INFO] Reactor build order:[INFO] SimpleM2Project[INFO] MultiM2Project[INFO] —————————————————————————-[INFO] Building SimpleM2Project[INFO] task-segment: [clean, package][INFO] —————————————————————————-[INFO] [clean:clean][INFO] Deleting directory C:\local\project\SimpleM2Project\target[INFO] Deleting directory C:\local\project\SimpleM2Project\target\classes[INFO] Deleting directory C:\local\project\SimpleM2Project\target\test-classes[INFO] Deleting directory C:\local\project\SimpleM2Project\target\site[INFO] [javancss:report {execution: default}][WARNING] Unable to locate Source XRef to link to - DISABLED[INFO] [resources:resources][INFO] Using default encoding to copy filtered resources.[INFO] [compiler:compile][INFO] Compiling 1 source files to C:\local\project\SimpleM2Project\target\classes[INFO] [resources:testResources][INFO] Using default encoding to copy filtered resources.[INFO] [compiler:testCompile][INFO] No sources to compile[INFO] [surefire:test][INFO] No tests to run.[INFO] [jar:jar][INFO] Building jar: C:\local\project\SimpleM2Project\target\SimpleM2Project-1.0-SNAPSHOT.jar[INFO] —————————————————————————-[INFO] Building MultiM2Project[INFO] task-segment: [clean, package][INFO] —————————————————————————-[INFO] [clean:clean][INFO] Deleting directory C:\local\project\target[INFO] Deleting directory C:\local\project\target\classes[INFO] Deleting directory C:\local\project\target\test-classes[INFO] Deleting directory C:\local\project\target\site[INFO] [site:attach-descriptor][INFO][INFO][INFO] ————————————————————————[INFO] Reactor Summary:[INFO] ————————————————————————[INFO] SimpleM2Project ………………………………… SUCCESS [7.240s][INFO] MultiM2Project …………………………………. SUCCESS [2.754s][INFO] ————————————————————————[INFO] ————————————————————————[INFO] BUILD SUCCESSFUL[INFO] ————————————————————————[INFO] Total time: 10 seconds[INFO] Finished at: Sun Jan 13 22:16:37 GMT 2008[INFO] Final Memory: 7M/15M[INFO] ————————————————————————And there should be a javancss-raw-report.xml file in the SimpleM2Project/target directory.The Hudson setupWe’ll start by running mvn hpi:create to create a base to work from:C:\local>mvn hpi:create[INFO] Scanning for projects…[INFO] Searching repository for plugin with prefix: ‘hpi’.[INFO] —————————————————————————-[INFO] Building Maven Default Project[INFO] task-segment: [hpi:create] (aggregator-style)[INFO] —————————————————————————-…[INFO] [hpi:create]Enter the groupId of your plugin: org.jvnet.hudson.plugins[INFO] Defaulting package to group ID: org.jvnet.hudson.pluginsEnter the artifactId of your plugin: javancss…[INFO] ————————————————————————[INFO] BUILD SUCCESSFUL[INFO] ————————————————————————[INFO] Total time: 2 minutes 44 seconds[INFO] Finished at: Sun Jan 13 22:34:58 GMT 2008[INFO] Final Memory: 8M/15M[INFO] ————————————————————————The result should be a sub-directory called javancss with a pom.xml file and a basic Hudson plugin starting point. We’ll edit the pom.xml to update the version of Hudson to the latest (at the time of writing 1.170) and launch a live development instance to set our projects up in.Open up the pom.xml file, and change the 1.153 tags for the hudson-core and hudson-war dependencies to 1.170.The dependency section should now look like this: org.jvnet.hudson.main hudson-core 1.170 provided org.jvnet.hudson.main hudson-war war 1.170 testNext launch a copy of Hudson by running mvn hpi:runC:\local\javancss>mvn hpi:run[INFO] Scanning for projects…[INFO] Searching repository for plugin with prefix: ‘hpi’.[INFO] —————————————————————————-[INFO] Building Unnamed - org.jvnet.hudson.plugins:javancss:hpi:1.0-SNAPSHOT[INFO] task-segment: [hpi:run][INFO] —————————————————————————-[INFO] Preparing hpi:run…[INFO] Started Jetty Server[INFO] Starting scanner at interval of 1 seconds.Now fire up a web browser for http://localhost:8080/We want to create five projects: Freestyle project for the Simple Ant projectFreestyle project for the Single module Maven2 projectFreestyle project for the Multi-module Maven2 projectMaven2 project for the Single module Maven2 projectMaven2 project for the Multi-module Maven2 projectFor extra finesse, we can add a slave executor, and create a second set of these projects tied to the slave. An final extra project that does nothing other than trigger all the other projects to build is the icing on the cake.

    Jenkins Created Sun, 13 Jan 2008 00:00:00 +0000
Next