1
0
mirror of https://github.com/firstBitMarksistskaya/jenkins-lib.git synced 2024-12-13 11:27:08 +02:00

Merge branch 'develop'

This commit is contained in:
Nikita Gryzlov 2020-04-30 17:25:25 +03:00
commit fb6f0e14bf
No known key found for this signature in database
GPG Key ID: C1EAE411FEF0BF2F
26 changed files with 510 additions and 49 deletions

View File

@ -5,7 +5,7 @@
Создание библиотеки (или плагина) для Jenkins, позволяющей:
* максимально упростить написание Jenkinsfile для процесса CI в условиях платформы 1С:Предприятие 8
* иметь схожий и контроллируемый пайплайн для всех проектов
* иметь схожий и контролируемый пайплайн для всех проектов
* дать пользователю в руки простой декларативный конфигурационный файл, вместо требования описывать всю сложную логику по работе с 1С
## Общие положения
@ -21,11 +21,21 @@
1. Для шага подготовки требуется любой агент с меткой `agent`.
1. Для запуска шага анализа SonarQube требуется агент с меткой `sonar`.
1. Для запуска шага валидации EDT требуется агент с меткой `edt` (для собственно валидации) и агент с меткой `oscript` (для трансформации результатов с помощью библиотеки [stebi](https://github.com/Stepa86/stebi)).
1. Для запуска шагов, работающих с 1С (подготовка, синтаксический контроль и т.д.) требуется агент с меткой, совпадающей со значением в поле `v8version` файла конфигурации.
1. В качестве ИБ используется файловая база, создаваемая в `./build/ib`, без данных авторизации. Переопределение "в следующих сериях".
1. Stage "Дымовые тесты" пока пустой.
1. Запуск `vrunner` на текущий момент происходит из локального каталога `oscript_modules`. Предполагается наличие в корне репозитория файла `packagedef`, в котором бы была указана зависимость от `vanessa-runner`
## Возможности
1. Все шаги можно запустить на базе docker-образов из форка репозитория onec-docker. См. [памятку по слоям и последовательности сборки](https://github.com/firstBitSemenovskaya/onec-docker/blob/feature/first-bit/Layers.md)
1. Трансформация кода из формата конфигуратора в формат EDT (только если включен шаг `edtValidate`).
1. Подготовка информационной базы по версии из хранилища конфигурации.
1. Запуск синтаксического контроля средствами конфигуратора и сохранение результатов в виде отчета jUnit.
1. Запуск валидации проекта средствами EDT и конвертация отчета в формате generic issues.
1. Запуск статического анализа для SonarQube
## Подключение
Инструкция по подключению библиотеки: https://jenkins.io/doc/book/pipeline/shared-libraries/#using-libraries
@ -48,7 +58,7 @@ pipeline1C()
> Да, вот и весь пайплайн. Конфигурирование через json.
## Конфигуирование
## Конфигурирование
По умолчанию применяется [файл конфигурации из ресурсов библиотеки](resources/globalConfiguration.json)
@ -57,11 +67,12 @@ pipeline1C()
Пример переопределения:
* указывается точная версия платформы (и соответственно метка агента, см. ограничения)
* идентификаторы credentials для пути к хранилищу и к паре логин/пароль для авторизации в хранилище
* включаются шаги запуска статического анализа и синтаксического контроля
* идентификаторы credentials для пути к хранилищу и к паре логин/пароль для авторизации в хранилище (необходимы, если применяются шаги, работающие с информационной базой)
* включаются шаги запуска статического анализа SonarQube, валидации средствами EDT и синтаксического контроля
```json
{
"$schema": "https://raw.githubusercontent.com/firstBitSemenovskaya/jenkins-lib/master/resources/schema.json",
"v8version": "8.3.14.1976",
"secrets": {
"storagePath": "f7b21c02-711a-4883-81c5-d429454e3f8b",
@ -69,6 +80,7 @@ pipeline1C()
},
"stages": {
"sonarqube": true,
"edtValidation": true,
"syntaxCheck": true
}
}

View File

@ -1,12 +1,15 @@
{
"$schema": "schema.json",
"srcDir": "src/cf",
"secrets": {
"storagePath": "UNKNOWN_ID",
"storage": "UNKNOWN_ID"
},
"stages": {
"sonarqube": false,
"syntaxCheck": false
"syntaxCheck": false,
"edtValidate": false,
"smoke": false
},
"sonarqube": {
"sonarQubeInstallation": "",
@ -29,5 +32,9 @@
"-CheckUseSynchronousCalls",
"-DistributiveModules"
]
},
"resultsTransform": {
"removeSupport": false,
"supportLevel": 0
}
}

View File

@ -6,6 +6,10 @@
"type" : "string",
"description" : "Версия платформы 1С:Предприятие в формате 8.3.хх.хххх."
},
"srcDir" : {
"type" : "string",
"description" : "Путь к корневому каталогу с исходниками конфигурации"
},
"secrets" : {
"type" : "object",
"id" : "urn:jsonschema:ru:pulsar:jenkins:library:configuration:Secrets",
@ -33,6 +37,14 @@
"syntaxCheck" : {
"type" : "boolean",
"description" : "Синтаксический контроль включен"
},
"edtValidate" : {
"type" : "boolean",
"description" : "Валидация EDT включена"
},
"smoke" : {
"type" : "boolean",
"description" : "Дымовые тесты включены"
}
}
},
@ -76,6 +88,21 @@
}
}
}
},
"resultsTransform" : {
"type" : "object",
"id" : "urn:jsonschema:ru:pulsar:jenkins:library:configuration:ResultsTransformOptions",
"description" : "Настройки трансформации результатов анализа",
"properties" : {
"removeSupport" : {
"type" : "boolean",
"description" : "Фильтровать замечания по уровню поддержки модуля"
},
"supportLevel" : {
"type" : "integer",
"description" : "Настройка фильтрации замечаний по уровню поддержки.\n 0 - удалить файлы на замке;\n 1 - удалить файлы на замке и на поддержке;\n 2 - удалить файлы на замке, на поддержке и снятые с поддержки.\n "
}
}
}
}
}

View File

@ -16,13 +16,33 @@ interface IStepExecutor {
void echo(message)
void cmd(String script, boolean returnStatus)
int cmd(String script, boolean returnStatus)
void cmd(String script)
int cmd(String script)
void tool(String toolName)
void withSonarQubeEnv(String installationName, Closure body)
EnvironmentAction env()
void createDir(String path)
def withEnv(List<String> strings, Closure body)
def archiveArtifacts(String path)
def stash(String name, String includes)
def unstash(String name)
def zip(String dir, String zipFile)
def zip(String dir, String zipFile, String glob)
def unzip(String dir, String zipFile)
def unzip(String dir, String zipFile, quiet)
def catchError(Closure body)
}

View File

@ -41,8 +41,8 @@ class StepExecutor implements IStepExecutor {
}
@Override
void cmd(String script, boolean returnStatus = false) {
steps.cmd(script, returnStatus)
int cmd(String script, boolean returnStatus = false) {
return steps.cmd(script, returnStatus)
}
@Override
@ -61,4 +61,46 @@ class StepExecutor implements IStepExecutor {
EnvironmentAction env() {
return steps.env
}
@Override
void createDir(String path) {
steps.createDir(path)
}
@Override
def withEnv(List<String> strings, Closure body) {
steps.withEnv(strings) {
body()
}
}
@Override
def archiveArtifacts(String path) {
steps.archiveArtifacts path
}
@Override
def stash(String name, String includes) {
steps.stash name: name, includes: includes
}
@Override
def unstash(String name) {
steps.unstash name
}
@Override
def zip(String dir, String zipFile, String glob = '') {
steps.zip dir: dir, zipFile: zipFile, glob: glob
}
@Override
def unzip(String dir, String zipFile, quiet = true) {
steps.unzip dir: dir, zipFile: zipFile, quiet: quiet
}
@Override
def catchError(Closure body) {
steps.catchError body
}
}

View File

@ -42,7 +42,8 @@ class ConfigurationReader implements Serializable {
"secrets",
"stageFlags",
"sonarQubeOptions",
"syntaxCheckOptions"
"syntaxCheckOptions",
"resultsTransformOptions"
).toSet()
mergeObjects(baseConfiguration, configurationToMerge, nonMergeableSettings)

View File

@ -10,6 +10,9 @@ class JobConfiguration implements Serializable {
@JsonPropertyDescription("Версия платформы 1С:Предприятие в формате 8.3.хх.хххх.")
String v8version
@JsonPropertyDescription("Путь к корневому каталогу с исходниками конфигурации")
String srcDir
@JsonProperty("stages")
@JsonPropertyDescription("Включение этапов сборок")
StageFlags stageFlags;
@ -25,15 +28,21 @@ class JobConfiguration implements Serializable {
@JsonPropertyDescription("Настройки синтаксического контроля")
SyntaxCheckOptions syntaxCheckOptions;
@JsonProperty("resultsTransform")
@JsonPropertyDescription("Настройки трансформации результатов анализа")
ResultsTransformOptions resultsTransformOptions;
@Override
@NonCPS
String toString() {
return "JobConfiguration{" +
"v8version='" + v8version + '\'' +
", srcDir='" + srcDir + '\'' +
", stageFlags=" + stageFlags +
", secrets=" + secrets +
", sonarQubeOptions=" + sonarQubeOptions +
", syntaxCheckOptions=" + syntaxCheckOptions +
", resultsTransformOptions=" + resultsTransformOptions +
'}';
}
}

View File

@ -0,0 +1,28 @@
package ru.pulsar.jenkins.library.configuration
import com.cloudbees.groovy.cps.NonCPS
import com.fasterxml.jackson.annotation.JsonIgnoreProperties
import com.fasterxml.jackson.annotation.JsonPropertyDescription
@JsonIgnoreProperties(ignoreUnknown = true)
class ResultsTransformOptions implements Serializable {
@JsonPropertyDescription("Фильтровать замечания по уровню поддержки модуля")
boolean removeSupport
@JsonPropertyDescription("""Настройка фильтрации замечаний по уровню поддержки.
0 - удалить файлы на замке;
1 - удалить файлы на замке и на поддержке;
2 - удалить файлы на замке, на поддержке и снятые с поддержки.
""")
int supportLevel
@Override
@NonCPS
String toString() {
return "ResultsTransformOptions{" +
"removeSupport=" + removeSupport +
", supportLevel=" + supportLevel +
'}';
}
}

View File

@ -12,12 +12,24 @@ class StageFlags implements Serializable {
@JsonPropertyDescription("Синтаксический контроль включен")
boolean syntaxCheck
@JsonPropertyDescription("Валидация EDT включена")
boolean edtValidate
@JsonPropertyDescription("Дымовые тесты включены")
boolean smoke
@Override
@NonCPS
String toString() {
return "StageFlags{" +
"sonarQube=" + sonarqube +
"sonarqube=" + sonarqube +
", syntaxCheck=" + syntaxCheck +
", edtValidate=" + edtValidate +
", smoke=" + smoke +
'}';
}
boolean needInfobase() {
return smoke || syntaxCheck
}
}

View File

@ -0,0 +1,50 @@
package ru.pulsar.jenkins.library.steps
import ru.pulsar.jenkins.library.IStepExecutor
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import ru.pulsar.jenkins.library.utils.Logger
class EdtTransform implements Serializable {
public static final String PROJECT_NAME = 'temp'
public static final String WORKSPACE = 'build/edt-workspace'
public static final String WORKSPACE_ZIP = 'build/edt-workspace.zip'
public static final String WORKSPACE_ZIP_STASH = 'edt-workspace-zip'
private final JobConfiguration config;
EdtTransform(JobConfiguration config) {
this.config = config
}
def run() {
IStepExecutor steps = ContextRegistry.getContext().getStepExecutor()
Logger.printLocation()
if (!config.stageFlags.edtValidate) {
Logger.println("EDT validate step is disabled. No transform is needed.")
return
}
def env = steps.env();
def workspaceDir = "$env.WORKSPACE/$WORKSPACE"
def configurationRoot = new File(env.WORKSPACE, config.srcDir).getAbsolutePath()
steps.createDir(workspaceDir)
Logger.println("Конвертация исходников из формата конфигуратора в формат EDT")
def ringCommand = "ring edt workspace import --configuration-files '$configurationRoot' --project-name $PROJECT_NAME --workspace-location '$workspaceDir'"
def ringOpts = ['RING_OPTS=-Dfile.encoding=UTF-8 -Dosgi.nl=ru -Duser.language=ru']
steps.withEnv(ringOpts) {
steps.cmd(ringCommand)
}
steps.zip(WORKSPACE, WORKSPACE_ZIP)
steps.stash(WORKSPACE_ZIP_STASH, WORKSPACE_ZIP)
}
}

View File

@ -0,0 +1,52 @@
package ru.pulsar.jenkins.library.steps
import ru.pulsar.jenkins.library.IStepExecutor
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import ru.pulsar.jenkins.library.utils.Logger
class EdtValidate implements Serializable {
public static final String RESULT_STASH = 'edt-validate'
public static final String RESULT_FILE = 'build/out/edt-validate.out'
private final JobConfiguration config;
EdtValidate(JobConfiguration config) {
this.config = config
}
def run() {
IStepExecutor steps = ContextRegistry.getContext().getStepExecutor()
Logger.printLocation()
if (!config.stageFlags.edtValidate) {
Logger.println("EDT validate step is disabled")
return
}
steps.unstash(EdtTransform.WORKSPACE_ZIP_STASH)
steps.unzip(EdtTransform.WORKSPACE, EdtTransform.WORKSPACE_ZIP)
def env = steps.env();
def resultFile = "$env.WORKSPACE/$RESULT_FILE"
def workspaceLocation = "$env.WORKSPACE/$EdtTransform.WORKSPACE"
steps.createDir(new File(resultFile).getParent())
Logger.println("Выполнение валидации EDT")
def ringCommand = "ring edt workspace validate --workspace-location '$workspaceLocation' --file '$resultFile' --project-name-list $EdtTransform.PROJECT_NAME"
def ringOpts = ['RING_OPTS=-Dfile.encoding=UTF-8 -Dosgi.nl=ru -Duser.language=ru']
steps.withEnv(ringOpts) {
steps.catchError {
steps.cmd(ringCommand)
}
}
steps.archiveArtifacts("$EdtTransform.WORKSPACE/.metadata/.log")
steps.archiveArtifacts(RESULT_FILE)
steps.stash(RESULT_STASH, RESULT_FILE)
}
}

View File

@ -0,0 +1,48 @@
package ru.pulsar.jenkins.library.steps
import ru.pulsar.jenkins.library.IStepExecutor
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import ru.pulsar.jenkins.library.utils.Logger
class ResultsTransformer implements Serializable {
public static final String RESULT_STASH = 'edt-generic-issue'
public static final String RESULT_FILE = 'build/out/edt-generic-issue.json'
private final JobConfiguration config;
ResultsTransformer(JobConfiguration config) {
this.config = config
}
def run() {
IStepExecutor steps = ContextRegistry.getContext().getStepExecutor()
Logger.printLocation()
def env = steps.env();
if (!config.stageFlags.edtValidate) {
Logger.println("EDT validation is disabled. No transform is needed.")
return
}
steps.unstash(EdtValidate.RESULT_STASH)
Logger.println("Конвертация результата EDT в Generic Issue")
def edtValidateFile = "$env.WORKSPACE/$EdtValidate.RESULT_FILE"
def genericIssueFile = "$env.WORKSPACE/$RESULT_FILE"
steps.cmd("stebi convert $edtValidateFile $genericIssueFile $config.srcDir")
if (config.resultsTransformOptions.removeSupport) {
def supportLevel = config.resultsTransformOptions.supportLevel
steps.cmd("stebi transform --remove_support $supportLevel --src $config.srcDir $genericIssueFile")
}
steps.archiveArtifacts(RESULT_FILE)
steps.stash(RESULT_STASH, RESULT_FILE)
}
}

View File

@ -11,9 +11,9 @@ class SonarScanner implements Serializable {
private final JobConfiguration config;
private final String rootFile
SonarScanner(JobConfiguration config, String rootFile = 'src/cf/Configuration.xml') {
SonarScanner(JobConfiguration config) {
this.config = config
this.rootFile = rootFile
this.rootFile = "$config.srcDir/Configuration.xml"
}
def run() {
@ -44,6 +44,11 @@ class SonarScanner implements Serializable {
sonarCommand += " -Dsonar.projectVersion=$configurationVersion"
}
if (config.stageFlags.edtValidate) {
steps.unstash("edt-generic-issue")
sonarCommand += " -Dsonar.externalIssuesReportPaths=build/out/edt-generic-issue.json"
}
def sonarQubeInstallation = config.sonarQubeOptions.sonarQubeInstallation
if (sonarQubeInstallation == '') {
sonarQubeInstallation = null

View File

@ -10,4 +10,10 @@ class Logger implements Serializable {
def env = steps.env();
steps.echo("Running on node $env.NODE_NAME")
}
static void println(String message) {
IStepExecutor steps = ContextRegistry.getContext().getStepExecutor()
steps.echo(message)
}
}

View File

@ -35,4 +35,28 @@ class cmdTest {
rule.assertLogContains('helloWorld', rule.buildAndAssertSuccess(workflowJob))
}
@Test
void "cmd should return status"() {
def pipeline = '''
pipeline {
agent any
stages {
stage('test') {
steps {
script {
def status = cmd("false", true)
echo "status = $status"
}
}
}
}
}
'''.stripIndent()
final CpsFlowDefinition flow = new CpsFlowDefinition(pipeline, true)
final WorkflowJob workflowJob = rule.createProject(WorkflowJob, 'project')
workflowJob.definition = flow
rule.assertLogContains('status = 1', rule.buildAndAssertSuccess(workflowJob))
}
}

View File

@ -37,6 +37,8 @@ class ConfigurationReaderTest {
.hasFieldOrPropertyWithValue("storagePath", "UNKNOWN_ID")
;
assertThat(jobConfiguration.getSyntaxCheckOptions().getCheckModes()).hasSize(1);
assertThat(jobConfiguration.getResultsTransformOptions().isRemoveSupport()).isTrue();
assertThat(jobConfiguration.getResultsTransformOptions().getSupportLevel()).isEqualTo(0);
}
}

View File

@ -8,5 +8,8 @@
},
"syntaxCheck": {
"checkModes": ["-ThinClient"]
},
"resultsTransform": {
"removeSupport": true
}
}

View File

@ -5,5 +5,5 @@ int call(String script, boolean returnStatus = false) {
ContextRegistry.registerDefaultContext(this)
def cmd = new Cmd(script, returnStatus)
cmd.run()
return cmd.run()
}

3
vars/createDir.groovy Normal file
View File

@ -0,0 +1,3 @@
def call(String path) {
dir(path) { echo '' }
}

10
vars/edtTransform.groovy Normal file
View File

@ -0,0 +1,10 @@
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import ru.pulsar.jenkins.library.steps.EdtTransform
def call(JobConfiguration config) {
ContextRegistry.registerDefaultContext(this)
def edtTransform = new EdtTransform(config)
edtTransform.run()
}

10
vars/edtValidate.groovy Normal file
View File

@ -0,0 +1,10 @@
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import ru.pulsar.jenkins.library.steps.EdtValidate
def call(JobConfiguration config) {
ContextRegistry.registerDefaultContext(this)
def edtValidate = new EdtValidate(config)
edtValidate.run()
}

View File

@ -35,22 +35,17 @@ void call() {
}
}
stage('SonarQube') {
agent {
label 'sonar'
}
steps {
sonarScanner config
}
}
stage('1C') {
stage('Подготовка') {
parallel {
stage('Подготовка 1C базы') {
agent {
label agent1C
}
when {
beforeAgent true
expression { config.stageFlags.needInfobase() }
}
stages {
stage('Подготовка базы') {
steps {
printLocation()
@ -65,25 +60,87 @@ void call() {
}
}
stage('Трансформация в формат EDT') {
agent {
label 'edt'
}
when {
beforeAgent true
expression { config.stageFlags.edtValidate }
}
steps {
edtTransform config
}
}
}
}
stage('Проверка качества') {
parallel {
stage('EDT контроль') {
agent {
label 'edt'
}
when {
beforeAgent true
expression { config.stageFlags.edtValidate }
}
steps {
edtValidate config
}
}
stage('Синтаксический контроль') {
agent {
label agent1C
}
when {
beforeAgent true
expression { config.stageFlags.syntaxCheck }
}
steps {
syntaxCheck config
}
}
stage('Дымовые тесты') {
agent {
label agent1C
}
when {
beforeAgent true
expression { config.stageFlags.smoke }
}
steps {
printLocation()
smoke config
}
}
}
}
installLocalDependencies()
stage('Трансформация результатов') {
agent {
label 'oscript'
}
when {
beforeAgent true
expression { config.stageFlags.edtValidate }
}
steps {
transform config
}
}
unzipInfobase()
}
}
stage('SonarQube') {
agent {
label 'sonar'
}
when {
beforeAgent true
expression { config.stageFlags.sonarqube }
}
steps {
sonarScanner config
}
}
}

23
vars/smoke.groovy Normal file
View File

@ -0,0 +1,23 @@
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.ioc.ContextRegistry
def call(JobConfiguration config) {
ContextRegistry.registerDefaultContext(this)
// TODO: Вынести в отдельный класс по аналогии с SonarScanner
printLocation()
if (!config.stageFlags.smoke) {
echo("Smoke tests step is disabled")
return
}
def options = config.syntaxCheckOptions
installLocalDependencies()
unzipInfobase()
}

View File

@ -2,9 +2,9 @@ import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import ru.pulsar.jenkins.library.steps.SonarScanner
def call(JobConfiguration config, String rootFile = 'src/cf/Configuration.xml') {
def call(JobConfiguration config) {
ContextRegistry.registerDefaultContext(this)
def sonarScanner = new SonarScanner(config, rootFile)
def sonarScanner = new SonarScanner(config)
sonarScanner.run()
}

View File

@ -21,7 +21,7 @@ def call(JobConfiguration config) {
unzipInfobase()
def outPath = new File(options.pathToJUnitReport).getParent()
dir(outPath) { echo '' }
createDir(outPath)
String command = "oscript_modules/bin/vrunner syntax-check --ibconnection \"/F./build/ib\""

10
vars/transform.groovy Normal file
View File

@ -0,0 +1,10 @@
import ru.pulsar.jenkins.library.configuration.JobConfiguration
import ru.pulsar.jenkins.library.ioc.ContextRegistry
import ru.pulsar.jenkins.library.steps.ResultsTransformer
def call(JobConfiguration config) {
ContextRegistry.registerDefaultContext(this)
def transformer = new ResultsTransformer(config)
transformer.run()
}