mirror of
https://github.com/karatektus/pl_mail.git
synced 2026-10-25 01:54:10 +00:00
main
13
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
13303b247e |
v0.3.9 milestone (#283)
* Run the end-to-end suite on pull requests into milestone and wip branches
A release's work is now collected on a milestone branch (milestone/v0.4.0)
before it reaches main, and the suite only ran on pull requests into main,
so nothing proposed for the milestone was tested. `milestone/**` joins
`main`, and `wip/**` comes along from wip/28, where it already runs the
suite on the phases of #28 — the same block on both, so they merge
without a conflict.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit
|
||
|
|
ffbf7bdd9b |
Clean up the compose files (#160)
Compose files keep only a short header; the reasoning lives in docs/install. truenas.compose.yaml is gone (the TrueNAS catalog app replaces it) and so is compose.prod.yaml (compose.yaml already runs the published prod image). compose.override.yaml.dist becomes compose.dev.yaml, passed with -f, so there is no untracked copy that compose loads on its own. Docs, CODESTYLE, CONTRIBUTING and the entrypoint's error text follow. Every remaining file resolves to the same `docker compose config` as before. Closes #30 Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
1ff8c81f4b |
Let the image run as a user that is not root
Nothing here needs root at run time, and a platform that chooses the uid
itself and drops every capability could not start it. Three things were
in the way, found by running it as uid 568 with nothing granted:
frankenphp carried cap_net_bind_service as a file capability, and
a process without that capability may not execute such
a binary at all. Removed: in a container the
unprivileged port range starts at 0 anyway.
var/ cache, log and share were root's. Writable by anyone
now, since the uid is not known when the image is built.
/data, /config root's as well, so the hub could not open its database
on a fresh volume. Same answer.
The entrypoint's setfacl step is skipped for a non-root user, who can
grant nothing and has no name for whoami to print.
Installs that run as root, which is every compose file here, are
unchanged.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
|
||
|
|
a8364e34d2 |
feat(deploy): one worker container runs every background process
The stack was nine long-running containers: the web server, the database, the Mercure hub, and six of plMail's own image, one per background process. It is three now: php, worker and database, with secrets-init still running once and exiting. The worker runs `app:work`, a small supervisor keeping seven processes alive the way Docker's restart policy kept seven containers alive: the IMAP supervisor, one consumer per queue, the scheduler and the hub. A clean exit (the hourly recycle, the restart signal) comes straight back; a crash backs off from one second to thirty. Output passes through a whole line at a time, so two processes' JSON records never interleave, and SIGTERM reaches every child at once with 25s to finish. Each queue keeps a process of its own, which is the part the split was for: a consumer inside a long handler cannot take a send. And each process keeps its container's old name as APP_CONTAINER_NAME, so heartbeats, /healthz and the log filters are unchanged. The hub is the official Mercure 1.x binary, copied into the image and run as one of those processes. Not FrankenPHP's built-in hub, which is 0.24 and refuses the RFC 9068 tokens plMail has minted since v0.2.31. The worker answers to `mercure` on the network, so the Caddyfile proxy and MERCURE_URL reach it where they reached the hub container, and older compose files keep working with this image unchanged. It is configured from the application's own settings, including MERCURE_COOKIE_NAME, so a plain-HTTP install names the cookie once for both halves. A 5s shutdown grace stops it waiting on open subscriptions, which made every stop take the whole 25s. The dev override loses its per-worker blocks, one of which (worker-bulk) had been missing, so that queue ran the published image's code in development. The demo overlay runs app:work --without=imap-supervisor. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
4cbdb4f511 |
feat(admin): a version checker, with a channel to follow
Admin → Updates: an administrator picks which builds count as an update (Releases, the `latest` image; Main, the `main` image; or Off), and every hour app:updates:check asks whether that channel has a newer one. Until somebody picks, an installation follows the tag its own build came from, so a `main` image follows main and an unbuilt checkout asks nothing of anyone. The registry decides what exists: the channel's image is read the way `docker pull` finds it (OCI distribution API, anonymous token from the 401's challenge, the index entry for this architecture, the config's labels), so a commit that builds no image is never announced. GitHub's compare decides which way two builds differ, so a main build following releases is not told to go back to an older release, and its commit list is the page's "what's new". An image from anywhere but GitHub falls back to what the version numbers alone can say. Each new build is announced once: a push to every administrator's registered devices (a new UpdateAvailable payload the service worker shows), a badge beside the build chip in the admin header, and a dot on the nav entry. A check that gets no answer keeps the last one and says why, and the hourly command exits 0 for it: an installation behind a firewall is not a fault. Images are stamped with where they are published (APP_IMAGE), so a fork that publishes its own checks its own. The channel travels in config backups; what the last check found does not. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> |
||
|
|
3234fecbf6 |
Stamp the build after the vendors, not before them
The Dockerfile writes APP_VERSION and APP_COMMIT into the prod image so
the admin panel can say which build it is. Both sat above the vendor
install, and every layer after an ENV is keyed on that ENV's value — so
a commit hash, which is different on every commit by construction, meant
`composer install` was never once served from cache. The comment on the
line directly below promises exactly the opposite:
# prevent the reinstallation of vendors at every changes in the source code
Every build of every branch and tag therefore re-resolved and
re-downloaded 111 packages, and the per-architecture gha cache
docker.yml is careful to scope could only ever hit the layers above that
point.
The cost was minutes; the exposure is what found it. A step that runs on
every build is a step that needs the network on every build, and the
`main` image build failed twice at that line while the release tag of
the same commit went through on a retry — the difference being which one
happened to ask a package host during a bad minute. Self-hosters follow
`:main`, so that image had been stale since.
Moved below the install, keyed on composer.lock alone. It cannot go
further: `composer dump-env prod` below snapshots the environment, and
these have to be in it by then.
Verified under BuildKit, which is what CI builds with — podman cannot
answer the question, since its handling of `COPY --link` busts that
layer whatever the ENVs do:
before change only APP_COMMIT -> "Installing dependencies from lock file"
after change only APP_COMMIT -> "#15 CACHED"
and the stamp still reaches the image: APP_VERSION and APP_COMMIT are
both in its environment, where AppVersion reads them.
This removes the exposure rather than explaining the upstream failure —
the log for it is behind an authenticated endpoint and says only that
composer exited 100.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
||
|
|
25f1cf74c8 |
Say which build this is, in the admin header
Migrations run on boot and `latest` moves, so "is this the build I think it is?" is a question with real consequences and no cheap answer. The release and the commit now sit in the admin header — in the header rather than in a panel, because it is asked while looking at something else. Stamped at image build and never discovered at runtime. A container has no .git: the source is copied in and the history stays behind, so anything that shelled out to git in production would answer from a mounted volume or from nothing. The release workflow passes both values from the metadata action it already computes; a plain `docker build` passes neither, and an image nobody stamped says "development" rather than guessing, which is also why the chip is absent entirely on a checkout — a permanent "development" badge is furniture. There is deliberately no test that a real checkout describes itself. That would assert a property of wherever the suite runs rather than of the class: the test container has the source but not the history, and where it has both, git refuses the directory for dubious ownership — so it passes on a laptop, fails in CI, and says nothing either way. This suite has been bitten by that exact shape twice already (an id sequence gap that only existed on a well-used database, a sidebar state that only existed after another spec ran). The contract is tested instead: an unreadable repository means "development". config/reference.php is gitignored and untracked. Symfony regenerates it on every cache warm-up and not deterministically — two keys inside one @psalm-type block swap places — so a tracked copy showed a phantom diff after any command that booted the kernel, all session. Its own header says it is auto-generated and for apps only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
9833695a27 |
Add /healthz and app:backup
Two gaps that only matter on the day something goes wrong, which is the worst day to discover them. Every compose service but Mercure had `healthcheck: disable: true`, and the image's own HEALTHCHECK probed Caddy's metrics port — which answers as soon as the web server is listening, well before PHP can reach the database. A stack whose database was unreachable reported itself healthy, and `depends_on: service_healthy` waited for nothing worth waiting for. /healthz answers the question that was actually being asked, and the image now probes it. It is unauthenticated, because a probe needing a session is useless to Docker and to every monitoring tool, and therefore deliberately vague: it returns verdicts and nothing else. HealthTest asserts that shape, so a well-meant "add the counts, they're useful" fails the suite rather than quietly turning a public endpoint into a window on the instance. app:backup makes the README's oldest warning actionable. It has always said to back up the database and the encryption key separately and then left the reader to work out how — the most dangerous instruction in the project to get wrong, because both halves fail silently. A dump alone restores every mailbox with credentials nothing can decrypt. The command checks rather than assumes: on an install that supplies APP_ENCRYPTION_KEY from the environment there is nothing to copy, so it says the key is *not* in the backup instead of printing a reassuring warning about a file that does not contain it. That case is the one where a silent success would be worst. pg_dump comes from PGDG pinned to the server's major version. Debian's postgresql-client trails 18 and pg_dump refuses to dump a newer server, so the distro package would install cleanly and fail at the only moment it was ever needed. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
||
|
|
122006460d | onboarding. this is a lot | ||
|
|
92593a756a | small fixes. | ||
|
|
a243636a34 | this is now a PWA und has push | ||
|
|
23dc43255d | update | ||
|
|
76f890a49e | init |