Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package apko for openSUSE:Factory checked in at 2025-12-19 16:44:04 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/apko (Old) and /work/SRC/openSUSE:Factory/.apko.new.1928 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "apko" Fri Dec 19 16:44:04 2025 rev:84 rq:1323612 version:0.30.34 Changes: -------- --- /work/SRC/openSUSE:Factory/apko/apko.changes 2025-12-18 18:35:37.284121820 +0100 +++ /work/SRC/openSUSE:Factory/.apko.new.1928/apko.changes 2025-12-19 16:47:13.133316812 +0100 @@ -1,0 +2,6 @@ +Fri Dec 19 08:12:34 UTC 2025 - Johannes Kastl <[email protected]> + +- Update to version 0.30.34: + * cli/dot: Rendering improvements (#1992) + +------------------------------------------------------------------- Old: ---- apko-0.30.33.obscpio New: ---- apko-0.30.34.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ apko.spec ++++++ --- /var/tmp/diff_new_pack.aPla49/_old 2025-12-19 16:47:18.813554351 +0100 +++ /var/tmp/diff_new_pack.aPla49/_new 2025-12-19 16:47:18.817554519 +0100 @@ -17,7 +17,7 @@ Name: apko -Version: 0.30.33 +Version: 0.30.34 Release: 0 Summary: Build OCI images from APK packages directly without Dockerfile License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.aPla49/_old 2025-12-19 16:47:18.877557028 +0100 +++ /var/tmp/diff_new_pack.aPla49/_new 2025-12-19 16:47:18.885557362 +0100 @@ -3,7 +3,7 @@ <param name="url">https://github.com/chainguard-dev/apko</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">v0.30.33</param> + <param name="revision">v0.30.34</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.aPla49/_old 2025-12-19 16:47:18.929559202 +0100 +++ /var/tmp/diff_new_pack.aPla49/_new 2025-12-19 16:47:18.933559370 +0100 @@ -1,6 +1,6 @@ <servicedata> <service name="tar_scm"> <param name="url">https://github.com/chainguard-dev/apko</param> - <param name="changesrevision">53957f9ee52c20f0db840d578fe5969ba49cc133</param></service></servicedata> + <param name="changesrevision">347c2ffac0648e5859d6965b8201c09c211ee7ae</param></service></servicedata> (No newline at EOF) ++++++ apko-0.30.33.obscpio -> apko-0.30.34.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-0.30.33/internal/cli/dot.go new/apko-0.30.34/internal/cli/dot.go --- old/apko-0.30.33/internal/cli/dot.go 2025-12-17 15:28:16.000000000 +0100 +++ new/apko-0.30.34/internal/cli/dot.go 2025-12-17 23:43:57.000000000 +0100 @@ -27,6 +27,7 @@ "strings" "time" + "github.com/charmbracelet/log" "github.com/pkg/browser" "github.com/spf13/cobra" "github.com/tmc/dot" @@ -40,6 +41,8 @@ "chainguard.dev/apko/pkg/build/types" ) +var extRegistryViewer string + func dotcmd() *cobra.Command { var extraKeys []string var extraBuildRepos []string @@ -85,6 +88,7 @@ cmd.Flags().BoolVar(&web, "web", false, "launch a browser") cmd.Flags().StringVar(&cacheDir, "cache-dir", "", "directory to use for caching apk packages and indexes (default '' means to use system-defined cache directory)") cmd.Flags().BoolVar(&offline, "offline", false, "do not use network to fetch packages (cache must be pre-populated)") + cmd.Flags().StringVarP(&extRegistryViewer, "registry-explorer", "e", "apk.dag.dev", "FQDN of the registry explorer that rendered nodes in SVG will link to.") return cmd } @@ -156,6 +160,7 @@ render := func(args []string) *dot.Graph { edges := map[string]struct{}{} deps := map[string]struct{}{} + addedNodes := map[string]*dot.Node{} out := dot.NewGraph("images") if err := out.Set("rankdir", "LR"); err != nil { @@ -165,38 +170,108 @@ panic(err) } + // Helper function to add an edge with a tooltip describing both nodes and dependency type + addEdgeWithTooltip := func(from, to *dot.Node, fromName, toName, depType string) error { + edge := dot.NewEdge(from, to) + + if web { + tooltip := fmt.Sprintf("%s → %s", fromName, toName) + if depType != "" { + tooltip += fmt.Sprintf(" (%s)", depType) + } + + if err := edge.Set("tooltip", tooltip); err != nil { + return err + } + } + + _, err := out.AddEdge(edge) + return err + } + file := dot.NewNode(configFile) if _, err := out.AddNode(file); err != nil { panic(err) } for _, pkg := range ic.Contents.Packages { - n := dot.NewNode(pkg) - if _, err := out.AddNode(n); err != nil { - panic(err) + var n *dot.Node + if existing, ok := addedNodes[pkg]; ok { + n = existing + } else { + n = dot.NewNode(pkg) + addedNodes[pkg] = n + if _, err := out.AddNode(n); err != nil { + panic(err) + } } - if _, err := out.AddEdge(dot.NewEdge(file, n)); err != nil { + + if err := addEdgeWithTooltip(file, n, configFile, pkg, "required"); err != nil { panic(err) } if before, _, ok := strings.Cut(pkg, "="); ok { - p := dot.NewNode(before) - if _, err := out.AddNode(p); err != nil { - panic(err) + var p *dot.Node + if existing, ok := addedNodes[before]; ok { + p = existing + } else { + p = dot.NewNode(before) + addedNodes[before] = p + if _, err := out.AddNode(p); err != nil { + panic(err) + } } - if _, err := out.AddEdge(dot.NewEdge(n, p)); err != nil { + if err := addEdgeWithTooltip(n, p, pkg, before, "exact version"); err != nil { panic(err) } + // Set URL on the constraint node to point to the same package + if web { + if targetPkg, ok := pkgMap[before]; ok { + url := extURL(targetPkg) + if err := n.Set("URL", url); err != nil { + panic(err) + } + if err := n.Set("target", "_blank"); err != nil { + panic(err) + } + if err := n.Set("tooltip", targetPkg.Description); err != nil { + panic(err) + } + } + } + deps[before] = struct{}{} } else if before, _, ok := strings.Cut(pkg, "~"); ok { - p := dot.NewNode(before) - if _, err := out.AddNode(p); err != nil { - panic(err) + var p *dot.Node + if existing, ok := addedNodes[before]; ok { + p = existing + } else { + p = dot.NewNode(before) + addedNodes[before] = p + if _, err := out.AddNode(p); err != nil { + panic(err) + } } - if _, err := out.AddEdge(dot.NewEdge(n, p)); err != nil { + if err := addEdgeWithTooltip(n, p, pkg, before, "compatible version"); err != nil { panic(err) } + // Set URL on the constraint node to point to the same package + if web { + if targetPkg, ok := pkgMap[before]; ok { + url := extURL(targetPkg) + if err := n.Set("URL", url); err != nil { + panic(err) + } + if err := n.Set("target", "_blank"); err != nil { + panic(err) + } + if err := n.Set("tooltip", targetPkg.Description); err != nil { + panic(err) + } + } + } + deps[before] = struct{}{} } else { deps[pkg] = struct{}{} @@ -204,17 +279,33 @@ } renderDeps := func(pkg *apk.RepositoryPackage) { - n := dot.NewNode(pkg.Name) + var n *dot.Node + if existing, ok := addedNodes[pkg.Name]; ok { + // Node already exists, use it + n = existing + } else { + // Create new node + n = dot.NewNode(pkg.Name) + addedNodes[pkg.Name] = n + if _, err := out.AddNode(n); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { + panic(err) + } + } + if err := n.Set("label", pkgver(pkg)); err != nil { panic(err) } + if err := n.Set("tooltip", pkg.Description); err != nil { + panic(err) + } if web { - if err := n.Set("URL", link(args, pkg.Name)); err != nil { + url := extURL(pkg) + if err := n.Set("URL", url); err != nil { + panic(err) + } + if err := n.Set("target", "_blank"); err != nil { panic(err) } - } - if _, err := out.AddNode(n); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { - panic(err) } for _, dep := range dmap[pkg.Name] { @@ -223,21 +314,41 @@ } else if before, _, ok := strings.Cut(dep, "~"); ok { dep = before } - d := dot.NewNode(dep) + + var d *dot.Node + if existing, ok := addedNodes[dep]; ok { + d = existing + } else { + d = dot.NewNode(dep) + addedNodes[dep] = d + if _, err := out.AddNode(d); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { + panic(err) + } + } + if web { if !strings.Contains(dep, ":") { - if err := d.Set("URL", link(args, dep)); err != nil { - panic(err) + if depPkg, ok := pkgMap[dep]; ok { + if err := d.Set("URL", extURL(depPkg)); err != nil { + panic(err) + } + if err := d.Set("target", "_blank"); err != nil { + panic(err) + } + if err := d.Set("tooltip", depPkg.Description); err != nil { + panic(err) + } + } else { + // Don't set a URL for dependencies not in the package map (e.g., virtual dependencies, provides). + // Setting a local link would cause a panic when clicked since the package doesn't exist. + log.Debugf("Dependency %s not in package map, skipping URL", dep) } } } - if _, err := out.AddNode(d); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { - panic(err) - } if _, ok := edges[dep]; !ok || !span { // This check is stupid but otherwise cycles render dumb. if pkg.Name != dep { - if _, err := out.AddEdge(dot.NewEdge(n, d)); err != nil { + if err := addEdgeWithTooltip(n, d, pkg.Name, dep, "dependency"); err != nil { panic(err) } edges[dep] = struct{}{} @@ -265,42 +376,67 @@ } renderProvs := func(pkg *apk.RepositoryPackage) { - n := dot.NewNode(pkg.Name) + var n *dot.Node + if existing, ok := addedNodes[pkg.Name]; ok { + n = existing + } else { + n = dot.NewNode(pkg.Name) + addedNodes[pkg.Name] = n + if _, err := out.AddNode(n); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { + panic(err) + } + } if err := n.Set("label", pkgver(pkg)); err != nil { panic(err) } - if _, err := out.AddNode(n); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { - panic(err) + if pkg.Description != "" { + if err := n.Set("tooltip", pkg.Description); err != nil { + panic(err) + } + } else { + log.Debugf("No description for package %s", pkg.Name) } for _, prov := range pmap[pkg.Name] { if _, ok := deps[prov]; !ok { if before, _, ok := strings.Cut(prov, "="); ok { if _, ok := deps[before]; ok { - p := dot.NewNode(before) - if err := p.Set("shape", "rect"); err != nil { - panic(err) + var p *dot.Node + if existing, ok := addedNodes[before]; ok { + p = existing + } else { + p = dot.NewNode(before) + addedNodes[before] = p + if _, err := out.AddNode(p); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { + panic(err) + } } - if _, err := out.AddNode(p); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { + if err := p.Set("shape", "rect"); err != nil { panic(err) } - if _, err := out.AddEdge(dot.NewEdge(p, n)); err != nil { + if err := addEdgeWithTooltip(p, n, before, pkg.Name, "provides"); err != nil { panic(err) } } continue } else if before, _, ok := strings.Cut(prov, "~"); ok { if _, ok := deps[before]; ok { - p := dot.NewNode(before) - if err := p.Set("shape", "rect"); err != nil { - panic(err) + var p *dot.Node + if existing, ok := addedNodes[before]; ok { + p = existing + } else { + p = dot.NewNode(before) + addedNodes[before] = p + if _, err := out.AddNode(p); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { + panic(err) + } } - if _, err := out.AddNode(p); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { + if err := p.Set("shape", "rect"); err != nil { panic(err) } - if _, err := out.AddEdge(dot.NewEdge(p, n)); err != nil { + if err := addEdgeWithTooltip(p, n, before, pkg.Name, "provides"); err != nil { panic(err) } } @@ -309,12 +445,18 @@ continue } } - p := dot.NewNode(prov) - if _, err := out.AddNode(p); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { - panic(err) + var p *dot.Node + if existing, ok := addedNodes[prov]; ok { + p = existing + } else { + p = dot.NewNode(prov) + addedNodes[prov] = p + if _, err := out.AddNode(p); err != nil && !errors.Is(err, dot.ErrDuplicateNode) { + panic(err) + } } if _, ok := edges[pkg.Name]; !ok || !span { - if _, err := out.AddEdge(dot.NewEdge(p, n)); err != nil { + if err := addEdgeWithTooltip(p, n, prov, pkg.Name, "provides"); err != nil { panic(err) } edges[pkg.Name] = struct{}{} @@ -352,6 +494,7 @@ } if web { + log.Infof("Prefixing node links with registry-explorer: %s", extRegistryViewer) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { return @@ -367,10 +510,22 @@ log.Infof("%s: rendering %v", r.URL, nodes) cmd := exec.Command("dot", "-Tsvg") cmd.Stdin = strings.NewReader(out.String()) - cmd.Stdout = w + + var svgBuf strings.Builder + cmd.Stdout = &svgBuf if err := cmd.Run(); err != nil { fmt.Fprintf(w, "error rendering %v: %v", nodes, err) + return + } + + // Post-process SVG to remove the graph-level tooltip + svg := svgBuf.String() + // Remove the constant "images" tooltip from the SVG + svg = strings.ReplaceAll(svg, `<title>images</title>`, "") + + if _, err := w.Write([]byte(svg)); err != nil { + log.Errorf("error writing SVG response: %v", err) } }) @@ -414,18 +569,25 @@ return fmt.Sprintf("%s-%s", pkg.Name, pkg.Version) } -func link(args []string, pkg string) string { - filtered := []string{} - for _, a := range args { - if a != pkg { - filtered = append(filtered, a) - } - } - ret := "/?node=" + pkg - if len(filtered) > 0 { - ret += "&node=" + strings.Join(filtered, "&node=") +func extURL(pkg *apk.RepositoryPackage) string { + // Get the package URL like: https://packages.wolfi.dev/repo/arch/package-version.apk + pkgURL := pkg.URL() + + // Replace protocol :// with / + pkgURL = strings.ReplaceAll(pkgURL, "://", "/") + + // Replace any remaining : with / + pkgURL = strings.ReplaceAll(pkgURL, ":", "/") + + // Normalize multiple consecutive slashes to single slash + for strings.Contains(pkgURL, "//") { + pkgURL = strings.ReplaceAll(pkgURL, "//", "/") } - return ret + + // Build the final chaindag URL + result := fmt.Sprintf("https://%s/%s", extRegistryViewer, pkgURL) + log.Debugf("Package %s -> URL: %s", pkg.Name, result) + return result } type unwrapper interface { ++++++ apko.obsinfo ++++++ --- /var/tmp/diff_new_pack.aPla49/_old 2025-12-19 16:47:20.177611394 +0100 +++ /var/tmp/diff_new_pack.aPla49/_new 2025-12-19 16:47:20.193612064 +0100 @@ -1,5 +1,5 @@ name: apko -version: 0.30.33 -mtime: 1765981696 -commit: 53957f9ee52c20f0db840d578fe5969ba49cc133 +version: 0.30.34 +mtime: 1766011437 +commit: 347c2ffac0648e5859d6965b8201c09c211ee7ae ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/apko/vendor.tar.gz /work/SRC/openSUSE:Factory/.apko.new.1928/vendor.tar.gz differ: char 91, line 2
