Werkator is the one repository its own instance builds but cannot deploy
the ordinary way: the deploy build is a child of the process it replaces,
so the stop ends the build, the build is recorded INTERRUPTED, and the
startup recovery re-runs it — a loop, not a glitch (RFC 0005, step 1).
The script therefore only stages, verifies and swaps the runtime, and
asks systemd for the restart from outside the service: a transient unit
scheduled with systemd-run --user, which the stop cannot end and which
fires once the build is recorded SUCCESS.
It takes no parameters in the deploy build. The primary checkout comes
from the common git dir, the unit name from its basename exactly as
SystemdServiceFiles.unitName derives it, the installation directory from
that unit's ExecStart, and the port from the installed binary's
config:print. It refuses the primary checkout, a missing bundle (a
restart of deploy alone must not install an older version), a foreign
instance, and an installation that is not a runtime bundle.
The swap is staged: unpack beside the running runtime, run the new
launcher natively — which catches a jlinked JRE that does not run on
this host — and only then take its place. The replaced runtime stays as
werkator.prev, which the watchdog puts back when the restarted instance
does not answer within two minutes.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
instance-update and instance-install unpacked the bundle inline: move the
running runtime aside, untar, and only then ask the new launcher for its
version — so a bundle that does not start on the host left the
installation without a runtime at all.
Both now run tools/self-deploy on the host, the script the instance uses
to deploy itself (RFC 0005). It is uploaded beside the werkdock binary
and called with an installation directory and a bundle, which is the
swap-only form: staged beside the running runtime, verified natively,
and swapped in only after that. No restart is scheduled — this script
stops and starts the service itself, exactly as before.
One swap, one implementation: the layout of an installation is now
described in a single place instead of drifting apart between the
deployment from a developer machine and the one the instance does
itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The repository's part of the self-deployment (RFC 0005): the default
build appends runtimeBundle to its command, and a deploy definition runs
tools/self-deploy.
The bundle has to be built in the sandbox, because only the sandbox has
a JDK and therefore jlink; the host has none. It reaches the deployment
through the worktree the two builds share, which is why deploy keeps it
with an empty cleanCommand. It is deliberately not an artifact: 67 MB
per build, kept retentionPerBranch times, with nobody to read it.
The definition has no trigger here and therefore never runs. When, for
which branches, and that it runs outside the sandbox is the host's part,
in the instance fragment — and pinned, so no branch can add that trigger
or widen it to deploy itself.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
deployment.md gains a Self-Deployment section: why this repository is
the exception, the two halves of the configuration, the staged swap with
its native verification, werkator.prev as the last known good version,
and the watchdog that puts it back when the restarted instance does not
answer. It also names the gap step 2 closes — the restart happens at a
fixed delay, so a build enqueued in between is interrupted and re-run.
configuration.md names this as the one deployment that restarts the very
instance running it, with the reason the restart cannot happen inside
the build.
The RFC records step 1 as implemented by PR#61.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Five scenarios from RFC 0005. The first one — a merged commit installing
itself — is marked as verifiable only on mih09 after the merge, not
claimed; the branch-cannot-deploy-itself scenario links the pinning test
that already covers it; the two script scenarios link the manual run
recorded in the attachment, since the repository has no harness for
shell scripts.
Open Questions carry the host fragment that belongs in .env.mih09.yml,
the fixed restart delay step 2 replaces, and the longer test build every
branch now pays for a bundle only main installs.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The first CI build that ran runtimeBundle failed after 45 seconds:
jlink failed: Cannot run program "objcopy": error 2
On Linux --strip-debug strips the native debug symbols too, and jlink
shells out to objcopy for that. The werkdock rootfs has no binutils, and
the bundle is built there — the host has no JDK at all.
--strip-java-debug-attributes strips the Java attributes only and runs
with nothing but the JDK. Verified in a bare trixie container with
openjdk-21-jdk-headless and no binutils: --strip-debug fails there
exactly as the CI does, the replacement produces a runtime. The bundle
grows by 671 bytes, because a headless JDK's native libraries carry
almost no debug symbols to begin with.
Adding binutils to the rootfs would work as well, but it would mean
rebuilding and uploading the rootfs before this branch could go green.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Compare the unit's WorkingDirectory with the build's repository resolved:
a webspace reaches the same directory under more than one path, and a
symlink is no reason to refuse a deployment into the right instance.
Refuse up front when the host has no curl. The watchdog decides by
asking the instance, so without curl it would find every deployment
broken and roll back a perfectly good runtime.
Let the health check run even when systemctl reports the restart as
failed. Whether the instance answers is what decides; bailing out on the
command's exit code skipped the rollback in exactly the case it exists
for.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
64 MiB, not "around 60 MB", and the test build on the CI instance went
from around 95 to 161 seconds with the runtimeBundle task — recorded in
the PR-doc's Open Questions, where the cost of building the bundle on
every branch is weighed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
tools/remote reached the swap-only path implicitly, by giving both an
installation directory and a bundle. It now passes --no-restart as well,
which changes nothing and says what it wants, and the script's usage
spells out that those two arguments together mean a swap and nothing
else — no git worktree, no systemd, no instance to ask.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Transient unit names are global to the user manager, and the name was a
constant. Two Werkator instances under one account — which tools/remote
supports through WERKATOR_REPO_DIR, each with its own unit — would have
collided on it: the second deployment's systemd-run would fail because
the first one's timer was still pending.
The unit is now werkator-self-restart-<repo-name>, derived from the
service it restarts. The watchdog binary keeps its plain name: it lives
in the installation directory, which is per instance already.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The PR-doc claims the repository's half of the configuration never runs
on its own. config:print --full says so field by field: an empty trigger
in every one, an empty artifactDirs that wins over the build/reports it
would otherwise inherit, inherited log names so the deployment's log is
still kept, and the empty cleanCommand that leaves the test build's
bundle in the shared worktree.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A self-deployment builds the bundle on the very machine that installs
it, so compressing 64 MiB and decompressing it moments later is work for
nobody. The task is split accordingly: runtimeBundleDir produces the
directory, runtimeBundle packs that directory as a tarball.
The CI build now runs runtimeBundleDir and tools/self-deploy prefers the
directory, falling back to a tarball when there is none. tools/remote,
which really does transfer the bundle to another host, still builds and
uploads the archive and is unchanged.
Measured here: writing the archive costs 5 s and reading it back 0.8 s,
against 0.1 s to copy the directory; the CI instance is around twice as
slow at this kind of work. It also keeps 64 MiB off the SSD during every
build. The local run of the CI command went from 86 to 81 seconds.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The pull request is "Werkator deploys itself", so the document is
2026-09-14-PR#61-werkator-deploys-itself.md. "Step 1" only meant
something to a reader who knows RFC 0005, and the change had grown
past it anyway: the staged swap and the split bundle task help every
installation, not only one that deploys itself.
The date follows the day the pull request was opened, as the convention
in docs/prs/README.md asks. Nothing linked the old filename.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
mi
merged commit 7d69cde150 into main2026-09-14 22:11:59 +02:00
mi
deleted branch self-deploy-step-12026-09-14 22:11:59 +02:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
The first CI build that ran runtimeBundle failed after 45 seconds: jlink failed: Cannot run program "objcopy": error 2 On Linux --strip-debug strips the native debug symbols too, and jlink shells out to objcopy for that. The werkdock rootfs has no binutils, and the bundle is built there — the host has no JDK at all. --strip-java-debug-attributes strips the Java attributes only and runs with nothing but the JDK. Verified in a bare trixie container with openjdk-21-jdk-headless and no binutils: --strip-debug fails there exactly as the CI does, the replacement produces a runtime. The bundle grows by 671 bytes, because a headless JDK's native libraries carry almost no debug symbols to begin with. Adding binutils to the rootfs would work as well, but it would mean rebuilding and uploading the rootfs before this branch could go green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>