Files
projectsend/resources/js/components/update-instructions.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

111 lines
4.7 KiB
TypeScript

import { UpdateOptionsDialog } from '@/components/update-options-dialog';
import { useTranslation } from '@/hooks/use-translation';
import { cn } from '@/lib/utils';
export type InstallKind = 'container' | 'container-source' | 'manual';
/**
* Whether this installation runs in a container, whichever way it got
* there. Everything that is about the runtime — restarting it, recreating
* it — is the same answer for both container kinds; only the upgrade
* itself differs.
*/
export function isContainerInstall(kind: InstallKind): boolean {
return kind === 'container' || kind === 'container-source';
}
/**
* How to actually apply an available update, for this server.
*
* One component rather than a string in each place that needs it, because
* there were two places and they were both wrong in the same way: each
* hardcoded `docker compose pull && docker compose up -d`, written back when
* Docker was the only supported way to install. Somebody who unpacked a
* release zip onto their own server was told to run a command they do not
* have, for a stack they are not using, at the exact moment they were trying
* to do the right thing.
*
* The same string was wrong a second way, for containers this time: a stack
* built from a checkout has no image to pull, so both commands succeed,
* report success, and change nothing at all. That install then keeps being
* offered the same release for as long as its operator keeps following the
* instructions on this screen (#1661) — which is why 'container' and
* 'container-source' are told different things.
*
* `compact` is for the dashboard card, a narrow column beside other widgets.
* `codeClassName` exists for the same caller — its code sits inside a warning
* alert and has to match it.
*
* This used to print the whole nine-command sequence, which was a third copy
* of something that also lived in INSTALL.md and UPDATE.md and had already
* drifted from both. It is now one script, and the reason it can be is that
* projectsend:update owns every part of an update that does not need root —
* see UpdateInstallation.
*/
export function UpdateInstructions({
kind,
compact = false,
codeClassName,
}: {
kind: InstallKind;
compact?: boolean;
codeClassName?: string;
}) {
const { t } = useTranslation();
if (kind === 'container') {
return (
<p>
{t('To update, run:')} <code className={cn('rounded px-2 py-1.5', codeClassName ?? 'bg-muted')}>docker compose pull && docker compose up -d</code>
</p>
);
}
if (kind === 'container-source') {
return (
<>
{/* On its own line rather than inline like the image's one-liner:
this command is long enough to wrap inside the dashboard's
narrow column, and a command split across two lines mid-word
is one somebody retypes wrongly. */}
<p className="text-muted-foreground mb-1">{t('To update, run this in the directory you cloned:')}</p>
<code className={cn('block rounded px-2 py-1.5 break-words', codeClassName ?? 'bg-muted')}>git pull && docker compose up -d --build</code>
<p className="text-muted-foreground mt-1 text-xs">
{t(
'This installation builds its own images, so pulling one changes nothing. If the release moved composer.lock or the frontend, run composer install and npm run build as well — UPDATE.md has the full procedure.',
)}
</p>
</>
);
}
if (compact) {
return (
<div className="text-muted-foreground space-y-1">
<p>{t('To update, run sudo ./update.sh in the install directory — it asks before it downloads or changes anything.')}</p>
<UpdateOptionsDialog />
</div>
);
}
return (
<>
<p className="text-muted-foreground mb-1">{t('To update, run this in the install directory:')}</p>
<code className={cn('block rounded px-2 py-1.5 whitespace-pre-wrap', codeClassName ?? 'bg-muted')}>
{[
'cd /var/www/projectsend',
'sudo ./update.sh',
].join('\n')}
</code>
<p className="text-muted-foreground mt-1 text-xs">
{t(
'It asks before checking for a release, before downloading one, and before touching your installation — and verifies the checksum of what it downloads. UPDATE.md has the full procedure.',
)}
</p>
<div className="mt-1">
<UpdateOptionsDialog />
</div>
</>
);
}