feat(pipeline): add Bandit SAST scanner and enhance linting configurations

Signed-off-by: 孙振宇 <>
This commit is contained in:
孙振宇 2025-02-07 15:18:40 +08:00
parent 18f4ae6f9a
commit 38bfc418c4
14 changed files with 477 additions and 36 deletions

View File

@ -20,7 +20,6 @@ module.exports = {
], ],
"type-case": [2, "always", "lower-case"], // Type must be in lower case "type-case": [2, "always", "lower-case"], // Type must be in lower case
"type-empty": [2, "never"], // Type must not be empty "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 "scope-case": [2, "always", "lower-case"], // Scope must be in lower case
"subject-empty": [2, "never"], // Subject must not be empty "subject-empty": [2, "never"], // Subject must not be empty
"subject-case": [2, "never", []], // Subject must be in sentence case "subject-case": [2, "never", []], // Subject must be in sentence case

View File

@ -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]"
}
]
]
}

View File

@ -33,8 +33,8 @@ class CodeLintExecutor {
steps.log.error("CodeLintExecutor", "Unknown linter type") steps.log.error("CodeLintExecutor", "Unknown linter type")
return return
} }
steps.writeFile file: "${workspace}/.lintconfig", text: steps.libraryResource(configFilePath) steps.writeFile file: "${workspace}.lintconfig", text: steps.libraryResource(configFilePath)
configs = "${workspace}/.lintconfig" configs = "${workspace}.lintconfig"
} }
Linter linter Linter linter

View File

@ -16,7 +16,7 @@ class CommitMessageLinter {
steps.log.info("Commit Message Linter","Custom commit lint rules found, using custom rules files: ${configurations.commitLintRules}") steps.log.info("Commit Message Linter","Custom commit lint rules found, using custom rules files: ${configurations.commitLintRules}")
rules = configurations.commitLintRules rules = configurations.commitLintRules
} else { } 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.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.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...") 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.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 // 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^" steps.sh "commitlint --verbose -g ${rules} -f HEAD^"
} }
} }

View File

@ -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}")
}
}
}

View File

@ -1,10 +1,31 @@
package com.freeleaps.devops package com.freeleaps.devops
import com.freeleaps.devops.enums.SASTScannerTypes import com.freeleaps.devops.enums.SASTScannerTypes
import com.freeleaps.devops.sast.SASTScanner
class SASTExecutor { class SASTExecutor {
def steps def steps
def workspace def workspace
def configs
def scannerType 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()
}
} }

View File

@ -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()
}
}
}

View File

@ -16,9 +16,9 @@ class SourceFetcher {
steps.error("serviceGitBranch is required") 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 steps.git branch: configurations.serviceGitBranch, credentialsId: 'git-bot-credentials', url: configurations.serviceGitRepo
} }
} }

View File

@ -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
}
}
}

View File

@ -4,11 +4,25 @@ import com.freeleaps.devops.enums.CodeLinterTypes
import com.freeleaps.devops.lint.LinterBase import com.freeleaps.devops.lint.LinterBase
class ESLint extends 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) { ESLint(steps, workspace, configs) {
super(steps, workspace, configs, CodeLinterTypes.ESLINT) super(steps, workspace, configs, CodeLinterTypes.ESLINT)
} }
def doLint() { 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}") steps.sh("eslint --config ${configs} ${workspace}")
} }
} }

View File

@ -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}")
}
}

View File

@ -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()
}

View File

