Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -85,6 +138,7 @@ data class Pipeline(
writer.closure("stages", stages::toGroovy)
post.toGroovy(writer)
}
post.toGroovy(writer)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,28 @@ 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?
abstract val customWorkspace: Var.Literal.Str?
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,
Expand All @@ -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,
Expand All @@ -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)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Loading