Commit Graph
193 Commits
Author SHA1 Message Date
mhoennigandClaude 129f38143d Warn that a statusContext rename mid-build strands a pending Gitea status
Renaming gitea.statusContext while a build runs splits that build over two
contexts: the abandoned one keeps its 'build running' entry, and Gitea reports
the commit as pending forever. Gitea cannot delete a commit status, so document
the manual closing POST as the only way out.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 11:17:24 +02:00
mhoennigandClaude 903a87e547 Correct the runtime bundle's glibc rule: the JDK vendor sets the floor
ADR 0006 assumed the bundle inherits the build machine's glibc, which would
have blocked the Hostsharing Managed Webspace (glibc 2.36, dev machine 2.39).
Measuring all 33 ELF files of the produced bundle shows GLIBC_2.15 as the
highest required symbol version: jlink copies Temurin's prebuilt binaries
instead of compiling, so the floor is the JDK vendor's build environment and
the build machine's glibc is irrelevant unless the toolchain resolves to a
distribution-packaged JDK.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 10:23:15 +02:00
mhoennigandClaude c77de1c725 Record the webspace's bwrap and kernel versions, and the glibc consequence
bubblewrap 0.8.0 covers every option the sandbox design uses; only overlayfs is
missing, which the design does not need. The kernel version, however, points at
Debian 12 and thus a glibc older than the dev machine's, which would break the
jlink runtime bundle on that host -- noted as a check to run before deploying.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 10:21:00 +02:00
mhoennigandClaude 3f93882dda Record the passing bwrap precondition check on a Managed Webspace
Plan step 17 hinges on unprivileged user namespaces being usable on a
Hostsharing Managed Webspace. The check ran on h68 and passed with all three
expected signals, so the step is viable there and the sandbox design stands.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 10:19:21 +02:00
mhoennigandClaude 021d97eca8 Mark the logs that carry a failure line on the artifact index
The artifact index listed the stored logs as bare file names, so a red build
gave no hint which of build.log, build.stdout.log and build.stderr.log actually
explains it — with stdout and stderr stored separately, the failure is usually
in only some of them.

Each log of a non-green build is now scanned for an upper-case FAILED/FAILURE,
which covers BUILD FAILED, Maven's BUILD FAILURE and Gradle's per-test
"SomeTest > works() FAILED"; lower-case prose does not count. The scan streams
line by line with an early exit and reads ISO-8859-1, so no byte sequence of a
build log can fail to decode. Logs of a successful build are not scanned at all
— that saves reading megabytes per page view and avoids an alarming badge on a
green build whose log mentions a deliberately failing sub-build.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 10:11:45 +02:00
mhoennigandClaude cff365767e Plan step 17: run on a dedicated unix user, and cap the unit's resources
Hostsharing recommends assigning domains to separate domain admins rather
than to the package admin, and all their service guides (Mattermost,
Tomcat, Nextcloud) run the daemon as its own user. For GitTally the
argument is stronger: it checks out foreign commits and executes their
build scripts, so running as the package admin would undo the sandbox
rationale of this step. The service user has to be named when ordering
the daemon port anyway.

Also records a trap found on the way: the RAM contingent is a package
slice, not a per-user quota, so a dedicated user buys isolation but no
extra memory — a runaway Gradle build could starve the whole webspace.
The unit from `init --systemd` sets neither MemoryMax nor TasksMax today,
so adding them (configurable, empty = unset) becomes part of step 17.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 08:52:24 +02:00
mhoennigandClaude d5f2784b60 Plan step 17: web access on a Managed Webspace, alongside the sandbox
Keeping both halves in one step: a bubblewrap runtime alone would only
prove that sandboxed builds work somewhere, and web access alone would
mean builds running unsandboxed on the webspace. Neither ships value on
its own, so step 17 now covers the whole deployment.

The web half needs no code: Hostsharing provides Apache plus Let's
Encrypt and documents the reverse proxy to a self-hosted service, so the
managed nginx container of ADR 0005 is not used there. What it needs is
the booked "eigener Serverdienst" option with an assigned localhost port,
a systemd user unit (which `init --systemd` already generates), and a
`.htaccess` with a `[proxy]` RewriteRule — with sources from Hostsharing's
own wiki and feature pages. GitTally fits as is, because it builds
external links from `server.publicBaseUrl` rather than from the request,
so no forward-headers handling is required.

