github.com/jaylevin/jenkins-library@v1.230.4/documentation/bin/createDocu.groovy (about)

     1  import groovy.io.FileType
     2  import groovy.json.JsonOutput
     3  import groovy.json.JsonSlurper
     4  import org.yaml.snakeyaml.Yaml
     5  import org.codehaus.groovy.control.CompilerConfiguration
     6  import com.sap.piper.GenerateDocumentation
     7  import com.sap.piper.GenerateStageDocumentation
     8  import com.sap.piper.DefaultValueCache
     9  import java.util.regex.Matcher
    10  import groovy.text.StreamingTemplateEngine
    11  
    12  import com.sap.piper.MapUtils
    13  
    14  //
    15  // Collects helper functions for rendering the documentation
    16  //
    17  class TemplateHelper {
    18  
    19      static createDependencyList(Set deps) {
    20          def t = ''
    21          t += 'The step depends on the following Jenkins plugins\n\n'
    22          def filteredDeps = deps.findAll { dep -> dep != 'UNIDENTIFIED' }
    23  
    24          if(filteredDeps.contains('kubernetes')) {
    25              // The docker plugin is not detected by the tests since it is not
    26              // handled via step call, but it is added to the environment.
    27              // Hovever kubernetes plugin and docker plugin are closely related,
    28              // hence adding docker if kubernetes is present.
    29              filteredDeps.add('docker')
    30          }
    31  
    32          if(filteredDeps.isEmpty()) {
    33              t += '* <none>\n'
    34          } else {
    35              filteredDeps
    36                  .sort()
    37                  .each { dep -> t += "* [${dep}](https://plugins.jenkins.io/${dep})\n" }
    38          }
    39  
    40          if(filteredDeps.contains('kubernetes')) {
    41              t += "\nThe kubernetes plugin is only used if running in a kubernetes environment."
    42          }
    43  
    44          t += '''|
    45                  |Transitive dependencies are omitted.
    46                  |
    47                  |The list might be incomplete.
    48                  |
    49                  |Consider using the [ppiper/jenkins-master](https://cloud.docker.com/u/ppiper/repository/docker/ppiper/jenkins-master)
    50                  |docker image. This images comes with preinstalled plugins.
    51                  |'''.stripMargin()
    52          return t
    53      }
    54  
    55      static createParametersTable(Map parameters) {
    56  
    57          def t = ''
    58          t += '| name | mandatory | default | possible values |\n'
    59          t += '|------|-----------|---------|-----------------|\n'
    60  
    61          parameters.keySet().toSorted().each {
    62  
    63              def props = parameters.get(it)
    64  
    65              def defaultValue = isComplexDefault(props.defaultValue) ? renderComplexDefaultValue(props.defaultValue) : renderSimpleDefaultValue(props.defaultValue)
    66  
    67              t +=  "| `${it}` | ${props.mandatory ?: props.required ? 'yes' : 'no'} | ${defaultValue} | ${props.value ?: ''} |\n"
    68          }
    69  
    70          t
    71      }
    72  
    73      private static boolean isComplexDefault(def _default) {
    74          if(! (_default in Collection)) return false
    75          if(_default.size() == 0) return false
    76          for(def entry in _default) {
    77              if(! (entry in Map)) return false
    78              if(! entry.dependentParameterKey) return false
    79              if(! entry.key) return false
    80          }
    81          return true
    82      }
    83  
    84      private static renderComplexDefaultValue(def _default) {
    85          _default
    86              .collect { "${it.dependentParameterKey}=`${it.key ?: '<empty>'}`:`${it.value ?: '<empty>'}`" }
    87              .join('<br />')
    88      }
    89  
    90      private static renderSimpleDefaultValue(def _default) {
    91          if (_default == null) return ''
    92          return "`${_default}`"
    93      }
    94  
    95      static createParameterDescriptionSection(Map parameters) {
    96          def t =  ''
    97          parameters.keySet().toSorted().each {
    98              def props = parameters.get(it)
    99              t += "* `${it}` - ${props.docu ?: ''}\n"
   100          }
   101  
   102          t.trim()
   103      }
   104  
   105      static createParametersSection(Map parameters) {
   106          createParametersTable(parameters) + '\n' + createParameterDescriptionSection(parameters)
   107      }
   108  
   109      static createStepConfigurationSection(Map parameters) {
   110  
   111          def t = '''|We recommend to define values of step parameters via [config.yml file](../configuration.md).
   112                     |
   113                     |In following sections of the config.yml the configuration is possible:\n\n'''.stripMargin()
   114  
   115          t += '| parameter | general | step/stage |\n'
   116          t += '|-----------|---------|------------|\n'
   117  
   118          parameters.keySet().toSorted().each {
   119              def props = parameters.get(it)
   120              t += "| `${it}` | ${props.GENERAL_CONFIG ? 'X' : ''} | ${props.STEP_CONFIG ? 'X' : ''} |\n"
   121          }
   122  
   123          t.trim()
   124      }
   125  
   126      static createStageContentSection(Map stageDescriptions) {
   127          def t = 'This stage comprises following steps which are activated depending on your use-case/configuration:\n\n'
   128  
   129          t += '| step | step description |\n'
   130          t += '| ---- | ---------------- |\n'
   131  
   132          stageDescriptions.each {step, description ->
   133              t += "| [${step}](../steps/${step}.md) | ${description.trim()} |\n"
   134          }
   135  
   136          return t
   137      }
   138  
   139      static createStageActivationSection() {
   140          def t = '''This stage will be active if any one of the following conditions is met:
   141  
   142  * Stage configuration in [config.yml file](../configuration.md) contains entries for this stage.
   143  * Any of the conditions are met which are explained in the section [Step Activation](#step-activation).
   144  '''
   145          return t.trim()
   146      }
   147  
   148      static createStepActivationSection(Map configConditions) {
   149          if (!configConditions) return 'For this stage no conditions are assigned to steps.'
   150          def t = 'Certain steps will be activated automatically depending on following conditions:\n\n'
   151  
   152  
   153          t += '| step | config key | config value | file pattern |\n'
   154          t += '| ---- | ---------- | ------------ | ------------ |\n'
   155  
   156          configConditions?.each {stepName, conditions ->
   157              t += "| ${stepName} "
   158              t += "| ${renderValueList(conditions?.configKeys)} "
   159              t += "| ${renderValueList(mapToValueList(conditions?.config))} "
   160  
   161              List filePatterns = []
   162              if (conditions?.filePattern) filePatterns.add(conditions?.filePattern)
   163              if (conditions?.filePatternFromConfig) filePatterns.add(conditions?.filePatternFromConfig)
   164              t += "| ${renderValueList(filePatterns)} |\n"
   165          }
   166  
   167          t += '''
   168  !!! info "Step condition details"
   169      There are currently several conditions which can be checked.<br /> This is done in the [Init stage](init.md) of the pipeline shortly after checkout of the source code repository.<br/ >
   170      **Important: It will be sufficient that any one condition per step is met.**
   171  
   172      * `config key`: Checks if a defined configuration parameter is set.
   173      * `config value`: Checks if a configuration parameter has a defined value.
   174      * `file pattern`: Checks if files according a defined pattern exist in the project. Either the pattern is speficified direcly or it is retrieved from a configuration parameter.
   175  
   176  
   177  !!! note "Overruling step activation conditions"
   178      It is possible to overrule the automatically detected step activation status.<br />
   179      Just add to your stage configuration `<stepName>: false`, for example `deployToKubernetes: false`.
   180  
   181  For details about the configuration options, please see [Configuration of Piper](../configuration.md).
   182  '''
   183  
   184          return t
   185      }
   186  
   187      private static renderValueList(List valueList) {
   188          if (!valueList) return ''
   189          if (valueList.size() > 1) {
   190              List quotedList = []
   191              valueList.each {listItem ->
   192                  quotedList.add("-`${listItem}`")
   193              }
   194              return quotedList.join('<br />')
   195          } else {
   196              return "`${valueList[0]}`"
   197          }
   198      }
   199  
   200      private static mapToValueList(Map map) {
   201          List valueList = []
   202          map?.each {key, value ->
   203              if (value instanceof List) {
   204                  value.each {listItem ->
   205                      valueList.add("${key}: ${listItem}")
   206                  }
   207              } else {
   208                  valueList.add("${key}: ${value}")
   209              }
   210          }
   211          return valueList
   212      }
   213  
   214      static createStageConfigurationSection() {
   215          return 'The stage parameters need to be defined in the section `stages` of [config.yml file](../configuration.md).'
   216      }
   217  }
   218  
   219  //
   220  // Collects generic helper functions
   221  //
   222  class Helper {
   223  
   224      static projectRoot = new File(Helper.class.protectionDomain.codeSource.location.path).getParentFile().getParentFile().getParentFile()
   225  
   226      static getConfigHelper(classLoader, roots, script) {
   227  
   228          def compilerConfig = new CompilerConfiguration()
   229          compilerConfig.setClasspathList( roots )
   230  
   231          new GroovyClassLoader(classLoader, compilerConfig, true)
   232              .parseClass(new File(projectRoot, 'src/com/sap/piper/ConfigurationHelper.groovy'))
   233              .newInstance(script, [:]).loadStepDefaults()
   234      }
   235  
   236      static Map getYamlResource(String resource) {
   237          def ymlContent = new File(projectRoot,"resources/${resource}").text
   238          return new Yaml().load(ymlContent)
   239      }
   240  
   241      static getDummyScript(def stepName) {
   242  
   243          def _stepName = stepName
   244  
   245          return  new Script() {
   246  
   247              def STEP_NAME = _stepName
   248              def env = [:]
   249  
   250              def handlePipelineStepErrors(def m, Closure c) {
   251                  c()
   252              }
   253  
   254              def libraryResource(def r) {
   255                  new File(projectRoot,"resources/${r}").text
   256              }
   257  
   258              def readYaml(def m) {
   259                  new Yaml().load(m.text)
   260              }
   261  
   262              void echo(m) {
   263                  println(m)
   264              }
   265  
   266              def run() {
   267                  throw new UnsupportedOperationException()
   268              }
   269          }
   270      }
   271  
   272      static trim(List lines) {
   273  
   274          removeLeadingEmptyLines(
   275              removeLeadingEmptyLines(lines.reverse())
   276                  .reverse())
   277      }
   278  
   279      private static removeLeadingEmptyLines(lines) {
   280  
   281          def _lines = new ArrayList(lines), trimmed = []
   282  
   283          boolean empty = true
   284  
   285          _lines.each() {
   286  
   287              if(empty &&  ! it.trim()) return
   288              empty = false
   289              trimmed << it
   290          }
   291  
   292          trimmed
   293      }
   294  
   295      private static normalize(Set p) {
   296  
   297          def normalized = [] as Set
   298  
   299          def interim = [:]
   300          p.each {
   301              def parts = it.split('/') as List
   302              _normalize(parts, interim)
   303          }
   304  
   305          interim.each { k, v -> flatten (normalized, k, v)   }
   306  
   307          normalized
   308      }
   309  
   310      private static void _normalize(List parts, Map interim) {
   311          if( parts.size >= 1) {
   312              if( ! interim[parts.head()]) interim[parts.head()] = [:]
   313              _normalize(parts.tail(), interim[parts.head()])
   314          }
   315      }
   316  
   317      private static flatten(Set flat, def key, Map interim) {
   318  
   319          if( ! interim ) flat << (key as String)
   320  
   321          interim.each { k, v ->
   322  
   323              def _key = "${key}/${k}"
   324  
   325              if( v && v.size() > 0 )
   326                  flatten(flat, _key, v)
   327              else
   328                  flat << (_key as String)
   329  
   330          }
   331      }
   332  
   333      static void scanDocu(File f, Map step) {
   334  
   335          boolean docu = false,
   336                  value = false,
   337                  mandatory = false,
   338                  parentObject = false,
   339                  docuEnd = false
   340  
   341          def docuLines = [], valueLines = [], mandatoryLines = [], parentObjectLines = []
   342  
   343          f.eachLine  {
   344              line ->
   345  
   346                  if(line ==~ /.*dependingOn.*/) {
   347                      def dependentConfigKey = (line =~ /.*dependingOn\('(.*)'\).mixin\('(.*)'/)[0][1]
   348                      def configKey = (line =~ /.*dependingOn\('(.*)'\).mixin\('(.*)'/)[0][2]
   349                      if(! step.dependentConfig[configKey]) {
   350                          step.dependentConfig[configKey] = []
   351                      }
   352                      step.dependentConfig[configKey] << dependentConfigKey
   353                  }
   354  
   355                  if(docuEnd) {
   356                      docuEnd = false
   357  
   358                      if(isHeader(line)) {
   359                          def _docu = []
   360                          docuLines.each { _docu << it  }
   361                          _docu = Helper.trim(_docu)
   362                          step.description = _docu.join('\n')
   363                      } else {
   364  
   365                          def param = retrieveParameterName(line)
   366  
   367                          if(!param) {
   368                              throw new RuntimeException("Cannot retrieve parameter for a comment. Affected line was: '${line}'")
   369                          }
   370  
   371                          def _docu = [], _value = [], _mandatory = [], _parentObject = []
   372                          docuLines.each { _docu << it  }
   373                          valueLines.each { _value << it }
   374                          mandatoryLines.each { _mandatory << it }
   375                          parentObjectLines.each { _parentObject << it }
   376                          _parentObject << param
   377                          param = _parentObject*.trim().join('/').trim()
   378  
   379                          if(step.parameters[param].docu || step.parameters[param].value)
   380                              System.err << "[WARNING] There is already some documentation for parameter '${param}. Is this parameter documented twice?'\n"
   381  
   382                          step.parameters[param].docu = _docu*.trim().join(' ').trim()
   383                          step.parameters[param].value = _value*.trim().join(' ').trim()
   384                          step.parameters[param].mandatory = _mandatory*.trim().join(' ').trim()
   385                      }
   386                      docuLines.clear()
   387                      valueLines.clear()
   388                      mandatoryLines.clear()
   389                      parentObjectLines.clear()
   390                  }
   391  
   392                  if( line.trim()  ==~ /^\/\*\*.*/ ) {
   393                      docu = true
   394                  }
   395  
   396                  if(docu) {
   397                      def _line = line
   398                      _line = _line.replaceAll('^\\s*', '') // leading white spaces
   399                      if(_line.startsWith('/**')) _line = _line.replaceAll('^\\/\\*\\*', '') // start comment
   400                      if(_line.startsWith('*/') || _line.trim().endsWith('*/')) _line = _line.replaceAll('^\\*/', '').replaceAll('\\*/\\s*$', '') // end comment
   401                      if(_line.startsWith('*')) _line = _line.replaceAll('^\\*', '') // continue comment
   402                      if(_line.startsWith(' ')) _line = _line.replaceAll('^\\s', '')
   403                      if(_line ==~ /.*@possibleValues.*/) {
   404                          mandatory = false // should be something like reset attributes
   405                          value = true
   406                          parentObject = false
   407                      }
   408                      // some remark for mandatory e.g. some parameters are only mandatory under certain conditions
   409                      if(_line ==~ /.*@mandatory.*/) {
   410                          value = false // should be something like reset attributes ...
   411                          mandatory = true
   412                          parentObject = false
   413                      }
   414                      // grouping config properties within a parent object for easier readability
   415                      if(_line ==~ /.*@parentConfigKey.*/) {
   416                          value = false // should be something like reset attributes ...
   417                          mandatory = false
   418                          parentObject = true
   419                      }
   420  
   421                      if(value) {
   422                          if(_line) {
   423                              _line = (_line =~ /.*@possibleValues\s*?(.*)/)[0][1]
   424                              valueLines << _line
   425                          }
   426                      }
   427  
   428                      if(mandatory) {
   429                          if(_line) {
   430                              _line = (_line =~ /.*@mandatory\s*?(.*)/)[0][1]
   431                              mandatoryLines << _line
   432                          }
   433                      }
   434  
   435                      if(parentObject) {
   436                          if(_line) {
   437                              _line = (_line =~ /.*@parentConfigKey\s*?(.*)/)[0][1]
   438                              parentObjectLines << _line
   439                          }
   440                      }
   441  
   442                      if(!value && !mandatory && !parentObject) {
   443                          docuLines << _line
   444                      }
   445                  }
   446  
   447                  if(docu && line.trim() ==~ /^.*\*\//) {
   448                      docu = false
   449                      value = false
   450                      mandatory = false
   451                      parentObject = false
   452                      docuEnd = true
   453                  }
   454          }
   455      }
   456  
   457      private static isHeader(line) {
   458          Matcher headerMatcher = (line =~ /(?:(?:def|void)\s*call\s*\()|(?:@.*)/ )
   459          return headerMatcher.size() == 1
   460      }
   461  
   462      private static retrieveParameterName(line) {
   463          Matcher m = (line =~ /.*'(.*)'.*/)
   464          if(m.size() == 1 && m[0].size() == 2)
   465              return m[0][1]
   466          return null
   467      }
   468  
   469      static getScopedParameters(def script) {
   470  
   471          def params = [:]
   472  
   473          params.put('STEP_CONFIG', script.STEP_CONFIG_KEYS ?: [])
   474          params.put('GENERAL_CONFIG', script.GENERAL_CONFIG_KEYS ?: [] )
   475          params.put('STAGE_CONFIG', script.PARAMETER_KEYS ?: [] )
   476  
   477          return params
   478      }
   479  
   480      static getStageStepKeys(def script) {
   481          try {
   482              return script.STAGE_STEP_KEYS ?: []
   483          } catch (groovy.lang.MissingPropertyException ex) {
   484              System.err << "[INFO] STAGE_STEP_KEYS not set for: ${script.STEP_NAME}.\n"
   485              return []
   486          }
   487      }
   488  
   489      static getRequiredParameters(File f) {
   490          def params = [] as Set
   491          f.eachLine  {
   492              line ->
   493                  if (line ==~ /.*withMandatoryProperty\(.*/) {
   494                      def param = (line =~ /.*withMandatoryProperty\('(.*)'/)[0][1]
   495                      params << param
   496                  }
   497          }
   498          return params
   499      }
   500  
   501      static getParentObjectMappings(File f) {
   502          def mappings = [:]
   503          def parentObjectKey = ''
   504          f.eachLine  {
   505              line ->
   506                  if (line ==~ /.*parentConfigKey.*/ && !parentObjectKey) {
   507                      def param = (line =~ /.*parentConfigKey\s*?(.*)/)[0][1]
   508                      parentObjectKey = param.trim()
   509                  } else if (line ==~ /\s*?(.*)[,]{0,1}/ && parentObjectKey) {
   510                      def pName = retrieveParameterName(line)
   511                      if(pName) {
   512                          mappings.put(pName, parentObjectKey)
   513                          parentObjectKey = ''
   514                      }
   515                  }
   516          }
   517          return mappings
   518      }
   519  
   520      static resolveDocuRelevantSteps(GroovyScriptEngine gse, File stepsDir) {
   521  
   522          def docuRelevantSteps = []
   523  
   524          stepsDir.traverse(type: FileType.FILES, maxDepth: 0) {
   525              if(it.getName().endsWith('.groovy')) {
   526                  def scriptName = (it =~  /vars\${File.separator}(.*)\.groovy/)[0][1]
   527                  def stepScript = gse.createScript("${scriptName}.groovy", new Binding())
   528                  for (def method in stepScript.getClass().getMethods()) {
   529                      if(method.getName() == 'call' && (method.getAnnotation(GenerateDocumentation) != null || method.getAnnotation(GenerateStageDocumentation) != null)) {
   530                          docuRelevantSteps << scriptName
   531                          break
   532                      }
   533                  }
   534              }
   535          }
   536          docuRelevantSteps
   537      }
   538  
   539      static resolveDocuRelevantStages(GroovyScriptEngine gse, File stepsDir) {
   540  
   541          def docuRelevantStages = [:]
   542  
   543          stepsDir.traverse(type: FileType.FILES, maxDepth: 0) {
   544              if(it.getName().endsWith('.groovy')) {
   545                  def scriptName = (it =~  /vars\${File.separator}(.*)\.groovy/)[0][1]
   546                  def stepScript = gse.createScript("${scriptName}.groovy", new Binding())
   547                  for (def method in stepScript.getClass().getMethods()) {
   548                      GenerateStageDocumentation stageDocsAnnotation = method.getAnnotation(GenerateStageDocumentation)
   549                      if(method.getName() == 'call' && stageDocsAnnotation != null) {
   550                          docuRelevantStages[scriptName] = stageDocsAnnotation.defaultStageName()
   551                          break
   552                      }
   553                  }
   554              }
   555          }
   556          docuRelevantStages
   557      }
   558  }
   559  
   560  roots = [
   561      new File(Helper.projectRoot, "vars").getAbsolutePath(),
   562      new File(Helper.projectRoot, "src").getAbsolutePath()
   563  ]
   564  
   565  stepsDir = null
   566  stepsDocuDir = null
   567  stagesDocuDir = null
   568  customDefaults = null
   569  
   570  steps = []
   571  
   572  //
   573  // assign parameters
   574  
   575  
   576  def cli = new CliBuilder(
   577      usage: 'groovy createDocu [<options>]',
   578      header: 'Options:',
   579      footer: 'Copyright: SAP SE')
   580  
   581  cli.with {
   582      s longOpt: 'stepsDir', args: 1, argName: 'dir', 'The directory containing the steps. Defaults to \'vars\'.'
   583      d longOpt: 'docuDir', args: 1, argName: 'dir', 'The directory containing the docu stubs. Defaults to \'documentation/docs/steps\'.'
   584      p longOpt: 'docuDirStages', args: 1, argName: 'dir', 'The directory containing the docu stubs for pipeline stages. Defaults to \'documentation/docs/stages\'.'
   585      c longOpt: 'customDefaults', args: 1, argName: 'file', 'Additional custom default configuration'
   586      i longOpt: 'stageInitFile', args: 1, argName: 'file', 'The file containing initialization data for step piperInitRunStageConfiguration'
   587      h longOpt: 'help', 'Prints this help.'
   588  }
   589  
   590  def options = cli.parse(args)
   591  
   592  if(options.h) {
   593      System.err << "Printing help.\n"
   594      cli.usage()
   595      return
   596  }
   597  
   598  if(options.s){
   599      System.err << "[INFO] Using custom step root: ${options.s}.\n"
   600      stepsDir = new File(Helper.projectRoot, options.s)
   601  }
   602  
   603  
   604  stepsDir = stepsDir ?: new File(Helper.projectRoot, "vars")
   605  
   606  if(options.d) {
   607      System.err << "[INFO] Using custom doc dir for steps: ${options.d}.\n"
   608      stepsDocuDir = new File(Helper.projectRoot, options.d)
   609  }
   610  
   611  stepsDocuDir = stepsDocuDir ?: new File(Helper.projectRoot, "documentation/docs/steps")
   612  
   613  if(options.p) {
   614      System.err << "[INFO] Using custom doc dir for stages: ${options.p}.\n"
   615      stagesDocuDir = new File(Helper.projectRoot, options.p)
   616  }
   617  
   618  stagesDocuDir = stagesDocuDir ?: new File(Helper.projectRoot, "documentation/docs/stages")
   619  
   620  if(options.c) {
   621      System.err << "[INFO] Using custom defaults: ${options.c}.\n"
   622      customDefaults = options.c
   623  }
   624  
   625  // retrieve default conditions for steps
   626  Map stageConfig
   627  if (options.i) {
   628      System.err << "[INFO] Using stageInitFile ${options.i}.\n"
   629      stageConfig = Helper.getYamlResource(options.i)
   630      System.err << "[INFO] Default stage configuration: ${stageConfig}.\n"
   631  }
   632  
   633  steps.addAll(options.arguments())
   634  
   635  // assign parameters
   636  //
   637  
   638  //
   639  // sanity checks
   640  
   641  if( !stepsDocuDir.exists() ) {
   642      System.err << "Steps docu dir '${stepsDocuDir}' does not exist.\n"
   643      System.exit(1)
   644  }
   645  
   646  if( !stepsDir.exists() ) {
   647      System.err << "Steps dir '${stepsDir}' does not exist.\n"
   648      System.exit(1)
   649  }
   650  
   651  // sanity checks
   652  //
   653  
   654  def gse = new GroovyScriptEngine([ stepsDir.getAbsolutePath()  ] as String[], GenerateDocumentation.class.getClassLoader() )
   655  
   656  //
   657  // find all the steps we have to document (if no step has been provided from outside)
   658  if( ! steps) {
   659      steps = Helper.resolveDocuRelevantSteps(gse, stepsDir)
   660  } else {
   661      System.err << "[INFO] Generating docu only for step ${steps.size > 1 ? 's' : ''} ${steps}.\n"
   662  }
   663  
   664  // find all the stages that we have to document
   665  Map stages = Helper.resolveDocuRelevantStages(gse, stepsDir)
   666  
   667  boolean exceptionCaught = false
   668  
   669  def stepDescriptors = [:]
   670  DefaultValueCache.prepare(Helper.getDummyScript('noop'), [customDefaults: customDefaults])
   671  for (step in steps) {
   672      try {
   673          stepDescriptors."${step}" = handleStep(step, gse)
   674      } catch(Exception e) {
   675          exceptionCaught = true
   676          def writer = new StringWriter()
   677          e.printStackTrace(new PrintWriter(writer))
   678          System.err << "${e.getClass().getName()} caught while handling step '${step}': ${e.getMessage()}.\n${writer.toString()}\n"
   679      }
   680  }
   681  
   682  // replace @see tag in docu by docu from referenced step.
   683  for(step in stepDescriptors) {
   684      if(step.value?.parameters) {
   685          for(param in step.value.parameters) {
   686              if( param?.value?.docu?.contains('@see')) {
   687                  def otherStep = param.value.docu.replaceAll('@see', '').trim()
   688                  param.value.docu = fetchTextFrom(otherStep, param.key, stepDescriptors)
   689                  param.value.mandatory = fetchMandatoryFrom(otherStep, param.key, stepDescriptors)
   690                  if(! param.value.value)
   691                      param.value.value = fetchPossibleValuesFrom(otherStep, param.key, stepDescriptors)
   692              }
   693          }
   694      }
   695  }
   696  
   697  //update stepDescriptors: remove stages and put into separate stageDescriptors map
   698  def stageDescriptors = [:]
   699  stages.each {key, value ->
   700      System.err << "[INFO] Processing stage '${key}' ...\n"
   701      if (stepDescriptors."${key}") {
   702          stageDescriptors."${key}" = [:] << stepDescriptors."${key}"
   703          stepDescriptors.remove(key)
   704      } else {
   705          stageDescriptors."${key}" = [:]
   706      }
   707  
   708      //add stage name to stageDescriptors
   709      stageDescriptors."${key}".name = value
   710  
   711      //add stepCondition informmation to stageDescriptors
   712      stageDescriptors."${key}".configConditions = stageConfig?.stages?.get(value)?.stepConditions
   713  
   714      //identify step keys in stages
   715      def stageStepKeys = Helper.getStageStepKeys(gse.createScript( "${key}.groovy", new Binding() ))
   716  
   717      // prepare step descriptions
   718      stageDescriptors."${key}".stepDescriptions = [:]
   719      stageDescriptors."${key}".parameters.each {paramKey, paramValue ->
   720  
   721          if (paramKey in stageStepKeys) {
   722              stageDescriptors."${key}".stepDescriptions."${paramKey}" = "${paramValue.docu ?: ''}\n"
   723          }
   724      }
   725  
   726      //remove details from parameter map
   727      stageStepKeys.each {stepKey ->
   728          stageDescriptors."${key}".parameters.remove(stepKey)
   729      }
   730  
   731  
   732  }
   733  
   734  for(step in stepDescriptors) {
   735      try {
   736          renderStep(step.key, step.value)
   737          System.err << "[INFO] Step '${step.key}' has been rendered.\n"
   738      } catch(Exception e) {
   739          exceptionCaught = true
   740          System.err << "${e.getClass().getName()} caught while rendering step '${step}': ${e.getMessage()}.\n"
   741      }
   742  }
   743  
   744  for (stage in stageDescriptors) {
   745      try {
   746          renderStage(stage.key, stage.value)
   747          System.err << "[INFO] Stage '${stage.key}' has been rendered.\n"
   748      } catch(Exception e) {
   749          exceptionCaught = true
   750          System.err << "${e.getClass().getName()} caught while rendering stage '${stage}': ${e.getMessage()}.\n"
   751      }
   752  }
   753  
   754  if(exceptionCaught) {
   755      System.err << "[ERROR] Exception caught during generating documentation. Check earlier log for details.\n"
   756      System.exit(1)
   757  }
   758  
   759  File docuMetaData = new File('target/docuMetaData.json')
   760  if(docuMetaData.exists()) docuMetaData.delete()
   761  docuMetaData << new JsonOutput().toJson(stepDescriptors)
   762  
   763  System.err << "[INFO] done.\n"
   764  
   765  void renderStep(stepName, stepProperties) {
   766  
   767      File theStepDocu = new File(stepsDocuDir, "${stepName}.md")
   768  
   769      if(!theStepDocu.exists()) {
   770          System.err << "[WARNING] step docu input file for step '${stepName}' is missing.\n"
   771          return
   772      }
   773  
   774      def binding = [
   775          docGenStepName      : stepName,
   776          docGenDescription   : 'Description\n\n' + stepProperties.description,
   777          docGenParameters    : 'Parameters\n\n' + TemplateHelper.createParametersSection(stepProperties.parameters),
   778          docGenConfiguration : 'Step configuration\n\n' + TemplateHelper.createStepConfigurationSection(stepProperties.parameters),
   779          docJenkinsPluginDependencies     : 'Dependencies\n\n' + TemplateHelper.createDependencyList(stepProperties.dependencies)
   780      ]
   781  
   782      def template = new StreamingTemplateEngine().createTemplate(theStepDocu.text)
   783      String text = template.make(binding)
   784  
   785      theStepDocu.withWriter { w -> w.write text }
   786  }
   787  
   788  void renderStage(stageName, stageProperties) {
   789  
   790      def stageFileName = stageName.indexOf('Stage') != -1 ? stageName.split('Stage')[1].toLowerCase() : stageFileName
   791      File theStageDocu = new File(stagesDocuDir, "${stageFileName}.md")
   792  
   793      if(!theStageDocu.exists()) {
   794          System.err << "[WARNING] stage docu input file for stage '${stageName}' is missing.\n"
   795          return
   796      }
   797  
   798      def binding = [
   799          docGenStageName     : stageProperties.name,
   800          docGenDescription   : stageProperties.description,
   801          docGenStageContent  : 'Stage Content\n\n' + TemplateHelper.createStageContentSection(stageProperties.stepDescriptions),
   802          docGenStageActivation: 'Stage Activation\n\n' + TemplateHelper.createStageActivationSection(),
   803          docGenStepActivation: 'Step Activation\n\n' + TemplateHelper.createStepActivationSection(stageProperties.configConditions),
   804          docGenStageParameters    : 'Additional Stage Parameters\n\n' + TemplateHelper.createParametersSection(stageProperties.parameters),
   805          docGenStageConfiguration : 'Configuration of Additional Stage Parameters\n\n' + TemplateHelper.createStageConfigurationSection()
   806      ]
   807      def template = new StreamingTemplateEngine().createTemplate(theStageDocu.text)
   808      String text = template.make(binding)
   809  
   810      theStageDocu.withWriter { w -> w.write text }
   811  }
   812  
   813  def fetchTextFrom(def step, def parameterName, def steps) {
   814      try {
   815          def docuFromOtherStep = steps[step]?.parameters[parameterName]?.docu
   816          if(! docuFromOtherStep) throw new IllegalStateException("No docu found for parameter '${parameterName}' in step ${step}.")
   817          return docuFromOtherStep
   818      } catch(e) {
   819          System.err << "[ERROR] Cannot retrieve docu for parameter ${parameterName} from step ${step}.\n"
   820          throw e
   821      }
   822  }
   823  
   824  def fetchMandatoryFrom(def step, def parameterName, def steps) {
   825      try {
   826          return steps[step]?.parameters[parameterName]?.mandatory
   827      } catch(e) {
   828          System.err << "[ERROR] Cannot retrieve docu for parameter ${parameterName} from step ${step}.\n"
   829          throw e
   830      }
   831  }
   832  
   833  def fetchPossibleValuesFrom(def step, def parameterName, def steps) {
   834      return steps[step]?.parameters[parameterName]?.value ?: ''
   835  }
   836  
   837  def handleStep(stepName, gse) {
   838  
   839      File theStep = new File(stepsDir, "${stepName}.groovy")
   840      File theStepDocu = new File(stepsDocuDir, "${stepName}.md")
   841      File theStepDeps = new File('documentation/jenkins_workspace/plugin_mapping.json')
   842  
   843      def stageNameFields = stepName.split('Stage')
   844      if (!theStepDocu.exists() && stepName.indexOf('Stage') != -1 && stageNameFields.size() > 1) {
   845          //try to get a corresponding stage documentation
   846          def stageName = stepName.split('Stage')[1].toLowerCase()
   847          theStepDocu = new File(stagesDocuDir,"${stageName}.md" )
   848      }
   849  
   850      if(!theStepDocu.exists()) {
   851          System.err << "[WARNING] step docu input file for step '${stepName}' is missing.\n"
   852          return
   853      }
   854  
   855      System.err << "[INFO] Handling step '${stepName}'.\n"
   856  
   857      def defaultConfig = Helper.getConfigHelper(getClass().getClassLoader(),
   858          roots,
   859          Helper.getDummyScript(stepName)).use()
   860  
   861      def params = [] as Set
   862  
   863      //
   864      // scopedParameters is a map containing the scope as key and the parameters
   865      // defined with that scope as a set of strings.
   866  
   867      def scopedParameters
   868  
   869      try {
   870          scopedParameters = Helper.getScopedParameters(gse.createScript( "${stepName}.groovy", new Binding() ))
   871          scopedParameters.each { k, v -> params.addAll(v) }
   872      } catch(Exception e) {
   873          System.err << "[ERROR] Step '${stepName}' violates naming convention for scoped parameters: ${e}.\n"
   874          throw e
   875      }
   876      def requiredParameters = Helper.getRequiredParameters(theStep)
   877  
   878      params.addAll(requiredParameters)
   879  
   880      // translate parameter names according to compatibility annotations
   881      def parentObjectMappings = Helper.getParentObjectMappings(theStep)
   882      def compatibleParams = [] as Set
   883      if(parentObjectMappings) {
   884          params.each {
   885              if (parentObjectMappings[it])
   886                  compatibleParams.add(parentObjectMappings[it] + '/' + it)
   887              else
   888                  compatibleParams.add(it)
   889          }
   890          if (compatibleParams)
   891              params = compatibleParams
   892      }
   893  
   894      // 'dependentConfig' is only present here for internal reasons and that entry is removed at
   895      // end of method.
   896      def step = [
   897          parameters:[:],
   898          dependencies: (Set)[],
   899          dependentConfig: [:]
   900      ]
   901  
   902      //
   903      // provide dependencies to Jenkins plugins
   904      if(theStepDeps.exists()) {
   905          def pluginDependencies = new JsonSlurper().parse(theStepDeps)
   906          step.dependencies.addAll(pluginDependencies[stepName].collect { k, v -> k })
   907      }
   908  
   909      //
   910      // START special handling for 'script' parameter
   911      // ... would be better if there is no special handling required ...
   912  
   913      step.parameters['script'] = [
   914          docu: 'The common script environment of the Jenkinsfile running. ' +
   915              'Typically the reference to the script calling the pipeline ' +
   916              'step is provided with the `this` parameter, as in `script: this`. ' +
   917              'This allows the function to access the ' +
   918              '`commonPipelineEnvironment` for retrieving, e.g. configuration parameters.',
   919          required: true,
   920  
   921          GENERAL_CONFIG: false,
   922          STEP_CONFIG: false
   923      ]
   924  
   925      // END special handling for 'script' parameter
   926  
   927      Helper.normalize(params).toSorted().each {
   928  
   929          it ->
   930  
   931              def defaultValue = MapUtils.getByPath(defaultConfig, it)
   932  
   933              def parameterProperties =   [
   934                  defaultValue: defaultValue,
   935                  required: requiredParameters.contains((it as String)) && defaultValue == null
   936              ]
   937  
   938              step.parameters.put(it, parameterProperties)
   939  
   940              // The scope is only defined for the first level of a hierarchical configuration.
   941              // If the first part is found, all nested parameters are allowed with that scope.
   942              def firstPart = it.split('/').head()
   943              scopedParameters.each { key, val ->
   944                  parameterProperties.put(key, val.contains(firstPart))
   945              }
   946      }
   947  
   948      Helper.scanDocu(theStep, step)
   949  
   950      step.parameters.each { k, v ->
   951          if(step.dependentConfig.get(k)) {
   952  
   953              def dependentParameterKey = step.dependentConfig.get(k)[0]
   954              def dependentValues = step.parameters.get(dependentParameterKey)?.value
   955  
   956              if (dependentValues) {
   957                  def the_defaults = []
   958                  dependentValues
   959                      .replaceAll('[\'"` ]', '')
   960                      .split(',').each {possibleValue ->
   961                      if (!possibleValue instanceof Boolean && defaultConfig.get(possibleValue)) {
   962                          the_defaults <<
   963                              [
   964                                  dependentParameterKey: dependentParameterKey,
   965                                  key: possibleValue,
   966                                  value: MapUtils.getByPath(defaultConfig.get(possibleValue), k)
   967                              ]
   968                      }
   969                  }
   970                  v.defaultValue = the_defaults
   971              }
   972          }
   973      }
   974  
   975      //
   976      // 'dependentConfig' is only present for internal purposes and must not be used outside.
   977      step.remove('dependentConfig')
   978  
   979      step
   980  }