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