Two points are explicitly marked unverified in the step file: the
effective AllowOverride value and whether an unassigned port would bind.
Also ticks steps 15 and 16 in the plan index — both carry a Result
section and are long done.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 08:37:58 +02:00
mhoennigandClaude 2351ddcecd Document the update procedure for an existing installation
deployment.md only said "replace the jar and restart", which left out the
runtime-bundle case entirely — including the trap that the tarball
unpacks to a `gittally/` directory and must not be extracted over ~/opt.
Both variants now list the actual commands, with a rollback copy and the
note that a restart is safe because in-flight builds are re-enqueued.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 08:09:01 +02:00
mhoennigandClaude 960dc97e75 Record the v0.9.10 deployment to vm4006
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 08:08:15 +02:00
mhoennigandClaude dbb26be38b Settle TODO 5: public build logs are intended, not a leak
The watched projects (GitTally and hs.hsadmin.ng) are open source, keep
no secrets in the repository and build against test data, so credentials
appearing in a log are fixtures. The builds neither deploy nor sign; the
only planned artifact is a jar. Public logs are also the point: a red
build has to be diagnosable from the link in the Gitea status without a
login.

Recorded as a property of the watched project rather than of GitTally —
deployment.md now says that an installation whose builds touch real
credentials has to stay off the public internet, since GitTally offers no
per-endpoint gating.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 08:06:04 +02:00
mhoennigandClaude a734d91918 Stop embedding the control token in every page (v0.9.10)
Closes TODO 2 of the security audit in docs/prs/2026-07-08-PR#000.

Every rendered page carried the live control token in a meta tag so that
gittally.js could send it, but no GET is authenticated — so `curl … |
grep gittally-control-token` handed the token to anyone, and read access
was effectively write access.

Reading stays fully public, which is a requirement rather than an
oversight: build states, logs and artifacts must be linkable from Gitea,
chats or tickets without a login. Only the distribution of the token
changed. The meta tag is gone; gittally.js keeps the token in
localStorage and asks for it once per browser, so knowing it requires
shell access to `.git/gittally/control-token` on the host. A token the
server rejects is dropped and asked for once more, so a rotated secret is
not a dead end. As a request header it stays inherently CSRF-safe.

The five branches of that flow (first use, reuse, stale token, cancelled
prompt, wrong token twice) were exercised against the real source with a
throwaway node harness; the UI test now asserts the token does not appear
in the rendered page. `docs/deployment.md` gained a "Control Token"
section on the public-read/token-protected-write split.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 08:00:09 +02:00
mhoennigandClaude ea0b67d331 Record the v0.9.9 deployment to vm4006
Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 07:45:45 +02:00
mhoennigandClaude a4c995592f Header-only control token, masked secrets, loopback default (v0.9.9)
Finishes the small items of the security audit in
docs/prs/2026-07-08-PR#000: TODO 3, 4 and 7.

The three mutating endpoints of BuildsApiController no longer accept the
control token as a `token` query parameter — only the X-GitTally-Token
header, which the bundled UI has always used. URLs end up in access logs,
proxy logs, browser history and Referer headers, and the token never
expires, so a historical log capture would yield a valid credential.

`config:print` masks git.token as `***` on both the raw and the --full
path and names the new --show-secrets flag in a leading YAML comment, so
the output stays parseable when piped. The setup script points at
--show-secrets where it used to steer the operator to the plain token.

`server.bindAddress` now defaults to 127.0.0.1: neither the UI nor the
API authenticates read access, so reaching GitTally should require the
host's reverse proxy. Existing .gittally.yml files keep their explicit
value; the managed nginx container needs `0.0.0.0` set deliberately,
which is noted in the release notes, docs/configuration.md and
docs/deployment.md.

Released as v0.9.9, which also carries the previous two commits.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 07:38:33 +02:00
mhoennigandClaude dea6770998 Harden secret-file creation, token comparison and git refname args
Works off the security audit in docs/prs/2026-07-08-PR#000: TODO 1, 8, 9
and 10, the four items that need no design decision.

