mirror of
https://github.com/Studio-Saelix/sencho.git
synced 2026-08-19 23:06:49 +00:00
feat: add build-aware compose stack updates (#1561)
Detect services with build: in the update preview and run compose build --pull plus pull --ignore-buildable when Update is triggered on those stacks, while keeping the existing pull-only path for image-only stacks.
This commit is contained in:
@@ -23,16 +23,21 @@ import StackAnatomyPanel from './StackAnatomyPanel';
|
||||
|
||||
const COMPOSE = 'services:\n web:\n image: nginx:1.25\n';
|
||||
|
||||
function previewBody(hasUpdate: boolean) {
|
||||
function previewBody(hasUpdate: boolean, buildServices: string[] = []) {
|
||||
const hasBuild = buildServices.length > 0;
|
||||
return {
|
||||
build_services: buildServices,
|
||||
summary: {
|
||||
has_update: hasUpdate,
|
||||
primary_image: 'nginx',
|
||||
current_tag: '1.25',
|
||||
next_tag: '1.26',
|
||||
semver_bump: 'minor',
|
||||
update_kind: hasUpdate ? 'tag' : 'none',
|
||||
blocked: false,
|
||||
blocked_reason: null,
|
||||
has_build_services: hasBuild,
|
||||
rebuild_available: hasBuild,
|
||||
},
|
||||
changelog: null,
|
||||
};
|
||||
@@ -86,6 +91,21 @@ describe('StackAnatomyPanel update banner', () => {
|
||||
expect(onApply).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('shows Rebuild & Update for build-only stacks', async () => {
|
||||
vi.mocked(apiFetch).mockImplementation(async (input: RequestInfo | URL) => {
|
||||
const url = String(input);
|
||||
if (url.includes('/update-preview')) return jsonRes(previewBody(false, ['app']));
|
||||
if (url.includes('/scan-status')) return jsonRes({ status: 'ok' });
|
||||
return jsonRes(null, false);
|
||||
});
|
||||
|
||||
render(panel(false));
|
||||
|
||||
expect(await screen.findByTestId('update-available-banner')).toBeInTheDocument();
|
||||
expect(screen.getByText(/Rebuild available/i)).toBeInTheDocument();
|
||||
expect(screen.getByRole('button', { name: 'Rebuild & Update' })).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('disables the apply button and shows progress while applying', async () => {
|
||||
const onApply = vi.fn();
|
||||
const { rerender } = render(panel(false, onApply));
|
||||
|
||||
@@ -39,6 +39,7 @@ interface StackAnatomyPanelProps {
|
||||
}
|
||||
|
||||
type SemverBump = 'none' | 'patch' | 'minor' | 'major' | 'unknown';
|
||||
type UpdateKind = 'tag' | 'digest' | 'none';
|
||||
|
||||
interface UpdatePreviewSummary {
|
||||
has_update: boolean;
|
||||
@@ -46,12 +47,16 @@ interface UpdatePreviewSummary {
|
||||
current_tag: string | null;
|
||||
next_tag: string | null;
|
||||
semver_bump: SemverBump;
|
||||
update_kind?: UpdateKind;
|
||||
blocked: boolean;
|
||||
blocked_reason: string | null;
|
||||
has_build_services: boolean;
|
||||
rebuild_available: boolean;
|
||||
}
|
||||
|
||||
interface UpdatePreview {
|
||||
summary: UpdatePreviewSummary;
|
||||
build_services?: string[];
|
||||
changelog: string | null;
|
||||
}
|
||||
|
||||
@@ -335,6 +340,10 @@ export default function StackAnatomyPanel({
|
||||
|
||||
const bump = updatePreview?.summary.semver_bump ?? 'none';
|
||||
const hasUpdate = Boolean(updatePreview?.summary.has_update);
|
||||
const hasBuildServices = Boolean(updatePreview?.summary.has_build_services);
|
||||
const rebuildAvailable = Boolean(updatePreview?.summary.rebuild_available);
|
||||
const showUpdateBanner = hasUpdate || rebuildAvailable;
|
||||
const updateKind = updatePreview?.summary.update_kind ?? 'none';
|
||||
const blocked = Boolean(updatePreview?.summary.blocked);
|
||||
const bannerSeverity: 'danger' | 'warn' | 'ok' = bump === 'major' || blocked
|
||||
? 'danger'
|
||||
@@ -352,13 +361,29 @@ export default function StackAnatomyPanel({
|
||||
const bumpLabel = bump === 'none' || bump === 'unknown' ? '' : `${bump}`;
|
||||
const bannerLeadIn = blocked
|
||||
? 'review required'
|
||||
: bump === 'patch'
|
||||
? 'safe to apply'
|
||||
: bump === 'minor'
|
||||
? 'review recommended'
|
||||
: bump === 'major'
|
||||
? 'breaking changes possible'
|
||||
: '';
|
||||
: hasUpdate && updateKind === 'digest'
|
||||
? 'same-tag digest rebuild'
|
||||
: hasUpdate && hasBuildServices
|
||||
? 'registry update + local rebuild'
|
||||
: rebuildAvailable && !hasUpdate
|
||||
? 'local build / rebuild required'
|
||||
: bump === 'patch'
|
||||
? 'safe to apply'
|
||||
: bump === 'minor'
|
||||
? 'review recommended'
|
||||
: bump === 'major'
|
||||
? 'breaking changes possible'
|
||||
: '';
|
||||
const buildServiceNames = updatePreview?.build_services ?? [];
|
||||
const buildHint = hasBuildServices
|
||||
? `Rebuilds ${buildServiceNames.length} local build service${buildServiceNames.length === 1 ? '' : 's'} from Dockerfile context; may take longer and needs network access for base images.`
|
||||
: '';
|
||||
const gitRebuildHint = hasBuildServices && activeGitSource
|
||||
? 'After applying Git source changes, use Rebuild & Update to deploy the updated source.'
|
||||
: '';
|
||||
const applyLabel = hasBuildServices
|
||||
? (applying ? 'rebuilding...' : 'Rebuild & Update')
|
||||
: (applying ? 'applying...' : 'apply');
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 flex-col rounded-xl border border-muted bg-card/40">
|
||||
@@ -529,13 +554,13 @@ export default function StackAnatomyPanel({
|
||||
</Row>
|
||||
</>
|
||||
)}
|
||||
{hasUpdate && updatePreview && (
|
||||
{showUpdateBanner && updatePreview && (
|
||||
<div data-testid="update-available-banner" className={cn('mt-3 mb-3 rounded-lg border p-3', bannerTone)}>
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<div className="font-mono text-xs uppercase tracking-wide">
|
||||
Update available
|
||||
{updatePreview.summary.current_tag && updatePreview.summary.next_tag && (
|
||||
{hasBuildServices && !hasUpdate ? 'Rebuild available' : 'Update available'}
|
||||
{updatePreview.summary.current_tag && updatePreview.summary.next_tag && hasUpdate && (
|
||||
<span className="text-foreground">
|
||||
{' · '}
|
||||
<span className="text-stat-subtitle">{updatePreview.summary.current_tag}</span>
|
||||
@@ -548,6 +573,8 @@ export default function StackAnatomyPanel({
|
||||
{[
|
||||
bumpLabel,
|
||||
bannerLeadIn,
|
||||
buildHint,
|
||||
gitRebuildHint,
|
||||
updatePreview.changelog ? updatePreview.changelog.split(/[.\n]/)[0] : '',
|
||||
].filter(Boolean).join(' · ')}
|
||||
</div>
|
||||
@@ -565,7 +592,7 @@ export default function StackAnatomyPanel({
|
||||
onClick={onApplyUpdate}
|
||||
>
|
||||
<Rocket className={cn('h-3 w-3', applying && 'animate-pulse')} strokeWidth={1.5} />
|
||||
{applying ? 'applying...' : 'apply'}
|
||||
{applyLabel}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user