Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package apko for openSUSE:Factory checked in at 2026-08-03 17:17:07 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/apko (Old) and /work/SRC/openSUSE:Factory/.apko.new.16738 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "apko" Mon Aug 3 17:17:07 2026 rev:131 rq:1369151 version:1.2.31 Changes: -------- --- /work/SRC/openSUSE:Factory/apko/apko.changes 2026-08-01 18:35:11.135924587 +0200 +++ /work/SRC/openSUSE:Factory/.apko.new.16738/apko.changes 2026-08-03 17:17:22.882894390 +0200 @@ -1,0 +2,8 @@ +Mon Aug 03 05:55:40 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 1.2.31: + * build(deps): bump go.step.sm/crypto from 0.85.0 to 0.86.0 + (#2362) + * Intern parsed repository packages (#2357) + +------------------------------------------------------------------- Old: ---- apko-1.2.30.obscpio New: ---- apko-1.2.31.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ apko.spec ++++++ --- /var/tmp/diff_new_pack.KWc2lO/_old 2026-08-03 17:17:25.498985428 +0200 +++ /var/tmp/diff_new_pack.KWc2lO/_new 2026-08-03 17:17:25.502985568 +0200 @@ -17,7 +17,7 @@ Name: apko -Version: 1.2.30 +Version: 1.2.31 Release: 0 Summary: Build OCI images from APK packages directly without Dockerfile License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.KWc2lO/_old 2026-08-03 17:17:25.538986820 +0200 +++ /var/tmp/diff_new_pack.KWc2lO/_new 2026-08-03 17:17:25.542986960 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/chainguard-dev/apko.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">refs/tags/v1.2.30</param> + <param name="revision">refs/tags/v1.2.31</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.KWc2lO/_old 2026-08-03 17:17:25.570987934 +0200 +++ /var/tmp/diff_new_pack.KWc2lO/_new 2026-08-03 17:17:25.574988073 +0200 @@ -3,6 +3,6 @@ <param name="url">https://github.com/chainguard-dev/apko</param> <param name="changesrevision">861f83f69e6fa9114405a2f7bb5cf6585ad00421</param></service><service name="tar_scm"> <param name="url">https://github.com/chainguard-dev/apko.git</param> - <param name="changesrevision">502643080cb38d339c5804fe7e62c9a5bd46b6d5</param></service></servicedata> + <param name="changesrevision">fc40b1ae3059b97fd591afe4d203379b16887b2a</param></service></servicedata> (No newline at EOF) ++++++ apko-1.2.30.obscpio -> apko-1.2.31.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.2.30/go.mod new/apko-1.2.31/go.mod --- old/apko-1.2.30/go.mod 2026-07-30 20:33:18.000000000 +0200 +++ new/apko-1.2.31/go.mod 2026-07-31 20:19:09.000000000 +0200 @@ -25,7 +25,7 @@ go.lsp.dev/uri v1.0.1 go.opentelemetry.io/otel v1.44.0 go.opentelemetry.io/otel/trace v1.44.0 - go.step.sm/crypto v0.85.0 + go.step.sm/crypto v0.86.0 golang.org/x/oauth2 v0.36.0 golang.org/x/sync v0.22.0 golang.org/x/sys v0.47.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.2.30/go.sum new/apko-1.2.31/go.sum --- old/apko-1.2.30/go.sum 2026-07-30 20:33:18.000000000 +0200 +++ new/apko-1.2.31/go.sum 2026-07-31 20:19:09.000000000 +0200 @@ -267,8 +267,8 @@ go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA= go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk= go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE= -go.step.sm/crypto v0.85.0 h1:h3U9gzCcxP1fWbseqy+CwsTxzHFNuoblefy7YP4jBHk= -go.step.sm/crypto v0.85.0/go.mod h1:Rp/BdP0/ZJ7eYREhXcE4izCABLp0e8ziFqb0LBNG7Cc= +go.step.sm/crypto v0.86.0 h1:ORItETVOEAo0+rfCm0VKSAolLtoxMSL+pOKfvUeR4MM= +go.step.sm/crypto v0.86.0/go.mod h1:G+SbDNGA8ZYZBOXYJnxZLJnVCWp3RIKMF+ECS8G0Qug= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.2.30/pkg/apk/apk/index.go new/apko-1.2.31/pkg/apk/apk/index.go --- old/apko-1.2.30/pkg/apk/apk/index.go 2026-07-30 20:33:18.000000000 +0200 +++ new/apko-1.2.31/pkg/apk/apk/index.go 2026-07-31 20:19:09.000000000 +0200 @@ -432,6 +432,13 @@ return nil, fmt.Errorf("unable to read convert repository index bytes to index struct: %w", err) } + // Share package payloads with other indexes carrying the same packages. + // Only the repository-fetch path interns: index manipulation tooling + // building on IndexFromArchive directly keeps exclusive ownership. + for i, pkg := range index.Packages { + index.Packages[i] = internPackage(pkg) + } + return index, err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.2.30/pkg/apk/apk/intern.go new/apko-1.2.31/pkg/apk/apk/intern.go --- old/apko-1.2.30/pkg/apk/apk/intern.go 1970-01-01 01:00:00.000000000 +0100 +++ new/apko-1.2.31/pkg/apk/apk/intern.go 2026-07-31 20:19:09.000000000 +0200 @@ -0,0 +1,78 @@ +// Copyright 2026 Chainguard, Inc. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package apk + +import ( + "reflect" + "sync" + "weak" +) + +// Repository indexes overlap heavily: consecutive generations of one index +// share almost all of their packages, and indexes of related repositories +// carry large common subsets. Interning parsed packages lets all of them +// share a single Package per distinct package instead of a copy per index. +// Entries hold weak pointers, so the table itself pins nothing. +// +// Sharing requires that packages parsed from repository indexes are never +// mutated; they are treated as immutable everywhere. +var packageInterner = struct { + sync.Mutex + m map[internKey]weak.Pointer[Package] +}{m: map[internKey]weak.Pointer[Package]{}} + +type internKey struct { + name string + checksum string +} + +// internPackage returns the canonical *Package for pkg: a previously interned +// package with an identical record, or pkg itself if none is registered yet. +// Packages without a checksum are not interned. +// +// The {name, checksum} pair only buckets candidates; identity is confirmed +// with reflect.DeepEqual over the whole package, so two records share a +// Package only when every field matches. That keeps interning safe across +// repository trust boundaries - a crafted index cannot donate its +// dependencies to another repository's package by matching the control hash +// alone - and cannot silently drop a field the way a hand-written comparison +// could. +func internPackage(pkg *Package) *Package { + if len(pkg.Checksum) == 0 { + return pkg + } + key := internKey{name: pkg.Name, checksum: string(pkg.Checksum)} + + packageInterner.Lock() + defer packageInterner.Unlock() + + if wp, ok := packageInterner.m[key]; ok { + if canonical := wp.Value(); canonical != nil { + if reflect.DeepEqual(pkg, canonical) { + return canonical + } + // A live but different package holds the bucket (a control-hash + // collision or a poisoning attempt): keep both distinct. + return pkg + } + } + + // Weak so the payload is collected once no live index references it. A + // dead entry reads back as a miss and is overwritten when its record + // recurs; one forms only when a record leaves every live index at once, + // rare for largely append-only indexes, so tombstones accumulate slowly. + packageInterner.m[key] = weak.Make(pkg) + return pkg +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/apko-1.2.30/pkg/apk/apk/intern_test.go new/apko-1.2.31/pkg/apk/apk/intern_test.go --- old/apko-1.2.30/pkg/apk/apk/intern_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/apko-1.2.31/pkg/apk/apk/intern_test.go 2026-07-31 20:19:09.000000000 +0200 @@ -0,0 +1,154 @@ +package apk + +import ( + "fmt" + "io" + "net/http" + "net/http/httptest" + "runtime" + "strings" + "sync/atomic" + "testing" + "time" + "weak" +) + +func internTestIndexBody(t *testing.T, nPkgs int) []byte { + t.Helper() + idx := &APKIndex{Description: "intern-test"} + for i := range nPkgs { + idx.Packages = append(idx.Packages, &Package{ + Name: fmt.Sprintf("pkg-%d", i), + Version: "1.0.0-r0", + Arch: "x86_64", + // A fat payload so sharing dominates the heap measurements. + Description: strings.Repeat(fmt.Sprintf("synthetic package %d ", i), 50), + Checksum: fmt.Appendf(nil, "checksum-of-pkg-%05d", i), + Dependencies: []string{fmt.Sprintf("pkg-%d", min(i+1, 20))}, + Provides: []string{fmt.Sprintf("cmd:tool-%d=1.0.0-r0", i)}, + BuildTime: time.Unix(1700000000, 0).UTC(), + }) + } + archive, err := ArchiveFromIndex(idx) + if err != nil { + t.Fatal(err) + } + body, err := io.ReadAll(archive) + if err != nil { + t.Fatal(err) + } + return body +} + +func internTestServer(t *testing.T, body []byte) *httptest.Server { + t.Helper() + var etag atomic.Value + etag.Store("gen-0") + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("ETag", fmt.Sprintf("%q", r.URL.Query().Get("gen")+"gen")) + if r.Method == http.MethodHead { + return + } + _, _ = w.Write(body) + })) + t.Cleanup(srv.Close) + return srv +} + +// Indexes at different URLs carrying the same packages must share the parsed +// Package payloads. +func TestInterningSharesPackagesAcrossIndexes(t *testing.T) { + ctx := t.Context() + body := internTestIndexBody(t, 100) + srv := internTestServer(t, body) + + fetch := func(repo string) NamedIndex { + indexes, err := GetRepositoryIndexes(ctx, []string{repo}, nil, "x86_64", + WithIgnoreSignatures(true), WithHTTPClient(srv.Client())) + if err != nil { + t.Fatal(err) + } + return indexes[0] + } + + a := fetch(srv.URL + "/repo-a") + b := fetch(srv.URL + "/repo-b") + + for i, pkg := range a.Packages() { + if pkg.Package != b.Packages()[i].Package { + t.Fatalf("package %d not shared across indexes", i) + } + } +} + +// Records that differ in any field must not be conflated, even when they share +// a name and control hash. Dependencies is a field the old six-field guard did +// not compare; DeepEqual does, which is what keeps interning safe across +// repository trust boundaries. +func TestInterningRejectsMismatchedRecords(t *testing.T) { + a := &Package{Name: "pkg", Version: "1.0.0-r0", Checksum: []byte("intern-mismatch-test"), Dependencies: []string{"honest-dep"}} + b := &Package{Name: "pkg", Version: "1.0.0-r0", Checksum: []byte("intern-mismatch-test"), Dependencies: []string{"honest-dep", "evil-dep"}} + + canonical := internPackage(a) + if canonical != a { + t.Fatal("first package should become canonical") + } + // b shares a's name and checksum but differs, so it must not be conflated + // with the canonical. Comparing against canonical also keeps a live across + // this call, so the mismatch path is exercised rather than a dead-entry miss. + if internPackage(b) == canonical { + t.Fatal("record differing only in dependencies was conflated") + } +} + +// The intern table holds packages weakly, so an interned package must be +// collectable once no live index references it; a strong table would pin it +// forever. (The dead map entry lingers until overwritten, which is intended.) +func TestInterningDoesNotPinPackages(t *testing.T) { + var observer weak.Pointer[Package] + func() { + pkg := internPackage(&Package{ + Name: "collectable", + Version: "1.0.0-r0", + Checksum: []byte("intern-collect-test"), + }) + observer = weak.Make(pkg) + }() + + deadline := time.Now().Add(10 * time.Second) + for observer.Value() != nil { + if time.Now().After(deadline) { + t.Fatal("interned package was not collected; the table is pinning it") + } + runtime.GC() + time.Sleep(10 * time.Millisecond) + } +} + +// Loading identical indexes from distinct URLs must share package objects, not +// duplicate them: the union of every index's package pointers is one index's +// worth, not one per index. +func TestInterningDeduplicatesAcrossURLs(t *testing.T) { + const nPkgs = 1000 + const copies = 4 + + ctx := t.Context() + body := internTestIndexBody(t, nPkgs) + srv := internTestServer(t, body) + + seen := map[*Package]struct{}{} + for i := range copies { + got, err := GetRepositoryIndexes(ctx, []string{fmt.Sprintf("%s/repo-%d", srv.URL, i)}, nil, "x86_64", + WithIgnoreSignatures(true), WithHTTPClient(srv.Client())) + if err != nil { + t.Fatal(err) + } + for _, p := range got[0].Packages() { + seen[p.Package] = struct{}{} + } + } + + if len(seen) != nPkgs { + t.Errorf("saw %d distinct package objects across %d identical indexes, want %d", len(seen), copies, nPkgs) + } +} ++++++ apko.obsinfo ++++++ --- /var/tmp/diff_new_pack.KWc2lO/_old 2026-08-03 17:17:26.199009789 +0200 +++ /var/tmp/diff_new_pack.KWc2lO/_new 2026-08-03 17:17:26.203009928 +0200 @@ -1,5 +1,5 @@ name: apko -version: 1.2.30 -mtime: 1785436398 -commit: 502643080cb38d339c5804fe7e62c9a5bd46b6d5 +version: 1.2.31 +mtime: 1785521949 +commit: fc40b1ae3059b97fd591afe4d203379b16887b2a ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/apko/vendor.tar.gz /work/SRC/openSUSE:Factory/.apko.new.16738/vendor.tar.gz differ: char 133, line 1