New `SecretFiles` creates files holding secrets with mode 0600 and their
directories with 0700 *at creation*, as a file attribute, instead of
writing at the umask default and chmod-ing afterwards — that left a
window in which the Gitea token was world-readable, which matters on a
multi-tenant host. It is used by `init` for .git/gittally/.gittally.yml
and by `ControlTokenService` for the control token; the shell setup
script now writes its YAML in a `umask 077` subshell for the same reason.

`ControlTokenService.matches` hashes both sides with SHA-256 before
`MessageDigest.isEqual`, so the comparison always runs over two 32-byte
buffers and cannot return early on a length mismatch.

`GitService.checkout` and `fetchBranch` pass `--` before the refname, so
a branch named like an option cannot be read as one. `resetHardToOrigin`
keeps its plain form: `git reset --hard -- <commit>` is rejected outright
and its argument is already `origin/`-prefixed.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 07:09:34 +02:00
mhoennigandClaude 3eb41d4c66 Stack page title and repository name in the mobile header
On narrow screens the page title and the repository name shared one flex
line and wrapped unreadably. Below 680px the h1 now becomes a grid: the
logo spans both rows on the left, the title takes the first line and the
repository name the second (slightly smaller, wrapping anywhere so long
owner/repo names cannot overflow). The desktop layout is unchanged.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-08-11 06:59:52 +02:00
mhoennigandClaude Fable 5 daba7f6acc Record the v0.9.8 deployment to vm4006
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 21:37:30 +02:00
mhoennigandClaude Fable 5 962836beaf Stable per-branch report URLs and a reachable live view (v0.9.8)
A report directory holding a single page is now linked and served as a
directory, so Gradle's --profile report has a stable permanent URL although
its file name carries the build timestamp.

The permanent link moves to the build it resolves to — the branch's latest
green build — and appears on every build table instead of only the branches
view. The Current tab gave way to a link in the artifacts column, shown
while a build runs; /current itself stays routable.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 21:27:53 +02:00
mhoennigandClaude Fable 5 c1869ea427 Link index-less report pages from the artifact index (v0.9.8)
Gradle's --profile report is archived but was unreachable: report discovery
only looked for index.html, while the profile page carries a timestamped
file name. Scan reports/ and its direct sub-directories for HTML pages that
no index covers, so a report tree cannot flood the index with inner pages.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 21:11:15 +02:00
mhoennigandClaude Fable 5 632e4396e4 Plan step 17: bubblewrap build sandbox for Hostsharing Managed Webspaces
Third build runtime behind BuildRunner: unprivileged user namespace via
bwrap with a prepared Debian rootfs, for hosts without Docker or root.
The step starts with a one-line precondition check to run on the target
webspace; the step-16 git metadata mounts port 1:1.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 20:42:48 +02:00
mhoennigandClaude Fable 5 e1f3f5f384 Install a nightly Docker cleanup timer with init --systemd (v0.9.7)
Port of the legacy host's docker-prune.timer: 02:00 host time,
Persistent=true, docker system prune -af — but without --volumes, so
the per-repository Gradle cache volumes survive. The units are
host-global; several GitTally instances share one timer.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 19:22:44 +02:00
mhoennigandClaude Fable 5 74bc4c2d73 Mention the utilization highlighting in the 0.9.6 release notes
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 19:09:21 +02:00
mhoennigandClaude Fable 5 f2d4dbfda0 Highlight critical utilization on the system page (v0.9.6)
The Current cell of CPU/RAM/disk used turns orange from 80% of the
total and red from 90%. UiFormats.utilizationClass and the mirrored
utilizationClass in gittally.js apply the same thresholds; unavailable
metrics (n/a) are never highlighted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 19:08:23 +02:00
mhoennigandClaude Fable 5 428d1a06cb Add a release-notes page linked from the footer version (v0.9.6)
Reconstructed from the commit history since the first production
deployment; the initial entry is the 0.9.0 port of the legacy bash
script to Kotlin/Spring Boot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 18:02:30 +02:00
mhoennigandClaude Fable 5 d2b39c56d3 Collapse the live indicator to a state dot on small screens
On mobile widths the "live" badge squeezed the view-toggle menu into
unreadable widths; the dot keeps the state color (green/red/grey) and
the row may wrap as a last resort.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 17:57:35 +02:00
mhoennigandClaude Fable 5 bfe9010485 Bump version to 0.9.5 for the shutdown-interruption deployment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 17:31:36 +02:00
mhoennigandClaude Fable 5 8b87fbeb7d Merge branch 'claude/amazing-khayyam-38cad4': interrupt builds on server shutdown
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 17:31:11 +02:00
mhoennigandClaude Fable 5 40f54a1298 Record builds interrupted by a server shutdown as INTERRUPTED, not FAILED
A systemd stop killed the running build process and BuildExecutor
classified the death as FAILED, posting a red Gitea status; FAILED is
not restartable, so the startup recovery never re-enqueued the build.

