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 }