56 Commits (2321854967ffce9e12a91d78cf7df7aeac9243b9)
| Author | SHA1 | Message | Date |
|---|---|---|---|
|
|
4d4d1dd5d5 |
Stop the update agent dying on its own tag arithmetic
tests / pest (push) Failing after 8m4s
Details
tests / assets (push) Successful in 22s
Details
tests / release (push) Has been skipped
Details
Since 1.2.0 the agent has ended with a non-zero status on every tick, before writing anything. The console showed a check that never finished and a last-checked time frozen at the moment of the deployment, and the update button could not be reached at all — the one mechanism that would have delivered the fix. Two instances of the same trap, both mine, both under `set -Eeuo pipefail`: The tag count was `release_version_gt … && echo`. The LAST iteration is almost always a tag that is not newer, so the loop ended non-zero, pipefail carried it out of the pipeline, the command substitution inherited it, and set -e ended the agent. `if` rather than `&&` is the whole fix: an if whose condition is false still returns 0. The newest tag came from `git tag … | head -1`. head exits after one line, git takes SIGPIPE, pipefail does the rest. install-agent.sh has carried a written warning about exactly this since it was written; I read it and wrote it anyway. Both now live in deploy/lib/release.sh, because the reason neither was caught is that no PHP test can reach a bash pipeline. tests/Feature/ReleaseComparisonTest.php runs the real functions in a real throwaway repository under the agent's own set -Eeuo pipefail, and asserts the EXIT CODE as well as the answer — an answer nobody lives to read is not an answer. The SIGPIPE case needs four hundred tags to reproduce, because a short list finishes writing before head leaves. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
6998a22527 |
Repair ownership by looking inside, not at the door
tests / pest (push) Failing after 8m2s
Details
tests / assets (push) Successful in 21s
Details
tests / release (push) Has been skipped
Details
The ownership repair added in 1.1.1 sampled the owner of each top-level directory and skipped the recursion when it already matched. node_modules was owned by www-data; node_modules/.vite-temp, left behind by an earlier root build, was not. So the repair walked past it, `npm run build` failed with EACCES, and the deployment stopped in maintenance mode — on the release whose whole point was that this could not happen. A directory's owner says nothing about its contents. It now walks each tree once with find and changes only the entries that are wrong, which is also cheaper than the detect-then-chown-everything it replaces. Verified against the shape that actually failed: a root-owned file inside a www-data-owned node_modules, repaired. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
c7a3224466 |
Cut 1.2.0
tests / pest (push) Failing after 7m51s
Details
tests / assets (push) Successful in 23s
Details
tests / release (push) Has been skipped
Details
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
00b9107ee3 |
Deploy as the application's user, and take back what root already took
tests / pest (push) Failing after 7m19s
Details
tests / assets (push) Successful in 19s
Details
tests / release (push) Has been skipped
Details
The 500 on the VPN config download was never in the VPN code. storage/logs/ laravel.log was owned by root, mode 644, since 27.07 04:16 — the moment a failed `artisan optimize` wrote its error there during a deployment. From then on the application could not append to its own log: Monolog threw on every attempt, and a throw while logging is a 500 on any page that logs, with nothing written down to say why. The error page said "Er wurde protokolliert". It was not. It surfaced on the config download because that is one of the few pages writing a log line on its way through — on success and on a wrong password alike. Pages that log nothing were unaffected, which is exactly why it looked like a VPN fault for days. `docker compose exec` is root unless told otherwise, and every deployment script relied on that default — including the agent's allowlist sync, which runs every minute. docker/entrypoint.sh had it right all along and drops to www-data for precisely these commands. Now so does everything else, help text included: telling an operator to run artisan as root is how the file ends up owned by root in the first place. update.sh also repairs what an earlier run left behind, before its first unprivileged step rather than after — a root-owned log file is not self-healing, the page that trips over it is nowhere near the deployment that caused it, and with in_app unprivileged a root-owned vendor/ would break the next composer step outright. tests/Feature/DeploymentRunsAsTheAppUserTest.php holds the line. It was checked against the previous commit and finds all nine places that were wrong. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
21b7aaca42 |
Update only to a released version, never to whatever landed on main
tests / pest (push) Failing after 7m21s
Details
tests / assets (push) Successful in 20s
Details
tests / release (push) Has been skipped
Details
Asked for directly: a commit on main must change nothing on a server, and only a tag whose version is higher than the installed one may be installed. The agent asked two different questions before this. A pinned server compared tags; everything else counted commits on its branch — so a console following main offered an update for every push, verified or not, and "Jetzt aktualisieren" meant "take whatever is on the branch right now". Landing on main and being released are different events, and only the second is a decision somebody made. It is one question now, regardless of what the checkout is attached to: is there a tag whose version is higher than the deployed one? Compared with sort -V against the version in the deployment manifest — not the VERSION file, which an update moves before it migrates, so a run that failed in between would leave the checkout claiming a version it never finished installing. A run always targets that tag; the branch path is gone. A request that arrives with nothing to install is answered rather than obeyed, before anything announces a run. The console follows: the button is disabled with nothing released, says so in a line rather than leaving a grey button to be guessed at, names the version it would install rather than a count, and refuses the action server-side as well — a Livewire action is a public endpoint, and one place a rule may not live is only in the disabled attribute of a button. VERSION becomes 1.1.0, to be tagged as the first release this mechanism can see. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |
|
|
|
e9240c1324 |
feat(deploy): releases you can pin to, and a version that tells the truth
VERSION at the repository root is the release number and the only place it is written down — a file rather than `git describe`, because a source archive, a shallow CI checkout and a container built without .git have no tags to describe. Cutting a release is editing that file and merging it; CI creates the annotated v<VERSION> tag once green, only on the commit that raised the number, and never moves it. Servers are pinned to these. Two modes, and the difference is the point. RELEASE=v1.0.0 pins a server to an immutable tag, checked out detached: it moves when someone decides it moves. BRANCH=main follows the edge, where CI tags a commit only after it is pushed. The mode is remembered, so re-running the updater on a pinned box does not quietly walk it back onto main. Going backwards is refused, by commit ancestry rather than by comparing version strings — a tag can be cut from anywhere, and only ancestry says whether history is going back. The schema has already moved forward by then, and the migrations needed to reverse it are not in the older checkout at all. What the console reports comes from a manifest written atomically after every step succeeded, never from live git: git says what the files are, not whether the deployment came up. So a failed update keeps reporting the version that is actually serving — including its version number, not the newer one the checkout has already moved to. A deployment pinned to the tag reads "1.0.0 (abc1234)"; anything else reads "1.0.0-dev (abc1234) · main", because every commit after the tag still carries VERSION=1.0.0 and is not that release. Reporting it as one is how a bug gets filed against the wrong code. Codex reviewed the design before it was written and the result six times after. Its findings, all real: the version had to come from the manifest too, not just the commit; branch names need JSON escaping or the manifest silently vanishes; a `case` glob does not anchor and accepted 1x.2y.3garbage; pinning to the commit a server already sits on skipped recording the mode, and then skipped detaching; a manifest that failed to write would never be repaired; an unparseable timestamp would 500 every admin page; and an empty tag list read as a connectivity failure. 466 tests green. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> |