A ContextClosedEvent listener now sets a shuttingDown flag, terminates
the process trees of executing builds, and drains until their
INTERRUPTED results are persisted. Queued builds stay PENDING without
starting a process; recovery re-enqueues both after the restart.
INTERRUPTED publishes as Gitea state "pending" instead of "failure",
since the build is going to be re-run.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 17:26:10 +02:00
mhoennigandClaude Fable 5 355d8ae90f Record the vm2176 retirement and redirect setup in plan step 15
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 16:56:22 +02:00
mhoennigandClaude Fable 5 eb49387c97 Redirect legacy page names to the new routes (v0.9.4)
The retired legacy instance now blanket-redirects its old host to the new
one, so pre-rewrite deep links like /index.html or /branches.html arrive
here — they answer 301 to the new routes instead of 404; about/license had
no successor pages and land on the start page.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 16:55:11 +02:00
mhoennigandClaude Fable 5 fbdc6f54b7 Bump version to 0.9.3 for the report-badge deployment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 16:44:12 +02:00
mhoennigandClaude Fable 5 b351ab33c5 Show a failed-badge behind report links on the artifact index
Each listed report link now carries the failures counter parsed from the
report's Gradle-style index.html (id="failures" info box); reports with
failures get a red "N failed" badge, so a red build reveals which report to
open without clicking through all of them. Pages without such a counter
(Jacoco, profile, documentation) stay unmarked. A full-text FAILED scan was
deliberately not used: even green hsadmin-ng builds contain the word in
embedded test output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 16:44:12 +02:00
mhoennigandClaude Fable 5 4ca077da80 Document the initial build burst after the first server start
Investigating six near-simultaneous builds on vm4006 showed no duplicate
enqueue: they were six distinct recently-active origin branches, each built
once by the documented new-origin-branch rule (in a fresh clone every origin
branch counts as new). The watcher already guards against duplicates per
branch and per commit, with test coverage; only the first-start behavior was
undocumented.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 16:33:04 +02:00
mhoennigandClaude Fable 5 421d51063b Bump version to 0.9.2 for the UI/cancel fixes deployment
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 16:14:48 +02:00
mhoennigandClaude Fable 5 ed9562d1a7 Let cancel terminate auxiliary build phases instead of blocking the slot
Cancelling a build only killed the running build process; the synchronous
preparation phases — most notably a multi-minute Docker image build, but also
the Gradle-volume preparation — ran to completion and kept the concurrency
slot occupied, so the next queued build stayed PENDING for a long time.
Build runners now report every auxiliary process through an onAuxProcess sink
(GitCommandRunner gained an onProcess hook), and the executor registers them
like the build process, so cancellation terminates whatever is currently
running. Measured on vm4006: cancel to next-build-running is ~3s in the
normal case; the unit test covers the aux-phase case.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 16:14:38 +02:00
mhoennigandClaude Fable 5 76321a6f5c Bump version to 0.9.1 and note the bump-per-deployment convention
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 16:03:52 +02:00
mhoennigandClaude Fable 5 56aa306481 Separate queue wait from build duration
BuildResult gains runningSince, set when a build leaves the queue; the
recorded duration now measures pure build time from that point, so build
runtimes can be tracked without queue wait. A build cancelled while still
queued records neither. The UI shows the live wait time in italics while
pending and switches to the real build time once the build runs; the Gitea
"after mm:ss" descriptions now also report pure build time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 15:55:36 +02:00
mhoennigandClaude Fable 5 a03f85bc17 Show a log-only artifact icon while a build is running or pending
An in-progress build's artifact page only offers the build command and log —
the table now shows an hourglass instead of the document icon (server-rendered
rows and the JS-rendered rows alike) until the build finishes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 15:48:30 +02:00
mhoennigandClaude Fable 5 2cb6b1598a Render live durations immediately instead of leaving them to the ticker
The 10s table poll rebuilt the rows with an empty duration cell for
running/pending builds (durationSeconds is null until a build finishes),
which the once-per-second ticker then filled back in — a visible flicker.
Rows and current-build cards now compute the elapsed time at render time.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 15:45:33 +02:00
mhoennigandClaude Fable 5 9e7982ce34 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>
2026-08-10 15:35:35 +02:00
mhoennigandClaude Fable 5 8b71d0db8e Fix Docker builds under rootless daemons and expose git metadata to build containers
Two fixes from the vm4006 rollout (docs/plan/16-git-in-docker-builds.md):

