diff --git a/build.gradle.kts b/build.gradle.kts index 2aa2578..dbecb5e 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -29,9 +29,6 @@ dependencies { implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0-rc-2") implementation("org.jetbrains.exposed:exposed-dao:1.0.0-rc-2") implementation("org.jetbrains.exposed:exposed-kotlin-datetime:1.0.0-rc-2") - - // YAML parsing - implementation("net.mamoe.yamlkt:yamlkt:0.13.0") } tasks.test { diff --git a/src/main/kotlin/Main.kt b/src/main/kotlin/Main.kt index 1501ed1..fd4a9eb 100644 --- a/src/main/kotlin/Main.kt +++ b/src/main/kotlin/Main.kt @@ -3,7 +3,6 @@ package de.steamwar import com.github.ajalt.clikt.core.main import com.github.ajalt.clikt.core.subcommands import de.steamwar.commands.SteamWar -import de.steamwar.commands.ci.CiCommand import de.steamwar.commands.database.DatabaseCommand import de.steamwar.commands.database.InfoCommand import de.steamwar.commands.database.ResetCommand @@ -15,7 +14,6 @@ import de.steamwar.commands.user.UserSearchCommand fun main(args: Array) = SteamWar() .subcommands( - CiCommand(), DatabaseCommand().subcommands(InfoCommand(), ResetCommand()), UserCommand().subcommands(UserInfoCommand(), UserSearchCommand()), DevCommand(), diff --git a/src/main/kotlin/commands/ci/CiCommand.kt b/src/main/kotlin/commands/ci/CiCommand.kt deleted file mode 100644 index 9201713..0000000 --- a/src/main/kotlin/commands/ci/CiCommand.kt +++ /dev/null @@ -1,57 +0,0 @@ -package de.steamwar.commands.ci - -import com.github.ajalt.clikt.core.CliktCommand -import com.github.ajalt.clikt.core.Context -import java.io.File -import java.io.PrintStream -import kotlin.concurrent.thread - -class CiCommand : CliktCommand("ci") { - override val hiddenFromHelp = true - - override fun help(context: Context): String = "CI daemon for processing git push events" - - override fun run() { - echo("SteamWar CI daemon started. Waiting for push events...") - echo("Format: ") - - try { - while (true) { - val input = readlnOrNull() ?: break - val parts = input.split(" ") - - if (parts.size < 3) { - echo("Invalid input format. Expected: ", err = true) - continue - } - - val (oldref, newref, branch) = parts - - // Fork/detach the build process - thread(isDaemon = false) { - processBuild(oldref, newref, branch) - } - } - } catch (e: Exception) { - echo("CI daemon error: ${e.message}", err = true) - } - } - - private fun processBuild(oldref: String, newref: String, branch: String) { - try { - val config = CiConfig(oldref, newref, branch) - - // Create log file - val logFile = File(config.logpath) - logFile.parentFile?.mkdirs() - - PrintStream(logFile).use { logStream -> - val runner = CiRunner(config) - runner.run(logStream) - } - } catch (e: Exception) { - System.err.println("Build failed: ${e.message}") - e.printStackTrace() - } - } -} \ No newline at end of file diff --git a/src/main/kotlin/commands/ci/CiConfig.kt b/src/main/kotlin/commands/ci/CiConfig.kt deleted file mode 100644 index 3188d61..0000000 --- a/src/main/kotlin/commands/ci/CiConfig.kt +++ /dev/null @@ -1,155 +0,0 @@ -package de.steamwar.commands.ci - -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonObject -import kotlinx.serialization.json.jsonPrimitive -import net.mamoe.yamlkt.Yaml -import java.io.File -import java.net.URI -import java.net.http.HttpClient -import java.net.http.HttpRequest -import java.net.http.HttpResponse -import java.nio.channels.FileChannel -import java.nio.file.Path -import java.nio.file.StandardOpenOption -import java.util.Base64 - -@Serializable -data class CiDaemonConfig( - val token: String, - val nextlog: Int, - val logpath: String -) - -@Serializable -data class ProjectCiConfig( - val setup: List? = null, - val build: List, - val artifacts: Map? = null, - val release: List? = null, - @SerialName("release-branches") - val releaseBranches: Boolean = false -) - -@Serializable -data class GiteaRepoInfo( - @SerialName("default_branch") - val defaultBranch: String -) - -class CiConfig( - oldref: String, - newref: String, - val branch: String -) { - val oldcommit: String = oldref - val commit: String = newref - - val user: String = System.getenv("GITEA_REPO_USER_NAME") - ?: throw IllegalStateException("GITEA_REPO_USER_NAME not set") - val repo: String = (System.getenv("GITEA_REPO_NAME") - ?: throw IllegalStateException("GITEA_REPO_NAME not set")).lowercase() - val repopath: String = System.getenv("PWD") - ?: System.getProperty("user.dir") - - private val homeDir: String = System.getProperty("user.home") - val cidir: String = "$homeDir/ci" - - val token: String - val lognumber: Int - val logpath: String - - private val httpClient = HttpClient.newHttpClient() - private val json = Json { ignoreUnknownKeys = true } - private val yaml = Yaml { encodeDefaultValues = false } - - init { - val configPath = "$homeDir/steamwarci.yml" - val configFile = File(configPath) - - FileChannel.open( - Path.of(configPath), - StandardOpenOption.CREATE, - StandardOpenOption.READ, - StandardOpenOption.WRITE - ).use { channel -> - val lock = channel.lock() - try { - val daemonConfig = if (configFile.length() == 0L) { - throw IllegalStateException("$configPath is empty or missing required keys") - } else { - yaml.decodeFromString(CiDaemonConfig.serializer(), configFile.readText()) - } - - token = daemonConfig.token - lognumber = daemonConfig.nextlog - logpath = "${daemonConfig.logpath}/${daemonConfig.nextlog}.txt" - - val updated = daemonConfig.copy(nextlog = daemonConfig.nextlog + 1) - configFile.writeText(yaml.encodeToString(CiDaemonConfig.serializer(), updated)) - } finally { - lock.release() - } - } - } - - fun apiQuery(method: String, endpoint: String, body: String? = null): String { - val requestBuilder = HttpRequest.newBuilder() - .uri(URI.create("https://git.steamwar.de/api/v1/repos/$user/$repo$endpoint?token=$token")) - .header("Content-Type", "application/json") - - val request = when (method.uppercase()) { - "GET" -> requestBuilder.GET().build() - "POST" -> requestBuilder.POST( - body?.let { HttpRequest.BodyPublishers.ofString(it) } - ?: HttpRequest.BodyPublishers.noBody() - ).build() - - else -> throw IllegalArgumentException("Unsupported HTTP method: $method") - } - - val response = httpClient.send(request, HttpResponse.BodyHandlers.ofString()) - if (response.statusCode() !in 200..299) { - throw RuntimeException("Could not execute API request: ${response.statusCode()} ${response.body()}") - } - return response.body() - } - - fun updateState(state: String, description: String) { - println("Statusupdate $state $description") - val body = """ - { - "context": "SteamWarCI", - "description": "$description", - "state": "$state", - "target_url": "https://steamwar.de/buildlogs/$lognumber.txt" - } - """.trimIndent() - apiQuery("POST", "/statuses/$commit", body) - } - - fun getCiConfig(): ProjectCiConfig { - println("Loading master/steamwarci.yml") - val response = apiQuery("GET", "/contents/steamwarci.yml") - val jsonObj = json.parseToJsonElement(response) as JsonObject - val content = jsonObj["content"]?.jsonPrimitive?.content - ?: throw RuntimeException("Could not get steamwarci.yml content") - val decoded = String(Base64.getDecoder().decode(content.replace("\n", ""))) - return yaml.decodeFromString(ProjectCiConfig.serializer(), decoded) - } - - fun getDefaultBranch(): String { - println("Getting default branch") - val response = apiQuery("GET", "") - val repoInfo = json.decodeFromString(response) - return repoInfo.defaultBranch - } - - val udir: String - get() = "$cidir/$user" - val wdir: String - get() = "$udir/$repo" -} - diff --git a/src/main/kotlin/commands/ci/CiRunner.kt b/src/main/kotlin/commands/ci/CiRunner.kt deleted file mode 100644 index b6f6167..0000000 --- a/src/main/kotlin/commands/ci/CiRunner.kt +++ /dev/null @@ -1,166 +0,0 @@ -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") - - runCommand("pwd", "Could not get working directory", config.wdir) - - val ciConfig = config.getCiConfig() - val freshClone = !File(config.wdir).exists() - - if (freshClone) { - File(config.udir).mkdirs() - runCommand("git clone -n ${config.repopath} ${config.repo}", "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/bash", "-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() - } -} -