diff --git a/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/api/user/UserBuildResource.kt b/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/api/user/UserBuildResource.kt index fd9a3526ed8..72f94ec69a3 100644 --- a/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/api/user/UserBuildResource.kt +++ b/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/api/user/UserBuildResource.kt @@ -701,4 +701,25 @@ interface UserBuildResource { @QueryParam("stageId") stageId: String ): Result + + @Operation(summary = "回放指定构建任务的触发事件") + @POST + @Path("/{projectId}/{pipelineId}/{buildId}/replayByBuild") + fun replayByBuild( + @Parameter(description = "用户ID", required = true, example = AUTH_HEADER_USER_ID_DEFAULT_VALUE) + @HeaderParam(AUTH_HEADER_USER_ID) + userId: String, + @Parameter(description = "项目ID", required = true) + @PathParam("projectId") + projectId: String, + @Parameter(description = "流水线ID", required = true) + @PathParam("pipelineId") + pipelineId: String, + @Parameter(description = "构建ID", required = true) + @PathParam("buildId") + buildId: String, + @Parameter(description = "强制触发", required = false) + @QueryParam("forceTrigger") + forceTrigger: Boolean? = false + ): Result } diff --git a/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/constant/ProcessMessageCode.kt b/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/constant/ProcessMessageCode.kt index 95d527275a8..cf879c365c5 100644 --- a/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/constant/ProcessMessageCode.kt +++ b/src/backend/ci/core/process/api-process/src/main/kotlin/com/tencent/devops/process/constant/ProcessMessageCode.kt @@ -362,6 +362,7 @@ object ProcessMessageCode { const val ERROR_PIPELINE_BUILD_START_PARAM_NO_EMPTY = "2101254" // 构建启动参数如果必填,不能为空 const val ERROR_REPEATEDLY_START_VM = "2101255" // 重复启动构建机,当前构建机的状态为:{0} const val ERROR_PIPELINE_VARIABLES_OUT_OF_LENGTH = "2101256" // 流水线启动参数{0}超出4000长度限制 + const val ERROR_TRIGGER_CONDITION_NOT_MATCH = "2101260" // 触发条件不匹配 const val BK_SUCCESSFULLY_DISTRIBUTED = "bkSuccessfullyDistributed" // 跨项目构件分发成功,共分发了{0}个文件 const val BK_SUCCESSFULLY_FAILED = "bkSuccessfullyFailed" // 跨项目构件分发失败, diff --git a/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/api/UserBuildResourceImpl.kt b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/api/UserBuildResourceImpl.kt index a269d560e49..12d8b511884 100644 --- a/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/api/UserBuildResourceImpl.kt +++ b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/api/UserBuildResourceImpl.kt @@ -635,6 +635,24 @@ class UserBuildResourceImpl @Autowired constructor( ) } + override fun replayByBuild( + userId: String, + projectId: String, + pipelineId: String, + buildId: String, + forceTrigger: Boolean? + ): Result { + return Result( + pipelineBuildFacadeService.replayBuild( + projectId = projectId, + pipelineId = pipelineId, + buildId = buildId, + userId = userId, + forceTrigger = forceTrigger ?: false + ) + ) + } + private fun checkParam(userId: String, projectId: String, pipelineId: String) { if (userId.isBlank()) { throw ParamBlankException("Invalid userId") diff --git a/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/SubPipelineStartUpService.kt b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/SubPipelineStartUpService.kt index 677b20f0b18..27f71a2b044 100644 --- a/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/SubPipelineStartUpService.kt +++ b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/SubPipelineStartUpService.kt @@ -97,7 +97,7 @@ class SubPipelineStartUpService @Autowired constructor( companion object { private val logger = LoggerFactory.getLogger(SubPipelineStartUpService::class.java) - private const val SYNC_RUN_MODE = "syn" + const val SYNC_RUN_MODE = "syn" } /** diff --git a/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/builds/PipelineBuildFacadeService.kt b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/builds/PipelineBuildFacadeService.kt index 189a71059ea..79412903965 100644 --- a/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/builds/PipelineBuildFacadeService.kt +++ b/src/backend/ci/core/process/biz-process/src/main/kotlin/com/tencent/devops/process/service/builds/PipelineBuildFacadeService.kt @@ -56,6 +56,7 @@ import com.tencent.devops.common.pipeline.pojo.BuildFormProperty import com.tencent.devops.common.pipeline.pojo.BuildFormValue import com.tencent.devops.common.pipeline.pojo.BuildParameters import com.tencent.devops.common.pipeline.pojo.StageReviewRequest +import com.tencent.devops.common.pipeline.pojo.element.EmptyElement import com.tencent.devops.common.pipeline.pojo.element.agent.ManualReviewUserTaskElement import com.tencent.devops.common.pipeline.pojo.element.atom.ManualReviewParam import com.tencent.devops.common.pipeline.pojo.element.atom.ManualReviewParamType @@ -88,6 +89,7 @@ import com.tencent.devops.process.engine.control.lock.PipelineRefreshBuildLock import com.tencent.devops.process.engine.interceptor.InterceptData import com.tencent.devops.process.engine.interceptor.PipelineInterceptorChain import com.tencent.devops.process.engine.pojo.BuildInfo +import com.tencent.devops.process.engine.pojo.PipelineInfo import com.tencent.devops.process.engine.pojo.event.PipelineBuildContainerEvent import com.tencent.devops.process.engine.service.PipelineBuildDetailService import com.tencent.devops.process.engine.service.PipelineBuildQualityService @@ -123,11 +125,14 @@ import com.tencent.devops.process.pojo.pipeline.PipelineResourceVersion import com.tencent.devops.process.service.BuildVariableService import com.tencent.devops.process.service.ParamFacadeService import com.tencent.devops.process.service.PipelineTaskPauseService +import com.tencent.devops.process.service.SubPipelineStartUpService import com.tencent.devops.process.service.pipeline.PipelineBuildService -import com.tencent.devops.process.service.template.TemplateFacadeService import com.tencent.devops.process.strategy.context.UserPipelinePermissionCheckContext import com.tencent.devops.process.strategy.factory.UserPipelinePermissionCheckStrategyFactory import com.tencent.devops.process.util.TaskUtils +import com.tencent.devops.process.utils.BK_CI_MATERIAL_ID +import com.tencent.devops.process.utils.BK_CI_MATERIAL_NAME +import com.tencent.devops.process.utils.BK_CI_MATERIAL_URL import com.tencent.devops.process.utils.PIPELINE_BUILD_MSG import com.tencent.devops.process.utils.PIPELINE_NAME import com.tencent.devops.process.utils.PIPELINE_RETRY_ALL_FAILED_CONTAINER @@ -135,12 +140,19 @@ import com.tencent.devops.process.utils.PIPELINE_RETRY_BUILD_ID import com.tencent.devops.process.utils.PIPELINE_RETRY_COUNT import com.tencent.devops.process.utils.PIPELINE_RETRY_START_TASK_ID import com.tencent.devops.process.utils.PIPELINE_SKIP_FAILED_TASK +import com.tencent.devops.process.utils.PIPELINE_START_PARENT_BUILD_ID +import com.tencent.devops.process.utils.PIPELINE_START_PARENT_BUILD_TASK_ID +import com.tencent.devops.process.utils.PIPELINE_START_PARENT_EXECUTE_COUNT +import com.tencent.devops.process.utils.PIPELINE_START_PARENT_PIPELINE_ID +import com.tencent.devops.process.utils.PIPELINE_START_PARENT_PROJECT_ID +import com.tencent.devops.process.utils.PIPELINE_START_SUB_RUN_MODE import com.tencent.devops.process.utils.PIPELINE_START_TASK_ID import com.tencent.devops.process.utils.PipelineVarUtil.recommendVersionKey import com.tencent.devops.process.yaml.PipelineYamlFacadeService import com.tencent.devops.quality.api.v2.pojo.ControlPointPosition import org.slf4j.LoggerFactory import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Lazy import org.springframework.stereotype.Service import java.util.concurrent.TimeUnit import javax.ws.rs.core.Response @@ -177,7 +189,8 @@ class PipelineBuildFacadeService( private val pipelineRetryFacadeService: PipelineRetryFacadeService, private val webhookBuildParameterService: WebhookBuildParameterService, private val pipelineYamlFacadeService: PipelineYamlFacadeService, - private val templateFacadeService: TemplateFacadeService + @Lazy + private val subPipelineStartUpService: SubPipelineStartUpService ) { @Value("\${pipeline.build.cancel.intervalLimitTime:60}") @@ -834,7 +847,9 @@ class PipelineBuildFacadeService( startType: StartType = StartType.WEB_HOOK, startValues: Map? = null, userParameters: List? = null, - triggerReviewers: List? = null + triggerReviewers: List? = null, + pipelineResource: PipelineResourceVersion? = null, + pipelineInfo: PipelineInfo? = null, ): String? { if (checkPermission) { @@ -849,8 +864,10 @@ class PipelineBuildFacadeService( ) ) } - val readyToBuildPipelineInfo = pipelineRepositoryService.getPipelineInfo(projectId, pipelineId) - ?: return null + val readyToBuildPipelineInfo = pipelineInfo ?: pipelineRepositoryService.getPipelineInfo( + projectId = projectId, + pipelineId = pipelineId + ) ?: return null if (readyToBuildPipelineInfo.locked == true) { throw ErrorCodeException(errorCode = ProcessMessageCode.ERROR_PIPELINE_LOCK) } @@ -859,8 +876,11 @@ class PipelineBuildFacadeService( ) val startEpoch = System.currentTimeMillis() try { - - val resource = getPipelineResourceVersion(projectId, pipelineId, readyToBuildPipelineInfo.version) + val resource = pipelineResource ?: getPipelineResourceVersion( + projectId = projectId, + pipelineId = pipelineId, + version = readyToBuildPipelineInfo.version + ) val model = resource.model /** @@ -2683,6 +2703,185 @@ class PipelineBuildFacadeService( ) } + fun replayBuild( + projectId: String, + pipelineId: String, + buildId: String, + userId: String, + forceTrigger: Boolean + ): BuildId { + pipelinePermissionService.validPipelinePermission( + userId = userId, + projectId = projectId, + pipelineId = pipelineId, + permission = AuthPermission.EXECUTE, + message = MessageUtil.getMessageByLocale( + CommonMessageCode.USER_NOT_PERMISSIONS_OPERATE_PIPELINE, + I18nUtil.getLanguage(userId), + arrayOf( + userId, + projectId, + AuthPermission.EXECUTE.getI18n(I18nUtil.getLanguage(userId)), + pipelineId + ) + ) + ) + val buildInfo = checkPipelineInfo(projectId, pipelineId, buildId) + // 按原有的启动参数组装启动参数(排除重试次数) + val startParameters = buildInfo.buildParameters?.filter { + it.key != PIPELINE_RETRY_COUNT + }?.associate { + it.key to it.value.toString() + }?.toMutableMap() ?: mutableMapOf() + val startType = StartType.toStartType(buildInfo.trigger) + val pipelineInfo = pipelineRepositoryService.getPipelineInfo(projectId, pipelineId) + ?: throw ErrorCodeException(errorCode = ProcessMessageCode.ERROR_PIPELINE_NOT_EXISTS) + if (pipelineInfo.locked == true) { + throw ErrorCodeException(errorCode = ProcessMessageCode.ERROR_PIPELINE_LOCK) + } + if (pipelineInfo.latestVersionStatus?.isNotReleased() == true) throw ErrorCodeException( + errorCode = ProcessMessageCode.ERROR_NO_RELEASE_PIPELINE_VERSION + ) + val pipelineResourceVersion = getPipelineResourceVersion(projectId, pipelineId, pipelineInfo.version) + val model = pipelineResourceVersion.model + val triggerContainer = model.getTriggerContainer() + // 检查触发器是否存在 + val checkTriggerResult = forceTrigger || when (startType) { + StartType.WEB_HOOK -> { + triggerContainer.elements.find { it.id == startParameters[PIPELINE_START_TASK_ID] } + } + + StartType.MANUAL, StartType.SERVICE -> { + triggerContainer.elements.find { it is ManualTriggerElement } + } + + StartType.REMOTE -> { + triggerContainer.elements.find { it is RemoteTriggerElement } + } + + StartType.TIME_TRIGGER -> { + triggerContainer.elements.find { it.id == startParameters[PIPELINE_START_TASK_ID] } + } + + StartType.PIPELINE -> { + EmptyElement() + } + } != null + if (!checkTriggerResult) { + throw ErrorCodeException( + errorCode = ProcessMessageCode.ERROR_TRIGGER_CONDITION_NOT_MATCH, + params = arrayOf(pipelineResourceVersion.versionName ?: "") + ) + } + return triggerPipeline( + userId = buildInfo.startUser, + projectId = projectId, + pipelineId = pipelineId, + buildId = buildId, + startParameters = startParameters, + startType = startType, + pipelineInfo = pipelineInfo, + pipelineResourceVersion = pipelineResourceVersion + ) + } + + private fun triggerPipeline( + startType: StartType, + projectId: String, + pipelineId: String, + buildId: String, + startParameters: MutableMap, + pipelineInfo: PipelineInfo? = null, + pipelineResourceVersion: PipelineResourceVersion? = null, + userId: String + ) = when (startType) { + StartType.WEB_HOOK -> { + // webhook触发 + webhookBuildParameterService.getBuildParameters(buildId = buildId)?.forEach { param -> + startParameters[param.key] = param.value.toString() + } + // webhook触发 + BuildId( + webhookTriggerPipelineBuild( + userId = userId, + projectId = projectId, + pipelineId = pipelineId, + parameters = startParameters, + checkPermission = false, + startType = startType, + pipelineInfo = pipelineInfo, + pipelineResource = pipelineResourceVersion + )!! + ) + } + + StartType.MANUAL, StartType.SERVICE, StartType.REMOTE -> { + startParameters.putAll( + // 自定义触发源材料参数 + buildVariableService.getAllVariable( + projectId = projectId, + pipelineId = pipelineId, + buildId = buildId, + keys = setOf(BK_CI_MATERIAL_ID, BK_CI_MATERIAL_NAME, BK_CI_MATERIAL_URL) + ) + ) + buildManualStartup( + userId = userId, + projectId = projectId, + pipelineId = pipelineId, + channelCode = ChannelCode.BS, + values = startParameters, + startType = startType + ) + } + + StartType.TIME_TRIGGER -> { + BuildId( + timerTriggerPipelineBuild( + userId = userId, + projectId = projectId, + pipelineId = pipelineId, + parameters = startParameters, + checkPermission = false + ) ?: "" + ) + } + + StartType.PIPELINE -> { + // 父子流水线核心参数 + startParameters.putAll( + buildVariableService.getAllVariable( + projectId = projectId, + pipelineId = pipelineId, + buildId = buildId, + keys = setOf( + PIPELINE_START_PARENT_PROJECT_ID, + PIPELINE_START_PARENT_PIPELINE_ID, + PIPELINE_START_PARENT_BUILD_ID, + PIPELINE_START_PARENT_BUILD_TASK_ID, + PIPELINE_START_SUB_RUN_MODE, + PIPELINE_START_PARENT_EXECUTE_COUNT + ) + ) + ) + BuildId( + subPipelineStartUpService.callPipelineStartup( + projectId = startParameters[PIPELINE_START_PARENT_PROJECT_ID]!!, + parentPipelineId = startParameters[PIPELINE_START_PARENT_PIPELINE_ID]!!, + buildId = startParameters[PIPELINE_START_PARENT_BUILD_ID]!!, + callProjectId = projectId, + callPipelineId = pipelineId, + atomCode = "SubPipelineExec", + taskId = startParameters[PIPELINE_START_PARENT_BUILD_TASK_ID]!!, + channelCode = ChannelCode.BS, + values = startParameters, + runMode = startParameters[PIPELINE_START_SUB_RUN_MODE] ?: SubPipelineStartUpService.SYNC_RUN_MODE, + executeCount = startParameters[PIPELINE_START_PARENT_EXECUTE_COUNT]?.toInt() + ).data!!.id + ) + } + } + private fun buildRestartPipeline( projectId: String, pipelineId: String, diff --git a/support-files/i18n/process/message_en_US.properties b/support-files/i18n/process/message_en_US.properties index ec2298e4ab7..23236a6e635 100644 --- a/support-files/i18n/process/message_en_US.properties +++ b/support-files/i18n/process/message_en_US.properties @@ -248,7 +248,7 @@ 2101254=Parameter {0} is required and cannot be empty 2101255=Start the build machine repeatedly. The current status of the build machine is: {0} 2101256=pipeline build parameter {0} value exceeds 4000 length limit - +2101260=Same trigger parameters, no longer meet the trigger conditions of the current pipeline latest version {0}. Continuing to run may produce errors. Confirm to continue? ATOM_POST_EXECUTE_TIP=###Tip:this is the post-action hooked by [step{0}]{1}### # 公共变量 ci.build-no=Build number. Only available when the recommended version number is enabled. diff --git a/support-files/i18n/process/message_zh_CN.properties b/support-files/i18n/process/message_zh_CN.properties index 643e85bda27..5343caa0153 100644 --- a/support-files/i18n/process/message_zh_CN.properties +++ b/support-files/i18n/process/message_zh_CN.properties @@ -248,7 +248,7 @@ 2101254=构建入参[{0}]必填,不能为空 2101255=重复启动构建机,当前构建机的状态为:{0} 2101256=流水线变量{0}值超出4000长度限制 - +2101260=同样的触发参数, 已不满足当前流水线最新版本 {0} 的触发条件, 继续运行可能会产生错误, 确认继续吗 ATOM_POST_EXECUTE_TIP=###Tip:this is the post-action hooked by [step{0}]{1}### ci.build-no=构建号,开启推荐版本号时有效 ci.build_num=当前构建的唯一标示ID,从1开始自增