diff --git a/src/main/kotlin/alfred/web/core/AlfredHome.kt b/src/main/kotlin/alfred/web/core/AlfredHome.kt new file mode 100644 index 0000000..6b77174 --- /dev/null +++ b/src/main/kotlin/alfred/web/core/AlfredHome.kt @@ -0,0 +1,22 @@ +package alfred.web.core + +import alfred.web.core.build.BuildId +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import java.nio.file.Paths + +@Service +class AlfredHome( + @Value("\${ALFRED_HOME}") + private val home: String, +) { + + val homePath = Paths.get(home) + + fun logsDir() = + homePath.resolve("logs") + + fun buildConfig(buildId: BuildId) = + homePath.resolve("builds/${buildId}.properties") + +} diff --git a/src/main/kotlin/alfred/web/core/build/Builds.kt b/src/main/kotlin/alfred/web/core/build/Builds.kt index f30a928..082eedb 100644 --- a/src/main/kotlin/alfred/web/core/build/Builds.kt +++ b/src/main/kotlin/alfred/web/core/build/Builds.kt @@ -1,32 +1,30 @@ package alfred.web.core.build +import alfred.web.core.AlfredHome import com.fasterxml.jackson.annotation.JsonIgnore import org.slf4j.Logger import org.slf4j.LoggerFactory -import org.springframework.beans.factory.annotation.Value import org.springframework.http.HttpStatus import org.springframework.stereotype.Service import org.springframework.web.bind.annotation.ResponseStatus import java.nio.file.Files.createFile -import java.nio.file.Paths import java.time.LocalDateTime import java.time.format.DateTimeFormatter import java.util.* @Service class Builds( - @Value("\${ALFRED_HOME}") - private val home: String + val alfredHome: AlfredHome ) { private val logger: Logger = LoggerFactory.getLogger(this::class.java) init { - logger.info("ALFRED_HOME is $home") + logger.info("ALFRED_HOME is ${alfredHome.homePath.toFile().absolutePath}") } fun buildConfig(build: BuildId): BuildConfig { - val path = Paths.get(home, "builds", "$build.properties") + val path = alfredHome.buildConfig(build) if (!path.toFile().exists()) { throw UnknownBuild(build) @@ -47,10 +45,11 @@ class Builds( val nowStr = LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME) val fileName = "$build.$nowStr.log" - val path = Paths.get(home, "logs", fileName) - path.toFile().parentFile.mkdirs() + val logsDir = alfredHome.logsDir() + logsDir.toFile().mkdir() - return LogFile(createFile(path).toFile()) + val logFilePath = logsDir.resolve(fileName) + return LogFile(createFile(logFilePath).toFile()) } } diff --git a/src/main/kotlin/alfred/web/core/build/LogFile.kt b/src/main/kotlin/alfred/web/core/build/LogFile.kt index b970826..65ac81d 100644 --- a/src/main/kotlin/alfred/web/core/build/LogFile.kt +++ b/src/main/kotlin/alfred/web/core/build/LogFile.kt @@ -17,10 +17,9 @@ class LogFile( val logger: Logger = LoggerFactory.getLogger(this::class.java) - fun header(buildId: BuildId, rev: String?, workspace: Path) { + fun header(buildId: BuildId, rev: String?) { append("At your service.") append("Build $buildId, rev [${rev ?: "-none-"}] started at ${ZonedDateTime.now()}") - append("Workspace directory is: ${workspace.toFile().absolutePath}") append("------------------------------------------------------------") } diff --git a/src/main/kotlin/alfred/web/core/build/Workspaces.kt b/src/main/kotlin/alfred/web/core/build/Workspaces.kt index b8cac99..cf44402 100644 --- a/src/main/kotlin/alfred/web/core/build/Workspaces.kt +++ b/src/main/kotlin/alfred/web/core/build/Workspaces.kt @@ -16,7 +16,7 @@ class Workspaces { try { block(wsDir) } finally { - wsDir.toFile().deleteRecursively() + cleanUp(wsDir) } } diff --git a/src/main/kotlin/alfred/web/core/process/Git.kt b/src/main/kotlin/alfred/web/core/process/Git.kt new file mode 100644 index 0000000..19e5b37 --- /dev/null +++ b/src/main/kotlin/alfred/web/core/process/Git.kt @@ -0,0 +1,64 @@ +package alfred.web.core.process + +import alfred.web.core.Handle +import alfred.web.core.Handles +import alfred.web.core.build.BuildContext +import alfred.web.core.build.BuildId +import alfred.web.core.build.Workspace +import org.springframework.stereotype.Service +import java.util.concurrent.TimeUnit + +@Service +class Git( + val processes: Processes, + val handles: Handles, +) { + + fun clone(ctx: BuildContext, ws: Workspace) { + ctx.log("cloning ${ctx.config.gitRepo} into $ws") + + val proc = processes.builder(ctx.config, ctx.logFile, "") + .command("git", "clone", ctx.config.gitRepo, ".") + .directory(ws.toFile()) + .start() + handles.add(Handle(proc.toHandle(), ctx.buildId)) + + val cloneSuccess = proc.waitFor(ctx.config.gitCloneTimeout, TimeUnit.SECONDS) + if (!cloneSuccess) { + throw FailedToClone(ctx.buildId, ctx.config.gitRepo ?: "[no repo configured]") + } + } + + fun checkout(ctx: BuildContext, ws: Workspace) { + ctx.log("checkout rev ${ctx.rev}") + + val proc = processes.builder(ctx.config, ctx.logFile, "") + .command("git", "checkout", ctx.rev) + .directory(ws.toFile()) + .start() + handles.add(Handle(proc.toHandle(), ctx.buildId)) + + val checkoutSuccess = proc.waitFor(ctx.config.gitCloneTimeout, TimeUnit.SECONDS) + if (!checkoutSuccess) { + throw FailedToCheckout(ctx.buildId, ctx.config.gitRepo!!, ctx.rev) + } + } + + sealed class GitException(msg: String): Exception(msg) + + private class FailedToClone( + buildId: BuildId, + gitRepo: String + ) : GitException( + "failed to clone $gitRepo for build id $buildId" + ) + + private class FailedToCheckout( + buildId: BuildId, + gitRepo: String, + rev: String + ) : GitException( + "failed to checkout revision $rev on repo $gitRepo for build id $buildId" + ) + +} diff --git a/src/main/kotlin/alfred/web/core/process/Script.kt b/src/main/kotlin/alfred/web/core/process/Script.kt new file mode 100644 index 0000000..3ec9497 --- /dev/null +++ b/src/main/kotlin/alfred/web/core/process/Script.kt @@ -0,0 +1,34 @@ +package alfred.web.core.process + +import alfred.web.core.Handle +import alfred.web.core.Handles +import alfred.web.core.build.BuildContext +import alfred.web.core.build.Workspace +import org.springframework.stereotype.Service +import java.io.File + +@Service +class Script( + val handles: Handles, + val processes: Processes +) { + + fun execute(ctx: BuildContext, ws: Workspace, scriptFile: File): Process { + ctx.log("Running build file: ${scriptFile.name}") + + val scriptProcess = processes.builder(ctx.config, ctx.logFile, ctx.rev) + .command(scriptFile.absolutePath) + .directory(ws.toFile()) + .start() + handles.add(Handle(scriptProcess.toHandle(), ctx.buildId)) + + return scriptProcess + } + + fun exists(scriptFile: File) = scriptFile.exists() + + fun onExit(process: Process, block: () -> Unit) { + process.onExit().whenComplete { _, _ -> block() } + } + +} diff --git a/src/main/kotlin/alfred/web/core/runner/GitRunner.kt b/src/main/kotlin/alfred/web/core/runner/GitRunner.kt index 8d806a3..70ed901 100644 --- a/src/main/kotlin/alfred/web/core/runner/GitRunner.kt +++ b/src/main/kotlin/alfred/web/core/runner/GitRunner.kt @@ -1,7 +1,5 @@ package alfred.web.core.runner -import alfred.web.core.Handle -import alfred.web.core.Handles import alfred.web.core.build.BuildContext import alfred.web.core.build.BuildId import alfred.web.core.build.Builds @@ -9,22 +7,23 @@ import alfred.web.core.build.ProcessInfo import alfred.web.core.build.Workspace import alfred.web.core.build.Workspaces import alfred.web.core.event.BuildFinished +import alfred.web.core.process.Git import alfred.web.core.process.Processes +import alfred.web.core.process.Script import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service -import java.util.concurrent.TimeUnit import kotlin.collections.forEach -import kotlin.jvm.java @Service class GitRunner( val builds: Builds, - val handles: Handles, val processes: Processes, val workspaces: Workspaces, - val eventPublisher: ApplicationEventPublisher + val eventPublisher: ApplicationEventPublisher, + val git: Git, + val script: Script ) { val scriptsDir = ".alfred" @@ -36,7 +35,7 @@ class GitRunner( val logFile = builds.createLogFile(buildId) logger.info("preparing process for build $buildId") - logger.info("log file: $logFile") + logger.info("log file: ${logFile.backingFile.absolutePath}") val ctx = BuildContext( buildId = buildId, @@ -46,74 +45,37 @@ class GitRunner( ) processes.startThread { - workspaces.withWorkspace(ctx) { wsPath -> - logFile.header(buildId, rev, wsPath) + logFile.header(buildId, rev) - clone(ctx, wsPath) - checkout(ctx, wsPath) - execScripts(ctx, wsPath) + workspaces.withWorkspace(ctx) { ws -> - logFile.footer() + git.clone(ctx, ws) + git.checkout(ctx, ws) + execScripts(ctx, ws) - eventPublisher.publishEvent(BuildFinished(ctx)) + logFile.footer() } - } - return ProcessInfo(-1, logFile) - } - - private fun clone(ctx: BuildContext, ws: Workspace) { - ctx.log("cloning ${ctx.config.gitRepo} into $ws") - - val proc = processes.builder(ctx.config, ctx.logFile, "") - .command("git", "clone", ctx.config.gitRepo, ".") - .directory(ws.toFile()) - .start() - handles.add(Handle(proc.toHandle(), ctx.buildId)) - - val cloneSuccess = proc.waitFor(ctx.config.gitCloneTimeout, TimeUnit.SECONDS) - if (!cloneSuccess) { - throw FailedToClone(ctx.buildId, ctx.config.gitRepo ?: "[no repo configured]") + eventPublisher.publishEvent(BuildFinished(ctx)) } - } - private fun checkout(ctx: BuildContext, ws: Workspace) { - ctx.log("checkout rev ${ctx.rev}") - - val proc = processes.builder(ctx.config, ctx.logFile, "") - .command("git", "checkout", ctx.rev) - .directory(ws.toFile()) - .start() - handles.add(Handle(proc.toHandle(), ctx.buildId)) - - val checkoutSuccess = proc.waitFor(ctx.config.gitCloneTimeout, TimeUnit.SECONDS) - if (!checkoutSuccess) { - throw FailedToCheckout(ctx.buildId, ctx.config.gitRepo!!, ctx.rev) - } + return ProcessInfo(-1, logFile) } private fun execScripts(ctx: BuildContext, ws: Workspace) { ctx.log("looking for scripts $scriptFiles in $scriptsDir/") - scriptFiles.forEach { script -> - val scriptFile = shFile(ws, script) - if (scriptFile.exists()) { - ctx.log("found script $script, running it") + scriptFiles.forEach { scriptName -> + val scriptFile = shFile(ws, scriptName) + if (script.exists(scriptFile)) { + ctx.log("found script $scriptName, running it") - ctx.log("Running build file: $script") + val proc = script.execute(ctx, ws, scriptFile) + proc.waitFor() - val scriptProcess = processes.builder(ctx.config, ctx.logFile, ctx.rev) - .command(scriptFile.absolutePath) - .directory(ws.toFile()) - .start() - handles.add(Handle(scriptProcess.toHandle(), ctx.buildId)) - - val ret = scriptProcess.waitFor() - ctx.log("script $script returned $ret") - - ctx.log("$script returned $ret") + ctx.log("$scriptName returned ${proc.exitValue()}") } else { - ctx.log("Build file $scriptsDir/$script not found") + ctx.log("Build file $scriptsDir/$scriptName not found") } } } @@ -124,19 +86,3 @@ class GitRunner( val logger: Logger = LoggerFactory.getLogger(this::class.java) } - - -class FailedToClone( - buildId: BuildId, - gitRepo: String -) : RuntimeException( - "failed to clone $gitRepo for build id $buildId" -) - -class FailedToCheckout( - buildId: BuildId, - gitRepo: String, - rev: String -) : RuntimeException( - "failed to checkout revision $rev on repo $gitRepo for build id $buildId" -) diff --git a/src/main/kotlin/alfred/web/core/runner/ScriptRunner.kt b/src/main/kotlin/alfred/web/core/runner/ScriptRunner.kt index 4561643..8d2beba 100644 --- a/src/main/kotlin/alfred/web/core/runner/ScriptRunner.kt +++ b/src/main/kotlin/alfred/web/core/runner/ScriptRunner.kt @@ -1,22 +1,20 @@ package alfred.web.core.runner -import alfred.web.core.Handle -import alfred.web.core.Handles import alfred.web.core.build.* import alfred.web.core.event.BuildFinished -import alfred.web.core.process.Processes +import alfred.web.core.process.Script import org.slf4j.Logger import org.slf4j.LoggerFactory import org.springframework.context.ApplicationEventPublisher import org.springframework.stereotype.Service +import java.io.File @Service class ScriptRunner( val builds: Builds, - val handles: Handles, - val processes: Processes, val workspaces: Workspaces, - val eventPublisher: ApplicationEventPublisher + val eventPublisher: ApplicationEventPublisher, + val script: Script ) { fun run(buildId: BuildId, rev: String?): ProcessInfo { @@ -41,27 +39,27 @@ class ScriptRunner( return ProcessInfo(pid = pid, logFile = logFile) } - private fun runScript( - ctx: BuildContext - ): Process { + private fun runScript(ctx: BuildContext): Process { + ctx.logFile.header(ctx.buildId, ctx.rev) + val wsDir = workspaces.prepare(ctx) - ctx.logFile.header(ctx.buildId, ctx.rev, wsDir) ctx.log("running ${ctx.config.script} in ${wsDir.toAbsolutePath()}") - val process = processes.builder(ctx) - .command(ctx.config.script) - .directory(wsDir.toFile()) - .start() - handles.add(Handle(process.toHandle(), ctx.buildId)) - process.onExit().whenComplete { _, _ -> + val proc = script.execute( + ctx, + wsDir, + File(ctx.config.script!!) + ) + + script.onExit(proc) { ctx.logFile.footer() workspaces.cleanUp(wsDir) eventPublisher.publishEvent(BuildFinished(ctx)) } - return process + return proc } val logger: Logger = LoggerFactory.getLogger(this::class.java) diff --git a/src/test/kotlin/alfred/web/core/build/BuildsTest.kt b/src/test/kotlin/alfred/web/core/build/BuildsTest.kt index 44cd5e1..b178680 100644 --- a/src/test/kotlin/alfred/web/core/build/BuildsTest.kt +++ b/src/test/kotlin/alfred/web/core/build/BuildsTest.kt @@ -1,5 +1,6 @@ package alfred.web.core.build +import alfred.web.core.AlfredHome import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertThrows import java.nio.file.Paths @@ -44,10 +45,12 @@ class BuildsTest { private fun builds() = Builds( - Paths.get("") - .toAbsolutePath() - .resolve("src/test/resources/home-1") - .toString() + AlfredHome( + Paths.get("") + .toAbsolutePath() + .resolve("src/test/resources/home-1") + .toString() + ) ) } diff --git a/src/test/kotlin/alfred/web/core/commonMocks.kt b/src/test/kotlin/alfred/web/core/commonMocks.kt new file mode 100644 index 0000000..cf50193 --- /dev/null +++ b/src/test/kotlin/alfred/web/core/commonMocks.kt @@ -0,0 +1,12 @@ +package alfred.web.core + +import io.mockk.justRun +import io.mockk.mockk +import org.springframework.context.ApplicationEventPublisher +import kotlin.reflect.KClass + +fun eventPublisher(eventType: KClass): ApplicationEventPublisher { + val eventPublisher = mockk() + justRun { eventPublisher.publishEvent(any(eventType)) } + return eventPublisher +} diff --git a/src/test/kotlin/alfred/web/core/runner/GitRunnerTest.kt b/src/test/kotlin/alfred/web/core/runner/GitRunnerTest.kt new file mode 100644 index 0000000..2ce3c3a --- /dev/null +++ b/src/test/kotlin/alfred/web/core/runner/GitRunnerTest.kt @@ -0,0 +1,73 @@ +package alfred.web.core.runner + +import alfred.web.core.AlfredHome +import alfred.web.core.build.Builds +import alfred.web.core.build.Workspaces +import alfred.web.core.event.BuildFinished +import alfred.web.core.eventPublisher +import alfred.web.core.process.Git +import alfred.web.core.process.ProcessEnvironment +import alfred.web.core.process.Processes +import alfred.web.core.process.Script +import io.mockk.every +import io.mockk.justRun +import io.mockk.mockk +import io.mockk.spyk +import io.mockk.verify +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.io.TempDir +import java.nio.file.Path +import java.nio.file.Paths + +class GitRunnerTest { + + @Test + fun runs(@TempDir logsDir: Path) { + // Given + val eventPublisher = eventPublisher(BuildFinished::class) + + val git = mockk() + justRun { git.clone(any(), any()) } + justRun { git.checkout(any(), any()) } + + val script = mockk