implemented 12-deployment.md: added systemd service generation (init --systemd) and migration guide from legacy script; introduced JSON-file persistence, server-rendered UI with polling, and reverse-proxy-based deployment; updated documentation
This commit is contained in:
@@ -3,6 +3,7 @@ package de.hoennig.gittally.commands
|
||||
import de.hoennig.gittally.git.GitService
|
||||
import org.springframework.stereotype.Component
|
||||
import picocli.CommandLine.Command
|
||||
import picocli.CommandLine.Option
|
||||
import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
@@ -17,6 +18,18 @@ class InitCommand(
|
||||
) : Runnable {
|
||||
var workingDir: Path = Paths.get(".")
|
||||
|
||||
@Option(
|
||||
names = ["--systemd"],
|
||||
description = ["also generate a systemd user unit that runs `gittally server` for this repository"],
|
||||
)
|
||||
var systemd: Boolean = false
|
||||
|
||||
/** Replaceable for tests: the jar this JVM was started from, or null when not run via `java -jar`. */
|
||||
internal var jarPathResolver: () -> Path? = { runningJarPath() }
|
||||
|
||||
/** Replaceable for tests: the `java` binary of the current JVM. */
|
||||
internal var javaExecutableResolver: () -> Path = { Paths.get(System.getProperty("java.home"), "bin", "java") }
|
||||
|
||||
override fun run() {
|
||||
val normalizedWorkingDir = workingDir.toAbsolutePath().normalize()
|
||||
val root =
|
||||
@@ -32,6 +45,9 @@ class InitCommand(
|
||||
|
||||
createRepoInstallConfig(root, detected, normalizedWorkingDir)
|
||||
createProjectConfig(root, detected, normalizedWorkingDir)
|
||||
if (systemd) {
|
||||
createSystemdFiles(root, normalizedWorkingDir)
|
||||
}
|
||||
}
|
||||
|
||||
private fun detectFromUrl(url: String?): DetectedValues {
|
||||
@@ -165,10 +181,70 @@ class InitCommand(
|
||||
println("created ${file.toFile().relativeTo(normalizedWorkingDir.toFile())}")
|
||||
}
|
||||
|
||||
private fun createSystemdFiles(
|
||||
root: Path,
|
||||
normalizedWorkingDir: Path,
|
||||
) {
|
||||
val jarPath = jarPathResolver()
|
||||
if (jarPath == null) {
|
||||
println("Error: cannot determine the GitTally jar path — run `init --systemd` via `java -jar <path-to>/gittally.jar`")
|
||||
return
|
||||
}
|
||||
val gittallyDir = root.resolve(".git/gittally")
|
||||
gittallyDir.toFile().mkdirs()
|
||||
val unitName = SystemdServiceFiles.unitName(root)
|
||||
val unitFile = gittallyDir.resolve(unitName)
|
||||
val envFile = gittallyDir.resolve(SystemdServiceFiles.ENV_FILE_NAME)
|
||||
|
||||
unitFile.toFile().writeText(
|
||||
SystemdServiceFiles.unitFileContent(
|
||||
repoRoot = root,
|
||||
javaExecutable = javaExecutableResolver(),
|
||||
jarPath = jarPath,
|
||||
envFile = envFile,
|
||||
),
|
||||
)
|
||||
println("created ${unitFile.toFile().relativeTo(normalizedWorkingDir.toFile())}")
|
||||
|
||||
if (envFile.toFile().exists()) {
|
||||
println("${envFile.toFile().relativeTo(normalizedWorkingDir.toFile())} already exists — not overwritten")
|
||||
} else {
|
||||
envFile.toFile().writeText(SystemdServiceFiles.envFileContent())
|
||||
println("created ${envFile.toFile().relativeTo(normalizedWorkingDir.toFile())}")
|
||||
}
|
||||
|
||||
println("install and start the service with:")
|
||||
println(" ln -sf $unitFile ~/.config/systemd/user/$unitName")
|
||||
println(" systemctl --user daemon-reload")
|
||||
println(" systemctl --user enable --now $unitName")
|
||||
}
|
||||
|
||||
private data class DetectedValues(
|
||||
val baseUrl: String = "",
|
||||
val owner: String = "",
|
||||
val repo: String = "",
|
||||
val account: String = "",
|
||||
)
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The jar this JVM was started from. With `java -jar` the launch command starts with the
|
||||
* jar path; as a fallback (e.g. custom launchers) the Spring Boot loader's nested code
|
||||
* source URL contains it. Null when running from classes (IDE, Gradle, tests).
|
||||
*/
|
||||
private fun runningJarPath(): Path? {
|
||||
val launchCommand = System.getProperty("sun.java.command").orEmpty().substringBefore(' ')
|
||||
if (launchCommand.endsWith(".jar")) {
|
||||
return Paths.get(launchCommand).toAbsolutePath().normalize()
|
||||
}
|
||||
val codeSource =
|
||||
InitCommand::class.java.protectionDomain.codeSource
|
||||
?.location
|
||||
?.toString()
|
||||
.orEmpty()
|
||||
return Regex("""(/[^!]*?\.jar)""")
|
||||
.find(codeSource)
|
||||
?.let { Paths.get(it.groupValues[1]) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,61 @@
|
||||
package de.hoennig.gittally.commands
|
||||
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* Generates the content of the systemd user unit and its `EnvironmentFile` for running
|
||||
* `gittally server` as a service — the shape of the legacy `generate_systemd_config`,
|
||||
* without the self-copy/self-update machinery (the unit points at the jar in place).
|
||||
*/
|
||||
object SystemdServiceFiles {
|
||||
const val ENV_FILE_NAME = "gittally.env"
|
||||
|
||||
/** Per-repository unit name, because one GitTally instance serves exactly one repository. */
|
||||
fun unitName(repoRoot: Path): String = "gittally-${sanitize(repoRoot.fileName.toString())}.service"
|
||||
|
||||
fun unitFileContent(
|
||||
repoRoot: Path,
|
||||
javaExecutable: Path,
|
||||
jarPath: Path,
|
||||
envFile: Path,
|
||||
): String =
|
||||
"""
|
||||
[Unit]
|
||||
Description=GitTally CI for ${repoRoot.fileName}
|
||||
Wants=network-online.target
|
||||
After=network-online.target docker.service
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
WorkingDirectory=${systemdPath("$repoRoot")}
|
||||
EnvironmentFile=-${systemdPath("$envFile")}
|
||||
ExecStart=${systemdQuote("$javaExecutable")} ${'$'}JAVA_OPTS -jar ${systemdQuote("$jarPath")} server
|
||||
Restart=always
|
||||
RestartSec=30
|
||||
|
||||
[Install]
|
||||
WantedBy=default.target
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
fun envFileContent(): String =
|
||||
"""
|
||||
# EnvironmentFile for the GitTally systemd service.
|
||||
# GitTally itself is configured via .gittally.yml and .git/gittally/.gittally.yml,
|
||||
# not via environment variables; this file only tunes the JVM process.
|
||||
#JAVA_OPTS=-Xmx256m
|
||||
""".trimIndent() + "\n"
|
||||
|
||||
private fun sanitize(name: String): String = name.replace(Regex("[^A-Za-z0-9_.-]"), "-")
|
||||
|
||||
/** Escape `%` specifiers in systemd unit values (legacy `systemd_path`). */
|
||||
private fun systemdPath(value: String): String = value.replace("%", "%%")
|
||||
|
||||
/** Quote one `ExecStart` word (legacy `systemd_quote`). */
|
||||
private fun systemdQuote(value: String): String =
|
||||
"\"" +
|
||||
value
|
||||
.replace("\\", "\\\\")
|
||||
.replace("\"", "\\\"")
|
||||
.replace("%", "%%") +
|
||||
"\""
|
||||
}
|
||||
Reference in New Issue
Block a user