Rootless daemons map the host user to container root, so running the build
container as --user <host-uid> put it into the subuid range and it could not
even create .gradle in a fresh worktree (legacy only worked because its
ownership-repair chown had accidentally moved build/ and .gradle/ into subuid
ownership in its reused primary checkout). The container now always runs as
--user 0: the unprivileged host user under rootless, real root under rootful
where the ownership repair still applies; under rootless it degenerates to 0:0.

Git now works inside build containers: the primary .git is mounted read-only
with .git/gittally/ masked by an empty tmpfs (git.token and the control token
stay unreachable, the workspace bind resurfaces only the build's own worktree)
and the worktree admin dir mounted read-write for index-refreshing commands.
Verified on vm4006: git log/status succeed, the machine config is invisible,
ref writes fail on the read-only mount.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 15:34:39 +02:00
mhoennigandClaude Fable 5 800fcae2f7 Bundle certbot's DH parameters as a resource instead of downloading them
certbot removed ssl-dhparams.pem from its repository, so the managed-nginx
startup failed with HTTP 404 on fresh installations (the legacy script only
kept working through its year-old state-dir cache). The RFC 7919 ffdhe2048
parameters are now a classpath resource; the download seam and
NginxConfigFiles.DH_PARAMS_URL are gone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-10 15:34:20 +02:00
mhoennig 66183f9765 Add build-phase timing and overhead optimization plan to docs 2026-08-10 13:32:17 +02:00
mhoennig 32bf25978b Update .idea config: add templates folder configuration 2026-08-10 13:31:32 +02:00
Michael Hoennig fb6d4501b9 Added worktree-layered build config: resolves .gittally.yml from the worktree for per-branch build settings, with precedence worktree > .git > project; pinned secrets, server-side keys, and sandbox policy to .git. 2026-07-09 09:20:34 +02:00
Michael Hoennig 095e6fa44f PR-doc: split config precendence and hosisting TODO 9 2026-07-09 07:08:10 +02:00
Michael Hoennig b21b8b8a28 Security Report 2026-07-09 06:32:49 +02:00
Michael Hoennig 347efc16c5 standardized JAR naming to gittally.jar (version-free); updated scripts, docs, and build config to align; added --version support via BuildProperties 2026-07-08 22:45:23 +02:00
Michael HoennigandClaude Fable 5 d0b38c557a added age-based build retention: artifacts.retentionMaxAge (e.g. 30d, empty = no limit) drops builds older than the given age; combines with retentionPerBranch as independent caps — a build is kept only while it satisfies both limits; a branch's newest build is never age-pruned and keepLatestGreen now shields the latest green build from both limits, keeping the permanent /branches/... links valid
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 22:35:31 +02:00
Michael HoennigandClaude Fable 5 b104eeee05 added opt-in managed nginx/TLS container (ADR 0005, plan step 13): server.nginx.* config serves GitTally over HTTPS on hosts without a reverse proxy — two-phase startup (ACME webroot via certbot container, then full HTTPS config), daily certificate renewal with nginx reload, labelled container removed on shutdown; all failures are non-fatal, the plain HTTP server keeps running
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-08 21:51:33 +02:00
Michael Hoennig fdbbd0516a added setup-gittally-instance script: migrates legacy .gitTally configurations to YAML, sets up a GitTally instance on a Docker host, and prompts for Gitea secrets 2026-07-08 20:42:03 +02:00