mirror of
https://github.com/SAP/jenkins-library.git
synced 2024-12-12 10:55:20 +02:00
8169d56ef7
Co-authored-by: Christopher Fenner <26137398+CCFenner@users.noreply.github.com>
981 lines
34 KiB
Groovy
981 lines
34 KiB
Groovy
import groovy.io.FileType
|
|
import groovy.json.JsonOutput
|
|
import groovy.json.JsonSlurper
|
|
import org.yaml.snakeyaml.Yaml
|
|
import org.codehaus.groovy.control.CompilerConfiguration
|
|
import com.sap.piper.GenerateDocumentation
|
|
import com.sap.piper.GenerateStageDocumentation
|
|
import com.sap.piper.DefaultValueCache
|
|
import java.util.regex.Matcher
|
|
import groovy.text.StreamingTemplateEngine
|
|
|
|
import com.sap.piper.MapUtils
|
|
|
|
//
|
|
// Collects helper functions for rendering the documentation
|
|
//
|
|
class TemplateHelper {
|
|
|
|
static createDependencyList(Set deps) {
|
|
def t = ''
|
|
t += 'The step depends on the following Jenkins plugins\n\n'
|
|
def filteredDeps = deps.findAll { dep -> dep != 'UNIDENTIFIED' }
|
|
|
|
if(filteredDeps.contains('kubernetes')) {
|
|
// The docker plugin is not detected by the tests since it is not
|
|
// handled via step call, but it is added to the environment.
|
|
// Hovever kubernetes plugin and docker plugin are closely related,
|
|
// hence adding docker if kubernetes is present.
|
|
filteredDeps.add('docker')
|
|
}
|
|
|
|
if(filteredDeps.isEmpty()) {
|
|
t += '* <none>\n'
|
|
} else {
|
|
filteredDeps
|
|
.sort()
|
|
.each { dep -> t += "* [${dep}](https://plugins.jenkins.io/${dep})\n" }
|
|
}
|
|
|
|
if(filteredDeps.contains('kubernetes')) {
|
|
t += "\nThe kubernetes plugin is only used if running in a kubernetes environment."
|
|
}
|
|
|
|
t += '''|
|
|
|Transitive dependencies are omitted.
|
|
|
|
|
|The list might be incomplete.
|
|
|
|
|
|Consider using the [ppiper/jenkins-master](https://cloud.docker.com/u/ppiper/repository/docker/ppiper/jenkins-master)
|
|
|docker image. This images comes with preinstalled plugins.
|
|
|'''.stripMargin()
|
|
return t
|
|
}
|
|
|
|
static createParametersTable(Map parameters) {
|
|
|
|
def t = ''
|
|
t += '| name | mandatory | default | possible values |\n'
|
|
t += '|------|-----------|---------|-----------------|\n'
|
|
|
|
parameters.keySet().toSorted().each {
|
|
|
|
def props = parameters.get(it)
|
|
|
|
def defaultValue = isComplexDefault(props.defaultValue) ? renderComplexDefaultValue(props.defaultValue) : renderSimpleDefaultValue(props.defaultValue)
|
|
|
|
t += "| `${it}` | ${props.mandatory ?: props.required ? 'yes' : 'no'} | ${defaultValue} | ${props.value ?: ''} |\n"
|
|
}
|
|
|
|
t
|
|
}
|
|
|
|
private static boolean isComplexDefault(def _default) {
|
|
if(! (_default in Collection)) return false
|
|
if(_default.size() == 0) return false
|
|
for(def entry in _default) {
|
|
if(! (entry in Map)) return false
|
|
if(! entry.dependentParameterKey) return false
|
|
if(! entry.key) return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
private static renderComplexDefaultValue(def _default) {
|
|
_default
|
|
.collect { "${it.dependentParameterKey}=`${it.key ?: '<empty>'}`:`${it.value ?: '<empty>'}`" }
|
|
.join('<br />')
|
|
}
|
|
|
|
private static renderSimpleDefaultValue(def _default) {
|
|
if (_default == null) return ''
|
|
return "`${_default}`"
|
|
}
|
|
|
|
static createParameterDescriptionSection(Map parameters) {
|
|
def t = ''
|
|
parameters.keySet().toSorted().each {
|
|
def props = parameters.get(it)
|
|
t += "* `${it}` - ${props.docu ?: ''}\n"
|
|
}
|
|
|
|
t.trim()
|
|
}
|
|
|
|
static createParametersSection(Map parameters) {
|
|
createParametersTable(parameters) + '\n' + createParameterDescriptionSection(parameters)
|
|
}
|
|
|
|
static createStepConfigurationSection(Map parameters) {
|
|
|
|
def t = '''|We recommend to define values of step parameters via [config.yml file](../configuration.md).
|
|
|
|
|
|In following sections of the config.yml the configuration is possible:\n\n'''.stripMargin()
|
|
|
|
t += '| parameter | general | step/stage |\n'
|
|
t += '|-----------|---------|------------|\n'
|
|
|
|
parameters.keySet().toSorted().each {
|
|
def props = parameters.get(it)
|
|
t += "| `${it}` | ${props.GENERAL_CONFIG ? 'X' : ''} | ${props.STEP_CONFIG ? 'X' : ''} |\n"
|
|
}
|
|
|
|
t.trim()
|
|
}
|
|
|
|
static createStageContentSection(Map stageDescriptions) {
|
|
def t = 'This stage comprises following steps which are activated depending on your use-case/configuration:\n\n'
|
|
|
|
t += '| step | step description |\n'
|
|
t += '| ---- | ---------------- |\n'
|
|
|
|
stageDescriptions.each {step, description ->
|
|
t += "| [${step}](../steps/${step}.md) | ${description.trim()} |\n"
|
|
}
|
|
|
|
return t
|
|
}
|
|
|
|
static createStageActivationSection() {
|
|
def t = '''This stage will be active if any one of the following conditions is met:
|
|
|
|
* Stage configuration in [config.yml file](../configuration.md) contains entries for this stage.
|
|
* Any of the conditions are met which are explained in the section [Step Activation](#step-activation).
|
|
'''
|
|
return t.trim()
|
|
}
|
|
|
|
static createStepActivationSection(Map configConditions) {
|
|
if (!configConditions) return 'For this stage no conditions are assigned to steps.'
|
|
def t = 'Certain steps will be activated automatically depending on following conditions:\n\n'
|
|
|
|
|
|
t += '| step | config key | config value | file pattern |\n'
|
|
t += '| ---- | ---------- | ------------ | ------------ |\n'
|
|
|
|
configConditions?.each {stepName, conditions ->
|
|
t += "| ${stepName} "
|
|
t += "| ${renderValueList(conditions?.configKeys)} "
|
|
t += "| ${renderValueList(mapToValueList(conditions?.config))} "
|
|
|
|
List filePatterns = []
|
|
if (conditions?.filePattern) filePatterns.add(conditions?.filePattern)
|
|
if (conditions?.filePatternFromConfig) filePatterns.add(conditions?.filePatternFromConfig)
|
|
t += "| ${renderValueList(filePatterns)} |\n"
|
|
}
|
|
|
|
t += '''
|
|
!!! info "Step condition details"
|
|
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/ >
|
|
**Important: It will be sufficient that any one condition per step is met.**
|
|
|
|
* `config key`: Checks if a defined configuration parameter is set.
|
|
* `config value`: Checks if a configuration parameter has a defined value.
|
|
* `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.
|
|
|
|
|
|
!!! note "Overruling step activation conditions"
|
|
It is possible to overrule the automatically detected step activation status.<br />
|
|
Just add to your stage configuration `<stepName>: false`, for example `deployToKubernetes: false`.
|
|
|
|
For details about the configuration options, please see [Configuration of Piper](../configuration.md).
|
|
'''
|
|
|
|
return t
|
|
}
|
|
|
|
private static renderValueList(List valueList) {
|
|
if (!valueList) return ''
|
|
if (valueList.size() > 1) {
|
|
List quotedList = []
|
|
valueList.each {listItem ->
|
|
quotedList.add("-`${listItem}`")
|
|
}
|
|
return quotedList.join('<br />')
|
|
} else {
|
|
return "`${valueList[0]}`"
|
|
}
|
|
}
|
|
|
|
private static mapToValueList(Map map) {
|
|
List valueList = []
|
|
map?.each {key, value ->
|
|
if (value instanceof List) {
|
|
value.each {listItem ->
|
|
valueList.add("${key}: ${listItem}")
|
|
}
|
|
} else {
|
|
valueList.add("${key}: ${value}")
|
|
}
|
|
}
|
|
return valueList
|
|
}
|
|
|
|
static createStageConfigurationSection() {
|
|
return 'The stage parameters need to be defined in the section `stages` of [config.yml file](../configuration.md).'
|
|
}
|
|
}
|
|
|
|
//
|
|
// Collects generic helper functions
|
|
//
|
|
class Helper {
|
|
|
|
static projectRoot = new File(Helper.class.protectionDomain.codeSource.location.path).getParentFile().getParentFile().getParentFile()
|
|
|
|
static getConfigHelper(classLoader, roots, script) {
|
|
|
|
def compilerConfig = new CompilerConfiguration()
|
|
compilerConfig.setClasspathList( roots )
|
|
|
|
new GroovyClassLoader(classLoader, compilerConfig, true)
|
|
.parseClass(new File(projectRoot, 'src/com/sap/piper/ConfigurationHelper.groovy'))
|
|
.newInstance(script, [:]).loadStepDefaults()
|
|
}
|
|
|
|
static Map getYamlResource(String resource) {
|
|
def ymlContent = new File(projectRoot,"resources/${resource}").text
|
|
return new Yaml().load(ymlContent)
|
|
}
|
|
|
|
static getDummyScript(def stepName) {
|
|
|
|
def _stepName = stepName
|
|
|
|
return new Script() {
|
|
|
|
def STEP_NAME = _stepName
|
|
def env = [:]
|
|
|
|
def handlePipelineStepErrors(def m, Closure c) {
|
|
c()
|
|
}
|
|
|
|
def libraryResource(def r) {
|
|
new File(projectRoot,"resources/${r}").text
|
|
}
|
|
|
|
def readYaml(def m) {
|
|
new Yaml().load(m.text)
|
|
}
|
|
|
|
void echo(m) {
|
|
println(m)
|
|
}
|
|
|
|
def run() {
|
|
throw new UnsupportedOperationException()
|
|
}
|
|
}
|
|
}
|
|
|
|
static trim(List lines) {
|
|
|
|
removeLeadingEmptyLines(
|
|
removeLeadingEmptyLines(lines.reverse())
|
|
.reverse())
|
|
}
|
|
|
|
private static removeLeadingEmptyLines(lines) {
|
|
|
|
def _lines = new ArrayList(lines), trimmed = []
|
|
|
|
boolean empty = true
|
|
|
|
_lines.each() {
|
|
|
|
if(empty && ! it.trim()) return
|
|
empty = false
|
|
trimmed << it
|
|
}
|
|
|
|
trimmed
|
|
}
|
|
|
|
private static normalize(Set p) {
|
|
|
|
def normalized = [] as Set
|
|
|
|
def interim = [:]
|
|
p.each {
|
|
def parts = it.split('/') as List
|
|
_normalize(parts, interim)
|
|
}
|
|
|
|
interim.each { k, v -> flatten (normalized, k, v) }
|
|
|
|
normalized
|
|
}
|
|
|
|
private static void _normalize(List parts, Map interim) {
|
|
if( parts.size >= 1) {
|
|
if( ! interim[parts.head()]) interim[parts.head()] = [:]
|
|
_normalize(parts.tail(), interim[parts.head()])
|
|
}
|
|
}
|
|
|
|
private static flatten(Set flat, def key, Map interim) {
|
|
|
|
if( ! interim ) flat << (key as String)
|
|
|
|
interim.each { k, v ->
|
|
|
|
def _key = "${key}/${k}"
|
|
|
|
if( v && v.size() > 0 )
|
|
flatten(flat, _key, v)
|
|
else
|
|
flat << (_key as String)
|
|
|
|
}
|
|
}
|
|
|
|
static void scanDocu(File f, Map step) {
|
|
|
|
boolean docu = false,
|
|
value = false,
|
|
mandatory = false,
|
|
parentObject = false,
|
|
docuEnd = false
|
|
|
|
def docuLines = [], valueLines = [], mandatoryLines = [], parentObjectLines = []
|
|
|
|
f.eachLine {
|
|
line ->
|
|
|
|
if(line ==~ /.*dependingOn.*/) {
|
|
def dependentConfigKey = (line =~ /.*dependingOn\('(.*)'\).mixin\('(.*)'/)[0][1]
|
|
def configKey = (line =~ /.*dependingOn\('(.*)'\).mixin\('(.*)'/)[0][2]
|
|
if(! step.dependentConfig[configKey]) {
|
|
step.dependentConfig[configKey] = []
|
|
}
|
|
step.dependentConfig[configKey] << dependentConfigKey
|
|
}
|
|
|
|
if(docuEnd) {
|
|
docuEnd = false
|
|
|
|
if(isHeader(line)) {
|
|
def _docu = []
|
|
docuLines.each { _docu << it }
|
|
_docu = Helper.trim(_docu)
|
|
step.description = _docu.join('\n')
|
|
} else {
|
|
|
|
def param = retrieveParameterName(line)
|
|
|
|
if(!param) {
|
|
throw new RuntimeException("Cannot retrieve parameter for a comment. Affected line was: '${line}'")
|
|
}
|
|
|
|
def _docu = [], _value = [], _mandatory = [], _parentObject = []
|
|
docuLines.each { _docu << it }
|
|
valueLines.each { _value << it }
|
|
mandatoryLines.each { _mandatory << it }
|
|
parentObjectLines.each { _parentObject << it }
|
|
_parentObject << param
|
|
param = _parentObject*.trim().join('/').trim()
|
|
|
|
if(step.parameters[param].docu || step.parameters[param].value)
|
|
System.err << "[WARNING] There is already some documentation for parameter '${param}. Is this parameter documented twice?'\n"
|
|
|
|
step.parameters[param].docu = _docu*.trim().join(' ').trim()
|
|
step.parameters[param].value = _value*.trim().join(' ').trim()
|
|
step.parameters[param].mandatory = _mandatory*.trim().join(' ').trim()
|
|
}
|
|
docuLines.clear()
|
|
valueLines.clear()
|
|
mandatoryLines.clear()
|
|
parentObjectLines.clear()
|
|
}
|
|
|
|
if( line.trim() ==~ /^\/\*\*.*/ ) {
|
|
docu = true
|
|
}
|
|
|
|
if(docu) {
|
|
def _line = line
|
|
_line = _line.replaceAll('^\\s*', '') // leading white spaces
|
|
if(_line.startsWith('/**')) _line = _line.replaceAll('^\\/\\*\\*', '') // start comment
|
|
if(_line.startsWith('*/') || _line.trim().endsWith('*/')) _line = _line.replaceAll('^\\*/', '').replaceAll('\\*/\\s*$', '') // end comment
|
|
if(_line.startsWith('*')) _line = _line.replaceAll('^\\*', '') // continue comment
|
|
if(_line.startsWith(' ')) _line = _line.replaceAll('^\\s', '')
|
|
if(_line ==~ /.*@possibleValues.*/) {
|
|
mandatory = false // should be something like reset attributes
|
|
value = true
|
|
parentObject = false
|
|
}
|
|
// some remark for mandatory e.g. some parameters are only mandatory under certain conditions
|
|
if(_line ==~ /.*@mandatory.*/) {
|
|
value = false // should be something like reset attributes ...
|
|
mandatory = true
|
|
parentObject = false
|
|
}
|
|
// grouping config properties within a parent object for easier readability
|
|
if(_line ==~ /.*@parentConfigKey.*/) {
|
|
value = false // should be something like reset attributes ...
|
|
mandatory = false
|
|
parentObject = true
|
|
}
|
|
|
|
if(value) {
|
|
if(_line) {
|
|
_line = (_line =~ /.*@possibleValues\s*?(.*)/)[0][1]
|
|
valueLines << _line
|
|
}
|
|
}
|
|
|
|
if(mandatory) {
|
|
if(_line) {
|
|
_line = (_line =~ /.*@mandatory\s*?(.*)/)[0][1]
|
|
mandatoryLines << _line
|
|
}
|
|
}
|
|
|
|
if(parentObject) {
|
|
if(_line) {
|
|
_line = (_line =~ /.*@parentConfigKey\s*?(.*)/)[0][1]
|
|
parentObjectLines << _line
|
|
}
|
|
}
|
|
|
|
if(!value && !mandatory && !parentObject) {
|
|
docuLines << _line
|
|
}
|
|
}
|
|
|
|
if(docu && line.trim() ==~ /^.*\*\//) {
|
|
docu = false
|
|
value = false
|
|
mandatory = false
|
|
parentObject = false
|
|
docuEnd = true
|
|
}
|
|
}
|
|
}
|
|
|
|
private static isHeader(line) {
|
|
Matcher headerMatcher = (line =~ /(?:(?:def|void)\s*call\s*\()|(?:@.*)/ )
|
|
return headerMatcher.size() == 1
|
|
}
|
|
|
|
private static retrieveParameterName(line) {
|
|
Matcher m = (line =~ /.*'(.*)'.*/)
|
|
if(m.size() == 1 && m[0].size() == 2)
|
|
return m[0][1]
|
|
return null
|
|
}
|
|
|
|
static getScopedParameters(def script) {
|
|
|
|
def params = [:]
|
|
|
|
params.put('STEP_CONFIG', script.STEP_CONFIG_KEYS ?: [])
|
|
params.put('GENERAL_CONFIG', script.GENERAL_CONFIG_KEYS ?: [] )
|
|
params.put('STAGE_CONFIG', script.PARAMETER_KEYS ?: [] )
|
|
|
|
return params
|
|
}
|
|
|
|
static getStageStepKeys(def script) {
|
|
try {
|
|
return script.STAGE_STEP_KEYS ?: []
|
|
} catch (groovy.lang.MissingPropertyException ex) {
|
|
System.err << "[INFO] STAGE_STEP_KEYS not set for: ${script.STEP_NAME}.\n"
|
|
return []
|
|
}
|
|
}
|
|
|
|
static getRequiredParameters(File f) {
|
|
def params = [] as Set
|
|
f.eachLine {
|
|
line ->
|
|
if (line ==~ /.*withMandatoryProperty\(.*/) {
|
|
def param = (line =~ /.*withMandatoryProperty\('(.*)'/)[0][1]
|
|
params << param
|
|
}
|
|
}
|
|
return params
|
|
}
|
|
|
|
static getParentObjectMappings(File f) {
|
|
def mappings = [:]
|
|
def parentObjectKey = ''
|
|
f.eachLine {
|
|
line ->
|
|
if (line ==~ /.*parentConfigKey.*/ && !parentObjectKey) {
|
|
def param = (line =~ /.*parentConfigKey\s*?(.*)/)[0][1]
|
|
parentObjectKey = param.trim()
|
|
} else if (line ==~ /\s*?(.*)[,]{0,1}/ && parentObjectKey) {
|
|
def pName = retrieveParameterName(line)
|
|
if(pName) {
|
|
mappings.put(pName, parentObjectKey)
|
|
parentObjectKey = ''
|
|
}
|
|
}
|
|
}
|
|
return mappings
|
|
}
|
|
|
|
static resolveDocuRelevantSteps(GroovyScriptEngine gse, File stepsDir) {
|
|
|
|
def docuRelevantSteps = []
|
|
|
|
stepsDir.traverse(type: FileType.FILES, maxDepth: 0) {
|
|
if(it.getName().endsWith('.groovy')) {
|
|
def scriptName = (it =~ /vars\${File.separator}(.*)\.groovy/)[0][1]
|
|
def stepScript = gse.createScript("${scriptName}.groovy", new Binding())
|
|
for (def method in stepScript.getClass().getMethods()) {
|
|
if(method.getName() == 'call' && (method.getAnnotation(GenerateDocumentation) != null || method.getAnnotation(GenerateStageDocumentation) != null)) {
|
|
docuRelevantSteps << scriptName
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
docuRelevantSteps
|
|
}
|
|
|
|
static resolveDocuRelevantStages(GroovyScriptEngine gse, File stepsDir) {
|
|
|
|
def docuRelevantStages = [:]
|
|
|
|
stepsDir.traverse(type: FileType.FILES, maxDepth: 0) {
|
|
if(it.getName().endsWith('.groovy')) {
|
|
def scriptName = (it =~ /vars\${File.separator}(.*)\.groovy/)[0][1]
|
|
def stepScript = gse.createScript("${scriptName}.groovy", new Binding())
|
|
for (def method in stepScript.getClass().getMethods()) {
|
|
GenerateStageDocumentation stageDocsAnnotation = method.getAnnotation(GenerateStageDocumentation)
|
|
if(method.getName() == 'call' && stageDocsAnnotation != null) {
|
|
docuRelevantStages[scriptName] = stageDocsAnnotation.defaultStageName()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
docuRelevantStages
|
|
}
|
|
}
|
|
|
|
roots = [
|
|
new File(Helper.projectRoot, "vars").getAbsolutePath(),
|
|
new File(Helper.projectRoot, "src").getAbsolutePath()
|
|
]
|
|
|
|
stepsDir = null
|
|
stepsDocuDir = null
|
|
stagesDocuDir = null
|
|
customDefaults = null
|
|
|
|
steps = []
|
|
|
|
//
|
|
// assign parameters
|
|
|
|
|
|
def cli = new CliBuilder(
|
|
usage: 'groovy createDocu [<options>]',
|
|
header: 'Options:',
|
|
footer: 'Copyright: SAP SE')
|
|
|
|
cli.with {
|
|
s longOpt: 'stepsDir', args: 1, argName: 'dir', 'The directory containing the steps. Defaults to \'vars\'.'
|
|
d longOpt: 'docuDir', args: 1, argName: 'dir', 'The directory containing the docu stubs. Defaults to \'documentation/docs/steps\'.'
|
|
p longOpt: 'docuDirStages', args: 1, argName: 'dir', 'The directory containing the docu stubs for pipeline stages. Defaults to \'documentation/docs/stages\'.'
|
|
c longOpt: 'customDefaults', args: 1, argName: 'file', 'Additional custom default configuration'
|
|
i longOpt: 'stageInitFile', args: 1, argName: 'file', 'The file containing initialization data for step piperInitRunStageConfiguration'
|
|
h longOpt: 'help', 'Prints this help.'
|
|
}
|
|
|
|
def options = cli.parse(args)
|
|
|
|
if(options.h) {
|
|
System.err << "Printing help.\n"
|
|
cli.usage()
|
|
return
|
|
}
|
|
|
|
if(options.s){
|
|
System.err << "[INFO] Using custom step root: ${options.s}.\n"
|
|
stepsDir = new File(Helper.projectRoot, options.s)
|
|
}
|
|
|
|
|
|
stepsDir = stepsDir ?: new File(Helper.projectRoot, "vars")
|
|
|
|
if(options.d) {
|
|
System.err << "[INFO] Using custom doc dir for steps: ${options.d}.\n"
|
|
stepsDocuDir = new File(Helper.projectRoot, options.d)
|
|
}
|
|
|
|
stepsDocuDir = stepsDocuDir ?: new File(Helper.projectRoot, "documentation/docs/steps")
|
|
|
|
if(options.p) {
|
|
System.err << "[INFO] Using custom doc dir for stages: ${options.p}.\n"
|
|
stagesDocuDir = new File(Helper.projectRoot, options.p)
|
|
}
|
|
|
|
stagesDocuDir = stagesDocuDir ?: new File(Helper.projectRoot, "documentation/docs/stages")
|
|
|
|
if(options.c) {
|
|
System.err << "[INFO] Using custom defaults: ${options.c}.\n"
|
|
customDefaults = options.c
|
|
}
|
|
|
|
// retrieve default conditions for steps
|
|
Map stageConfig
|
|
if (options.i) {
|
|
System.err << "[INFO] Using stageInitFile ${options.i}.\n"
|
|
stageConfig = Helper.getYamlResource(options.i)
|
|
System.err << "[INFO] Default stage configuration: ${stageConfig}.\n"
|
|
}
|
|
|
|
steps.addAll(options.arguments())
|
|
|
|
// assign parameters
|
|
//
|
|
|
|
//
|
|
// sanity checks
|
|
|
|
if( !stepsDocuDir.exists() ) {
|
|
System.err << "Steps docu dir '${stepsDocuDir}' does not exist.\n"
|
|
System.exit(1)
|
|
}
|
|
|
|
if( !stepsDir.exists() ) {
|
|
System.err << "Steps dir '${stepsDir}' does not exist.\n"
|
|
System.exit(1)
|
|
}
|
|
|
|
// sanity checks
|
|
//
|
|
|
|
def gse = new GroovyScriptEngine([ stepsDir.getAbsolutePath() ] as String[], GenerateDocumentation.class.getClassLoader() )
|
|
|
|
//
|
|
// find all the steps we have to document (if no step has been provided from outside)
|
|
if( ! steps) {
|
|
steps = Helper.resolveDocuRelevantSteps(gse, stepsDir)
|
|
} else {
|
|
System.err << "[INFO] Generating docu only for step ${steps.size > 1 ? 's' : ''} ${steps}.\n"
|
|
}
|
|
|
|
// find all the stages that we have to document
|
|
Map stages = Helper.resolveDocuRelevantStages(gse, stepsDir)
|
|
|
|
boolean exceptionCaught = false
|
|
|
|
def stepDescriptors = [:]
|
|
DefaultValueCache.prepare(Helper.getDummyScript('noop'), [customDefaults: customDefaults])
|
|
for (step in steps) {
|
|
try {
|
|
stepDescriptors."${step}" = handleStep(step, gse)
|
|
} catch(Exception e) {
|
|
exceptionCaught = true
|
|
def writer = new StringWriter()
|
|
e.printStackTrace(new PrintWriter(writer))
|
|
System.err << "${e.getClass().getName()} caught while handling step '${step}': ${e.getMessage()}.\n${writer.toString()}\n"
|
|
}
|
|
}
|
|
|
|
// replace @see tag in docu by docu from referenced step.
|
|
for(step in stepDescriptors) {
|
|
if(step.value?.parameters) {
|
|
for(param in step.value.parameters) {
|
|
if( param?.value?.docu?.contains('@see')) {
|
|
def otherStep = param.value.docu.replaceAll('@see', '').trim()
|
|
param.value.docu = fetchTextFrom(otherStep, param.key, stepDescriptors)
|
|
param.value.mandatory = fetchMandatoryFrom(otherStep, param.key, stepDescriptors)
|
|
if(! param.value.value)
|
|
param.value.value = fetchPossibleValuesFrom(otherStep, param.key, stepDescriptors)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//update stepDescriptors: remove stages and put into separate stageDescriptors map
|
|
def stageDescriptors = [:]
|
|
stages.each {key, value ->
|
|
System.err << "[INFO] Processing stage '${key}' ...\n"
|
|
if (stepDescriptors."${key}") {
|
|
stageDescriptors."${key}" = [:] << stepDescriptors."${key}"
|
|
stepDescriptors.remove(key)
|
|
} else {
|
|
stageDescriptors."${key}" = [:]
|
|
}
|
|
|
|
//add stage name to stageDescriptors
|
|
stageDescriptors."${key}".name = value
|
|
|
|
//add stepCondition informmation to stageDescriptors
|
|
stageDescriptors."${key}".configConditions = stageConfig?.stages?.get(value)?.stepConditions
|
|
|
|
//identify step keys in stages
|
|
def stageStepKeys = Helper.getStageStepKeys(gse.createScript( "${key}.groovy", new Binding() ))
|
|
|
|
// prepare step descriptions
|
|
stageDescriptors."${key}".stepDescriptions = [:]
|
|
stageDescriptors."${key}".parameters.each {paramKey, paramValue ->
|
|
|
|
if (paramKey in stageStepKeys) {
|
|
stageDescriptors."${key}".stepDescriptions."${paramKey}" = "${paramValue.docu ?: ''}\n"
|
|
}
|
|
}
|
|
|
|
//remove details from parameter map
|
|
stageStepKeys.each {stepKey ->
|
|
stageDescriptors."${key}".parameters.remove(stepKey)
|
|
}
|
|
|
|
|
|
}
|
|
|
|
for(step in stepDescriptors) {
|
|
try {
|
|
renderStep(step.key, step.value)
|
|
System.err << "[INFO] Step '${step.key}' has been rendered.\n"
|
|
} catch(Exception e) {
|
|
exceptionCaught = true
|
|
System.err << "${e.getClass().getName()} caught while rendering step '${step}': ${e.getMessage()}.\n"
|
|
}
|
|
}
|
|
|
|
for (stage in stageDescriptors) {
|
|
try {
|
|
renderStage(stage.key, stage.value)
|
|
System.err << "[INFO] Stage '${stage.key}' has been rendered.\n"
|
|
} catch(Exception e) {
|
|
exceptionCaught = true
|
|
System.err << "${e.getClass().getName()} caught while rendering stage '${stage}': ${e.getMessage()}.\n"
|
|
}
|
|
}
|
|
|
|
if(exceptionCaught) {
|
|
System.err << "[ERROR] Exception caught during generating documentation. Check earlier log for details.\n"
|
|
System.exit(1)
|
|
}
|
|
|
|
File docuMetaData = new File('target/docuMetaData.json')
|
|
if(docuMetaData.exists()) docuMetaData.delete()
|
|
docuMetaData << new JsonOutput().toJson(stepDescriptors)
|
|
|
|
System.err << "[INFO] done.\n"
|
|
|
|
void renderStep(stepName, stepProperties) {
|
|
|
|
File theStepDocu = new File(stepsDocuDir, "${stepName}.md")
|
|
|
|
if(!theStepDocu.exists()) {
|
|
System.err << "[WARNING] step docu input file for step '${stepName}' is missing.\n"
|
|
return
|
|
}
|
|
|
|
def binding = [
|
|
docGenStepName : stepName,
|
|
docGenDescription : 'Description\n\n' + stepProperties.description,
|
|
docGenParameters : 'Parameters\n\n' + TemplateHelper.createParametersSection(stepProperties.parameters),
|
|
docGenConfiguration : 'Step configuration\n\n' + TemplateHelper.createStepConfigurationSection(stepProperties.parameters),
|
|
docJenkinsPluginDependencies : 'Dependencies\n\n' + TemplateHelper.createDependencyList(stepProperties.dependencies)
|
|
]
|
|
|
|
def template = new StreamingTemplateEngine().createTemplate(theStepDocu.text)
|
|
String text = template.make(binding)
|
|
|
|
theStepDocu.withWriter { w -> w.write text }
|
|
}
|
|
|
|
void renderStage(stageName, stageProperties) {
|
|
|
|
def stageFileName = stageName.indexOf('Stage') != -1 ? stageName.split('Stage')[1].toLowerCase() : stageFileName
|
|
File theStageDocu = new File(stagesDocuDir, "${stageFileName}.md")
|
|
|
|
if(!theStageDocu.exists()) {
|
|
System.err << "[WARNING] stage docu input file for stage '${stageName}' is missing.\n"
|
|
return
|
|
}
|
|
|
|
def binding = [
|
|
docGenStageName : stageProperties.name,
|
|
docGenDescription : stageProperties.description,
|
|
docGenStageContent : 'Stage Content\n\n' + TemplateHelper.createStageContentSection(stageProperties.stepDescriptions),
|
|
docGenStageActivation: 'Stage Activation\n\n' + TemplateHelper.createStageActivationSection(),
|
|
docGenStepActivation: 'Step Activation\n\n' + TemplateHelper.createStepActivationSection(stageProperties.configConditions),
|
|
docGenStageParameters : 'Additional Stage Parameters\n\n' + TemplateHelper.createParametersSection(stageProperties.parameters),
|
|
docGenStageConfiguration : 'Configuration of Additional Stage Parameters\n\n' + TemplateHelper.createStageConfigurationSection()
|
|
]
|
|
def template = new StreamingTemplateEngine().createTemplate(theStageDocu.text)
|
|
String text = template.make(binding)
|
|
|
|
theStageDocu.withWriter { w -> w.write text }
|
|
}
|
|
|
|
def fetchTextFrom(def step, def parameterName, def steps) {
|
|
try {
|
|
def docuFromOtherStep = steps[step]?.parameters[parameterName]?.docu
|
|
if(! docuFromOtherStep) throw new IllegalStateException("No docu found for parameter '${parameterName}' in step ${step}.")
|
|
return docuFromOtherStep
|
|
} catch(e) {
|
|
System.err << "[ERROR] Cannot retrieve docu for parameter ${parameterName} from step ${step}.\n"
|
|
throw e
|
|
}
|
|
}
|
|
|
|
def fetchMandatoryFrom(def step, def parameterName, def steps) {
|
|
try {
|
|
return steps[step]?.parameters[parameterName]?.mandatory
|
|
} catch(e) {
|
|
System.err << "[ERROR] Cannot retrieve docu for parameter ${parameterName} from step ${step}.\n"
|
|
throw e
|
|
}
|
|
}
|
|
|
|
def fetchPossibleValuesFrom(def step, def parameterName, def steps) {
|
|
return steps[step]?.parameters[parameterName]?.value ?: ''
|
|
}
|
|
|
|
def handleStep(stepName, gse) {
|
|
|
|
File theStep = new File(stepsDir, "${stepName}.groovy")
|
|
File theStepDocu = new File(stepsDocuDir, "${stepName}.md")
|
|
File theStepDeps = new File('documentation/jenkins_workspace/plugin_mapping.json')
|
|
|
|
def stageNameFields = stepName.split('Stage')
|
|
if (!theStepDocu.exists() && stepName.indexOf('Stage') != -1 && stageNameFields.size() > 1) {
|
|
//try to get a corresponding stage documentation
|
|
def stageName = stepName.split('Stage')[1].toLowerCase()
|
|
theStepDocu = new File(stagesDocuDir,"${stageName}.md" )
|
|
}
|
|
|
|
if(!theStepDocu.exists()) {
|
|
System.err << "[WARNING] step docu input file for step '${stepName}' is missing.\n"
|
|
return
|
|
}
|
|
|
|
System.err << "[INFO] Handling step '${stepName}'.\n"
|
|
|
|
def defaultConfig = Helper.getConfigHelper(getClass().getClassLoader(),
|
|
roots,
|
|
Helper.getDummyScript(stepName)).use()
|
|
|
|
def params = [] as Set
|
|
|
|
//
|
|
// scopedParameters is a map containing the scope as key and the parameters
|
|
// defined with that scope as a set of strings.
|
|
|
|
def scopedParameters
|
|
|
|
try {
|
|
scopedParameters = Helper.getScopedParameters(gse.createScript( "${stepName}.groovy", new Binding() ))
|
|
scopedParameters.each { k, v -> params.addAll(v) }
|
|
} catch(Exception e) {
|
|
System.err << "[ERROR] Step '${stepName}' violates naming convention for scoped parameters: ${e}.\n"
|
|
throw e
|
|
}
|
|
def requiredParameters = Helper.getRequiredParameters(theStep)
|
|
|
|
params.addAll(requiredParameters)
|
|
|
|
// translate parameter names according to compatibility annotations
|
|
def parentObjectMappings = Helper.getParentObjectMappings(theStep)
|
|
def compatibleParams = [] as Set
|
|
if(parentObjectMappings) {
|
|
params.each {
|
|
if (parentObjectMappings[it])
|
|
compatibleParams.add(parentObjectMappings[it] + '/' + it)
|
|
else
|
|
compatibleParams.add(it)
|
|
}
|
|
if (compatibleParams)
|
|
params = compatibleParams
|
|
}
|
|
|
|
// 'dependentConfig' is only present here for internal reasons and that entry is removed at
|
|
// end of method.
|
|
def step = [
|
|
parameters:[:],
|
|
dependencies: (Set)[],
|
|
dependentConfig: [:]
|
|
]
|
|
|
|
//
|
|
// provide dependencies to Jenkins plugins
|
|
if(theStepDeps.exists()) {
|
|
def pluginDependencies = new JsonSlurper().parse(theStepDeps)
|
|
step.dependencies.addAll(pluginDependencies[stepName].collect { k, v -> k })
|
|
}
|
|
|
|
//
|
|
// START special handling for 'script' parameter
|
|
// ... would be better if there is no special handling required ...
|
|
|
|
step.parameters['script'] = [
|
|
docu: 'The common script environment of the Jenkinsfile running. ' +
|
|
'Typically the reference to the script calling the pipeline ' +
|
|
'step is provided with the `this` parameter, as in `script: this`. ' +
|
|
'This allows the function to access the ' +
|
|
'`commonPipelineEnvironment` for retrieving, e.g. configuration parameters.',
|
|
required: true,
|
|
|
|
GENERAL_CONFIG: false,
|
|
STEP_CONFIG: false
|
|
]
|
|
|
|
// END special handling for 'script' parameter
|
|
|
|
Helper.normalize(params).toSorted().each {
|
|
|
|
it ->
|
|
|
|
def defaultValue = MapUtils.getByPath(defaultConfig, it)
|
|
|
|
def parameterProperties = [
|
|
defaultValue: defaultValue,
|
|
required: requiredParameters.contains((it as String)) && defaultValue == null
|
|
]
|
|
|
|
step.parameters.put(it, parameterProperties)
|
|
|
|
// The scope is only defined for the first level of a hierarchical configuration.
|
|
// If the first part is found, all nested parameters are allowed with that scope.
|
|
def firstPart = it.split('/').head()
|
|
scopedParameters.each { key, val ->
|
|
parameterProperties.put(key, val.contains(firstPart))
|
|
}
|
|
}
|
|
|
|
Helper.scanDocu(theStep, step)
|
|
|
|
step.parameters.each { k, v ->
|
|
if(step.dependentConfig.get(k)) {
|
|
|
|
def dependentParameterKey = step.dependentConfig.get(k)[0]
|
|
def dependentValues = step.parameters.get(dependentParameterKey)?.value
|
|
|
|
if (dependentValues) {
|
|
def the_defaults = []
|
|
dependentValues
|
|
.replaceAll('[\'"` ]', '')
|
|
.split(',').each {possibleValue ->
|
|
if (!possibleValue instanceof Boolean && defaultConfig.get(possibleValue)) {
|
|
the_defaults <<
|
|
[
|
|
dependentParameterKey: dependentParameterKey,
|
|
key: possibleValue,
|
|
value: MapUtils.getByPath(defaultConfig.get(possibleValue), k)
|
|
]
|
|
}
|
|
}
|
|
v.defaultValue = the_defaults
|
|
}
|
|
}
|
|
}
|
|
|
|
//
|
|
// 'dependentConfig' is only present for internal purposes and must not be used outside.
|
|
step.remove('dependentConfig')
|
|
|
|
step
|
|
}
|