Remove CI
All checks were successful
SteamWarCI Build successful

This commit is contained in:
2026-01-23 22:51:55 +01:00
parent 83d52d7c62
commit 06de3c5b43
5 changed files with 0 additions and 383 deletions

View File

@@ -29,9 +29,6 @@ dependencies {
implementation("org.jetbrains.exposed:exposed-jdbc:1.0.0-rc-2") 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-dao:1.0.0-rc-2")
implementation("org.jetbrains.exposed:exposed-kotlin-datetime: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 { tasks.test {

View File

@@ -3,7 +3,6 @@ package de.steamwar
import com.github.ajalt.clikt.core.main import com.github.ajalt.clikt.core.main
import com.github.ajalt.clikt.core.subcommands import com.github.ajalt.clikt.core.subcommands
import de.steamwar.commands.SteamWar import de.steamwar.commands.SteamWar
import de.steamwar.commands.ci.CiCommand
import de.steamwar.commands.database.DatabaseCommand import de.steamwar.commands.database.DatabaseCommand
import de.steamwar.commands.database.InfoCommand import de.steamwar.commands.database.InfoCommand
import de.steamwar.commands.database.ResetCommand import de.steamwar.commands.database.ResetCommand
@@ -15,7 +14,6 @@ import de.steamwar.commands.user.UserSearchCommand
fun main(args: Array<String>) = SteamWar() fun main(args: Array<String>) = SteamWar()
.subcommands( .subcommands(
CiCommand(),
DatabaseCommand().subcommands(InfoCommand(), ResetCommand()), DatabaseCommand().subcommands(InfoCommand(), ResetCommand()),
UserCommand().subcommands(UserInfoCommand(), UserSearchCommand()), UserCommand().subcommands(UserInfoCommand(), UserSearchCommand()),
DevCommand(), DevCommand(),

View File

@@ -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: <oldref> <newref> <branch>")
try {
while (true) {
val input = readlnOrNull() ?: break
val parts = input.split(" ")
if (parts.size < 3) {
echo("Invalid input format. Expected: <oldref> <newref> <branch>", 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()
}
}
}

View File

@@ -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<String>? = null,
val build: List<String>,
val artifacts: Map<String, String>? = null,
val release: List<String>? = 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<GiteaRepoInfo>(response)
return repoInfo.defaultBranch
}
val udir: String
get() = "$cidir/$user"
val wdir: String
get() = "$udir/$repo"
}

View File

@@ -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()
}
}