diff --git a/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/Agent.kt b/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/Agent.kt index 22bd00e4..f5807f8a 100644 --- a/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/Agent.kt +++ b/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/Agent.kt @@ -2,4 +2,11 @@ package com.code42.jenkins.pipelinekt.core import com.code42.jenkins.pipelinekt.core.writer.GroovyScript -interface Agent : GroovyScript +interface Agent : GroovyScript { + /** + * Returns a copy of this agent configured to use the customWorkspacePath variable. + * This is used for multibranch pipelines to ensure consistent workspace paths. + * Default implementation returns this agent unchanged. + */ + fun withCustomWorkspaceVariable(): Agent = this +} diff --git a/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/Pipeline.kt b/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/Pipeline.kt index 696f30bd..2385dc7b 100644 --- a/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/Pipeline.kt +++ b/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/Pipeline.kt @@ -30,16 +30,69 @@ data class Pipeline( val customWorkspace: String? = null, val useMultibranchWorkspace: Boolean = true, ) : GroovyScript { + + /** + * Transforms an agent to use customWorkspacePath for multibranch pipelines. + */ + private fun Agent.withMultibranchWorkspace(): Agent { + return if (useMultibranchWorkspace || customWorkspace != null) { + this.withCustomWorkspaceVariable() + } else { + this + } + } + + /** + * Recursively transforms stages to use customWorkspacePath for their agents. + */ + private fun Stage.withMultibranchWorkspace(pipeline: Pipeline): Stage { + val transformedAgent = this.agent?.withMultibranchWorkspace() + + return when (this) { + is Stage.Steps -> this.copy(agent = transformedAgent) + is Stage.Parallel -> this.copy( + agent = transformedAgent, + stages = stages.map { it.withMultibranchWorkspace(pipeline) }, + ) + is Stage.Sequence -> this.copy( + agent = transformedAgent, + stages = stages.map { it.withMultibranchWorkspace(pipeline) }, + ) + is Stage.Matrix -> this // Matrix doesn't support agent transformation + } + } + override fun toGroovy(writer: GroovyWriter) { if (methods.isNotEmpty()) { methods.toGroovy(writer) } + // Calculate custom workspace path BEFORE pipeline block for multibranch pipelines + // This ensures each branch gets its own workspace to avoid conflicts and + // prevent workspace path truncation issues in Jenkins + if (customWorkspace != null) { + writer.writeln("// Custom workspace specified") + writer.writeln("def customWorkspacePath = \"$customWorkspace\"") + writer.writeln("") + } else if (useMultibranchWorkspace) { + writer.writeln("// Calculate custom workspace for multibranch pipelines") + writer.writeln("// This prevents workspace path truncation and branch conflicts") + writer.writeln("// by using a relative path that includes the sanitized branch name") + writer.writeln("def customWorkspacePath = null") + writer.writeln("if (env.BRANCH_NAME) {") + val innerWriter = writer.inner() + innerWriter.writeln("def safeBranch = env.BRANCH_NAME.replaceAll(/[^A-Za-z0-9._-]/, '_')") + innerWriter.writeln("customWorkspacePath = \"../\${env.JOB_NAME}-\${safeBranch}\"") + writer.writeln("}") + writer.writeln("") + } + writer.closure("pipeline") { writer -> if (environment.isNotEmpty()) { writer.closure("environment", environment::toGroovy) } - agent?.toGroovy(writer) + // Transform agent to use customWorkspacePath if multibranch is enabled + agent?.withMultibranchWorkspace()?.toGroovy(writer) if (tools.isNotEmpty()) { writer.closure("tools", tools::toGroovy) } @@ -85,6 +138,7 @@ data class Pipeline( writer.closure("stages", stages::toGroovy) post.toGroovy(writer) } + post.toGroovy(writer) } } } diff --git a/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/agent/DockerAgent.kt b/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/agent/DockerAgent.kt index 575c9faf..f9701bad 100644 --- a/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/agent/DockerAgent.kt +++ b/core/src/main/kotlin/com/code42/jenkins/pipelinekt/core/agent/DockerAgent.kt @@ -4,6 +4,14 @@ import com.code42.jenkins.pipelinekt.core.Agent import com.code42.jenkins.pipelinekt.core.vars.Var import com.code42.jenkins.pipelinekt.core.writer.GroovyWriter +/** + * Sealed class representing Docker-based Jenkins agents. + * + * Docker agents can use custom workspaces to prevent path truncation + * in multibranch pipelines. When useCustomWorkspaceVariable is true, + * the customWorkspace will reference the dynamically calculated + * customWorkspacePath variable. + */ sealed class DockerAgent : Agent { abstract val args: Var.Literal.Str? abstract val label: Var.Literal.Str? @@ -11,7 +19,13 @@ sealed class DockerAgent : Agent { abstract val registryUrl: Var.Literal.Str? abstract val registryCredentialsId: Var.Literal.Str? abstract val reuseNode: Var.Literal.Bool? + abstract val useCustomWorkspaceVariable: Boolean + /** + * Docker agent using a container image. + * + * @param useCustomWorkspaceVariable If true, uses customWorkspacePath variable for multibranch support + */ data class Image( val image: Var.Literal.Str, override val args: Var.Literal.Str? = null, @@ -27,15 +41,26 @@ sealed class DockerAgent : Agent { writer.writeln("image ${image.toGroovy()}") args?.let { writer.writeln("args ${it.toGroovy()}") } label?.let { writer.writeln("label ${it.toGroovy()}") } - customWorkspace?.let { writer.writeln("customWorkspace ${it.toGroovy()}") } + if (useCustomWorkspaceVariable) { + writer.writeln("customWorkspace customWorkspacePath") + } else { + customWorkspace?.let { writer.writeln("customWorkspace ${it.toGroovy()}") } + } registryUrl?.let { writer.writeln("registryUrl ${it.toGroovy()}") } registryCredentialsId?.let { writer.writeln("registryCredentialsId ${it.toGroovy()}") } reuseNode?.let { writer.writeln("reuseNode ${it.toGroovy()}") } } } } + + override fun withCustomWorkspaceVariable() = this.copy(useCustomWorkspaceVariable = true) } + /** + * Docker agent using a Dockerfile. + * + * @param useCustomWorkspaceVariable If true, uses customWorkspacePath variable for multibranch support + */ data class File( val filename: Var.Literal.Str, val dir: Var.Literal.Str? = null, @@ -55,12 +80,18 @@ sealed class DockerAgent : Agent { additionalBuildArgs?.let { writer.writeln("additionalBuildArgs ${it.toGroovy()}") } dir?.let { writer.writeln("dir ${it.toGroovy()}") } label?.let { writer.writeln("label ${it.toGroovy()}") } - customWorkspace?.let { writer.writeln("customWorkspace ${it.toGroovy()}") } + if (useCustomWorkspaceVariable) { + writer.writeln("customWorkspace customWorkspacePath") + } else { + customWorkspace?.let { writer.writeln("customWorkspace ${it.toGroovy()}") } + } registryUrl?.let { writer.writeln("registryUrl ${it.toGroovy()}") } registryCredentialsId?.let { writer.writeln("registryCredentialsId ${it.toGroovy()}") } reuseNode?.let { writer.writeln("reuseNode ${it.toGroovy()}") } } } } + + override fun withCustomWorkspaceVariable() = this.copy(useCustomWorkspaceVariable = true) } } diff --git a/internal/src/main/kotlin/com/code42/jenkins/pipelinekt/internal/agent/Label.kt b/internal/src/main/kotlin/com/code42/jenkins/pipelinekt/internal/agent/Label.kt index a1ac8263..c913675b 100644 --- a/internal/src/main/kotlin/com/code42/jenkins/pipelinekt/internal/agent/Label.kt +++ b/internal/src/main/kotlin/com/code42/jenkins/pipelinekt/internal/agent/Label.kt @@ -4,10 +4,33 @@ import com.code42.jenkins.pipelinekt.core.Agent import com.code42.jenkins.pipelinekt.core.vars.Var import com.code42.jenkins.pipelinekt.core.writer.GroovyWriter -data class Label(val label: Var.Literal.Str) : Agent { +/** + * Represents a simple Jenkins label agent. + * + * When useCustomWorkspaceVariable is true (typically in multibranch pipelines), + * this will expand to use a node block with customWorkspace to prevent path + * truncation and branch conflicts. + * + * @param label The agent label to match against + * @param useCustomWorkspaceVariable If true, expands to node block with customWorkspacePath variable + */ +data class Label( + val label: Var.Literal.Str, + val useCustomWorkspaceVariable: Boolean = false, +) : Agent { override fun toGroovy(writer: GroovyWriter) { writer.closure("agent") { writer -> - writer.writeln("label ${label.toGroovy()}") + if (useCustomWorkspaceVariable) { + // Expand to node block with customWorkspace for multibranch support + writer.closure("node") { writer -> + writer.writeln("label ${label.toGroovy()}") + writer.writeln("customWorkspace customWorkspacePath") + } + } else { + writer.writeln("label ${label.toGroovy()}") + } } } + + override fun withCustomWorkspaceVariable() = this.copy(useCustomWorkspaceVariable = true) } diff --git a/internal/src/main/kotlin/com/code42/jenkins/pipelinekt/internal/agent/Node.kt b/internal/src/main/kotlin/com/code42/jenkins/pipelinekt/internal/agent/Node.kt index 6e8f1f39..b40543fe 100644 --- a/internal/src/main/kotlin/com/code42/jenkins/pipelinekt/internal/agent/Node.kt +++ b/internal/src/main/kotlin/com/code42/jenkins/pipelinekt/internal/agent/Node.kt @@ -4,13 +4,36 @@ import com.code42.jenkins.pipelinekt.core.Agent import com.code42.jenkins.pipelinekt.core.vars.Var import com.code42.jenkins.pipelinekt.core.writer.GroovyWriter -data class Node(val label: Var.Literal.Str, val customWorkspace: Var.Literal.Str? = null) : Agent { +/** + * Represents a Jenkins node agent with optional custom workspace. + * + * When used in a pipeline with useMultibranchWorkspace=true, the customWorkspace + * will reference the dynamically calculated customWorkspacePath variable, which + * ensures each branch gets its own workspace and prevents path truncation issues. + * + * @param label The node label to match against + * @param customWorkspace Optional custom workspace path. If null and pipeline uses multibranch, + * will automatically reference the calculated customWorkspacePath variable. + * @param useCustomWorkspaceVariable If true, uses the customWorkspacePath variable instead of a literal value + */ +data class Node( + val label: Var.Literal.Str, + val customWorkspace: Var.Literal.Str? = null, + val useCustomWorkspaceVariable: Boolean = false, +) : Agent { override fun toGroovy(writer: GroovyWriter) { writer.closure("agent") { writer -> writer.closure("node") { writer -> writer.writeln("label ${label.toGroovy()}") - customWorkspace?.let { writer.writeln("customWorkspace ${it.toGroovy()}") } + if (useCustomWorkspaceVariable) { + // Reference the customWorkspacePath variable calculated before pipeline block + writer.writeln("customWorkspace customWorkspacePath") + } else { + customWorkspace?.let { writer.writeln("customWorkspace ${it.toGroovy()}") } + } } } } + + override fun withCustomWorkspaceVariable() = this.copy(useCustomWorkspaceVariable = true) }