package org.jvnet.hudson.tools.versionnumber; import hudson.Extension; import hudson.Launcher; import hudson.util.FormValidation; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Result; import hudson.model.Run; import hudson.tasks.BuildWrapper; import hudson.tasks.BuildWrapperDescriptor; import hudson.tasks.Builder; import net.sf.json.JSONObject; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; import java.io.IOException; import java.io.PrintStream; import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; import java.util.Map; /** * Sample {@link Builder}. * * <p> * This build wrapper makes an environment variable with a version number available * to the build. For more information on how the format stream works, see the Version * Number Plugin wiki page. * </p> * <p> * This plugin keeps track of its version through a {@link VersionNumberAction} attached * to the project. Each build that uses this plugin has its own VersionNumberAction, * and this contains the builds today/this month/ this year/ all time. When incrementing * each of these values, unless they're overridden in the configuration the value from * the previous build will be used. * </p> * * @author Carl Lischeske - NETFLIX */ public class VersionNumberBuilder extends BuildWrapper { private static final DateFormat defaultDateFormat = new SimpleDateFormat("yyyy-MM-dd"); private final String versionNumberString; private final Date projectStartDate; private final String environmentVariableName; private int oBuildsToday; private int oBuildsThisMonth; private int oBuildsThisYear; private int oBuildsAllTime; private boolean skipFailedBuilds; @DataBoundConstructor public VersionNumberBuilder(String versionNumberString, String projectStartDate, String environmentVariableName, String buildsToday, String buildsThisMonth, String buildsThisYear, String buildsAllTime, boolean skipFailedBuilds) { this.versionNumberString = versionNumberString; this.projectStartDate = parseDate(projectStartDate); this.environmentVariableName = environmentVariableName; this.skipFailedBuilds = skipFailedBuilds; try { oBuildsToday = Integer.parseInt(buildsToday); } catch (Exception e) { oBuildsToday = -1; } try { oBuildsThisMonth = Integer.parseInt(buildsThisMonth); } catch (Exception e) { oBuildsThisMonth = -1; } try { oBuildsThisYear = Integer.parseInt(buildsThisYear); } catch (Exception e) { oBuildsThisYear = -1; } try { oBuildsAllTime = Integer.parseInt(buildsAllTime); } catch (Exception e) { oBuildsAllTime = -1; } } public String getBuildsToday() { return ""; } public String getBuildsThisMonth() { return ""; } public String getBuildsThisYear() { return ""; } public String getBuildsAllTime() { return ""; } public boolean getSkipFailedBuilds() { return this.skipFailedBuilds; } private static Date parseDate(String dateString) { try { return defaultDateFormat.parse(dateString); } catch (Exception e) { return new Date(0); } } /** * We'll use this from the <tt>config.jelly</tt>. */ public String getVersionNumberString() { return versionNumberString; } public String getProjectStartDate() { return defaultDateFormat.format(projectStartDate); } public String getEnvironmentVariableName() { return this.environmentVariableName; } @SuppressWarnings("unchecked") private VersionNumberBuildInfo incBuild(AbstractBuild build, PrintStream log) throws IOException { Run prevBuild = build.getPreviousBuild(); int buildsToday = 1; int buildsThisMonth = 1; int buildsThisYear = 1; int buildsAllTime = 1; // this is what we add to the previous version number to get builds today/this month/ this year/all time int buildInc = 1; if (prevBuild != null) { // if we're skipping version numbers on failed builds and the last build failed... if (skipFailedBuilds && !prevBuild.getResult().equals(Result.SUCCESS)) { // don't increment buildInc = 0; } // get the current build date and the previous build date Calendar curCal = build.getTimestamp(); Calendar todayCal = prevBuild.getTimestamp(); // get the previous build version number information VersionNumberAction prevAction = (VersionNumberAction)prevBuild.getAction(VersionNumberAction.class); if (prevAction != null) { VersionNumberBuildInfo info = prevAction.getInfo(); // increment builds per day if ( curCal.get(Calendar.DAY_OF_MONTH) == todayCal.get(Calendar.DAY_OF_MONTH) && curCal.get(Calendar.MONTH) == todayCal.get(Calendar.MONTH) && curCal.get(Calendar.YEAR) == todayCal.get(Calendar.YEAR) ) { buildsToday = info.getBuildsToday() + buildInc; } else { buildsToday = 1; } // increment builds per month if ( curCal.get(Calendar.MONTH) == todayCal.get(Calendar.MONTH) && curCal.get(Calendar.YEAR) == todayCal.get(Calendar.YEAR) ) { buildsThisMonth = info.getBuildsThisMonth() + buildInc; } else { buildsThisMonth = 1; } // increment builds per year if ( curCal.get(Calendar.YEAR) == todayCal.get(Calendar.YEAR) ) { buildsThisYear = info.getBuildsThisYear() + buildInc; } else { buildsThisYear = 1; } // increment total builds buildsAllTime = info.getBuildsAllTime() + buildInc; } } // have we overridden any of the version number info? If so, set it up here boolean saveOverrides = false; if (this.oBuildsToday >= 0) { buildsToday = oBuildsToday; oBuildsToday = -1; saveOverrides = true; } if (this.oBuildsThisMonth >= 0) { buildsThisMonth = oBuildsThisMonth; oBuildsThisMonth = -1; saveOverrides = true; } if (this.oBuildsThisYear >= 0) { buildsThisYear = oBuildsThisYear; oBuildsThisYear = -1; saveOverrides = true; } if (this.oBuildsAllTime >= 0) { buildsAllTime = oBuildsAllTime; oBuildsAllTime = -1; saveOverrides = true; } // if we've used any of the overrides, reset them in the project if (saveOverrides) { build.getProject().save(); } return new VersionNumberBuildInfo(buildsToday, buildsThisMonth, buildsThisYear, buildsAllTime); } private static String formatVersionNumber(String versionNumberFormatString, Date projectStartDate, VersionNumberBuildInfo info, Map<String, String> enVars, Calendar buildDate, PrintStream log) { String vnf = new String(versionNumberFormatString); int blockStart = 0; do { // blockStart and blockEnd define the starting and ending positions of the entire block, including // the ${} blockStart = vnf.indexOf("${"); if (blockStart >= 0) { int blockEnd = vnf.indexOf("}", blockStart) + 1; // if this is an unclosed block... if (blockEnd <= blockStart) { // include everything up to the unclosed block, then exit vnf = vnf.substring(0, blockStart); break; } // command start/end include only the actual name of the variable to be replaced int commandStart = blockStart + 2; int commandEnd = blockEnd - 1; int argumentStart = vnf.indexOf(",", blockStart); int argumentEnd = 0; if (argumentStart > 0 && argumentStart < blockEnd) { argumentEnd = blockEnd - 1; commandEnd = argumentStart; } String expressionKey = vnf.substring(commandStart, commandEnd); String argumentString = argumentEnd > 0 ? vnf.substring(argumentStart + 1, argumentEnd).trim() : ""; String replaceValue = ""; // we have the expression key; if it's any known key, fill in the value if ("".equals(expressionKey)) { replaceValue = ""; } else if ("BUILD_DATE_FORMATTED".equals(expressionKey)) { DateFormat fmt = SimpleDateFormat.getInstance(); if (!"".equals(argumentString)) { // this next line is a bit tricky, but basically, we're looking returning everything // inside a pair of quote marks; in other words, everything from after the first quote // to before the second String fmtString = argumentString.substring(argumentString.indexOf('"') + 1, argumentString.indexOf('"', argumentString.indexOf('"') + 1)); fmt = new SimpleDateFormat(fmtString); } replaceValue = fmt.format(buildDate.getTime()); } else if ("BUILD_DAY".equals(expressionKey)) { replaceValue = sizeTo(Integer.toString(buildDate.get(Calendar.DAY_OF_MONTH)), argumentString.length()); } else if ("BUILD_MONTH".equals(expressionKey)) { replaceValue = sizeTo(Integer.toString(buildDate.get(Calendar.MONTH) + 1), argumentString.length()); } else if ("BUILD_YEAR".equals(expressionKey)) { replaceValue = sizeTo(Integer.toString(buildDate.get(Calendar.YEAR)), argumentString.length()); } else if ("BUILDS_TODAY".equals(expressionKey)) { replaceValue = sizeTo(Integer.toString(info.getBuildsToday()), argumentString.length()); } else if ("BUILDS_THIS_MONTH".equals(expressionKey)) { replaceValue = sizeTo(Integer.toString(info.getBuildsThisMonth()), argumentString.length()); } else if ("BUILDS_THIS_YEAR".equals(expressionKey)) { replaceValue = sizeTo(Integer.toString(info.getBuildsThisYear()), argumentString.length()); } else if ("BUILDS_ALL_TIME".equals(expressionKey)) { replaceValue = sizeTo(Integer.toString(info.getBuildsAllTime()), argumentString.length()); } else if ("BUILDS_TODAY_Z".equals(expressionKey)) { replaceValue = sizeTo(Integer.toString(info.getBuildsToday() - 1), argumentString.length()); } else if ("BUILDS_THIS_MONTH_Z".equals(expressionKey)) { replaceValue = sizeTo(Integer.toString(info.getBuildsThisMonth() - 1), argumentString.length()); } else if ("BUILDS_THIS_YEAR_Z".equals(expressionKey)) { replaceValue = sizeTo(Integer.toString(info.getBuildsThisYear() - 1), argumentString.length()); } else if ("BUILDS_ALL_TIME_Z".equals(expressionKey)) { replaceValue = sizeTo(Integer.toString(info.getBuildsAllTime() - 1), argumentString.length()); } else if ("MONTHS_SINCE_PROJECT_START".equals(expressionKey)) { Calendar projectStartCal = Calendar.getInstance(); projectStartCal.setTime(projectStartDate); int monthsSinceStart = buildDate.get(Calendar.MONTH) - projectStartCal.get(Calendar.MONTH); monthsSinceStart += (buildDate.get(Calendar.YEAR) - projectStartCal.get(Calendar.YEAR)) * 12; replaceValue = sizeTo(Integer.toString(monthsSinceStart), argumentString.length()); } else if ("YEARS_SINCE_PROJECT_START".equals(expressionKey)) { Calendar projectStartCal = Calendar.getInstance(); projectStartCal.setTime(projectStartDate); int yearsSinceStart = buildDate.get(Calendar.YEAR) - projectStartCal.get(Calendar.YEAR); replaceValue = sizeTo(Integer.toString(yearsSinceStart), argumentString.length()); } // if it's not one of the defined values, check the environment variables else { for (String enVarKey : enVars.keySet()) { if (enVarKey.equals(expressionKey)) { replaceValue = enVars.get(enVarKey); } } } vnf = vnf.substring(0, blockStart) + replaceValue + vnf.substring(blockEnd, vnf.length()); } } while (blockStart >= 0); return vnf; } private static String sizeTo(String s, int length) { while (s.length() < length) { s = "0" + s; } return s; } @SuppressWarnings("unchecked") @Override public Environment setUp(AbstractBuild build, Launcher launcher, BuildListener listener) { String formattedVersionNumber = ""; try { VersionNumberBuildInfo info = incBuild(build, listener.getLogger()); formattedVersionNumber = formatVersionNumber(this.versionNumberString, this.projectStartDate, info, build.getEnvironment(listener), build.getTimestamp(), listener.getLogger() ); build.addAction(new VersionNumberAction(info, formattedVersionNumber)); } catch (IOException e) { // TODO Auto-generated catch block listener.error(e.toString()); build.setResult(Result.FAILURE); } catch (InterruptedException e) { // TODO Auto-generated catch block listener.error(e.toString()); build.setResult(Result.FAILURE); } catch (Exception e) { listener.error(e.toString()); build.setResult(Result.FAILURE); } final String finalVersionNumber = formattedVersionNumber; return new Environment() { @Override public void buildEnvVars(Map<String, String> env) { env.put(environmentVariableName, finalVersionNumber); } }; } @Override public BuildWrapperDescriptor getDescriptor() { // see Descriptor javadoc for more about what a descriptor is. return (DescriptorImpl)super.getDescriptor(); } /** * Descriptor for {@link VersionNumberBuilder}. Used as a singleton. * The class is marked as public so that it can be accessed from views. */ @Extension public static final class DescriptorImpl extends BuildWrapperDescriptor { public DescriptorImpl() { super(VersionNumberBuilder.class); load(); } /** * Performs on-the-fly validation of the form field 'name'. * * @param value * This receives the current value of the field. */ public FormValidation doCheckEnvironmentVariableName(@QueryParameter final String value) { if(value.length()==0) return FormValidation.error("Please set an environment variable name"); else return FormValidation.ok(); } /** * Performs on-the-fly validation of the form field 'name'. * * @param value * This receives the current value of the field. */ public FormValidation doCheckVersionNumberString(@QueryParameter final String value) { if(value.length()==0) return FormValidation.error("Please set a version number format string. For more information, click on the ?."); else if(value.length()<4) return FormValidation.warning("Isn't the name too short?"); else return FormValidation.ok(); } /** * Performs on-the-fly validation of the form field 'name'. * * @param value * This receives the current value of the field. */ public FormValidation doCheckProjectStartDate(@QueryParameter final String value) { if (value.length() > 0 && parseDate(value).compareTo(new Date(0)) == 0) { return FormValidation.error("Valid dates are in the format yyyy-mm-dd"); } else { return FormValidation.ok(); } } /** * This human readable name is used in the configuration screen. */ public String getDisplayName() { return "Create a formatted version number"; } @Override public boolean configure(StaplerRequest req, JSONObject json) throws FormException { return super.configure(req, json); } @Override public boolean isApplicable(AbstractProject<?, ?> proj) { return true; } } }