Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb1db30f2a | ||
|
|
2cddd76c88 | ||
|
|
7fd473b88b | ||
|
|
092183ca30 | ||
|
|
0761a274db | ||
|
|
5c73c4cc21 | ||
|
|
16543f038b | ||
|
|
3ccc901d1b | ||
|
|
11f6f9bd26 | ||
|
|
9221550a2a |
@@ -68,6 +68,7 @@ Keep sentences short.
|
||||
- `docs/deployment.md` — running Werkator as a systemd user service behind an existing reverse proxy (`init --systemd` generates the unit).
|
||||
- `docs/werkator-migrationsplan.md` — renaming a running installation from GitTally to Werkator: what the name fallback covers and what has to be moved by hand.
|
||||
- `docs/plan/` — the step-by-step rewrite plan; `docs/plan/README.md` explains how to execute a step, `docs/plan/00-legacy-analysis.md` summarizes the legacy bash script.
|
||||
- `docs/rfcs/` — requests for comments: proposals that are larger than one PR and not yet a decision (an accepted RFC becomes an ADR or a plan step).
|
||||
- `docs/prs/` — one document per pull request; every PR needs one. IMPORTANT: Before opening or finishing a pull request, load the [pr-doc skill](.claude/skills/pr-doc/SKILL.md) and write the PR-doc.
|
||||
|
||||
## Key Architectural Decisions
|
||||
|
||||
@@ -0,0 +1,42 @@
|
||||
> **WARNING:** This document describes only the change applied in this PR.
|
||||
> It may already be outdated once the next PR is merged.
|
||||
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
|
||||
|
||||
## The Problem
|
||||
|
||||
The build for commit `7028ca8` on `main` failed on the mih09 production instance with
|
||||
`BuildExecutorTest > with maxConcurrent 1 a second branch stays PENDING until the first finished`,
|
||||
while a retry of the very same commit passed, and the test passes locally.
|
||||
|
||||
The test was timing-dependent.
|
||||
It started a build for `branch-a` whose build command was `sleep 1`, immediately started a second build for `branch-b`,
|
||||
and then asserted — without any synchronization at all — that `branch-b` was still `PENDING`.
|
||||
|
||||
That assertion only held as long as the test thread reached it within the one second `branch-a` slept.
|
||||
On a shared host under CPU contention the executor can get through `branch-a` entirely (queued, running, slept, succeeded) first,
|
||||
and `branch-b` is then already `RUNNING` or `SUCCESS` when the assertion runs.
|
||||
The failure therefore says nothing about the executor; it is pure scheduling noise that costs a build and a retry every time it hits.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- No change to production code — `BuildExecutor` is not touched, its queueing behaviour is unchanged.
|
||||
- No sweep of the other timing-sensitive tests in the suite; only the one that actually flaked is fixed.
|
||||
|
||||
## The Solution
|
||||
|
||||
`branch-a` no longer sleeps for a fixed time, it blocks until the test says so:
|
||||
its build command is `until [ -f gate ]; do sleep 0.05; done`, and the build workspace is the test's working directory.
|
||||
|
||||
The test now
|
||||
1. waits (via `eventually`) until `branch-a` is `RUNNING`, so the single executor slot is provably occupied,
|
||||
2. asserts that `branch-b` is `PENDING` — which cannot race anything, because `branch-a` cannot finish before the gate file exists,
|
||||
3. creates the gate file, and only then awaits both builds' `SUCCESS`.
|
||||
|
||||
The assertion on the event transitions (`branch-b` goes `RUNNING` only after `branch-a` reached `SUCCESS`) is unchanged.
|
||||
A blocking gate was chosen over a mocked `BuildRunner` because it keeps the test on the real `ProcessBuildRunner`,
|
||||
so it still covers the actual process handling rather than only the executor's bookkeeping.
|
||||
|
||||
Verified by running `BuildExecutorTest` five times on an idle machine and three more times with twice `nproc` busy-loops saturating the CPU,
|
||||
which is the condition that produced the original failure.
|
||||
|
||||
- [BuildExecutorTest](../../src/test/kotlin/de/hoennig/werkator/build/BuildExecutorTest.kt)
|
||||
@@ -0,0 +1,30 @@
|
||||
> **WARNING:** This document describes only the change applied in this PR.
|
||||
> It may already be outdated once the next PR is merged.
|
||||
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
|
||||
|
||||
## The Problem
|
||||
|
||||
The repository's home is `https://git.javagil.de/mi/werkator.git` since the move to the own Gitea instance,
|
||||
but `tools/remote` still defaulted `WERKATOR_REPO_URL` to the GitHub mirror.
|
||||
|
||||
The mih09 production instance was cloned from that mirror and consequently watched a `main` that nobody pushes to any more:
|
||||
it kept reporting the last GitHub state as green while three merged pull requests sat unbuilt on the real `main`.
|
||||
The failure that started this — a flaky test already fixed on Gitea's `main` — could not be re-verified live,
|
||||
because the instance had no way to see the fix.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- The existing clone on mih09 is not touched by this change; its remote was repointed by hand (`git remote set-url`), and `repo-init` skips an existing clone.
|
||||
- The generic placeholder URLs in `docs/deployment.md` stay as they are — they describe cloning *any* watched repository, not werkator's own.
|
||||
- No decision about mirroring to GitHub; the mirror simply stops being the source an instance builds from.
|
||||
|
||||
## The Solution
|
||||
|
||||
`REPO_URL` in [tools/remote](../../tools/remote) defaults to the Gitea URL, and the usage comment says so.
|
||||
|
||||
The value stays overridable via `WERKATOR_REPO_URL` in the instance's env file,
|
||||
so an installation that deliberately watches a different remote is unaffected.
|
||||
Anonymous HTTPS works against Gitea exactly as it did against GitHub, so no deploy key or token is involved.
|
||||
|
||||
Note that pull requests opened via AGit-Flow create no branch in Gitea,
|
||||
so an instance watching this remote sees `main` only — branch builds require pushing real branches.
|
||||
@@ -0,0 +1,90 @@
|
||||
> **WARNING:** This document describes only the change applied in this PR.
|
||||
> It may already be outdated once the next PR is merged.
|
||||
> Historic PR-documentation is not maintained along with new PRs — treat it as a snapshot, not as current documentation.
|
||||
|
||||
## The Problem
|
||||
|
||||
`init --systemd` generates the host integration — the systemd unit's resource limits, the Apache `.htaccess` and the maintenance page — from the effective configuration.
|
||||
It read that configuration through a helper with two independent defects, both of which fail silently.
|
||||
|
||||
**It swallowed every error.**
|
||||
The load sat in `try { … } catch (_: Exception) { ServerConfig() }`.
|
||||
Any configuration error at all — a missing required field, a malformed layer, a version floor violation — produced a default `ServerConfig` with a blank `publicBaseUrl`.
|
||||
A repository with a broken `.werkator.yml` then looked exactly like one that simply has no public base URL configured:
|
||||
the `.htaccess` and the maintenance page were skipped without a word.
|
||||
This surfaced while verifying [PR#17](2026-09-03-PR%2317-maintenance-page.md) on mih09, where the missing files looked like an unconfigured `publicBaseUrl` and were in fact an unrelated validation error.
|
||||
|
||||
**It read from the wrong directory.**
|
||||
The helper called `configLoader.load(Paths.get("."))` — the process's current directory — while everything else in the command works off the git top level resolved by `GitService.getTopLevel`.
|
||||
`ConfigLoader.loadRaw` resolves the layers directly under the directory it is given and does not walk up to the repository root, so the two agree only when `init` happens to be invoked from the root itself.
|
||||
From a subdirectory the command read another repository's configuration, or none.
|
||||
That also broke `--apply`: the fragment is installed into the repository root deliberately before the systemd files are written, so that its port and limits reach the generated unit, and a current-directory read does not see it.
|
||||
|
||||
`init --systemd` runs during initial deployment setup, which is exactly when a silent wrong answer is most expensive.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- The fallback itself is kept: a configuration that cannot be loaded is not fatal for `init`, the units are still generated with the defaults.
|
||||
During the very first bootstrap there is legitimately nothing to load yet.
|
||||
- No change to `ConfigLoader`, to the configuration schema, or to any other command.
|
||||
- No sweep for catch-all exception handlers elsewhere in the code base;
|
||||
the two other `catch` blocks in `InitCommand` already print an `Error:` and abort, so they were only checked, not changed.
|
||||
|
||||
## The Scenarios
|
||||
|
||||
### Feature: init reports what it read and where it read it from
|
||||
|
||||
#### Background
|
||||
|
||||
- The *repository root* is the git top level as resolved by `GitService.getTopLevel`, the directory holding `.werkator.yml`, `.git/werkator/.werkator.yml` and an applied fragment.
|
||||
- The *current directory* is the process working directory, which is the repository root only when `init` is invoked there.
|
||||
|
||||
#### Scenario#22.01: A broken configuration is named, not defaulted over
|
||||
|
||||
So that a validation error during deployment setup is not mistaken for an unconfigured installation.
|
||||
|
||||
- **Given** a repository whose effective configuration cannot be loaded
|
||||
- **When** `init --systemd` runs
|
||||
- **Then** the exception message is printed as a warning
|
||||
- **and** the unit files are still generated with the default settings
|
||||
- **and** the warning appears exactly once, although three settings are read from the configuration
|
||||
|
||||
##### Verified by
|
||||
|
||||
- [InitCommandTest: `--systemd warns once when the effective configuration cannot be loaded`](../../src/test/kotlin/de/hoennig/werkator/commands/InitCommandTest.kt)
|
||||
|
||||
#### Scenario#22.02: The configuration is read from the repository root
|
||||
|
||||
So that the generated host integration reflects the repository being initialized, whatever directory `init` was invoked from.
|
||||
|
||||
- **Given** a repository whose root configuration sets `server.publicBaseUrl` and `server.port`
|
||||
- **and** a current directory that is not that repository root
|
||||
- **When** `init --systemd` runs
|
||||
- **Then** the `.htaccess` and the maintenance page are generated
|
||||
- **and** the `.htaccess` proxies to the port from the root configuration
|
||||
|
||||
##### Verified by
|
||||
|
||||
- [InitCommandTest: `--systemd reads the configuration from the repository root, not the current directory`](../../src/test/kotlin/de/hoennig/werkator/commands/InitCommandTest.kt)
|
||||
|
||||
## The Solution
|
||||
|
||||
The catch-all now prints the exception message before falling back:
|
||||
|
||||
```
|
||||
Warning: the effective configuration could not be loaded (<message>)
|
||||
continuing with default server settings — check the generated unit and host files
|
||||
```
|
||||
|
||||
The configuration is read three times while the systemd files are written (`memoryMax`, `tasksMax`, `publicBaseUrl`), which would repeat the warning three times.
|
||||
It is therefore loaded once per run and cached in the command, and the cache is reset at the top of `run()` so a reused instance — the command is a Spring singleton — re-reads.
|
||||
|
||||
The repository root is passed down into the two accessors instead of `Paths.get(".")`.
|
||||
This matches every other caller of `ConfigLoader.load` in the code base, all of which pass an explicit working directory;
|
||||
`InitCommand` was the only one relying on the process's current directory.
|
||||
|
||||
Both fixes are the same failure in two forms — the command answered from a configuration it never actually read — which is why they are in one PR.
|
||||
|
||||
## Additional Changes
|
||||
|
||||
- None.
|
||||
@@ -0,0 +1,151 @@
|
||||
# RFC 0001: Web UI Redesign — the Instrument Panel
|
||||
|
||||
**Status:**
|
||||
- proposed: 2026-09-03
|
||||
- accepted: -
|
||||
- rejected: -
|
||||
|
||||
**Proposal:** The Werkator web UI adopts the **Instrument Panel** direction: a teal palette in a light and a dark mode, IBM Plex typography, a repository strip that previews the state of every served repository, a title hierarchy that names the view first and explains it second, and a tab bar at the foot that becomes the mobile navigation.
|
||||
The architecture does not change: server-rendered Thymeleaf, one `werkator.css`, one hand-written `werkator.js`, JSON polling, no framework, no frontend build pipeline.
|
||||
|
||||
## Context and Problem Statement
|
||||
|
||||
The current UI is a functional port of the legacy generated pages: a table per view, pill badges, system font, blue links.
|
||||
It is correct and calm, but it looks like every other CI page and gives no hint of the other repositories an instance serves (ADR 0009).
|
||||
The brief for this RFC was "fancy, but serious and trustworthy", with two references from the same author for visual kinship:
|
||||
|
||||
- [werkbaum.javagil.de](https://werkbaum.javagil.de/) — light paper with a fine grid, IBM Plex, a petrol accent, panel labels in small caps.
|
||||
- [javagil.de/vibe-engineering](https://javagil.de/vibe-engineering) — a dark instrument panel: ink and petrol, clay for warnings, monospaced spaced labels, a tab bar at the foot.
|
||||
|
||||
A hard constraint of this RFC is honesty towards the data.
|
||||
The mockups show only what the API delivers today; nothing is invented to make a screen look richer.
|
||||
|
||||
### What the UI Has to Work With
|
||||
|
||||
Per build row (`BuildRowView`, `BuildResultDto`): status, branch name, commit (12-character abbreviation, full id for copying), started at (`yyyy-MM-dd HH:mm`), duration (`m:ss`; a pending build shows its wait time in italics), artifact key with the artifact, permalink and live-log links, and the actions restart and delete (history has no restart).
|
||||
Statuses: `pending`, `running`, `success`, `failed`, `interrupted`, `cancelled`, plus `unknown` for a never-built branch and the client-side `finished` on a card whose build has left the current list.
|
||||
Views: Latest (one build per name), Branches (every origin branch and its latest build), History (all stored builds), Current (running builds with their live log), System (seven metric rows with current/min/max/avg, warn from 80 %, critical from 90 %), the artifact page, and the release notes.
|
||||
Live state: the indicator is `static`, `live` or `error`; the watcher banner reports `watcher stopped`, `origin unreachable` or `poll cycle failed`.
|
||||
Multi-repo: the repository switcher is a server-rendered `<select>` of names; `WatcherState.repositories` already carries a per-repository watcher state that the UI does not show.
|
||||
|
||||
What does **not** exist, and therefore appears in no mockup: a commit subject line, a typical or expected duration, an ETA, a per-branch build history, test counts on a row, and any cross-repository status summary in the API.
|
||||
|
||||
## Considered Options
|
||||
|
||||
Six directions were sketched on a shared design canvas, two rounds of three, all with the same sample rows.
|
||||
|
||||
| Option | Idea | Why | Tradeoff |
|
||||
|---|---|---|---|
|
||||
| A · Quiet Console | Today's design refined: top bar, dot-plus-word statuses, hover actions | Smallest step, everything stays valid | Least distinctive |
|
||||
| B · Mission Board | Health tiles, one card per branch with a history strip, running build with progress | Answers "is everything fine?" at a glance | Needs data the API does not have (history, typical duration) |
|
||||
| C · Ledger | Warm paper, serif masthead, hairline rules, typographic status marks | The most "serious"; reads like a signed record | Leaves the system font, needs its own dark theme |
|
||||
| D · Paper Rail | Werkbaum's paper and grid, a repository rail on the left with per-branch dots | Family resemblance to Werkbaum; other repositories visible | 250 px of table width lost; empty with one repository |
|
||||
| **E · Instrument Panel** | Vibe-Engineering's dark panel, repositories as tiles, tab bar at the foot | Reads like a control room; failures in clay stay serious without alarm | Dark-only as drawn; needs a light palette |
|
||||
| F · Fleet Overview | A new landing page with one panel per repository, ledger typography on paper | One page answers the question for the whole instance | Becomes a list beyond five repositories |
|
||||
|
||||
Round one (A–C) still contained invented data; it is kept on the canvas for the visual ideas only.
|
||||
**E was chosen**, and round three worked out what it lacked: the light mode, the ten-repository case, the title hierarchy, and the phone layout.
|
||||
|
||||
## The Design
|
||||
|
||||
### Palette
|
||||
|
||||
Both modes are CSS custom properties on `:root`, switched by `prefers-color-scheme` as today (`color-scheme: light dark`).
|
||||
Failures use clay, not red, so they stay serious without shouting; the accent is teal in both modes.
|
||||
|
||||
| Token | Dark | Light | Used for |
|
||||
|---|---|---|---|
|
||||
| bg | `#061C1F` | `#EAF4F2` | page ground |
|
||||
| panel | `#0A2A2E` | `#FFFFFF` | tables, cards, chips |
|
||||
| panel-2 | `#0F3A3D` | `#D6ECE8` | the current repository, the active tab |
|
||||
| line | `#17474B` | `#C9DFDB` | borders and rules |
|
||||
| text | `#E4EEEC` | `#0B2B2E` | body text |
|
||||
| text-2 | `#B4CBC8` | `#35595B` | timestamps |
|
||||
| muted | `#7DA19E` | `#5E8583` | labels, footers |
|
||||
| accent | `#5FD3C7` | `#0E8079` | links, success, running, the live indicator |
|
||||
| accent-2 | `#1E9A93` | `#149A90` | underlines, the current repository's border |
|
||||
| clay | `#E09070` | `#B0563B` | failed, error, delete, watcher warnings |
|
||||
| clay-2 | `#C4664A` | `#C4664A` | the border of a failing repository chip |
|
||||
| ghost | `#4A7370` | `#BFD4D1` | cancelled, interrupted, unknown |
|
||||
|
||||
Tinted rows: a running row gets 16 % (dark) or 10 % (light) of accent-2 as background, a failed row 12 % or 10 % of clay-2.
|
||||
The reference's background grid was tried and dropped: it competes with the table, especially in light mode.
|
||||
|
||||
### Typography
|
||||
|
||||
IBM Plex Sans for text, IBM Plex Mono for commits, timestamps, durations and every label.
|
||||
Labels are 10 px Mono, uppercase, letter-spaced 0.12 em, in `muted`; statuses are 11 px Mono uppercase in their status color, each preceded by an 8 px dot (outlined for pending, pulsing for running).
|
||||
Fallback stacks: `"IBM Plex Sans", "Segoe UI", system-ui, sans-serif` and `"IBM Plex Mono", ui-monospace, Consolas, monospace`.
|
||||
Whether Plex is bundled under `static/` or the fallback stack is accepted is an open question below.
|
||||
|
||||
### Anatomy of a Page (desktop)
|
||||
|
||||
1. **Header**, 52 px: logo, `Werkator` with the Gitea repository name in accent, a small label `updated HH:mm:ss`; right: the live indicator as an outlined chip with a pulsing dot, the reload button.
|
||||
2. **Repository strip**: see below.
|
||||
3. **Panel** with the view's title: the view name at 22 px semibold with a 2 px accent-2 underline, followed by a one-line label that explains it (`Latest` — `one build per branch, newest first`; `Branches` — `every origin branch and its latest build`; `History` — `all stored builds, newest first`; `System` — `instance metrics since first start`); on the right a Mono line with the row count, the last poll and the watcher state.
|
||||
4. **Table**, columns as today (Status, Branch, Commit, Started, Duration, Artifacts, Actions), rows 9 px padding on a 1 px `line` rule; copy buttons as outlined 13 px icons; artifact links and actions as stroke icons (no emoji).
|
||||
5. **Footer**: version and copyright left, the navigation as a Mono tab bar in the middle (Latest, Branches, History, System with icons; the active tab in panel-2 with an accent underline), Impressum and Privacy right.
|
||||
|
||||
### The Repository Strip
|
||||
|
||||
The `<select>` switcher is replaced by a strip below the header that shows every served repository with its state, so a failure elsewhere is visible without leaving the page.
|
||||
|
||||
- Up to about three repositories: **tiles** (220 px), each with `current` or `repo` label, the name, one dot per branch in the branch's latest status, a summary line (`6 builds · 1 failed · 1 running`), and the watcher warning in clay when that repository's watcher reports an error.
|
||||
- More repositories: **chips** (30 px), each with one dot for the worst status in the repository, the name, an optional short finding (`1 failed`, `main`, `never built`), and a warning triangle when the watcher reports an error; the current repository has an accent-2 border on panel-2, a failing one a clay-2 border on the clay tint.
|
||||
- Order is *failing first*: the current repository, then failing, running, then green; a summary line above (`10 served · 2 failing · 1 unreachable · 2 running`) and a sort control on the right.
|
||||
- The strip **scrolls**: horizontally on the phone, and on the desktop it wraps to a second row up to about ten repositories and becomes a horizontally scrollable band beyond that, with the failing chips pinned at the front so they never scroll out of view.
|
||||
- Beyond roughly twenty repositories the strip shows only the conspicuous chips (failing, running, unreachable) plus a search field for the rest.
|
||||
- With a single served repository the strip is omitted, as the switcher is today.
|
||||
|
||||
### Phone (below 680 px)
|
||||
|
||||
The existing breakpoint behaviour is kept and restyled: rows become cards with the `data-label` captions, the live indicator collapses to a dot.
|
||||
The header stacks `Werkator` over the repository name; the repository strip scrolls horizontally under its summary line; the panel title keeps its hierarchy; each card carries the status line with the branch, then commit, started and duration, then the artifact icons and the actions as 44 px targets.
|
||||
The footer tab bar becomes a fixed bottom tab bar with icons — the same four entries as on the desktop.
|
||||
No painted status bar or keyboard; the device provides those.
|
||||
|
||||
### What Is Deliberately Not in the Proposal
|
||||
|
||||
- The `DE` language button in the mockups is a leftover of the reference; the UI stays English-only.
|
||||
- No commit subjects, typical durations, ETAs or history strips: they need data the server does not have, and each would be its own RFC with its own storage.
|
||||
- No manual theme toggle; `prefers-color-scheme` decides, as today.
|
||||
|
||||
## Consequences
|
||||
|
||||
### Backend
|
||||
|
||||
- One new endpoint, `GET /api/repos`: for every served repository its name, its UI root (`/repos/<name>`), whether it is the current one, the latest status per build name (the Latest view's `latestPerName` reduced to counts, plus the worst status), and its `RepoWatcherState` (`lastFetchError`, `lastPollError`, `lastPollAt`).
|
||||
With one served repository the endpoint returns a list of one and the strip stays hidden.
|
||||
- `werkator.js` polls it on the table interval (10 s) and renders the strip; every fetch keeps the timeout and the explicit error badge.
|
||||
- `UiFormats` and `werkator.js` keep producing identical formats; the palette and the title labels are template and CSS only.
|
||||
|
||||
### Rollout, One Concern per Pull Request
|
||||
|
||||
1. Palette, typography and the title hierarchy in `werkator.css` and the fragments — no data change, both modes.
|
||||
2. Header and footer tab bar, including the phone tab bar.
|
||||
3. `GET /api/repos` and the repository strip, replacing the `<select>`.
|
||||
4. Card refinements on the phone and the System and artifact pages in the new vocabulary.
|
||||
|
||||
Each step leaves the UI usable, and the tests in `server` that assert on markup are adjusted with the step that changes it.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **Fonts:** bundle IBM Plex Sans and Mono under `static/fonts/` (about 100–150 KB in WOFF2 for the four faces), or accept the fallback stack on hosts without the font; the reference sites load Plex from a CDN, which the deployment behind a strict reverse proxy may not want.
|
||||
- **Current view:** it is reachable today only from a running row's live icon; the tab bar has room for it as a fifth entry with a count badge, or it stays a link from the row.
|
||||
- **Instance pages:** `/system` and `/releases` are instance-level; in the tab bar they sit next to the per-repository views, which the strip makes visible enough, or they move to the footer's right side.
|
||||
|
||||
## Design Sources
|
||||
|
||||
The design canvas with all eleven artboards (rounds one to three, desktop and phone) is a private Claude artifact of the author; its renderings live next to this RFC under `0001-web-ui-instrument-panel/`.
|
||||
The sample rows are real field shapes with invented values; the repositories other than `werkator` are invented.
|
||||
|
||||
The proposal:
|
||||
|
||||
- [E · dark, desktop](0001-web-ui-instrument-panel/e-dark-desktop.png) · [E · light, desktop](0001-web-ui-instrument-panel/e-light-desktop.png)
|
||||
- [E · dark, ten repositories](0001-web-ui-instrument-panel/e-dark-10-repos.png) · [E · light, ten repositories](0001-web-ui-instrument-panel/e-light-10-repos.png)
|
||||
- [E · dark, phone](0001-web-ui-instrument-panel/e-dark-phone.png) · [E · light, phone](0001-web-ui-instrument-panel/e-light-phone.png)
|
||||
|
||||
The alternatives, for the record:
|
||||
|
||||
- [A · Quiet Console](0001-web-ui-instrument-panel/a-quiet-console.png), [B · Mission Board](0001-web-ui-instrument-panel/b-mission-board.png), [C · Ledger](0001-web-ui-instrument-panel/c-ledger.png) — round one, still with invented data.
|
||||
- [D · Paper Rail](0001-web-ui-instrument-panel/d-paper-rail.png), [F · Fleet Overview](0001-web-ui-instrument-panel/f-fleet-overview.png) — round two.
|
||||
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 128 KiB |
|
After Width: | Height: | Size: 106 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 92 KiB |
|
After Width: | Height: | Size: 54 KiB |
|
After Width: | Height: | Size: 98 KiB |
|
After Width: | Height: | Size: 91 KiB |
|
After Width: | Height: | Size: 53 KiB |
|
After Width: | Height: | Size: 171 KiB |
@@ -48,6 +48,7 @@ class InitCommand(
|
||||
internal var javaExecutableResolver: () -> Path = { Paths.get(System.getProperty("java.home"), "bin", "java") }
|
||||
|
||||
override fun run() {
|
||||
cachedServerConfig = null
|
||||
val normalizedWorkingDir = workingDir.toAbsolutePath().normalize()
|
||||
val root =
|
||||
try {
|
||||
@@ -296,14 +297,31 @@ class InitCommand(
|
||||
* already loadable (re-running `init --systemd` on an installed instance); during
|
||||
* the very first bootstrap they stay unset and the defaults (no directives) apply.
|
||||
*/
|
||||
private fun loadedSystemdConfig(): de.hoennig.werkator.config.SystemdConfig = loadedServerConfig().systemd
|
||||
private fun loadedSystemdConfig(root: Path): de.hoennig.werkator.config.SystemdConfig = loadedServerConfig(root).systemd
|
||||
|
||||
private fun loadedServerConfig(): de.hoennig.werkator.config.ServerConfig =
|
||||
/** Loaded once per run, so a broken configuration is reported once and not per caller. */
|
||||
private var cachedServerConfig: de.hoennig.werkator.config.ServerConfig? = null
|
||||
|
||||
/**
|
||||
* Read from the repository root like every other file this command touches — the
|
||||
* layers sit there, not in whatever directory the process happens to run in, and
|
||||
* an applied fragment must reach the generated unit even when `init` is invoked
|
||||
* from a subdirectory.
|
||||
*
|
||||
* A configuration error here is not fatal — the units are still generated with defaults —
|
||||
* but it must not pass for "nothing configured": without the warning a broken `.werkator.yml`
|
||||
* looks exactly like an unset `publicBaseUrl` and the host integration is skipped silently.
|
||||
*/
|
||||
private fun loadedServerConfig(root: Path): de.hoennig.werkator.config.ServerConfig =
|
||||
cachedServerConfig ?: run {
|
||||
try {
|
||||
configLoader.load(Paths.get(".")).server
|
||||
} catch (_: Exception) {
|
||||
configLoader.load(root).server
|
||||
} catch (e: Exception) {
|
||||
println("Warning: the effective configuration could not be loaded (${e.message})")
|
||||
println(" continuing with default server settings — check the generated unit and host files")
|
||||
de.hoennig.werkator.config
|
||||
.ServerConfig()
|
||||
}.also { cachedServerConfig = it }
|
||||
}
|
||||
|
||||
private fun createSystemdFiles(
|
||||
@@ -327,8 +345,8 @@ class InitCommand(
|
||||
javaExecutable = javaExecutableResolver(),
|
||||
jarPath = jarPath,
|
||||
envFile = envFile,
|
||||
memoryMax = loadedSystemdConfig().memoryMax,
|
||||
tasksMax = loadedSystemdConfig().tasksMax,
|
||||
memoryMax = loadedSystemdConfig(root).memoryMax,
|
||||
tasksMax = loadedSystemdConfig(root).tasksMax,
|
||||
),
|
||||
)
|
||||
println("created ${unitFile.toFile().relativeTo(normalizedWorkingDir.toFile())}")
|
||||
@@ -351,7 +369,7 @@ class InitCommand(
|
||||
|
||||
// generated host integration like the units: only meaningful behind a web
|
||||
// frontend, so it needs a public base URL; unused elsewhere and harmless
|
||||
val server = loadedServerConfig()
|
||||
val server = loadedServerConfig(root)
|
||||
if (server.publicBaseUrl.isNotBlank()) {
|
||||
val htaccessFile = werkatorDir.resolve(SystemdServiceFiles.HTACCESS_NAME)
|
||||
htaccessFile.toFile().writeText(SystemdServiceFiles.htaccessContent(server.port))
|
||||
|
||||
@@ -505,6 +505,8 @@ class BuildExecutorTest : FunSpec() {
|
||||
}
|
||||
|
||||
test("with maxConcurrent 1 a second branch stays PENDING until the first finished") {
|
||||
// branch-a blocks on a gate file the test creates, so the PENDING assertion
|
||||
// below cannot race the first build finishing on a loaded machine
|
||||
val h =
|
||||
Harness(
|
||||
"""
|
||||
@@ -512,7 +514,7 @@ class BuildExecutorTest : FunSpec() {
|
||||
maxConcurrent: 1
|
||||
branches:
|
||||
branch-a:
|
||||
buildCommand: "sleep 1"
|
||||
buildCommand: "until [ -f gate ]; do sleep 0.05; done"
|
||||
cleanCommand: ""
|
||||
branch-b:
|
||||
buildCommand: "echo ok"
|
||||
@@ -523,8 +525,13 @@ class BuildExecutorTest : FunSpec() {
|
||||
h.executor.startBuild(h.repo, "branch-a", "sha-a")
|
||||
h.executor.startBuild(h.repo, "branch-b", "sha-b")
|
||||
|
||||
eventually(30.seconds) {
|
||||
h.repository.latestFor("branch-a")?.status shouldBe BuildStatus.RUNNING
|
||||
}
|
||||
h.repository.latestFor("branch-b")?.status shouldBe BuildStatus.PENDING
|
||||
|
||||
Files.createFile(h.workingDir.resolve("gate"))
|
||||
|
||||
awaitStatus(h, "branch-b", BuildStatus.SUCCESS)
|
||||
awaitStatus(h, "branch-a", BuildStatus.SUCCESS)
|
||||
val transitions = h.events.map { it.result.branch to it.result.status }
|
||||
|
||||
@@ -252,5 +252,50 @@ class InitCommandTest : FunSpec() {
|
||||
// This should not throw IllegalArgumentException
|
||||
initCommand.run()
|
||||
}
|
||||
|
||||
test("--systemd reads the configuration from the repository root, not the current directory") {
|
||||
val tempDir = Files.createTempDirectory("werkator-init-test")
|
||||
// written before the run, so `init` keeps it instead of creating a template
|
||||
tempDir.resolve(".werkator.yml").toFile().writeText(
|
||||
"server:\n publicBaseUrl: \"https://werkator.example.org/\"\n port: 18099\n",
|
||||
)
|
||||
initCommand.workingDir = tempDir
|
||||
initCommand.systemd = true
|
||||
initCommand.jarPathResolver = { Paths.get("/home/ci/bin/werkator.jar") }
|
||||
initCommand.javaExecutableResolver = { Paths.get("/usr/bin/java") }
|
||||
|
||||
every { gitService.getTopLevel(tempDir) } returns tempDir
|
||||
every { gitService.getOriginUrl(tempDir) } returns "https://git.example.org/my-org/my-repo.git"
|
||||
|
||||
initCommand.run()
|
||||
|
||||
// the host integration is generated only when the root's config has a public base URL
|
||||
val htaccess = tempDir.resolve(".git/werkator/${SystemdServiceFiles.HTACCESS_NAME}")
|
||||
htaccess.toFile().shouldExist()
|
||||
htaccess.toFile().readText() shouldContain "18099"
|
||||
tempDir.resolve(".git/werkator/${SystemdServiceFiles.MAINTENANCE_PAGE_NAME}").toFile().shouldExist()
|
||||
}
|
||||
|
||||
test("--systemd warns once when the effective configuration cannot be loaded") {
|
||||
val tempDir = Files.createTempDirectory("werkator-init-test")
|
||||
val brokenLoader = mockk<de.hoennig.werkator.config.ConfigLoader>()
|
||||
every { brokenLoader.load(any()) } throws IllegalStateException("gitea.owner is required")
|
||||
val command = InitCommand(gitService, brokenLoader)
|
||||
command.workingDir = tempDir
|
||||
command.systemd = true
|
||||
command.jarPathResolver = { Paths.get("/home/ci/bin/werkator.jar") }
|
||||
command.javaExecutableResolver = { Paths.get("/usr/bin/java") }
|
||||
|
||||
every { gitService.getTopLevel(tempDir) } returns tempDir
|
||||
every { gitService.getOriginUrl(tempDir) } returns "https://git.example.org/my-org/my-repo.git"
|
||||
|
||||
val console = captureConsole { command.run() }
|
||||
|
||||
console.stdout shouldContain "gitea.owner is required"
|
||||
// the three readers of the configuration must not repeat the warning
|
||||
console.stdout.windowed("Warning:".length).count { it == "Warning:" } shouldBe 1
|
||||
// the units are still written with the defaults
|
||||
tempDir.resolve(".git/werkator/${SystemdServiceFiles.unitName(tempDir)}").toFile().shouldExist()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,7 +44,7 @@
|
||||
# Optional in the env file:
|
||||
# WERKATOR_INIT_CONFIG the init fragment to apply (repo-init, instance-start)
|
||||
# WERKATOR_REPO_URL https clone URL of the watched repository
|
||||
# (default: https://github.com/mhoennig/werkator.git)
|
||||
# (default: https://git.javagil.de/mi/werkator.git)
|
||||
# WERKATOR_REPO_DIR directory of the watched repository, absolute or relative to
|
||||
# WERKATOR_PATH (default: werkator); it also names the systemd
|
||||
# unit, exactly as `init --systemd` derives it
|
||||
@@ -122,7 +122,7 @@ require_env WERKATOR_REMOTE WERKATOR_PATH
|
||||
HOST="$WERKATOR_REMOTE"
|
||||
TARGET_DIR="$WERKATOR_PATH"
|
||||
ROOTFS="${WERKATOR_ROOTFS:-$REPO_ROOT/build/werkator-buildenv-trixie-java-go-node.tar.zst}"
|
||||
REPO_URL="${WERKATOR_REPO_URL:-https://github.com/mhoennig/werkator.git}"
|
||||
REPO_URL="${WERKATOR_REPO_URL:-https://git.javagil.de/mi/werkator.git}"
|
||||
|
||||
# The host layout is three values, not one convention: an installation that grew
|
||||
# before this script existed puts them elsewhere, and the defaults are exactly what
|
||||
|
||||