package de.steamwar.commands.ci import java.io.File import java.io.PrintStream import java.nio.channels.FileChannel import java.nio.file.Path import java.nio.file.StandardOpenOption class CiRunner(private val config: CiConfig) { private var logFile: PrintStream? = null fun run(logStream: PrintStream) { this.logFile = logStream try { config.updateState("pending", "Waiting for previous builds") // Use file lock to ensure exclusive CI access FileChannel.open( Path.of(System.getProperty("user.home"), ".cilock"), StandardOpenOption.CREATE, StandardOpenOption.READ, StandardOpenOption.WRITE ).use { channel -> val lock = channel.lock() try { ciMain() } finally { lock.release() } } } catch (e: Exception) { log("Error: ${e.message}") e.printStackTrace(logStream) config.updateState("failure", "Build failed") throw e } } private fun ciMain() { config.updateState("pending", "Building project") val ciConfig = config.getCiConfig() val freshClone = !File(config.wdir).exists() if (freshClone) { File(config.udir).mkdirs() runCommand("git clone -n ${config.repopath}", "Could not clone repository", config.udir) } else { runCommand("git fetch", "Could not fetch updates", config.wdir) } runCommand("git checkout ${config.commit}", "Could not checkout commit", config.wdir) runCommand("git submodule update --init", "Could not init submodules", config.wdir) if (freshClone && ciConfig.setup != null) { for (cmd in ciConfig.setup) { log("Setup: Executing $cmd") runCommand(cmd, "Could not run setup command", config.wdir) } } for (cmd in ciConfig.build) { log("Build: Executing $cmd") runCommand(cmd, "Could not run build command", config.wdir) } val onMaster = config.branch == "refs/heads/${config.getDefaultBranch()}" val releaseBranchName = getReleaseBranchName() val onReleaseBranch = releaseBranchName != null && ciConfig.releaseBranches if (ciConfig.artifacts != null) { log("Checking artifact existence") for ((_, source) in ciConfig.artifacts) { if (!File("${config.wdir}/$source").exists()) { throw RuntimeException("Artifact $source missing") } } if (onMaster) { log("Deploying artifacts") for ((target, source) in ciConfig.artifacts) { log("Copying $source to $target") val expandedTarget = expandPath(target) runCommand("rm -f $expandedTarget", "Could not delete artifact", config.wdir) runCommand("cp -r $source $expandedTarget", "Could not copy artifacts", config.wdir) runCommand("chmod o-w $expandedTarget", "Could not change artifact perms", config.wdir) } } else if (onReleaseBranch) { log("Deploying artifacts for release branch: $releaseBranchName") for ((target, source) in ciConfig.artifacts) { val expandedTarget = expandPath(target) val targetFile = File(expandedTarget) val parentDir = targetFile.parentFile?.absolutePath ?: expandedTarget.substringBeforeLast("/") val fileName = targetFile.name val releaseTargetDir = "$parentDir/$releaseBranchName" val releaseTarget = "$releaseTargetDir/$fileName" log("Copying $source to $releaseTarget") runCommand("mkdir -p $releaseTargetDir", "Could not create release directory", config.wdir) runCommand("rm -f $releaseTarget", "Could not delete artifact", config.wdir) runCommand("cp -r $source $releaseTarget", "Could not copy artifacts", config.wdir) runCommand("chmod o-w $releaseTarget", "Could not change artifact perms", config.wdir) } } } if (ciConfig.release != null && (onMaster || onReleaseBranch)) { log("Running release commands" + if (onReleaseBranch) " for release branch: $releaseBranchName" else "") for (cmd in ciConfig.release) { log("Release: Executing $cmd") runCommand(cmd, "Could not run release command", config.wdir) } } config.updateState("success", "Build successful") } private fun runCommand(command: String, errorMessage: String, workingDir: String) { log("Running: $command") val processBuilder = ProcessBuilder("/bin/sh", "-c", command) .directory(File(workingDir)) .redirectErrorStream(true) val process = processBuilder.start() // Read and log output process.inputStream.bufferedReader().forEachLine { line -> log(line) } val exitCode = process.waitFor() if (exitCode != 0) { log("Running in $workingDir") throw RuntimeException("$errorMessage (exit code: $exitCode)") } } private fun expandPath(path: String): String { return if (path.startsWith("~")) { path.replaceFirst("~", System.getProperty("user.home")) } else { path } } private fun getReleaseBranchName(): String? { val releaseBranchPrefix = "refs/heads/release/" return if (config.branch.startsWith(releaseBranchPrefix)) { config.branch.removePrefix(releaseBranchPrefix) } else { null } } private fun log(message: String) { println(message) logFile?.println(message) logFile?.flush() } }