All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.github.goldin.plugins.jenkins.Job.groovy Maven / Gradle / Ivy

package com.github.goldin.plugins.jenkins

import static com.github.goldin.plugins.common.GMojoUtils.*
import org.apache.maven.plugin.MojoExecutionException
import org.gcontracts.annotations.Requires
import com.github.goldin.plugins.jenkins.beans.*


/**
 * Class describing a Jenkins job
 */
@SuppressWarnings([ 'StatelessClass' ])
class Job
{
   /**
    * Folder names that are illegal on Windows:
    *
    * "Illegal Characters on Various Operating Systems"
    * http://support.grouplogic.com/?p=1607
    * http://msdn.microsoft.com/en-us/library/aa365247%28VS.85%29.aspx
    */
    private static final Set ILLEGAL_NAMES = new HashSet
        ([ 'com1', 'com2', 'com3', 'com4', 'com5', 'com6', 'com7', 'com8', 'com9',
           'lpt1', 'lpt2', 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9',
           'con',  'nul',  'prn' ])

    /**
     * Error messages to display when jobs is not properly configured
     */
    private static String notConfigured( String errorMessage ){ "[${ this }] is not configured correctly: $errorMessage" }
    private static String misConfigured( String errorMessage ){ "[${ this }] is mis-configured: $errorMessage" }

    /**
     * Default Maven goals used in Maven projects and Maven tasks in free-style projects.
     */
    static final String DEFAULT_MAVEN_GOALS = '-B -e clean install'


    /**
     * Job types supported
     */
    static enum JobType
    {
        free  ( 'Free-Style' ),
        maven ( 'Maven'      )

        final String description

        @Requires({ description })
        JobType ( String description )
        {
            this.description = description
        }
    }

    /**
     * "runPostStepsIfResult" types supported
     */
    static enum PostStepResult
    {
        success  ( 'SUCCESS',  0, 'BLUE'   ),
        unstable ( 'UNSTABLE', 1, 'YELLOW' ),
        all      ( 'FAILURE',  2, 'RED'    )

        final String name
        final int    ordinal
        final String color

        @Requires({ name && color })
        PostStepResult( String name, int ordinal, String color )
        {
            this.name    = name
            this.ordinal = ordinal
            this.color   = color
        }
    }

    /**
     * Individual property, not inherited from parent job
     * @see #extend
     */
    String  id             // Job's ID when a folder is created, has illegal characters "fixed"
    String  originalId     // Job's ID used for logging

    void setId( String id )
    {
        assert id?.trim()?.length()

        /**
         * Job Id should never have any illegal characters in it since it becomes a folder name later
         * (in Jenkins workspace - '.jenkins/jobs/JobId' )
         */
        this.originalId = id
        this.id         = fixIllegalChars( id, 'Job id' )
    }

    /**
     * Individual properties, *not inherited* from parent job
     * @see #extend
     */
    boolean              isAbstract  = false
    void                 setAbstract( boolean isAbstract ) { this.isAbstract = isAbstract }
    boolean              disabled    = false
    String               displayName = ''

    /**
    * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    * When adding fields:
    * 1) Update {@link #extend(Job)} where current job is "extended" with the "parent Job"
    * 2) DO NOT specify default values, let {@link #extend(Job)} inheritance take care of it.
    * 3) Update {@link #validate} where job configuration is validated for correctness.
    * ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    */

    JobType              jobType
    Boolean              buildOnSNAPSHOT
    Boolean              useUpdate
    Boolean              doRevert
    Boolean              privateRepository
    Boolean              privateRepositoryPerExecutor
    Boolean              archivingDisabled
    Boolean              blockBuildWhenDownstreamBuilding
    Boolean              blockBuildWhenUpstreamBuilding
    Boolean              appendTasks
    Boolean              incrementalBuild
    String               parent
    String               description
    DescriptionRow[]     descriptionTable
    Integer              daysToKeep
    Integer              numToKeep
    Integer              artifactDaysToKeep
    Integer              artifactNumToKeep
    Task[]               tasks
    Task[]               prebuildersTasks
    Task[]               postbuildersTasks
    String               node
    String               pom
    String               localRepoBase
    String               localRepo
    void                 setLocalRepo ( String localRepo ){ this.localRepo = fixIllegalChars( localRepo, 'Local repo' ) }
    String               localRepoPath
    Artifactory          artifactory
    String               mavenGoals
    String               mavenName
    String               jdkName
    String               mavenOpts
    String               quietPeriod
    String               scmCheckoutRetryCount
    String               gitHubUrl
    Deploy               deploy
    Mail                 mail
    Invoke               invoke
    String               authToken
    PostStepResult       runPostStepsIfResult

