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
+1 -1
View File
@@ -7,4 +7,4 @@ spring:
logging:
level:
de.hoennig.gittally: INFO
de.hoennig.werkator: INFO
+1 -1
View File
@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-label="GitTally">
<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 @@
/* GitTally 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 @@
// GitTally 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";
@@ -97,13 +97,23 @@ function metaContent(name) {
return element ? element.content : "";
}
const giteaRepoUrl = metaContent("gittally-gitea-repo-url");
const giteaRepoUrl = metaContent("werkator-gitea-repo-url");
// The control token is deliberately NOT embedded in the pages: reading them is
// unauthenticated, so anyone could have read it out of the HTML. The operator
// pastes it once per browser from `.git/gittally/control-token` on the server;
// pastes it once per browser from `.git/werkator/control-token` on the server;
// it is kept in localStorage and only ever sent as a request header.
const CONTROL_TOKEN_KEY = "gittally.controlToken";
const CONTROL_TOKEN_KEY = "werkator.controlToken";
// The key was named after the old product name, so the rename left every browser
// with a token under a name nothing reads any more — a secret that not even
// "forget token" can reach. Dropped on load; the token itself is unchanged on the
// server, so the one re-entry per browser is all the rename costs.
try {
window.localStorage.removeItem("gittally.controlToken");
} catch (error) {
// localStorage unavailable (private mode, blocked cookies) — nothing stored, nothing to drop
}
function storedControlToken() {
try {
@@ -131,7 +141,7 @@ function forgetControlToken() {
function askForControlToken() {
const answer = window.prompt(
"Control token — the content of .git/gittally/control-token on the GitTally host:",
"Control token — the content of .git/werkator/control-token on the Werkator host:",
"",
);
return answer ? answer.trim() : "";
@@ -169,7 +179,7 @@ async function sendAction(url, method) {
function sendWithToken(url, method, token) {
return fetch(url, {
method,
headers: { "X-GitTally-Token": token },
headers: { "X-werkator-Token": token },
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
});
}
+1 -1
View File
@@ -76,6 +76,6 @@
</article>
</main>
<footer th:replace="~{fragments :: footer}"></footer>
<script src="/gittally.js"></script>
<script src="/werkator.js"></script>
</body>
</html>
+1 -1
View File
@@ -78,6 +78,6 @@
</div>
</main>
<footer th:replace="~{fragments :: footer}"></footer>
<script src="/gittally.js"></script>
<script src="/werkator.js"></script>
</body>
</html>
+1 -1
View File
@@ -35,6 +35,6 @@
</div>
</main>
<footer th:replace="~{fragments :: footer}"></footer>
<script src="/gittally.js"></script>
<script src="/werkator.js"></script>
</body>
</html>
+5 -5
View File
@@ -3,10 +3,10 @@
<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} + ' — GitTally' : ${title} + ' — ' + ${repoName} + ' — GitTally'">GitTally</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="/gittally.css">
<meta name="gittally-gitea-repo-url" th:content="${giteaRepoUrl}">
<link rel="stylesheet" href="/werkator.css">
<meta name="werkator-gitea-repo-url" th:content="${giteaRepoUrl}">
</head>
<body>
@@ -34,13 +34,13 @@
<button id="reload-button" class="reload-button" type="button" title="Reload view" aria-label="Reload view"></button>
</span>
</div>
<!-- Filled by gittally.js from /api/watcher: while the watcher cannot reach origin, every
<!-- Filled by werkator.js from /api/watcher: while the watcher cannot reach origin, every
row below is as old as its last successful poll, and a calm list would imply otherwise. -->
<div id="watcher-banner" class="watcher-banner" role="status" hidden></div>
</th:block>
<footer th:fragment="footer" class="site-footer">
<strong><a href="/releases" title="Release notes"><em th:text="'GitTally v' + ${version}">GitTally</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>
+21 -1
View File
@@ -7,6 +7,26 @@
<div th:replace="~{fragments :: nav(${view})}"></div>
<div class="panel release-notes">
<h2>v1.0.0 <span class="muted">— 2026-08-31</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>. The version marks the new name rather than a claim about
maturity: the release after <code>0.9.21</code> is <code>1.0.0</code>, because a
product that changes its name is better off counting from one under it.</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>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 +314,6 @@
</div>
</main>
<footer th:replace="~{fragments :: footer}"></footer>
<script src="/gittally.js"></script>
<script src="/werkator.js"></script>
</body>
</html>
+1 -1
View File
@@ -39,6 +39,6 @@
</p>
</main>
<footer th:replace="~{fragments :: footer}"></footer>
<script src="/gittally.js"></script>
<script src="/werkator.js"></script>
</body>
</html>
@@ -1,8 +1,8 @@
package de.hoennig.gittally
package de.hoennig.werkator
import de.hoennig.gittally.commands.ConfigPrintCommand
import de.hoennig.gittally.commands.InitCommand
import de.hoennig.gittally.commands.ServerCommand
import de.hoennig.werkator.commands.ConfigPrintCommand
import de.hoennig.werkator.commands.InitCommand
import de.hoennig.werkator.commands.ServerCommand
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldNotBe
import org.springframework.beans.factory.annotation.Autowired
@@ -11,7 +11,7 @@ import org.springframework.boot.test.context.SpringBootTest
@SpringBootTest
class ApplicationContextTest : FunSpec() {
@Autowired
lateinit var rootCommand: GitTallyCommand
lateinit var rootCommand: WerkatorCommand
@Autowired
lateinit var initCommand: InitCommand
@@ -1,8 +1,8 @@
package de.hoennig.gittally
package de.hoennig.werkator
import com.ninjasquad.springmockk.MockkBean
import de.hoennig.gittally.metrics.SystemMetricsCollector
import de.hoennig.gittally.watcher.Watcher
import de.hoennig.werkator.metrics.SystemMetricsCollector
import de.hoennig.werkator.watcher.Watcher
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.booleans.shouldBeTrue
import io.mockk.verify
@@ -0,0 +1,81 @@
package de.hoennig.werkator
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.booleans.shouldBeFalse
import io.kotest.matchers.booleans.shouldBeTrue
import io.kotest.matchers.shouldBe
import java.nio.file.Files
import java.nio.file.Path
class StateDirMigrationTest : FunSpec() {
private fun repoWithLegacyState(): Path {
val dir = Files.createTempDirectory("werkator-state")
Files.createDirectories(dir.resolve(".git/gittally"))
dir.resolve(".git/gittally/build-results.json").toFile().writeText("[]")
return dir
}
init {
test("moves the pre-rename state directory to its current path") {
val dir = repoWithLegacyState()
StateDirMigration.migrateIfNeeded(dir)
Files.exists(dir.resolve(".git/gittally")).shouldBeFalse()
dir.resolve(".git/werkator/build-results.json").toFile().readText() shouldBe "[]"
}
test("renames the machine configuration inside the moved directory") {
val dir = repoWithLegacyState()
dir.resolve(".git/gittally/.gittally.yml").toFile().writeText("git:\n account: ci-user\n")
StateDirMigration.migrateIfNeeded(dir)
Files.exists(dir.resolve(".git/werkator/.gittally.yml")).shouldBeFalse()
dir.resolve(".git/werkator/.werkator.yml").toFile().readText() shouldBe "git:\n account: ci-user\n"
}
test("never overwrites a machine configuration that already carries the current name") {
val dir = repoWithLegacyState()
dir.resolve(".git/gittally/.gittally.yml").toFile().writeText("git:\n account: old\n")
dir.resolve(".git/gittally/.werkator.yml").toFile().writeText("git:\n account: current\n")
StateDirMigration.migrateIfNeeded(dir)
dir.resolve(".git/werkator/.werkator.yml").toFile().readText() shouldBe "git:\n account: current\n"
Files.exists(dir.resolve(".git/werkator/.gittally.yml")).shouldBeTrue()
}
test("drops the moved worktrees, because they point at their old path") {
val dir = repoWithLegacyState()
Files.createDirectories(dir.resolve(".git/gittally/worktrees/main"))
StateDirMigration.migrateIfNeeded(dir)
Files.exists(dir.resolve(".git/werkator/worktrees")).shouldBeFalse()
// the rest of the state survives the drop
Files.exists(dir.resolve(".git/werkator/build-results.json")).shouldBeTrue()
}
test("leaves both alone when the current directory already exists") {
val dir = repoWithLegacyState()
Files.createDirectories(dir.resolve(".git/werkator"))
dir.resolve(".git/werkator/build-results.json").toFile().writeText("[\"live\"]")
StateDirMigration.migrateIfNeeded(dir)
// which of the two is the live state is not guessed
dir.resolve(".git/werkator/build-results.json").toFile().readText() shouldBe "[\"live\"]"
Files.exists(dir.resolve(".git/gittally")).shouldBeTrue()
}
test("does nothing where there is no pre-rename directory") {
val dir = Files.createTempDirectory("werkator-state")
Files.createDirectories(dir.resolve(".git"))
StateDirMigration.migrateIfNeeded(dir)
Files.exists(dir.resolve(".git/werkator")).shouldBeFalse()
}
}
}
@@ -1,11 +1,11 @@
package de.hoennig.gittally.artifacts
package de.hoennig.werkator.artifacts
import de.hoennig.gittally.build.BranchWorkspaces
import de.hoennig.gittally.build.BuildExecutor
import de.hoennig.gittally.build.FileBuildResultRepository
import de.hoennig.gittally.build.ProcessBuildRunner
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.gittally.gitea.GiteaClient
import de.hoennig.werkator.build.BranchWorkspaces
import de.hoennig.werkator.build.BuildExecutor
import de.hoennig.werkator.build.FileBuildResultRepository
import de.hoennig.werkator.build.ProcessBuildRunner
import de.hoennig.werkator.config.ConfigLoader
import de.hoennig.werkator.gitea.GiteaClient
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.nulls.shouldNotBeNull
@@ -20,10 +20,10 @@ import kotlin.time.Duration.Companion.seconds
class BuildExecutorArtifactIntegrationTest : FunSpec() {
init {
test("the executor persists a successful build's logs and reports through the real store") {
val workingDir = Files.createTempDirectory("gittally-artifact-integration-test")
val workingDir = Files.createTempDirectory("werkator-artifact-integration-test")
val root = workingDir.resolve("artifact-root")
Files.writeString(
workingDir.resolve(".gittally.yml"),
workingDir.resolve(".werkator.yml"),
"""
artifacts:
rootDir: "$root"
@@ -1,9 +1,9 @@
package de.hoennig.gittally.artifacts
package de.hoennig.werkator.artifacts
import de.hoennig.gittally.build.ArtifactKeys
import de.hoennig.gittally.build.BuildResult
import de.hoennig.gittally.build.BuildStatus
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.werkator.build.ArtifactKeys
import de.hoennig.werkator.build.BuildResult
import de.hoennig.werkator.build.BuildStatus
import de.hoennig.werkator.config.ConfigLoader
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContainExactly
@@ -22,13 +22,13 @@ class FileArtifactStoreTest : FunSpec() {
private val startedAt = Instant.parse("2026-07-07T10:00:00Z")
private class Harness {
val workingDir: Path = Files.createTempDirectory("gittally-store-test")
val workingDir: Path = Files.createTempDirectory("werkator-store-test")
val root: Path = workingDir.resolve("artifact-root")
val store = FileArtifactStore(ConfigLoader(), workingDir)
init {
Files.writeString(
workingDir.resolve(".gittally.yml"),
workingDir.resolve(".werkator.yml"),
"""
artifacts:
rootDir: "$root"
@@ -57,7 +57,7 @@ class FileArtifactStoreTest : FunSpec() {
)
private fun stagingDir(): Path {
val dir = Files.createTempDirectory("gittally-staging-test")
val dir = Files.createTempDirectory("werkator-staging-test")
Files.writeString(dir.resolve("build.stdout.log"), "out")
Files.writeString(dir.resolve("build.stderr.log"), "err")
Files.writeString(dir.resolve("build.log"), "live")
@@ -133,8 +133,8 @@ class FileArtifactStoreTest : FunSpec() {
}
test("the artifact root defaults to XDG_STATE_HOME plus the repo key") {
val workingDir = Files.createTempDirectory("gittally-store-test")
val stateHome = Files.createTempDirectory("gittally-state-home-test")
val workingDir = Files.createTempDirectory("werkator-store-test")
val stateHome = Files.createTempDirectory("werkator-state-home-test")
val store =
FileArtifactStore(ConfigLoader(), workingDir) { name ->
if (name == "XDG_STATE_HOME") stateHome.toString() else null
@@ -145,7 +145,7 @@ class FileArtifactStoreTest : FunSpec() {
val expectedDir =
stateHome
.resolve("gittally/artifacts")
.resolve("werkator/artifacts")
.resolve(ArtifactKeys.repoKey(workingDir))
.resolve("branches")
.resolve(build.artifactKey)
@@ -183,7 +183,7 @@ class FileArtifactStoreTest : FunSpec() {
test("prune deletes a symlink without touching its target outside the root") {
val h = Harness()
val outside = Files.createTempDirectory("gittally-outside-test")
val outside = Files.createTempDirectory("werkator-outside-test")
Files.writeString(outside.resolve("keep-me.txt"), "precious")
Files.createDirectories(h.branchesDir())
val link = h.branchesDir().resolve("evil-link")
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
@@ -1,7 +1,7 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.gittally.gitea.GiteaClient
import de.hoennig.werkator.config.ConfigLoader
import de.hoennig.werkator.gitea.GiteaClient
import io.kotest.assertions.nondeterministic.eventually
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.booleans.shouldBeFalse
@@ -33,7 +33,7 @@ class BuildExecutorTest : FunSpec() {
workspaceSubdir: String? = null,
val buildRunner: BuildRunner = ProcessBuildRunner(),
) {
val workingDir: Path = Files.createTempDirectory("gittally-executor-test")
val workingDir: Path = Files.createTempDirectory("werkator-executor-test")
val repository = FileBuildResultRepository(workingDir.resolve("build-results.json"))
val giteaClient = mockk<GiteaClient>(relaxed = true)
val artifactStore = mockk<ArtifactStore>(relaxed = true)
@@ -65,7 +65,7 @@ class BuildExecutorTest : FunSpec() {
)
init {
Files.writeString(workingDir.resolve(".gittally.yml"), configYaml)
Files.writeString(workingDir.resolve(".werkator.yml"), configYaml)
}
}
@@ -195,7 +195,7 @@ class BuildExecutorTest : FunSpec() {
workingDir: java.nio.file.Path,
environment: Map<String, String>,
repoDir: java.nio.file.Path,
branchConfig: de.hoennig.gittally.config.BranchConfig,
branchConfig: de.hoennig.werkator.config.BranchConfig,
onAuxProcess: (Process) -> Unit,
): Process {
val aux = ProcessBuilder("bash", "-c", "sleep 60").directory(workingDir.toFile()).start()
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
@@ -1,7 +1,7 @@
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.werkator.config.BranchConfig
import de.hoennig.werkator.config.DockerConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.mockk.Called
@@ -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.DockerConfig
import de.hoennig.gittally.git.GitCommandResult
import de.hoennig.gittally.git.GitCommandRunner
import de.hoennig.werkator.config.BranchConfig
import de.hoennig.werkator.config.DockerConfig
import de.hoennig.werkator.git.GitCommandResult
import de.hoennig.werkator.git.GitCommandRunner
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.collections.shouldContain
@@ -47,7 +47,7 @@ class DockerBuildRunnerTest : FunSpec() {
beforeEach {
clearMocks(commandRunner, socketLocator)
captured.clear()
repoDir = Files.createTempDirectory("gittally-docker-runner")
repoDir = Files.createTempDirectory("werkator-docker-runner")
workspace = repoDir.resolve("workspace")
every { commandRunner.run(any(), any(), any(), any()) } returns GitCommandResult(0, "", "")
every { commandRunner.runOrThrow(any(), any(), any(), any()) } returns GitCommandResult(0, "", "")
@@ -77,19 +77,19 @@ class DockerBuildRunnerTest : FunSpec() {
"--rm",
"--init",
"--name",
"gittally-build-$repoKey-${ArtifactKeys.branchKey("main")}",
"werkator-build-$repoKey-${ArtifactKeys.branchKey("main")}",
"--label",
"org.hoennig.gittally=true",
"org.hoennig.werkator=true",
"--label",
"org.hoennig.gittally.repository=$repoKey",
"org.hoennig.werkator.repository=$repoKey",
"--label",
"org.hoennig.gittally.role=build",
"org.hoennig.werkator.role=build",
"--workdir",
"$workspace",
"--volume",
"$workspace:$workspace",
"--volume",
"gittally-gradle-$repoKey:/gradle-user-home",
"werkator-gradle-$repoKey:/gradle-user-home",
"--env",
"HOME=/tmp/docker-home",
"--env",
@@ -150,11 +150,11 @@ class DockerBuildRunnerTest : FunSpec() {
}
}
test("exposes git metadata read-only with the gittally dir masked for a worktree workspace") {
test("exposes git metadata read-only with the werkator dir masked for a worktree workspace") {
val gitDir = repoDir.resolve(".git")
val adminDir = gitDir.resolve("worktrees/workspace")
Files.createDirectories(adminDir)
Files.createDirectories(gitDir.resolve("gittally"))
Files.createDirectories(gitDir.resolve("werkator"))
Files.createDirectories(workspace)
Files.writeString(workspace.resolve(".git"), "gitdir: $adminDir\n")
@@ -162,7 +162,7 @@ class DockerBuildRunnerTest : FunSpec() {
val args = captured.single()
args shouldContain "$gitDir:$gitDir:ro"
args[args.indexOf("--tmpfs") + 1] shouldBe "${gitDir.resolve("gittally")}"
args[args.indexOf("--tmpfs") + 1] shouldBe "${gitDir.resolve("werkator")}"
args shouldContain "$adminDir:$adminDir"
}
@@ -209,13 +209,13 @@ class DockerBuildRunnerTest : FunSpec() {
"docker",
"build",
"--label",
"org.gittally.dockerfile=Dockerfile",
"org.werkator.dockerfile=Dockerfile",
"--label",
"org.gittally.dockerfile-sha256=$dockerfileHash",
"org.werkator.dockerfile-sha256=$dockerfileHash",
"--label",
"org.gittally.build-context=.",
"org.werkator.build-context=.",
"--label",
"org.gittally.build-inputs-sha256=$inputsHash",
"org.werkator.build-inputs-sha256=$inputsHash",
"-t",
"build-env:latest",
"-f",
@@ -251,11 +251,11 @@ class DockerBuildRunnerTest : FunSpec() {
val repoKey = ArtifactKeys.repoKey(repoDir)
verify(exactly = 1) {
commandRunner.runOrThrow(listOf("docker", "volume", "create", "gittally-gradle-$repoKey"), repoDir, any(), any())
commandRunner.runOrThrow(listOf("docker", "volume", "create", "werkator-gradle-$repoKey"), repoDir, any(), any())
}
verify(exactly = 2) {
commandRunner.run(
listOf("docker", "rm", "-f", "gittally-build-$repoKey-${ArtifactKeys.branchKey("main")}"),
listOf("docker", "rm", "-f", "werkator-build-$repoKey-${ArtifactKeys.branchKey("main")}"),
repoDir,
any(),
any(),
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
@@ -9,7 +9,7 @@ import java.nio.file.Files
class DockerImageInputsTest : FunSpec() {
init {
test("dockerfileSha256 is a stable hex checksum of the file contents") {
val dir = Files.createTempDirectory("gittally-docker-inputs")
val dir = Files.createTempDirectory("werkator-docker-inputs")
val dockerfile = dir.resolve("Dockerfile")
Files.writeString(dockerfile, "FROM eclipse-temurin:21\n")
@@ -20,7 +20,7 @@ class DockerImageInputsTest : FunSpec() {
}
test("inputs checksum changes when the Dockerfile contents change") {
val dir = Files.createTempDirectory("gittally-docker-inputs")
val dir = Files.createTempDirectory("werkator-docker-inputs")
val dockerfile = dir.resolve("Dockerfile")
Files.writeString(dockerfile, "FROM eclipse-temurin:21\n")
val before =
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.booleans.shouldBeFalse
@@ -16,7 +16,7 @@ import java.time.Instant
class FileBuildResultRepositoryTest : FunSpec() {
private val baseTime = Instant.parse("2026-07-07T10:00:00Z")
private fun newFile(): Path = Files.createTempDirectory("gittally-results-test").resolve("build-results.json")
private fun newFile(): Path = Files.createTempDirectory("werkator-results-test").resolve("build-results.json")
private fun result(
branch: String = "main",
@@ -1,8 +1,8 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.gittally.git.GitCommandRunner
import de.hoennig.gittally.git.GitService
import de.hoennig.werkator.config.ConfigLoader
import de.hoennig.werkator.git.GitCommandRunner
import de.hoennig.werkator.git.GitService
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.nulls.shouldBeNull
import io.kotest.matchers.shouldBe
@@ -20,16 +20,16 @@ class GitWorktreeWorkspacesTest : FunSpec() {
// hermetic git: fixed identity, no user/system config (hooks, gpg signing, ...)
private val gitEnvironment =
mapOf(
"GIT_AUTHOR_NAME" to "GitTally Test",
"GIT_AUTHOR_NAME" to "Werkator Test",
"GIT_AUTHOR_EMAIL" to "test@example.com",
"GIT_COMMITTER_NAME" to "GitTally 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",
)
private inner class Fixture {
val repo: Path = Files.createTempDirectory("gittally-workspaces-test").resolve("repo")
val repo: Path = Files.createTempDirectory("werkator-workspaces-test").resolve("repo")
init {
Files.createDirectories(repo)
@@ -1,4 +1,4 @@
package de.hoennig.gittally.build
package de.hoennig.werkator.build
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
@@ -9,7 +9,7 @@ import java.nio.file.Path
class ProcessBuildRunnerTest : FunSpec() {
private val runner = ProcessBuildRunner()
private fun tempDir(): Path = Files.createTempDirectory("gittally-runner-test")
private fun tempDir(): Path = Files.createTempDirectory("werkator-runner-test")
init {
test("propagates the exit code") {
@@ -1,4 +1,4 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
@@ -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 io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
@@ -1,4 +1,4 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import java.io.ByteArrayOutputStream
import java.io.PrintStream
@@ -1,8 +1,8 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import de.hoennig.gittally.config.ConfigLoader
import de.hoennig.gittally.config.GitConfig
import de.hoennig.gittally.config.GitTallyConfig
import de.hoennig.werkator.config.ConfigLoader
import de.hoennig.werkator.config.GitConfig
import de.hoennig.werkator.config.WerkatorConfig
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.string.shouldContain
import io.kotest.matchers.string.shouldNotContain
@@ -26,7 +26,7 @@ class ConfigPrintCommandTest : FunSpec() {
clearMocks(configLoader)
every { configLoader.toYaml(any()) } answers { yamlWriter.toYaml(firstArg()) }
every { configLoader.loadRaw() } returns rawConfig
every { configLoader.load() } returns GitTallyConfig(git = GitConfig(account = "ci-user", token = "s3cr3t-token"))
every { configLoader.load() } returns WerkatorConfig(git = GitConfig(account = "ci-user", token = "s3cr3t-token"))
command.full = false
command.showSecrets = false
}
@@ -1,11 +1,11 @@
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.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 io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
@@ -55,7 +55,7 @@ class ConsoleBuildRunnerTest : FunSpec() {
init {
beforeEach {
clearMocks(buildExecutor, repository, artifactStore)
tempDir = Files.createTempDirectory("gittally-console-build-test")
tempDir = Files.createTempDirectory("werkator-console-build-test")
}
afterEach {
@@ -1,7 +1,8 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import de.hoennig.gittally.git.GitService
import de.hoennig.werkator.git.GitService
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.booleans.shouldBeFalse
import io.kotest.matchers.file.shouldExist
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
@@ -17,7 +18,7 @@ class InitCommandTest : FunSpec() {
init {
test("creates config files with auto-detected values") {
val tempDir = Files.createTempDirectory("gittally-init-test")
val tempDir = Files.createTempDirectory("werkator-init-test")
initCommand.workingDir = tempDir
every { gitService.getTopLevel(tempDir) } returns tempDir
@@ -25,21 +26,40 @@ class InitCommandTest : FunSpec() {
initCommand.run()
val projectConfig = tempDir.resolve(".gittally.yml")
val projectConfig = tempDir.resolve(".werkator.yml")
projectConfig.toFile().shouldExist()
val projectContent = projectConfig.toFile().readText()
projectContent shouldContain "baseUrl: https://git.example.org"
projectContent shouldContain "owner: my-org"
projectContent shouldContain "repo: my-repo"
val repoConfig = tempDir.resolve(".git/gittally/.gittally.yml")
val repoConfig = tempDir.resolve(".git/werkator/.werkator.yml")
repoConfig.toFile().shouldExist()
val repoContent = repoConfig.toFile().readText()
repoContent shouldContain "account: \"\"" // no user in https URL
}
test("does not write a second config beside one under the previous name") {
val tempDir = Files.createTempDirectory("werkator-init-test")
initCommand.workingDir = tempDir
Files.createDirectories(tempDir.resolve(".git/gittally"))
tempDir.resolve(".gittally.yml").toFile().writeText("gitea:\n owner: kept\n")
tempDir.resolve(".git/gittally/.gittally.yml").toFile().writeText("git:\n account: kept\n")
every { gitService.getTopLevel(tempDir) } returns tempDir
every { gitService.getOriginUrl(tempDir) } returns "https://git.example.org/my-org/my-repo.git"
initCommand.run()
// a second file would shadow the first one, and the repository would
// silently build something else than what its configuration says
Files.exists(tempDir.resolve(".werkator.yml")).shouldBeFalse()
Files.exists(tempDir.resolve(".git/werkator/.werkator.yml")).shouldBeFalse()
tempDir.resolve(".gittally.yml").toFile().readText() shouldContain "owner: kept"
}
test("creates the secrets config and its directory readable only by the owner") {
val tempDir = Files.createTempDirectory("gittally-init-test")
val tempDir = Files.createTempDirectory("werkator-init-test")
initCommand.workingDir = tempDir
every { gitService.getTopLevel(tempDir) } returns tempDir
@@ -47,13 +67,13 @@ class InitCommandTest : FunSpec() {
initCommand.run()
val repoConfig = tempDir.resolve(".git/gittally/.gittally.yml")
val repoConfig = tempDir.resolve(".git/werkator/.werkator.yml")
PosixFilePermissions.toString(Files.getPosixFilePermissions(repoConfig)) shouldBe "rw-------"
PosixFilePermissions.toString(Files.getPosixFilePermissions(repoConfig.parent)) shouldBe "rwx------"
}
test("detects account from https url") {
val tempDir = Files.createTempDirectory("gittally-init-test")
val tempDir = Files.createTempDirectory("werkator-init-test")
initCommand.workingDir = tempDir
every { gitService.getTopLevel(tempDir) } returns tempDir
@@ -61,13 +81,13 @@ class InitCommandTest : FunSpec() {
initCommand.run()
val repoConfig = tempDir.resolve(".git/gittally/.gittally.yml")
val repoConfig = tempDir.resolve(".git/werkator/.werkator.yml")
val repoContent = repoConfig.toFile().readText()
repoContent shouldContain "account: \"ci-user\""
}
test("parses ssh url") {
val tempDir = Files.createTempDirectory("gittally-init-test")
val tempDir = Files.createTempDirectory("werkator-init-test")
initCommand.workingDir = tempDir
every { gitService.getTopLevel(tempDir) } returns tempDir
@@ -75,7 +95,7 @@ class InitCommandTest : FunSpec() {
initCommand.run()
val projectConfig = tempDir.resolve(".gittally.yml")
val projectConfig = tempDir.resolve(".werkator.yml")
val projectContent = projectConfig.toFile().readText()
projectContent shouldContain "baseUrl: https://git.example.org" // fallback to https
projectContent shouldContain "owner: my-org"
@@ -83,10 +103,10 @@ class InitCommandTest : FunSpec() {
}
test("does not overwrite existing files") {
val tempDir = Files.createTempDirectory("gittally-init-test")
val tempDir = Files.createTempDirectory("werkator-init-test")
initCommand.workingDir = tempDir
val projectConfig = tempDir.resolve(".gittally.yml")
val projectConfig = tempDir.resolve(".werkator.yml")
projectConfig.toFile().writeText("existing: content")
every { gitService.getTopLevel(tempDir) } returns tempDir
@@ -98,10 +118,10 @@ class InitCommandTest : FunSpec() {
}
test("--systemd generates unit and environment file with install instructions") {
val tempDir = Files.createTempDirectory("gittally-init-test")
val tempDir = Files.createTempDirectory("werkator-init-test")
initCommand.workingDir = tempDir
initCommand.systemd = true
initCommand.jarPathResolver = { Paths.get("/home/ci/bin/gittally.jar") }
initCommand.jarPathResolver = { Paths.get("/home/ci/bin/werkator.jar") }
initCommand.javaExecutableResolver = { Paths.get("/usr/bin/java") }
every { gitService.getTopLevel(tempDir) } returns tempDir
@@ -110,20 +130,20 @@ class InitCommandTest : FunSpec() {
initCommand.run()
val unitName = SystemdServiceFiles.unitName(tempDir)
val unitFile = tempDir.resolve(".git/gittally/$unitName")
val unitFile = tempDir.resolve(".git/werkator/$unitName")
unitFile.toFile().shouldExist()
val unitContent = unitFile.toFile().readText()
unitContent shouldContain "WorkingDirectory=$tempDir"
unitContent shouldContain """ExecStart="/usr/bin/java" ${'$'}JAVA_OPTS -jar "/home/ci/bin/gittally.jar" server"""
unitContent shouldContain """ExecStart="/usr/bin/java" ${'$'}JAVA_OPTS -jar "/home/ci/bin/werkator.jar" server"""
tempDir.resolve(".git/gittally/gittally.env").toFile().shouldExist()
tempDir.resolve(".git/werkator/werkator.env").toFile().shouldExist()
}
test("--systemd also generates the nightly Docker cleanup timer") {
val tempDir = Files.createTempDirectory("gittally-init-test")
val tempDir = Files.createTempDirectory("werkator-init-test")
initCommand.workingDir = tempDir
initCommand.systemd = true
initCommand.jarPathResolver = { Paths.get("/home/ci/bin/gittally.jar") }
initCommand.jarPathResolver = { Paths.get("/home/ci/bin/werkator.jar") }
initCommand.javaExecutableResolver = { Paths.get("/usr/bin/java") }
every { gitService.getTopLevel(tempDir) } returns tempDir
@@ -131,22 +151,22 @@ class InitCommandTest : FunSpec() {
initCommand.run()
val pruneService = tempDir.resolve(".git/gittally/gittally-docker-prune.service")
val pruneService = tempDir.resolve(".git/werkator/werkator-docker-prune.service")
pruneService.toFile().shouldExist()
pruneService.toFile().readText() shouldContain "docker system prune -af"
val pruneTimer = tempDir.resolve(".git/gittally/gittally-docker-prune.timer")
val pruneTimer = tempDir.resolve(".git/werkator/werkator-docker-prune.timer")
pruneTimer.toFile().shouldExist()
pruneTimer.toFile().readText() shouldContain "OnCalendar=*-*-* 02:00:00"
}
test("--systemd keeps an existing environment file") {
val tempDir = Files.createTempDirectory("gittally-init-test")
val tempDir = Files.createTempDirectory("werkator-init-test")
initCommand.workingDir = tempDir
initCommand.systemd = true
initCommand.jarPathResolver = { Paths.get("/home/ci/bin/gittally.jar") }
initCommand.jarPathResolver = { Paths.get("/home/ci/bin/werkator.jar") }
initCommand.javaExecutableResolver = { Paths.get("/usr/bin/java") }
val envFile = tempDir.resolve(".git/gittally/gittally.env")
val envFile = tempDir.resolve(".git/werkator/werkator.env")
Files.createDirectories(envFile.parent)
envFile.toFile().writeText("JAVA_OPTS=-Xmx1g\n")
@@ -159,7 +179,7 @@ class InitCommandTest : FunSpec() {
}
test("--systemd without a resolvable jar path generates no unit file") {
val tempDir = Files.createTempDirectory("gittally-init-test")
val tempDir = Files.createTempDirectory("werkator-init-test")
initCommand.workingDir = tempDir
initCommand.systemd = true
initCommand.jarPathResolver = { null }
@@ -169,12 +189,12 @@ class InitCommandTest : FunSpec() {
initCommand.run()
val unitFile = tempDir.resolve(".git/gittally/${SystemdServiceFiles.unitName(tempDir)}")
val unitFile = tempDir.resolve(".git/werkator/${SystemdServiceFiles.unitName(tempDir)}")
unitFile.toFile().exists() shouldBe false
}
test("reproduces path root mismatch issue") {
val tempDir = Files.createTempDirectory("gittally-init-test").toAbsolutePath().normalize()
val tempDir = Files.createTempDirectory("werkator-init-test").toAbsolutePath().normalize()
initCommand.workingDir = Paths.get(".") // Set to relative path as in real app
// We need to mock getTopLevel to return the absolute path
@@ -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 io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
@@ -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.build.BuildStatus
import de.hoennig.werkator.build.BuildResult
import de.hoennig.werkator.build.BuildResultRepository
import de.hoennig.werkator.build.BuildStatus
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
import io.kotest.matchers.string.shouldContain
@@ -1,4 +1,4 @@
package de.hoennig.gittally.commands
package de.hoennig.werkator.commands
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.shouldBe
@@ -9,8 +9,8 @@ import java.nio.file.Paths
class SystemdServiceFilesTest : FunSpec() {
init {
test("unit name is derived from the sanitized repository directory name") {
SystemdServiceFiles.unitName(Paths.get("/srv/repos/my-repo")) shouldBe "gittally-my-repo.service"
SystemdServiceFiles.unitName(Paths.get("/srv/repos/my repo!")) shouldBe "gittally-my-repo-.service"
SystemdServiceFiles.unitName(Paths.get("/srv/repos/my-repo")) shouldBe "werkator-my-repo.service"
SystemdServiceFiles.unitName(Paths.get("/srv/repos/my repo!")) shouldBe "werkator-my-repo-.service"
}
test("unit file runs the server jar in the repository with restart and environment file") {
@@ -18,16 +18,16 @@ class SystemdServiceFilesTest : FunSpec() {
SystemdServiceFiles.unitFileContent(
repoRoot = Paths.get("/srv/repos/my-repo"),
javaExecutable = Paths.get("/usr/lib/jvm/java-21/bin/java"),
jarPath = Paths.get("/home/ci/bin/gittally.jar"),
envFile = Paths.get("/srv/repos/my-repo/.git/gittally/gittally.env"),
jarPath = Paths.get("/home/ci/bin/werkator.jar"),
envFile = Paths.get("/srv/repos/my-repo/.git/werkator/werkator.env"),
)
content shouldContain "Description=GitTally 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/gittally/gittally.env"
content shouldContain "EnvironmentFile=-/srv/repos/my-repo/.git/werkator/werkator.env"
content shouldContain
"""ExecStart="/usr/lib/jvm/java-21/bin/java" ${'$'}JAVA_OPTS -jar "/home/ci/bin/gittally.jar" server"""
"""ExecStart="/usr/lib/jvm/java-21/bin/java" ${'$'}JAVA_OPTS -jar "/home/ci/bin/werkator.jar" server"""
content shouldContain "Restart=always"
content shouldContain "RestartSec=30"
content shouldContain "WantedBy=default.target"
@@ -38,13 +38,13 @@ class SystemdServiceFilesTest : FunSpec() {
SystemdServiceFiles.unitFileContent(
repoRoot = Paths.get("/srv/100%-repo"),
javaExecutable = Paths.get("/usr/bin/java"),
jarPath = Paths.get("/srv/100%-repo/gittally.jar"),
envFile = Paths.get("/srv/100%-repo/gittally.env"),
jarPath = Paths.get("/srv/100%-repo/werkator.jar"),
envFile = Paths.get("/srv/100%-repo/werkator.env"),
)
content shouldContain "WorkingDirectory=/srv/100%%-repo"
content shouldContain "EnvironmentFile=-/srv/100%%-repo/gittally.env"
content shouldContain """-jar "/srv/100%%-repo/gittally.jar" server"""
content shouldContain "EnvironmentFile=-/srv/100%%-repo/werkator.env"
content shouldContain """-jar "/srv/100%%-repo/werkator.jar" server"""
}
test("prune service cleans containers and images but never volumes") {
@@ -69,7 +69,7 @@ class SystemdServiceFilesTest : FunSpec() {
val content = SystemdServiceFiles.envFileContent()
content shouldContain "#JAVA_OPTS="
content shouldContain ".gittally.yml"
content shouldContain ".werkator.yml"
}
}
}
@@ -1,4 +1,4 @@
package de.hoennig.gittally.config
package de.hoennig.werkator.config
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec
@@ -18,7 +18,7 @@ import java.util.Properties
class ConfigLoaderTest : FunSpec() {
private val loader = ConfigLoader()
/** A loader that knows which GitTally it is, for the `gitTally.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) })
@@ -27,18 +27,18 @@ class ConfigLoaderTest : FunSpec() {
init {
test("returns defaults when no config files exist") {
val dir = Files.createTempDirectory("gittally-test")
loader.load(dir) shouldBe GitTallyConfig()
val dir = Files.createTempDirectory("werkator-test")
loader.load(dir) shouldBe WerkatorConfig()
}
test("loadRaw returns empty map when no config files exist") {
val dir = Files.createTempDirectory("gittally-test")
val dir = Files.createTempDirectory("werkator-test")
loader.loadRaw(dir).shouldBeEmpty()
}
test("reads gitea config from .gittally.yml") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
test("reads gitea config from .werkator.yml") {
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
gitea:
owner: my-org
@@ -50,11 +50,71 @@ 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("finds the machine config left under its old name in an already-moved directory") {
val dir = Files.createTempDirectory("werkator-test")
Files.createDirectories(dir.resolve(".git/werkator"))
dir.resolve(".git/werkator/.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("gittally-test")
val dir = Files.createTempDirectory("werkator-test")
loader.load(dir).executor.maxConcurrent shouldBe 1
dir.resolve(".gittally.yml").toFile().writeText(
dir.resolve(".werkator.yml").toFile().writeText(
"""
executor:
maxConcurrent: 3
@@ -64,8 +124,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("the builds section holds named build definitions") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
executor:
maxConcurrent: 2
@@ -100,8 +160,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("an explicit builds.default entry overrides the implicit default build") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
builds:
default:
@@ -113,11 +173,11 @@ class ConfigLoaderTest : FunSpec() {
loader.load(dir).effectiveBuildDefinitions()["default"] shouldBe BuildDefinition(trigger = TriggerConfig(onPush = false))
}
test("a config that needs a newer GitTally is refused, naming the file and both versions") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
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(
"""
gitTally:
werkator:
version:
since: "0.9.16"
""".trimIndent(),
@@ -126,7 +186,7 @@ class ConfigLoaderTest : FunSpec() {
val error = shouldThrow<ConfigVersionException> { loaderRunning("0.9.15").load(dir) }
error.message.shouldNotBeNull().let {
it shouldContain ".gittally.yml"
it shouldContain ".werkator.yml"
it shouldContain "0.9.16"
it shouldContain "0.9.15"
it shouldContain "roll back"
@@ -134,10 +194,10 @@ class ConfigLoaderTest : FunSpec() {
}
test("a config within its declared range loads, and exceeding only the ceiling still loads") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
gitTally:
werkator:
version:
since: "0.9.16"
below: "1.0"
@@ -149,20 +209,20 @@ class ConfigLoaderTest : FunSpec() {
loaderRunning("0.9.16").load(dir).gitea.owner shouldBe "my-org"
// beyond `below`: a warning, never a refusal — an unmaintained marker must not stop a CI
loaderRunning("1.4.0").load(dir).gitea.owner shouldBe "my-org"
loaderRunning("0.9.16").load(dir).gitTally.version shouldBe
loaderRunning("0.9.16").load(dir).werkator.version shouldBe
VersionRequirement(since = "0.9.16", below = "1.0")
}
test("an incompatible branch config is refused as the branch's problem, not the server's") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText("gitea:\n owner: my-org")
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText("gitea:\n owner: my-org")
val error =
shouldThrow<ConfigVersionException> {
loaderRunning("0.9.15").loadWithBranchLayer(
dir,
"""
gitTally:
werkator:
version:
since: "2.0.0"
""".trimIndent(),
@@ -178,11 +238,11 @@ class ConfigLoaderTest : FunSpec() {
}
test("the machine config is checked as its own file") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".git/gittally").toFile().mkdirs()
dir.resolve(".git/gittally/.gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".git/werkator").toFile().mkdirs()
dir.resolve(".git/werkator/.werkator.yml").toFile().writeText(
"""
gitTally:
werkator:
version:
since: "1.0.0"
""".trimIndent(),
@@ -190,12 +250,12 @@ class ConfigLoaderTest : FunSpec() {
shouldThrow<ConfigVersionException> {
loaderRunning("0.9.16").load(dir)
}.message.shouldNotBeNull() shouldContain ".git/gittally/.gittally.yml"
}.message.shouldNotBeNull() shouldContain ".git/werkator/.werkator.yml"
}
test("a leftover builds.maxConcurrent is ignored instead of failing the config") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
builds:
maxConcurrent: 1
@@ -211,8 +271,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("a branch may redefine the builds section for its own builds") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
builds:
pitest:
@@ -221,8 +281,8 @@ class ConfigLoaderTest : FunSpec() {
buildCommand: ./gradlew piTestPartial
""".trimIndent(),
)
val worktree = Files.createTempDirectory("gittally-test-worktree")
worktree.resolve(".gittally.yml").toFile().writeText(
val worktree = Files.createTempDirectory("werkator-test-worktree")
worktree.resolve(".werkator.yml").toFile().writeText(
"""
builds:
pitest:
@@ -246,21 +306,21 @@ class ConfigLoaderTest : FunSpec() {
}
test("a branch cannot raise the concurrency limit or reach the sandbox policy through a build definition") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
builds:
default:
requirePullRequest: true
statusContext: GitTally
statusContext: werkator
docker:
enabled: true
network: none
image: host-image
""".trimIndent(),
)
val worktree = Files.createTempDirectory("gittally-test-worktree")
worktree.resolve(".gittally.yml").toFile().writeText(
val worktree = Files.createTempDirectory("werkator-test-worktree")
worktree.resolve(".werkator.yml").toFile().writeText(
"""
executor:
maxConcurrent: 99
@@ -269,7 +329,7 @@ class ConfigLoaderTest : FunSpec() {
builds:
default:
requirePullRequest: false
statusContext: GitTally/impersonated
statusContext: werkator/impersonated
docker:
enabled: false
network: host
@@ -285,14 +345,14 @@ class ConfigLoaderTest : FunSpec() {
settings.requirePullRequest shouldBe true
settings.docker.enabled shouldBe true
settings.docker.network shouldBe "none"
settings.statusContext shouldBe "GitTally"
settings.statusContext shouldBe "werkator"
// everything that describes the build itself stays the branch's own business
settings.docker.image shouldBe "attacker-image"
}
test("a build the branch invents inherits the host's sandbox policy") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
builds:
default:
@@ -302,8 +362,8 @@ class ConfigLoaderTest : FunSpec() {
network: none
""".trimIndent(),
)
val worktree = Files.createTempDirectory("gittally-test-worktree")
worktree.resolve(".gittally.yml").toFile().writeText(
val worktree = Files.createTempDirectory("werkator-test-worktree")
worktree.resolve(".werkator.yml").toFile().writeText(
"""
builds:
invented:
@@ -327,8 +387,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("an exclusion pattern takes a branch out of a build that would otherwise select it") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
builds:
default:
@@ -374,8 +434,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("a trigger key written outside the trigger block is refused, naming the definition") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
builds:
nightly:
@@ -388,14 +448,14 @@ class ConfigLoaderTest : FunSpec() {
// stops running is worse than a configuration that refuses to load
val thrown = shouldThrow<ConfigFormatException> { loader.load(dir) }
thrown.message.shouldContain(".gittally.yml")
thrown.message.shouldContain(".werkator.yml")
thrown.message.shouldContain("builds.nightly: atTimes")
thrown.message.shouldContain("trigger:")
}
test("a branch writing its trigger flat fails only its own builds") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
builds:
default:
@@ -424,8 +484,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("builds.default is the base of every other build, but never its trigger") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
builds:
default:
@@ -455,7 +515,7 @@ class ConfigLoaderTest : FunSpec() {
}
test("branches is honored while no build is defined and ignored as soon as one is") {
val dir = Files.createTempDirectory("gittally-test")
val dir = Files.createTempDirectory("werkator-test")
val legacy =
"""
branches:
@@ -464,14 +524,14 @@ class ConfigLoaderTest : FunSpec() {
docker:
enabled: true
""".trimIndent()
dir.resolve(".gittally.yml").toFile().writeText(legacy)
dir.resolve(".werkator.yml").toFile().writeText(legacy)
// the leftover execution key is not a definition, so the legacy section still wins
loader.load(dir).buildSettings("main", "default").buildCommand shouldBe "from-branches"
dir.resolve(".gittally.yml").toFile().writeText("builds:\n maxConcurrent: 1\n" + legacy)
dir.resolve(".werkator.yml").toFile().writeText("builds:\n maxConcurrent: 1\n" + legacy)
loader.load(dir).buildSettings("main", "default").buildCommand shouldBe "from-branches"
dir.resolve(".gittally.yml").toFile().writeText(
dir.resolve(".werkator.yml").toFile().writeText(
legacy +
"\n" +
"""
@@ -487,8 +547,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("repo install config overrides project config for same keys") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
gitea:
owner: original-org
@@ -496,8 +556,8 @@ class ConfigLoaderTest : FunSpec() {
token: from-project
""".trimIndent(),
)
dir.resolve(".git/gittally").toFile().mkdirs()
dir.resolve(".git/gittally/.gittally.yml").toFile().writeText(
dir.resolve(".git/werkator").toFile().mkdirs()
dir.resolve(".git/werkator/.werkator.yml").toFile().writeText(
"""
gitea:
owner: override-org
@@ -511,8 +571,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("loadRaw only returns explicitly set values") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
branches:
default:
@@ -528,8 +588,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("branch-specific config inherits from branches.default") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
branches:
default:
@@ -545,8 +605,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("branch-specific buildCommand overrides branches.default") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
branches:
default:
@@ -560,8 +620,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("empty publicBaseUrl defaults to https://<nginx.serverName>/ when set") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
server:
nginx:
@@ -572,8 +632,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("explicit publicBaseUrl wins over the nginx.serverName default") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
server:
publicBaseUrl: https://other.example.org/
@@ -585,29 +645,29 @@ class ConfigLoaderTest : FunSpec() {
}
test("publicBaseUrl stays empty without an nginx.serverName") {
val dir = Files.createTempDirectory("gittally-test")
val dir = Files.createTempDirectory("werkator-test")
loader.load(dir).server.publicBaseUrl shouldBe ""
}
test("loadForWorktree lets the worktree override build config (worktree > .git > project)") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
branches:
default:
buildCommand: from-project
""".trimIndent(),
)
dir.resolve(".git/gittally").toFile().mkdirs()
dir.resolve(".git/gittally/.gittally.yml").toFile().writeText(
dir.resolve(".git/werkator").toFile().mkdirs()
dir.resolve(".git/werkator/.werkator.yml").toFile().writeText(
"""
branches:
default:
buildCommand: from-git
""".trimIndent(),
)
val worktree = Files.createTempDirectory("gittally-worktree")
worktree.resolve(".gittally.yml").toFile().writeText(
val worktree = Files.createTempDirectory("werkator-worktree")
worktree.resolve(".werkator.yml").toFile().writeText(
"""
branches:
default:
@@ -618,30 +678,30 @@ class ConfigLoaderTest : FunSpec() {
}
test("loadForWorktree falls back to .git over project when the worktree sets nothing") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
branches:
default:
buildCommand: from-project
""".trimIndent(),
)
dir.resolve(".git/gittally").toFile().mkdirs()
dir.resolve(".git/gittally/.gittally.yml").toFile().writeText(
dir.resolve(".git/werkator").toFile().mkdirs()
dir.resolve(".git/werkator/.werkator.yml").toFile().writeText(
"""
branches:
default:
buildCommand: from-git
""".trimIndent(),
)
val worktree = Files.createTempDirectory("gittally-worktree")
val worktree = Files.createTempDirectory("werkator-worktree")
loader.loadForWorktree(dir, worktree).branches["default"]!!.buildCommand shouldBe "from-git"
}
test("loadForWorktree pins secrets and the docker sandbox policy to .git, but allows docker.image") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".git/gittally").toFile().mkdirs()
dir.resolve(".git/gittally/.gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".git/werkator").toFile().mkdirs()
dir.resolve(".git/werkator/.werkator.yml").toFile().writeText(
"""
git:
token: real-secret
@@ -655,8 +715,8 @@ class ConfigLoaderTest : FunSpec() {
image: trusted-image
""".trimIndent(),
)
val worktree = Files.createTempDirectory("gittally-worktree")
worktree.resolve(".gittally.yml").toFile().writeText(
val worktree = Files.createTempDirectory("werkator-worktree")
worktree.resolve(".werkator.yml").toFile().writeText(
"""
git:
token: stolen
@@ -681,9 +741,9 @@ class ConfigLoaderTest : FunSpec() {
}
test("loadWithBranchLayer applies a branch config read from git, pinning the same keys") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".git/gittally").toFile().mkdirs()
dir.resolve(".git/gittally/.gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".git/werkator").toFile().mkdirs()
dir.resolve(".git/werkator/.werkator.yml").toFile().writeText(
"""
git:
token: real-secret
@@ -717,8 +777,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("loadWithBranchLayer without a branch config equals load") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
branches:
default:
@@ -731,8 +791,8 @@ class ConfigLoaderTest : FunSpec() {
}
test("loadForWorktree without a worktree config equals load") {
val dir = Files.createTempDirectory("gittally-test")
dir.resolve(".gittally.yml").toFile().writeText(
val dir = Files.createTempDirectory("werkator-test")
dir.resolve(".werkator.yml").toFile().writeText(
"""
gitea:
owner: my-org
@@ -741,12 +801,12 @@ class ConfigLoaderTest : FunSpec() {
buildCommand: ./mvnw test
""".trimIndent(),
)
val worktree = Files.createTempDirectory("gittally-worktree")
val worktree = Files.createTempDirectory("werkator-worktree")
loader.loadForWorktree(dir, worktree) shouldBe loader.load(dir)
}
test("toYaml serializes GitTallyConfig with all sections") {
val yaml = loader.toYaml(GitTallyConfig())
test("toYaml serializes WerkatorConfig with all sections") {
val yaml = loader.toYaml(WerkatorConfig())
yaml shouldContain "server:"
yaml shouldContain "git:"
yaml shouldContain "gitea:"
@@ -1,4 +1,4 @@
package de.hoennig.gittally.config
package de.hoennig.werkator.config
import io.kotest.core.spec.style.FunSpec
import io.kotest.matchers.nulls.shouldBeNull
@@ -14,7 +14,7 @@ class ConfigVersionsTest : FunSpec() {
) = ConfigVersions.verdict(VersionRequirement(since = since, below = below), running)
init {
test("a file needing a newer GitTally 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: GitTally 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 GitTally 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,4 +1,4 @@
package de.hoennig.gittally.config
package de.hoennig.werkator.config
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.FunSpec

Some files were not shown because too many files have changed in this diff Show More