Files
projectsend/resources/js/components/code-notice-banner.tsx
ignacionelson 8f12c83d21 Tell a clone-and-build install to rebuild, not to pull
ProjectSend prints the update instructions for the way this server was
installed, and it knew two answers where it needed three: anything inside
a container was handed `docker compose pull && docker compose up -d`. On
the Compose stack that builds from a checkout there is no image behind
those containers, so `pull` skips every ProjectSend service and `up -d`
then finds them all current — the update reports success, changes
nothing, and the dashboard goes on offering the same release. Reported by
@mueller7382, who stayed on 2.0.0 that way while 2.1.0 was out (#1661).

Those installations are now their own kind, told to `git pull` and
rebuild, with the two steps a checkout needs that an image does not: its
dependencies and its compiled frontend live outside git, so a release
that moved either leaves them stale.

Two signals decide it, in that order. The published image now declares
itself with PROJECTSEND_IMAGE, which is the only evidence an operator
bind-mounting over /var/www/html can neither hide nor forge; failing that
— images published before this — a working tree in the install directory,
which the image never has and the repository's own stack always does.
getenv() rather than env(), because a cached configuration makes env()
outside a config file return null, and the answer would flip silently on
exactly the installs most likely to have cached it.

The stale-code banner keeps treating both container kinds alike: what
clears it is recreating the container, whichever way its image was built.

The changelog also credits the reporter of #1663, which was missed when
that entry was written.
2026-08-21 14:35:49 -03:00

90 lines
4.2 KiB
TypeScript

import { usePage } from '@inertiajs/react';
import { AlertTriangle } from 'lucide-react';
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
import { isContainerInstall } from '@/components/update-instructions';
import { useTranslation } from '@/hooks/use-translation';
import { type SharedData } from '@/types';
/**
* Says out loud that this server is not running the code it was updated
* to — the one update failure that has no other symptom.
*
* With OPcache configured the way production guides recommend, replacing
* the files and reloading nothing leaves the database on the new version
* and every visitor on the old code, indefinitely and without an error
* anywhere. `projectsend:update` records what it applied; this compares it
* with what the process rendering this page actually compiled.
*
* Deliberately on every staff page rather than the dashboard alone: the
* person who missed the reload has no reason to suspect anything, so the
* notice has to find them rather than wait to be visited.
*/
export function CodeNoticeBanner() {
const { t } = useTranslation();
const { code_notice: notice } = usePage<SharedData>().props;
if (!notice) {
return null;
}
// Both container kinds answer this the same way: what fixes it is
// recreating the container, whether its image came from a registry or
// from a build on this machine.
const container = isContainerInstall(notice.install_kind);
// Stated as a fact first, cause second. A deliberate rollback lands
// here too, and telling somebody to reload PHP-FPM when they meant to
// go back a version would send them the wrong way.
const title =
notice.reason === 'stale_code'
? t('This server is running version :running, but version :applied is installed', {
running: notice.running,
applied: notice.applied,
})
: t('Version :running is installed, but the update has not been run', { running: notice.running });
return (
<Alert variant={notice.reason === 'stale_code' ? 'destructive' : 'warning'}>
<AlertTriangle className="size-4" />
<AlertTitle>{title}</AlertTitle>
<AlertDescription className="space-y-2">
{notice.reason === 'stale_code' ? (
<>
<p>
{container
? t('The database was updated to :applied, and this container is still running the code it started with.', {
applied: notice.applied,
})
: t(
'The database was updated to :applied, and PHP is still serving the code it had compiled before that. Reloading it clears this notice.',
{ applied: notice.applied },
)}
</p>
<Command>{container ? 'docker compose up -d --force-recreate' : 'sudo systemctl reload php8.4-fpm'}</Command>
{!container && (
<p className="text-xs">
{t('If you rolled back on purpose, run php artisan projectsend:update so the database matches the code you restored.')}
</p>
)}
</>
) : (
<>
<p>
{t(
'New files are in place, but the database has only been brought up to :applied. Until the update runs, this installation is running code its database does not match.',
{ applied: notice.applied },
)}
</p>
<Command>{container ? 'docker compose up -d --force-recreate' : 'sudo ./update.sh'}</Command>
</>
)}
</AlertDescription>
</Alert>
);
}
function Command({ children }: { children: string }) {
return <code className="bg-background/60 inline-block rounded px-2 py-1 font-mono text-xs">{children}</code>;
}