    Groovy[]             groovys
    Groovy               groovy
    List         groovys(){ general().list( groovys, groovy )}

    Trigger[]            triggers
    Trigger              trigger
    List        triggers() { general().list( triggers, trigger ) }

    Parameter[]          parameters
    Parameter            parameter
    List      parameters() { general().list( parameters, parameter ) }

    Repository[]         repositories
    Repository           repository
    List     repositories() { general().list( repositories, repository ) }


    String               scmType
    String getScmMarkup()
    {
        if ( repositories() || ( 'none' == scmType ))
        {
            assert scmType
            Scm scm = ( scmType == 'none' ? new None() :
                        scmType == 'cvs'  ? new Cvs()  :
                        scmType == 'svn'  ? new Svn()  :
                        scmType == 'git'  ? new Git()  :
                        scmType == 'hg'   ? new Hg()   :
                                            null )

            assert scm, "Unknown ${ scmType }"

            scm.job          = this
            scm.repositories = repositories()
            scm.markup
        }
        else
        {
            ''
        }
    }


    /**
     * Extension points - tags accepting raw XML content wrapped in 
     */
    String               scm
    String               reporters
    String               publishers
    String               buildWrappers
    String               properties
    String               prebuilders
    String               postbuilders

    /**
     * Groovy extension point
     */
    String               process

    /**
     * Set by {@link JenkinsMojo#configureJobs}
     */
    String               jenkinsUrl
    String               generationPom
    Boolean              parentIsReal
    Job[]                childJobs
    Job[]                invokedBy


   /**
    * Retrieves job description table
    */
    String getDescriptionTableMarkup() { makeTemplate( '/descriptionTable.html', [ job : this, jenkinsUrl : jenkinsUrl ] ) }


    /**
     * Checks that String specifies contains no illegal characters and can be used for creating a folder
     */
    String fixIllegalChars( String s, String title )
    {
        if ( ! s )
        {
            return s
        }

        if ( ILLEGAL_NAMES.contains( s.toLowerCase()))
        {
            throw new MojoExecutionException( "$title [${ id }] is illegal! " +
                                              "It becomes a folder name and the following names are illegal on Windows: ${ ILLEGAL_NAMES.sort() }" )
        }

        /**
         *
         * Leaving only letters/digits, '-', '.' and '_' characters:
         * \w = word character: [a-zA-Z_0-9]
         */
        s.replaceAll( /[^\w\.-]+/, '-' )
    }


    @Override
    String toString () { "Job \"${ originalId }\"" }


    /**
     * Sets properties specified by calling {@link #set} for each one.
     *
     * @param propertyNames name of the properties to set
     * @param parentJob     job to copy property value from if current job has this property undefined
     * @param override      whether current property should be overridden in any case
     */
    private void setMany( List propertyNames,
                          Job          parentJob,
                          boolean      override )
    {
        assert propertyNames && parentJob
        propertyNames.each { set( it, parentJob, override ) }
    }


    /**
     * Sets the property specified using the value of another job or default value.
     *
     * @param propertyName  name of the property to set
     * @param parentJob     job to copy property value from if current job has this property undefined
     * @param override      whether current property should be overridden in any case
     * @param defaultValue  default value to set to the property if other job has it undefined as well,
     *                      if unspecified or null, then {@code ''} for {@code String},
     *                      {@code false} for {@code Boolean} and {@code -1} for {@code Integer} properties are used
     * @param verifyClosure closure to pass the resulting property values, it can verify its correctness
     */
    private void set( String  propertyName,
                      Job     parentJob,
                      boolean override,
                      Object  defaultValue  = null,
                      Closure verifyClosure = null )
    {
        assert propertyName && parentJob

        final propertyType = metaClass.getMetaProperty( propertyName ).type
        // noinspection GroovyAssignmentToMethodParameter
        defaultValue       = ( defaultValue != null    ) ? defaultValue :
                             ( propertyType == String  ) ? ''           :
                             ( propertyType == Boolean ) ? false        :
                             ( propertyType == Integer ) ? -1           :
                                                           null

        assert ( defaultValue != null ), "Default value should be specified for unknown property type [$propertyType]"

        if (( this[ propertyName ] == null ) || override )
        {
            this[ propertyName ] = parentJob[ propertyName ] ?: defaultValue
        }

        assert ( this[ propertyName ] != null ), "[$this] has null [$propertyName]"

        if ( verifyClosure ) { verifyClosure( this[ propertyName ] ) }
    }


