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-04 23:28:34
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/apko (Old)
 and      /work/SRC/openSUSE:Factory/.apko.new.16738 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "apko"

Tue Aug  4 23:28:34 2026 rev:132 rq:1369445 version:1.2.32

Changes:
--------
--- /work/SRC/openSUSE:Factory/apko/apko.changes        2026-08-03 
17:17:22.882894390 +0200
+++ /work/SRC/openSUSE:Factory/.apko.new.16738/apko.changes     2026-08-04 
23:28:38.170252474 +0200
@@ -1,0 +2,20 @@
+Tue Aug 04 08:33:39 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 1.2.32:
+  * Evict superseded index generations from the derived caches
+    (#2360)
+  * apk/installed: guard ParseInstalled against short lines (#2328)
+  * build(deps): bump github.com/go-git/go-git/v5 from 5.19.1 to
+    5.19.2 (#2364)
+  * build(deps): bump go.step.sm/crypto from 0.86.0 to 0.87.0
+    (#2365)
+  * build(deps): bump google.golang.org/api from 0.290.0 to 0.291.0
+    (#2366)
+  * build(deps): bump github/codeql-action/analyze from 4.37.3 to
+    4.37.4 (#2367)
+  * build(deps): bump github/codeql-action/init from 4.37.3 to
+    4.37.4 (#2368)
+  * build(deps): bump chainguard.dev/sdk from 0.1.158 to 0.1.163
+    (#2363)
+
+-------------------------------------------------------------------

Old:
----
  apko-1.2.31.obscpio

New:
----
  apko-1.2.32.obscpio

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ apko.spec ++++++
--- /var/tmp/diff_new_pack.bzLPuu/_old  2026-08-04 23:28:39.886313157 +0200
+++ /var/tmp/diff_new_pack.bzLPuu/_new  2026-08-04 23:28:39.890313298 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           apko
-Version:        1.2.31
+Version:        1.2.32
 Release:        0
 Summary:        Build OCI images from APK packages directly without Dockerfile
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.bzLPuu/_old  2026-08-04 23:28:39.930314713 +0200
+++ /var/tmp/diff_new_pack.bzLPuu/_new  2026-08-04 23:28:39.934314855 +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.31</param>
+    <param name="revision">refs/tags/v1.2.32</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.bzLPuu/_old  2026-08-04 23:28:39.954315562 +0200
+++ /var/tmp/diff_new_pack.bzLPuu/_new  2026-08-04 23:28:39.958315703 +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">fc40b1ae3059b97fd591afe4d203379b16887b2a</param></service></servicedata>
+              <param 
name="changesrevision">ea62bef5271a0c94f3fba641ee0e8d506b9b57ef</param></service></servicedata>
 (No newline at EOF)
 

++++++ apko-1.2.31.obscpio -> apko-1.2.32.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.31/go.mod new/apko-1.2.32/go.mod
--- old/apko-1.2.31/go.mod      2026-07-31 20:19:09.000000000 +0200
+++ new/apko-1.2.32/go.mod      2026-08-04 07:48:25.000000000 +0200
@@ -3,10 +3,10 @@
 go 1.26.0
 
 require (
-       chainguard.dev/sdk v0.1.158
+       chainguard.dev/sdk v0.1.163
        github.com/chainguard-dev/clog v1.8.1
        github.com/charmbracelet/log v1.0.0
-       github.com/go-git/go-git/v5 v5.19.1
+       github.com/go-git/go-git/v5 v5.19.2
        github.com/google/go-cmp v0.7.0
        github.com/google/go-containerregistry v0.21.7
        github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510
@@ -25,13 +25,13 @@
        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.86.0
+       go.step.sm/crypto v0.87.0
        golang.org/x/oauth2 v0.36.0
        golang.org/x/sync v0.22.0
        golang.org/x/sys v0.47.0
        golang.org/x/term v0.45.0
        golang.org/x/time v0.15.0
-       google.golang.org/api v0.290.0
+       google.golang.org/api v0.291.0
        gopkg.in/ini.v1 v1.67.3
        gopkg.in/yaml.v3 v3.0.1
        k8s.io/apimachinery v0.36.3
@@ -40,7 +40,7 @@
 
 require (
        chainguard.dev/go-grpc-kit v0.17.17 // indirect
-       cloud.google.com/go/auth v0.21.0 // indirect
+       cloud.google.com/go/auth v0.22.0 // indirect
        cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
        cloud.google.com/go/compute/metadata v0.9.0 // indirect
        dario.cat/mergo v1.0.2 // indirect
@@ -82,7 +82,7 @@
        github.com/golang/groupcache v0.0.0-20241129210726-2c02b8208cf8 // 
indirect
        github.com/google/s2a-go v0.1.9 // indirect
        github.com/google/uuid v1.6.0 // indirect
-       github.com/googleapis/enterprise-certificate-proxy v0.3.18 // indirect
+       github.com/googleapis/enterprise-certificate-proxy v0.3.19 // indirect
        github.com/googleapis/gax-go/v2 v2.23.0 // indirect
        github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus 
v1.1.0 // indirect
        github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect
@@ -129,7 +129,7 @@
        golang.org/x/net v0.57.0 // indirect
        golang.org/x/text v0.40.0 // indirect
        google.golang.org/genproto/googleapis/api 
v0.0.0-20260706201446-f0a921348800 // indirect
-       google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260706201446-f0a921348800 // indirect
+       google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260724162435-b2f20204f0df // indirect
        google.golang.org/grpc v1.82.1 // indirect
        google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af // 
indirect
        gopkg.in/warnings.v0 v0.1.2 // indirect
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.31/go.sum new/apko-1.2.32/go.sum
--- old/apko-1.2.31/go.sum      2026-07-31 20:19:09.000000000 +0200
+++ new/apko-1.2.32/go.sum      2026-08-04 07:48:25.000000000 +0200
@@ -1,9 +1,9 @@
 chainguard.dev/go-grpc-kit v0.17.17 
h1:Jwhc0zyUwQbC2hNcsi+YMeUX/JUnM+dXVCkTw6wtPzs=
 chainguard.dev/go-grpc-kit v0.17.17/go.mod 
h1:qn0meP6RtrbLicE1bgBZnnVU9dvX95eLs0x0T6kZ+b4=
-chainguard.dev/sdk v0.1.158 h1:aOWATQ4zxYKQ8D+CCUj+hOU18wNQkbHw6tmj+/+GDAk=
-chainguard.dev/sdk v0.1.158/go.mod 
h1:WIaM60bR6qU84RvE45u4QkOh+BN2uc1nfHuZYLnRfvU=
-cloud.google.com/go/auth v0.21.0 
h1:g/QwYfYb2Ai6HH8oomAOyBaIHLbscZ4+T/F/f5JZHkE=
-cloud.google.com/go/auth v0.21.0/go.mod 
h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s=
+chainguard.dev/sdk v0.1.163 h1:G85vXLU7qJ2pJo1hHcPRYWWZnGRUAVIomZfoq9dg8Dk=
+chainguard.dev/sdk v0.1.163/go.mod 
h1:y5AGo04erWjKtEyGX7fO9zZA4vtj9cLGXZjgIYxeMdo=
+cloud.google.com/go/auth v0.22.0 
h1:Xp9wAKkLoeaYb5pYZZoQGz4E9sdPxIbzS3gywZE3ciQ=
+cloud.google.com/go/auth v0.22.0/go.mod 
h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 
h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
 cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod 
h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c=
 cloud.google.com/go/compute/metadata v0.9.0 
h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs=
@@ -92,8 +92,8 @@
 github.com/go-git/go-billy/v5 v5.9.0/go.mod 
h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 
h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
 github.com/go-git/go-git-fixtures/v4 
v4.3.2-0.20231010084843-55a94097c399/go.mod 
h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.19.1 
h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
-github.com/go-git/go-git/v5 v5.19.1/go.mod 
h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
+github.com/go-git/go-git/v5 v5.19.2 
h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
+github.com/go-git/go-git/v5 v5.19.2/go.mod 
h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
 github.com/go-jose/go-jose/v3 v3.0.5 
h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
 github.com/go-jose/go-jose/v3 v3.0.5/go.mod 
h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-logfmt/logfmt v0.6.1 
h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
@@ -119,8 +119,8 @@
 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod 
h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ=
 github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
 github.com/google/uuid v1.6.0/go.mod 
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
-github.com/googleapis/enterprise-certificate-proxy v0.3.18 
h1:hvVi34VucdrV1IIsiWuqYM8kutw/92MxNEFxCJZEh0k=
-github.com/googleapis/enterprise-certificate-proxy v0.3.18/go.mod 
h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
+github.com/googleapis/enterprise-certificate-proxy v0.3.19 
h1:mMOE7DN2+p76/EdIrmAy9B9bH+yC4563vmnJ34QR8i4=
+github.com/googleapis/enterprise-certificate-proxy v0.3.19/go.mod 
h1:rSEsBUemEBZEexP2y6jPp16LUmUbjmSbcPMQizR0o4k=
 github.com/googleapis/gax-go/v2 v2.23.0 
h1:Tchl7qkvE7Ip3y+ztvNufYFvkfqTe7NfLTYGIdJRLuE=
 github.com/googleapis/gax-go/v2 v2.23.0/go.mod 
h1:rBQKOVJCdb8IFEzg+FCwlt1LP/xMDGuqUXhUG+XMXEg=
 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 
h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
@@ -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.86.0 h1:ORItETVOEAo0+rfCm0VKSAolLtoxMSL+pOKfvUeR4MM=
-go.step.sm/crypto v0.86.0/go.mod 
h1:G+SbDNGA8ZYZBOXYJnxZLJnVCWp3RIKMF+ECS8G0Qug=
+go.step.sm/crypto v0.87.0 h1:+u0pDz1OV9M/SswA6KtcQ3GSrYMMelbL//0W/ceJVYI=
+go.step.sm/crypto v0.87.0/go.mod 
h1:gBr1mpMiKs5804/Yt03uAoOBiw3DFxARZnGUKEUO82M=
 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=
@@ -345,14 +345,14 @@
 golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod 
h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
 gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
 gonum.org/v1/gonum v0.17.0/go.mod 
h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
-google.golang.org/api v0.290.0 h1:eMw0Xo+IfbbMlKmW7aHvpyQRv9RCXuWx/vs8AD+0x9A=
-google.golang.org/api v0.290.0/go.mod 
h1:weJZ3lldHFYI0DBFNKpJelUDNnusTt5YaOEgxvt8ci8=
+google.golang.org/api v0.291.0 h1:wfPbbY+mr9c7wZLqqzrHJLft/q8iFKREd6IgTBUene0=
+google.golang.org/api v0.291.0/go.mod 
h1:at7kwWbuonglBFEBoeMDAV1bguHqL3qf0BHFsv3coa0=
 google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d 
h1:N1Ec54vZnIPd7MnxRiYLW+oY4fDR4BOS/LrssdD9+ek=
 google.golang.org/genproto v0.0.0-20260406210006-6f92a3bedf2d/go.mod 
h1:c2hJ1grtnH0xUiEKGDGkjGNTJ1Hy2LrblyKOHF0sqRM=
 google.golang.org/genproto/googleapis/api v0.0.0-20260706201446-f0a921348800 
h1:admdQBe8jR3VWhBsUrAOaF2Qw6K/+p5pSm1GN8+6Fw4=
 google.golang.org/genproto/googleapis/api 
v0.0.0-20260706201446-f0a921348800/go.mod 
h1:FPk7EXUKMtImne7AmknoYjT4QXqKIzzRbeQIXzLk6fQ=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260706201446-f0a921348800 
h1:qEHAMpSaUhtD0p3NbEEI83HwNGFxEwaSJ1G9PLnCBZE=
-google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260706201446-f0a921348800/go.mod 
h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260724162435-b2f20204f0df 
h1:O3ig1i5WDDzsVzRp+cCdgelT9vXnlnOFdlEeFtL4HCc=
+google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260724162435-b2f20204f0df/go.mod 
h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
 google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE=
 google.golang.org/grpc v1.82.1/go.mod 
h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA=
 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af 
h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.31/pkg/apk/apk/index.go 
new/apko-1.2.32/pkg/apk/apk/index.go
--- old/apko-1.2.31/pkg/apk/apk/index.go        2026-07-31 20:19:09.000000000 
+0200
+++ new/apko-1.2.32/pkg/apk/apk/index.go        2026-08-04 07:48:25.000000000 
+0200
@@ -56,6 +56,7 @@
 var globalIndexCache = &indexCache{
        indexes:  newFlightCache[cacheKey, NamedIndex](),
        modtimes: map[string]time.Time{},
+       current:  map[string]NamedIndex{},
 }
 
 type cacheKey struct {
@@ -69,6 +70,39 @@
        // For local indexes.
        sync.Mutex
        modtimes map[string]time.Time
+
+       // current is the latest index object served per index URL. When a newer
+       // generation replaces an entry, derived data cached for the superseded
+       // index is purged, and the resolver caches consult isCurrent to avoid
+       // re-caching data for superseded indexes that in-flight resolutions
+       // still hold.
+       currentMu sync.Mutex
+       current   map[string]NamedIndex
+}
+
+// setCurrent records idx as the latest generation for the index URL u,
+// purging derived caches for the generation it replaces.
+func (i *indexCache) setCurrent(u string, idx NamedIndex) {
+       i.currentMu.Lock()
+       prev := i.current[u]
+       i.current[u] = idx
+       i.currentMu.Unlock()
+
+       if prev != nil && prev != idx {
+               globalResolverCache.ForgetIndex(prev)
+               globalDisqualifyCache.ForgetIndex(prev)
+       }
+}
+
+// isCurrent reports whether idx is still the latest generation of its index.
+// Indexes this cache has never served (e.g. locally constructed ones) are
+// considered current: they are not subject to generation replacement.
+func (i *indexCache) isCurrent(idx NamedIndex) bool {
+       i.currentMu.Lock()
+       defer i.currentMu.Unlock()
+
+       cur, ok := i.current[idx.Source()]
+       return !ok || cur == idx
 }
 
 func (i *indexCache) get(ctx context.Context, repoName, repoURL string, keys 
map[string][]byte, arch string, opts *indexOpts) (NamedIndex, error) {
@@ -144,6 +178,9 @@
                        return k.url == u && k.etag != etag
                })
 
+               if err == nil {
+                       i.setCurrent(u, idx)
+               }
                return idx, err
        } else {
                i.Lock()
@@ -166,7 +203,7 @@
                }
                i.modtimes[u] = mod
 
-               return i.indexes.Do(key, func() (NamedIndex, error) {
+               idx, err := i.indexes.Do(key, func() (NamedIndex, error) {
                        b, err := os.ReadFile(u)
                        if err != nil {
                                return nil, fmt.Errorf("reading file: %w", err)
@@ -177,6 +214,10 @@
                        }
                        return NewNamedRepositoryWithIndex(repoName, 
repoRef.WithIndex(idx)), nil
                })
+               if err == nil {
+                       i.setCurrent(u, idx)
+               }
+               return idx, err
        }
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.31/pkg/apk/apk/index_eviction_test.go 
new/apko-1.2.32/pkg/apk/apk/index_eviction_test.go
--- old/apko-1.2.31/pkg/apk/apk/index_eviction_test.go  1970-01-01 
01:00:00.000000000 +0100
+++ new/apko-1.2.32/pkg/apk/apk/index_eviction_test.go  2026-08-04 
07:48:25.000000000 +0200
@@ -0,0 +1,205 @@
+package apk
+
+import (
+       "bytes"
+       "context"
+       "fmt"
+       "net/http"
+       "net/http/httptest"
+       "runtime"
+       "sync"
+       "sync/atomic"
+       "testing"
+       "time"
+)
+
+// TestSupersededGenerationsAreCollectable guards against derived resolution
+// data (resolvers, disqualify sets) pinning superseded index generations.
+// Every etag rotation used to leak the entire previous generation via the
+// global resolver and disqualify caches; replacing a generation now purges
+// its derived entries, and resolutions still holding a superseded generation
+// (as concurrent builds do when an index rotates mid-flight) must not
+// re-insert it.
+func TestSupersededGenerationsAreCollectable(t *testing.T) {
+       const nPkgs = 10000
+
+       idx := &APKIndex{Description: "leak-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",
+                       Description: fmt.Sprintf("synthetic package %d", i),
+                       Checksum:    []byte("01234567890123456789"),
+                       // A short chain keeps the resolve cheap; the index 
stays large.
+                       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)
+       }
+       var body bytes.Buffer
+       if _, err := body.ReadFrom(archive); err != nil {
+               t.Fatal(err)
+       }
+
+       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", etag.Load()))
+               if r.Method == http.MethodHead {
+                       return
+               }
+               _, _ = w.Write(body.Bytes())
+       }))
+       defer srv.Close()
+
+       ctx := context.Background()
+       heapMB := func() float64 {
+               runtime.GC()
+               runtime.GC()
+               var m runtime.MemStats
+               runtime.ReadMemStats(&m)
+               return float64(m.HeapAlloc) / (1 << 20)
+       }
+
+       resolve := func(indexes []NamedIndex) {
+               p := NewPkgResolver(ctx, indexes)
+               // Two arches so the disqualify cache is exercised too.
+               if _, _, err := p.GetPackagesWithDependencies(ctx, 
[]string{"pkg-0"},
+                       map[string][]NamedIndex{"x86_64": indexes, "aarch64": 
indexes}); err != nil {
+                       t.Fatal(err)
+               }
+       }
+
+       fetch := func(g int) []NamedIndex {
+               etag.Store(fmt.Sprintf("gen-%d", g))
+               indexes, err := GetRepositoryIndexes(ctx, []string{srv.URL}, 
nil, "x86_64",
+                       WithIgnoreSignatures(true), 
WithHTTPClient(srv.Client()))
+               if err != nil {
+                       t.Fatal(err)
+               }
+               return indexes
+       }
+
+       // Warm up allocator and caches, then measure growth across generations.
+       resolve(fetch(0))
+       base := heapMB()
+       const generations = 8
+       prev := fetch(1)
+       for g := 2; g <= generations+1; g++ {
+               // Fetching generation g evicts generation g-1, which an 
unfinished
+               // resolution still holds — like a build racing an index 
rotation.
+               // Resolving with it afterwards must not re-pin it.
+               indexes := fetch(g)
+               resolve(prev)
+               resolve(indexes)
+               prev = indexes
+       }
+       prev = nil
+       _ = prev
+       grown := heapMB() - base
+
+       // Each pinned generation retains several MB (index, resolver maps,
+       // disqualify sets). With correct purging, growth stays near zero; the
+       // unpurged caches grew by roughly generations * per-generation size
+       // (tens of MB here).
+       if grown > 20 {
+               t.Errorf("heap grew by %.1f MB across %d index generations, 
derived data is likely pinned", grown, generations)
+       }
+}
+
+// TestSupersededGenerationsConcurrentlyBounded drives index rotations and
+// resolutions concurrently, so a purge genuinely races an in-flight Get that
+// still holds a superseded generation - the case the insert gate exists for
+// and the sequential test above does not reach. It asserts the resolver cache
+// retains only a bounded number of generations for the URL rather than one
+// per rotation. Run under -race to also catch data races and deadlocks.
+func TestSupersededGenerationsConcurrentlyBounded(t *testing.T) {
+       const nPkgs = 300
+
+       idx := &APKIndex{Description: "concurrent-leak-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",
+                       Checksum:     []byte("01234567890123456789"),
+                       Dependencies: []string{fmt.Sprintf("pkg-%d", min(i+1, 
20))},
+                       BuildTime:    time.Unix(1700000000, 0).UTC(),
+               })
+       }
+       archive, err := ArchiveFromIndex(idx)
+       if err != nil {
+               t.Fatal(err)
+       }
+       var body bytes.Buffer
+       if _, err := body.ReadFrom(archive); err != nil {
+               t.Fatal(err)
+       }
+
+       // ETag tracks a counter the workers bump; each distinct value the HEAD
+       // observes is a new generation of the same URL.
+       var gen atomic.Int64
+       gen.Store(1)
+       srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, 
r *http.Request) {
+               w.Header().Set("ETag", fmt.Sprintf("%q", fmt.Sprintf("gen-%d", 
gen.Load())))
+               if r.Method == http.MethodHead {
+                       return
+               }
+               _, _ = w.Write(body.Bytes())
+       }))
+       defer srv.Close()
+
+       ctx := t.Context()
+       const (
+               workers = 4
+               rounds  = 60
+       )
+       var wg sync.WaitGroup
+       for range workers {
+               wg.Go(func() {
+                       for range rounds {
+                               gen.Add(1) // rotate: a new generation 
supersedes the current one
+                               indexes, err := GetRepositoryIndexes(ctx, 
[]string{srv.URL}, nil, "x86_64",
+                                       WithIgnoreSignatures(true), 
WithHTTPClient(srv.Client()))
+                               if err != nil {
+                                       t.Errorf("fetch: %v", err)
+                                       return
+                               }
+                               // Populates globalResolverCache for the 
current generation, or
+                               // takes the gate's uncached path if this index 
was superseded
+                               // by a concurrent rotation in the meantime.
+                               _ = NewPkgResolver(ctx, indexes)
+                       }
+               })
+       }
+       wg.Wait()
+
+       // The workers drove workers*rounds rotations. Without eviction the 
resolver
+       // cache would hold ~that many generations for the URL; with it, only 
the
+       // current generation and a few racing stragglers survive.
+       url := IndexURL(srv.URL, "x86_64")
+       if got := countResolverGenerations(url); got > 20 {
+               t.Errorf("resolver cache retains %d generations for %s; 
superseded generations are not being evicted (expected a small bound)", got, 
url)
+       }
+}
+
+// countResolverGenerations counts the distinct index generations the global
+// resolver cache still holds for a single index URL (one top-level trie child
+// per generation, since these resolvers are built from a single index).
+func countResolverGenerations(url string) int {
+       globalResolverCache.Lock()
+       defer globalResolverCache.Unlock()
+
+       n := 0
+       for k := range globalResolverCache.children {
+               if k.Source() == url {
+                       n++
+               }
+       }
+       return n
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.31/pkg/apk/apk/installed.go 
new/apko-1.2.32/pkg/apk/apk/installed.go
--- old/apko-1.2.31/pkg/apk/apk/installed.go    2026-07-31 20:19:09.000000000 
+0200
+++ new/apko-1.2.32/pkg/apk/apk/installed.go    2026-08-04 07:48:25.000000000 
+0200
@@ -237,7 +237,10 @@
                        continue
                }
 
-               if len(line) > 1 && line[1:2] != ":" {
+               if len(line) < 2 {
+                       return nil, fmt.Errorf("cannot parse line %d: expected 
\"<token>:<value>\", saw %q", linenr, line)
+               }
+               if line[1:2] != ":" {
                        return nil, fmt.Errorf("cannot parse line %d: expected 
\":\" in not found", linenr)
                }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.31/pkg/apk/apk/installed_test.go 
new/apko-1.2.32/pkg/apk/apk/installed_test.go
--- old/apko-1.2.31/pkg/apk/apk/installed_test.go       2026-07-31 
20:19:09.000000000 +0200
+++ new/apko-1.2.32/pkg/apk/apk/installed_test.go       2026-08-04 
07:48:25.000000000 +0200
@@ -653,6 +653,24 @@
        }
 }
 
