Rename GitTally to Werkator
`gitTally` is the name of another product in the git space, so the rename is a precaution; nothing about what the build system does changes. The name follows one rule: `Werkator` where it is prose, capitalized where it is a Kotlin type and its file, lowercase everywhere a machine reads it — the command, packages, paths, configuration keys and values, the Gitea check context. Environment variables keep their convention and are uppercase throughout. Every configuration file is still found under its pre-rename name (`ConfigFiles`): `.gittally.yml` at the repository root, in a build worktree and as committed on a branch, `.git/gittally/.gittally.yml` for the machine layer. The current name wins where both exist, and the old file is then ignored rather than merged — two files side by side are a half-done rename, not a layering. Without the fallback an installation that updated without renaming would not fail: a configuration that is not found leaves every setting at its default, so it would come up looking healthy while having forgotten its credentials and its builds. `docs/werkator-migrationsplan.md` lists what the fallback does not cover and has to be moved by hand — above all the state directory `.git/werkator/`, which holds the build history, the control token and the worktrees, and has no fallback of its own. `docs/migration-from-legacy.md` is deleted with this: it mapped the legacy script's environment variables, and every host it addressed has long since moved to the YAML configuration. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
7f550689dd
commit
35f06ec1ec
+2
-2
@@ -20,7 +20,7 @@ class WerkatorApplication
|
||||
@Profile("!server")
|
||||
class CliRunner(
|
||||
private val factory: IFactory,
|
||||
private val rootCommand: werkatorCommand,
|
||||
private val rootCommand: WerkatorCommand,
|
||||
) : CommandLineRunner,
|
||||
ExitCodeGenerator {
|
||||
private var exitCode = 0
|
||||
@@ -29,7 +29,7 @@ class CliRunner(
|
||||
exitCode =
|
||||
CommandLine(rootCommand, factory)
|
||||
.setExecutionExceptionHandler { exception, commandLine, _ ->
|
||||
// a config werkator must not read is a stated fact, not a crash: the message
|
||||
// a config Werkator must not read is a stated fact, not a crash: the message
|
||||
// names the file, the versions, and the way out — a stack trace would bury it
|
||||
if (exception is ConfigException) {
|
||||
commandLine.err.println("Error: ${exception.message}")
|
||||
+2
-2
@@ -27,7 +27,7 @@ import picocli.CommandLine.Command
|
||||
versionProvider = BuildPropertiesVersionProvider::class,
|
||||
description = ["Lightweight, declarative CI/CD system"],
|
||||
)
|
||||
class werkatorCommand : Runnable {
|
||||
class WerkatorCommand : Runnable {
|
||||
override fun run(): Unit = throw CommandLine.ParameterException(CommandLine(this), "Specify a subcommand")
|
||||
}
|
||||
|
||||
@@ -41,5 +41,5 @@ class werkatorCommand : Runnable {
|
||||
class BuildPropertiesVersionProvider(
|
||||
private val buildProperties: ObjectProvider<BuildProperties>,
|
||||
) : CommandLine.IVersionProvider {
|
||||
override fun getVersion(): Array<String> = arrayOf("werkator v${buildProperties.getIfAvailable()?.version ?: "dev"}")
|
||||
override fun getVersion(): Array<String> = arrayOf("Werkator v${buildProperties.getIfAvailable()?.version ?: "dev"}")
|
||||
}
|
||||
+1
-1
@@ -162,7 +162,7 @@ class FileArtifactStore(
|
||||
/**
|
||||
* The settings [build] ran with, from the build [workspace]'s `.werkator.yml` layered
|
||||
* on top of the primary config (see [ConfigLoader.loadForWorktree]) — resolved through
|
||||
* [werkatorConfig.buildSettings], so a job's own `artifactDirs` are archived and not
|
||||
* [WerkatorConfig.buildSettings], so a job's own `artifactDirs` are archived and not
|
||||
* only the ones its branch would have used.
|
||||
*/
|
||||
private fun buildSettings(
|
||||
+8
-8
@@ -194,11 +194,11 @@ class DockerBuildRunner(
|
||||
"ps",
|
||||
"-aq",
|
||||
"--filter",
|
||||
"label=$werkator_LABEL=true",
|
||||
"label=$WERKATOR_LABEL=true",
|
||||
"--filter",
|
||||
"label=$werkator_LABEL.repository=$repoKey",
|
||||
"label=$WERKATOR_LABEL.repository=$repoKey",
|
||||
"--filter",
|
||||
"label=$werkator_LABEL.role=build",
|
||||
"label=$WERKATOR_LABEL.role=build",
|
||||
),
|
||||
repoDir,
|
||||
)
|
||||
@@ -238,11 +238,11 @@ class DockerBuildRunner(
|
||||
args +=
|
||||
listOf(
|
||||
"--label",
|
||||
"$werkator_LABEL=true",
|
||||
"$WERKATOR_LABEL=true",
|
||||
"--label",
|
||||
"$werkator_LABEL.repository=$repoKey",
|
||||
"$WERKATOR_LABEL.repository=$repoKey",
|
||||
"--label",
|
||||
"$werkator_LABEL.role=build",
|
||||
"$WERKATOR_LABEL.role=build",
|
||||
)
|
||||
args += listOf("--workdir", "$workspace", "--volume", "$workspace:$workspace")
|
||||
args += gitMetadataMounts(workspace, repoDir)
|
||||
@@ -279,7 +279,7 @@ class DockerBuildRunner(
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes git work inside the build container without exposing werkator's secrets.
|
||||
* Makes git work inside the build container without exposing Werkator's secrets.
|
||||
*
|
||||
* The workspace is a git worktree whose `.git` file points into the primary
|
||||
* repository's `.git`, which is not part of the workspace mount — so any git call
|
||||
@@ -327,7 +327,7 @@ class DockerBuildRunner(
|
||||
|
||||
companion object {
|
||||
/** Container label namespace; legacy used `org.hostsharing.werkator`. */
|
||||
const val werkator_LABEL = "org.hoennig.werkator"
|
||||
const val WERKATOR_LABEL = "org.hoennig.werkator"
|
||||
|
||||
fun gradleVolumeName(repoKey: String): String = "werkator-gradle-$repoKey"
|
||||
|
||||
+8
-8
@@ -13,7 +13,7 @@ import java.nio.file.Paths
|
||||
@Component
|
||||
@Command(
|
||||
name = "init",
|
||||
description = ["Initialize werkator for the current repository"],
|
||||
description = ["Initialize Werkator for the current repository"],
|
||||
mixinStandardHelpOptions = true,
|
||||
)
|
||||
class InitCommand(
|
||||
@@ -130,10 +130,10 @@ class InitCommand(
|
||||
}
|
||||
val content =
|
||||
"""
|
||||
# The werkator this file is written for.
|
||||
# since: enforced — an older werkator refuses to read this file instead of
|
||||
# The Werkator this file is written for.
|
||||
# since: enforced — an older Werkator refuses to read this file instead of
|
||||
# silently ignoring the keys it does not know yet.
|
||||
# below: your release marker for a coming major; werkator decides how strictly
|
||||
# below: your release marker for a coming major; Werkator decides how strictly
|
||||
# to take it, and warns rather than blocks unless the format really broke.
|
||||
werkator:
|
||||
version:
|
||||
@@ -141,13 +141,13 @@ class InitCommand(
|
||||
# below: "2.0"
|
||||
|
||||
server:
|
||||
# Public base URL of this werkator installation — used for all links posted to Gitea.
|
||||
# Public base URL of this Werkator installation — used for all links posted to Gitea.
|
||||
publicBaseUrl: ""
|
||||
# HTTP port of the `server` subcommand
|
||||
port: 18080
|
||||
# bind address of the `server` subcommand; loopback only, because the UI and the
|
||||
# API are unauthenticated — use 0.0.0.0 only without a reverse proxy in front
|
||||
# (and with the managed nginx below, which reaches werkator from its container)
|
||||
# (and with the managed nginx below, which reaches Werkator from its container)
|
||||
bindAddress: 127.0.0.1
|
||||
# optional Impressum (legal disclosure) link in the web UI footer; empty hides the link
|
||||
impressumUrl: ""
|
||||
@@ -256,7 +256,7 @@ class InitCommand(
|
||||
) {
|
||||
val jarPath = jarPathResolver()
|
||||
if (jarPath == null) {
|
||||
println("Error: cannot determine the werkator jar path — run `init --systemd` via `java -jar <path-to>/werkator.jar`")
|
||||
println("Error: cannot determine the Werkator jar path — run `init --systemd` via `java -jar <path-to>/werkator.jar`")
|
||||
return
|
||||
}
|
||||
val werkatorDir = root.resolve(".git/werkator")
|
||||
@@ -283,7 +283,7 @@ class InitCommand(
|
||||
}
|
||||
|
||||
// the nightly Docker cleanup is host-global: every repository generates the same
|
||||
// units, so with several werkator instances the symlinks simply coincide
|
||||
// units, so with several Werkator instances the symlinks simply coincide
|
||||
val pruneServiceFile = werkatorDir.resolve(SystemdServiceFiles.PRUNE_SERVICE_NAME)
|
||||
val pruneTimerFile = werkatorDir.resolve(SystemdServiceFiles.PRUNE_TIMER_NAME)
|
||||
pruneServiceFile.toFile().writeText(SystemdServiceFiles.pruneServiceContent())
|
||||
+2
-2
@@ -24,7 +24,7 @@ import java.util.concurrent.CountDownLatch
|
||||
@Component
|
||||
@Command(
|
||||
name = "server",
|
||||
description = ["Start the werkator server"],
|
||||
description = ["Start the Werkator server"],
|
||||
mixinStandardHelpOptions = true,
|
||||
)
|
||||
class ServerCommand(
|
||||
@@ -43,7 +43,7 @@ class ServerCommand(
|
||||
"server.address=${config.server.bindAddress}",
|
||||
).run()
|
||||
val port = context.environment.getProperty("local.server.port", config.server.port.toString())
|
||||
println("werkator server listening on http://${config.server.bindAddress}:$port/ — Ctrl-C to stop")
|
||||
println("Werkator server listening on http://${config.server.bindAddress}:$port/ — Ctrl-C to stop")
|
||||
awaitShutdown(context)
|
||||
}
|
||||
|
||||
+7
-7
@@ -10,11 +10,11 @@ import java.nio.file.Path
|
||||
object SystemdServiceFiles {
|
||||
const val ENV_FILE_NAME = "werkator.env"
|
||||
|
||||
/** Host-global unit names of the nightly Docker cleanup — shared by all werkator repositories on the host. */
|
||||
/** Host-global unit names of the nightly Docker cleanup — shared by all Werkator repositories on the host. */
|
||||
const val PRUNE_SERVICE_NAME = "werkator-docker-prune.service"
|
||||
const val PRUNE_TIMER_NAME = "werkator-docker-prune.timer"
|
||||
|
||||
/** Per-repository unit name, because one werkator instance serves exactly one repository. */
|
||||
/** Per-repository unit name, because one Werkator instance serves exactly one repository. */
|
||||
fun unitName(repoRoot: Path): String = "werkator-${sanitize(repoRoot.fileName.toString())}.service"
|
||||
|
||||
fun unitFileContent(
|
||||
@@ -25,7 +25,7 @@ object SystemdServiceFiles {
|
||||
): String =
|
||||
"""
|
||||
[Unit]
|
||||
Description=werkator CI for ${repoRoot.fileName}
|
||||
Description=Werkator CI for ${repoRoot.fileName}
|
||||
Wants=network-online.target
|
||||
After=network-online.target docker.service
|
||||
|
||||
@@ -49,7 +49,7 @@ object SystemdServiceFiles {
|
||||
fun pruneServiceContent(): String =
|
||||
"""
|
||||
[Unit]
|
||||
Description=Clean up unused Docker containers and images (werkator)
|
||||
Description=Clean up unused Docker containers and images (Werkator)
|
||||
|
||||
[Service]
|
||||
Type=oneshot
|
||||
@@ -62,7 +62,7 @@ object SystemdServiceFiles {
|
||||
fun pruneTimerContent(): String =
|
||||
"""
|
||||
[Unit]
|
||||
Description=Nightly Docker cleanup before the auto builds (werkator)
|
||||
Description=Nightly Docker cleanup before the auto builds (Werkator)
|
||||
|
||||
[Timer]
|
||||
OnCalendar=*-*-* 02:00:00
|
||||
@@ -74,8 +74,8 @@ object SystemdServiceFiles {
|
||||
|
||||
fun envFileContent(): String =
|
||||
"""
|
||||
# EnvironmentFile for the werkator systemd service.
|
||||
# werkator itself is configured via .werkator.yml and .git/werkator/.werkator.yml,
|
||||
# EnvironmentFile for the Werkator systemd service.
|
||||
# Werkator itself is configured via .werkator.yml and .git/werkator/.werkator.yml,
|
||||
# not via environment variables; this file only tunes the JVM process.
|
||||
#JAVA_OPTS=-Xmx256m
|
||||
""".trimIndent() + "\n"
|
||||
@@ -0,0 +1,47 @@
|
||||
package de.hoennig.werkator.config
|
||||
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
|
||||
/**
|
||||
* The names a configuration file is looked up under, current name first and the name
|
||||
* from before the rename to Werkator second — spelled exactly as it was.
|
||||
*
|
||||
* The fallback exists because a missing configuration is not an error: it leaves every
|
||||
* setting at its default. An installation that updates without moving its files would
|
||||
* therefore not fail, it would come up as a plausible-looking instance that has
|
||||
* forgotten its credentials, its addresses, and what it builds.
|
||||
*/
|
||||
object ConfigFiles {
|
||||
/** The committed configuration, at the repository root and in a build worktree. */
|
||||
const val COMMITTED = ".werkator.yml"
|
||||
|
||||
/** The machine-specific configuration inside `.git`; secrets live here. */
|
||||
const val REPO_INSTALL = ".git/werkator/$COMMITTED"
|
||||
|
||||
private const val LEGACY_COMMITTED = ".gittally.yml"
|
||||
private const val LEGACY_REPO_INSTALL = ".git/gittally/$LEGACY_COMMITTED"
|
||||
|
||||
/** Both names of the committed configuration, current first. */
|
||||
val committed = listOf(COMMITTED, LEGACY_COMMITTED)
|
||||
|
||||
/** Both paths of the machine-specific configuration, current first. */
|
||||
val repoInstall = listOf(REPO_INSTALL, LEGACY_REPO_INSTALL)
|
||||
|
||||
/**
|
||||
* The first of [candidates] that exists under [dir], or the current name when none
|
||||
* does — so a message about a file names the one to write, never the one that is
|
||||
* history.
|
||||
*/
|
||||
fun firstExisting(
|
||||
dir: Path,
|
||||
candidates: List<String> = committed,
|
||||
): String = candidates.firstOrNull { Files.isRegularFile(dir.resolve(it)) } ?: candidates.first()
|
||||
|
||||
/**
|
||||
* The committed configuration as [read] answers it for a name, current name first.
|
||||
* Null when neither name is committed — used where the file is read out of git
|
||||
* rather than off the filesystem.
|
||||
*/
|
||||
fun readCommitted(read: (String) -> String?): String? = committed.firstNotNullOfOrNull(read)
|
||||
}
|
||||
+11
-8
@@ -48,7 +48,7 @@ class ConfigLoader(
|
||||
fun loadForWorktree(
|
||||
workingDir: Path,
|
||||
worktreeDir: Path,
|
||||
): WerkatorConfig = withBranchLayer(workingDir, loadFile(worktreeDir.resolve(".werkator.yml").toFile()))
|
||||
): WerkatorConfig = withBranchLayer(workingDir, loadFile(worktreeDir.resolve(ConfigFiles.firstExisting(worktreeDir)).toFile()))
|
||||
|
||||
/**
|
||||
* The primary/`.git` config with the committed `.werkator.yml` of one branch
|
||||
@@ -276,13 +276,16 @@ class ConfigLoader(
|
||||
}
|
||||
|
||||
fun loadRaw(workingDir: Path = Paths.get(".")): Map<String, Any?> {
|
||||
val repoInstall = loadFile(workingDir.resolve(".git/werkator/.werkator.yml").toFile())
|
||||
val project = loadFile(workingDir.resolve(".werkator.yml").toFile())
|
||||
// each layer under its current name, or under the one it had before the rename
|
||||
val repoInstallName = ConfigFiles.firstExisting(workingDir, ConfigFiles.repoInstall)
|
||||
val projectName = ConfigFiles.firstExisting(workingDir)
|
||||
val repoInstall = loadFile(workingDir.resolve(repoInstallName).toFile())
|
||||
val project = loadFile(workingDir.resolve(projectName).toFile())
|
||||
// per file, so the message names the file to fix — the merged map has no provenance
|
||||
checkVersion(project, ".werkator.yml", ROLLBACK_HINT)
|
||||
checkVersion(repoInstall, ".git/werkator/.werkator.yml", ROLLBACK_HINT)
|
||||
checkTriggerBlocks(project, ".werkator.yml", ROLLBACK_HINT)
|
||||
checkTriggerBlocks(repoInstall, ".git/werkator/.werkator.yml", ROLLBACK_HINT)
|
||||
checkVersion(project, projectName, ROLLBACK_HINT)
|
||||
checkVersion(repoInstall, repoInstallName, ROLLBACK_HINT)
|
||||
checkTriggerBlocks(project, projectName, ROLLBACK_HINT)
|
||||
checkTriggerBlocks(repoInstall, repoInstallName, ROLLBACK_HINT)
|
||||
return deepMerge(project, repoInstall)
|
||||
}
|
||||
|
||||
@@ -411,7 +414,7 @@ class ConfigLoader(
|
||||
private const val NO_TRIGGER_WARNING = "no-build-triggered"
|
||||
|
||||
private const val ROLLBACK_HINT =
|
||||
"Migrate the file, or roll back to the werkator version it was written for."
|
||||
"Migrate the file, or roll back to the Werkator version it was written for."
|
||||
|
||||
private const val BRANCH_HINT =
|
||||
"Migrate the file on this branch; the other branches keep building."
|
||||
+17
-17
@@ -1,33 +1,33 @@
|
||||
package de.hoennig.werkator.config
|
||||
|
||||
/**
|
||||
* The werkator version a configuration file declares itself for, the `werkator.version`
|
||||
* The Werkator version a configuration file declares itself for, the `werkator.version`
|
||||
* section:
|
||||
*
|
||||
* ```yaml
|
||||
* werkator:
|
||||
* version:
|
||||
* since: "0.9.16" # always hard: an older werkator refuses this file
|
||||
* below: "2.0" # werkator decides how hard, see ConfigVersions.verdict
|
||||
* since: "0.9.16" # always hard: an older Werkator refuses this file
|
||||
* below: "2.0" # Werkator decides how hard, see ConfigVersions.verdict
|
||||
* ```
|
||||
*
|
||||
* There is deliberately no version of the file format itself (no `apiVersion`): no API is
|
||||
* involved — werkator reads its own configuration — and only one configuration generation
|
||||
* involved — Werkator reads its own configuration — and only one configuration generation
|
||||
* is ever supported. The declared version exists to make an incompatibility nameable,
|
||||
* never to run two parsers.
|
||||
*/
|
||||
data class VersionRequirement(
|
||||
/** Oldest werkator that understands this file; empty means the file does not say. */
|
||||
/** Oldest Werkator that understands this file; empty means the file does not say. */
|
||||
val since: String = "",
|
||||
/** First werkator this file was not released for; empty means no ceiling. */
|
||||
/** First Werkator this file was not released for; empty means no ceiling. */
|
||||
val below: String = "",
|
||||
)
|
||||
|
||||
data class werkatorMeta(
|
||||
data class WerkatorMeta(
|
||||
val version: VersionRequirement = VersionRequirement(),
|
||||
)
|
||||
|
||||
/** What a [VersionRequirement] means for the werkator that reads the file. */
|
||||
/** What a [VersionRequirement] means for the Werkator that reads the file. */
|
||||
sealed interface VersionVerdict {
|
||||
/** The running version is covered by the declaration. */
|
||||
data object Compatible : VersionVerdict
|
||||
@@ -37,24 +37,24 @@ sealed interface VersionVerdict {
|
||||
val message: String,
|
||||
) : VersionVerdict
|
||||
|
||||
/** Not usable: the file predates a change that werkator cannot bridge. */
|
||||
/** Not usable: the file predates a change that Werkator cannot bridge. */
|
||||
data class Incompatible(
|
||||
val message: String,
|
||||
) : VersionVerdict
|
||||
}
|
||||
|
||||
/** A configuration file this werkator must not read; carries the file's name in its message. */
|
||||
/** A configuration file this Werkator must not read; carries the file's name in its message. */
|
||||
open class ConfigException(
|
||||
message: String,
|
||||
) : RuntimeException(message)
|
||||
|
||||
/** The file declares a werkator that cannot read it, see [ConfigVersions]. */
|
||||
/** The file declares a Werkator that cannot read it, see [ConfigVersions]. */
|
||||
class ConfigVersionException(
|
||||
message: String,
|
||||
) : ConfigException(message)
|
||||
|
||||
/**
|
||||
* The file is written in a shape this werkator no longer reads. Refusing it is the point:
|
||||
* The file is written in a shape this Werkator no longer reads. Refusing it is the point:
|
||||
* a key that moved and is silently ignored means a build that quietly stops happening.
|
||||
*/
|
||||
class ConfigFormatException(
|
||||
@@ -76,13 +76,13 @@ object ConfigVersions {
|
||||
/**
|
||||
* Decides what [requirement] means for [running].
|
||||
*
|
||||
* `since` is always hard — a file that needs a newer werkator cannot be honored, and
|
||||
* `since` is always hard — a file that needs a newer Werkator cannot be honored, and
|
||||
* silently ignoring its unknown keys is exactly the failure mode this section exists
|
||||
* to prevent.
|
||||
*
|
||||
* `below` alone only warns: it is the team's release marker, and an unmaintained
|
||||
* marker must never stop a CI. Whether the running version really broke the file is
|
||||
* werkator's own knowledge ([FORMAT_BROKE_IN]) — a file written before that change
|
||||
* Werkator's own knowledge ([FORMAT_BROKE_IN]) — a file written before that change
|
||||
* and read after it is incompatible regardless of what it declares as its ceiling.
|
||||
*/
|
||||
fun verdict(
|
||||
@@ -95,13 +95,13 @@ object ConfigVersions {
|
||||
val since = parse(requirement.since)
|
||||
if (since != null && version < since) {
|
||||
return VersionVerdict.Incompatible(
|
||||
"needs werkator ${requirement.since} or newer (werkator.version.since), this is $running",
|
||||
"needs Werkator ${requirement.since} or newer (werkator.version.since), this is $running",
|
||||
)
|
||||
}
|
||||
val broke = parse(brokeIn)
|
||||
if (since != null && broke != null && since < broke && version >= broke) {
|
||||
return VersionVerdict.Incompatible(
|
||||
"is written for werkator ${requirement.since} (werkator.version.since), " +
|
||||
"is written for Werkator ${requirement.since} (werkator.version.since), " +
|
||||
"but the configuration format changed incompatibly in $brokeIn" +
|
||||
brokeDescription.takeIf { it.isNotBlank() }?.let { ": $it" }.orEmpty(),
|
||||
)
|
||||
@@ -109,7 +109,7 @@ object ConfigVersions {
|
||||
val below = parse(requirement.below)
|
||||
if (below != null && version >= below) {
|
||||
return VersionVerdict.Warn(
|
||||
"was released for werkator below ${requirement.below} (werkator.version.below), this is $running",
|
||||
"was released for Werkator below ${requirement.below} (werkator.version.below), this is $running",
|
||||
)
|
||||
}
|
||||
return VersionVerdict.Compatible
|
||||
+3
-3
@@ -3,8 +3,8 @@ package de.hoennig.werkator.config
|
||||
import com.fasterxml.jackson.annotation.JsonProperty
|
||||
|
||||
data class WerkatorConfig(
|
||||
/** What this file declares about the werkator that reads it; see [VersionRequirement]. */
|
||||
val werkator: werkatorMeta = werkatorMeta(),
|
||||
/** What this file declares about the Werkator that reads it; see [VersionRequirement]. */
|
||||
val werkator: WerkatorMeta = WerkatorMeta(),
|
||||
val server: ServerConfig = ServerConfig(),
|
||||
val git: GitConfig = GitConfig(),
|
||||
val gitea: GiteaConfig = GiteaConfig(),
|
||||
@@ -61,7 +61,7 @@ data class ServerConfig(
|
||||
)
|
||||
|
||||
/**
|
||||
* Opt-in managed nginx+certbot Docker container serving werkator over HTTPS,
|
||||
* Opt-in managed nginx+certbot Docker container serving Werkator over HTTPS,
|
||||
* for hosts without a usable reverse proxy (ADR 0005). Off by default; the
|
||||
* reverse-proxy deployment from `docs/deployment.md` stays the recommended setup.
|
||||
*/
|
||||
+4
-4
@@ -15,10 +15,10 @@ object GitAskPass {
|
||||
#!/bin/sh
|
||||
case "${'$'}1" in
|
||||
*[Uu]sername*)
|
||||
printf '%s\n' "${'$'}werkator_GIT_ACCOUNT"
|
||||
printf '%s\n' "${'$'}WERKATOR_GIT_ACCOUNT"
|
||||
;;
|
||||
*)
|
||||
printf '%s\n' "${'$'}werkator_GIT_TOKEN"
|
||||
printf '%s\n' "${'$'}WERKATOR_GIT_TOKEN"
|
||||
;;
|
||||
esac
|
||||
""".trimIndent() + "\n"
|
||||
@@ -40,8 +40,8 @@ object GitAskPass {
|
||||
mapOf(
|
||||
"GIT_ASKPASS" to script.toAbsolutePath().toString(),
|
||||
"GIT_TERMINAL_PROMPT" to "0",
|
||||
"werkator_GIT_ACCOUNT" to account,
|
||||
"werkator_GIT_TOKEN" to token,
|
||||
"WERKATOR_GIT_ACCOUNT" to account,
|
||||
"WERKATOR_GIT_TOKEN" to token,
|
||||
),
|
||||
)
|
||||
} finally {
|
||||
+1
-1
@@ -12,7 +12,7 @@ object NginxConfigFiles {
|
||||
* The `nginx.conf` content. Without [full] it is the init config for the
|
||||
* two-phase startup: HTTP only, serving the ACME webroot challenge and
|
||||
* redirecting everything else to HTTPS. With [full] an HTTPS server block
|
||||
* with the Let's Encrypt certificate and the proxy to werkator is added.
|
||||
* with the Let's Encrypt certificate and the proxy to Werkator is added.
|
||||
*/
|
||||
fun nginxConf(
|
||||
serverName: String,
|
||||
+11
-11
@@ -1,7 +1,7 @@
|
||||
package de.hoennig.werkator.server
|
||||
|
||||
import de.hoennig.werkator.build.ArtifactKeys
|
||||
import de.hoennig.werkator.build.DockerBuildRunner.Companion.werkator_LABEL
|
||||
import de.hoennig.werkator.build.DockerBuildRunner.Companion.WERKATOR_LABEL
|
||||
import de.hoennig.werkator.config.ConfigLoader
|
||||
import de.hoennig.werkator.git.GitCommandRunner
|
||||
import org.slf4j.LoggerFactory
|
||||
@@ -11,7 +11,7 @@ import java.nio.file.Path
|
||||
import java.nio.file.Paths
|
||||
|
||||
/**
|
||||
* Manages the opt-in nginx+certbot Docker container that serves werkator over
|
||||
* Manages the opt-in nginx+certbot Docker container that serves Werkator over
|
||||
* HTTPS on hosts without a reverse proxy (ADR 0005), ported from the legacy
|
||||
* `start_artifact_nginx` subsystem. Shells out to the `docker` CLI via the
|
||||
* generic [GitCommandRunner] process wrapper, like [de.hoennig.werkator.build.DockerBuildRunner].
|
||||
@@ -197,7 +197,7 @@ class NginxProxyManager(
|
||||
/**
|
||||
* Legacy `cleanup_stale_artifact_nginx_containers`: remove the container by
|
||||
* name, all nginx-role containers of this repository by label, and any
|
||||
* werkator container still occupying the configured ports.
|
||||
* Werkator container still occupying the configured ports.
|
||||
*/
|
||||
private fun cleanupStaleContainers(settings: NginxSettings) {
|
||||
commandRunner.run(listOf("docker", "rm", "-f", settings.containerName), workingDir)
|
||||
@@ -208,11 +208,11 @@ class NginxProxyManager(
|
||||
"ps",
|
||||
"-aq",
|
||||
"--filter",
|
||||
"label=$werkator_LABEL=true",
|
||||
"label=$WERKATOR_LABEL=true",
|
||||
"--filter",
|
||||
"label=$werkator_LABEL.repository=${settings.repoKey}",
|
||||
"label=$WERKATOR_LABEL.repository=${settings.repoKey}",
|
||||
"--filter",
|
||||
"label=$werkator_LABEL.role=nginx",
|
||||
"label=$WERKATOR_LABEL.role=nginx",
|
||||
),
|
||||
workingDir,
|
||||
)
|
||||
@@ -220,11 +220,11 @@ class NginxProxyManager(
|
||||
commandRunner.run(listOf("docker", "rm", "-f") + labelled.lines(), workingDir)
|
||||
}
|
||||
for (container in listContainersUsingPorts(settings)) {
|
||||
if (container.labels.contains("$werkator_LABEL=true") ||
|
||||
if (container.labels.contains("$WERKATOR_LABEL=true") ||
|
||||
container.name.startsWith("werkator-") ||
|
||||
container.name.startsWith("git-watch-origin-and-test-nginx-")
|
||||
) {
|
||||
log.info("removing stale werkator container using an nginx port: {}", container.name)
|
||||
log.info("removing stale Werkator container using an nginx port: {}", container.name)
|
||||
commandRunner.run(listOf("docker", "rm", "-f", container.id), workingDir)
|
||||
}
|
||||
}
|
||||
@@ -303,11 +303,11 @@ class NginxProxyManager(
|
||||
"--volume",
|
||||
"${settings.nginxConf}:/etc/nginx/nginx.conf:ro",
|
||||
"--label",
|
||||
"$werkator_LABEL=true",
|
||||
"$WERKATOR_LABEL=true",
|
||||
"--label",
|
||||
"$werkator_LABEL.repository=${settings.repoKey}",
|
||||
"$WERKATOR_LABEL.repository=${settings.repoKey}",
|
||||
"--label",
|
||||
"$werkator_LABEL.role=nginx",
|
||||
"$WERKATOR_LABEL.role=nginx",
|
||||
"nginx",
|
||||
)
|
||||
|
||||
+2
-2
@@ -5,10 +5,10 @@ import de.hoennig.werkator.build.BuildExecutor
|
||||
import de.hoennig.werkator.build.BuildResult
|
||||
import de.hoennig.werkator.build.BuildResultRepository
|
||||
import de.hoennig.werkator.build.BuildStatus
|
||||
import de.hoennig.werkator.config.ConfigFiles
|
||||
import de.hoennig.werkator.config.ConfigLoader
|
||||
import de.hoennig.werkator.git.GitService
|
||||
import de.hoennig.werkator.metrics.SystemMetricsCollector
|
||||
import de.hoennig.werkator.watcher.Watcher
|
||||
import jakarta.servlet.http.HttpServletRequest
|
||||
import org.springframework.beans.factory.ObjectProvider
|
||||
import org.springframework.boot.info.BuildProperties
|
||||
@@ -235,7 +235,7 @@ class UiController(
|
||||
try {
|
||||
configLoader.loadWithBranchLayer(
|
||||
workingDir,
|
||||
gitService.showFileAtCommit(result.commit, Watcher.CONFIG_FILE, workingDir),
|
||||
ConfigFiles.readCommitted { gitService.showFileAtCommit(result.commit, it, workingDir) },
|
||||
)
|
||||
} catch (_: Exception) {
|
||||
configLoader.load(workingDir)
|
||||
+6
-3
@@ -7,6 +7,7 @@ import de.hoennig.werkator.build.BuildResultRepository
|
||||
import de.hoennig.werkator.build.BuildStatus
|
||||
import de.hoennig.werkator.build.GitWorktreeWorkspaces
|
||||
import de.hoennig.werkator.config.BuildDefinition
|
||||
import de.hoennig.werkator.config.ConfigFiles
|
||||
import de.hoennig.werkator.config.ConfigLoader
|
||||
import de.hoennig.werkator.config.DurationParser
|
||||
import de.hoennig.werkator.config.WerkatorConfig
|
||||
@@ -261,8 +262,10 @@ class Watcher(
|
||||
val definitions =
|
||||
try {
|
||||
configLoader
|
||||
.loadWithBranchLayer(workingDir, gitService.showFileAtCommit(commit, CONFIG_FILE, workingDir))
|
||||
.effectiveBuildDefinitions()
|
||||
.loadWithBranchLayer(
|
||||
workingDir,
|
||||
ConfigFiles.readCommitted { gitService.showFileAtCommit(commit, it, workingDir) },
|
||||
).effectiveBuildDefinitions()
|
||||
} catch (e: Exception) {
|
||||
log.warn(
|
||||
"ignoring the committed {} of branch {} at {}: {}",
|
||||
@@ -466,6 +469,6 @@ class Watcher(
|
||||
const val AUTO_BUILDS_FILE = ".git/werkator/auto-builds.json"
|
||||
|
||||
/** The committed config read per branch for its build definitions. */
|
||||
const val CONFIG_FILE = ".werkator.yml"
|
||||
const val CONFIG_FILE = ConfigFiles.COMMITTED
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="werkator">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="Werkator">
|
||||
<rect width="64" height="64" rx="14" fill="#155eef"/>
|
||||
<path d="M17 47V18m0 14h13c7 0 10-4 10-11" fill="none" stroke="#f9fafb" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="17" cy="18" r="5" fill="#DD4901"/>
|
||||
|
||||
|
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 564 B |
@@ -1,4 +1,4 @@
|
||||
/* werkator web UI — loosely ported from the legacy generated pages. */
|
||||
/* Werkator web UI — loosely ported from the legacy generated pages. */
|
||||
|
||||
:root {
|
||||
color-scheme: light dark;
|
||||
@@ -1,4 +1,4 @@
|
||||
// werkator web UI — polls the JSON API and re-renders table bodies from data.
|
||||
// Werkator web UI — polls the JSON API and re-renders table bodies from data.
|
||||
// Every fetch has a timeout and failures render an explicit error badge, so the
|
||||
// UI can never get stuck on a loading animation (the legacy defect).
|
||||
"use strict";
|
||||
@@ -131,7 +131,7 @@ function forgetControlToken() {
|
||||
|
||||
function askForControlToken() {
|
||||
const answer = window.prompt(
|
||||
"Control token — the content of .git/werkator/control-token on the werkator host:",
|
||||
"Control token — the content of .git/werkator/control-token on the Werkator host:",
|
||||
"",
|
||||
);
|
||||
return answer ? answer.trim() : "";
|
||||
@@ -3,7 +3,7 @@
|
||||
<head th:fragment="head(title)">
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title th:text="${#strings.isEmpty(repoName)} ? ${title} + ' — werkator' : ${title} + ' — ' + ${repoName} + ' — werkator'">werkator</title>
|
||||
<title th:text="${#strings.isEmpty(repoName)} ? ${title} + ' — Werkator' : ${title} + ' — ' + ${repoName} + ' — Werkator'">Werkator</title>
|
||||
<link rel="icon" href="/favicon.svg" type="image/svg+xml">
|
||||
<link rel="stylesheet" href="/werkator.css">
|
||||
<meta name="werkator-gitea-repo-url" th:content="${giteaRepoUrl}">
|
||||
@@ -40,7 +40,7 @@
|
||||
</th:block>
|
||||
|
||||
<footer th:fragment="footer" class="site-footer">
|
||||
<strong><a href="/releases" title="Release notes"><em th:text="'werkator v' + ${version}">werkator</em></a></strong>
|
||||
<strong><a href="/releases" title="Release notes"><em th:text="'Werkator v' + ${version}">Werkator</em></a></strong>
|
||||
— © <a href="https://michael.hoennig.de" target="_blank" rel="noopener noreferrer">Michael Hönnig</a>, 2026
|
||||
<th:block th:unless="${#strings.isEmpty(impressumUrl)}">
|
||||
— <a th:href="${impressumUrl}" target="_blank" rel="noopener noreferrer">Impressum (Legal Disclosure)</a>
|
||||
|
||||
@@ -7,6 +7,34 @@
|
||||
<div th:replace="~{fragments :: nav(${view})}"></div>
|
||||
<div class="panel release-notes">
|
||||
|
||||
<h2>v0.9.22 <span class="muted">— unreleased</span></h2>
|
||||
<ul>
|
||||
<li>GitTally is now <strong>Werkator</strong>. The old name already belongs to another
|
||||
product in the git space, so the rename is a precaution and nothing more: what the
|
||||
build system does, how it is configured, and how it reports to Gitea are unchanged.
|
||||
In prose it is Werkator, everywhere a machine reads the name it is
|
||||
<code>werkator</code>.</li>
|
||||
<li>The rename reaches the file names an installation depends on. The committed
|
||||
configuration is <code>.werkator.yml</code>, the machine-specific one
|
||||
<code>.git/werkator/.werkator.yml</code>, and all state — build results, artifacts,
|
||||
worktrees, the control token — lives under <code>.git/werkator/</code>.</li>
|
||||
<li>Every configuration is still found under its old name, spelled as it was:
|
||||
<code>.gittally.yml</code> at the repository root, in a build worktree and as
|
||||
committed on a branch, and <code>.git/gittally/.gittally.yml</code> for the machine
|
||||
layer. The current name wins where both exist, and the old file is then ignored
|
||||
rather than merged. The fallback is there because a configuration that is not found
|
||||
is not an error — it leaves every setting at its default, so an installation that
|
||||
updated without renaming would have come up looking healthy while having forgotten
|
||||
its credentials and its builds. Rename at your convenience; the state directory
|
||||
<code>.git/werkator/</code> has no such fallback and does have to be moved, or the
|
||||
instance starts without its build history.</li>
|
||||
<li>The default Gitea check context is <code>werkator</code>. Where the old name is
|
||||
pinned in a branch protection rule, the rule has to be updated with it, or a pull
|
||||
request waits forever for a check nobody posts any more. Statuses already written
|
||||
keep their old context, so a commit built before and after the change shows
|
||||
both.</li>
|
||||
</ul>
|
||||
|
||||
<h2>v0.9.21 <span class="muted">— 2026-08-30</span></h2>
|
||||
<ul>
|
||||
<li>An origin the watcher cannot reach is now visible in the web UI: a banner above the
|
||||
@@ -294,6 +322,6 @@
|
||||
</div>
|
||||
</main>
|
||||
<footer th:replace="~{fragments :: footer}"></footer>
|
||||
<script src="/gittally.js"></script>
|
||||
<script src="/werkator.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+1
-1
@@ -11,7 +11,7 @@ import org.springframework.boot.test.context.SpringBootTest
|
||||
@SpringBootTest
|
||||
class ApplicationContextTest : FunSpec() {
|
||||
@Autowired
|
||||
lateinit var rootCommand: werkatorCommand
|
||||
lateinit var rootCommand: WerkatorCommand
|
||||
|
||||
@Autowired
|
||||
lateinit var initCommand: InitCommand
|
||||
+2
-2
@@ -20,9 +20,9 @@ class GitWorktreeWorkspacesTest : FunSpec() {
|
||||
// hermetic git: fixed identity, no user/system config (hooks, gpg signing, ...)
|
||||
private val gitEnvironment =
|
||||
mapOf(
|
||||
"GIT_AUTHOR_NAME" to "werkator Test",
|
||||
"GIT_AUTHOR_NAME" to "Werkator Test",
|
||||
"GIT_AUTHOR_EMAIL" to "test@example.com",
|
||||
"GIT_COMMITTER_NAME" to "werkator Test",
|
||||
"GIT_COMMITTER_NAME" to "Werkator Test",
|
||||
"GIT_COMMITTER_EMAIL" to "test@example.com",
|
||||
"GIT_CONFIG_GLOBAL" to "/dev/null",
|
||||
"GIT_CONFIG_SYSTEM" to "/dev/null",
|
||||
+1
-1
@@ -22,7 +22,7 @@ class SystemdServiceFilesTest : FunSpec() {
|
||||
envFile = Paths.get("/srv/repos/my-repo/.git/werkator/werkator.env"),
|
||||
)
|
||||
|
||||
content shouldContain "Description=werkator CI for my-repo"
|
||||
content shouldContain "Description=Werkator CI for my-repo"
|
||||
content shouldContain "After=network-online.target docker.service"
|
||||
content shouldContain "WorkingDirectory=/srv/repos/my-repo"
|
||||
content shouldContain "EnvironmentFile=-/srv/repos/my-repo/.git/werkator/werkator.env"
|
||||
+51
-3
@@ -18,7 +18,7 @@ import java.util.Properties
|
||||
class ConfigLoaderTest : FunSpec() {
|
||||
private val loader = ConfigLoader()
|
||||
|
||||
/** A loader that knows which werkator it is, for the `werkator.version` checks. */
|
||||
/** A loader that knows which Werkator it is, for the `werkator.version` checks. */
|
||||
private fun loaderRunning(version: String): ConfigLoader {
|
||||
val provider = mockk<ObjectProvider<BuildProperties>>()
|
||||
every { provider.getIfAvailable() } returns BuildProperties(Properties().apply { setProperty("version", version) })
|
||||
@@ -50,6 +50,54 @@ class ConfigLoaderTest : FunSpec() {
|
||||
config.gitea.repo shouldBe "my-repo"
|
||||
}
|
||||
|
||||
test("falls back to the pre-rename .gittally.yml at the repository root") {
|
||||
val dir = Files.createTempDirectory("werkator-test")
|
||||
dir.resolve(".gittally.yml").toFile().writeText(
|
||||
"""
|
||||
gitea:
|
||||
owner: my-org
|
||||
repo: my-repo
|
||||
""".trimIndent(),
|
||||
)
|
||||
loader.load(dir).gitea.owner shouldBe "my-org"
|
||||
}
|
||||
|
||||
test("falls back to the pre-rename machine config under .git/gittally") {
|
||||
val dir = Files.createTempDirectory("werkator-test")
|
||||
Files.createDirectories(dir.resolve(".git/gittally"))
|
||||
dir.resolve(".git/gittally/.gittally.yml").toFile().writeText(
|
||||
"""
|
||||
git:
|
||||
account: ci-user
|
||||
""".trimIndent(),
|
||||
)
|
||||
loader.load(dir).git.account shouldBe "ci-user"
|
||||
}
|
||||
|
||||
test("the current name wins where both exist, so a half-done rename is not merged") {
|
||||
val dir = Files.createTempDirectory("werkator-test")
|
||||
dir.resolve(".werkator.yml").toFile().writeText("gitea:\n owner: current\n")
|
||||
dir.resolve(".gittally.yml").toFile().writeText("gitea:\n owner: legacy\n repo: legacy-repo\n")
|
||||
val config = loader.load(dir)
|
||||
config.gitea.owner shouldBe "current"
|
||||
// not merged: the old file is a leftover, not a layer
|
||||
config.gitea.repo shouldBe ""
|
||||
}
|
||||
|
||||
test("a branch whose config is committed under the pre-rename name is still read as the branch layer") {
|
||||
val dir = Files.createTempDirectory("werkator-test")
|
||||
val worktree = Files.createTempDirectory("werkator-worktree")
|
||||
worktree.resolve(".gittally.yml").toFile().writeText(
|
||||
"""
|
||||
builds:
|
||||
default:
|
||||
buildCommand: ./gradlew fromBranch
|
||||
""".trimIndent(),
|
||||
)
|
||||
loader.loadForWorktree(dir, worktree).buildSettings("any-branch", "default").buildCommand shouldBe
|
||||
"./gradlew fromBranch"
|
||||
}
|
||||
|
||||
test("reads executor.maxConcurrent and defaults it to 1") {
|
||||
val dir = Files.createTempDirectory("werkator-test")
|
||||
loader.load(dir).executor.maxConcurrent shouldBe 1
|
||||
@@ -113,7 +161,7 @@ class ConfigLoaderTest : FunSpec() {
|
||||
loader.load(dir).effectiveBuildDefinitions()["default"] shouldBe BuildDefinition(trigger = TriggerConfig(onPush = false))
|
||||
}
|
||||
|
||||
test("a config that needs a newer werkator is refused, naming the file and both versions") {
|
||||
test("a config that needs a newer Werkator is refused, naming the file and both versions") {
|
||||
val dir = Files.createTempDirectory("werkator-test")
|
||||
dir.resolve(".werkator.yml").toFile().writeText(
|
||||
"""
|
||||
@@ -745,7 +793,7 @@ class ConfigLoaderTest : FunSpec() {
|
||||
loader.loadForWorktree(dir, worktree) shouldBe loader.load(dir)
|
||||
}
|
||||
|
||||
test("toYaml serializes werkatorConfig with all sections") {
|
||||
test("toYaml serializes WerkatorConfig with all sections") {
|
||||
val yaml = loader.toYaml(WerkatorConfig())
|
||||
yaml shouldContain "server:"
|
||||
yaml shouldContain "git:"
|
||||
+3
-3
@@ -14,7 +14,7 @@ class ConfigVersionsTest : FunSpec() {
|
||||
) = ConfigVersions.verdict(VersionRequirement(since = since, below = below), running)
|
||||
|
||||
init {
|
||||
test("a file needing a newer werkator is refused, naming both versions") {
|
||||
test("a file needing a newer Werkator is refused, naming both versions") {
|
||||
val result = verdict(since = "0.9.16", running = "0.9.15")
|
||||
|
||||
result
|
||||
@@ -45,7 +45,7 @@ class ConfigVersionsTest : FunSpec() {
|
||||
val description = "`builds:` is now `buildSpec:`"
|
||||
val written14 = VersionRequirement(since = "1.4.0")
|
||||
|
||||
// no ceiling declared, and none needed: werkator knows its own breaking change
|
||||
// no ceiling declared, and none needed: Werkator knows its own breaking change
|
||||
ConfigVersions
|
||||
.verdict(written14, "2.0.1", brokeIn, description)
|
||||
.shouldBeInstanceOf<VersionVerdict.Incompatible>()
|
||||
@@ -54,7 +54,7 @@ class ConfigVersionsTest : FunSpec() {
|
||||
it shouldContain "2.0.0"
|
||||
it shouldContain "buildSpec"
|
||||
}
|
||||
// a werkator from before the change still reads that file
|
||||
// a Werkator from before the change still reads that file
|
||||
ConfigVersions.verdict(written14, "1.9.0", brokeIn, description) shouldBe VersionVerdict.Compatible
|
||||
// and a file written after the change is fine on both sides of it
|
||||
ConfigVersions.verdict(
|
||||
+1
-1
@@ -41,7 +41,7 @@ class GitCommandRunnerTest : FunSpec() {
|
||||
}
|
||||
|
||||
test("passes extra environment variables") {
|
||||
val result = runner.run(listOf("sh", "-c", "echo \"\$werkator_TEST_VAR\""), tempDir, mapOf("werkator_TEST_VAR" to "hello"))
|
||||
val result = runner.run(listOf("sh", "-c", "echo \"\$WERKATOR_TEST_VAR\""), tempDir, mapOf("WERKATOR_TEST_VAR" to "hello"))
|
||||
|
||||
result.stdout.trim() shouldBe "hello"
|
||||
}
|
||||
+2
-2
@@ -27,9 +27,9 @@ class GitServiceTest : FunSpec() {
|
||||
// hermetic git: fixed identity, no user/system config (hooks, gpg signing, ...)
|
||||
private val gitEnvironment =
|
||||
mapOf(
|
||||
"GIT_AUTHOR_NAME" to "werkator Test",
|
||||
"GIT_AUTHOR_NAME" to "Werkator Test",
|
||||
"GIT_AUTHOR_EMAIL" to "test@example.com",
|
||||
"GIT_COMMITTER_NAME" to "werkator Test",
|
||||
"GIT_COMMITTER_NAME" to "Werkator Test",
|
||||
"GIT_COMMITTER_EMAIL" to "test@example.com",
|
||||
"GIT_CONFIG_GLOBAL" to "/dev/null",
|
||||
"GIT_CONFIG_SYSTEM" to "/dev/null",
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user