Commit Graph
17 Commits
Author SHA1 Message Date
mhoennigandClaude Opus 5 389fae388e fix(remote): upload before stopping the service, and verify the transfer
instance-update stopped the unit and only then started the upload, so a transfer
that dies mid-way leaves the host with no running Werkator and nothing to start
again. That is not theoretical: deploying to vm4006 on 2026-09-03 failed with
"scp: Connection closed" with the service already stopped.

The upload now happens before the stop, and each artifact is transferred to a
.part file whose sha256 is compared with the local one before it is moved into
place, retrying twice. A truncated archive would otherwise unpack into a broken
runtime, which is worse than the failed transfer it came from.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 19:55:23 +02:00
mhoennigandClaude Opus 5 506e817c82 feat(remote): the host layout is configurable, not the mih convention
tools/remote assumed the layout instance-install creates: the watched repository
in $WERKATOR_PATH/werkator, the runtime in $WERKATOR_PATH/.werkator, a werkdock
binary and a rootfs beside it, and the unit hardcoded as werkator-werkator.service.
An installation that predates the script — vm4006, a docker host with the repository
in ~/hs.hsadmin.ng and the runtime in ~/opt — could not be deployed with it at all.

WERKATOR_REPO_DIR, WERKATOR_INSTALL_DIR and WERKATOR_SANDBOX name the three values
that actually differ; their defaults are what instance-install writes, so the
existing env files resolve to exactly the same paths as before. The unit name is
derived from the repository directory the way SystemdServiceFiles.unitName does it,
instead of being spelled out. With WERKATOR_SANDBOX=docker the werkdock binary and
the rootfs archive are neither built nor uploaded — a docker host has no sandbox to
install, and check-prerequisites asks the docker daemon instead of werkdock doctor.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 19:53:04 +02:00
mhoennigandClaude Sonnet 5 ca9c23d55b Show a static maintenance page while the service restarts
A deployment's ErrorDocument-covered downtime window looked dead: Apache's
default 502/503/504 error page, or a hung request, while instance-update
briefly restarts the systemd unit. init --systemd now also writes a static
werkator-maintenance.html next to the generated .htaccess, and the .htaccess
maps a refused connection straight to it — served by Apache alone, no
change needed to Werkator itself since it is exactly the process that is
down.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 17:48:06 +02:00
mhoennigandClaude Sonnet 5 4d76a93927 Add a script to mirror main to GitHub after the move to Gitea
Werkator's canonical repository now lives on git.javagil.de; GitHub stays
around as a public mirror. This keeps its main branch in sync without
touching any other branch or the repo's other history.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 16:34:44 +02:00
Michael HönnigandClaude Opus 5 ac53b5f211 step 21e werkdock moved to own repo (#15)
* refactor: werkdock/ verlässt dieses Repository

Die neun Commits unterhalb von werkdock/ sind per `git subtree split`
historieerhaltend nach https://git.javagil.de/mi/werkdock gehoben; hier
bleibt nichts davon zurück.

Mit dem Verzeichnis geht die Build-Definition `werkdock`: Sie beschrieb ein
Go-Modul, das der Kotlin-Build um sie herum nicht berühren kann — jeder
Werkator-Branch baute es dennoch als `<branch>@werkdock` mit. Drüben ist
sie die einzige und heißt `default`.

Die Kopplung bleibt, wie Sitzung C sie gemacht hat: Werkator ruft das
`werkdock`-Binary über PATH (`bwrap.werkdock`, weiterhin gepinnt), so wie
es `git` ruft.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* refactor(remote): das werkdock-Binary kommt aus einem eigenen Checkout

Bisher baute das Skript es aus dem Unterverzeichnis. Jetzt nennt
WERKDOCK_REPO den Checkout (Vorgabe: ein Geschwister dieses Repositories —
die übliche Ablage, wenn man an beidem arbeitet), WERKDOCK_BINARY das
gebaute Binary darin; beides ist wie jeder Transportwert aus der Umgebung
übersteuerbar.

Fehlt der Checkout, nennt die Meldung die Klon-URL, statt bloß „file not
found" zu sagen: Das Skript kann die Lage nicht mehr selbst beheben, also
sagt es, was sie beheben würde.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: Sitzung E — Werkdock hat ein eigenes Repository

Plan-Schritt 21 bekommt die Sitzung, die die Herauslösung beschreibt, samt
Abnahmekriterium; Plan-Index und Architektur-Skill sagen nicht mehr, sie
stehe noch bevor, und der Deployment-Leitfaden verlinkt das Repository
statt eines Verzeichnisses, das es hier nicht mehr gibt.

Dazu das PR-Dokument nach Hausregel (docs/prs/README.md).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(remote): repo-add wendet das Instanz-Fragment an

`repo-add` rief `werkator init` ohne `--apply`, `repo-init` dagegen mit.
Folge: Ein so aufgenommenes Repository bekam die Sandbox-Politik nicht —
`bwrap.rootfs` und `bwrap.werkdock` fehlten, und der erste Build lief auf
dem nackten Host statt in der Sandbox. Gefunden beim Aufnehmen von
Werkdock: `gofmt: command not found`, weil die Go-Toolchain nur im
Sandbox-Image liegt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(remote): repo-add erkennt einen ~/-Registry-Eintrag, und der Kommentar sagt die Wahrheit

Die Registry-Prüfung suchte nur den absoluten Pfad und meldete deshalb
„not registered yet“, obwohl der Eintrag als `path: ~/werkdock` dasteht —
eine Einladung, ihn ein zweites Mal einzutragen. Gegriffen wird jetzt
lokal, damit beide Schreibweisen ohne zweite Schicht Remote-Quoting
treffbar sind.

Dazu der Kommentar über dem Laden der Env-Datei: Er behauptete, die
ausdrückliche Umgebung gewinne. `set -a; source` weist unbedingt zu — die
Datei gewinnt. Beim Nachziehen des Werkdock-Fragments hat mich genau das
eine Runde gekostet.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 15:15:15 +02:00
mhoennigandClaude Opus 5 c1629cace3 feat(remote): repo-add bereitet ein weiteres Repository für die Registry vor
`tools/remote` konnte genau ein beobachtetes Repository einrichten
(`repo-init`, fest auf $WERKATOR_PATH/werkator). Für eine Instanz, die eine
Registry bedient (ADR 0009), fehlte der Weg, ein zweites hinzuzunehmen —
und das braucht der Rollout unabhängig davon, welche Repositories es am Ende
sind.

`repo-add <url> [name]` klont daneben, führt `init` darin aus und PRÜFT, ob
die Registry den Pfad schon nennt — geschrieben wird sie nicht. `~/.werkator.yml`
ist die Datei der Instanz: Port, globale Nebenläufigkeit, womöglich geteilte
Zugangsdaten. Ein Skript, das sie in place umschreibt, verändert die
Konfiguration des Betreibers hinter dessen Rücken. Klonen und Initialisieren
ist mechanisch, Registrieren ist eine Entscheidung — also druckt es den
Eintrag und sagt, dass die Registry beim Start gelesen wird.

Der Name muss ein Pfadsegment sein: Er wird das Routen-Segment /repos/<name>.
docs/deployment.md verweist im Registry-Abschnitt darauf.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-03 13:59:24 +02:00
mhoennigandClaude Fable 5 1d1bd19e10 Step 23 sessions B+C: tools/remote wraps, verified live on mih34
The wrapper's config writing is gone: --env-file FILE selects the
target instance (default .env; named like docker's flag — --env means a
single variable there), the init fragment named by WERKATOR_INIT_CONFIG
is uploaded and installed remotely via 'werkator init --apply', and
instance-start places the .htaccess that init now generates. The
heredoc/sed machine-config writing is deleted; control-token delegates
to the werkator CLI; check-prerequisites uploads werkdock and runs its
doctor — tools/werkator-build-prerequisites.sh retires. The idle-check
and port-forward port lookups read the effective config via
config:print, so a port living in the applied fragment is found too.

Verified live against mih34 with the .env.mih34 + .env.mih34.yml pair
(gitignored via the new /.env.* rule): update, doctor PASS 6/6,
repo-init applying the fragment as the new layer, instance-start
placing the generated proxy and restarting the unit, control-token.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 19:00:38 +02:00
mhoennigandClaude Fable 5 2b538b668e Step 21 session D: the managed-webspace install path replaces the self-build
tools/remote reworked to role-named commands (the builder-vs-built
tangle noted 2026-09-01): instance-install/instance-update/
instance-start manage the installed Werkator + werkdock, repo-init the
watched repository. The self-build is retired — no repo clone for
building, no GitHub-key step; the instance installs from locally built
artifacts (ADR 0006), the watched repo clones anonymously via https.

instance-update refuses to swap the runtime under a running build
(FORCE=1 overrides) and keeps the previous runtime as the rollback
asset. repo-init is idempotent: checksum-skipped rootfs upload, and the
machine-config guard is fixed — its indentation mismatch (grep for two
spaces, block written with four) appended the bwrap block on every run,
the source of the nine duplicates found on mih34. The retired
install/build/start commands fail loudly naming their successors.

docs/deployment.md gains the Hostsharing Managed Webspace as the third
deployment variant, written from the live-verified setup: both new
commands ran against mih34 (full update cycle; fully skipping re-init).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 17:49:41 +02:00
mhoennigandClaude Fable 5 a30a16c235 rootfs build: trim what a build environment never reads
openjdk-21-jdk-headless instead of the full JDK (drops the X11 and
fontconfig library stack; headless AWT still works for tests),
message catalogs except en/de removed, man pages, package docs, apt
lists and cache cleared after install.

Measured on the JDK+Go+Node image: 516 MB -> 351 MB compressed —
smaller than the original JDK-only archive (375 MB).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 09:02:45 +02:00
mhoennigandClaude Fable 5 598e8d335f rootfs build: anchor the tar excludes to the archive root
--exclude=sys matched every path component named sys anywhere in the
tree — GNU tar applies slash-less patterns to all components — and so
silently dropped usr/share/go-1.24/src/internal/runtime/sys from the
archive, breaking every go build in the sandbox with 'package
internal/runtime/sys is not in std'. The excludes are now anchored
(./proc, ./sys, ./dev), so only the top-level mountpoints stay out.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 08:02:08 +02:00
mhoennigandClaude Fable 5 89de3e0378 Step 21 session A: close step 17's paperwork
- PR-docs get their real numbers: bwrap-build-runtime -> PR#4,
  build-current-head-from-the-branches-view -> PR#3 (files and scenario IDs)
- tools/remote: header note marking install's clone step and build as the
  self-build prototype, superseded by session D of step 21 (usage range grown)
- ADR 0008: bubblewrap user-namespace sandbox as the third build runtime;
  step 17 and the plan README now point at 0008 (0007 was taken by build
  definitions before the step landed)
- AGENTS.md: decisions list catches up with ADR 0007 and ADR 0008
- architecture skill: BwrapBuildRunner paragraph (rootfs unpack, uid mapping,
  mount ordering, pinned keys) and the dispatcher's three-way routing
- plan README: step 21 entry now describes the werkdock/ subdirectory path

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-01 06:21:29 +02:00
Michael Hönnig 71f1fc62c6 Bwrap build runtime und Installation in Hostsharing Managed Webspace (#4)
* Add the bubblewrap build runtime (step 17, ADR 0007)

BwrapBuildRunner: third runtime behind BuildRunner for hosts without root
and without Docker (e.g. Hostsharing managed webspaces). Shells out to the
bwrap CLI, unpacks a prepared rootfs on demand into
.git/werkator/buildenv/<envKey>/rootfs, reuses the Docker runner's git
metadata mounts, and returns the attached bwrap process for streaming and
cancellation.

Config: bwrap.enabled/rootfs/env on BranchConfig and BwrapOverrides on
BuildDefinition; enabled/rootfs are pinned like the docker sandbox policy.
Docker and bwrap are mutually exclusive per build, rejected in
buildSettings instead of picked silently. DispatchingBuildRunner routes
bwrap; InitCommand template, docs/configuration.md and AGENTS.md in sync.

* bwrap rollout tooling: remote script, prerequisites disk/quota check, absolute workspace binds

- tools/remote: central remote control script with check-prerequisites, install and build commands
- tools/werkator-build-prerequisites.sh: compact PASS/FAIL output, target-dir parameter, free-space and
  group-quota headroom checks against the ~5 GiB build footprint, home-filesystem reference
- BwrapBuildRunner: bind workspace and home at absolute paths resolved against repoDir — a relative
  path made bwrap create mountpoints inside the read-only rootfs (seen on the webspace); regression test
- TestcontainersSmokeTest: gated with enabledIf docker available (skip, never fail, without a daemon)
- docs: configuration reference, step-17 plan notes, PR-doc

* bwrap: bind the repo read-write before the workspace so mountpoints are creatable

bwrap creates mountpoints for bind destinations inside the sandbox; with only a
read-only rootfs bound at /, creating them for the workspace under .git/werkator/
worktrees failed with 'Read-only file system' (seen on the webspace). Binding the
repo dir read-write first provides the base; the git metadata mounts then layer
the usual isolation on top (read-only .git, tmpfs mask over .git/werkator,
read-write worktree admin dir).

* bwrap: pre-create bind mountpoints inside the unpacked rootfs

bwrap mkdirs mountpoints for bind destinations against the sandbox view; with the
rootfs ro-bound at / every destination missing from the rootfs (the repo dir under
/home/storage/... on the webspace) fails with 'Read-only file system'. The rootfs
directory is a plain host dir, so create the mountpoints there before launching
bwrap; it then finds them and has nothing left to create.

* bwrap: skip existing rootfs files when pre-creating bind mountpoints

/etc/resolv.conf is a file the rootfs already ships; createDirectories threw on it.
Only missing directories are created now.

* bwrap: pre-create proc/dev/tmpfs mountpoints in the rootfs too

The rootfs archive ships no /proc or /dev (excluded when packed), so bwrap failed
mkdir'ing their mountpoints against the read-only root.

* bwrap: bind the workspace after the git metadata mounts

The tmpfs mask over .git/werkator shadowed the earlier workspace bind, because the
worktree lives under .git/werkator/worktrees — chdir then failed with ENOENT. The
workspace bind now comes last and shadows the mask at exactly its own path.

* systemd resource limits and webspace start command (step 17, web access)

- server.systemd.memoryMax/tasksMax (empty = directive omitted): on platforms where
  the service runs in a shared memory slice (Hostsharing Managed Webspaces) a
  runaway Gradle build must not starve the whole package; init --systemd reads the
  effective config and bakes the values into the generated unit
- tools/remote werkator start: writes server settings (assigned port, loopback
  bind, publicBaseUrl, nginx off) plus the Apache reverse-proxy .htaccess into
  ~/doms/<domain>/subs/www, runs init --systemd and enables the user unit
- docs/configuration.md documents the new keys

* tools/remote: env-based configuration and background port-forward

All connection and deployment values come from .env in the repository root
(WERKATOR_REMOTE, WERKATOR_PATH, WERKATOR_PORT, WERKATOR_DOMAIN,
WERKATOR_LOCAL_PORT, optional WERKATOR_BRANCH/MEMORY_MAX/TASKS_MAX/ROOTFS);
missing values fail with a pointing error instead of positional parameters.

- port-forward is now 'tools/remote port-forward start|stop' with a detached
  ssh tunnel, pid file under /tmp, and idempotent start
- start restarts the systemd unit after updating the machine config
- control-token generates the token in place when the server has not yet
- the rootfs archive default moves to build/ (already gitignored)
2026-08-31 19:56:04 +02:00
Michael HönnigandClaude Opus 5 d0169e57bc Renaming gitTally to Werkator because there is another git-related tool named gittally (#1)
* renaming from gitTally to Werkator

* Rename GitTally to Werkator

`gitTally` is the name of another product in the git space, so the
rename is a precaution; nothing about what the build system does changes.

The name follows one rule: `Werkator` where it is prose, capitalized
where it is a Kotlin type and its file, lowercase everywhere a machine
reads it — the command, packages, paths, configuration keys and values,
the Gitea check context. Environment variables keep their convention and
are uppercase throughout.

Every configuration file is still found under its pre-rename name
(`ConfigFiles`): `.gittally.yml` at the repository root, in a build
worktree and as committed on a branch, `.git/gittally/.gittally.yml` for
the machine layer. The current name wins where both exist, and the old
file is then ignored rather than merged — two files side by side are a
half-done rename, not a layering. Without the fallback an installation
that updated without renaming would not fail: a configuration that is
not found leaves every setting at its default, so it would come up
looking healthy while having forgotten its credentials and its builds.

`docs/werkator-migrationsplan.md` lists what the fallback does not
cover and has to be moved by hand — above all the state directory
`.git/werkator/`, which holds the build history, the control token and
the worktrees, and has no fallback of its own.

`docs/migration-from-legacy.md` is deleted with this: it mapped the
legacy script's environment variables, and every host it addressed has
long since moved to the YAML configuration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Move the pre-rename state directory at the first start

The configuration is found under either name, the state is not: build
history, control token, auto-build slots and worktrees live at one fixed
path. An installation that updates without moving `.git/gittally` would
not fail — it would come up with an empty history and a fresh control
token, quietly. So the first start moves it instead of the release notes
asking for it.

Only when the old directory exists and the new one does not. Where both
exist nothing is touched and a warning names the leftover: which of the
two is the live state is not something to guess. A failed move is an
error in the log, never an abort — a CI must not hang on it.

The worktrees are dropped rather than moved, since they point at their
old path in both directions; `GitWorktreeWorkspaces` prunes the stale
admin entry and recreates each on its branch's next build. A generated
systemd unit moves with the directory and leaves its symlink dangling,
which is warned about — the running service is unaffected, the next
start is not.

Runs from `CliRunner`, before any command resolves a path under the
directory, and so before the second context of `server` exists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Document PR#1: the rename to Werkator

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Drop the legacy env-to-YAML conversion from the setup tool

The old bash script configured itself through `GITTALLY_*` environment
variables. The blanket rename rewrote those literals, so the converter
was looking for `WERKATOR_*` — a spelling no host has ever written. Fed
a real legacy file it would have found nothing and written an almost
empty configuration, without an error, which is the same silent failure
this rename is otherwise careful to avoid.

The conversion has served its purpose with the vm2176 to vm4006
migration, so it goes instead of being repaired. What remains is the
setup of a new instance: the preconditions, the credential prompt, and
the machine configuration written mode 600 — now carrying the host's
public URL as well, since that is host-specific too. Everything the
repository builds comes from `init` and its templates.

It also stops emitting a legacy `branches:` section, which step 18 is
about to reject outright.

`docs/plan/00-legacy-analysis.md` and `13-nginx-tls.md` get the real
`GITTALLY_*` spelling back: they record what the old script read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Describe this repository's build with a build definition

Its own `.werkator.yml` still used the deprecated `branches` section
with an `autoBuild` schedule that was switched off. That section is read
only while nothing defines a build at all, and step 18 rejects it by
name — so this repository would have blocked the precondition of that
step, which asks that no configuration still in play carries it.

Nothing about the build changes: `builds.default` with `trigger.onPush`
is a build of every new commit on every branch, which is what the branch
section said. `config:print --full` resolves the definition completely
and logs no deprecation warning any more.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Stop documenting the pre-rename fallback for users

Exactly one repository is configured with the old names, and it is
migrated by hand in the same move as this release. The fallback is
therefore a transition of days, not a feature anyone reading the release
notes or the configuration reference has to plan around.

Removed from `releases.html` and `docs/configuration.md`. The mechanism
itself is unchanged and stays described where it is worked on: in
`ConfigFiles`, in `StateDirMigration`, and in the migration plan.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Bring the PR-doc to its final state

The `statusContext` question is answered and marked as decided rather
than left standing: it is the one value a human reads as a label, and it
stays lowercase because Gitea matches it and the client reads it back,
which makes it a value.

Also records that the pre-rename fallback is deliberately absent from
the release notes and the configuration reference.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Release v1.0.0: Werkator

The release after 0.9.21 is 1.0.0, because a product that changes its
name is better off counting from one under it. The release note says as
much, so the jump is not read as a claim about maturity — plan steps 14,
17 and 18 are still open.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Correct the PR-doc about the version

It claimed the PR carries no version bump, which the release commit made
untrue, and records why the number is 1.0.0 instead of 0.9.22.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Point the legacy references at the history

`legacy/gitTally` was removed from the tree with the rename, but the
README still described it as a reference kept in the repository, and the
plan told an executing session to read parts of it — including step 14,
which is open.

The README section is gone; `docs/plan/README.md`, step 14 and the
legacy analysis now say where the script actually is
(`git show 7f55068^:legacy/gitTally`). Executed steps and ADR 0004 keep
their wording: they record what was true when they ran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Retarget the links in the historic PR-docs

The package rename moved every file the older PR-docs link to, leaving
60 dead links. Only the link targets are rewritten, never the visible
text and never a statement: those documents record what was true when
they were written, GitTally in the prose included. A snapshot may be
outdated; it should still be navigable.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Add the v1.0.0 deployment procedure for vm4006

Measured, not estimated: the state directory is 878 MB, of which 878 MB
are the nine build worktrees. What cannot be recreated is 84 KB, so the
snapshot before an in-place switch is instant and the rollback is one
sequence of moves.

Records the three expected non-failures — a cold Gradle volume, one
image rebuild, containers left under the old label — and that
`gitea.statusContext` needs no attention because it comes from the
watched repository's committed configuration.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Rename the machine configuration along with its directory

Found by the deployment to vm4006: the move renames the directory and
leaves the file inside it alone, so the machine configuration ended up at
`.git/werkator/.gittally.yml` — a pair of names the lookup did not
expect, because it pairs directory and file name. The instance resolved
empty credentials, no public URL and none of the host's build
definitions, and said nothing about it. That is the exact failure this
change exists to prevent, produced by the change itself.

`StateDirMigration` now renames the configuration with the directory,
unless one under the current name is already there. `ConfigFiles` carries
`.git/werkator/.gittally.yml` as a third candidate as well, for a
directory somebody moved by hand, where the migration never runs and so
can rename nothing.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Let init see a configuration under its previous name

`init --systemd` runs `init`, and its "already exists" check knew only
the current name. On the one repository still carrying `.gittally.yml`
it therefore wrote a fresh template `.werkator.yml` beside it — and
since the current name wins, that repository would have built the
template's `./gradlew test` instead of what its own configuration says.
Found on vm4006, where the file was created in the watched working tree
and removed again by hand.

Both checks now ask `ConfigFiles`, so init decides existence by the same
rule the loader uses to read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Record the v1.0.0 deployment to vm4006

Deployed from the branch as the final test of PR#1, and it did what a
final test is for: it found two silent-failure defects before the
service was started, both fixed and redeployed in the same window.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Drop the control token left under the old localStorage key

The key is named after the product, so the rename left every browser
with a token under `gittally.controlToken`, which nothing reads any more
and which "forget token" can no longer reach. It is a write-scope token
in a browser store, not a password, but a secret nobody owns is worth
one line to remove.

Removed on load. The token on the server is unchanged, so re-entering it
once per browser is all the rename costs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-31 13:32:54 +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
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
Michael Hoennig 84ca8b441c add adr-status + direnv 2026-06-09 14:31:35 +02:00