detect newer version tags for pinned images and show them as a badge with release notes
This commit is contained in:
4 files changed
+42
-20
No files matched your search
@@ -7,6 +7,7 @@
|
||||
import { copyToClipboard } from '$lib/utils/clipboard';
|
||||
import { renderMarkdown } from '$lib/utils/markdown';
|
||||
import { formatDate } from '$lib/stores/settings';
|
||||
import { repoBaseOf } from '$lib/utils/pinned-ref';
|
||||
import type { NewerVersion } from '$lib/types';
|
||||
|
||||
// Minimal shape shared by ContainerInfo and StackContainer - all the modal needs.
|
||||
@@ -93,11 +94,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
// Repo without the tag, e.g. `ghcr.io/owner/app` from `ghcr.io/owner/app:1.2`.
|
||||
const repoBase = $derived.by(() => {
|
||||
const img = container?.image ?? '';
|
||||
return img.slice(0, img.lastIndexOf(':') > img.lastIndexOf('/') ? img.lastIndexOf(':') : img.length);
|
||||
});
|
||||
// Repo without the tag OR digest, e.g. `ghcr.io/owner/app` from
|
||||
// `ghcr.io/owner/app:1.2@sha256:...` - a running image is often digest-pinned,
|
||||
// whose `@sha256:` colon must not be mistaken for the tag separator (#1437).
|
||||
const repoBase = $derived(repoBaseOf(container?.image ?? ''));
|
||||
|
||||
async function copyTag() {
|
||||
if (!container || !newerVersion) return;
|
||||
|
||||
+11
-10
@@ -19,6 +19,7 @@ import {
|
||||
type DeletionSkipReason
|
||||
} from './git-deletions';
|
||||
import { isAllowedStackFilename } from './stack-filename';
|
||||
import { deriveStackStatus } from './stack-status';
|
||||
import {
|
||||
getEnvironment,
|
||||
getSecretEnvVarsAsRecord,
|
||||
@@ -110,7 +111,7 @@ export interface ComposeStackInfo {
|
||||
name: string;
|
||||
containers: string[];
|
||||
containerDetails: ContainerDetail[];
|
||||
status: 'running' | 'stopped' | 'partial' | 'created';
|
||||
status: 'running' | 'stopped' | 'partial' | 'restarting' | 'created';
|
||||
sourceType?: StackSourceType;
|
||||
hasComposeFile?: boolean;
|
||||
}
|
||||
@@ -1884,12 +1885,14 @@ export async function listComposeStacks(envId?: number | null): Promise<ComposeS
|
||||
const result: ComposeStackInfo[] = Array.from(stacks.entries()).map(([name, containerIds]) => {
|
||||
const stackContainers = containers.filter((c) => containerIds.has(c.id));
|
||||
const runningCount = stackContainers.filter((c) => c.state === 'running').length;
|
||||
// A container in a restart loop is 'restarting' - it is NOT stopped (it is actively
|
||||
// trying to come up), so the stack must expose Stop, not Start (#1438).
|
||||
const restartingCount = stackContainers.filter((c) => c.state === 'restarting').length;
|
||||
// Containers that exited with code 0 are "completed" (e.g., init/migration containers)
|
||||
// and should not count against stack health
|
||||
const completedCount = stackContainers.filter((c) =>
|
||||
c.state === 'exited' && c.exitCode === 0
|
||||
).length;
|
||||
const activeTotal = stackContainers.length - completedCount;
|
||||
|
||||
const containerDetails: ContainerDetail[] = stackContainers
|
||||
.map((c) => {
|
||||
@@ -1947,14 +1950,12 @@ export async function listComposeStacks(envId?: number | null): Promise<ComposeS
|
||||
updateCount: stackContainers.filter((c) => pendingUpdateIds.has(c.id)).length,
|
||||
// Newer-version-tag (semver) suggestions in this stack - drives the Tag badge.
|
||||
newerVersionCount: stackContainers.filter((c) => newerVersionIds.has(c.id)).length,
|
||||
status:
|
||||
activeTotal === 0
|
||||
? 'stopped'
|
||||
: runningCount >= activeTotal
|
||||
? 'running'
|
||||
: runningCount === 0
|
||||
? 'stopped'
|
||||
: 'partial'
|
||||
status: deriveStackStatus({
|
||||
total: stackContainers.length,
|
||||
running: runningCount,
|
||||
restarting: restartingCount,
|
||||
completed: completedCount
|
||||
})
|
||||
};
|
||||
});
|
||||
|
||||
|
||||
@@ -11,13 +11,31 @@
|
||||
*/
|
||||
|
||||
/** Split `repo:tag` (or a bare `repo`) into its repo and tag parts. A colon that
|
||||
* is part of a registry `host:port` is not a tag separator. */
|
||||
* is part of a registry `host:port` is not a tag separator. A digest suffix
|
||||
* (`@sha256:...`) is stripped first so its colon is never mistaken for the tag. */
|
||||
function splitRepoTag(fullRef: string): { repo: string; tag: string | null } {
|
||||
const lastColon = fullRef.lastIndexOf(':');
|
||||
if (lastColon === -1) return { repo: fullRef, tag: null };
|
||||
const ref = stripDigest(fullRef);
|
||||
const lastColon = ref.lastIndexOf(':');
|
||||
if (lastColon === -1) return { repo: ref, tag: null };
|
||||
// A tag never contains a slash; `host:port/repo` has the slash AFTER the colon.
|
||||
if (fullRef.slice(lastColon + 1).includes('/')) return { repo: fullRef, tag: null };
|
||||
return { repo: fullRef.slice(0, lastColon), tag: fullRef.slice(lastColon + 1) };
|
||||
if (ref.slice(lastColon + 1).includes('/')) return { repo: ref, tag: null };
|
||||
return { repo: ref.slice(0, lastColon), tag: ref.slice(lastColon + 1) };
|
||||
}
|
||||
|
||||
/** Drop a `@sha256:...` (or any `@digest`) suffix from an image reference. */
|
||||
function stripDigest(ref: string): string {
|
||||
const at = ref.indexOf('@');
|
||||
return at === -1 ? ref : ref.slice(0, at);
|
||||
}
|
||||
|
||||
/**
|
||||
* The repo part of a running image reference, WITHOUT its tag or digest.
|
||||
* `ghcr.io/o/app:1.2@sha256:abc` -> `ghcr.io/o/app`, `nginx:1.25` -> `nginx`.
|
||||
* A digest-pinned running image is why the naive "cut at the last colon"
|
||||
* approach breaks: that colon lives inside `@sha256:` (#1437).
|
||||
*/
|
||||
export function repoBaseOf(image: string): string {
|
||||
return splitRepoTag(image).repo;
|
||||
}
|
||||
|
||||
/** The `sha256:...` part of a `repo@sha256:...` digest, or null. */
|
||||
|
||||
@@ -362,6 +362,7 @@
|
||||
const stackStatusTypes = [
|
||||
{ value: 'running', label: 'Running', icon: Play, color: 'text-emerald-500' },
|
||||
{ value: 'partial', label: 'Partial', icon: CircleDashed, color: 'text-amber-500' },
|
||||
{ value: 'restarting', label: 'Restarting', icon: RotateCw, color: 'text-orange-500' },
|
||||
{ value: 'stopped', label: 'Stopped', icon: Square, color: 'text-rose-500' },
|
||||
{ value: 'created', label: 'Created', icon: CircleDashed, color: 'text-slate-500' },
|
||||
{ value: 'not deployed', label: 'Not deployed', icon: Rocket, color: 'text-violet-500' }
|
||||
@@ -1131,6 +1132,8 @@
|
||||
return `${base} bg-red-200 dark:bg-red-800 text-red-900 dark:text-red-100`;
|
||||
case 'partial':
|
||||
return `${base} bg-amber-200 dark:bg-amber-800 text-amber-900 dark:text-amber-100`;
|
||||
case 'restarting':
|
||||
return `${base} bg-orange-200 dark:bg-orange-800 text-orange-900 dark:text-orange-100`;
|
||||
case 'created':
|
||||
return `${base} bg-slate-200 dark:bg-slate-700 text-slate-900 dark:text-slate-100`;
|
||||
case 'not deployed':
|
||||
|
||||
Reference in new issue
Block a user