    /**
     * Sets job's tasks using property name and parent job specified.
     *
     * @param propertyName  name of the property to set
     * @param parentJob     job to copy tasks from if current job has tasks undefined
     * @param override      whether current job tasks should be overridden in any case
     */
    private void setTasks( String  propertyName,
                           Job     parentJob,
                           boolean override )
    {
        assert ( propertyName && parentJob )

        Task[] parentTasks   = ( Task[] ) ( parentJob[ propertyName ] ?: [] as Task[] )
        Task[] ourTasks      = ( Task[] ) ( this[ propertyName ]      ?: [] as Task[] )
        this[ propertyName ] = ( override    ) ? parentTasks :
                               ( appendTasks ) ? ( parentTasks.toList() + ourTasks.toList()) as Task[] :
                                                 ourTasks ?: parentTasks

        assert ( this[ propertyName ] != null ), "[$this] has null [$propertyName]"
    }


   /**
    * Extends job definition using the parent job specified
    * (completes missing data with that inherited from the parent job)
    *
    * @param parentJob parent job to take the missing data from
    * @param override  whether or not parentJob data is of higher priority than this job data,
    *                  usually it's not - only used when we want to "override" this job data
    */
    @SuppressWarnings( 'AbcComplexity' )
    void extend ( Job parentJob, boolean override = false )
    {
        set( 'description',      parentJob, override, ' ',      { String  s -> assert s } )
        set( 'scmType',          parentJob, override, 'svn',         { String  s -> assert s } )
        set( 'jobType',          parentJob, override, JobType.maven, { JobType t -> assert t } )
        set( 'node',             parentJob, override, 'master',      { String  s -> assert s } )
        set( 'jdkName',          parentJob, override, '(Default)',   { String  s -> assert s } )
        set( 'mail',             parentJob, override, new Mail())
        set( 'invoke',           parentJob, override, new Invoke())
        set( 'descriptionTable', parentJob, override, new DescriptionRow[ 0 ])

        setMany( split( '|authToken|scm|buildWrappers|properties|publishers|process|quietPeriod|scmCheckoutRetryCount|gitHubUrl' +
                        '|useUpdate|doRevert|blockBuildWhenDownstreamBuilding|blockBuildWhenUpstreamBuilding|appendTasks|daysToKeep' +
                        '|numToKeep|artifactDaysToKeep|artifactNumToKeep', '\\|' ),
                 parentJob, override )

        if ((( ! triggers())   || ( override )) && parentJob.triggers())
        {
            triggers = parentJob.triggers() as Trigger[]
        }

        if ((( ! parameters()) || ( override )) && parentJob.parameters())
        {
            parameters = parentJob.parameters() as Parameter[]
        }
        else if ( parentJob.parameters())
        {   /**
             * Set gives a lower priority to parentJob parameters - parameters having the
             * same name and type *are not taken*, see {@link Parameter#equals(Object)}
             */
            parameters = joinParameters( parentJob.parameters(), parameters()) as Parameter[]
        }

        if ((( ! repositories()) || ( override )) && parentJob.repositories())
        {
            repositories = parentJob.repositories() as Repository[]
        }

        if ((( ! groovys()) || ( override )) && parentJob.groovys())
        {
            groovys = parentJob.groovys() as Groovy[]
        }

        if ( jobType == JobType.free )
        {
            setTasks( 'tasks', parentJob, override )
        }

        if ( jobType == JobType.maven )
        {
            set( 'pom',                    parentJob, override, 'pom.xml',           { String s         -> assert s } )
            set( 'mavenGoals',             parentJob, override, DEFAULT_MAVEN_GOALS, { String s         -> assert s } )
            set( 'runPostStepsIfResult',   parentJob, override, PostStepResult.all,  { PostStepResult r -> assert r } )
            set( 'deploy',                 parentJob, override, new Deploy())
            set( 'artifactory',            parentJob, override, new Artifactory())
            setTasks( 'prebuildersTasks',  parentJob, override )
            setTasks( 'postbuildersTasks', parentJob, override )

            setMany( split( '|mavenName|mavenOpts|reporters|localRepoBase|localRepo|buildOnSNAPSHOT' +
                            '|privateRepository|privateRepositoryPerExecutor|archivingDisabled' +
                            '|prebuilders|postbuilders|incrementalBuild', '\\|' ),
                     parentJob, override )

        }
    }


