Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
95564587d3 | ||
|
|
618a2acb9f | ||
|
|
2ffc14a20c | ||
|
|
b5df75f844 | ||
|
|
11f6f9bd26 |
@@ -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
|
||||
|
||||
@@ -184,6 +184,11 @@ ssh <user>@<host>
|
||||
The tarball unpacks to a `werkator/` directory, so it must not be extracted over `~/opt` directly — unpack it in `/tmp` and move it into place, as above.
|
||||
Rollback is the reverse: stop, remove the new directory (or jar), move `.bak` back, start.
|
||||
|
||||
`tools/remote --env-file .env.<instance> werkator instance-update` does the same sequence for any host, not only the webspace layout it was written for.
|
||||
Three optional keys in the env file name what differs (see the script's header): `WERKATOR_REPO_DIR` (directory of the watched repository, which also names the systemd unit), `WERKATOR_INSTALL_DIR` (where the runtime bundle is unpacked), and `WERKATOR_SANDBOX` (`bwrap`, the default, or `docker` — a Docker host has no werkdock binary and no rootfs archive to upload).
|
||||
Their defaults are the layout `instance-install` creates, so an env file that names none of them behaves exactly as before.
|
||||
The upload happens before the service is stopped and every artifact is checksum-verified after the transfer, so a dropped connection costs the transfer and not the running service.
|
||||
|
||||
Then check `https://<public-url>/` for the new version in the footer, and `journalctl --user -u werkator-<repo-name>.service -n 50` for a clean start.
|
||||
Config file changes are not needed for an update; new keys take their defaults.
|
||||
|
||||
@@ -335,6 +340,7 @@ tools/remote --env-file .env.mih34 port-forward start # browser tunne
|
||||
```
|
||||
|
||||
Layout on the host: the watched repository at `$WERKATOR_PATH/werkator/`, the unpacked runtime at `$WERKATOR_PATH/.werkator/werkator/`, the werkdock binary at `$WERKATOR_PATH/.werkator/bin/werkdock`.
|
||||
That is the default, not a requirement: `WERKATOR_REPO_DIR`, `WERKATOR_INSTALL_DIR` and `WERKATOR_SANDBOX` bend it to an installation that predates the script, see [Updating an Existing Installation](#updating-an-existing-installation).
|
||||
The rootfs archive is loaded once per source into werkdock's image store (`~/.werkdock`), shared by every repository of the user.
|
||||
Fill `git.account`/`git.token` in the machine config when the origin is private, and make the user's services survive logout with `loginctl enable-linger`.
|
||||
|
||||
|
||||
@@ -0,0 +1,98 @@
|
||||
> **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
|
||||
|
||||
`tools/remote` grew with the Hostsharing Managed Webspace rollout (steps 21 and 23) and encoded that rollout's layout as if it were the only one.
|
||||
The watched repository had to be `$WERKATOR_PATH/werkator`, the runtime `$WERKATOR_PATH/.werkator/werkator`, and the systemd unit was spelled out as `werkator-werkator.service`; a werkdock binary and a bwrap rootfs archive were built and uploaded unconditionally.
|
||||
`vm4006`, the Docker host that has been running Werkator since long before the script existed, matches none of that: its watched repository is `~/hs.hsadmin.ng` (so its unit is `werkator-hs.hsadmin.ng.service`), its runtime lives in `~/opt/werkator`, and it has neither werkdock nor a rootfs because its builds run in Docker.
|
||||
So that host could only be deployed by hand, and had drifted nine releases behind — the question that started this PR was whether the Werkdock and multi-repo work had broken it, which it had not: what was broken was the deployment tooling.
|
||||
|
||||
The second problem surfaced while deploying: `instance-update` stopped the systemd unit and *then* started the upload.
|
||||
The 66 MB transfer to `vm4006` died with `scp: Connection closed`, leaving the host with no running Werkator and nothing new to start.
|
||||
|
||||
## Non-Goals
|
||||
|
||||
- `instance-start` for a Docker host: it places a Hostsharing `.htaccess` into a `doms/<domain>/` docroot, which only exists on a Managed Webspace. `vm4006` uses the managed nginx container instead, and keeps its existing units.
|
||||
- `repo-init` for `vm4006` — the repository has been cloned and configured there for months; only `instance-update` was needed.
|
||||
- Making the machine configuration of `vm4006` current: it still carries the pre-rename `gitTally:` meta key, which is read by nothing today. Harmless while `ConfigVersions.FORMAT_BROKE_IN` is empty, a trap on the day it is not.
|
||||
|
||||
## The Scenarios
|
||||
|
||||
### Feature: one deployment command for every host layout
|
||||
|
||||
#### Background
|
||||
|
||||
- The env file carries transport values only; the three new keys describe *where* things are on the host, not what Werkator does.
|
||||
- The defaults are exactly the layout `instance-install` creates, so an env file naming none of them resolves as before.
|
||||
|
||||
#### Scenario#18.01: A host that predates the script can be deployed with it
|
||||
|
||||
So that an installation is not condemned to hand-typed `scp` sequences because it was set up before the tooling existed.
|
||||
|
||||
- **Given** an env file with `WERKATOR_REPO_DIR=hs.hsadmin.ng`, `WERKATOR_INSTALL_DIR=/home/tallyman/opt` and `WERKATOR_SANDBOX=docker`
|
||||
- **When** `tools/remote --env-file .env.vm4006 werkator instance-update` runs
|
||||
- **Then** it addresses `werkator-hs.hsadmin.ng.service`, unpacks into `~/opt`, and uploads neither a werkdock binary nor a rootfs archive
|
||||
|
||||
##### Verified by
|
||||
|
||||
- Live on `vm4006` (2026-09-03): `check-prerequisites` reported the Docker daemon instead of running `werkdock doctor`; the update swapped `~/opt/werkator` from v1.0.1 to **v1.1.2**, the unit came up `active`, `/api/watcher` polls without errors, and `/api/system` still reports real disk figures (`diskSource.kind: volume`) rather than the quota shape from PR#16.
|
||||
|
||||
#### Scenario#18.02: The webspace hosts are unaffected
|
||||
|
||||
So that making the script layout-aware does not break the deployment path that is actually in production.
|
||||
|
||||
- **Given** `.env.mih09` and `.env.mih34`, neither naming any of the new keys
|
||||
- **When** the layout is resolved
|
||||
- **Then** repository directory, install directory, unit name and sandbox are identical to the hardcoded values they replace
|
||||
|
||||
##### Verified by
|
||||
|
||||
- Resolution measured for both env files (2026-09-03): `REPO_DIR=$WERKATOR_PATH/werkator`, `INSTALL_DIR=$WERKATOR_PATH/.werkator`, `UNIT=werkator-werkator.service`, `SANDBOX=bwrap`.
|
||||
- Live on `mih09` (2026-09-03): a full `instance-update` ran through the bwrap path — werkdock uploaded, `werkdock 0.1.0-dev` reported after the swap, service `active`, `https://werkator.javagil.de/` answering 200.
|
||||
|
||||
#### Scenario#18.03: A failed transfer does not take the service down
|
||||
|
||||
So that a dropped connection costs the upload and nothing else.
|
||||
|
||||
- **Given** an instance whose service is running
|
||||
- **When** the runtime bundle cannot be transferred
|
||||
- **Then** the service is still running, because the upload happens before the stop, and a partially transferred file is never moved into place
|
||||
|
||||
##### Verified by
|
||||
|
||||
- The failure itself on `vm4006` (2026-09-03), which is what this scenario is written from: with the old order, `scp: Connection closed` left the unit stopped and the host without Werkator.
|
||||
- Live on `mih09` (2026-09-03): the bundle already on the host was recognised by its sha256 and skipped, so the upload step cost nothing and the stop followed only after it.
|
||||
|
||||
## The Solution
|
||||
|
||||
Three optional env keys replace three hardcoded assumptions.
|
||||
`WERKATOR_REPO_DIR` and `WERKATOR_INSTALL_DIR` are resolved absolute-or-relative-to-`WERKATOR_PATH`, and the unit name is now *derived* from the repository directory the way `SystemdServiceFiles.unitName` derives it (basename, every character outside `[A-Za-z0-9_.-]` replaced by a dash) instead of being spelled out — one rule, in two places, with the script naming the Kotlin function it mirrors.
|
||||
`WERKATOR_SANDBOX=docker` skips everything bwrap-shaped: no werkdock build, no werkdock upload, no rootfs archive, and `check-prerequisites` asks the Docker daemon instead of running `werkdock doctor`.
|
||||
`repo-add` clones beside the watched repository rather than into `WERKATOR_PATH`, which is the same directory whenever the default layout is used.
|
||||
|
||||
`deploy_instance` is split into `upload_instance_artifacts` and `swap_instance_runtime`, and `instance_update` calls the first *before* stopping the unit.
|
||||
Every artifact is uploaded to `<name>.part`, compared by sha256 with the local file, and only then moved into place; three attempts, and an unchanged artifact is skipped entirely.
|
||||
The checksum is not belt-and-braces: a truncated tarball would unpack into a runtime that starts and misbehaves, which is far worse than the failed transfer it came from.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **Should `instance-start` learn the Docker-host shape too?** Not answered here. `vm4006` keeps its existing units and its managed nginx; the day it needs regenerating, `init --systemd` on the host is the documented path.
|
||||
|
||||
## Additional Changes
|
||||
|
||||
- None beyond the feature itself.
|
||||
|
||||
## Follow-up work discovered while deploying this
|
||||
|
||||
- `https://vm4006.hostsharing.net:8443/` is not reachable from outside the host, while `https://127.0.0.1:8443/` answers 200 and the `werkator-nginx-hs.hsadmin.ng` container publishes both ports. Pre-existing and unrelated to this PR — the host's own firewall, not Werkator.
|
||||
- The machine configuration on `vm4006` still declares `gitTally: version: since: "0.9.20"`; the current code reads only `werkator:`, so the file's version claim is silently ignored (`werkator.version.since` prints empty). It costs nothing while `ConfigVersions.FORMAT_BROKE_IN` is `""`, and stops protecting that host the moment plan step 18 sets it.
|
||||
|
||||
## Prerequisite PRs
|
||||
|
||||
- None; it changes only `tools/remote` and the documentation.
|
||||
|
||||
## Follow-up PRs
|
||||
|
||||
- None planned.
|
||||
@@ -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 |
@@ -45,13 +45,22 @@
|
||||
# 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)
|
||||
# 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
|
||||
# WERKATOR_INSTALL_DIR directory holding the unpacked runtime bundle, absolute or
|
||||
# relative to WERKATOR_PATH (default: .werkator)
|
||||
# WERKATOR_SANDBOX build runtime of the host: bwrap (default) or docker; a docker
|
||||
# host needs neither the werkdock binary nor a rootfs archive
|
||||
# WERKDOCK_REPO checkout of the werkdock repository, whose binary the
|
||||
# instance runs (default: <repo>/../werkdock)
|
||||
# WERKDOCK_BINARY the built werkdock binary (default: $WERKDOCK_REPO/dist/werkdock)
|
||||
# WERKATOR_ROOTFS rootfs archive path for repo-init
|
||||
# (default: <repo>/build/werkator-buildenv-trixie-java-go-node.tar.zst)
|
||||
#
|
||||
# Install layout on the host:
|
||||
# Install layout on the host — the default, which the three keys above bend to an
|
||||
# installation that predates this script (e.g. the docker host vm4006: the watched
|
||||
# repository is ~/hs.hsadmin.ng, the runtime lives in ~/opt, there is no werkdock):
|
||||
# $WERKATOR_PATH/werkator/ the watched repository (clone)
|
||||
# $WERKATOR_PATH/.werkator/werkator/ the unpacked runtime bundle
|
||||
# $WERKATOR_PATH/.werkator/bin/ the werkdock binary
|
||||
@@ -113,9 +122,33 @@ 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}"
|
||||
MACHINE_CONFIG="$TARGET_DIR/werkator/.git/werkator/.werkator.yml"
|
||||
WERKATOR_BIN="$TARGET_DIR/.werkator/werkator/bin/werkator"
|
||||
UNIT="werkator-werkator.service"
|
||||
|
||||
# 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
|
||||
# `instance-install` creates, so an env file that names none of them behaves as before.
|
||||
# Both directories may be absolute; a bare name is taken relative to WERKATOR_PATH.
|
||||
resolve_dir() {
|
||||
case "$1" in
|
||||
/*) echo "$1" ;;
|
||||
*) echo "$TARGET_DIR/$1" ;;
|
||||
esac
|
||||
}
|
||||
REPO_DIR="$(resolve_dir "${WERKATOR_REPO_DIR:-werkator}")"
|
||||
INSTALL_DIR="$(resolve_dir "${WERKATOR_INSTALL_DIR:-.werkator}")"
|
||||
# where `repo-add` puts a further repository of the registry: beside the watched one
|
||||
SIBLING_DIR="$(dirname "$REPO_DIR")"
|
||||
SANDBOX="${WERKATOR_SANDBOX:-bwrap}"
|
||||
case "$SANDBOX" in
|
||||
bwrap|docker) ;;
|
||||
*) die "WERKATOR_SANDBOX is 'bwrap' or 'docker', not '$SANDBOX'" ;;
|
||||
esac
|
||||
|
||||
MACHINE_CONFIG="$REPO_DIR/.git/werkator/.werkator.yml"
|
||||
WERKATOR_BIN="$INSTALL_DIR/werkator/bin/werkator"
|
||||
# mirrors SystemdServiceFiles.unitName: the repository's directory name, every
|
||||
# character outside [A-Za-z0-9_.-] replaced by a dash — the unit `init --systemd`
|
||||
# writes, which is the one this script may stop and start.
|
||||
UNIT="werkator-$(basename "$REPO_DIR" | sed 's/[^A-Za-z0-9_.-]/-/g').service"
|
||||
|
||||
ssh_present() {
|
||||
ssh -o BatchMode=yes -o ConnectTimeout=10 "$HOST" true 2>/dev/null
|
||||
@@ -133,13 +166,21 @@ ensure_ssh() {
|
||||
|
||||
# Werkdock owns the host checks (`werkdock doctor` ports the old prerequisites
|
||||
# script); the binary is uploaded first, so the check works pre-install.
|
||||
# On a docker host there is no werkdock and no sandbox to check: the build runtime
|
||||
# is the docker daemon, so the check is that the daemon answers this user.
|
||||
check_prerequisites() {
|
||||
if [ "$SANDBOX" = "docker" ]; then
|
||||
echo "==> Checking the docker build runtime on $HOST (WERKATOR_SANDBOX=docker)"
|
||||
ssh "$HOST" "docker info >/dev/null" || die "docker is not usable by this user on $HOST"
|
||||
ssh "$HOST" "docker --version"
|
||||
return 0
|
||||
fi
|
||||
ensure_werkdock_binary
|
||||
echo "==> Uploading werkdock and running its doctor on $HOST (target dir: $TARGET_DIR)"
|
||||
ssh "$HOST" "mkdir -p '$TARGET_DIR/.werkator/bin'"
|
||||
scp -q "$WERKDOCK_BINARY" "$HOST:$TARGET_DIR/.werkator/bin/werkdock.new"
|
||||
ssh "$HOST" "mv '$TARGET_DIR/.werkator/bin/werkdock.new' '$TARGET_DIR/.werkator/bin/werkdock' && chmod 755 '$TARGET_DIR/.werkator/bin/werkdock'"
|
||||
if ! ssh "$HOST" "'$TARGET_DIR/.werkator/bin/werkdock' doctor '$TARGET_DIR'"; then
|
||||
ssh "$HOST" "mkdir -p '$INSTALL_DIR/bin'"
|
||||
scp -q "$WERKDOCK_BINARY" "$HOST:$INSTALL_DIR/bin/werkdock.new"
|
||||
ssh "$HOST" "mv '$INSTALL_DIR/bin/werkdock.new' '$INSTALL_DIR/bin/werkdock' && chmod 755 '$INSTALL_DIR/bin/werkdock'"
|
||||
if ! ssh "$HOST" "'$INSTALL_DIR/bin/werkdock' doctor '$TARGET_DIR'"; then
|
||||
die "werkdock doctor failed on $HOST — install aborted"
|
||||
fi
|
||||
}
|
||||
@@ -160,7 +201,7 @@ next to this repository, or point WERKDOCK_REPO/WERKDOCK_BINARY at your checkout
|
||||
upload_fragment() {
|
||||
[ -n "${WERKATOR_INIT_CONFIG:-}" ] || { echo ""; return 0; }
|
||||
[ -f "$WERKATOR_INIT_CONFIG" ] || die "init fragment missing: $WERKATOR_INIT_CONFIG"
|
||||
local remote="$TARGET_DIR/.werkator/$(basename "$WERKATOR_INIT_CONFIG")"
|
||||
local remote="$INSTALL_DIR/$(basename "$WERKATOR_INIT_CONFIG")"
|
||||
scp -q "$WERKATOR_INIT_CONFIG" "$HOST:$remote"
|
||||
echo "$remote"
|
||||
}
|
||||
@@ -174,43 +215,85 @@ ensure_instance_artifacts() {
|
||||
(cd "$REPO_ROOT" && ./gradlew runtimeBundle --console=plain -q)
|
||||
fi
|
||||
[ -f "$RUNTIME_BUNDLE" ] || die "runtime bundle missing: $RUNTIME_BUNDLE"
|
||||
ensure_werkdock_binary
|
||||
[ "$SANDBOX" = "docker" ] || ensure_werkdock_binary
|
||||
}
|
||||
|
||||
# Uploads and unpacks the instance artifacts. The previous runtime stays as
|
||||
# werkator.prev for one deployment as the rollback asset.
|
||||
deploy_instance() {
|
||||
echo "==> Uploading runtime bundle and werkdock binary"
|
||||
ssh "$HOST" "mkdir -p '$TARGET_DIR/.werkator/bin'"
|
||||
scp -q "$RUNTIME_BUNDLE" "$HOST:$TARGET_DIR/.werkator/"
|
||||
scp -q "$WERKDOCK_BINARY" "$HOST:$TARGET_DIR/.werkator/bin/werkdock.new"
|
||||
# Uploads one file and verifies it arrived whole: a transfer that dies mid-way
|
||||
# (scp: Connection closed) otherwise leaves a truncated archive that unpacks into
|
||||
# a broken runtime. Retries twice, because a dropped WAN connection is not a reason
|
||||
# to abort a deployment.
|
||||
upload_verified() {
|
||||
local src="$1" dest="$2"
|
||||
local local_sha remote_sha attempt
|
||||
local_sha="$(sha256sum "$src" | cut -d' ' -f1)"
|
||||
remote_sha="$(ssh "$HOST" "sha256sum '$dest' 2>/dev/null | cut -d' ' -f1" || true)"
|
||||
if [ "$local_sha" = "$remote_sha" ]; then
|
||||
echo " $(basename "$src"): already on the host, skipping"
|
||||
return 0
|
||||
fi
|
||||
for attempt in 1 2 3; do
|
||||
if scp -q "$src" "$HOST:$dest.part"; then
|
||||
remote_sha="$(ssh "$HOST" "sha256sum '$dest.part' 2>/dev/null | cut -d' ' -f1" || true)"
|
||||
if [ "$local_sha" = "$remote_sha" ]; then
|
||||
ssh "$HOST" "mv '$dest.part' '$dest'"
|
||||
return 0
|
||||
fi
|
||||
echo " checksum mismatch after transfer $attempt of $(basename "$src")" >&2
|
||||
else
|
||||
echo " transfer $attempt of $(basename "$src") failed" >&2
|
||||
fi
|
||||
done
|
||||
ssh "$HOST" "rm -f '$dest.part'" || true
|
||||
die "cannot upload $src to $HOST:$dest — three attempts failed"
|
||||
}
|
||||
|
||||
# Uploads the instance artifacts, without touching the installed runtime: the
|
||||
# service keeps running until swap_instance_runtime replaces it, so a failed
|
||||
# transfer costs nothing but the transfer.
|
||||
upload_instance_artifacts() {
|
||||
if [ "$SANDBOX" = "docker" ]; then
|
||||
echo "==> Uploading runtime bundle"
|
||||
else
|
||||
echo "==> Uploading runtime bundle and werkdock binary"
|
||||
fi
|
||||
ssh "$HOST" "mkdir -p '$INSTALL_DIR/bin'"
|
||||
upload_verified "$RUNTIME_BUNDLE" "$INSTALL_DIR/$(basename "$RUNTIME_BUNDLE")"
|
||||
if [ "$SANDBOX" != "docker" ]; then
|
||||
upload_verified "$WERKDOCK_BINARY" "$INSTALL_DIR/bin/werkdock.new"
|
||||
fi
|
||||
}
|
||||
|
||||
# Swaps in the uploaded artifacts. The previous runtime stays as werkator.prev
|
||||
# for one deployment as the rollback asset.
|
||||
swap_instance_runtime() {
|
||||
echo "==> Unpacking"
|
||||
ssh "$HOST" "set -e
|
||||
cd '$TARGET_DIR/.werkator'
|
||||
mv bin/werkdock.new bin/werkdock && chmod 755 bin/werkdock
|
||||
cd '$INSTALL_DIR'
|
||||
[ ! -f bin/werkdock.new ] || { mv bin/werkdock.new bin/werkdock && chmod 755 bin/werkdock; }
|
||||
rm -rf werkator.prev
|
||||
[ ! -d werkator ] || mv werkator werkator.prev
|
||||
tar xzf '$(basename "$RUNTIME_BUNDLE")'
|
||||
'./werkator/bin/werkator' --version
|
||||
'./bin/werkdock' version"
|
||||
[ ! -x bin/werkdock ] || './bin/werkdock' version"
|
||||
}
|
||||
|
||||
instance_install() {
|
||||
ensure_ssh
|
||||
check_prerequisites
|
||||
ensure_instance_artifacts
|
||||
deploy_instance
|
||||
upload_instance_artifacts
|
||||
swap_instance_runtime
|
||||
echo
|
||||
echo "==> Instance installed."
|
||||
echo " Runtime: $WERKATOR_BIN"
|
||||
echo " werkdock: $TARGET_DIR/.werkator/bin/werkdock"
|
||||
[ "$SANDBOX" = "docker" ] || echo " werkdock: $INSTALL_DIR/bin/werkdock"
|
||||
echo " Next: tools/remote werkator repo-init, then instance-start"
|
||||
}
|
||||
|
||||
# Refuse to swap the runtime under a running build; FORCE=1 overrides.
|
||||
require_idle() {
|
||||
local port
|
||||
port="$(ssh "$HOST" "cd '$TARGET_DIR/werkator' 2>/dev/null && '$WERKATOR_BIN' config:print 2>/dev/null" | awk '/^server:/{f=1;next} f && /^ port:/{print $2; exit}' | tr -d '"' || true)"
|
||||
port="$(ssh "$HOST" "cd '$REPO_DIR' 2>/dev/null && '$WERKATOR_BIN' config:print 2>/dev/null" | awk '/^server:/{f=1;next} f && /^ port:/{print $2; exit}' | tr -d '"' || true)"
|
||||
[ -n "$port" ] || return 0
|
||||
local current
|
||||
current="$(ssh "$HOST" "curl -s --max-time 5 http://127.0.0.1:$port/api/builds/current" || true)"
|
||||
@@ -224,6 +307,9 @@ instance_update() {
|
||||
ensure_ssh
|
||||
ensure_instance_artifacts
|
||||
require_idle
|
||||
# upload first, stop second: a transfer that fails must not leave the host
|
||||
# without a running service (measured on vm4006, 2026-09-03)
|
||||
upload_instance_artifacts
|
||||
local was_active=0
|
||||
if ssh "$HOST" "XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user is-active --quiet '$UNIT'"; then
|
||||
was_active=1
|
||||
@@ -232,7 +318,7 @@ instance_update() {
|
||||
echo "==> Stopping $UNIT"
|
||||
ssh "$HOST" "XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user stop '$UNIT'"
|
||||
fi
|
||||
deploy_instance
|
||||
swap_instance_runtime
|
||||
if [ "$was_active" = "1" ]; then
|
||||
echo "==> Starting $UNIT"
|
||||
ssh "$HOST" "XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user start '$UNIT' && sleep 3 && systemctl --user is-active '$UNIT'"
|
||||
@@ -249,40 +335,45 @@ instance_update() {
|
||||
# writing is init's — this script transports and invokes (step 23).
|
||||
repo_init() {
|
||||
ensure_ssh
|
||||
[ -f "$ROOTFS" ] || die "rootfs archive missing: $ROOTFS — build it with tools/build-bwrap-rootfs.sh or set WERKATOR_ROOTFS"
|
||||
[ "$SANDBOX" = "docker" ] || [ -f "$ROOTFS" ] ||
|
||||
die "rootfs archive missing: $ROOTFS — build it with tools/build-bwrap-rootfs.sh or set WERKATOR_ROOTFS"
|
||||
ssh "$HOST" "test -x '$WERKATOR_BIN'" || die "no instance on $HOST — run instance-install first"
|
||||
|
||||
echo "==> Cloning the watched repository"
|
||||
if ssh "$HOST" "test -d '$TARGET_DIR/werkator/.git'"; then
|
||||
if ssh "$HOST" "test -d '$REPO_DIR/.git'"; then
|
||||
echo " (already cloned, skipping)"
|
||||
else
|
||||
ssh "$HOST" "git clone '$REPO_URL' '$TARGET_DIR/werkator'"
|
||||
ssh "$HOST" "git clone '$REPO_URL' '$REPO_DIR'"
|
||||
fi
|
||||
|
||||
echo "==> Uploading the rootfs archive (skipped when unchanged)"
|
||||
local rootfs_remote="$TARGET_DIR/.werkator/$(basename "$ROOTFS")"
|
||||
local local_sha remote_sha
|
||||
local_sha="$(sha256sum "$ROOTFS" | cut -d' ' -f1)"
|
||||
remote_sha="$(ssh "$HOST" "sha256sum '$rootfs_remote' 2>/dev/null | cut -d' ' -f1" || true)"
|
||||
if [ "$local_sha" = "$remote_sha" ]; then
|
||||
echo " (already on the host, skipping)"
|
||||
if [ "$SANDBOX" = "docker" ]; then
|
||||
echo "==> No rootfs needed (WERKATOR_SANDBOX=docker) — the build image is the repository's own Dockerfile"
|
||||
else
|
||||
scp -q "$ROOTFS" "$HOST:$rootfs_remote"
|
||||
remote_sha="$(ssh "$HOST" "sha256sum '$rootfs_remote' | cut -d' ' -f1")"
|
||||
[ "$local_sha" = "$remote_sha" ] || die "rootfs upload checksum mismatch"
|
||||
echo "==> Uploading the rootfs archive (skipped when unchanged)"
|
||||
local rootfs_remote="$INSTALL_DIR/$(basename "$ROOTFS")"
|
||||
local local_sha remote_sha
|
||||
local_sha="$(sha256sum "$ROOTFS" | cut -d' ' -f1)"
|
||||
remote_sha="$(ssh "$HOST" "sha256sum '$rootfs_remote' 2>/dev/null | cut -d' ' -f1" || true)"
|
||||
if [ "$local_sha" = "$remote_sha" ]; then
|
||||
echo " (already on the host, skipping)"
|
||||
else
|
||||
scp -q "$ROOTFS" "$HOST:$rootfs_remote"
|
||||
remote_sha="$(ssh "$HOST" "sha256sum '$rootfs_remote' | cut -d' ' -f1")"
|
||||
[ "$local_sha" = "$remote_sha" ] || die "rootfs upload checksum mismatch"
|
||||
fi
|
||||
fi
|
||||
|
||||
echo "==> Running werkator init${WERKATOR_INIT_CONFIG:+ --apply $(basename "${WERKATOR_INIT_CONFIG}")}"
|
||||
local fragment_remote
|
||||
fragment_remote="$(upload_fragment)"
|
||||
ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'}"
|
||||
ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'}"
|
||||
|
||||
echo "==> Verifying the effective configuration"
|
||||
ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' config:print 2>/dev/null | grep -A4 'bwrap:' | head -5"
|
||||
ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' config:print 2>/dev/null | grep -A4 '$SANDBOX:' | head -5"
|
||||
|
||||
echo
|
||||
echo "==> Repository ready."
|
||||
echo " Repo: $TARGET_DIR/werkator"
|
||||
echo " Repo: $REPO_DIR"
|
||||
echo " Next: fill git.account/git.token in $MACHINE_CONFIG if the origin is private,"
|
||||
echo " then tools/remote werkator instance-start"
|
||||
}
|
||||
@@ -306,10 +397,10 @@ repo_add() {
|
||||
ssh "$HOST" "test -x '$WERKATOR_BIN'" || die "no instance on $HOST — run instance-install first"
|
||||
|
||||
echo "==> Cloning $url as '$name'"
|
||||
if ssh "$HOST" "test -d '$TARGET_DIR/$name/.git'"; then
|
||||
if ssh "$HOST" "test -d '$SIBLING_DIR/$name/.git'"; then
|
||||
echo " (already cloned, skipping)"
|
||||
else
|
||||
ssh "$HOST" "git clone '$url' '$TARGET_DIR/$name'"
|
||||
ssh "$HOST" "git clone '$url' '$SIBLING_DIR/$name'"
|
||||
fi
|
||||
|
||||
# The instance fragment carries the sandbox policy (bwrap rootfs and werkdock
|
||||
@@ -318,26 +409,26 @@ repo_add() {
|
||||
echo "==> Running werkator init in $name${WERKATOR_INIT_CONFIG:+ --apply $(basename "${WERKATOR_INIT_CONFIG}")}"
|
||||
local fragment_remote
|
||||
fragment_remote="$(upload_fragment)"
|
||||
ssh "$HOST" "cd '$TARGET_DIR/$name' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'}"
|
||||
ssh "$HOST" "cd '$SIBLING_DIR/$name' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'}"
|
||||
|
||||
echo "==> Checking the registry"
|
||||
# Grepped locally: the entry may name the path absolute or as ~/<name>, and
|
||||
# matching both is easier without a second layer of remote shell quoting.
|
||||
if ssh "$HOST" "cat ~/.werkator.yml 2>/dev/null" |
|
||||
grep -qE "path: *(~|$TARGET_DIR)/$name[[:space:]]*$"; then
|
||||
grep -qE "path: *(~|$SIBLING_DIR)/$name[[:space:]]*$"; then
|
||||
echo " (~/.werkator.yml already names this path)"
|
||||
else
|
||||
echo " not registered yet — add this entry to ~/.werkator.yml on $HOST:"
|
||||
echo
|
||||
echo " repositories:"
|
||||
echo " - path: $TARGET_DIR/$name"
|
||||
echo " - path: $SIBLING_DIR/$name"
|
||||
echo " name: $name"
|
||||
echo
|
||||
fi
|
||||
|
||||
echo "==> Repository prepared."
|
||||
echo " Repo: $TARGET_DIR/$name"
|
||||
echo " Next: fill git.account/git.token in $TARGET_DIR/$name/.git/werkator/.werkator.yml if the origin is private"
|
||||
echo " Repo: $SIBLING_DIR/$name"
|
||||
echo " Next: fill git.account/git.token in $SIBLING_DIR/$name/.git/werkator/.werkator.yml if the origin is private"
|
||||
echo " (or once for all repositories in the 'defaults' block of ~/.werkator.yml),"
|
||||
echo " then restart the service — the registry is read at start."
|
||||
}
|
||||
@@ -353,11 +444,11 @@ instance_start() {
|
||||
echo "==> Applying the instance fragment and generating the host integration (init --systemd)"
|
||||
local fragment_remote
|
||||
fragment_remote="$(upload_fragment)"
|
||||
ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'} --systemd"
|
||||
ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' init ${fragment_remote:+--apply '$fragment_remote'} --systemd"
|
||||
|
||||
local htaccess_src="$TARGET_DIR/werkator/.git/werkator/werkator.htaccess"
|
||||
local htaccess_src="$REPO_DIR/.git/werkator/werkator.htaccess"
|
||||
local htaccess="$TARGET_DIR/doms/$WERKATOR_DOMAIN/subs/www/.htaccess"
|
||||
local maintenance_src="$TARGET_DIR/werkator/.git/werkator/werkator-maintenance.html"
|
||||
local maintenance_src="$REPO_DIR/.git/werkator/werkator-maintenance.html"
|
||||
local maintenance="$TARGET_DIR/doms/$WERKATOR_DOMAIN/subs/www/werkator-maintenance.html"
|
||||
if ssh "$HOST" "test -f '$htaccess_src'"; then
|
||||
echo "==> Placing the generated Apache reverse proxy at $htaccess"
|
||||
@@ -368,9 +459,9 @@ instance_start() {
|
||||
|
||||
echo "==> Linking the units into ~/.config/systemd/user and enabling the service"
|
||||
ssh "$HOST" "mkdir -p ~/.config/systemd/user && \
|
||||
ln -sf '$TARGET_DIR/werkator/.git/werkator/$UNIT' ~/.config/systemd/user/ && \
|
||||
ln -sf '$TARGET_DIR/werkator/.git/werkator/werkator-docker-prune.service' ~/.config/systemd/user/ && \
|
||||
ln -sf '$TARGET_DIR/werkator/.git/werkator/werkator-docker-prune.timer' ~/.config/systemd/user/ && \
|
||||
ln -sf '$REPO_DIR/.git/werkator/$UNIT' ~/.config/systemd/user/ && \
|
||||
ln -sf '$REPO_DIR/.git/werkator/werkator-docker-prune.service' ~/.config/systemd/user/ && \
|
||||
ln -sf '$REPO_DIR/.git/werkator/werkator-docker-prune.timer' ~/.config/systemd/user/ && \
|
||||
XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user daemon-reload && \
|
||||
XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user restart '$UNIT' && \
|
||||
XDG_RUNTIME_DIR=/run/user/\$(id -u) systemctl --user status '$UNIT' --no-pager -l | head -12"
|
||||
@@ -388,7 +479,7 @@ port_forward() {
|
||||
# the effective port, wherever it is configured (machine config or applied
|
||||
# fragment) — config:print is the single answer, not this script's parser
|
||||
local remote_port
|
||||
remote_port="$(ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' config:print 2>/dev/null" | awk '/^server:/{f=1;next} f && /^ port:/{print $2; exit}' | tr -d '"')"
|
||||
remote_port="$(ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' config:print 2>/dev/null" | awk '/^server:/{f=1;next} f && /^ port:/{print $2; exit}' | tr -d '"')"
|
||||
[ -n "$remote_port" ] || die "no server.port configured — run 'tools/remote werkator instance-start' first"
|
||||
|
||||
case "$COMMAND" in
|
||||
@@ -430,7 +521,7 @@ port_forward() {
|
||||
# CLI owns creation and format (step 23), this script only invokes it.
|
||||
control_token() {
|
||||
ensure_ssh
|
||||
ssh "$HOST" "cd '$TARGET_DIR/werkator' && '$WERKATOR_BIN' control-token"
|
||||
ssh "$HOST" "cd '$REPO_DIR' && '$WERKATOR_BIN' control-token"
|
||||
}
|
||||
|
||||
case "$REPO" in
|
||||
|
||||