+func TestParseInstalledShortLine(t *testing.T) {
+       // A single-character line in an installed database has no ":" 
delimiter and
+       // no value. Before the length guard, the "len(line) > 1" short-circuit 
let a
+       // length-1 line skip the delimiter check and then "line[2:]" sliced 
out of
+       // range and panicked. It should now be reported as a parse error 
instead.
+       for _, in := range []string{
+               "P:foo\nV:1.0\nP\n",
+               "P",
+               "X",
+       } {
+               t.Run(in, func(t *testing.T) {
+                       _, err := ParseInstalled(strings.NewReader(in))
+                       require.Error(t, err)
+                       require.Contains(t, err.Error(), "cannot parse line")
+               })
+       }
+}
+
 func TestRemoveOrphanedEntries(t *testing.T) {
        cases := []struct {
                name     string
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.31/pkg/apk/apk/shameful_global_caches.go 
new/apko-1.2.32/pkg/apk/apk/shameful_global_caches.go
--- old/apko-1.2.31/pkg/apk/apk/shameful_global_caches.go       2026-07-31 
20:19:09.000000000 +0200
+++ new/apko-1.2.32/pkg/apk/apk/shameful_global_caches.go       2026-08-04 
07:48:25.000000000 +0200
@@ -69,6 +69,13 @@
 }
 
 func (r *resolverCache) Get(ctx context.Context, indexes []NamedIndex) 
*PkgResolver {
+       // A superseded index generation will never be requested again once the
+       // in-flight resolutions holding it finish, and no future replacement
+       // will purge it, so it must not (re-)enter the cache.
+       if !currentAll(indexes) {
+               return newPkgResolver(ctx, indexes)
+       }
+
        r.Lock()
        defer r.Unlock()
 
@@ -79,9 +86,42 @@
        pr := newPkgResolver(ctx, indexes)
        r.fill(indexes, pr)
 
+       // A generation replacement that raced the fill has already run its
+       // purge, so purge its index ourselves.
+       for _, idx := range indexes {
+               if !globalIndexCache.isCurrent(idx) {
+                       r.forgetIndex(idx)
+               }
+       }
+
        return pr.Clone()
 }
 
+func currentAll(indexes []NamedIndex) bool {
+       for _, idx := range indexes {
+               if !globalIndexCache.isCurrent(idx) {
+                       return false
+               }
+       }
+       return true
+}
+
+// ForgetIndex removes every cached entry whose index combination includes
+// idx: a resolver over a superseded generation is superseded itself.
+func (r *resolverCache) ForgetIndex(idx NamedIndex) {
+       r.Lock()
+       defer r.Unlock()
+
+       r.forgetIndex(idx)
+}
+
+func (r *resolverCache) forgetIndex(idx NamedIndex) {
+       delete(r.children, idx)
+       for _, child := range r.children {
+               child.forgetIndex(idx)
+       }
+}
+
 // It is expensive to compute the complement
 // This a PkgResolver based on the input []NamedIndex.
 var globalDisqualifyCache = &disqualifyCache{}
@@ -135,6 +175,10 @@
        defer r.Unlock()
 
        indexes := slices.Concat(slices.Collect(maps.Values(byArch))...)
+       if !currentAll(indexes) {
+               return disqualifyDifference(ctx, byArch)
+       }
+
        slices.SortFunc(indexes, func(a, b NamedIndex) int {
                return strings.Compare(a.Name(), b.Name())
        })
@@ -145,5 +189,27 @@
        dq := disqualifyDifference(ctx, byArch)
        r.fill(indexes, dq)
 
+       for _, idx := range indexes {
+               if !globalIndexCache.isCurrent(idx) {
+                       r.forgetIndex(idx)
+               }
+       }
+
        return maps.Clone(dq)
 }
+
+// ForgetIndex removes every cached entry whose index combination includes
+// idx: a difference over a superseded generation is superseded itself.
+func (r *disqualifyCache) ForgetIndex(idx NamedIndex) {
+       r.Lock()
+       defer r.Unlock()
+
+       r.forgetIndex(idx)
+}
+
+func (r *disqualifyCache) forgetIndex(idx NamedIndex) {
+       delete(r.children, idx)
+       for _, child := range r.children {
+               child.forgetIndex(idx)
+       }
+}

++++++ apko.obsinfo ++++++
--- /var/tmp/diff_new_pack.bzLPuu/_old  2026-08-04 23:28:40.766344276 +0200
+++ /var/tmp/diff_new_pack.bzLPuu/_new  2026-08-04 23:28:40.770344417 +0200
@@ -1,5 +1,5 @@
 name: apko
-version: 1.2.31
-mtime: 1785521949
-commit: fc40b1ae3059b97fd591afe4d203379b16887b2a
+version: 1.2.32
+mtime: 1785822505
+commit: ea62bef5271a0c94f3fba641ee0e8d506b9b57ef
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/apko/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.apko.new.16738/vendor.tar.gz differ: char 21, line 1

Reply via email to