    /**
     * Joins two set of parameters, those inhered from the parent job and those of the current job.
     *
     * @param parentParameters  parameters inherited from the parent job.
     * @param currentParameters parameters of the current job.
     * @return new set of parameters
     */
    private static List joinParameters( List parentParameters, List currentParameters )
    {
        List parentNames  = parentParameters*.name
        List currentNames = currentParameters*.name

        if ( parentNames.intersect( currentNames ))
        {
            parentParameters.findAll { ! currentNames.contains( it.name ) } + currentParameters
        }
        else
        {
            parentParameters + currentParameters
        }
    }


   /**
    * Updates job's {@link #mavenGoals}:
    * - replaces "{...}" with "${...}"
    * - updates "-Dmaven.repo.local" value
    */
    void updateMavenGoals()
    {
        assert jobType == JobType.maven

        /**
         * {..} => ${..}
         */
        mavenGoals = verify().notNullOrEmpty( mavenGoals ).addDollar()

        if ( privateRepository || privateRepositoryPerExecutor )
        {
            assert ( ! ( localRepoBase || localRepo )), "[${this}] has  or  set, " +
                                                        " and  shouldn't be specified"
        }
        else if ( localRepoBase || localRepo )
        {
            /**
             * Adding "-Dmaven.repo.local=/x/y/z" using {@link #localRepoBase} and {@link #localRepo}
             * or replacing it with updated value, if already exists
             */

            localRepoPath = (( localRepoBase ?: "${ constants().USER_HOME }/.m2/repository" ) +
                              '/' +
                             ( localRepo     ?: '.' )).
                            replace( '\\', '/' )

            String localRepoArg = "-Dmaven.repo.local="${ localRepoPath }""
            mavenGoals          = ( mavenGoals.contains( '-Dmaven.repo.local' )) ?
                                      ( mavenGoals.replaceAll( /-Dmaven.repo.local\S+/, localRepoArg )) :
                                      ( mavenGoals +  ' ' + localRepoArg )
        }
    }