@ -2,13 +2,13 @@ library 'first-class-pipeline'
executeFreeleapsPipeline { executeFreeleapsPipeline {
// serviceName is the name of the service, which is used to identify the service // 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 // serviceSlug used to identify the service environment
environmentSlug = 'alpha' environmentSlug = 'alpha'
// serviceGitBranch used to specify the git repo branch of the service codes // serviceGitBranch used to specify the git repo branch of the service codes
serviceGitBranch = 'master' serviceGitBranch = 'master'
// serviceGitRepo used to specify the git repo of the service codes // 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 // 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 // monorepo: all services codes are in the same repo and using sub-folders to separate them
// separated: each service has its own repo // 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 // fully: the pipeline will be triggered without code changes, all components will be executed
executeMode = 'fully' // on-demand, fully executeMode = 'fully' // on-demand, fully
// commitMessageLintEnabled used to specify whether to enable commit message lint // commitMessageLintEnabled used to specify whether to enable commit message lint
commitMessageLintEnabled = false commitMessageLintEnabled = true
// components used to specify the service components // components used to specify the service components
components = [ components = [
[ [
@ -34,8 +34,12 @@ executeFreeleapsPipeline {
npmPackageJsonFile: 'package.json', npmPackageJsonFile: 'package.json',
// buildCacheEnabled used to specify whether to enable build dependencies cache // buildCacheEnabled used to specify whether to enable build dependencies cache
buildCacheEnabled: true, 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 used to specify the build command of the component
buildCommand: 'npm run build', 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 used to specify whether to enable code lint
lintEnabled: true, lintEnabled: true,
// linter used to specify the code linter // linter used to specify the code linter
@ -52,7 +56,7 @@ executeFreeleapsPipeline {
// imageRepository used to specify the image repository // imageRepository used to specify the image repository
imageRepository: 'sunzhenyucn', imageRepository: 'sunzhenyucn',
// imageName used to specify the image name // imageName used to specify the image name
imageName: 'magicleaps-frontend', imageName: 'gitops-mvp-app-frontend',
// imageBuilder used to specify the image builder // imageBuilder used to specify the image builder
// dind: using docker-in-docker to build the image // dind: using docker-in-docker to build the image
// kaniko: using Kaniko 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 used to specify the image build context root
imageBuildRoot: '.', imageBuildRoot: '.',
// imageReleaseArchitectures used to specify the released image architectures // 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 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 used to specify whether to enable semantic release
semanticReleaseEnabled: true, semanticReleaseEnabled: true
// semanticReleaseBranch used to specify the which branch to publish release notes
semanticReleaseBranch: 'master'
], ],
[ [
// name is the name of the component, which is used to identify the component // name is the name of the component, which is used to identify the component
@ -79,6 +81,10 @@ executeFreeleapsPipeline {
language: 'python', language: 'python',
// dependenciesManager used to specify which dependencies manager to use // dependenciesManager used to specify which dependencies manager to use
dependenciesManager: 'pip', 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 used to specify whether to enable build dependencies cache
buildCacheEnabled: true, buildCacheEnabled: true,
// buildCommand used to specify the build command of the component // buildCommand used to specify the build command of the component
@ -97,7 +103,7 @@ executeFreeleapsPipeline {
// imageRepository used to specify the image repository // imageRepository used to specify the image repository
imageRepository: 'sunzhenyucn', imageRepository: 'sunzhenyucn',
// imageName used to specify the image name // imageName used to specify the image name
imageName: 'magicleaps-backend', imageName: 'gitops-mvp-app-backend',
// imageBuilder used to specify the image builder // imageBuilder used to specify the image builder
// dind: using docker-in-docker to build the image // dind: using docker-in-docker to build the image
// kaniko: using Kaniko 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 used to specify the image build context root
imageBuildRoot: '.', imageBuildRoot: '.',
// imageReleaseArchitectures used to specify the released image architectures // 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 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 used to specify whether to enable semantic release
semanticReleaseEnabled: true, semanticReleaseEnabled: true
// semanticReleaseBranch used to specify the which branch to publish release notes
semanticReleaseBranch: 'master'
] ]
] ]
} }

View File

@ -5,10 +5,13 @@ import com.freeleaps.devops.DependenciesResolver
import com.freeleaps.devops.CommitMessageLinter import com.freeleaps.devops.CommitMessageLinter
import com.freeleaps.devops.ChangedComponentsDetector import com.freeleaps.devops.ChangedComponentsDetector
import com.freeleaps.devops.CodeLintExecutor 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.DependenciesManager
import com.freeleaps.devops.enums.ServiceLanguage import com.freeleaps.devops.enums.ServiceLanguage
import com.freeleaps.devops.enums.CodeLinterTypes import com.freeleaps.devops.enums.CodeLinterTypes
import com.freeleaps.devops.enums.ImageBuilderTypes
def generateComponentStages(component, configurations) { def generateComponentStages(component, configurations) {
return [ return [
@ -61,7 +64,7 @@ def generateComponentStages(component, configurations) {
def language = ServiceLanguage.parse(component.language) def language = ServiceLanguage.parse(component.language)
def depManager = DependenciesManager.parse(component.dependenciesManager) 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) dependenciesResolver.useManager(depManager)
if (component.buildCacheEnabled) { 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}") log.info("Pipeline", "Using ${linter.linter} with image ${linter.containerImage} as linter for ${component.name}")
env.linterContainerImage = linter.containerImage 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") { 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( podTemplate(
label: "code-linter-${component.name}", label: "code-linter-${component.name}",
containers: [ containers: [
@ -131,7 +140,7 @@ def generateComponentStages(component, configurations) {
def linterType = CodeLinterTypes.parse(component.linter) 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() codeLintExecutor.execute()
} }
} }
@ -151,40 +160,248 @@ def generateComponentStages(component, configurations) {
log.error("Pipeline", "Not set sastScanner for ${component.name}") log.error("Pipeline", "Not set sastScanner for ${component.name}")
} }
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}") log.info("Pipeline", "Using ${sastScanner} as SAST scanner for ${component.name}")
env.sastScanner = sastScanner env.sastScannerContainerImage = sastScannerType.containerImage
}
} }
} }
} }
}, },
stage("${component.name} :: SAST Scanning If Enabled") { 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") { 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") { 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") { 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") { 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") { stage("${component.name} :: Image Building & Publishing") {
when {
}, expression {
return (env.executeMode == "fully" || env.changedComponents.contains(component.name)) && env.imageBuilderImage != null && !env.imageBuilderImage.isEmpty()
stage("${component.name} :: Argo Application Version Updating (Deploying)") {
} }
}
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}"
}
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-<BUILD_COMMIT_HASH>' 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 { stages {
stage("Commit Linting If Enabled") { stage("Pipeline :: Commit Linting If Enabled") {
when { when {
expression { expression {
return configurations.commitMessageLintEnabled != null && configurations.commitMessageLintEnabled return configurations.commitMessageLintEnabled != null && configurations.commitMessageLintEnabled
@ -246,7 +463,7 @@ spec:
} }
} }
stage("Execute Mode Detection") { stage("Pipeline :: Execute Mode Detection") {
steps { steps {
script { script {
def executeMode = configurations.executeMode def executeMode = configurations.executeMode
@ -264,7 +481,7 @@ spec:
} }
} }
stage("Code Changes Detection") { stage("Pipeline :: Code Changes Detection") {
when { when {
expression { expression {
return env.executeMode == "on-demand" return env.executeMode == "on-demand"
@ -276,7 +493,7 @@ spec:
sourceFetcher.fetch(configurations) sourceFetcher.fetch(configurations)
def changedComponentsDetector = new ChangedComponentsDetector(this) 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}") log.info("Pipeline","Changed components: ${changedComponents}")
env.changedComponents = changedComponents.join(' ') env.changedComponents = changedComponents.join(' ')
@ -284,7 +501,7 @@ spec:
} }
} }
stage("Components Build (Dynamic Generated Stages)") { stage("Pipeline :: Components Build (Dynamic Generated Stages)") {
when { when {
expression { expression {
return env.executeMode == "fully" || env.changedComponents.size() > 0 return env.executeMode == "fully" || env.changedComponents.size() > 0