diff --git a/first-class-pipeline/resources/com/freeleaps/devops/builtins/commitlint/default.js b/first-class-pipeline/resources/com/freeleaps/devops/builtins/commitlint/default.js index c2ffe91e..457b3df8 100644 --- a/first-class-pipeline/resources/com/freeleaps/devops/builtins/commitlint/default.js +++ b/first-class-pipeline/resources/com/freeleaps/devops/builtins/commitlint/default.js @@ -20,7 +20,6 @@ module.exports = { ], "type-case": [2, "always", "lower-case"], // Type must be in lower case "type-empty": [2, "never"], // Type must not be empty - "scope-empty": [2, "never"], // Scope must not be empty "scope-case": [2, "always", "lower-case"], // Scope must be in lower case "subject-empty": [2, "never"], // Subject must not be empty "subject-case": [2, "never", []], // Subject must be in sentence case diff --git a/first-class-pipeline/resources/com/freeleaps/devops/builtins/semantic-release/releaserc.json b/first-class-pipeline/resources/com/freeleaps/devops/builtins/semantic-release/releaserc.json new file mode 100644 index 00000000..1d97c21a --- /dev/null +++ b/first-class-pipeline/resources/com/freeleaps/devops/builtins/semantic-release/releaserc.json @@ -0,0 +1,23 @@ +{ + "branches": ["master"], + "plugins": [ + [ + "@semantic-release/commit-analyzer", + { + "preset": "angular", + "parserOpts": { + "noteKeywords": ["BREAKING CHANGE", "BREAKING CHANGES"] + } + } + ], + "@semantic-release/release-notes-generator", + "@semantic-release/changelog", + [ + "@semantic-release/git", + { + "assets": ["VERSION", "CHANGELOG.md"], + "message": "chore(release): bump version and upload release assets [ci skip]" + } + ] + ] +} diff --git a/first-class-pipeline/src/com/freeleaps/devops/CodeLintExecutor.groovy b/first-class-pipeline/src/com/freeleaps/devops/CodeLintExecutor.groovy index 152db6ba..1ec40c1f 100644 --- a/first-class-pipeline/src/com/freeleaps/devops/CodeLintExecutor.groovy +++ b/first-class-pipeline/src/com/freeleaps/devops/CodeLintExecutor.groovy @@ -33,8 +33,8 @@ class CodeLintExecutor { steps.log.error("CodeLintExecutor", "Unknown linter type") return } - steps.writeFile file: "${workspace}/.lintconfig", text: steps.libraryResource(configFilePath) - configs = "${workspace}/.lintconfig" + steps.writeFile file: "${workspace}.lintconfig", text: steps.libraryResource(configFilePath) + configs = "${workspace}.lintconfig" } Linter linter diff --git a/first-class-pipeline/src/com/freeleaps/devops/CommitMessageLinter.groovy b/first-class-pipeline/src/com/freeleaps/devops/CommitMessageLinter.groovy index 7e64edf9..899e794d 100644 --- a/first-class-pipeline/src/com/freeleaps/devops/CommitMessageLinter.groovy +++ b/first-class-pipeline/src/com/freeleaps/devops/CommitMessageLinter.groovy @@ -16,7 +16,7 @@ class CommitMessageLinter { steps.log.info("Commit Message Linter","Custom commit lint rules found, using custom rules files: ${configurations.commitLintRules}") rules = configurations.commitLintRules } else { - steps.dir(steps.env.workspace) { + steps.dir(steps.env.workroot) { steps.log.info("Commit Message Linter","No custom commit lint rules found, using built-in rules at: ${defaultRule}") steps.writeFile file: '.commitlintrc.js', text: steps.libraryResource(defaultRule) steps.log.info("Commit Message Linter","Built-in commit lint rules requires @commitlint/config-angular, ready to install it...") @@ -27,9 +27,9 @@ class CommitMessageLinter { steps.log.info("Commit Message Linter","Linting commit messages from HEAD...") - steps.dir(steps.env.workspace) { + steps.dir(steps.env.workroot) { // commit lint cli requires a git repository to lint commit messages, so we need make sure the workspace is a trusted git repository - steps.sh "git config --global --add safe.directory ${steps.env.workspace}" + steps.sh "git config --global --add safe.directory ${steps.env.workroot}" steps.sh "commitlint --verbose -g ${rules} -f HEAD^" } } diff --git a/first-class-pipeline/src/com/freeleaps/devops/ImageBuilder.groovy b/first-class-pipeline/src/com/freeleaps/devops/ImageBuilder.groovy new file mode 100644 index 00000000..1c45c278 --- /dev/null +++ b/first-class-pipeline/src/com/freeleaps/devops/ImageBuilder.groovy @@ -0,0 +1,57 @@ +package com.freeleaps.devops + +import com.freeleaps.devops.enums.ImageBuilderTypes + +class ImageBuilder { + def steps + def workspace + def contextRoot + def dockerfile + def builderType + + ImageBuilder(steps, workspace, contextRoot, dockerfile, builderType) { + this.steps = steps + this.workspace = workspace + this.contextRoot = contextRoot + this.dockerfile = dockerfile + this.builderType = builderType + } + + def build(name, repository, registry, architectures, version) { + steps.log.info("ImageBuilder", "Building image with ${builderType.builder}") + steps.log.info("ImageBuilder", "Workspace sets to: ${workspace}") + steps.log.info("ImageBuilder", "Using dockerfile at: ${dockerfile}, context root sets to: ${contextRoot}") + + if (architectures == null || architectures.isEmpty()) { + steps.log.warn("ImageBuilder", "No architectures specified, using default amd64") + architectures = ['linux/amd64'] + } + + steps.log.info("ImageBuilder", "Login to ${registry}") + steps.sh "docker login ${registry}" + + switch(builderType) { + case ImageBuilderTypes.DOCKER_IN_DOCKER: + steps.dir(workspace) { + architectures.each { architecture -> + def archTag = architecture.split("/")[1] + steps.log.info("ImageBuilder", "Building image ${registry}/${repository}/${name} with architectures: ${architectures}, tag sets to ${version}-${archTag}") + steps.sh "docker build -t ${registry}/${repository}/${name}:${version}-${archTag} --platform ${architecture} -f ${dockerfile} ${contextRoot}" + steps.sh "docker push ${registry}/${repository}/${name}:${version}-${archTag}" + } + } + break + case ImageBuilderTypes.KANIKO: + steps.dir(workspace) { + architectures.each { architecture -> + def archTag = architecture.split("/")[1] + steps.log.info("ImageBuilder", "Building image ${registry}/${repository}/${name} with architectures: ${architectures}, tag sets to ${version}-${archTag}") + steps.sh "/kaniko/executor --log-format text --context ${contextRoot} --dockerfile ${dockerfile} --destination ${registry}/${repository}/${name}:${version}-${archTag} --custom-platform ${architecture}" + } + } + break + default: + steps.error("Unsupported builder type: ${builderType.builder}") + } + } +} diff --git a/first-class-pipeline/src/com/freeleaps/devops/SASTExecutor.groovy b/first-class-pipeline/src/com/freeleaps/devops/SASTExecutor.groovy index 703cfd1b..3d908a44 100644 --- a/first-class-pipeline/src/com/freeleaps/devops/SASTExecutor.groovy +++ b/first-class-pipeline/src/com/freeleaps/devops/SASTExecutor.groovy @@ -1,10 +1,31 @@ package com.freeleaps.devops import com.freeleaps.devops.enums.SASTScannerTypes +import com.freeleaps.devops.sast.SASTScanner class SASTExecutor { def steps def workspace - def configs def scannerType + + SASTExecutor(steps, workspace, scannerType) { + this.steps = steps + this.workspace = workspace + this.scannerType = scannerType + } + + def execute() { + SASTScanner scanner + + switch (scannerType) { + case SASTScannerTypes.BANDIT: + scanner = new Bandit(steps, workspace) + break + default: + steps.error("Unsupported SAST scanner type: ${scannerType}") + return + } + + scanner.scan() + } } \ No newline at end of file diff --git a/first-class-pipeline/src/com/freeleaps/devops/SemanticReleasingExecutor.groovy b/first-class-pipeline/src/com/freeleaps/devops/SemanticReleasingExecutor.groovy new file mode 100644 index 00000000..169d61d6 --- /dev/null +++ b/first-class-pipeline/src/com/freeleaps/devops/SemanticReleasingExecutor.groovy @@ -0,0 +1,34 @@ +package com.freeleaps.devops + +class SemanticReleasingExecutor { + def steps + def workspace + def config + def plugins = [ + '@semantic-release/git', + '@semantic-release/changelog', + '@semantic-release/exec', + 'conventional-changelog-conventionalcommits' + ] + + SemanticReleasingExecutor(steps, workspace) { + this.steps = steps + this.workspace = workspace + // TODO: This should be a parameter, not hardcoded + this.config = 'com/freeleaps/devops/builtins/semantic-release/releaserc.json' + } + + def release() { + steps.log.warn("SemanticReleasingExecutor", "Configuration file customization is not supported yet, using builtin release rules as fallback") + steps.log.info("SemanticReleasingExecutor", "Releasing with config: ${config}") + + steps.dir(steps.env.workroot) { + steps.writeFile file: 'releaserc.json', text: steps.libraryResource(config) + steps.log.info("SemanticReleasingExecutor", "Installing semantic-release plugins...") + steps.sh "npm install -g ${plugins.join(' ')}" + steps.sh "semantic-release" + steps.log.info("SemanticReleasingExecutor", "Semantic release completed, read latest version from VERSION file") + steps.env.LATEST_VERSION = steps.readFile('VERSION').trim() + } + } +} \ No newline at end of file diff --git a/first-class-pipeline/src/com/freeleaps/devops/SourceFetcher.groovy b/first-class-pipeline/src/com/freeleaps/devops/SourceFetcher.groovy index 89a6bfec..25c453e4 100644 --- a/first-class-pipeline/src/com/freeleaps/devops/SourceFetcher.groovy +++ b/first-class-pipeline/src/com/freeleaps/devops/SourceFetcher.groovy @@ -16,9 +16,9 @@ class SourceFetcher { steps.error("serviceGitBranch is required") } - steps.env.workspace = "${steps.env.WORKSPACE}/devops-workspace/${configurations.serviceName}" + steps.env.workroot = "${steps.env.WORKSPACE}/devops-workspace/${configurations.serviceName}" - steps.dir(steps.env.workspace) { + steps.dir(steps.env.workroot) { steps.git branch: configurations.serviceGitBranch, credentialsId: 'git-bot-credentials', url: configurations.serviceGitRepo } } diff --git a/first-class-pipeline/src/com/freeleaps/devops/enums/ImageBuilderTypes.groovy b/first-class-pipeline/src/com/freeleaps/devops/enums/ImageBuilderTypes.groovy new file mode 100644 index 00000000..7b9df4cf --- /dev/null +++ b/first-class-pipeline/src/com/freeleaps/devops/enums/ImageBuilderTypes.groovy @@ -0,0 +1,25 @@ +package com.freeleaps.devops.enums + +enum ImageBuilderTypes { + DOCKER_IN_DOCKER("dind", "docker:dind"), + KANIKO("kaniko", "gcr.io/kaniko-project/executor:latest") + + final String builder + final String image + + ImageBuilderTypes(String builder, String image) { + this.builder = builder + this.image = image + } + + static ImageBuilderTypes parse(String builder) { + switch (builder) { + case 'dind': + return ImageBuilderTypes.DOCKER_IN_DOCKER + case 'kaniko': + return ImageBuilderTypes.KANIKO + default: + return null + } + } +} \ No newline at end of file diff --git a/first-class-pipeline/src/com/freeleaps/devops/lint/ESLint.groovy b/first-class-pipeline/src/com/freeleaps/devops/lint/ESLint.groovy index e34c2440..13a92d5b 100644 --- a/first-class-pipeline/src/com/freeleaps/devops/lint/ESLint.groovy +++ b/first-class-pipeline/src/com/freeleaps/devops/lint/ESLint.groovy @@ -4,11 +4,25 @@ import com.freeleaps.devops.enums.CodeLinterTypes import com.freeleaps.devops.lint.LinterBase class ESLint extends LinterBase { + + deps = [ + 'eslint-define-config', + 'eslint-config-prettier', + 'eslint-plugin-prettier', + 'eslint-plugin-vue', + '@typescript-eslint/eslint-plugin', + '@typescript-eslint/parser', + 'typescript' + ] + ESLint(steps, workspace, configs) { super(steps, workspace, configs, CodeLinterTypes.ESLINT) } def doLint() { + steps.log.info("${linterType.linter}", "Install eslint dependencies...") + steps.sh("npm install -g ${deps.join(' ')}") + steps.log.info("${linterType.linter}", "Running eslint...") steps.sh("eslint --config ${configs} ${workspace}") } } \ No newline at end of file diff --git a/first-class-pipeline/src/com/freeleaps/devops/sast/Bandit.groovy b/first-class-pipeline/src/com/freeleaps/devops/sast/Bandit.groovy new file mode 100644 index 00000000..6da12dde --- /dev/null +++ b/first-class-pipeline/src/com/freeleaps/devops/sast/Bandit.groovy @@ -0,0 +1,14 @@ +package com.freeleaps.devops.sast + +import com.freeleaps.devops.enums.SASTScannerTypes +import com.freeleaps.devops.sast.SASTScannerBase + +class Bandit extends SASTScannerBase { + Bandit(steps, workspace, configs) { + super(steps, workspace, configs, SASTScannerTypes.BANDIT) + } + + def doScan() { + steps.sh("bandit -r ${workspace}") + } +} \ No newline at end of file diff --git a/first-class-pipeline/src/com/freeleaps/devops/sast/SASTScanner.groovy b/first-class-pipeline/src/com/freeleaps/devops/sast/SASTScanner.groovy new file mode 100644 index 00000000..9b21daac --- /dev/null +++ b/first-class-pipeline/src/com/freeleaps/devops/sast/SASTScanner.groovy @@ -0,0 +1,33 @@ +package com.freeleaps.devops.sast + +interface SASTScanner { + def scan() +} + +abstract class SASTScannerBase implements SASTScanner { + def steps + def workspace + def configs // TODO: add configurations file support + def scannerType + + SASTScannerBase(steps, workspace, configs, scannerType) { + this.steps = steps + this.workspace = workspace + this.configs = configs + this.scannerType = scannerType + } + + def scan() { + steps.log.info("${scannerType.scanner}", "Scanning ${scannerType.language.language} code") + steps.log.info("${scannerType.scanner}", "Workspace sets to: ${workspace}") + + if (configs != null || !configs.isEmpty()) { + steps.log.warn("${scannerType.scanner}", "Configurations file is not supported for ${scannerType.scanner} yet") + } + + doScan() + steps.log.info("${scannerType.scanner}", "Code scanning has been completed") + } + + abstract def doScan() +} \ No newline at end of file diff --git a/first-class-pipeline/tests/Jenkinsfile b/first-class-pipeline/tests/Jenkinsfile index 3961fe70..c4619335 100644 --- a/first-class-pipeline/tests/Jenkinsfile +++ b/first-class-pipeline/tests/Jenkinsfile @@ -2,13 +2,13 @@ library 'first-class-pipeline' executeFreeleapsPipeline { // serviceName is the name of the service, which is used to identify the service - serviceName = 'magicleaps' + serviceName = 'gitops-mvp-app' // serviceSlug used to identify the service environment environmentSlug = 'alpha' // serviceGitBranch used to specify the git repo branch of the service codes serviceGitBranch = 'master' // serviceGitRepo used to specify the git repo of the service codes - serviceGitRepo = "https://freeleaps@dev.azure.com/freeleaps/magicleaps/_git/magicleaps" + serviceGitRepo = "https://zhenyus@dev.azure.com/zhenyus/gitops-mvp-app/_git/gitops-mvp-app" // serviceGitRepoType used to specify the git repo type of the service codes // monorepo: all services codes are in the same repo and using sub-folders to separate them // separated: each service has its own repo @@ -18,7 +18,7 @@ executeFreeleapsPipeline { // fully: the pipeline will be triggered without code changes, all components will be executed executeMode = 'fully' // on-demand, fully // commitMessageLintEnabled used to specify whether to enable commit message lint - commitMessageLintEnabled = false + commitMessageLintEnabled = true // components used to specify the service components components = [ [ @@ -34,8 +34,12 @@ executeFreeleapsPipeline { npmPackageJsonFile: 'package.json', // buildCacheEnabled used to specify whether to enable build dependencies cache buildCacheEnabled: true, + // buildAgentImage used to specify the build environment container image + buildAgentImage: 'node:lts', // buildCommand used to specify the build command of the component buildCommand: 'npm run build', + // buildArtifacts used to specify the build artifacts that needs to be stores and shares between components + buildArtifacts: ['dist'] // lintEnabled used to specify whether to enable code lint lintEnabled: true, // linter used to specify the code linter @@ -52,7 +56,7 @@ executeFreeleapsPipeline { // imageRepository used to specify the image repository imageRepository: 'sunzhenyucn', // imageName used to specify the image name - imageName: 'magicleaps-frontend', + imageName: 'gitops-mvp-app-frontend', // imageBuilder used to specify the image builder // dind: using docker-in-docker to build the image // kaniko: using Kaniko to build the image @@ -62,13 +66,11 @@ executeFreeleapsPipeline { // imageBuildRoot used to specify the image build context root imageBuildRoot: '.', // imageReleaseArchitectures used to specify the released image architectures - imageReleaseArchitectures: ['amd64', 'arm64'], + imageReleaseArchitectures: ['linux/amd64', 'linux/arm64'], // registryCredentialName used to specify the registry credential that stored in Freeleaps Kubernetes Cluster - registryCredentialName: 'first-class-pipeline-dev-secret', + registryCredentialName: 'gitops-mvp-app-secret', // semanticReleaseEnabled used to specify whether to enable semantic release - semanticReleaseEnabled: true, - // semanticReleaseBranch used to specify the which branch to publish release notes - semanticReleaseBranch: 'master' + semanticReleaseEnabled: true ], [ // name is the name of the component, which is used to identify the component @@ -79,6 +81,10 @@ executeFreeleapsPipeline { language: 'python', // dependenciesManager used to specify which dependencies manager to use dependenciesManager: 'pip', + // buildAgentImage used to specify the build environment container image + buildAgentImage: 'python:3.8-slim-buster', + // buildArtifacts used to specify the build artifacts that needs to be stores and shares between components + buildArtifacts: ['.'] // buildCacheEnabled used to specify whether to enable build dependencies cache buildCacheEnabled: true, // buildCommand used to specify the build command of the component @@ -97,7 +103,7 @@ executeFreeleapsPipeline { // imageRepository used to specify the image repository imageRepository: 'sunzhenyucn', // imageName used to specify the image name - imageName: 'magicleaps-backend', + imageName: 'gitops-mvp-app-backend', // imageBuilder used to specify the image builder // dind: using docker-in-docker to build the image // kaniko: using Kaniko to build the image @@ -107,13 +113,11 @@ executeFreeleapsPipeline { // imageBuildRoot used to specify the image build context root imageBuildRoot: '.', // imageReleaseArchitectures used to specify the released image architectures - imageReleaseArchitectures: ['amd64', 'arm64'], + imageReleaseArchitectures: ['linux/amd64', 'linux/arm64'], // registryCredentialName used to specify the registry credential that stored in Freeleaps Kubernetes Cluster - registryCredentialName: 'first-class-pipeline-dev-secret', + registryCredentialName: 'gitops-mvp-app-secret', // semanticReleaseEnabled used to specify whether to enable semantic release - semanticReleaseEnabled: true, - // semanticReleaseBranch used to specify the which branch to publish release notes - semanticReleaseBranch: 'master' + semanticReleaseEnabled: true ] ] } \ No newline at end of file diff --git a/first-class-pipeline/vars/executeFreeleapsPipeline.groovy b/first-class-pipeline/vars/executeFreeleapsPipeline.groovy index 3ddedb63..e2e3b618 100644 --- a/first-class-pipeline/vars/executeFreeleapsPipeline.groovy +++ b/first-class-pipeline/vars/executeFreeleapsPipeline.groovy @@ -5,10 +5,13 @@ import com.freeleaps.devops.DependenciesResolver import com.freeleaps.devops.CommitMessageLinter import com.freeleaps.devops.ChangedComponentsDetector import com.freeleaps.devops.CodeLintExecutor +import com.freeleaps.devops.SASTExecutor +import com.freeleaps.devops.ImageBuilder import com.freeleaps.devops.enums.DependenciesManager import com.freeleaps.devops.enums.ServiceLanguage import com.freeleaps.devops.enums.CodeLinterTypes +import com.freeleaps.devops.enums.ImageBuilderTypes def generateComponentStages(component, configurations) { return [ @@ -61,7 +64,7 @@ def generateComponentStages(component, configurations) { def language = ServiceLanguage.parse(component.language) def depManager = DependenciesManager.parse(component.dependenciesManager) - def dependenciesResolver = new DependenciesResolver(this, language, env.workspace + "/" + component.root + "/") + def dependenciesResolver = new DependenciesResolver(this, language, env.workroot + "/" + component.root + "/") dependenciesResolver.useManager(depManager) if (component.buildCacheEnabled) { @@ -100,13 +103,19 @@ def generateComponentStages(component, configurations) { log.info("Pipeline", "Using ${linter.linter} with image ${linter.containerImage} as linter for ${component.name}") env.linterContainerImage = linter.containerImage + } else { + log.info("Pipeline", "Code linting is not enabled for ${component.name}, skipping...") } - log.info("Pipeline", "Code linting is not enabled for ${component.name}, skipping...") } } }, stage("${component.name} :: Code Linting If Enabled") { + when { + expression { + return (env.executeMode == "fully" || env.changedComponents.contains(component.name)) && env.linterContainerImage != null && !env.linterContainerImage.isEmpty() + } + } podTemplate( label: "code-linter-${component.name}", containers: [ @@ -131,7 +140,7 @@ def generateComponentStages(component, configurations) { def linterType = CodeLinterTypes.parse(component.linter) - def codeLintExecutor = new CodeLintExecutor(this, env.workspace + "/" + component.root + "/", component.linterConfig, linterType) + def codeLintExecutor = new CodeLintExecutor(this, env.workroot + "/" + component.root + "/", component.linterConfig, linterType) codeLintExecutor.execute() } } @@ -151,40 +160,248 @@ def generateComponentStages(component, configurations) { log.error("Pipeline", "Not set sastScanner for ${component.name}") } - log.info("Pipeline", "Using ${sastScanner} as SAST scanner for ${component.name}") - env.sastScanner = sastScanner + def sastScannerType = SASTScannerTypes.parse(component.sastScanner) + if (sastScannerType == null) { + log.error("Pipeline", "Unknown SAST scanner for ${component.name}, skipping SAST scanning") + } else if (sastScannerType.language != ServiceLanguage.parse(component.language)) { + log.error("Pipeline", "SAST scanner ${sastScannerType.scanner} is not supported for ${component.language}, skipping SAST scanning") + } else { + log.info("Pipeline", "Using ${sastScanner} as SAST scanner for ${component.name}") + env.sastScannerContainerImage = sastScannerType.containerImage + } } } } }, stage("${component.name} :: SAST Scanning If Enabled") { + when { + expression { + return (env.executeMode == "fully" || env.changedComponents.contains(component.name)) && env.sastScannerContainerImage != null && !env.sastScannerContainerImage.isEmpty() + } + } + podTemplate( + label: "sast-scanner-${component.name}", + containers: [ + containerTemplate( + name: 'sast-scanner', + image: env.sastScannerContainerImage, + ttyEnabled: true, + command: 'sleep', + args: 'infinity' + ) + ] + ) { + node("sast-scanner-${component.name}") { + container('sast-scanner') { + script { + if (env.executeMode == "fully" || env.changedComponents.contains(component.name)) { + if (component.sastEnabled != null && component.sastEnabled) { + log.info("Pipeline", "SAST scanning has enabled, scanning code...") + def sourceFetcher = new SourceFetcher(this) + sourceFetcher.fetch(configurations) + + def sastScannerType = SASTScannerTypes.parse(component.sastScanner) + + def sastScanner = new SASTExecutor(this, env.workroot + "/" + component.root + "/", sastScannerType) + sastScanner.scan() + } + } + } + } + } + } }, stage("${component.name} :: Semantic Release Preparation") { + script { + if (env.executeMode == "fully" || env.changedComponents.contains(component.name)) { + if (component.semanticReleaseEnabled != null && component.semanticReleaseEnabled) { + log.info("Pipeline", "Semantic releasing has enabled, preparing semantic release...") + if (component.semanticReleaseBranch == null || component.semanticReleaseBranch.isEmpty()) { + log.error("Pipeline", "Not set semanticReleaseBranch for ${component.name}, please set it to enable semantic release") + } + + log.info("Pipeline", "Using ${component.semanticReleaseBranch} as semantic release branch for ${component.name}") + env.semanticReleasingContainerImage = "docker.io/semantic-release/semantic-release:latest" + } + } + } }, stage("${component.name} :: Semantic Releasing If Enabled") { + when { + expression { + return (env.executeMode == "fully" || env.changedComponents.contains(component.name)) && env.semanticReleasingContainerImage != null && !env.semanticReleasingContainerImage.isEmpty() + } + } + podTemplate( + label: "semantic-releasing-${component.name}", + containers: [ + containerTemplate( + name: 'semantic-releasing', + image: env.semanticReleasingContainerImage, + ttyEnabled: true, + command: 'sleep', + args: 'infinity' + ) + ] + ) { + node("semantic-releasing-${component.name}") { + container('semantic-releasing') { + script { + if (env.executeMode == "fully" || env.changedComponents.contains(component.name)) { + if (component.semanticReleaseEnabled != null && component.semanticReleaseEnabled) { + log.info("Pipeline", "Semantic releasing has enabled, releasing...") + def sourceFetcher = new SourceFetcher(this) + sourceFetcher.fetch(configurations) + + def semanticReleasingExecutor = new SemanticReleasingExecutor(this, env.workroot + "/" + component.root + "/") + semanticReleasingExecutor.release() + } + } + } + } + } + } }, stage("${component.name} :: Compilation & Packaging") { + podTemplate( + label: "build-agent-${component.name}", + containers: [ + containerTemplate( + name: 'build-agent', + image: env.buildAgentImage, + ttyEnabled: true, + command: 'sleep', + args: 'infinity' + ) + ] + ) { + node("build-agent-${component.name}") { + container('build-agent') { + script { + if (env.executeMode == "fully" || env.changedComponents.contains(component.name)) { + log.info("Pipeline", "Using ${env.buildAgentImage} as build agent image for compilation & packaging") + + def sourceFetcher = new SourceFetcher(this) + sourceFetcher.fetch(configurations) + def language = ServiceLanguage.parse(component.language) + def depManager = DependenciesManager.parse(component.dependenciesManager) + + def dependenciesResolver = new DependenciesResolver(this, language, env.workroot + "/" + component.root + "/") + dependenciesResolver.useManager(depManager) + if (component.buildCacheEnabled) { + dependenciesResolver.enableCachingSupport() + } else { + dependenciesResolver.disableCachingSupport() + } + + dependenciesResolver.resolve(component) + + dir(env.workroot + "/" + component.root) { + if (component.buildCommand != null && !component.buildCommand.isEmpty()) { + sh component.buildCommand + } + component.buildArtifacts.each { artifact -> + stash includes: artifact, name: "${component.name}-${artifact}" + } + } + } + } + } + } }, stage("${component.name} :: Image Builder Setup") { + script { + if (env.executeMode == "fully" || env.changedComponents.contains(component.name)) { + log.info("Pipeline", "Ready to setup image builder for ${component.name}") + def imageBuilder + if (component.imageBuilder == null || component.imageBuilder.isEmpty()) { + log.info("Pipeline", "imageBuilder not set for ${component.name}, using kaniko as default image builder") + imageBuilder = ImageBuilderTypes.KANIKO + } else { + imageBuilder = ImageBuilderTypes.parse(component.imageBuilder) + if (imageBuilder == null) { + log.error("Pipeline", "Unknown image builder for ${component.name}, skipping image building") + } + } + env.imageBuilderImage = imageBuilder.image + log.info("Pipeline", "Using ${imageBuilder.builder} (image: ${imageBuilder.image}) as image builder for ${component.name}") + } + } }, stage("${component.name} :: Image Building & Publishing") { + when { + expression { + return (env.executeMode == "fully" || env.changedComponents.contains(component.name)) && env.imageBuilderImage != null && !env.imageBuilderImage.isEmpty() + } + } + podTemplate( + label: "image-builder-${component.name}", + containers: [ + containerTemplate( + name: 'image-builder', + image: env.imageBuilderImage, + ttyEnabled: true, + command: 'sleep', + args: 'infinity' + ) + ] + ) { + node("image-builder-${component.name}") { + container('image-builder') { + script { + if (env.executeMode == "fully" || env.changedComponents.contains(component.name)) { + def sourceFetcher = new SourceFetcher(this) + sourceFetcher.fetch(configurations) - }, + dir(env.workroot + "/" + component.root) { + component.buildArtifacts.each { artifact -> + unstash "${component.name}-${artifact}" + } - stage("${component.name} :: Argo Application Version Updating (Deploying)") { + if (component.dockerfile != null && !component.dockerfile.isEmpty()) { + log.error("Pipeline", "Component ${component.name} dockerfile not set!") + } + def imageBuilderType = ImageBuilderTypes.parse(component.imageBuilder) + if (imageBuilderType == null) { + log.error("Pipeline", "Unknown image builder for ${component.name}, skipping image building") + } + + def imageBuilder = new ImageBuilder(this, env.workroot + "/" + component.root + "/", component.imageBuildRoot, component.dockerfilePath, imageBuilderType) + + log.info("Pipeline", "Retrieve version of image from pervious stage...") + if (env.LATEST_VERSION == null || env.LATEST_VERSION.isEmpty()) { + log.warn("Pipeline", "LATEST_VERSION environment value not set, using 'snapshot-' as default version") + + def commitHash = sh(script: 'git rev-parse HEAD', returnStdout: true).trim() + def shortCommitHash = commitHash.take(7) + env.LATEST_VERSION = "snapshot-${shortCommitHash}" + } + def version + + imageBuilder.build(component.imageName, component.imageRepository, component.imageRegistry, component.imageReleaseArchitectures, env.LATEST_VERSION) + } + } + } + } + } + } } + + // stage("${component.name} :: Argo Application Version Updating (Deploying)") { + + // } ] } @@ -203,7 +420,7 @@ def call(Closure closure) { } stages { - stage("Commit Linting If Enabled") { + stage("Pipeline :: Commit Linting If Enabled") { when { expression { return configurations.commitMessageLintEnabled != null && configurations.commitMessageLintEnabled @@ -246,7 +463,7 @@ spec: } } - stage("Execute Mode Detection") { + stage("Pipeline :: Execute Mode Detection") { steps { script { def executeMode = configurations.executeMode @@ -264,7 +481,7 @@ spec: } } - stage("Code Changes Detection") { + stage("Pipeline :: Code Changes Detection") { when { expression { return env.executeMode == "on-demand" @@ -276,7 +493,7 @@ spec: sourceFetcher.fetch(configurations) def changedComponentsDetector = new ChangedComponentsDetector(this) - def changedComponents = changedComponentsDetector.detect(env.workspace, configurations.components) + def changedComponents = changedComponentsDetector.detect(env.workroot, configurations.components) log.info("Pipeline","Changed components: ${changedComponents}") env.changedComponents = changedComponents.join(' ') @@ -284,7 +501,7 @@ spec: } } - stage("Components Build (Dynamic Generated Stages)") { + stage("Pipeline :: Components Build (Dynamic Generated Stages)") { when { expression { return env.executeMode == "fully" || env.changedComponents.size() > 0