    /**
     * Validates job is fully configured.
     *
     * @return validated {@link Job} instance
     */
     @SuppressWarnings( 'AbcComplexity' )
     Job validate ()
     {
         if ( isAbstract ) { return this }

         assert id,                                notConfigured( 'missing ' )
         assert jenkinsUrl,                        notConfigured( 'missing ' )
         assert generationPom,                     notConfigured( 'missing ' )
         assert description,                       notConfigured( 'missing ' )
         assert jobType,                           notConfigured( 'missing ' )
         assert scmType,                           notConfigured( 'missing ' )
         assert node,                              notConfigured( 'missing ' )
         assert jdkName,                           notConfigured( 'missing ' )

         assert ( scm                   != null ), notConfigured( '"scm" is null' )
         assert ( authToken             != null ), notConfigured( '"authToken" is null' )
         assert ( properties            != null ), notConfigured( '"properties" is null' )
         assert ( publishers            != null ), notConfigured( '"publishers" is null' )
         assert ( buildWrappers         != null ), notConfigured( '"buildWrappers" is null' )
         assert ( process               != null ), notConfigured( '"process" is null' )
         assert ( useUpdate             != null ), notConfigured( '"useUpdate" is null' )
         assert ( doRevert              != null ), notConfigured( '"doRevert" is null' )
         assert ( daysToKeep            != null ), notConfigured( '"daysToKeep" is null' )
         assert ( numToKeep             != null ), notConfigured( '"numToKeep" is null' )
         assert ( artifactDaysToKeep    != null ), notConfigured( '"artifactDaysToKeep" is null' )
         assert ( artifactNumToKeep     != null ), notConfigured( '"artifactNumToKeep" is null' )
         assert ( descriptionTable      != null ), notConfigured( '"descriptionTable" is null' )
         assert ( mail                  != null ), notConfigured( '"mail" is null' )
         assert ( invoke                != null ), notConfigured( '"invoke" is null' )
         assert ( quietPeriod           != null ), notConfigured( '"quietPeriod" is null' )
         assert ( scmCheckoutRetryCount != null ), notConfigured( '"scmCheckoutRetryCount" is null' )
         assert ( gitHubUrl             != null ), notConfigured( '"gitHubUrl" is null' )

         assert ( blockBuildWhenDownstreamBuilding != null ), notConfigured( '"blockBuildWhenDownstreamBuilding" is null' )
         assert ( blockBuildWhenUpstreamBuilding   != null ), notConfigured( '"blockBuildWhenUpstreamBuilding" is null' )
         assert ( appendTasks                      != null ), notConfigured( '"appendTasks" is null' )

         validateRepositories()

         if ( jobType == JobType.free )
         {
             assert tasks,                            notConfigured( 'missing ""' )

             assert ! pom,                            misConfigured( ' is not active in free-style jobs' )
             assert ! mavenGoals,                     misConfigured( ' is not active in free-style jobs' )
             assert ! mavenName,                      misConfigured( ' is not active in free-style jobs' )

             assert ( mavenOpts                    == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( buildOnSNAPSHOT              == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( incrementalBuild             == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( privateRepository            == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( privateRepositoryPerExecutor == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( archivingDisabled            == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( reporters                    == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( localRepoBase                == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( localRepo                    == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( deploy                       == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( artifactory                  == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( prebuilders                  == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( postbuilders                 == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( prebuildersTasks             == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( postbuildersTasks            == null ), misConfigured( ' is not active in free-style jobs' )
             assert ( runPostStepsIfResult         == null ), misConfigured( ' is not active in free-style jobs' )
         }
         else if ( jobType == JobType.maven )
         {
             assert ! tasks,                          misConfigured( ' is not active in maven jobs' )

             assert pom,                              notConfigured( 'missing ' )
             assert mavenGoals,                       notConfigured( 'missing ' )
             assert mavenName,                        notConfigured( 'missing ' )

             assert ( mavenOpts                    != null ), notConfigured( '"mavenOpts" is null' )
             assert ( buildOnSNAPSHOT              != null ), notConfigured( '"buildOnSNAPSHOT" is null' )
             assert ( incrementalBuild             != null ), notConfigured( '"incrementalBuild" is null' )
             assert ( privateRepository            != null ), notConfigured( '"privateRepository" is null' )
             assert ( privateRepositoryPerExecutor != null ), notConfigured( '"privateRepositoryPerExecutor" is null' )
             assert ( archivingDisabled            != null ), notConfigured( '"archivingDisabled" is null' )
             assert ( reporters                    != null ), notConfigured( '"reporters" is null' )
             assert ( localRepoBase                != null ), notConfigured( '"localRepoBase" is null' )
             assert ( localRepo                    != null ), notConfigured( '"localRepo" is null' )
             assert ( deploy                       != null ), notConfigured( '"deploy" is null' )
             assert ( artifactory                  != null ), notConfigured( '"artifactory" is null' )
             assert ( prebuilders                  != null ), notConfigured( '"prebuilders" is null' )
             assert ( postbuilders                 != null ), notConfigured( '"postbuilders" is null' )
             assert ( prebuildersTasks             != null ), notConfigured( '"prebuildersTasks" is null' )
             assert ( postbuildersTasks            != null ), notConfigured( '"postbuildersTasks" is null' )
             assert ( runPostStepsIfResult         != null ), notConfigured( '"runPostStepsIfResult" is null' )

             if ( deploy?.url || artifactory?.name )
             {
                 assert ( ! archivingDisabled ), \
                        "[${ this }] has archiving disabled - artifacts deploy to Maven or Artifactory repository can not be used"
             }

             assert ( ! ( privateRepository && privateRepositoryPerExecutor )), \
                    "[${ this }] - both  and  can't be set to \"true\""

             if ( privateRepository || privateRepositoryPerExecutor )
             {
                assert ( ! ( localRepoBase || localRepo || localRepoPath )), \
                        "[${ this }] has  or  specified, " +
                        "no , , or  should be defined"
             }
         }
         else
         {
             throw new IllegalArgumentException ( "Unknown job type [${ jobType }]. " +
                                                  "Known types are \"${ JobType.free.name() }\" and \"${ JobType.maven.name() }\"" )
         }

         this
     }


    /**
     * Validates remote repositories for correctness.
     */
     void validateRepositories ()
     {
         if ( gitHubUrl )
         {
            assert repositories(), "[${ this }]: Missing  or "
         }

         if ( repositories())
         {
            assert scmType, "[${ this }]: Missing "
         }

         for ( repo in repositories().remote )
         {
             int counter = 0

             repositories().remote.each
             {
                 String otherRepo ->

                 if (( repo == otherRepo ) && (( ++counter ) != 1 ))
                 {
                     /**
                      * Repository should only equal to itself once
                      */

                     throw new MojoExecutionException( "[${ this }]: Repo [$repo] is duplicated" )
                 }

                 if (( ! ( repo == otherRepo )) && ( otherRepo.toLowerCase().contains( repo.toLowerCase() + '/' )))
                 {
                     throw new MojoExecutionException(
                         "[${ this }]: Repo [$repo] is duplicated in [$otherRepo] - you should remove [$otherRepo]" )
                 }
             }
         }
     }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy