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-05-06 19:19:39
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/apko (Old)
 and      /work/SRC/openSUSE:Factory/.apko.new.30200 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "apko"

Wed May  6 19:19:39 2026 rev:112 rq:1351146 version:1.2.11

Changes:
--------
--- /work/SRC/openSUSE:Factory/apko/apko.changes        2026-05-05 
15:16:47.926694866 +0200
+++ /work/SRC/openSUSE:Factory/.apko.new.30200/apko.changes     2026-05-06 
19:23:43.819519734 +0200
@@ -1,0 +2,19 @@
+Wed May 06 09:10:19 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 1.2.11:
+  * Tweak solver's same-origin heuristic (#2208)
+  * build(deps): bump k8s.io/apimachinery from 0.35.4 to 0.36.0
+    (#2189)
+  * build(deps): bump google.golang.org/api from 0.276.0 to 0.277.0
+    (#2212)
+  * build(deps): bump github.com/klauspost/compress from 1.18.5 to
+    1.18.6 (#2211)
+  * build(deps): bump github/codeql-action from 4.35.2 to 4.35.3
+    (#2213)
+  * build(deps): bump step-security/harden-runner from 2.19.0 to
+    2.19.1 (#2214)
+  * build(deps): bump chainguard-dev/actions from 1.6.15 to 1.6.17
+    (#2215)
+  * retry package fetch+expand on transient errors (#2210)
+
+-------------------------------------------------------------------

Old:
----
  apko-1.2.10.obscpio

New:
----
  apko-1.2.11.obscpio

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

Other differences:
------------------
++++++ apko.spec ++++++
--- /var/tmp/diff_new_pack.JDt60y/_old  2026-05-06 19:23:44.775559173 +0200
+++ /var/tmp/diff_new_pack.JDt60y/_new  2026-05-06 19:23:44.779559338 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           apko
-Version:        1.2.10
+Version:        1.2.11
 Release:        0
 Summary:        Build OCI images from APK packages directly without Dockerfile
 License:        Apache-2.0
@@ -27,7 +27,7 @@
 BuildRequires:  bash-completion
 BuildRequires:  fish
 BuildRequires:  zsh
-BuildRequires:  golang(API) >= 1.25
+BuildRequires:  golang(API) >= 1.26
 
 %description
 Build and publish OCI container images built from apk packages.

++++++ _service ++++++
--- /var/tmp/diff_new_pack.JDt60y/_old  2026-05-06 19:23:44.835561648 +0200
+++ /var/tmp/diff_new_pack.JDt60y/_new  2026-05-06 19:23:44.839561813 +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.10</param>
+    <param name="revision">refs/tags/v1.2.11</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.JDt60y/_old  2026-05-06 19:23:44.863562803 +0200
+++ /var/tmp/diff_new_pack.JDt60y/_new  2026-05-06 19:23:44.871563133 +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">eebbe627f86c584c3ff9df826411a2b33dca5ca6</param></service></servicedata>
+              <param 
name="changesrevision">bfd6776788292e020d8cbee9928f441781af72c0</param></service></servicedata>
 (No newline at EOF)
 

++++++ apko-1.2.10.obscpio -> apko-1.2.11.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.10/go.mod new/apko-1.2.11/go.mod
--- old/apko-1.2.10/go.mod      2026-05-04 00:41:58.000000000 +0200
+++ new/apko-1.2.11/go.mod      2026-05-05 21:57:53.000000000 +0200
@@ -1,6 +1,6 @@
 module chainguard.dev/apko
 
-go 1.25.7
+go 1.26.0
 
 require (
        chainguard.dev/sdk v0.1.54
@@ -13,7 +13,7 @@
        github.com/hashicorp/go-cleanhttp v0.5.2
        github.com/hashicorp/go-retryablehttp v0.7.8
        github.com/invopop/jsonschema v0.14.0
-       github.com/klauspost/compress v1.18.5
+       github.com/klauspost/compress v1.18.6
        github.com/klauspost/pgzip v1.2.6
        github.com/package-url/packageurl-go v0.1.5
        github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0
@@ -30,10 +30,10 @@
        golang.org/x/sync v0.20.0
        golang.org/x/sys v0.43.0
        golang.org/x/time v0.15.0
-       google.golang.org/api v0.276.0
+       google.golang.org/api v0.277.0
        gopkg.in/ini.v1 v1.67.1
        gopkg.in/yaml.v3 v3.0.1
-       k8s.io/apimachinery v0.35.4
+       k8s.io/apimachinery v0.36.0
        sigs.k8s.io/release-utils v0.12.4
 )
 
@@ -81,7 +81,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.14 // indirect
+       github.com/googleapis/enterprise-certificate-proxy v0.3.15 // indirect
        github.com/googleapis/gax-go/v2 v2.22.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
@@ -130,9 +130,9 @@
        golang.org/x/net v0.53.0 // indirect
        golang.org/x/text v0.36.0 // indirect
        google.golang.org/genproto/googleapis/api 
v0.0.0-20260401024825-9d38bb4040a9 // indirect
-       google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260401024825-9d38bb4040a9 // indirect
+       google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260427160629-7cedc36a6bc4 // indirect
        google.golang.org/grpc v1.80.0 // indirect
-       google.golang.org/protobuf v1.36.11 // 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.10/go.sum new/apko-1.2.11/go.sum
--- old/apko-1.2.10/go.sum      2026-05-04 00:41:58.000000000 +0200
+++ new/apko-1.2.11/go.sum      2026-05-05 21:57:53.000000000 +0200
@@ -120,8 +120,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.14 
h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8=
-github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod 
h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
+github.com/googleapis/enterprise-certificate-proxy v0.3.15 
h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
+github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod 
h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
 github.com/googleapis/gax-go/v2 v2.22.0 
h1:PjIWBpgGIVKGoCXuiCoP64altEJCj3/Ei+kSU5vlZD4=
 github.com/googleapis/gax-go/v2 v2.22.0/go.mod 
h1:irWBbALSr0Sk3qlqb9SyJ1h68WjgeFuiOzI4Rqw5+aY=
 github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 
h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o=
@@ -146,8 +146,8 @@
 github.com/kelseyhightower/envconfig v1.4.0/go.mod 
h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg=
 github.com/kevinburke/ssh_config v1.2.0 
h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4=
 github.com/kevinburke/ssh_config v1.2.0/go.mod 
h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM=
-github.com/klauspost/compress v1.18.5 
h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
-github.com/klauspost/compress v1.18.5/go.mod 
h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
+github.com/klauspost/compress v1.18.6 
h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
+github.com/klauspost/compress v1.18.6/go.mod 
h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
 github.com/klauspost/pgzip v1.2.6 
h1:8RXeL5crjEUFnR2/Sn6GJNWtSQ3Dk8pq4CL3jvdDyjU=
 github.com/klauspost/pgzip v1.2.6/go.mod 
h1:Ch1tH69qFZu15pkjo5kYi6mth2Zzwzt50oCQKQE9RUs=
 github.com/kr/pretty v0.1.0/go.mod 
h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
@@ -179,8 +179,8 @@
 github.com/muesli/termenv v0.16.0/go.mod 
h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 
h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod 
h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
-github.com/onsi/gomega v1.38.2 h1:eZCjf2xjZAqe+LeWvKb5weQ+NcPwX84kqJ0cZNxok2A=
-github.com/onsi/gomega v1.38.2/go.mod 
h1:W2MJcYxRGV63b418Ai34Ud0hEdTVXq9NW9+Sx6uXf3k=
+github.com/onsi/gomega v1.34.1 h1:EUMJIKUjM8sKjYbtxQI9A4z2o+rruxnzNvpknOXie6k=
+github.com/onsi/gomega v1.34.1/go.mod 
h1:kU1QgUvBDLXBJq618Xvm2LUX6rSAfRaFRTcdOeDLwwY=
 github.com/opencontainers/go-digest v1.0.0 
h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
 github.com/opencontainers/go-digest v1.0.0/go.mod 
h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
 github.com/opencontainers/image-spec v1.1.1 
h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040=
@@ -276,7 +276,6 @@
 go.uber.org/goleak v1.3.0/go.mod 
h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
 go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
 go.yaml.in/yaml/v2 v2.4.4/go.mod 
h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
-go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
 go.yaml.in/yaml/v3 v3.0.4/go.mod 
h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
 go.yaml.in/yaml/v4 v4.0.0-rc.2 h1:/FrI8D64VSr4HtGIlUtlFMGsm7H7pWTbj6vOLVZcA6s=
 go.yaml.in/yaml/v4 v4.0.0-rc.2/go.mod 
h1:aZqd9kCMsGL7AuUv/m/PvWLdg5sjJsZ4oHDEnfPPfY0=
@@ -351,18 +350,18 @@
 golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/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.276.0 h1:nVArUtfLEihtW+b0DdcqRGK1xoEm2+ltAihyztq7MKY=
-google.golang.org/api v0.276.0/go.mod 
h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw=
+google.golang.org/api v0.277.0 h1:HJfyJUiNeBBUMai7ez8u14wkp/gH/I4wpGbbO9o+cSk=
+google.golang.org/api v0.277.0/go.mod 
h1:B9TqLBwJqVjp1mtt7WeoQwWRwvu/400y5lETOql+giQ=
 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 
h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0=
 google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod 
h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I=
 google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 
h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA=
 google.golang.org/genproto/googleapis/api 
v0.0.0-20260401024825-9d38bb4040a9/go.mod 
h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M=
-google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 
h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg=
-google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260401024825-9d38bb4040a9/go.mod 
h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 
h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
+google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260427160629-7cedc36a6bc4/go.mod 
h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
 google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
 google.golang.org/grpc v1.80.0/go.mod 
h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
-google.golang.org/protobuf v1.36.11 
h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
-google.golang.org/protobuf v1.36.11/go.mod 
h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
+google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af 
h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
+google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af/go.mod 
h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod 
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c 
h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
@@ -378,8 +377,8 @@
 gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q=
 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
-k8s.io/apimachinery v0.35.4 h1:xtdom9RG7e+yDp71uoXoJDWEE2eOiHgeO4GdBzwWpds=
-k8s.io/apimachinery v0.35.4/go.mod 
h1:NNi1taPOpep0jOj+oRha3mBJPqvi0hGdaV8TCqGQ+cc=
+k8s.io/apimachinery v0.36.0 h1:jZyPzhd5Z+3h9vJLt0z9XdzW9VzNzWAUw+P1xZ9PXtQ=
+k8s.io/apimachinery v0.36.0/go.mod 
h1:FklypaRJt6n5wUIwWXIP6GJlIpUizTgfo1T/As+Tyxc=
 pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk=
 pgregory.net/rapid v1.2.0/go.mod 
h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
 sigs.k8s.io/release-utils v0.12.4 
h1:kuG6WTWGCKx5uUrJwl2uFErOKOw+4Ba8WrPmOQh5J3g=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.10/pkg/apk/apk/package_getter.go 
new/apko-1.2.11/pkg/apk/apk/package_getter.go
--- old/apko-1.2.10/pkg/apk/apk/package_getter.go       2026-05-04 
00:41:58.000000000 +0200
+++ new/apko-1.2.11/pkg/apk/apk/package_getter.go       2026-05-05 
21:57:53.000000000 +0200
@@ -20,12 +20,16 @@
        "crypto/sha1" //nolint:gosec // this is what apk tools is using
        "encoding/base64"
        "encoding/hex"
+       "errors"
        "fmt"
        "io"
+       "net"
        "net/http"
        "os"
        "path/filepath"
        "strings"
+       "syscall"
+       "time"
 
        "go.opentelemetry.io/otel"
        "go.opentelemetry.io/otel/attribute"
@@ -39,6 +43,60 @@
        "github.com/chainguard-dev/clog"
 )
 
+const (
+       // maxFetchRetries is the number of additional attempts after the first 
failure.
+       maxFetchRetries = 2
+       // retryBaseDelay is the base delay between retry attempts (scaled 
linearly by attempt number).
+       retryBaseDelay = 1 * time.Second
+)
+
+// isRetryableError reports whether err is a transient error that warrants 
retrying
+// the fetch+expand pipeline from scratch.
+func isRetryableError(err error) bool {
+       if err == nil {
+               return false
+       }
+       if errors.Is(err, context.Canceled) || errors.Is(err, 
context.DeadlineExceeded) {
+               return false
+       }
+       if errors.Is(err, io.ErrUnexpectedEOF) {
+               return true
+       }
+       if errors.Is(err, syscall.ECONNRESET) || errors.Is(err, 
syscall.ECONNABORTED) {
+               return true
+       }
+       var opErr *net.OpError
+       if errors.As(err, &opErr) {
+               return true
+       }
+       var httpErr *httpStatusError
+       if errors.As(err, &httpErr) {
+               return httpErr.statusCode >= 500 || httpErr.statusCode == 
http.StatusTooManyRequests
+       }
+       msg := err.Error()
+       for _, substr := range []string{
+               "unexpected EOF",
+               "connection reset",
+               "broken pipe",
+       } {
+               if strings.Contains(msg, substr) {
+                       return true
+               }
+       }
+       return false
+}
+
+// httpStatusError represents a non-OK HTTP response status.
+type httpStatusError struct {
+       statusCode int
+       status     string
+       url        string
+}
+
+func (e *httpStatusError) Error() string {
+       return fmt.Sprintf("unable to get package apk at %s: %s", e.url, 
e.status)
+}
+
 // PackageGetter abstracts how packages are fetched and expanded.
 type PackageGetter interface {
        // GetPackage fetches and returns an expanded package.
@@ -152,12 +210,6 @@
                }
        }
 
-       rc, err := d.fetchPackage(ctx, pkg)
-       if err != nil {
-               return nil, fmt.Errorf("fetching package %q: %w", 
pkg.PackageName(), err)
-       }
-       defer rc.Close()
-
        var expandOpts []expandapk.Option
        if d.apkControlMaxSize != 0 {
                expandOpts = append(expandOpts, 
expandapk.WithMaxControlSize(d.apkControlMaxSize))
@@ -165,6 +217,54 @@
        if d.apkDataMaxSize != 0 {
                expandOpts = append(expandOpts, 
expandapk.WithMaxDataSize(d.apkDataMaxSize))
        }
+
+       exp, err := d.fetchExpandAndVerify(ctx, pkg, cacheDir, expandOpts)
+       if err != nil {
+               return nil, err
+       }
+
+       // If we don't have a cache, we're done.
+       if d.cache == nil {
+               return exp, nil
+       }
+
+       return d.cachePackage(ctx, pkg, exp, cacheDir)
+}
+
+// fetchExpandAndVerify fetches, expands, and verifies a package, retrying on 
transient errors.
+func (d *defaultPackageGetter) fetchExpandAndVerify(ctx context.Context, pkg 
InstallablePackage, cacheDir string, expandOpts []expandapk.Option) 
(*expandapk.APKExpanded, error) {
+       var lastErr error
+       for attempt := range maxFetchRetries + 1 {
+               if attempt > 0 {
+                       delay := time.Duration(attempt) * retryBaseDelay
+                       clog.FromContext(ctx).Warnf("retrying fetch of %s 
(attempt %d/%d) after error: %v", pkg.PackageName(), attempt+1, 
maxFetchRetries+1, lastErr)
+                       select {
+                       case <-ctx.Done():
+                               return nil, context.Cause(ctx)
+                       case <-time.After(delay):
+                       }
+               }
+
+               exp, err := d.doFetchExpandAndVerify(ctx, pkg, cacheDir, 
expandOpts)
+               if err == nil {
+                       return exp, nil
+               }
+               if !isRetryableError(err) {
+                       return nil, err
+               }
+               lastErr = err
+       }
+       return nil, fmt.Errorf("after %d attempts: %w", maxFetchRetries+1, 
lastErr)
+}
+
+// doFetchExpandAndVerify performs a single fetch+expand+verify cycle for a 
package.
+func (d *defaultPackageGetter) doFetchExpandAndVerify(ctx context.Context, pkg 
InstallablePackage, cacheDir string, expandOpts []expandapk.Option) 
(*expandapk.APKExpanded, error) {
+       rc, err := d.fetchPackage(ctx, pkg)
+       if err != nil {
+               return nil, fmt.Errorf("fetching package %q: %w", 
pkg.PackageName(), err)
+       }
+       defer rc.Close()
+
        exp, err := expandapk.ExpandApkWithOptions(ctx, rc, cacheDir, 
expandOpts...)
        if err != nil {
                return nil, fmt.Errorf("expanding %s: %w", pkg.PackageName(), 
err)
@@ -200,12 +300,7 @@
                return nil, fmt.Errorf("package %q data hash mismatch: expected 
%x, got %x", pkg.PackageName(), expectedDataHash, exp.PackageHash)
        }
 
-       // If we don't have a cache, we're done.
-       if d.cache == nil {
-               return exp, nil
-       }
-
-       return d.cachePackage(ctx, pkg, exp, cacheDir)
+       return exp, nil
 }
 
 // sha1File returns the SHA-1 of the file at path.
@@ -268,7 +363,7 @@
                }
                if res.StatusCode != http.StatusOK {
                        res.Body.Close()
-                       return nil, fmt.Errorf("unable to get package apk at 
%s: %v", u, res.Status)
+                       return nil, &httpStatusError{statusCode: 
res.StatusCode, status: res.Status, url: u}
                }
                return res.Body, nil
        default:
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.10/pkg/apk/apk/package_getter_test.go 
new/apko-1.2.11/pkg/apk/apk/package_getter_test.go
--- old/apko-1.2.10/pkg/apk/apk/package_getter_test.go  2026-05-04 
00:41:58.000000000 +0200
+++ new/apko-1.2.11/pkg/apk/apk/package_getter_test.go  2026-05-05 
21:57:53.000000000 +0200
@@ -3,14 +3,18 @@
 import (
        "context"
        "encoding/base64"
+       "errors"
        "fmt"
        "io"
+       "net"
        "net/http"
        "net/http/httptest"
        "net/url"
        "os"
        "path/filepath"
        "strings"
+       "sync/atomic"
+       "syscall"
        "testing"
 
        "github.com/stretchr/testify/require"
@@ -366,3 +370,125 @@
                require.Contains(t, err.Error(), "data hash mismatch")
        })
 }
+
+func TestIsRetryableError(t *testing.T) {
+       tests := []struct {
+               name string
+               err  error
+               want bool
+       }{
+               {"nil", nil, false},
+               {"context canceled", context.Canceled, false},
+               {"context deadline", context.DeadlineExceeded, false},
+               {"unexpected EOF", io.ErrUnexpectedEOF, true},
+               {"wrapped unexpected EOF", fmt.Errorf("expanding foo: %w", 
io.ErrUnexpectedEOF), true},
+               {"connection reset", syscall.ECONNRESET, true},
+               {"connection aborted", syscall.ECONNABORTED, true},
+               {"net.OpError", &net.OpError{Op: "read", Err: 
errors.New("reset")}, true},
+               {"string unexpected EOF", errors.New("something unexpected EOF 
happened"), true},
+               {"string connection reset", errors.New("connection reset by 
peer"), true},
+               {"string broken pipe", errors.New("write: broken pipe"), true},
+               {"http 500", &httpStatusError{statusCode: 500, status: "500 
Internal Server Error", url: "https://example.com/pkg.apk"}, true},
+               {"http 502", &httpStatusError{statusCode: 502, status: "502 Bad 
Gateway", url: "https://example.com/pkg.apk"}, true},
+               {"http 429", &httpStatusError{statusCode: 429, status: "429 Too 
Many Requests", url: "https://example.com/pkg.apk"}, true},
+               {"http 404", &httpStatusError{statusCode: 404, status: "404 Not 
Found", url: "https://example.com/pkg.apk"}, false},
+               {"wrapped http 503", fmt.Errorf("fetching package: %w", 
&httpStatusError{statusCode: 503, status: "503 Service Unavailable", url: 
"https://example.com/pkg.apk"}), true},
+               {"checksum mismatch", errors.New("control hash mismatch: 
expected abc, got def"), false},
+               {"generic error", errors.New("some other error"), false},
+       }
+       for _, tt := range tests {
+               t.Run(tt.name, func(t *testing.T) {
+                       got := isRetryableError(tt.err)
+                       require.Equal(t, tt.want, got)
+               })
+       }
+}
+
+// truncatingTransport serves a valid APK file but truncates the response on 
the first N requests.
+type truncatingTransport struct {
+       root      string
+       failCount int32 // number of remaining requests to truncate
+       attempts  atomic.Int32
+}
+
+func (t *truncatingTransport) RoundTrip(req *http.Request) (*http.Response, 
error) {
+       attempt := t.attempts.Add(1)
+
+       filename := filepath.Base(req.URL.Path)
+       data, err := os.ReadFile(filepath.Join(t.root, filename))
+       if err != nil {
+               return &http.Response{
+                       StatusCode: http.StatusNotFound,
+                       Body:       io.NopCloser(strings.NewReader("not 
found")),
+               }, nil
+       }
+
+       if attempt <= atomic.LoadInt32(&t.failCount) {
+               // Return a truncated response to simulate unexpected EOF 
during decompression.
+               truncated := data[:len(data)/2]
+               return &http.Response{
+                       StatusCode: http.StatusOK,
+                       Body:       
io.NopCloser(strings.NewReader(string(truncated))),
+               }, nil
+       }
+
+       return &http.Response{
+               StatusCode: http.StatusOK,
+               Body:       io.NopCloser(strings.NewReader(string(data))),
+       }, nil
+}
+
+func TestGetPackage_RetryOnTransientError(t *testing.T) {
+       repo := Repository{URI: fmt.Sprintf("%s/%s", testAlpineRepos, testArch)}
+       repoWithIndex := repo.WithIndex(&APKIndex{Packages: 
[]*Package{&testPkg}})
+       pkg := NewRepositoryPackage(&testPkg, repoWithIndex)
+       ctx := context.Background()
+
+       tr := &truncatingTransport{
+               root:      testPrimaryPkgDir,
+               failCount: 1, // fail once, then succeed
+       }
+
+       getter := newDefaultPackageGetter(
+               &http.Client{Transport: tr},
+               nil,
+               auth.DefaultAuthenticators,
+       )
+
+       exp, err := getter.GetPackage(ctx, pkg)
+       require.NoError(t, err, "expected retry to succeed")
+       require.NotNil(t, exp)
+       _ = exp.Close()
+
+       // Should have taken 2 attempts (1 failure + 1 success).
+       require.Equal(t, int32(2), tr.attempts.Load(), "expected exactly 2 
fetch attempts")
+}
+
+func TestGetPackage_NoRetryOnPermanentError(t *testing.T) {
+       tampered := testPkg
+       tampered.Checksum = make([]byte, len(testPkg.Checksum)) // wrong 
checksum
+       ctx := context.Background()
+
+       repo := Repository{URI: fmt.Sprintf("%s/%s", testAlpineRepos, testArch)}
+       repoWithIndex := repo.WithIndex(&APKIndex{Packages: 
[]*Package{&tampered}})
+       pkg := NewRepositoryPackage(&tampered, repoWithIndex)
+
+       // Use a transport that always serves the real APK data so we can count 
attempts.
+       tr := &truncatingTransport{
+               root:      testPrimaryPkgDir,
+               failCount: 0, // never truncate — always serve valid data
+       }
+
+       getter := newDefaultPackageGetter(
+               &http.Client{Transport: tr},
+               nil,
+               auth.DefaultAuthenticators,
+       )
+
+       _, err := getter.GetPackage(ctx, pkg)
+       require.Error(t, err)
+       require.Contains(t, err.Error(), "control hash mismatch")
+
+       // Should have attempted exactly once — permanent errors must not be 
retried.
+       require.Equal(t, int32(1), tr.attempts.Load(), "expected exactly 1 
fetch attempt for permanent error")
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.10/pkg/apk/apk/repo.go 
new/apko-1.2.11/pkg/apk/apk/repo.go
--- old/apko-1.2.10/pkg/apk/apk/repo.go 2026-05-04 00:41:58.000000000 +0200
+++ new/apko-1.2.11/pkg/apk/apk/repo.go 2026-05-05 21:57:53.000000000 +0200
@@ -569,11 +569,11 @@
 func (p *PkgResolver) GetPackageWithDependencies(ctx context.Context, pkgName 
string, existing map[string]*RepositoryPackage, dq 
map[*RepositoryPackage]string) (*RepositoryPackage, []*RepositoryPackage, 
[]string, error) {
        parents := make(map[string]bool)
        localExisting := make(map[string]*RepositoryPackage, len(existing))
-       existingOrigins := map[string]bool{}
+       existingOrigins := map[string]string{}
        for k, v := range existing {
                localExisting[k] = v
                if v != nil && v.Origin != "" {
-                       existingOrigins[v.Origin] = true
+                       existingOrigins[v.Origin] = v.Version
                }
        }
 
@@ -712,7 +712,7 @@
 // It might change the order of install.
 // In other words, this _should_ be a DAG (acyclical), but because the packages
 // are just listing dependencies in text, it might be cyclical. We need to be 
careful of that.
-func (p *PkgResolver) getPackageDependencies(ctx context.Context, pkg 
*RepositoryPackage, allowPin string, parents map[string]bool, existing 
map[string]*RepositoryPackage, existingOrigins map[string]bool, dq 
map[*RepositoryPackage]string) (dependencies []*RepositoryPackage, conflicts 
[]string, err error) {
+func (p *PkgResolver) getPackageDependencies(ctx context.Context, pkg 
*RepositoryPackage, allowPin string, parents map[string]bool, existing 
map[string]*RepositoryPackage, existingOrigins map[string]string, dq 
map[*RepositoryPackage]string) (dependencies []*RepositoryPackage, conflicts 
[]string, err error) {
        if err := ctx.Err(); err != nil {
                return nil, nil, context.Cause(ctx)
        }
@@ -902,7 +902,9 @@
                conflicts = append(conflicts, confs...)
                for _, dep := range subDeps {
                        existing[dep.Name] = dep
-                       existingOrigins[dep.Origin] = true
+                       if dep.Origin != "" {
+                               existingOrigins[dep.Origin] = dep.Version
+                       }
                }
        }
        return dependencies, conflicts, nil
@@ -944,11 +946,11 @@
 // For example, if the original search was for package "a", then pkgs may 
contain some that
 // are named "a", but others that provided "a". In that case, we should look 
not at the
 // version of the package, but the version of "a" that the package provides.
-func (p *PkgResolver) sortPackages(pkgs []*repositoryPackage, compare 
*RepositoryPackage, name string, existing map[string]*RepositoryPackage, 
existingOrigins map[string]bool, pin string) {
+func (p *PkgResolver) sortPackages(pkgs []*repositoryPackage, compare 
*RepositoryPackage, name string, existing map[string]*RepositoryPackage, 
existingOrigins map[string]string, pin string) {
        slices.SortFunc(pkgs, p.comparePackages(compare, name, existing, 
existingOrigins, pin))
 }
 
-func (p *PkgResolver) comparePackages(compare *RepositoryPackage, name string, 
existing map[string]*RepositoryPackage, existingOrigins map[string]bool, pin 
string) func(a, b *repositoryPackage) int { //nolint:gocyclo
+func (p *PkgResolver) comparePackages(compare *RepositoryPackage, name string, 
existing map[string]*RepositoryPackage, existingOrigins map[string]string, pin 
string) func(a, b *repositoryPackage) int { //nolint:gocyclo
        return func(a, b *repositoryPackage) int {
                // determine versions
                iVersionStr := p.getDepVersionForName(a, name)
@@ -990,9 +992,12 @@
                }
                // both matched, so keep looking
 
-               // see if an origin already is installed
-               iOriginMatched := existingOrigins[a.Origin]
-               jOriginMatched := existingOrigins[b.Origin]
+               // Prefer a candidate whose origin we've already pulled in, but 
only at
+               // the version we pulled in. Otherwise this heuristic would 
keep us on
+               // an older version of an origin (e.g. when a package moves 
origins at
+               // a newer version, or an old binary lingers in the index after 
rebuild).
+               iOriginMatched := a.Origin != "" && existingOrigins[a.Origin] 
== a.Version
+               jOriginMatched := b.Origin != "" && existingOrigins[b.Origin] 
== b.Version
                if iOriginMatched && !jOriginMatched {
                        return -1
                }
@@ -1052,7 +1057,7 @@
        }
 }
 
-func (p *PkgResolver) bestPackage(pkgs []*repositoryPackage, compare 
*RepositoryPackage, name string, existing map[string]*RepositoryPackage, 
existingOrigins map[string]bool, pin string) *repositoryPackage {
+func (p *PkgResolver) bestPackage(pkgs []*repositoryPackage, compare 
*RepositoryPackage, name string, existing map[string]*RepositoryPackage, 
existingOrigins map[string]string, pin string) *repositoryPackage {
        if len(pkgs) == 0 {
                return nil
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.10/pkg/apk/apk/repo_test.go 
new/apko-1.2.11/pkg/apk/apk/repo_test.go
--- old/apko-1.2.10/pkg/apk/apk/repo_test.go    2026-05-04 00:41:58.000000000 
+0200
+++ new/apko-1.2.11/pkg/apk/apk/repo_test.go    2026-05-05 21:57:53.000000000 
+0200
@@ -764,7 +764,7 @@
                                pkgs            []*RepositoryPackage
                                pkg             *RepositoryPackage
                                existing        = 
map[string]*RepositoryPackage{}
-                               existingOrigins = map[string]bool{}
+                               existingOrigins = map[string]string{}
                        )
                        for _, pkg := range tt.pkgs {
                                // we cheat and use the InstalledSize for the 
preferred order, so that it gets carried around.
@@ -778,7 +778,7 @@
                        }
                        for _, pkg := range tt.existing {
                                existing[pkg.pkg.Name] = 
NewRepositoryPackage(pkg.pkg, &RepositoryWithIndex{Repository: &Repository{URI: 
pkg.repo}})
-                               existingOrigins[pkg.pkg.Origin] = true
+                               existingOrigins[pkg.pkg.Origin] = 
pkg.pkg.Version
                        }
                        namedPkgs := testNamedPackageFromPackages(pkgs)
                        pr := NewPkgResolver(context.Background(), 
[]NamedIndex{})
@@ -900,6 +900,43 @@
        }
 }
 
+// When an origin is bumped to a new version that no longer ships one of its
+// old subpackages, but the old subpackage still lingers in the index providing
+// some so:, the resolver must not prefer that stale package over a fresh
+// provider in a different origin just because we already pulled the origin at
+// a different version.
+//
+// As a contrived example, if we want to drop libcrypt1 from glibc's origin,
+// it was difficult because we would prefer the libcrypt.so.1 provider due to
+// that same-origin heuristic. This tests that the heuristic does not activate
+// if the origins match but the versions don't.
+func TestProviderAcrossOriginVersionBump(t *testing.T) {
+       repo := Repository{}
+       index := repo.WithIndex(&APKIndex{
+               Packages: []*Package{
+                       {Name: "glibc", Version: "1", Origin: "glibc"},
+                       {Name: "libcrypt1", Version: "1", Origin: "glibc",
+                               Provides: []string{"so:libcrypt.so.1=1"}},
+                       {Name: "glibc", Version: "2", Origin: "glibc"},
+                       {Name: "libxcrypt", Version: "2", Origin: "libxcrypt",
+                               Provides: []string{"so:libcrypt.so.1=1"}},
+                       {Name: "consumer", Version: "1",
+                               Dependencies: []string{"so:libcrypt.so.1"}},
+               },
+       })
+       resolver := NewPkgResolver(context.Background(), 
testNamedRepositoryFromIndexes([]*RepositoryWithIndex{index}))
+       pkgs, _, err := 
resolver.GetPackagesWithDependencies(context.Background(), []string{"consumer", 
"glibc=2"}, nil)
+       require.NoError(t, err)
+
+       got := make([]string, 0, len(pkgs))
+       for _, p := range pkgs {
+               got = append(got, p.Filename())
+       }
+       require.NotContains(t, got, "libcrypt1-1.apk", "should not pull stale 
libcrypt1 from old glibc origin")
+       require.Contains(t, got, "libxcrypt-2.apk", "should select libxcrypt 
for so:libcrypt.so.1")
+       require.Contains(t, got, "glibc-2.apk")
+}
+
 func TestConstrains(t *testing.T) {
        providers := map[string][]string{
                "ld-linux=2.38-r10": {"so:ld-linux-aarch64.so.1=1.0"},
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.10/pkg/apk/apk/version_test.go 
new/apko-1.2.11/pkg/apk/apk/version_test.go
--- old/apko-1.2.10/pkg/apk/apk/version_test.go 2026-05-04 00:41:58.000000000 
+0200
+++ new/apko-1.2.11/pkg/apk/apk/version_test.go 2026-05-05 21:57:53.000000000 
+0200
@@ -910,10 +910,10 @@
                        found := filterPackages(pkgs, 
map[*RepositoryPackage]string{}, withVersion(tt.version, tt.compare), 
withPreferPin(tt.pin), withInstalledPackage(tt.installed))
                        // add the existing in, if any
                        existing := make(map[string]*RepositoryPackage)
-                       existingOrigins := make(map[string]bool)
+                       existingOrigins := make(map[string]string)
                        if tt.installed != nil {
                                existing[tt.installed.Name] = tt.installed
-                               existingOrigins[tt.installed.Origin] = true
+                               existingOrigins[tt.installed.Origin] = 
tt.installed.Version
                        }
                        pkg := pr.bestPackage(found, nil, "", existing, 
existingOrigins, tt.pin)
                        if tt.want == "" {

++++++ apko.obsinfo ++++++
--- /var/tmp/diff_new_pack.JDt60y/_old  2026-05-06 19:23:45.571592011 +0200
+++ /var/tmp/diff_new_pack.JDt60y/_new  2026-05-06 19:23:45.575592176 +0200
@@ -1,5 +1,5 @@
 name: apko
-version: 1.2.10
-mtime: 1777848118
-commit: eebbe627f86c584c3ff9df826411a2b33dca5ca6
+version: 1.2.11
+mtime: 1778011073
+commit: bfd6776788292e020d8cbee9928f441781af72c0
 

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

Reply via email to