Splitting zip builds onto their own queue (92a132d) left manual installs
carrying the one job the release note has to do, and the failure it
produces is the worst shape available: a worker still watching only
`default` sends every email cheerfully and finishes no zip downloads,
with nothing in any log to say why. An upgrade note is a poor place to
put that, because it is read on a laptop and needed on a server.
update.sh already finds projectsend-worker.service, so it now reads the
unit's ExecStart and offers to add --queue=default,zips, keeping a copy
of the original beside it. Before the restart, so the worker comes back
on the command it is going to keep.
Only the unambiguous case is rewritten: a queue:work line with no
--queue at all, which consumes `default` and nothing else. A unit that
already names its queues is somebody's deliberate arrangement, possibly
with a second worker for zips, so that one is described rather than
edited — and one that already includes zips is silently left alone.
Exercised against five unit shapes rather than reasoned about: the plain
command is rewritten and backed up, a declined prompt leaves it untouched
with a warning, a unit already naming zips is a no-op, a custom queue list
without zips warns instead of editing, and a unit that is not queue:work
at all is ignored. The sed itself would double-append if it ran twice;
it cannot, because the --queue= guard above it returns first, and both
read the same first ExecStart line.
The last thing an operator sees after a successful update is now where the
people are. Printed rather than asked: the answer to "would you like to
join?" is a browser, and this runs over SSH on a server that has none, so
a y/n could only print the URL its "no" branch prints anyway — and --yes
would answer it on behalf of a cron job that cannot join anything.
Only on success. --check is a report somebody may be scripting, and a
failed run's last line should stay the recovery instructions.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`chown -R` on the install directory also tries to change the ownership of
public/storage, which is a symlink — and some filesystems refuse that even
for root. It turned up on a bind-mounted install (a `fakeowner` mount
refuses lchown outright) and would equally turn up on an NFS export with
root_squash. The update stopped there: files replaced, database not yet
migrated, which is the worst place to stop.
A symlink's own ownership decides nothing, since access is governed by
whatever it points at, so those are now skipped rather than tolerated by
ignoring errors that might have mattered elsewhere.
Found by running the thing on a manual install rather than by reading it.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
Updating a server install cost nine artisan invocations plus a PHP-FPM
reload, written out in three places that had already drifted apart. One
of those steps is silently fatal to skip: with opcache.validate_timestamps
off — what production guides recommend and what our own image ships — the
database moves to the new version while every visitor keeps being served
the old code, and artisan reports the new version throughout.
`sudo ./update.sh` is now the whole procedure. It asks whether to check
GitHub, asks whether to download the release and verifies the checksum
published beside it, and asks whether there is a backup — offering to dump
the database when the answer is no. Then it takes the site down, replaces
the files, runs the update, reloads PHP-FPM, restarts the worker and
brings the site back. The application still has no self-updater: nothing
is fetched or applied unless somebody runs this and answers yes.
Underneath it is `php artisan projectsend:update`, which is everything an
update does that needs no root — and now the only definition of it. Both
container entrypoints call it instead of carrying their own copy of the
sequence, so the two paths cannot drift again.
Three findings worth keeping in the record, all from rehearsing rather
than reasoning:
- queue:restart has to come last. It writes its signal into the cache,
so clearing the cache afterwards deletes it and the worker runs old
code forever.
- optimize:clear is not safe to recommend. It runs cache:clear, which
on Redis is FLUSHDB — harmless on the default two-database layout,
but on a single-database Redis it takes the sessions and the queue
with it. The compiled caches are cleared individually instead.
- update.sh overwrites itself mid-run, because the zip contains it and
bash reads its own script lazily by byte offset. It re-execs from a
temporary copy before touching anything.
And when the reload is skipped anyway, the application now says so:
projectsend:update records the version it applied, and any staff page
compares that with what the running process actually compiled. The same
check catches the mirror image — new files in place, update never run.
Rehearsed end to end against real installs: a container upgrade (69 to 73
migrations, key and data intact, healthy), a scripted update on a real
nginx + php-fpm install with OPcache pinned (web process moved 2.1.0 to
2.1.1), the skipped-reload case (banner appears naming both versions, and
clears on reload), the refusals (downgrade, non-release zip, truncated
zip, URL passed to --zip, non-root), a database taken down mid-update
(site comes back out of maintenance mode by itself), and a real download
of the published 2.0.0 zip with its checksum verified.
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>