diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..4c49bd7
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1 @@
+.env
diff --git a/docker-compose.yaml b/docker-compose.yaml
index f6c0883..06c52b9 100644
--- a/docker-compose.yaml
+++ b/docker-compose.yaml
@@ -15,10 +15,11 @@ services:
dockerfile: Dockerfile
environment:
- XDG_DATA_HOME=/data
- - GITEA_REGISTRY_RO_TOKEN=
- - GITEA_RW_PACKAGE_TOKEN=
- - GITEA_ORG=
- - GITEA_USER=
+ - GITEA_REGISTRY_RO_TOKEN=${GITEA_REGISTRY_RO_TOKEN}
+ - GITEA_RW_PACKAGE_TOKEN=${GITEA_RW_PACKAGE_TOKEN}
+ - GITEA_ORG=${GITEA_ORG:-Dcntrlized}
+ - GITEA_USER=${GITEA_USER:-Admin}
+ - GIT_TOKEN=${GIT_TOKEN}
networks:
dcntrlized_net:
diff --git a/src/frontend/src/app.tsx b/src/frontend/src/app.tsx
index 8ed6ec6..f304b34 100644
--- a/src/frontend/src/app.tsx
+++ b/src/frontend/src/app.tsx
@@ -292,7 +292,7 @@ export function App() {
)}
{!isRemoteOnly && container.version && (
- Installed Ver:
+ Installed Version:
{container.version}
)}
diff --git a/src/handlers/registry/registry.go b/src/handlers/registry/registry.go
index 38d3402..772102a 100644
--- a/src/handlers/registry/registry.go
+++ b/src/handlers/registry/registry.go
@@ -63,12 +63,32 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) {
groupedPackages[pkg.Name] = append(groupedPackages[pkg.Name], pkg)
}
- // Helper function to normalize version strings for semver package (requires 'v' prefix)
- formatSemver := func(v string) string {
- if !strings.HasPrefix(v, "v") {
- return "v" + v
+ // normalizeSemver expands partial version tags to the full X.Y.Z form,
+ // preserving an optional pre-release suffix (v3 -> 3.0.0, v3.1 -> 3.1.0,
+ // v3.3.3-hotfix -> 3.3.3-hotfix).
+ normalizeSemver := func(v string) string {
+ tag := strings.TrimPrefix(v, "v")
+ prerelease := ""
+ if i := strings.IndexByte(tag, '-'); i >= 0 {
+ prerelease = tag[i:]
+ tag = tag[:i]
}
- return v
+ parts := strings.Split(tag, ".")
+ for len(parts) < 3 {
+ parts = append(parts, "0")
+ }
+ return strings.Join(parts, ".") + prerelease
+ }
+
+ // semverSpecificity counts the dot-separated segments of a version's
+ // major.minor.patch part, so that among versions that normalize equal the most
+ // complete one wins (3.4.0 over 3.4).
+ semverSpecificity := func(v string) int {
+ tag := strings.TrimPrefix(v, "v")
+ if i := strings.IndexByte(tag, '-'); i >= 0 {
+ tag = tag[:i]
+ }
+ return strings.Count(tag, ".")
}
// 3. For each container name, find the package entry with the highest semver
@@ -79,15 +99,25 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) {
}
sort.Slice(pkgs, func(i, j int) bool {
- vI := formatSemver(pkgs[i].Version)
- vJ := formatSemver(pkgs[j].Version)
+ nI := normalizeSemver(pkgs[i].Version)
+ nJ := normalizeSemver(pkgs[j].Version)
+ vI := "v" + nI
+ vJ := "v" + nJ
// Compare using semver package
if semver.IsValid(vI) && semver.IsValid(vJ) {
- return semver.Compare(vI, vJ) > 0
+ if c := semver.Compare(vI, vJ); c != 0 {
+ return c > 0
+ }
+ } else if nI != nJ {
+ return nI > nJ
}
- // Fall back to string comparison if not valid semver
- return pkgs[i].Version > pkgs[j].Version
+ // Tie-break on specificity (most complete wins)
+ if sI, sJ := semverSpecificity(pkgs[i].Version), semverSpecificity(pkgs[j].Version); sI != sJ {
+ return sI > sJ
+ }
+ // Tie-break on creation time (newest wins)
+ return pkgs[i].CreatedAt > pkgs[j].CreatedAt
})
// First item after descending sort is the highest version
@@ -115,7 +145,7 @@ func GetRegistry(w http.ResponseWriter, r *http.Request) {
item := types.RegistryContainer{
Name: pkg.Name,
- Version: pkg.Version,
+ Version: normalizeSemver(pkg.Version),
Image: imageRef,
Source: source,
CreatedAt: pkg.CreatedAt,
diff --git a/src/lib/containers/start.go b/src/lib/containers/start.go
index 916ea8d..aed8a10 100644
--- a/src/lib/containers/start.go
+++ b/src/lib/containers/start.go
@@ -38,6 +38,7 @@ func StartContainer(ctx context.Context, item types.RegistryContainer) error {
}
}
}
+ pinServiceImages(project, installedImage)
ensureServiceImages(ctx, dc, project, installedImage)
composeService, err := newComposeService(dc)
@@ -61,6 +62,26 @@ func StartContainer(ctx context.Context, item types.RegistryContainer) error {
return nil
}
+// pinServiceImages rewrites each compose service's image reference to the exact
+// installed image (e.g. git.../testservice:3.4.0) when its base name matches, so
+// containers run pinned to a concrete semver tag instead of a moving "latest" tag.
+func pinServiceImages(project *composetypes.Project, installedImage string) {
+ if installedImage == "" {
+ return
+ }
+ for name, s := range project.Services {
+ if s.Image == "" || s.Image == installedImage {
+ continue
+ }
+ if baseImageName(s.Image) != baseImageName(installedImage) {
+ continue
+ }
+ logger.Info("Pinning service %s image %s to installed image %s", name, s.Image, installedImage)
+ s.Image = installedImage
+ project.Services[name] = s
+ }
+}
+
// ensureServiceImages makes sure every image referenced by the compose project
// exists locally. Registries install the image under its full reference (e.g.
// git.../testservice:3.4) while the service's compose file often references it
diff --git a/src/lib/containers/watcher.go b/src/lib/containers/watcher.go
index 70db30f..1079a38 100644
--- a/src/lib/containers/watcher.go
+++ b/src/lib/containers/watcher.go
@@ -84,6 +84,9 @@ func (cache *ContainerCache) WatchContainers(ctx context.Context, dc *docker.Doc
// Save to Cache
log.Printf("Service started. Adding container %s - %s to cache", host, containerName)
existing, _ := cache.services[key]
+ if existing.Version != "" && existing.Version != "latest" && (ver == "" || ver == "latest") {
+ ver = existing.Version
+ }
container := types.Container{
ID: containerID,
ServiceName: serviceLabel,