Renaming gitTally to Werkator because there is another git-related tool named gittally (#1)

* renaming from gitTally to Werkator

* 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>

* Move the pre-rename state directory at the first start

The configuration is found under either name, the state is not: build
history, control token, auto-build slots and worktrees live at one fixed
path. An installation that updates without moving `.git/gittally` would
not fail — it would come up with an empty history and a fresh control
token, quietly. So the first start moves it instead of the release notes
asking for it.

Only when the old directory exists and the new one does not. Where both
exist nothing is touched and a warning names the leftover: which of the
two is the live state is not something to guess. A failed move is an
error in the log, never an abort — a CI must not hang on it.

The worktrees are dropped rather than moved, since they point at their
old path in both directions; `GitWorktreeWorkspaces` prunes the stale
admin entry and recreates each on its branch's next build. A generated
systemd unit moves with the directory and leaves its symlink dangling,
which is warned about — the running service is unaffected, the next
start is not.

Runs from `CliRunner`, before any command resolves a path under the
directory, and so before the second context of `server` exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Document PR#1: the rename to Werkator

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Drop the legacy env-to-YAML conversion from the setup tool

The old bash script configured itself through `GITTALLY_*` environment
variables. The blanket rename rewrote those literals, so the converter
was looking for `WERKATOR_*` — a spelling no host has ever written. Fed
a real legacy file it would have found nothing and written an almost
empty configuration, without an error, which is the same silent failure
this rename is otherwise careful to avoid.

The conversion has served its purpose with the vm2176 to vm4006
migration, so it goes instead of being repaired. What remains is the
setup of a new instance: the preconditions, the credential prompt, and
the machine configuration written mode 600 — now carrying the host's
public URL as well, since that is host-specific too. Everything the
repository builds comes from `init` and its templates.

It also stops emitting a legacy `branches:` section, which step 18 is
about to reject outright.

`docs/plan/00-legacy-analysis.md` and `13-nginx-tls.md` get the real
`GITTALLY_*` spelling back: they record what the old script read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Describe this repository's build with a build definition

Its own `.werkator.yml` still used the deprecated `branches` section
with an `autoBuild` schedule that was switched off. That section is read
only while nothing defines a build at all, and step 18 rejects it by
name — so this repository would have blocked the precondition of that
step, which asks that no configuration still in play carries it.

Nothing about the build changes: `builds.default` with `trigger.onPush`
is a build of every new commit on every branch, which is what the branch
section said. `config:print --full` resolves the definition completely
and logs no deprecation warning any more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Stop documenting the pre-rename fallback for users

Exactly one repository is configured with the old names, and it is
migrated by hand in the same move as this release. The fallback is
therefore a transition of days, not a feature anyone reading the release
notes or the configuration reference has to plan around.

Removed from `releases.html` and `docs/configuration.md`. The mechanism
itself is unchanged and stays described where it is worked on: in
`ConfigFiles`, in `StateDirMigration`, and in the migration plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Bring the PR-doc to its final state

The `statusContext` question is answered and marked as decided rather
than left standing: it is the one value a human reads as a label, and it
stays lowercase because Gitea matches it and the client reads it back,
which makes it a value.

Also records that the pre-rename fallback is deliberately absent from
the release notes and the configuration reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Release v1.0.0: Werkator

The release after 0.9.21 is 1.0.0, because a product that changes its
name is better off counting from one under it. The release note says as
much, so the jump is not read as a claim about maturity — plan steps 14,
17 and 18 are still open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Correct the PR-doc about the version

It claimed the PR carries no version bump, which the release commit made
untrue, and records why the number is 1.0.0 instead of 0.9.22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Point the legacy references at the history

`legacy/gitTally` was removed from the tree with the rename, but the
README still described it as a reference kept in the repository, and the
plan told an executing session to read parts of it — including step 14,
which is open.

The README section is gone; `docs/plan/README.md`, step 14 and the
legacy analysis now say where the script actually is
(`git show 7f55068^:legacy/gitTally`). Executed steps and ADR 0004 keep
their wording: they record what was true when they ran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Retarget the links in the historic PR-docs

The package rename moved every file the older PR-docs link to, leaving
60 dead links. Only the link targets are rewritten, never the visible
text and never a statement: those documents record what was true when
they were written, GitTally in the prose included. A snapshot may be
outdated; it should still be navigable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Add the v1.0.0 deployment procedure for vm4006

Measured, not estimated: the state directory is 878 MB, of which 878 MB
are the nine build worktrees. What cannot be recreated is 84 KB, so the
snapshot before an in-place switch is instant and the rollback is one
sequence of moves.

Records the three expected non-failures — a cold Gradle volume, one
image rebuild, containers left under the old label — and that
`gitea.statusContext` needs no attention because it comes from the
watched repository's committed configuration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Rename the machine configuration along with its directory

Found by the deployment to vm4006: the move renames the directory and
leaves the file inside it alone, so the machine configuration ended up at
`.git/werkator/.gittally.yml` — a pair of names the lookup did not
expect, because it pairs directory and file name. The instance resolved
empty credentials, no public URL and none of the host's build
definitions, and said nothing about it. That is the exact failure this
change exists to prevent, produced by the change itself.

`StateDirMigration` now renames the configuration with the directory,
unless one under the current name is already there. `ConfigFiles` carries
`.git/werkator/.gittally.yml` as a third candidate as well, for a
directory somebody moved by hand, where the migration never runs and so
can rename nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Let init see a configuration under its previous name

`init --systemd` runs `init`, and its "already exists" check knew only
the current name. On the one repository still carrying `.gittally.yml`
it therefore wrote a fresh template `.werkator.yml` beside it — and
since the current name wins, that repository would have built the
template's `./gradlew test` instead of what its own configuration says.
Found on vm4006, where the file was created in the watched working tree
and removed again by hand.

Both checks now ask `ConfigFiles`, so init decides existence by the same
rule the loader uses to read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Record the v1.0.0 deployment to vm4006

Deployed from the branch as the final test of PR#1, and it did what a
final test is for: it found two silent-failure defects before the
service was started, both fixed and redeployed in the same window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Drop the control token left under the old localStorage key

The key is named after the product, so the rename left every browser
with a token under `gittally.controlToken`, which nothing reads any more
and which "forget token" can no longer reach. It is a write-scope token
in a browser store, not a password, but a secret nobody owns is worth
one line to remove.

Removed on load. The token on the server is unchanged, so re-entering it
once per browser is all the rename costs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Michael Hönnig
2026-08-31 13:32:54 +02:00
committed by GitHub
co-authored by Claude Opus 5
parent 4d76744721
commit d0169e57bc
184 changed files with 2237 additions and 7837 deletions
@@ -1,4 +1,4 @@
package de.hoennig.gittally
package de.hoennig.werkator
import java.nio.ByteBuffer
import java.nio.file.Files
@@ -8,7 +8,7 @@ import java.nio.file.attribute.PosixFilePermissions
/**
* Creation of files and directories that hold secrets the Gitea token in
* `.git/gittally/.gittally.yml` and the control token.
* `.git/werkator/.werkator.yml` and the control token.
*
* The permissions are set *at creation*, never with a `chmod` after the write:
* writing at the umask default first (typically `0644`) would leave a window in
@@ -0,0 +1,103 @@
package de.hoennig.werkator
import de.hoennig.werkator.config.ConfigFiles
import org.slf4j.LoggerFactory
import java.io.IOException
import java.nio.file.Files
import java.nio.file.Path
/**
* Moves the state directory of an installation that predates the rename to Werkator,
* once, at the first start after the update.
*
* The configuration is found under either name ([de.hoennig.werkator.config.ConfigFiles]),
* but the state is not: build history, the control token, the auto-build slots and the
* build worktrees live at one fixed path. A missing state directory is as quiet as a
* missing configuration — the instance would come up with an empty history and a fresh
* control token, and nothing would fail. So this is done rather than documented.
*/
object StateDirMigration {
const val DIR = ".git/werkator"
private const val LEGACY_DIR = ".git/gittally"
private const val WORKTREES = "worktrees"
private val log = LoggerFactory.getLogger(StateDirMigration::class.java)
/**
* Renames `.git/gittally` to `.git/werkator` in [workingDir], if the first exists and
* the second does not. Never throws: a failed move must not stop a CI, it must say
* what to do by hand.
*/
fun migrateIfNeeded(workingDir: Path) {
val legacy = workingDir.resolve(LEGACY_DIR)
val current = workingDir.resolve(DIR)
if (!Files.isDirectory(legacy)) return
if (Files.exists(current)) {
// both exist: which of the two is the live state is not ours to guess
log.warn("{} exists next to {} — the leftover is ignored, remove it once you are sure", LEGACY_DIR, DIR)
return
}
try {
Files.move(legacy, current)
} catch (e: IOException) {
log.error("could not move {} to {}: {} — move it by hand", LEGACY_DIR, DIR, e.message)
return
}
log.info("moved {} to {}: build history, control token and configuration kept", LEGACY_DIR, DIR)
renameMachineConfig(current)
dropWorktrees(current)
warnAboutUnits(current)
}
/**
* Moving the directory leaves the file inside it under its old name, and the pair
* of names is then one the lookup does not expect. Renaming it here is what makes
* the move complete instead of half-done.
*/
private fun renameMachineConfig(stateDir: Path) {
val legacy = stateDir.resolve(ConfigFiles.LEGACY_COMMITTED)
val current = stateDir.resolve(ConfigFiles.COMMITTED)
if (!Files.isRegularFile(legacy) || Files.exists(current)) return
try {
Files.move(legacy, current)
} catch (e: IOException) {
log.warn("could not rename {} to {} in {}: {}", ConfigFiles.LEGACY_COMMITTED, ConfigFiles.COMMITTED, stateDir, e.message)
return
}
log.info("renamed the machine configuration to {}", ConfigFiles.COMMITTED)
}
/**
* The moved worktrees point at their old path in both directions, so they are dropped
* rather than repaired: the next build of a branch creates its worktree again, and
* `GitWorktreeWorkspaces` prunes the stale admin entry before it does.
*/
private fun dropWorktrees(stateDir: Path) {
val worktrees = stateDir.resolve(WORKTREES)
if (!Files.isDirectory(worktrees)) return
if (worktrees.toFile().deleteRecursively()) {
log.info("dropped the moved build worktrees; each is recreated by its branch's next build")
} else {
log.warn("could not drop the moved build worktrees in {} — delete them by hand", worktrees)
}
}
/**
* The generated systemd unit lives in the state directory and is symlinked from
* `~/.config/systemd/user`, so the move leaves that link dangling — the service keeps
* running and fails to start the next time.
*/
private fun warnAboutUnits(stateDir: Path) {
val units =
Files
.list(stateDir)
.use { paths -> paths.map { it.fileName.toString() }.filter { it.endsWith(".service") }.toList() }
if (units.isEmpty()) return
log.warn(
"the systemd unit {} moved with the state directory and its symlink now dangles — " +
"re-run `werkator init --systemd` and re-link it",
units.joinToString(", "),
)
}
}
@@ -1,6 +1,6 @@
package de.hoennig.gittally
package de.hoennig.werkator
import de.hoennig.gittally.config.ConfigException
import de.hoennig.werkator.config.ConfigException
import org.springframework.boot.CommandLineRunner
import org.springframework.boot.ExitCodeGenerator
import org.springframework.boot.SpringApplication
@@ -10,26 +10,29 @@ import org.springframework.context.annotation.Profile
import org.springframework.stereotype.Component
import picocli.CommandLine
import picocli.CommandLine.IFactory
import java.nio.file.Paths
import kotlin.system.exitProcess
@SpringBootApplication
class GitTallyApplication
class WerkatorApplication
/** Not in the `server` profile: the second context started by `ServerCommand` must not run picocli again. */
@Component
@Profile("!server")
class CliRunner(
private val factory: IFactory,
private val rootCommand: GitTallyCommand,
private val rootCommand: WerkatorCommand,
) : CommandLineRunner,
ExitCodeGenerator {
private var exitCode = 0
override fun run(vararg args: String) {
// before any command resolves a path under it, and once per process
StateDirMigration.migrateIfNeeded(Paths.get("."))
exitCode =
CommandLine(rootCommand, factory)
.setExecutionExceptionHandler { exception, commandLine, _ ->
// a config GitTally 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}")
@@ -49,5 +52,5 @@ class CliRunner(
}
fun main(args: Array<String>) {
exitProcess(SpringApplication.exit(runApplication<GitTallyApplication>(*args)))
exitProcess(SpringApplication.exit(runApplication<WerkatorApplication>(*args)))
}
@@ -1,11 +1,11 @@
package de.hoennig.gittally
package de.hoennig.werkator
import de.hoennig.gittally.commands.BuildCommand
import de.hoennig.gittally.commands.ConfigPrintCommand
import de.hoennig.gittally.commands.InitCommand
import de.hoennig.gittally.commands.RetryCommand
import de.hoennig.gittally.commands.ServerCommand
import de.hoennig.gittally.commands.StatusCommand
import de.hoennig.werkator.commands.BuildCommand
import de.hoennig.werkator.commands.ConfigPrintCommand
import de.hoennig.werkator.commands.InitCommand
import de.hoennig.werkator.commands.RetryCommand
import de.hoennig.werkator.commands.ServerCommand
import de.hoennig.werkator.commands.StatusCommand
import org.springframework.beans.factory.ObjectProvider
import org.springframework.boot.info.BuildProperties
import org.springframework.stereotype.Component
@@ -14,7 +14,7 @@ import picocli.CommandLine.Command
@Component
@Command(
name = "gittally",
name = "werkator",
subcommands = [
InitCommand::class,
ServerCommand::class,
@@ -27,7 +27,7 @@ import picocli.CommandLine.Command
versionProvider = BuildPropertiesVersionProvider::class,
description = ["Lightweight, declarative CI/CD system"],
)
class GitTallyCommand : Runnable {
class WerkatorCommand : Runnable {
override fun run(): Unit = throw CommandLine.ParameterException(CommandLine(this), "Specify a subcommand")
}
@@ -41,5 +41,5 @@ class GitTallyCommand : Runnable {
class BuildPropertiesVersionProvider(
private val buildProperties: ObjectProvider<BuildProperties>,
) : CommandLine.IVersionProvider {
override fun getVersion(): Array<String> = arrayOf("GitTally v${buildProperties.getIfAvailable()?.version ?: "dev"}")
override fun getVersion(): Array<String> = arrayOf("Werkator v${buildProperties.getIfAvailable()?.version ?: "dev"}")
}
@@ -1,7 +1,7 @@
package de.hoennig.gittally.artifacts
package de.hoennig.werkator.artifacts
import de.hoennig.gittally.build.ArtifactStore
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.werkator.build.ArtifactStore
import de.hoennig.werkator.config.ConfigLoader
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@@ -1,10 +1,10 @@
package de.hoennig.gittally.artifacts
package de.hoennig.werkator.artifacts
import de.hoennig.gittally.build.ArtifactKeys
import de.hoennig.gittally.build.ArtifactStore
import de.hoennig.gittally.build.BuildResult
import de.hoennig.gittally.config.BranchConfig
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.werkator.build.ArtifactKeys
import de.hoennig.werkator.build.ArtifactStore
import de.hoennig.werkator.build.BuildResult
import de.hoennig.werkator.config.BranchConfig
import de.hoennig.werkator.config.ConfigLoader
import org.slf4j.LoggerFactory
import java.io.IOException
import java.nio.file.FileVisitResult
@@ -22,7 +22,7 @@ import kotlin.concurrent.write
/**
* Stores build artifacts on the filesystem under `<root>/branches/<artifactKey>/`.
* The root is `artifacts.rootDir` from the config, or the platform default
* `XDG_STATE_HOME` (falling back to `~/.local/state`) plus `/gittally/artifacts/<repo-key>` when unset
* `XDG_STATE_HOME` (falling back to `~/.local/state`) plus `/werkator/artifacts/<repo-key>` when unset
* deliberately not `/tmp` like legacy, where artifacts vanished on reboot.
*
* Each build is assembled in a temporary directory next to its target and moved
@@ -115,7 +115,7 @@ class FileArtifactStore(
env("XDG_STATE_HOME")?.takeIf { it.isNotBlank() }?.let { Paths.get(it) }
?: Paths.get(System.getProperty("user.home"), ".local", "state")
return stateHome
.resolve("gittally")
.resolve("werkator")
.resolve("artifacts")
.resolve(ArtifactKeys.repoKey(workingDir))
.toAbsolutePath()
@@ -160,9 +160,9 @@ class FileArtifactStore(
}
/**
* The settings [build] ran with, from the build [workspace]'s `.gittally.yml` layered
* The settings [build] ran with, from the build [workspace]'s `.werkator.yml` layered
* on top of the primary config (see [ConfigLoader.loadForWorktree]) resolved through
* [GitTallyConfig.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(
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import java.nio.file.Path
import java.security.MessageDigest
@@ -1,11 +1,11 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import java.nio.file.Path
/**
* Persists the artifacts of finished builds (logs plus configured report directories)
* and prunes them together with the result retention.
* Implemented by `de.hoennig.gittally.artifacts.FileArtifactStore`.
* Implemented by `de.hoennig.werkator.artifacts.FileArtifactStore`.
*/
interface ArtifactStore {
/**
@@ -1,6 +1,6 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import de.hoennig.gittally.git.GitService
import de.hoennig.werkator.git.GitService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.nio.file.Files
@@ -20,7 +20,7 @@ fun interface BranchWorkspaces {
}
/**
* One reusable git worktree per branch under `.git/gittally/worktrees/<branchKey>`,
* One reusable git worktree per branch under `.git/werkator/worktrees/<branchKey>`,
* checked out detached at the requested commit. Reuse keeps incremental build
* caches; the branch's `cleanCommand` decides how much of them survives.
*/
@@ -50,6 +50,6 @@ class GitWorktreeWorkspaces(
}
companion object {
const val WORKTREES_DIR = ".git/gittally/worktrees"
const val WORKTREES_DIR = ".git/werkator/worktrees"
}
}
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@@ -8,9 +8,9 @@ import java.nio.file.Paths
class BuildConfiguration {
/**
* Results file relative to the working directory, matching how `ConfigLoader`
* resolves the `.git/gittally/` override file. Nothing is touched until the
* resolves the `.git/werkator/` override file. Nothing is touched until the
* first build runs, so the bean is safe outside a git repository.
*/
@Bean
fun buildResultRepository(): BuildResultRepository = FileBuildResultRepository(Paths.get(".git/gittally/build-results.json"))
fun buildResultRepository(): BuildResultRepository = FileBuildResultRepository(Paths.get(".git/werkator/build-results.json"))
}
@@ -1,9 +1,9 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import de.hoennig.gittally.config.BranchConfig
import de.hoennig.gittally.config.BuildDefinition
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.gittally.gitea.GiteaClient
import de.hoennig.werkator.config.BranchConfig
import de.hoennig.werkator.config.BuildDefinition
import de.hoennig.werkator.config.ConfigLoader
import de.hoennig.werkator.gitea.GiteaClient
import org.slf4j.LoggerFactory
import org.springframework.context.ApplicationEventPublisher
import org.springframework.context.event.ContextClosedEvent
@@ -93,7 +93,7 @@ class BuildExecutor(
return duplicate.runningBuild
}
val startedAt = Instant.now()
val stagingDir = Files.createTempDirectory("gittally-build-")
val stagingDir = Files.createTempDirectory("werkator-build-")
val runningBuild =
RunningBuild(
branch = branch,
@@ -249,7 +249,7 @@ class BuildExecutor(
private fun serialWorker(branch: String): ExecutorService =
Executors.newSingleThreadExecutor { runnable ->
Thread(runnable, "gittally-build-${ArtifactKeys.branchKey(branch)}").apply { isDaemon = true }
Thread(runnable, "werkator-build-${ArtifactKeys.branchKey(branch)}").apply { isDaemon = true }
}
private fun runBuildCommands(
@@ -324,7 +324,7 @@ class BuildExecutor(
input: InputStream,
vararg sinks: OutputStream,
): Thread =
thread(isDaemon = true, name = "gittally-build-log") {
thread(isDaemon = true, name = "werkator-build-log") {
val buffer = ByteArray(8192)
try {
while (true) {
@@ -480,7 +480,7 @@ class BuildExecutor(
/**
* The effective settings of this run: the branch config with the build [worktree]'s
* `.gittally.yml` layered on top (see [ConfigLoader.loadForWorktree]), then the
* `.werkator.yml` layered on top (see [ConfigLoader.loadForWorktree]), then the
* build definition's overrides applied last the job wins, and it always comes
* from the primary config (`builds` is a pinned section). An unknown build name
* (a stale result whose job was removed) falls back to the plain branch settings.
@@ -1,6 +1,6 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import de.hoennig.gittally.config.BuildDefinition
import de.hoennig.werkator.config.BuildDefinition
import java.time.Duration
import java.time.Instant
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import java.time.Instant
@@ -1,6 +1,6 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import de.hoennig.gittally.config.BranchConfig
import de.hoennig.werkator.config.BranchConfig
import org.springframework.context.annotation.Primary
import org.springframework.stereotype.Component
import java.nio.file.Path
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
enum class BuildStatus {
PENDING,
@@ -1,8 +1,8 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import de.hoennig.gittally.config.BranchConfig
import de.hoennig.gittally.config.DockerConfig
import de.hoennig.gittally.git.GitCommandRunner
import de.hoennig.werkator.config.BranchConfig
import de.hoennig.werkator.config.DockerConfig
import de.hoennig.werkator.git.GitCommandRunner
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Component
import java.nio.file.Files
@@ -23,7 +23,7 @@ import java.nio.file.Path
* same container run; under a rootless daemon the container runs as root, which
* already is the host user, so the repair chown degenerates to `0:0`.
* Git works inside the container: the primary `.git` is mounted read-only with
* `.git/gittally/` masked, see [gitMetadataMounts].
* `.git/werkator/` masked, see [gitMetadataMounts].
*/
@Component
class DockerBuildRunner(
@@ -114,11 +114,11 @@ class DockerBuildRunner(
"docker",
"build",
"--label",
"org.gittally.dockerfile=${docker.dockerfile}",
"org.werkator.dockerfile=${docker.dockerfile}",
"--label",
"org.gittally.dockerfile-sha256=$dockerfileHash",
"org.werkator.dockerfile-sha256=$dockerfileHash",
"--label",
"org.gittally.build-context=${docker.context}",
"org.werkator.build-context=${docker.context}",
"--label",
"${DockerImageInputs.INPUTS_LABEL}=$inputsHash",
"-t",
@@ -194,11 +194,11 @@ class DockerBuildRunner(
"ps",
"-aq",
"--filter",
"label=$GITTALLY_LABEL=true",
"label=$WERKATOR_LABEL=true",
"--filter",
"label=$GITTALLY_LABEL.repository=$repoKey",
"label=$WERKATOR_LABEL.repository=$repoKey",
"--filter",
"label=$GITTALLY_LABEL.role=build",
"label=$WERKATOR_LABEL.role=build",
),
repoDir,
)
@@ -238,11 +238,11 @@ class DockerBuildRunner(
args +=
listOf(
"--label",
"$GITTALLY_LABEL=true",
"$WERKATOR_LABEL=true",
"--label",
"$GITTALLY_LABEL.repository=$repoKey",
"$WERKATOR_LABEL.repository=$repoKey",
"--label",
"$GITTALLY_LABEL.role=build",
"$WERKATOR_LABEL.role=build",
)
args += listOf("--workdir", "$workspace", "--volume", "$workspace:$workspace")
args += gitMetadataMounts(workspace, repoDir)
@@ -279,12 +279,12 @@ class DockerBuildRunner(
}
/**
* Makes git work inside the build container without exposing GitTally'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
* in the build would fail. Three layered mounts fix that (Docker nests mounts by
* target path): the primary `.git` read-only, an empty tmpfs masking `.git/gittally/`
* target path): the primary `.git` read-only, an empty tmpfs masking `.git/werkator/`
* (machine config with `git.token`, control token, build state the workspace bind
* resurfaces only this build's own worktree inside it), and this worktree's admin
* directory read-write, so index-refreshing commands like `git status` keep working.
@@ -312,9 +312,9 @@ class DockerBuildRunner(
return emptyList()
}
val args = mutableListOf("--volume", "$gitDir:$gitDir:ro")
val gittallyDir = gitDir.resolve("gittally")
if (Files.isDirectory(gittallyDir)) {
args += listOf("--tmpfs", "$gittallyDir")
val werkatorDir = gitDir.resolve("werkator")
if (Files.isDirectory(werkatorDir)) {
args += listOf("--tmpfs", "$werkatorDir")
}
args += listOf("--volume", "$adminDir:$adminDir")
return args
@@ -326,15 +326,15 @@ class DockerBuildRunner(
): String = commandRunner.runOrThrow(listOf("id", flag), repoDir).stdout.trim()
companion object {
/** Container label namespace; legacy used `org.hostsharing.gittally`. */
const val GITTALLY_LABEL = "org.hoennig.gittally"
/** Container label namespace; legacy used `org.hostsharing.werkator`. */
const val WERKATOR_LABEL = "org.hoennig.werkator"
fun gradleVolumeName(repoKey: String): String = "gittally-gradle-$repoKey"
fun gradleVolumeName(repoKey: String): String = "werkator-gradle-$repoKey"
fun containerName(
repoKey: String,
branch: String?,
): String = "gittally-build-$repoKey" + (branch?.let { "-${ArtifactKeys.branchKey(it)}" } ?: "")
): String = "werkator-build-$repoKey" + (branch?.let { "-${ArtifactKeys.branchKey(it)}" } ?: "")
private val INSPECT_INPUTS_LABEL_FORMAT = """{{ index .Config.Labels "${DockerImageInputs.INPUTS_LABEL}" }}"""
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import java.nio.file.Files
import java.nio.file.Path
@@ -10,7 +10,7 @@ import java.security.MessageDigest
* with the configured Dockerfile and context paths, stored as an image label.
*/
object DockerImageInputs {
const val INPUTS_LABEL = "org.gittally.build-inputs-sha256"
const val INPUTS_LABEL = "org.werkator.build-inputs-sha256"
fun dockerfileSha256(dockerfile: Path): String = sha256Hex(Files.readAllBytes(dockerfile))
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import org.springframework.stereotype.Component
import java.nio.file.Files
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
@@ -13,7 +13,7 @@ import java.nio.file.StandardCopyOption
import java.time.Instant
/**
* Stores build results as a JSON file, e.g. `.git/gittally/build-results.json`.
* Stores build results as a JSON file, e.g. `.git/werkator/build-results.json`.
* Writes are atomic (temp file + atomic move) so readers never see partial content.
*/
class FileBuildResultRepository(
@@ -1,6 +1,6 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import de.hoennig.gittally.config.BuildDefinition
import de.hoennig.werkator.config.BuildDefinition
import java.nio.file.Path
import java.time.Instant
@@ -1,4 +1,4 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
/**
* Port of the legacy `resolve_branch_name` partial-name matching: a branch-name
@@ -1,7 +1,7 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import de.hoennig.gittally.build.BuildStatus
import de.hoennig.gittally.git.GitService
import de.hoennig.werkator.build.BuildStatus
import de.hoennig.werkator.git.GitService
import org.springframework.stereotype.Component
import picocli.CommandLine.Command
import picocli.CommandLine.ExitCode
@@ -1,6 +1,6 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.werkator.config.ConfigLoader
import org.springframework.stereotype.Component
import picocli.CommandLine.Command
import picocli.CommandLine.Option
@@ -1,13 +1,13 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import de.hoennig.gittally.build.ArtifactStore
import de.hoennig.gittally.build.BuildExecutor
import de.hoennig.gittally.build.BuildResult
import de.hoennig.gittally.build.BuildResultRepository
import de.hoennig.gittally.build.BuildStatus
import de.hoennig.gittally.build.RunningBuild
import de.hoennig.gittally.config.BuildDefinition
import de.hoennig.gittally.server.UiFormats
import de.hoennig.werkator.build.ArtifactStore
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.build.RunningBuild
import de.hoennig.werkator.config.BuildDefinition
import de.hoennig.werkator.server.UiFormats
import org.springframework.stereotype.Component
import java.io.IOException
import java.nio.channels.Channels
@@ -1,7 +1,8 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import de.hoennig.gittally.SecretFiles
import de.hoennig.gittally.git.GitService
import de.hoennig.werkator.SecretFiles
import de.hoennig.werkator.config.ConfigFiles
import de.hoennig.werkator.git.GitService
import org.springframework.beans.factory.ObjectProvider
import org.springframework.boot.info.BuildProperties
import org.springframework.stereotype.Component
@@ -13,19 +14,19 @@ import java.nio.file.Paths
@Component
@Command(
name = "init",
description = ["Initialize GitTally for the current repository"],
description = ["Initialize Werkator for the current repository"],
mixinStandardHelpOptions = true,
)
class InitCommand(
private val gitService: GitService,
/** The version written into the generated config as `gitTally.version.since`. */
/** The version written into the generated config as `werkator.version.since`. */
private val buildProperties: ObjectProvider<BuildProperties>? = null,
) : Runnable {
var workingDir: Path = Paths.get(".")
@Option(
names = ["--systemd"],
description = ["also generate a systemd user unit that runs `gittally server` for this repository"],
description = ["also generate a systemd user unit that runs `werkator server` for this repository"],
)
var systemd: Boolean = false
@@ -56,7 +57,7 @@ class InitCommand(
}
/**
* The running version for `gitTally.version.since`; outside a built jar (IDE, tests)
* The running version for `werkator.version.since`; outside a built jar (IDE, tests)
* there is none, and `0.0.0` then declares no floor at all rather than a wrong one.
*/
private fun runningVersion(): String = buildProperties?.getIfAvailable()?.version ?: "0.0.0"
@@ -99,7 +100,8 @@ class InitCommand(
detected: DetectedValues,
normalizedWorkingDir: Path,
) {
val file = root.resolve(".git/gittally/.gittally.yml")
val existing = ConfigFiles.firstExisting(root, ConfigFiles.repoInstall)
val file = root.resolve(existing)
if (file.toFile().exists()) {
println("${file.toFile().relativeTo(normalizedWorkingDir.toFile())} already exists — not overwritten")
return
@@ -107,7 +109,7 @@ class InitCommand(
SecretFiles.createDirectoriesOwnerOnly(file.parent)
val content =
"""
# Machine- or user-specific overrides and secrets. Keys here win over .gittally.yml.
# Machine- or user-specific overrides and secrets. Keys here win over .werkator.yml.
git:
account: "${detected.account}" # technical username for git HTTPS authentication
token: "" # Gitea API token never commit this
@@ -123,31 +125,35 @@ class InitCommand(
detected: DetectedValues,
normalizedWorkingDir: Path,
) {
val file = root.resolve(".gittally.yml")
// under either name: writing a second one beside a config under the previous
// name would shadow it, and a repository would silently start building
// something else than what it says it builds
val existing = ConfigFiles.firstExisting(root)
val file = root.resolve(existing)
if (file.toFile().exists()) {
println("${file.toFile().relativeTo(normalizedWorkingDir.toFile())} already exists — not overwritten")
return
}
val content =
"""
# The GitTally this file is written for.
# since: enforced an older GitTally 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; GitTally 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.
gitTally:
werkator:
version:
since: "${runningVersion()}"
# below: "2.0"
server:
# Public base URL of this GitTally 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 GitTally 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: ""
@@ -159,8 +165,8 @@ class InitCommand(
httpPort: 8080 # host port published as nginx port 80
httpsPort: 8443 # host port published as nginx port 443
upstreamHost: "" # host nginx proxies to; empty = serverName
containerName: "" # empty = gittally-nginx-<repo-name>
stateDir: "" # empty = XDG_STATE_HOME (or ~/.local/state) + /gittally/nginx/<repo-key>
containerName: "" # empty = werkator-nginx-<repo-name>
stateDir: "" # empty = XDG_STATE_HOME (or ~/.local/state) + /werkator/nginx/<repo-key>
letsencryptEmail: "" # e-mail for the Let's Encrypt account; empty registers without one
# Gitea integration for fetching commits and posting build statuses.
@@ -168,7 +174,7 @@ class InitCommand(
baseUrl: ${detected.baseUrl} # base URL of the Gitea instance
owner: ${detected.owner} # repository owner (user or organisation) for Gitea API (e.g. status checks)
repo: ${detected.repo} # repository name
statusContext: GitTally # label shown on Gitea commit status checks (default: GitTally)
statusContext: werkator # label shown on Gitea commit status checks (default: werkator)
# Build execution settings, enforced for all builds regardless of their trigger.
executor:
@@ -178,7 +184,7 @@ class InitCommand(
# Named build definitions (jobs); every key names a build.
# "default" is the base every other definition inherits its settings from never
# its trigger and is itself the build of every branch as long as it has one.
# A branch may add or override definitions in its own committed .gittally.yml;
# A branch may add or override definitions in its own committed .werkator.yml;
# they apply to that branch alone, so a new job can be tried out on one branch.
builds:
default:
@@ -218,11 +224,11 @@ class InitCommand(
# atTimes: ["01:00"]
# branches: ["master"]
# buildCommand: ./gradlew pitestFull
# statusContext: GitTally/pitest
# statusContext: werkator/pitest
# Build artifact storage and retention.
artifacts:
# root directory for stored artifacts; empty = XDG_STATE_HOME (or ~/.local/state) + /gittally/artifacts/<repo-key>
# root directory for stored artifacts; empty = XDG_STATE_HOME (or ~/.local/state) + /werkator/artifacts/<repo-key>
rootDir: ""
# number of builds to keep per branch
retentionPerBranch: 3
@@ -256,14 +262,14 @@ class InitCommand(
) {
val jarPath = jarPathResolver()
if (jarPath == null) {
println("Error: cannot determine the GitTally jar path — run `init --systemd` via `java -jar <path-to>/gittally.jar`")
println("Error: cannot determine the Werkator jar path — run `init --systemd` via `java -jar <path-to>/werkator.jar`")
return
}
val gittallyDir = root.resolve(".git/gittally")
SecretFiles.createDirectoriesOwnerOnly(gittallyDir)
val werkatorDir = root.resolve(".git/werkator")
SecretFiles.createDirectoriesOwnerOnly(werkatorDir)
val unitName = SystemdServiceFiles.unitName(root)
val unitFile = gittallyDir.resolve(unitName)
val envFile = gittallyDir.resolve(SystemdServiceFiles.ENV_FILE_NAME)
val unitFile = werkatorDir.resolve(unitName)
val envFile = werkatorDir.resolve(SystemdServiceFiles.ENV_FILE_NAME)
unitFile.toFile().writeText(
SystemdServiceFiles.unitFileContent(
@@ -283,9 +289,9 @@ class InitCommand(
}
// the nightly Docker cleanup is host-global: every repository generates the same
// units, so with several GitTally instances the symlinks simply coincide
val pruneServiceFile = gittallyDir.resolve(SystemdServiceFiles.PRUNE_SERVICE_NAME)
val pruneTimerFile = gittallyDir.resolve(SystemdServiceFiles.PRUNE_TIMER_NAME)
// 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())
println("created ${pruneServiceFile.toFile().relativeTo(normalizedWorkingDir.toFile())}")
pruneTimerFile.toFile().writeText(SystemdServiceFiles.pruneTimerContent())
@@ -1,9 +1,9 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import de.hoennig.gittally.build.BuildResult
import de.hoennig.gittally.build.BuildResultRepository
import de.hoennig.gittally.build.BuildStatus
import de.hoennig.gittally.git.GitService
import de.hoennig.werkator.build.BuildResult
import de.hoennig.werkator.build.BuildResultRepository
import de.hoennig.werkator.build.BuildStatus
import de.hoennig.werkator.git.GitService
import org.springframework.stereotype.Component
import picocli.CommandLine.Command
import picocli.CommandLine.ExitCode
@@ -1,7 +1,7 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import de.hoennig.gittally.GitTallyApplication
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.werkator.WerkatorApplication
import de.hoennig.werkator.config.ConfigLoader
import org.springframework.boot.WebApplicationType
import org.springframework.boot.builder.SpringApplicationBuilder
import org.springframework.context.ApplicationListener
@@ -24,7 +24,7 @@ import java.util.concurrent.CountDownLatch
@Component
@Command(
name = "server",
description = ["Start the GitTally server"],
description = ["Start the Werkator server"],
mixinStandardHelpOptions = true,
)
class ServerCommand(
@@ -35,7 +35,7 @@ class ServerCommand(
override fun run() {
val config = configLoader.load(workingDir)
val context =
SpringApplicationBuilder(GitTallyApplication::class.java)
SpringApplicationBuilder(WerkatorApplication::class.java)
.web(WebApplicationType.SERVLET)
.profiles(SERVER_PROFILE)
.properties(
@@ -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("GitTally 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)
}
@@ -1,8 +1,8 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import de.hoennig.gittally.build.BuildResult
import de.hoennig.gittally.build.BuildResultRepository
import de.hoennig.gittally.server.UiFormats
import de.hoennig.werkator.build.BuildResult
import de.hoennig.werkator.build.BuildResultRepository
import de.hoennig.werkator.server.UiFormats
import org.springframework.stereotype.Component
import picocli.CommandLine.Command
import picocli.CommandLine.ExitCode
@@ -1,21 +1,21 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.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`,
* `werkator 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"
const val ENV_FILE_NAME = "werkator.env"
/** Host-global unit names of the nightly Docker cleanup — shared by all GitTally repositories on the host. */
const val PRUNE_SERVICE_NAME = "gittally-docker-prune.service"
const val PRUNE_TIMER_NAME = "gittally-docker-prune.timer"
/** 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 GitTally instance serves exactly one repository. */
fun unitName(repoRoot: Path): String = "gittally-${sanitize(repoRoot.fileName.toString())}.service"
/** 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(
repoRoot: Path,
@@ -25,7 +25,7 @@ object SystemdServiceFiles {
): String =
"""
[Unit]
Description=GitTally 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 (GitTally)
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 (GitTally)
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 GitTally systemd service.
# GitTally itself is configured via .gittally.yml and .git/gittally/.gittally.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"
@@ -1,4 +1,4 @@
package de.hoennig.gittally.config
package de.hoennig.werkator.config
import java.time.Duration
import java.time.Instant
@@ -0,0 +1,57 @@
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"
/** The name the committed configuration had before the rename. */
const val LEGACY_COMMITTED = ".gittally.yml"
private const val LEGACY_REPO_INSTALL = ".git/gittally/$LEGACY_COMMITTED"
/**
* The state directory moves without touching the file inside it, so between the
* move and the rename the machine configuration sits under the old name in the
* new directory. `StateDirMigration` closes that gap where it moves the directory
* itself; this candidate covers a directory somebody moved by hand.
*/
private const val MOVED_REPO_INSTALL = ".git/werkator/$LEGACY_COMMITTED"
/** Both names of the committed configuration, current first. */
val committed = listOf(COMMITTED, LEGACY_COMMITTED)
/** Every path the machine-specific configuration can sit at, current first. */
val repoInstall = listOf(REPO_INSTALL, MOVED_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)
}
@@ -1,4 +1,4 @@
package de.hoennig.gittally.config
package de.hoennig.werkator.config
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
@@ -17,7 +17,7 @@ import java.util.concurrent.ConcurrentHashMap
@Service
class ConfigLoader(
/** The running version, for the `gitTally.version` check; absent outside a built jar (IDE, tests). */
/** The running version, for the `werkator.version` check; absent outside a built jar (IDE, tests). */
private val buildProperties: ObjectProvider<BuildProperties>? = null,
) {
private val log = LoggerFactory.getLogger(ConfigLoader::class.java)
@@ -37,21 +37,21 @@ class ConfigLoader(
/** Section-level warnings already reported, keyed by a fixed slug; the config is loaded on every poll cycle. */
private val warnedSections = ConcurrentHashMap.newKeySet<String>()
fun load(workingDir: Path = Paths.get(".")): GitTallyConfig = toConfig(loadRaw(workingDir))
fun load(workingDir: Path = Paths.get(".")): WerkatorConfig = toConfig(loadRaw(workingDir))
/**
* Config for building a branch in [worktreeDir]: the worktree's `.gittally.yml`
* Config for building a branch in [worktreeDir]: the worktree's `.werkator.yml`
* (the committed config of the branch being built) is applied as the branch layer,
* see [loadWithBranchLayer]. With no worktree `.gittally.yml` this is identical
* see [loadWithBranchLayer]. With no worktree `.werkator.yml` this is identical
* to [load].
*/
fun loadForWorktree(
workingDir: Path,
worktreeDir: Path,
): GitTallyConfig = withBranchLayer(workingDir, loadFile(worktreeDir.resolve(".gittally.yml").toFile()))
): WerkatorConfig = withBranchLayer(workingDir, loadFile(worktreeDir.resolve(ConfigFiles.firstExisting(worktreeDir)).toFile()))
/**
* The primary/`.git` config with the committed `.gittally.yml` of one branch
* The primary/`.git` config with the committed `.werkator.yml` of one branch
* ([branchConfigYaml], null or blank for a branch without one) merged on top:
* precedence branch > `.git` > project. A branch describes its own CI build
* settings (`buildCommand`, `cleanCommand`, `artifactDirs`, `docker.image`/`env`, )
@@ -70,25 +70,25 @@ class ConfigLoader(
fun loadWithBranchLayer(
workingDir: Path,
branchConfigYaml: String?,
): GitTallyConfig = withBranchLayer(workingDir, parseYaml(branchConfigYaml))
): WerkatorConfig = withBranchLayer(workingDir, parseYaml(branchConfigYaml))
private fun withBranchLayer(
workingDir: Path,
branchLayer: Map<String, Any?>,
): GitTallyConfig {
): WerkatorConfig {
// scoped to this branch: an incompatible branch config fails its own builds and
// must never stop the server or hold up the branches that are fine
checkVersion(branchLayer, "the committed .gittally.yml of this branch", BRANCH_HINT)
checkTriggerBlocks(branchLayer, "the committed .gittally.yml of this branch", BRANCH_HINT)
checkVersion(branchLayer, "the committed .werkator.yml of this branch", BRANCH_HINT)
checkTriggerBlocks(branchLayer, "the committed .werkator.yml of this branch", BRANCH_HINT)
return toConfig(deepMerge(loadRaw(workingDir), stripPinned(branchLayer)))
}
private fun toConfig(raw: Map<String, Any?>): GitTallyConfig {
private fun toConfig(raw: Map<String, Any?>): WerkatorConfig {
val config =
if (raw.isEmpty()) {
GitTallyConfig()
WerkatorConfig()
} else {
yaml.convertValue(resolveBuildSections(dropNonDefinitionBuilds(raw)), GitTallyConfig::class.java)
yaml.convertValue(resolveBuildSections(dropNonDefinitionBuilds(raw)), WerkatorConfig::class.java)
}
return defaultPublicBaseUrl(config)
}
@@ -265,7 +265,7 @@ class ConfigLoader(
}
/** Legacy default: an empty `server.publicBaseUrl` becomes `https://<nginx.serverName>/`. */
private fun defaultPublicBaseUrl(config: GitTallyConfig): GitTallyConfig {
private fun defaultPublicBaseUrl(config: WerkatorConfig): WerkatorConfig {
if (config.server.publicBaseUrl.isNotBlank() ||
config.server.nginx.serverName
.isBlank()
@@ -276,18 +276,21 @@ class ConfigLoader(
}
fun loadRaw(workingDir: Path = Paths.get(".")): Map<String, Any?> {
val repoInstall = loadFile(workingDir.resolve(".git/gittally/.gittally.yml").toFile())
val project = loadFile(workingDir.resolve(".gittally.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, ".gittally.yml", ROLLBACK_HINT)
checkVersion(repoInstall, ".git/gittally/.gittally.yml", ROLLBACK_HINT)
checkTriggerBlocks(project, ".gittally.yml", ROLLBACK_HINT)
checkTriggerBlocks(repoInstall, ".git/gittally/.gittally.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)
}
/**
* Enforces the `gitTally.version` declaration of one configuration file.
* Enforces the `werkator.version` declaration of one configuration file.
* An incompatible file throws reading it would mean honoring keys that mean
* something else now, which is worse than not building. A file that merely exceeds
* its own `below` marker is a warning, logged once: an unmaintained marker must
@@ -314,7 +317,7 @@ class ConfigLoader(
@Suppress("UNCHECKED_CAST")
private fun requirementOf(raw: Map<String, Any?>): VersionRequirement {
val version = (raw["gitTally"] as? Map<String, Any?>)?.get("version") as? Map<String, Any?> ?: return VersionRequirement()
val version = (raw["werkator"] as? Map<String, Any?>)?.get("version") as? Map<String, Any?> ?: return VersionRequirement()
return VersionRequirement(
since = version["since"]?.toString()?.trim().orEmpty(),
below = version["below"]?.toString()?.trim().orEmpty(),
@@ -329,7 +332,7 @@ class ConfigLoader(
return yaml.readValue(file, Map::class.java) as Map<String, Any?>
}
/** Parses a `.gittally.yml` read from git (not from disk); blank or null yields no layer. */
/** Parses a `.werkator.yml` read from git (not from disk); blank or null yields no layer. */
private fun parseYaml(text: String?): Map<String, Any?> {
if (text.isNullOrBlank()) return emptyMap()
@Suppress("UNCHECKED_CAST")
@@ -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 GitTally 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."
@@ -1,33 +1,33 @@
package de.hoennig.gittally.config
package de.hoennig.werkator.config
/**
* The GitTally version a configuration file declares itself for, the `gitTally.version`
* The Werkator version a configuration file declares itself for, the `werkator.version`
* section:
*
* ```yaml
* gitTally:
* werkator:
* version:
* since: "0.9.16" # always hard: an older GitTally refuses this file
* below: "2.0" # GitTally 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 GitTally 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 GitTally 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 GitTally 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 GitTallyMeta(
data class WerkatorMeta(
val version: VersionRequirement = VersionRequirement(),
)
/** What a [VersionRequirement] means for the GitTally 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 GitTally cannot bridge. */
/** Not usable: the file predates a change that Werkator cannot bridge. */
data class Incompatible(
val message: String,
) : VersionVerdict
}
/** A configuration file this GitTally 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 GitTally 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 GitTally 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(
@@ -64,7 +64,7 @@ class ConfigFormatException(
object ConfigVersions {
/**
* The version in which the configuration format last changed incompatibly a file
* written before it cannot be read by this GitTally. Empty while no such change has
* written before it cannot be read by this werkator. Empty while no such change has
* happened; set it to the release that introduces one, together with the migration
* note the message points at.
*/
@@ -76,13 +76,13 @@ object ConfigVersions {
/**
* Decides what [requirement] means for [running].
*
* `since` is always hard a file that needs a newer GitTally 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
* GitTally'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 GitTally ${requirement.since} or newer (gitTally.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 GitTally ${requirement.since} (gitTally.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 GitTally below ${requirement.below} (gitTally.version.below), this is $running",
"was released for Werkator below ${requirement.below} (werkator.version.below), this is $running",
)
}
return VersionVerdict.Compatible
@@ -1,4 +1,4 @@
package de.hoennig.gittally.config
package de.hoennig.werkator.config
import java.time.Duration
@@ -1,10 +1,10 @@
package de.hoennig.gittally.config
package de.hoennig.werkator.config
import com.fasterxml.jackson.annotation.JsonProperty
data class GitTallyConfig(
/** What this file declares about the GitTally that reads it; see [VersionRequirement]. */
val gitTally: GitTallyMeta = GitTallyMeta(),
data class WerkatorConfig(
/** 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 GitTally 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.
*/
@@ -76,11 +76,11 @@ data class NginxConfig(
val httpsPort: Int = 8443,
/** Host nginx proxies to; empty uses [serverName] (the container cannot reach `localhost`). */
val upstreamHost: String = "",
/** Name of the managed container; empty means `gittally-nginx-<repo-name>`. */
/** Name of the managed container; empty means `werkator-nginx-<repo-name>`. */
val containerName: String = "",
/**
* Directory for nginx config, certificates, and logs; empty means the platform
* default `XDG_STATE_HOME` (or `~/.local/state`) + `/gittally/nginx/<repo-key>`.
* default `XDG_STATE_HOME` (or `~/.local/state`) + `/werkator/nginx/<repo-key>`.
*/
val stateDir: String = "",
/** E-mail for the Let's Encrypt account; empty registers without one. */
@@ -96,7 +96,7 @@ data class GiteaConfig(
val baseUrl: String = "",
val owner: String = "",
val repo: String = "",
val statusContext: String = "GitTally",
val statusContext: String = "werkator",
)
data class ArtifactsConfig(
@@ -117,7 +117,7 @@ data class ArtifactsConfig(
val keepLatestGreen: Boolean = true,
/**
* Root directory for stored build artifacts; empty means the platform default
* `XDG_STATE_HOME` (or `~/.local/state`) + `/gittally/artifacts/<repo-key>`.
* `XDG_STATE_HOME` (or `~/.local/state`) + `/werkator/artifacts/<repo-key>`.
*/
val rootDir: String = "",
)
@@ -130,7 +130,7 @@ data class WatcherConfig(
* Honor the `branches.<name>.requirePullRequest` gates. Set false for a plain git
* origin without pull-request refs (no Gitea/GitHub) gated branches then build
* on new commits like any other branch. Typically overridden per machine in
* `.git/gittally/.gittally.yml` when the committed config enables the gates.
* `.git/werkator/.werkator.yml` when the committed config enables the gates.
*/
val pullRequestGate: Boolean = true,
/**
@@ -1,4 +1,4 @@
package de.hoennig.gittally.git
package de.hoennig.werkator.git
import java.nio.file.Files
import java.nio.file.attribute.PosixFilePermissions
@@ -15,10 +15,10 @@ object GitAskPass {
#!/bin/sh
case "${'$'}1" in
*[Uu]sername*)
printf '%s\n' "${'$'}GITTALLY_GIT_ACCOUNT"
printf '%s\n' "${'$'}WERKATOR_GIT_ACCOUNT"
;;
*)
printf '%s\n' "${'$'}GITTALLY_GIT_TOKEN"
printf '%s\n' "${'$'}WERKATOR_GIT_TOKEN"
;;
esac
""".trimIndent() + "\n"
@@ -30,7 +30,7 @@ object GitAskPass {
): T {
val script =
Files.createTempFile(
"gittally-askpass",
"werkator-askpass",
".sh",
PosixFilePermissions.asFileAttribute(PosixFilePermissions.fromString("rwx------")),
)
@@ -40,8 +40,8 @@ object GitAskPass {
mapOf(
"GIT_ASKPASS" to script.toAbsolutePath().toString(),
"GIT_TERMINAL_PROMPT" to "0",
"GITTALLY_GIT_ACCOUNT" to account,
"GITTALLY_GIT_TOKEN" to token,
"WERKATOR_GIT_ACCOUNT" to account,
"WERKATOR_GIT_TOKEN" to token,
),
)
} finally {
@@ -1,4 +1,4 @@
package de.hoennig.gittally.git
package de.hoennig.werkator.git
import org.springframework.stereotype.Component
import java.nio.file.Path
@@ -1,6 +1,6 @@
package de.hoennig.gittally.git
package de.hoennig.werkator.git
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.werkator.config.ConfigLoader
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.nio.file.Path
@@ -260,7 +260,7 @@ class GitService(
/**
* The content of [path] as committed in [commit], or null when that commit has no
* such file used to read a branch's committed `.gittally.yml` without a worktree.
* such file used to read a branch's committed `.werkator.yml` without a worktree.
*/
fun showFileAtCommit(
commit: String,
@@ -1,13 +1,13 @@
package de.hoennig.gittally.gitea
package de.hoennig.werkator.gitea
import com.fasterxml.jackson.core.JacksonException
import com.fasterxml.jackson.core.type.TypeReference
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
import com.fasterxml.jackson.module.kotlin.registerKotlinModule
import de.hoennig.gittally.build.BuildStatus
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.gittally.config.GitTallyConfig
import de.hoennig.werkator.build.BuildStatus
import de.hoennig.werkator.config.ConfigLoader
import de.hoennig.werkator.config.WerkatorConfig
import org.slf4j.LoggerFactory
import org.springframework.http.MediaType
import org.springframework.http.client.JdkClientHttpRequestFactory
@@ -170,7 +170,7 @@ class GiteaClient(
}
}
private fun isEnabled(config: GitTallyConfig): Boolean =
private fun isEnabled(config: WerkatorConfig): Boolean =
config.gitea.baseUrl.isNotBlank() &&
config.gitea.owner.isNotBlank() &&
config.gitea.repo.isNotBlank() &&
@@ -182,7 +182,7 @@ class GiteaClient(
HttpClient.newBuilder().connectTimeout(REQUEST_TIMEOUT).build(),
).apply { setReadTimeout(REQUEST_TIMEOUT) }
private fun restClient(config: GitTallyConfig): RestClient =
private fun restClient(config: WerkatorConfig): RestClient =
RestClient
.builder()
.requestFactory(requestFactory)
@@ -1,6 +1,6 @@
package de.hoennig.gittally.gitea
package de.hoennig.werkator.gitea
import de.hoennig.gittally.build.BuildStatus
import de.hoennig.werkator.build.BuildStatus
/**
* Gitea commit-status state published for this build status.
@@ -1,6 +1,6 @@
package de.hoennig.gittally.metrics
package de.hoennig.werkator.metrics
import de.hoennig.gittally.build.ArtifactStore
import de.hoennig.werkator.build.ArtifactStore
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import java.time.Clock
@@ -1,4 +1,4 @@
package de.hoennig.gittally.metrics
package de.hoennig.werkator.metrics
import java.time.Instant
@@ -1,4 +1,4 @@
package de.hoennig.gittally.metrics
package de.hoennig.werkator.metrics
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
@@ -30,7 +30,7 @@ data class PersistedMetricsState(
* repository size every 60 seconds and keeps running min/max/avg per metric.
* The aggregation state is persisted, so restarts continue the series.
* Every source degrades gracefully: an unreadable source makes its metric null
* (shown as `n/a`), never fails a sample. Like the [de.hoennig.gittally.watcher.Watcher],
* (shown as `n/a`), never fails a sample. Like the [de.hoennig.werkator.watcher.Watcher],
* nothing is scheduled until [start] is called (server mode only).
*/
class SystemMetricsCollector(
@@ -83,7 +83,7 @@ class SystemMetricsCollector(
scheduler =
Executors
.newSingleThreadScheduledExecutor { runnable ->
Thread(runnable, "gittally-metrics").apply { isDaemon = true }
Thread(runnable, "werkator-metrics").apply { isDaemon = true }
}.also {
it.scheduleWithFixedDelay(::sampleSafely, 0, SAMPLE_INTERVAL_SECONDS, TimeUnit.SECONDS)
}
@@ -1,7 +1,7 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.build.BuildResult
import de.hoennig.gittally.build.BuildStatus
import de.hoennig.werkator.build.BuildResult
import de.hoennig.werkator.build.BuildStatus
import java.time.Instant
/** JSON statuses are lowercase like the legacy TSV/HTML statuses. */
@@ -1,6 +1,6 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.build.ArtifactStore
import de.hoennig.werkator.build.ArtifactStore
import jakarta.servlet.http.HttpServletRequest
import org.springframework.core.io.FileSystemResource
import org.springframework.core.io.Resource
@@ -1,7 +1,7 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.build.BuildResultRepository
import de.hoennig.gittally.git.GitService
import de.hoennig.werkator.build.BuildResultRepository
import de.hoennig.werkator.git.GitService
import org.springframework.stereotype.Component
import java.nio.file.Path
import java.nio.file.Paths
@@ -1,8 +1,8 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.build.ArtifactKeys
import de.hoennig.gittally.build.BuildResult
import de.hoennig.gittally.build.BuildResultRepository
import de.hoennig.werkator.build.ArtifactKeys
import de.hoennig.werkator.build.BuildResult
import de.hoennig.werkator.build.BuildResultRepository
import org.springframework.http.HttpStatus
import org.springframework.stereotype.Component
import org.springframework.web.server.ResponseStatusException
@@ -1,12 +1,12 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.build.ArtifactStore
import de.hoennig.gittally.build.BuildExecutor
import de.hoennig.gittally.build.BuildResult
import de.hoennig.gittally.build.BuildResultRepository
import de.hoennig.gittally.build.BuildStatus
import de.hoennig.gittally.config.BuildDefinition
import de.hoennig.gittally.git.GitService
import de.hoennig.werkator.build.ArtifactStore
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.BuildDefinition
import de.hoennig.werkator.git.GitService
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.DeleteMapping
@@ -192,7 +192,7 @@ class BuildsApiController(
}
companion object {
const val TOKEN_HEADER = "X-GitTally-Token"
const val TOKEN_HEADER = "X-werkator-Token"
private const val MAX_LOG_CHUNK = 1024L * 1024L
}
}
@@ -1,6 +1,6 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.SecretFiles
import de.hoennig.werkator.SecretFiles
import java.nio.file.Files
import java.nio.file.Path
import java.security.MessageDigest
@@ -1,4 +1,4 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
/**
* Generates the nginx configuration for the managed proxy container, ported from
@@ -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 GitTally is added.
* with the Let's Encrypt certificate and the proxy to Werkator is added.
*/
fun nginxConf(
serverName: String,
@@ -1,9 +1,9 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.build.ArtifactKeys
import de.hoennig.gittally.build.DockerBuildRunner.Companion.GITTALLY_LABEL
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.gittally.git.GitCommandRunner
import de.hoennig.werkator.build.ArtifactKeys
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
import org.springframework.stereotype.Component
import java.nio.file.Files
@@ -11,10 +11,10 @@ import java.nio.file.Path
import java.nio.file.Paths
/**
* Manages the opt-in nginx+certbot Docker container that serves GitTally 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.gittally.build.DockerBuildRunner].
* generic [GitCommandRunner] process wrapper, like [de.hoennig.werkator.build.DockerBuildRunner].
*
* Startup is two-phase: an HTTP-only init config serves the ACME webroot
* challenge, the certificate is obtained via a certbot container, then nginx is
@@ -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
* GitTally 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=$GITTALLY_LABEL=true",
"label=$WERKATOR_LABEL=true",
"--filter",
"label=$GITTALLY_LABEL.repository=${settings.repoKey}",
"label=$WERKATOR_LABEL.repository=${settings.repoKey}",
"--filter",
"label=$GITTALLY_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("$GITTALLY_LABEL=true") ||
container.name.startsWith("gittally-") ||
if (container.labels.contains("$WERKATOR_LABEL=true") ||
container.name.startsWith("werkator-") ||
container.name.startsWith("git-watch-origin-and-test-nginx-")
) {
log.info("removing stale GitTally 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",
"$GITTALLY_LABEL=true",
"$WERKATOR_LABEL=true",
"--label",
"$GITTALLY_LABEL.repository=${settings.repoKey}",
"$WERKATOR_LABEL.repository=${settings.repoKey}",
"--label",
"$GITTALLY_LABEL.role=nginx",
"$WERKATOR_LABEL.role=nginx",
"nginx",
)
@@ -405,16 +405,16 @@ class NginxProxyManager(
System.getenv("XDG_STATE_HOME")?.takeIf { it.isNotBlank() }?.let { Paths.get(it) }
?: Paths.get(System.getProperty("user.home"), ".local", "state")
return stateHome
.resolve("gittally")
.resolve("werkator")
.resolve("nginx")
.resolve(ArtifactKeys.repoKey(repoDir))
.toAbsolutePath()
.normalize()
}
/** Legacy default `gittally-nginx-<repo-name>` with unsafe characters replaced. */
/** Legacy default `werkator-nginx-<repo-name>` with unsafe characters replaced. */
fun defaultContainerName(repoDir: Path): String =
"gittally-nginx-" + repoDir.fileName.toString().replace(Regex("[^A-Za-z0-9_.-]"), "-")
"werkator-nginx-" + repoDir.fileName.toString().replace(Regex("[^A-Za-z0-9_.-]"), "-")
/**
* Certbot's pinned DH parameters (RFC 7919 ffdhe2048), bundled as a resource:
@@ -1,4 +1,4 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@@ -12,5 +12,5 @@ class ServerConfiguration {
* until the first guarded request, so the bean is safe outside a git repository.
*/
@Bean
fun controlTokenService(): ControlTokenService = ControlTokenService(Paths.get(".git/gittally/control-token"))
fun controlTokenService(): ControlTokenService = ControlTokenService(Paths.get(".git/werkator/control-token"))
}
@@ -1,6 +1,6 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.metrics.SystemMetricsCollector
import de.hoennig.werkator.metrics.SystemMetricsCollector
import jakarta.annotation.PreDestroy
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.annotation.Profile
@@ -1,4 +1,4 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import jakarta.annotation.PreDestroy
import org.springframework.boot.context.event.ApplicationReadyEvent
@@ -26,7 +26,7 @@ class ServerNginxLifecycle(
/** Replaceable for tests: the scheduler running startup and renewal checks. */
internal var schedulerFactory: () -> ScheduledExecutorService = {
Executors.newSingleThreadScheduledExecutor { runnable ->
Thread(runnable, "gittally-nginx").apply { isDaemon = true }
Thread(runnable, "werkator-nginx").apply { isDaemon = true }
}
}
@@ -1,6 +1,6 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.watcher.Watcher
import de.hoennig.werkator.watcher.Watcher
import jakarta.annotation.PreDestroy
import org.springframework.boot.context.event.ApplicationReadyEvent
import org.springframework.context.annotation.Profile
@@ -1,8 +1,8 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.build.BuildResultRepository
import de.hoennig.gittally.gitea.GiteaClient
import de.hoennig.gittally.gitea.GiteaStatusResult
import de.hoennig.werkator.build.BuildResultRepository
import de.hoennig.werkator.gitea.GiteaClient
import de.hoennig.werkator.gitea.GiteaStatusResult
import org.springframework.http.HttpStatus
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
@@ -1,7 +1,7 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.metrics.SystemMetrics
import de.hoennig.gittally.metrics.SystemMetricsCollector
import de.hoennig.werkator.metrics.SystemMetrics
import de.hoennig.werkator.metrics.SystemMetricsCollector
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@@ -1,14 +1,14 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.build.ArtifactStore
import de.hoennig.gittally.build.BuildExecutor
import de.hoennig.gittally.build.BuildResult
import de.hoennig.gittally.build.BuildResultRepository
import de.hoennig.gittally.build.BuildStatus
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.gittally.git.GitService
import de.hoennig.gittally.metrics.SystemMetricsCollector
import de.hoennig.gittally.watcher.Watcher
import de.hoennig.werkator.build.ArtifactStore
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 jakarta.servlet.http.HttpServletRequest
import org.springframework.beans.factory.ObjectProvider
import org.springframework.boot.info.BuildProperties
@@ -28,7 +28,7 @@ import kotlin.streams.asSequence
/**
* Server-rendered Thymeleaf views over the JSON API. The pages render the full
* state server-side (usable without JavaScript); `gittally.js` then polls the
* state server-side (usable without JavaScript); `werkator.js` then polls the
* `/api/` endpoints and re-renders the table bodies pages are never re-fetched
* and diffed like legacy, so the UI cannot get stuck on a loading animation.
*/
@@ -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)
@@ -1,9 +1,9 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.build.BuildResult
import de.hoennig.gittally.config.GiteaConfig
import de.hoennig.gittally.metrics.MetricAggregate
import de.hoennig.gittally.metrics.SystemMetrics
import de.hoennig.werkator.build.BuildResult
import de.hoennig.werkator.config.GiteaConfig
import de.hoennig.werkator.metrics.MetricAggregate
import de.hoennig.werkator.metrics.SystemMetrics
import java.net.URLEncoder
import java.nio.charset.StandardCharsets
import java.time.Duration
@@ -32,7 +32,7 @@ class GiteaWebLinks(
.joinToString("/") { URLEncoder.encode(it, StandardCharsets.UTF_8).replace("+", "%20") }
}
/** Display formatting shared by the server-rendered views; `gittally.js` renders the same formats. */
/** Display formatting shared by the server-rendered views; `werkator.js` renders the same formats. */
object UiFormats {
private val timestampFormat = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm").withZone(ZoneId.systemDefault())
@@ -69,7 +69,7 @@ object UiFormats {
/**
* CSS class highlighting a critical utilization: `metric-warn` from 80% of [total],
* `metric-crit` from 90%, empty below or when either value is unavailable.
* `gittally.js` (`utilizationClass`) must apply the same thresholds.
* `werkator.js` (`utilizationClass`) must apply the same thresholds.
*/
fun utilizationClass(
used: Double?,
@@ -176,7 +176,7 @@ data class LogFileView(
val failed: Boolean,
)
/** One card of the current-builds view; the live log is fetched by `gittally.js`. */
/** One card of the current-builds view; the live log is fetched by `werkator.js`. */
data class CurrentBuildView(
val branch: String,
/** The displayed build name; = [branch] unless a named auto-build slot triggered the build. */
@@ -194,7 +194,7 @@ data class CurrentBuildView(
/**
* One row of the system-metrics table. The [key] matches the JSON field of
* `GET /api/system`, so `gittally.js` can update the cells in place.
* `GET /api/system`, so `werkator.js` can update the cells in place.
*/
data class MetricRowView(
val key: String,
@@ -1,7 +1,7 @@
package de.hoennig.gittally.server
package de.hoennig.werkator.server
import de.hoennig.gittally.watcher.Watcher
import de.hoennig.gittally.watcher.WatcherState
import de.hoennig.werkator.watcher.Watcher
import de.hoennig.werkator.watcher.WatcherState
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
@@ -1,4 +1,4 @@
package de.hoennig.gittally.watcher
package de.hoennig.werkator.watcher
import com.fasterxml.jackson.databind.DeserializationFeature
import com.fasterxml.jackson.databind.ObjectMapper
@@ -74,7 +74,7 @@ object AutoBuildSlots {
/**
* Persists which auto-build slots already triggered as a JSON file,
* e.g. `.git/gittally/auto-builds.json` (replaces the legacy `auto-builds.tsv`).
* e.g. `.git/werkator/auto-builds.json` (replaces the legacy `auto-builds.tsv`).
* Entries of past days are dropped on write, so the file never grows unbounded.
*/
class FileAutoBuildState(
@@ -1,16 +1,17 @@
package de.hoennig.gittally.watcher
package de.hoennig.werkator.watcher
import de.hoennig.gittally.build.ArtifactKeys
import de.hoennig.gittally.build.ArtifactStore
import de.hoennig.gittally.build.BuildExecutor
import de.hoennig.gittally.build.BuildResultRepository
import de.hoennig.gittally.build.BuildStatus
import de.hoennig.gittally.build.GitWorktreeWorkspaces
import de.hoennig.gittally.config.BuildDefinition
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.gittally.config.DurationParser
import de.hoennig.gittally.config.GitTallyConfig
import de.hoennig.gittally.git.GitService
import de.hoennig.werkator.build.ArtifactKeys
import de.hoennig.werkator.build.ArtifactStore
import de.hoennig.werkator.build.BuildExecutor
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
import de.hoennig.werkator.git.GitService
import org.slf4j.LoggerFactory
import org.springframework.stereotype.Service
import java.nio.file.Files
@@ -80,7 +81,7 @@ class Watcher(
scheduler =
Executors
.newSingleThreadScheduledExecutor { runnable ->
Thread(runnable, "gittally-watcher").apply { isDaemon = true }
Thread(runnable, "werkator-watcher").apply { isDaemon = true }
}.also {
it.scheduleWithFixedDelay({ pollSafely(workingDir) }, 0, interval.toMillis(), TimeUnit.MILLISECONDS)
}
@@ -207,7 +208,7 @@ class Watcher(
}
private fun enqueueDueBranches(
config: GitTallyConfig,
config: WerkatorConfig,
originBranches: Set<String>,
workingDir: Path,
) {
@@ -238,7 +239,7 @@ class Watcher(
/**
* The build definitions that apply to [branch]: the primary configuration with the
* branch's own committed `.gittally.yml` merged on top (the pinned keys stripped),
* branch's own committed `.werkator.yml` merged on top (the pinned keys stripped),
* so a new `builds` configuration can be tried out on a branch without touching any
* other branch's builds. A branch's definitions only ever apply to that branch
* their selectors are evaluated for it alone, so a definition committed on one branch
@@ -254,15 +255,17 @@ class Watcher(
branch: String,
headCommit: String?,
workingDir: Path,
primary: GitTallyConfig,
primary: WerkatorConfig,
): Map<String, BuildDefinition> {
val commit = headCommit ?: return primary.effectiveBuildDefinitions()
branchDefinitions[branch]?.takeIf { it.commit == commit && it.primary == primary }?.let { return it.definitions }
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 {}: {}",
@@ -279,7 +282,7 @@ class Watcher(
private class CachedDefinitions(
val commit: String,
val primary: GitTallyConfig,
val primary: WerkatorConfig,
val definitions: Map<String, BuildDefinition>,
)
@@ -304,7 +307,7 @@ class Watcher(
private fun startBuildIfDue(
branch: String,
allowSameCommit: Boolean,
config: GitTallyConfig,
config: WerkatorConfig,
pullRequestHeads: Lazy<Set<String>>,
workingDir: Path,
build: String = BuildDefinition.DEFAULT,
@@ -335,7 +338,7 @@ class Watcher(
* the point of a scheduled build.
*/
private fun enqueueScheduledBuilds(
config: GitTallyConfig,
config: WerkatorConfig,
originBranches: Set<String>,
heads: Map<String, String>,
pullRequestHeads: Lazy<Set<String>>,
@@ -373,7 +376,7 @@ class Watcher(
* `builds` entry with `atTimes` and a single-branch selector would do.
*/
private fun enqueueDeprecatedAutoBuilds(
config: GitTallyConfig,
config: WerkatorConfig,
originBranches: Set<String>,
pullRequestHeads: Lazy<Set<String>>,
workingDir: Path,
@@ -413,7 +416,7 @@ class Watcher(
/** Results first, then artifacts of dropped results, then worktrees of branches gone from origin. */
private fun prune(
config: GitTallyConfig,
config: WerkatorConfig,
originBranches: List<String>,
workingDir: Path,
) {
@@ -463,9 +466,9 @@ class Watcher(
companion object {
/** Auto-build trigger state next to the build results (replaces legacy `auto-builds.tsv`). */
const val AUTO_BUILDS_FILE = ".git/gittally/auto-builds.json"
const val AUTO_BUILDS_FILE = ".git/werkator/auto-builds.json"
/** The committed config read per branch for its build definitions. */
const val CONFIG_FILE = ".gittally.yml"
const val CONFIG_FILE = ConfigFiles.COMMITTED
}
}
@@ -1,4 +1,4 @@
package de.hoennig.gittally.watcher
package de.hoennig.werkator.watcher
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
@@ -1,4 +1,4 @@
package de.hoennig.gittally.watcher
package de.hoennig.werkator.watcher
import java.time.Instant