1
0
mirror of https://github.com/SAP/jenkins-library.git synced 2024-12-14 11:03:09 +02:00
sap-jenkins-library/documentation/bin/createDocu.groovy

608 lines
20 KiB
Groovy
Raw Normal View History

import groovy.io.FileType
import org.yaml.snakeyaml.Yaml
import org.codehaus.groovy.control.CompilerConfiguration
import com.sap.piper.GenerateDocumentation
import java.util.regex.Matcher
import groovy.text.StreamingTemplateEngine
//
2019-03-08 13:33:31 +02:00
// Collects helper functions for rendering the documentation
//
class TemplateHelper {
2018-11-05 10:31:47 +02:00
static createParametersTable(Map parameters) {
2018-11-05 10:31:47 +02:00
def t = ''
t += '| name | mandatory | default | possible values |\n'
t += '|------|-----------|---------|-----------------|\n'
2018-11-05 10:31:47 +02:00
parameters.keySet().toSorted().each {
2018-11-05 10:31:47 +02:00
def props = parameters.get(it)
t += "| `${it}` | ${props.mandatory ?: props.required ? 'yes' : 'no'} | ${(props.defaultValue ? '`' + props.defaultValue + '`' : '') } | ${props.value ?: ''} |\n"
2018-11-05 10:31:47 +02:00
}
2018-11-05 10:31:47 +02:00
t
}
2018-11-05 10:31:47 +02:00
static createParameterDescriptionSection(Map parameters) {
def t = ''
parameters.keySet().toSorted().each {
def props = parameters.get(it)
t += "* `${it}` - ${props.docu ?: ''}\n"
}
2019-01-14 10:47:23 +02:00
t.trim()
}
static createParametersSection(Map parameters) {
createParametersTable(parameters) + '\n' + createParameterDescriptionSection(parameters)
}
2018-11-05 10:31:47 +02:00
static createStepConfigurationSection(Map parameters) {
2018-10-29 17:28:37 +02:00
2019-01-14 10:47:23 +02:00
def t = '''|We recommend to define values of step parameters via [config.yml file](../configuration.md).
2018-11-05 10:31:47 +02:00
|
|In following sections of the config.yml the configuration is possible:\n\n'''.stripMargin()
2018-10-29 17:28:37 +02:00
2019-02-13 15:29:05 +02:00
t += '| parameter | general | step/stage |\n'
t += '|-----------|---------|------------|\n'
2018-11-05 10:31:47 +02:00
parameters.keySet().toSorted().each {
def props = parameters.get(it)
2019-02-13 15:29:05 +02:00
t += "| `${it}` | ${props.GENERAL_CONFIG ? 'X' : ''} | ${props.STEP_CONFIG ? 'X' : ''} |\n"
2018-11-05 10:31:47 +02:00
}
2018-10-29 17:28:37 +02:00
2019-01-14 10:47:23 +02:00
t.trim()
2018-10-29 17:28:37 +02:00
}
}
//
// Collects generic helper functions
//
class Helper {
2019-03-08 13:33:31 +02:00
static projectRoot = new File(Helper.class.protectionDomain.codeSource.location.path).getParentFile().getParentFile().getParentFile()
2018-11-05 10:51:28 +02:00
static getConfigHelper(classLoader, roots, script) {
2018-11-05 10:51:28 +02:00
def compilerConfig = new CompilerConfiguration()
compilerConfig.setClasspathList( roots )
2018-11-05 10:51:28 +02:00
new GroovyClassLoader(classLoader, compilerConfig, true)
2019-03-08 13:33:31 +02:00
.parseClass(new File(projectRoot, 'src/com/sap/piper/ConfigurationHelper.groovy'))
2018-11-23 10:15:46 +02:00
.newInstance(script, [:]).loadStepDefaults()
}
2018-11-05 10:51:28 +02:00
static getPrepareDefaultValuesStep(def gse) {
2018-11-05 10:51:28 +02:00
def prepareDefaultValuesStep = gse.createScript('prepareDefaultValues.groovy', new Binding())
2018-11-05 10:51:28 +02:00
prepareDefaultValuesStep.metaClass.handlePipelineStepErrors {
m, c -> c()
}
prepareDefaultValuesStep.metaClass.libraryResource {
2019-03-08 13:33:31 +02:00
f -> new File(projectRoot,"resources/${f}").text
2018-11-05 10:51:28 +02:00
}
prepareDefaultValuesStep.metaClass.readYaml {
m -> new Yaml().load(m.text)
}
2018-11-05 10:51:28 +02:00
prepareDefaultValuesStep
}
2018-11-05 10:51:28 +02:00
static getDummyScript(def prepareDefaultValuesStep, def stepName) {
2018-11-05 10:51:28 +02:00
def _prepareDefaultValuesStep = prepareDefaultValuesStep
def _stepName = stepName
2018-11-05 10:51:28 +02:00
return new Script() {
2018-11-05 10:51:28 +02:00
def STEP_NAME = _stepName
2018-11-05 10:51:28 +02:00
def prepareDefaultValues() {
_prepareDefaultValuesStep()
2018-11-05 10:51:28 +02:00
}
2018-11-05 10:51:28 +02:00
def run() {
throw new UnsupportedOperationException()
}
}
}
2018-11-05 10:51:28 +02:00
static trim(List lines) {
2018-11-05 10:51:28 +02:00
removeLeadingEmptyLines(
removeLeadingEmptyLines(lines.reverse())
.reverse())
}
2018-11-05 10:51:28 +02:00
private static removeLeadingEmptyLines(lines) {
2018-11-05 10:51:28 +02:00
def _lines = new ArrayList(lines), trimmed = []
2018-11-05 10:51:28 +02:00
boolean empty = true
2018-11-05 10:51:28 +02:00
_lines.each() {
2018-11-05 10:51:28 +02:00
if(empty && ! it.trim()) return
empty = false
trimmed << it
}
2018-11-05 10:51:28 +02:00
trimmed
}
2018-11-05 10:51:28 +02:00
private static normalize(Set p) {
2018-11-05 10:51:28 +02:00
def normalized = [] as Set
2018-11-05 10:51:28 +02:00
def interim = [:]
p.each {
def parts = it.split('/') as List
_normalize(parts, interim)
}
2018-11-05 10:51:28 +02:00
interim.each { k, v -> flatten (normalized, k, v) }
2018-11-05 10:51:28 +02:00
normalized
}
2018-11-05 10:51:28 +02:00
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()])
}
}
2018-11-05 10:51:28 +02:00
private static flatten(Set flat, def key, Map interim) {
2018-11-05 10:51:28 +02:00
if( ! interim ) flat << (key as String)
2018-11-05 10:51:28 +02:00
interim.each { k, v ->
2018-11-05 10:51:28 +02:00
def _key = "${key}/${k}"
2018-11-05 10:51:28 +02:00
if( v && v.size() > 0 )
flatten(flat, _key, v)
else
flat << (_key as String)
2018-11-05 10:51:28 +02:00
}
}
2018-11-05 10:51:28 +02:00
static void scanDocu(File f, Map step) {
2018-11-05 10:51:28 +02:00
boolean docu = false,
value = false,
mandatory = false,
parentObject = false,
2018-11-05 10:51:28 +02:00
docuEnd = false
def docuLines = [], valueLines = [], mandatoryLines = [], parentObjectLines = []
2018-11-05 10:51:28 +02:00
f.eachLine {
line ->
2018-11-05 10:51:28 +02:00
if(docuEnd) {
docuEnd = false
if(isHeader(line)) {
def _docu = []
docuLines.each { _docu << it }
_docu = Helper.trim(_docu)
step.description = _docu.join('\n')
2018-11-05 10:51:28 +02:00
} else {
def param = retrieveParameterName(line)
if(!param) {
throw new RuntimeException('Cannot retrieve parameter for a comment')
}
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()
2018-11-05 10:51:28 +02:00
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()
2018-11-05 10:51:28 +02:00
}
docuLines.clear()
valueLines.clear()
mandatoryLines.clear()
parentObjectLines.clear()
2018-11-05 10:51:28 +02:00
}
if( line.trim() ==~ /^\/\*\*.*/ ) {
2018-11-05 10:51:28 +02:00
docu = true
}
2018-11-05 10:51:28 +02:00
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
2018-11-05 10:51:28 +02:00
if(_line.startsWith('*')) _line = _line.replaceAll('^\\*', '') // continue comment
if(_line.startsWith(' ')) _line = _line.replaceAll('^\\s', '')
2018-11-05 10:51:28 +02:00
if(_line ==~ /.*@possibleValues.*/) {
mandatory = false // should be something like reset attributes
2018-11-05 10:51:28 +02:00
value = true
parentObject = false
2018-11-05 10:51:28 +02:00
}
// 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
}
2018-11-05 10:51:28 +02:00
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
2018-11-05 10:51:28 +02:00
}
}
if(docu && line.trim() ==~ /^.*\*\//) {
2018-11-05 10:51:28 +02:00
docu = false
value = false
mandatory = false
parentObject = false
2018-11-05 10:51:28 +02:00
docuEnd = true
}
2018-11-05 10:51:28 +02:00
}
}
2018-11-05 10:51:28 +02:00
private static isHeader(line) {
Matcher headerMatcher = (line =~ /(?:(?:def|void)\s*call\s*\()|(?:@.*)/ )
return headerMatcher.size() == 1
2018-11-05 10:51:28 +02:00
}
2018-11-05 10:51:28 +02:00
private static retrieveParameterName(line) {
Matcher m = (line =~ /.*'(.*)'.*/)
if(m.size() == 1 && m[0].size() == 2)
return m[0][1]
return null
}
2018-11-05 10:51:28 +02:00
static getScopedParameters(def script) {
2018-11-05 10:51:28 +02:00
def params = [:]
2018-11-05 10:51:28 +02:00
params.put('STEP_CONFIG', script.STEP_CONFIG_KEYS ?: [])
params.put('GENERAL_CONFIG', script.GENERAL_CONFIG_KEYS ?: [] )
params.put('STAGE_CONFIG', script.PARAMETER_KEYS ?: [] )
2018-11-05 10:51:28 +02:00
return params
}
2018-11-05 10:51:28 +02:00
static getRequiredParameters(File f) {
def params = [] as Set
f.eachLine {
line ->
if (line ==~ /.*withMandatoryProperty.*/) {
def param = (line =~ /.*withMandatoryProperty\('(.*)'/)[0][1]
params << param
}
2018-11-05 10:51:28 +02:00
}
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
}
2018-11-05 10:51:28 +02:00
static getValue(Map config, def pPath) {
def p =config[pPath.head()]
if(pPath.size() == 1) return p // there is no tail
if(p in Map) getValue(p, pPath.tail())
else return p
}
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) {
docuRelevantSteps << scriptName
break
}
}
}
}
docuRelevantSteps
}
}
roots = [
2019-03-08 13:33:31 +02:00
new File(Helper.projectRoot, "vars").getAbsolutePath(),
new File(Helper.projectRoot, "src").getAbsolutePath()
]
stepsDir = null
stepsDocuDir = null
steps = []
//
// assign parameters
if(args.length >= 1)
2018-11-05 11:04:04 +02:00
stepsDir = new File(args[0])
2019-03-08 13:33:31 +02:00
stepsDir = stepsDir ?: new File(Helper.projectRoot, "vars")
if(args.length >= 2)
2018-11-05 11:04:04 +02:00
stepsDocuDir = new File(args[1])
2019-03-08 13:33:31 +02:00
stepsDocuDir = stepsDocuDir ?: new File(Helper.projectRoot, "documentation/docs/steps")
if(args.length >= 3)
steps = (args as List).drop(2) // the first two entries are stepsDir and docuDir
// the other parts are considered as step names
// assign parameters
//
//
// sanity checks
if( !stepsDocuDir.exists() ) {
2018-11-05 11:04:04 +02:00
System.err << "Steps docu dir '${stepsDocuDir}' does not exist.\n"
System.exit(1)
}
if( !stepsDir.exists() ) {
2018-11-05 11:04:04 +02:00
System.err << "Steps dir '${stepsDir}' does not exist.\n"
System.exit(1)
}
// sanity checks
//
2019-03-08 13:33:31 +02:00
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 {
2018-11-05 11:04:04 +02:00
System.err << "[INFO] Generating docu only for step ${steps.size > 1 ? 's' : ''} ${steps}.\n"
}
def prepareDefaultValuesStep = Helper.getPrepareDefaultValuesStep(gse)
boolean exceptionCaught = false
def stepDescriptors = [:]
for (step in steps) {
2018-11-05 11:04:04 +02:00
try {
stepDescriptors."${step}" = handleStep(step, prepareDefaultValuesStep, gse)
} catch(Exception e) {
exceptionCaught = true
System.err << "${e.getClass().getName()} caught while handling step '${step}': ${e.getMessage()}.\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)
}
}
}
}
for(step in stepDescriptors) {
2018-11-05 11:04:04 +02:00
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"
2018-11-05 11:14:01 +02:00
}
}
if(exceptionCaught) {
2018-11-05 11:04:04 +02:00
System.err << "[ERROR] Exception caught during generating documentation. Check earlier log for details.\n"
System.exit(1)
}
2018-10-29 14:39:37 +02:00
System.err << "[INFO] done.\n"
void renderStep(stepName, stepProperties) {
2018-11-05 11:04:04 +02:00
File theStepDocu = new File(stepsDocuDir, "${stepName}.md")
2018-11-05 11:04:04 +02:00
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)
]
def template = new StreamingTemplateEngine().createTemplate(theStepDocu.text)
String text = template.make(binding)
2018-11-05 11:04:04 +02:00
theStepDocu.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 handleStep(stepName, prepareDefaultValuesStep, gse) {
2018-11-05 11:04:04 +02:00
File theStep = new File(stepsDir, "${stepName}.groovy")
File theStepDocu = new File(stepsDocuDir, "${stepName}.md")
2018-11-05 11:04:04 +02:00
if(!theStepDocu.exists()) {
System.err << "[WARNING] step docu input file for step '${stepName}' is missing.\n"
return
}
2018-11-05 11:04:04 +02:00
System.err << "[INFO] Handling step '${stepName}'.\n"
2018-11-05 11:04:04 +02:00
def defaultConfig = Helper.getConfigHelper(getClass().getClassLoader(),
2018-11-05 11:14:01 +02:00
roots,
Helper.getDummyScript(prepareDefaultValuesStep, stepName)).use()
2018-11-05 11:04:04 +02:00
def params = [] as Set
2018-11-05 11:04:04 +02:00
//
// scopedParameters is a map containing the scope as key and the parameters
// defined with that scope as a set of strings.
2018-11-05 11:04:04 +02:00
def scopedParameters
2018-11-05 11:04:04 +02:00
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)
2018-11-05 11:04:04 +02:00
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
}
2018-11-05 11:04:04 +02:00
def step = [parameters:[:]]
2018-11-05 11:04:04 +02:00
//
// START special handling for 'script' parameter
// ... would be better if there is no special handling required ...
2018-11-05 11:04:04 +02:00
step.parameters['script'] = [
2018-11-05 11:14:01 +02:00
docu: 'The common script environment of the Jenkinsfile running. ' +
'Typically the reference to the script calling the pipeline ' +
2018-11-23 15:27:12 +02:00
'step is provided with the this parameter, as in `script: this`. ' +
2018-11-05 11:14:01 +02:00
'This allows the function to access the ' +
'commonPipelineEnvironment for retrieving, for example, configuration parameters.',
required: true,
2018-10-29 17:28:37 +02:00
GENERAL_CONFIG: false,
2019-02-13 15:29:05 +02:00
STEP_CONFIG: false
2018-11-05 11:16:20 +02:00
]
2018-11-05 11:04:04 +02:00
// END special handling for 'script' parameter
2018-11-05 11:04:04 +02:00
Helper.normalize(params).toSorted().each {
2018-11-05 11:04:04 +02:00
it ->
def defaultValue = Helper.getValue(defaultConfig, it.split('/'))
2018-11-05 11:19:57 +02:00
def parameterProperties = [
defaultValue: defaultValue,
required: requiredParameters.contains((it as String)) && defaultValue == null
2018-11-05 11:19:57 +02:00
]
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))
}
}
2018-11-05 11:04:04 +02:00
Helper.scanDocu(theStep, step)
2018-11-05 11:04:04 +02:00
step
}