release: v1.14.0 — the worker takes its flags, and can be run as a service - #166
Merged
Conversation
…d help documentation
`hkm worker --queue=mails` is a foreground process: it dies with the terminal, it does not come back after a crash or a reboot, and nothing collects its output. Every deployment therefore hand-writes the same unit file, and hand-writing it is where the failures live. `hkm service` generates that unit for whichever supervisor the host runs — systemd or launchd, with --platform to override so a Mac can produce the Linux unit it will deploy. Four verbs: preview (the default, writes nothing), write, install [--start], remove. Scope is --system or --user, defaulting to system on Linux and a user agent on macOS, where a LaunchDaemon running as root is the wrong answer on a developer machine. Three things the generated unit gets right that a hand-written one usually does not: - ExecStart runs the LAUNCHER, `hkm worker -p <root>`, not php plus an absolute vendor/autoload.php. The launcher self-locates the kernel, so a kernel upgrade that moves a version-stamped install directory cannot silently break the queue. `--exec=php` emits the direct form for a server with no launcher, and states the pinned autoload's cost in the unit. - TimeoutStopSec / ExitTimeOut is 90s. The worker traps SIGTERM and finishes the job in flight before exiting — that is what makes a redeploy safe — and launchd's 20s default SIGKILLs it mid-transaction instead. - PATH and HKM_PHP_BIN are pinned. A service inherits none of a login shell's PATH and /opt/homebrew/bin is on neither manager's default; the entire diagnostic without the pin is `error: FileNotFound`, with nothing anywhere naming php. This was found by running the generated unit, not by reading it. Values that reach the unit are validated rather than interpolated: a queue name may only contain [A-Za-z0-9._:-], so nothing can add an argument or a directive; ExecStart tokens containing whitespace are quoted; plist strings are XML-escaped. --dry-run (-n) reports every write and every command for write/install/ remove and performs none of them. Its one filesystem touch is the create-and-delete write probe in the destination directory, which is how it knows whether to tell you sudo.
…rvice `hkm worker --queue=mails` drained 'default' in silence: the entry point read only WORKER_QUEUE from the environment and ignored every forwarded argument. It now parses its flags, prefers them over the environment, and refuses an argument it does not recognise. `hkm service` generates the systemd unit or launchd plist that supervises that worker — one command instead of the file every deployment was hand-writing, with the stop timeout, the pinned PATH and the upgrade-proof ExecStart that a hand-written one usually gets wrong.
hakeemRash
requested review from
Alshatri and
craftdevscommunity
as code owners
September 5, 2026 03:44
craftdevscommunity
approved these changes
Sep 5, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
v1.14.0
Merging this pushes
v1.14.0—auto-release.ymlfires on a push tomaintouching
CHANGELOG.md, reads the first versioned heading, tags, builds, andlands the Homebrew formula bump itself.
Fixed — the worker ignored every flag it was given
hkm worker --queue=mailsforwards its arguments verbatim toapp/worker/run.php, which read onlyWORKER_QUEUEfrom the environment. Theflag was accepted in silence and the worker drained
default: no error, nowarning, a running process, and the wrong queue.
The entry point and the scaffolding template now parse
-q/--queue,-n/--max-iterations,--memoryand-h/--help, each overriding the matchingenvironment variable, and exit 2 on an argument they do not recognise rather
than ignoring it. The environment fallbacks also moved from
getenv()toenv(), since the loader injects.envinto$_ENVand skipsputenv()— sogetenv('WORKER_QUEUE')could not see a value set in the project's.env.Added —
hkm serviceGenerates the unit that supervises that worker, for whichever manager the host
runs.
systemd or launchd (
--platformoverrides, so a Mac generates the Linux unit itdeploys);
--system/--userchoose the location, defaulting to system onLinux and a user agent on macOS.
--dry-run(-n) reports every write andcommand for the three mutating verbs and performs none of them.
Three things the generated unit gets right that a hand-written one usually does
not:
ExecStartruns the launcher,hkm worker -p <root>, notphpplus anabsolute
vendor/autoload.php— the launcher self-locates the kernel, so anupgrade that moves a version-stamped install directory cannot break the queue.
--exec=phpemits the direct form for a server with no launcher, and statesthe pinned autoload's cost in the unit.
TimeoutStopSec/ExitTimeOutis 90s — the worker traps SIGTERM andfinishes the job in flight, and launchd's 20s default SIGKILLs it
mid-transaction.
PATHandHKM_PHP_BINare pinned — a service inherits none of a loginshell's PATH and
/opt/homebrew/binis on neither manager's default. Withoutthe pin the only diagnostic is
error: FileNotFound, naming nothing. Found byrunning the generated unit, not by reading it.
Values reaching the unit are validated, not interpolated: a queue name may hold
only
[A-Za-z0-9._:-];ExecStarttokens with whitespace are quoted; pliststrings are XML-escaped.
Verified
--start→launchctl printshowingstate = runningwith the process actually drainingmails→ remove, with nothing left behind.plutil -lintaccepts the plist.--exec=phpExecStartline run under a strippedenv -iwith only thepinned variables.
unknown-argument rejection.
zig build,zig build test(13 new unit tests),zig fmt --check.Not verified: the systemd path could only be rendered and read here — no
Linux host.
daemon-reload,enable --nowand the sudo copy are unexercised.