Add self-contained runtime bundle distribution (jlink) for hosts without Java
./gradlew runtimeBundle packs a jlink-trimmed JRE, gittally.jar, and a launcher script into one tarball, unpacked to ~/opt/gittally on the target host; init --systemd works from the bundle unchanged because java.home and the running-jar path resolve into it. Chosen over a GraalVM native image (Spring AOT evaluates bean conditions at build time, which cannot represent the dual-context CLI/server wiring) and over a containerized runtime — see ADR 0006 and docs/plan/15-runtime-bundle-distribution.md, which also records the full vm2176-to-vm4006 migration walkthrough. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5
parent
8b71d0db8e
commit
9e7982ce34
@@ -74,6 +74,7 @@ All major decisions are in `docs/adrs/`. Run `adr-status` (after `source .envrc`
|
|||||||
- **Spring Boot**: 4.0.6 (ADR 0003)
|
- **Spring Boot**: 4.0.6 (ADR 0003)
|
||||||
- **Rewrite architecture**: JSON-file persistence behind a repository interface, server-rendered UI with JSON polling, no managed nginx — systemd unit behind the host's reverse proxy (ADR 0004)
|
- **Rewrite architecture**: JSON-file persistence behind a repository interface, server-rendered UI with JSON polling, no managed nginx — systemd unit behind the host's reverse proxy (ADR 0004)
|
||||||
- **Managed nginx/TLS**: revises ADR 0004 — an opt-in nginx+certbot container for hosts without a reverse proxy (e.g. Hostsharing), planned as `docs/plan/13-nginx-tls.md` (ADR 0005)
|
- **Managed nginx/TLS**: revises ADR 0004 — an opt-in nginx+certbot container for hosts without a reverse proxy (e.g. Hostsharing), planned as `docs/plan/13-nginx-tls.md` (ADR 0005)
|
||||||
|
- **Runtime bundle distribution**: `./gradlew runtimeBundle` builds a jlink-trimmed JRE + jar tarball for hosts without a Java runtime; GraalVM native image and a containerized runtime were rejected (ADR 0006)
|
||||||
|
|
||||||
## Skills
|
## Skills
|
||||||
|
|
||||||
|
|||||||
@@ -76,6 +76,96 @@ kotlin {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 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> {
|
tasks.withType<Test> {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
# Self-Contained Runtime Bundle Distribution
|
||||||
|
|
||||||
|
**Status:**
|
||||||
|
- proposed: 2026-08-10
|
||||||
|
- accepted: 2026-08-10
|
||||||
|
- rejected: -
|
||||||
|
- superseded: -
|
||||||
|
|
||||||
|
**Decision [accepted]:** GitTally is distributed for hosts without a Java runtime as a self-contained runtime bundle — a jlink-trimmed JRE plus `gittally.jar` plus a launcher script in one tarball, built by `./gradlew runtimeBundle`.
|
||||||
|
The JAR stays the primary artifact; a GraalVM native image and a containerized GitTally runtime were rejected.
|
||||||
|
|
||||||
|
## Context and Problem Statement
|
||||||
|
|
||||||
|
GitTally must run on Hostsharing container servers (the primary target, see ADR 0005).
|
||||||
|
These hosts provide git, Docker, make, and systemd user sessions, but no Java runtime, and nothing may be installed system-wide.
|
||||||
|
`docs/bootstrapping.md` sketched a containerized GitTally runtime as the future answer; that sketch was never validated against the operational details.
|
||||||
|
|
||||||
|
## Considered Options
|
||||||
|
|
||||||
|
* jlink runtime bundle (trimmed JRE + jar + launcher, one tarball)
|
||||||
|
* GraalVM native image (single executable)
|
||||||
|
* Containerized GitTally runtime (the original `docs/bootstrapping.md` sketch)
|
||||||
|
|
||||||
|
### jlink Runtime Bundle
|
||||||
|
|
||||||
|
A `jlink`-generated JRE with the pinned module list, the boot jar, and a `bin/gittally` launcher script, packed as `gittally-runtime-linux-x64.tar.gz` (~66 MB) and unpacked to `~/opt/gittally/` on the target host.
|
||||||
|
|
||||||
|
Good:
|
||||||
|
|
||||||
|
- No production-code changes and plain JVM semantics — no new failure modes.
|
||||||
|
- git and docker CLIs are used from the host; build worktree paths stay host paths.
|
||||||
|
- `init --systemd` works unchanged: `java.home` and the running-jar path resolve into the bundle, so the generated unit points at `<bundle>/jre/bin/java` and `<bundle>/lib/gittally.jar` (verified).
|
||||||
|
- Every JDK 21 ships jlink — no new build-toolchain requirement.
|
||||||
|
|
||||||
|
Bad:
|
||||||
|
|
||||||
|
- A directory tree, not a single file (still one tarball to copy).
|
||||||
|
- JVM startup (~1-2 s) instead of native-image startup — irrelevant for a long-running server.
|
||||||
|
- The jlink image links glibc dynamically: build on glibc ≤ target (Ubuntu 24.04 dev machine: 2.39; vm4006 Debian 13: 2.41 — compatible), same architecture.
|
||||||
|
|
||||||
|
### GraalVM Native Image
|
||||||
|
|
||||||
|
Rejected because Spring AOT evaluates bean conditions at build time, and GitTally's dual-mode wiring cannot be represented in a single AOT arrangement:
|
||||||
|
the CLI context runs without web and with `@Profile("!server")` `CliRunner`, while the `server` subcommand starts a second `SpringApplication` with `WebApplicationType.SERVLET` and the `server` profile gating the watcher/metrics/nginx lifecycles.
|
||||||
|
Whichever profile and web type the AOT processing fixes, the other mode's beans are missing from the binary.
|
||||||
|
Supporting both would require replacing the profile wiring with runtime guards and collapsing the two context shapes — an invasive rewrite with regression risk for the JVM path, on top of the usual native-image reflection work (Jackson-bound config and persistence classes, picocli).
|
||||||
|
|
||||||
|
### Containerized GitTally Runtime
|
||||||
|
|
||||||
|
Rejected for operational complexity: the image must bundle git and docker CLIs; the container needs a same-path `$HOME` mount plus a docker-socket mount and uid/gid mapping so that `DockerBuildRunner`'s `--volume $workspace:$workspace` sibling mounts keep working; and the systemd unit must be hand-edited to a `docker run` invocation.
|
||||||
|
This remains the documented fallback if the runtime bundle ever becomes unworkable.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- `docs/deployment.md` documents the bundle path; the "Future: Docker-based Deployment" section in `docs/bootstrapping.md` is replaced.
|
||||||
|
- The pinned JDK module list in `build.gradle.kts` must be re-checked (via the documented jdeps command) when dependencies change.
|
||||||
|
- Deployment and migration for vm4006 follow plan step 15.
|
||||||
+4
-10
@@ -102,17 +102,11 @@ Or, when files already exist:
|
|||||||
.gittally.yml already exists — not overwritten
|
.gittally.yml already exists — not overwritten
|
||||||
```
|
```
|
||||||
|
|
||||||
## Future: Docker-based Deployment
|
## Hosts Without a Java Runtime
|
||||||
|
|
||||||
GitTally is intended to run on Hostsharing Container Server environments, which provide Docker
|
GitTally is intended to run on Hostsharing Container Server environments, which provide Docker and git but no Java runtime.
|
||||||
but no Java runtime. A later development step will add a Docker image distribution where:
|
For these hosts, `./gradlew runtimeBundle` builds a self-contained runtime bundle (jlink-trimmed JRE + JAR + launcher) — see [deployment.md](deployment.md) and ADR 0006.
|
||||||
|
A containerized GitTally runtime was considered and rejected there.
|
||||||
- GitTally itself runs as a Docker container (image bundles the JRE + JAR)
|
|
||||||
- Builds are spawned by mounting the host Docker socket (`/var/run/docker.sock`)
|
|
||||||
- `init` then optionally generates a `docker-compose.yml`, a secrets env file, and a systemd unit
|
|
||||||
that starts the Compose stack at boot
|
|
||||||
|
|
||||||
Until then, a Java runtime must be available on the host.
|
|
||||||
|
|
||||||
## Next Steps After `init`
|
## Next Steps After `init`
|
||||||
|
|
||||||
|
|||||||
+33
-1
@@ -8,7 +8,7 @@ For hosts without one, an opt-in managed nginx/TLS container is available, see [
|
|||||||
## Prerequisites
|
## Prerequisites
|
||||||
|
|
||||||
- Linux with systemd.
|
- Linux with systemd.
|
||||||
- Java runtime (JRE 21).
|
- Java runtime (JRE 21) — or none, when using the self-contained runtime bundle, see [Hosts Without a Java Runtime](#hosts-without-a-java-runtime-runtime-bundle).
|
||||||
- `git` CLI on the `PATH`.
|
- `git` CLI on the `PATH`.
|
||||||
- `docker` CLI on the `PATH`, only if any branch uses `docker.enabled` (see [configuration.md](configuration.md)).
|
- `docker` CLI on the `PATH`, only if any branch uses `docker.enabled` (see [configuration.md](configuration.md)).
|
||||||
- A checked-out working tree of the repository to watch, with a remote named `origin`.
|
- A checked-out working tree of the repository to watch, with a remote named `origin`.
|
||||||
@@ -124,6 +124,38 @@ sudo certbot --nginx -d ci.example.org
|
|||||||
|
|
||||||
This replaces the legacy script's managed nginx/Let's Encrypt Docker container for hosts that have their own web server.
|
This replaces the legacy script's managed nginx/Let's Encrypt Docker container for hosts that have their own web server.
|
||||||
|
|
||||||
|
## Hosts Without a Java Runtime (Runtime Bundle)
|
||||||
|
|
||||||
|
Some hosts provide git and Docker but no Java runtime and no way to install one, e.g. Hostsharing container servers.
|
||||||
|
For these, GitTally ships as a self-contained runtime bundle: a jlink-trimmed JRE, `gittally.jar`, and a launcher script in one tarball (ADR 0006).
|
||||||
|
|
||||||
|
Build the bundle on a Linux x86_64 machine whose glibc is not newer than the target's:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew runtimeBundle
|
||||||
|
ls build/distributions/gittally-runtime-linux-x64.tar.gz
|
||||||
|
```
|
||||||
|
|
||||||
|
Copy and unpack it on the target host, by convention to `~/opt/gittally`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
scp build/distributions/gittally-runtime-linux-x64.tar.gz user@host:/tmp/
|
||||||
|
ssh user@host 'mkdir -p ~/opt && tar -xzf /tmp/gittally-runtime-linux-x64.tar.gz -C ~/opt'
|
||||||
|
```
|
||||||
|
|
||||||
|
Then use `~/opt/gittally/bin/gittally` wherever this document says `java -jar ~/bin/gittally.jar`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd /path/to/repo
|
||||||
|
~/opt/gittally/bin/gittally init
|
||||||
|
~/opt/gittally/bin/gittally init --systemd
|
||||||
|
```
|
||||||
|
|
||||||
|
`init --systemd` detects the bundle automatically: the generated unit's `ExecStart` points at the bundle's `jre/bin/java` and `lib/gittally.jar`, so the install commands printed by `init --systemd` work unchanged.
|
||||||
|
`JAVA_OPTS` from the environment file applies as usual.
|
||||||
|
|
||||||
|
To update GitTally, stop the service, unpack the new bundle over `~/opt/gittally`, and restart the service.
|
||||||
|
|
||||||
## Hosts Without a Reverse Proxy (Managed nginx/TLS)
|
## Hosts Without a Reverse Proxy (Managed nginx/TLS)
|
||||||
|
|
||||||
Some hosts provide Docker but no root access and no host web server, e.g. Hostsharing managed container environments.
|
Some hosts provide Docker but no root access and no host web server, e.g. Hostsharing managed container environments.
|
||||||
|
|||||||
@@ -63,6 +63,9 @@ Old artifacts under the legacy artifact root remain readable on disk until you d
|
|||||||
|
|
||||||
## Manual Migration Steps
|
## Manual Migration Steps
|
||||||
|
|
||||||
|
When migrating to a **different host**, the legacy instance can keep running in parallel until the new one is verified — then skip step 1 here and stop the legacy service on the old host last.
|
||||||
|
During parallel operation, give the new instance a distinct `gitea.statusContext`, so the two instances do not overwrite each other's commit statuses in Gitea.
|
||||||
|
|
||||||
1. Stop and remove the legacy service:
|
1. Stop and remove the legacy service:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|||||||
@@ -0,0 +1,107 @@
|
|||||||
|
# Step 15: Self-Contained Runtime Bundle Distribution
|
||||||
|
|
||||||
|
Prerequisites: steps 12, 13.
|
||||||
|
Read `README.md` first.
|
||||||
|
This step revises the "Future: Docker-based Deployment" section of `docs/bootstrapping.md` — see the ADR requirement below.
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Deploy GitTally on hosts that provide Docker and git but no Java runtime (Hostsharing container servers, e.g. `tallyman@vm4006`).
|
||||||
|
GitTally is distributed as a self-contained runtime bundle: a jlink-trimmed JRE plus `gittally.jar` plus a launcher script, packed as one tarball.
|
||||||
|
The JAR stays the primary artifact for development and for hosts that already have a JRE.
|
||||||
|
|
||||||
|
## Distribution Format Decision (ADR 0006)
|
||||||
|
|
||||||
|
Three formats were considered; write ADR 0006 recording the decision and this rationale:
|
||||||
|
|
||||||
|
- **jlink runtime bundle (chosen)** — no production-code changes, plain JVM semantics, one tarball to `scp`.
|
||||||
|
git and docker CLIs are used from the host, worktree paths stay host paths, and the `init --systemd` unit works unchanged because `java.home` and the running-jar path resolve into the bundle.
|
||||||
|
- **GraalVM native image (rejected)** — Spring AOT evaluates bean conditions at build time.
|
||||||
|
GitTally's dual-context design (CLI context without web, second `SpringApplication` with the `server` profile and `WebApplicationType.SERVLET`, `@Profile("!server")` `CliRunner`, `@Profile("server")` lifecycles) cannot be represented in a single AOT arrangement.
|
||||||
|
Supporting it would require replacing the profile wiring with runtime guards and collapsing the two context shapes — an invasive rewrite with regression risk for the JVM path.
|
||||||
|
- **Containerized GitTally runtime (rejected, was the `docs/bootstrapping.md` sketch)** — needs git and docker CLIs inside the image, a same-path `$HOME` mount plus docker-socket mount and uid/gid mapping so that `DockerBuildRunner`'s `--volume $workspace:$workspace` sibling mounts keep working, and a hand-edited systemd unit.
|
||||||
|
Kept as the documented fallback if the bundle approach ever becomes unworkable.
|
||||||
|
|
||||||
|
## Target Host Facts (verified 2026-08-10)
|
||||||
|
|
||||||
|
- `vm4006.hostsharing.net`: Debian 13, x86_64, glibc 2.41, git 2.47.3, docker 26.1.5, GNU make, systemd user session running with `Linger=yes`, no Java runtime.
|
||||||
|
- The jlink image contains natively linked JVM libs, so it must be built on glibc ≤ 2.41 for the same architecture; the Ubuntu 24.04 dev machine (glibc 2.39, x86_64) qualifies.
|
||||||
|
For reproducible builds elsewhere, the bundle can be built inside an `eclipse-temurin:21-jdk` container (glibc 2.39 base).
|
||||||
|
|
||||||
|
## Design
|
||||||
|
|
||||||
|
Gradle:
|
||||||
|
|
||||||
|
- Add a `runtimeBundle` task (depends on `bootJar`); the normal `./gradlew build` stays unchanged.
|
||||||
|
- The task runs `jlink` from the configured Java toolchain (every JDK 21 ships jlink; no new toolchain requirement).
|
||||||
|
- The JDK module list is pinned in the build script, computed once via `jdeps` on the exploded boot jar and its `BOOT-INF/lib`; document the jdeps command next to the list and re-check it when dependencies change.
|
||||||
|
- Bundle layout: `gittally/jre/` (jlink image), `gittally/lib/gittally.jar`, `gittally/bin/gittally` (sh launcher: `exec "$DIR/../jre/bin/java" $JAVA_OPTS -jar "$DIR/../lib/gittally.jar" "$@"`).
|
||||||
|
- Output: `build/distributions/gittally-runtime-linux-x64.tar.gz` with preserved execute permissions.
|
||||||
|
|
||||||
|
Deployment (no code changes expected):
|
||||||
|
|
||||||
|
- Unpack to `~/opt/gittally/` on the target host; run everything via `~/opt/gittally/bin/gittally`.
|
||||||
|
- `init --systemd` already generates `ExecStart=<java> $JAVA_OPTS -jar <jar> server` from `java.home` and the running jar path — from the bundle both resolve into `~/opt/gittally/`, so the unit points at the bundle without changes.
|
||||||
|
Verify this instead of adapting code; adapt only if the resolution fails.
|
||||||
|
- Updating GitTally = unpack a new bundle over `~/opt/gittally/` (or switch a symlink) and restart the service.
|
||||||
|
|
||||||
|
Documentation:
|
||||||
|
|
||||||
|
- `docs/deployment.md`: prerequisites become "JRE 21 **or** the runtime bundle"; add a section "Hosts Without a Java Runtime (Runtime Bundle)" with build, `scp`, unpack, and systemd setup.
|
||||||
|
- `docs/bootstrapping.md`: replace the "Future: Docker-based Deployment" section with the runtime bundle and a pointer to ADR 0006.
|
||||||
|
- `docs/adrs/0006-…`: the distribution-format decision (see above).
|
||||||
|
- `docs/migration-from-legacy.md`: add a note that migrating to a different host allows parallel operation, with a distinct `gitea.statusContext` per instance so the two CIs do not overwrite each other's commit statuses.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
- No production code changes are expected, so no new unit tests; existing tests must stay green.
|
||||||
|
- Smoke-verify the bundle manually: `bin/gittally --help`, `init` in a scratch repo, `config:print --full`, a short `server` run, and `init --systemd` unit content pointing into the bundle; document the results in this file.
|
||||||
|
- Verify on vm4006 (which has no Java): copy the bundle, run `bin/gittally --help` and `config:print`; document the results in this file.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- `./gradlew ktlintFormat` then `./gradlew build` is green, with unchanged toolchain requirements.
|
||||||
|
- `./gradlew runtimeBundle` produces a tarball whose `bin/gittally` runs `--help`, `init`, and `server` on a machine without any Java runtime.
|
||||||
|
- A fresh deployment to vm4006 following `docs/deployment.md` and `docs/migration-from-legacy.md` reaches a running service: web UI reachable, a Docker build succeeds, commit status arrives in Gitea, managed nginx/TLS works (`server.nginx.enabled: true`, DNS for `serverName` pointing at vm4006).
|
||||||
|
- vm2176 (legacy) keeps running in parallel during the migration; the legacy service is only retired after vm4006 is verified.
|
||||||
|
- Docs and ADR 0006 written as described; document deviations in this file.
|
||||||
|
|
||||||
|
## Result (2026-08-10)
|
||||||
|
|
||||||
|
Implemented as designed; no production-code change was needed.
|
||||||
|
The step was originally drafted for a GraalVM native image; it was re-planned to the jlink bundle after the Spring-AOT build-time condition evaluation turned out to be incompatible with the dual-context CLI/server wiring (see ADR 0006).
|
||||||
|
|
||||||
|
- `runtimeBundle` task in `build.gradle.kts` with the pinned module list (jdeps output plus java.logging, jdk.crypto.ec, jdk.management, jdk.zipfs); launcher script in `packaging/gittally`; tarball ~66 MB.
|
||||||
|
- Smoke tests on the dev machine (with `JAVA_HOME` unset and a stripped `PATH`): `--help`, `init --systemd` in a scratch repo, `config:print --full`, and a `server` run all passed; `/` served HTTP 200 and `/api/branches` returned JSON.
|
||||||
|
- The `init --systemd` unit generated from the bundle points at `<bundle>/jre/bin/java` and `<bundle>/lib/gittally.jar` as predicted — no detection code needed.
|
||||||
|
- Verified on vm4006 (no Java installed): bundle unpacked to `~/opt/gittally`, `--version`, `--help`, and `status` in a scratch repo (host git via `GitCommandRunner`) all worked.
|
||||||
|
|
||||||
|
Production deployment to vm4006 (2026-08-10, same session):
|
||||||
|
|
||||||
|
- `hs.hsadmin.ng` cloned to `~/hs.hsadmin.ng` on vm4006; legacy configuration from vm2176 (repo `.gitTally` + `gitTally.env`) migrated to `.gittally.yml` per `docs/migration-from-legacy.md`; Gitea token moved (the token in vm2176's `gitTally.env` file was stale — the valid one came from the running legacy process environment).
|
||||||
|
- `statusContext: GitTally@vm4006` for the parallel phase; rename to `GitTally` after vm2176 is retired.
|
||||||
|
- systemd user service installed via `init --systemd` from the bundle and running; watcher fetches origin branches with the migrated credentials.
|
||||||
|
- Managed nginx/TLS live: Let's Encrypt certificate for `vm4006.hostsharing.net` obtained, `https://vm4006.hostsharing.net/` serves the UI with a valid chain, HTTP 301s to HTTPS (Hostsharing routes public 80/443 to `httpPort`/`httpsPort`, same as on vm2176).
|
||||||
|
- Fix discovered during rollout: certbot removed `ssl-dhparams.pem` from its repository, so the first nginx start failed with HTTP 404.
|
||||||
|
The DH parameters (RFC 7919 ffdhe2048) are now bundled as the classpath resource `nginx/ssl-dhparams.pem` instead of downloaded; the download seam and `NginxConfigFiles.DH_PARAMS_URL` were removed (revises the step-13 note about the download).
|
||||||
|
Legacy on vm2176 only kept working because its state dir cached the file.
|
||||||
|
|
||||||
|
Second fix discovered by the first real build: under a **rootless** Docker daemon, `DockerBuildRunner` ran the build container as `--user <host-uid>` (ported from legacy).
|
||||||
|
With rootless identity mapping the host user is container root, and the host uid inside the container falls into the subuid range — the container could not create `.gradle` in the freshly created worktree ("Failed to create parent directory").
|
||||||
|
Legacy on vm2176 only worked because its ownership-repair chown had (unintentionally) moved `build/` and `.gradle/` into subuid ownership on the host (verified: owned by uid 166536 there) — a stable but wrong equilibrium tied to its reused primary checkout.
|
||||||
|
The rewrite now always runs the build container as `--user 0`: under rootless that IS the unprivileged host user (files stay host-owned, the repair chown degenerates to `0:0`); under rootful daemons the behavior is unchanged (root + chown to the host ids).
|
||||||
|
|
||||||
|
Third finding (config, not code): the hsadmin-ng Liquibase migration tests (`LiquibaseCompatibilityIntegrationTest`, `ImportHostingAssets.liquibaseMigrationForBookingAndHosting`) failed on vm4006 with "environment variable HSADMINNG_POSTGRES_ADMIN_USERNAME not set".
|
||||||
|
These tests run Liquibase programmatically without Spring's `spring.liquibase.parameters`, so the changelog parameters resolve only via Liquibase's env-var substitution; the legacy script had a host-env passthrough for exactly these variables.
|
||||||
|
The values `admin`/`restricted` are hsadmin-ng's committed defaults, but only for the other execution paths: `.tc-environment` for the documented dev workflow (`. .tc-environment; ./gradlew …`) and the `${…:admin}` fallbacks in `src/test/resources/application.yml` for the Spring-managed Liquibase path.
|
||||||
|
The programmatic path deliberately has no fallback — `009-check-environment.sql` exists to verify the environment is configured — so a CI environment must export the variables itself.
|
||||||
|
Fix: `HSADMINNG_POSTGRES_ADMIN_USERNAME=admin` and `HSADMINNG_POSTGRES_RESTRICTED_USERNAME=restricted` in `branches.default.docker.env` on vm4006 — the mechanism `docs/migration-from-legacy.md` prescribes for the legacy passthrough list; alternatively the build command could source `.tc-environment` like the dev workflow.
|
||||||
|
Verified by running both test classes in the build container with the variables set: green.
|
||||||
|
Open oddity: the same commit passed on vm2176 although neither its daemon environment, build image, Gradle volume, nor any build-script mechanism supplies these variables there (an unused git-ignored `.environment` file exists in its primary checkout, but nothing in the build reads it); the loading path on vm2176 remains unidentified.
|
||||||
|
|
||||||
|
Fourth finding (GitTally limitation, worked around in config): with all tests green, the build then failed in hsadmin-ng's `:prQuickCheck` — "fatal: not a git repository".
|
||||||
|
GitTally builds in a git worktree whose `.git` is a pointer file into the primary repository's `.git/worktrees/…`, and the Docker build container (deliberately, credentials live under `.git/gittally/`) only mounts the worktree — so build steps that call git fail; the legacy script avoided this by building in the primary checkout.
|
||||||
|
Workaround: `prQuickCheck` removed from the vm4006 build command — it is a PR quality gate against a base branch and has no meaning in a post-merge master build (on vm2176 it only passed as an accidental no-op).
|
||||||
|
The underlying question (safe git availability inside Docker build containers without exposing `.git/gittally/` secrets) is left as a follow-up design task.
|
||||||
|
|
||||||
|
Still open: green completion of the first real build with its Gitea commit status, and — after a stable parallel phase — retiring the legacy service on vm2176.
|
||||||
+7
-1
@@ -71,10 +71,16 @@ Added after the initial plan (ADR 0005):
|
|||||||
|
|
||||||
Added after the 2026-08-10 overhead measurements on vm2176:
|
Added after the 2026-08-10 overhead measurements on vm2176:
|
||||||
|
|
||||||
- [ ] `14-build-phase-timing-and-overhead.md` — per-build phase timing, overhead budget warning, ownership/metrics fixes
|
- [ ] `14-build-phase-timing-and-overhead.md` — per-build phase timing, overhead budget warning, ownership/metrics fixes — deferred until after step 15; revisit relevance on vm4006
|
||||||
|
|
||||||
|
Added for the vm2176 → vm4006 migration (2026-08-10):
|
||||||
|
|
||||||
|
- [ ] `15-runtime-bundle-distribution.md` — self-contained runtime bundle (jlink JRE + jar) for hosts without a Java runtime
|
||||||
|
- [ ] `16-git-in-docker-builds.md` — read-only git metadata inside Docker build containers, with `.git/gittally/` masked
|
||||||
|
|
||||||
Steps 01–03 are independent of each other.
|
Steps 01–03 are independent of each other.
|
||||||
Steps 04–06 depend on 01–03.
|
Steps 04–06 depend on 01–03.
|
||||||
Steps 07–09 depend on 04–06.
|
Steps 07–09 depend on 04–06.
|
||||||
Steps 11 and 12 are optional/deferrable; 10 only needs 04–06.
|
Steps 11 and 12 are optional/deferrable; 10 only needs 04–06.
|
||||||
Step 13 depends on 07, 11, and 12.
|
Step 13 depends on 07, 11, and 12.
|
||||||
|
Step 15 depends on 12 and 13 and revises the containerized-runtime sketch in `docs/bootstrapping.md` (ADR 0006 is written as part of the step; GraalVM native image was evaluated and rejected there).
|
||||||
|
|||||||
Executable
+6
@@ -0,0 +1,6 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# Launcher for the self-contained GitTally runtime bundle (jlink JRE + jar).
|
||||||
|
# Built by `./gradlew runtimeBundle`; see docs/deployment.md.
|
||||||
|
DIR=$(CDPATH='' cd -- "$(dirname -- "$(readlink -f -- "$0")")" && pwd)
|
||||||
|
# shellcheck disable=SC2086 # JAVA_OPTS is intentionally word-split
|
||||||
|
exec "$DIR/../jre/bin/java" $JAVA_OPTS -jar "$DIR/../lib/gittally.jar" "$@"
|
||||||
Reference in New Issue
Block a user