Files
werkator/build.gradle.kts
T
mhoennig f0f996a53c A build definition says when it runs in a trigger block of its own
`onPush`, `atTimes`, `branches`, and `activeWithin` move into a nested
`trigger`. The split is structural on purpose: the inheritance from
`builds.default` now subtracts one key instead of a list of four, so a
selector added to `TriggerConfig` later is non-inheritable by
construction rather than because someone remembered to extend the list.

A definition still writing those keys flat is refused by name, per file
and scoped like the version check — the machine and project config abort
the start, a branch's committed config fails only that branch. Ignoring
them would leave the build with no trigger at all, which is a job that
quietly stops running: the failure this refusal exists to prevent.

Two more things a definition can now say:

- A `!` prefix in `trigger.branches` excludes, and an exclusion wins
  whatever the order. `["*", "!master"]` gives one branch a build of its
  own without the default build running over it as well — until now the
  only way out of that double build was to drop the second definition's
  push trigger.
- `statusContext` overrides the Gitea check this build reports as, empty
  keeping the repository-wide one. Two builds of a commit shared a
  context and overwrote each other's result, so a quick check beside a
  long build was not readable in Gitea. Pinned like `requirePullRequest`:
  a branch that could pick its context could take over the check a branch
  protection rule depends on.

Fixed on the way: a branch whose builds all belong to named definitions
rendered an empty row in the branches view, reading as "never built"
directly beside its real builds. That row was unreachable before the
exclusion patterns made such a branch possible.
2026-08-29 12:05:10 +02:00

175 lines
6.4 KiB
Kotlin

import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
id("org.springframework.boot") version "4.0.6"
id("io.spring.dependency-management") version "1.1.7"
id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
kotlin("jvm") version "2.2.0"
kotlin("plugin.spring") version "2.2.0"
}
group = "de.hoennig"
// bump at least the patch version for every deployment — and only then, not per commit —
// so the UI footer (BuildProperties), --version and the release notes identify what is
// actually running; a deployment bundles whatever was committed since the last one
version = "0.9.20"
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation("org.springframework.boot:spring-boot-starter")
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-thymeleaf")
implementation("org.springframework:spring-web")
implementation("org.jetbrains.kotlin:kotlin-reflect")
implementation("info.picocli:picocli-spring-boot-starter:4.7.6")
implementation("com.fasterxml.jackson.dataformat:jackson-dataformat-yaml")
implementation("com.fasterxml.jackson.module:jackson-module-kotlin")
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310")
testImplementation("org.springframework.boot:spring-boot-starter-test")
// @WebMvcTest lives in its own module since Spring Boot 4
testImplementation("org.springframework.boot:spring-boot-starter-webmvc-test")
// Kotest
testImplementation("io.kotest:kotest-runner-junit5:6.1.5")
testImplementation("io.kotest:kotest-assertions-core:6.1.5")
testImplementation("io.kotest:kotest-extensions-spring:6.1.5")
// MockK
testImplementation("io.mockk:mockk:1.13.11")
testImplementation("com.ninja-squad:springmockk:5.0.1")
// WireMock
testImplementation("org.wiremock.integrations:wiremock-spring-boot:4.2.1")
// Testcontainers (versions managed by Spring Boot BOM)
testImplementation("org.springframework.boot:spring-boot-testcontainers")
testImplementation("org.testcontainers:testcontainers")
}
springBoot {
// exposes the project version to the UI footer via the BuildProperties bean;
// the volatile build time is excluded to keep builds repeatable
buildInfo {
properties {
excludes.set(setOf("time"))
}
}
}
tasks.bootJar {
// version-free jar name, so docs and scripts never contain the version;
// the version itself stays available via BuildProperties (UI footer, --version)
archiveFileName = "gittally.jar"
}
kotlin {
compilerOptions {
freeCompilerArgs.addAll("-Xjsr305=strict")
jvmTarget = JvmTarget.JVM_21
}
}
// Self-contained runtime bundle for hosts without a Java runtime (plan step 15, ADR 0006):
// a jlink-trimmed JRE plus gittally.jar plus the packaging/gittally launcher, packed as a tarball.
// The JDK module list below was computed from the exploded boot jar via
// jdeps -q --ignore-missing-deps --multi-release 21 --print-module-deps \
// --class-path 'BOOT-INF/lib/*' BOOT-INF/classes BOOT-INF/lib/*.jar
// plus runtime-only modules jdeps cannot detect: java.logging (Tomcat JULI),
// jdk.crypto.ec (TLS ECDHE), jdk.management (extended OS MXBeans), jdk.zipfs (nested jar access).
// Re-check the jdeps output when dependencies change.
val runtimeBundleModules =
listOf(
"java.base",
"java.compiler",
"java.desktop",
"java.instrument",
"java.logging",
"java.management",
"java.naming",
"java.net.http",
"java.prefs",
"java.scripting",
"java.security.jgss",
"java.sql",
"jdk.crypto.ec",
"jdk.jfr",
"jdk.management",
"jdk.unsupported",
"jdk.zipfs",
).joinToString(",")
val runtimeBundle by tasks.registering {
group = "distribution"
description = "Builds the self-contained runtime bundle (jlink JRE + jar + launcher) as a tar.gz"
dependsOn(tasks.bootJar)
val jdkHome = javaToolchains.launcherFor(java.toolchain).map { it.metadata.installationPath.asFile }
val jarFile = tasks.bootJar.flatMap { it.archiveFile }
val launcherFile = layout.projectDirectory.file("packaging/gittally").asFile
val stagingDir =
layout.buildDirectory
.dir("runtime-bundle")
.get()
.asFile
val tarballFile =
layout.buildDirectory
.file("distributions/gittally-runtime-linux-x64.tar.gz")
.get()
.asFile
inputs.files(tasks.bootJar.map { it.outputs.files })
inputs.file(launcherFile)
inputs.property("modules", runtimeBundleModules)
outputs.file(tarballFile)
doLast {
val bundleRoot = stagingDir.resolve("gittally")
bundleRoot.deleteRecursively()
bundleRoot.parentFile.mkdirs()
val jlink = jdkHome.get().resolve("bin/jlink")
val jlinkProcess =
ProcessBuilder(
jlink.absolutePath,
"--add-modules",
runtimeBundleModules,
"--strip-debug",
"--no-header-files",
"--no-man-pages",
"--compress",
"zip-6",
"--output",
bundleRoot.resolve("jre").absolutePath,
).redirectErrorStream(true).start()
val jlinkOutput = jlinkProcess.inputStream.bufferedReader().readText()
check(jlinkProcess.waitFor() == 0) { "jlink failed:\n$jlinkOutput" }
jarFile.get().asFile.copyTo(bundleRoot.resolve("lib/gittally.jar").also { it.parentFile.mkdirs() })
val launcher = launcherFile.copyTo(bundleRoot.resolve("bin/gittally").also { it.parentFile.mkdirs() })
check(launcher.setExecutable(true, false)) { "cannot make $launcher executable" }
tarballFile.parentFile.mkdirs()
// system tar preserves the execute bits of jre/bin/* and jre/lib/jspawnhelper
val tarProcess =
ProcessBuilder("tar", "-czf", tarballFile.absolutePath, "-C", stagingDir.absolutePath, "gittally")
.redirectErrorStream(true)
.start()
val tarOutput = tarProcess.inputStream.bufferedReader().readText()
check(tarProcess.waitFor() == 0) { "tar failed:\n$tarOutput" }
}
}
tasks.withType<Test> {
useJUnitPlatform()
}