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-06-15 19:45:18
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/apko (Old)
 and      /work/SRC/openSUSE:Factory/.apko.new.1981 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "apko"

Mon Jun 15 19:45:18 2026 rev:118 rq:1359346 version:1.2.17

Changes:
--------
--- /work/SRC/openSUSE:Factory/apko/apko.changes        2026-06-08 
14:26:12.119251366 +0200
+++ /work/SRC/openSUSE:Factory/.apko.new.1981/apko.changes      2026-06-15 
19:48:57.785302887 +0200
@@ -1,0 +2,32 @@
+Mon Jun 15 05:00:45 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 1.2.17:
+  * build(deps): bump go.opentelemetry.io/otel/trace from 1.43.0 to
+    1.44.0 (#2256)
+  * build(deps): bump go.opentelemetry.io/otel from 1.43.0 to
+    1.44.0 (#2258)
+  * build(deps): bump go.step.sm/crypto from 0.81.0 to 0.82.0
+    (#2270)
+  * build(deps): bump chainguard.dev/sdk from 0.1.55 to 0.1.57
+    (#2275)
+  * build(deps): bump golang.org/x/sync from 0.20.0 to 0.21.0
+    (#2265)
+  * build(deps): bump google.golang.org/api from 0.280.0 to 0.283.0
+    (#2262)
+  * fix(auth): let chainctl write to terminal for interactive login
+    (#2276)
+  * paths: preserve setuid/setgid/sticky bits in permissions
+    (#2274)
+  * build(deps): bump actions/checkout from 6.0.2 to 6.0.3 (#2264)
+  * build(deps): bump golang.org/x/sys from 0.45.0 to 0.46.0
+    (#2266)
+  * build(deps): bump github/codeql-action from 4.36.0 to 4.36.2
+    (#2267)
+  * build(deps): bump chainguard-dev/actions from 1.6.19 to 1.6.22
+    (#2272)
+  * build(deps): bump gopkg.in/ini.v1 from 1.67.2 to 1.67.3 (#2273)
+  * Match apk-tools' provider comparison ordering in the solver
+    (#2271)
+  * expandapk: materialize uncompressed .dat.tar atomically (#2269)
+
+-------------------------------------------------------------------

Old:
----
  apko-1.2.16.obscpio

New:
----
  apko-1.2.17.obscpio

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

Other differences:
------------------
++++++ apko.spec ++++++
--- /var/tmp/diff_new_pack.cAZF1v/_old  2026-06-15 19:48:59.073357012 +0200
+++ /var/tmp/diff_new_pack.cAZF1v/_new  2026-06-15 19:48:59.081357348 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           apko
-Version:        1.2.16
+Version:        1.2.17
 Release:        0
 Summary:        Build OCI images from APK packages directly without Dockerfile
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.cAZF1v/_old  2026-06-15 19:48:59.121359029 +0200
+++ /var/tmp/diff_new_pack.cAZF1v/_new  2026-06-15 19:48:59.133359533 +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.16</param>
+    <param name="revision">refs/tags/v1.2.17</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.cAZF1v/_old  2026-06-15 19:48:59.165360878 +0200
+++ /var/tmp/diff_new_pack.cAZF1v/_new  2026-06-15 19:48:59.169361046 +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">8bf905593d457345beafe51490dd28a619d0690a</param></service></servicedata>
+              <param 
name="changesrevision">301fd0d625c79684d29b2de779b7fd882e8fd822</param></service></servicedata>
 (No newline at EOF)
 

++++++ apko-1.2.16.obscpio -> apko-1.2.17.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.16/go.mod new/apko-1.2.17/go.mod
--- old/apko-1.2.16/go.mod      2026-06-04 07:01:25.000000000 +0200
+++ new/apko-1.2.17/go.mod      2026-06-13 03:07:30.000000000 +0200
@@ -3,7 +3,7 @@
 go 1.26.0
 
 require (
-       chainguard.dev/sdk v0.1.55
+       chainguard.dev/sdk v0.1.57
        github.com/chainguard-dev/clog v1.8.0
        github.com/charmbracelet/log v1.0.0
        github.com/go-git/go-git/v5 v5.19.1
@@ -23,15 +23,16 @@
        github.com/tmc/dot v0.2.0
        github.com/u-root/u-root v0.16.0
        go.lsp.dev/uri v0.3.0
-       go.opentelemetry.io/otel v1.43.0
-       go.opentelemetry.io/otel/trace v1.43.0
-       go.step.sm/crypto v0.81.0
+       go.opentelemetry.io/otel v1.44.0
+       go.opentelemetry.io/otel/trace v1.44.0
+       go.step.sm/crypto v0.82.0
        golang.org/x/oauth2 v0.36.0
-       golang.org/x/sync v0.20.0
-       golang.org/x/sys v0.45.0
+       golang.org/x/sync v0.21.0
+       golang.org/x/sys v0.46.0
+       golang.org/x/term v0.43.0
        golang.org/x/time v0.15.0
-       google.golang.org/api v0.280.0
-       gopkg.in/ini.v1 v1.67.2
+       google.golang.org/api v0.283.0
+       gopkg.in/ini.v1 v1.67.3
        gopkg.in/yaml.v3 v3.0.1
        k8s.io/apimachinery v0.36.1
        sigs.k8s.io/release-utils v0.12.4
@@ -81,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.15 // indirect
+       github.com/googleapis/enterprise-certificate-proxy v0.3.16 // 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
@@ -123,15 +124,15 @@
        go.opentelemetry.io/auto/sdk v1.2.1 // indirect
        
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc 
v0.67.0 // indirect
        go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 
// indirect
-       go.opentelemetry.io/otel/metric v1.43.0 // indirect
+       go.opentelemetry.io/otel/metric v1.44.0 // indirect
        go.yaml.in/yaml/v2 v2.4.4 // indirect
        go.yaml.in/yaml/v4 v4.0.0-rc.2 // indirect
-       golang.org/x/crypto v0.51.0 // indirect
+       golang.org/x/crypto v0.52.0 // indirect
        golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
-       golang.org/x/net v0.54.0 // indirect
+       golang.org/x/net v0.55.0 // indirect
        golang.org/x/text v0.37.0 // indirect
        google.golang.org/genproto/googleapis/api 
v0.0.0-20260401024825-9d38bb4040a9 // indirect
-       google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260511170946-3700d4141b60 // indirect
+       google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260523011958-0a33c5d7ca68 // indirect
        google.golang.org/grpc v1.81.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.16/go.sum new/apko-1.2.17/go.sum
--- old/apko-1.2.16/go.sum      2026-06-04 07:01:25.000000000 +0200
+++ new/apko-1.2.17/go.sum      2026-06-13 03:07:30.000000000 +0200
@@ -1,7 +1,7 @@
 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.55 h1:QEGW0BVNcvbUkjBFKKCR/34in69PPoV4jZejkN45uu4=
-chainguard.dev/sdk v0.1.55/go.mod 
h1:Yz9yDGzD0/q9/uwaKEe3BThr7Kbs6YisXT3D3yjSSfs=
+chainguard.dev/sdk v0.1.57 h1:PDfKlCtKiElIpFLKh6C0GA2ueuTZeWmymrh0AxoG8i0=
+chainguard.dev/sdk v0.1.57/go.mod 
h1:LWSnEw2+4Y6dbmXyFHqbiyEKMWhG5FtWd/q8A82MX48=
 cloud.google.com/go/auth v0.20.0 
h1:kXTssoVb4azsVDoUiF8KvxAqrsQcQtB53DcSgta74CA=
 cloud.google.com/go/auth v0.20.0/go.mod 
h1:942/yi/itH1SsmpyrbnTMDgGfdy2BUqIKyd0cyYLc5Q=
 cloud.google.com/go/auth/oauth2adapt v0.2.8 
h1:keo8NaayQZ6wimpNSmW5OPc283g65QNIiLpZnkHRbnc=
@@ -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.15 
h1:xolVQTEXusUcAA5UgtyRLjelpFFHWlPQ4XfWGc7MBas=
-github.com/googleapis/enterprise-certificate-proxy v0.3.15/go.mod 
h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg=
+github.com/googleapis/enterprise-certificate-proxy v0.3.16 
h1:F/VPrx0YPBdksZJQdCAp0WUsqnNmZpUZszzfYt0M5Dw=
+github.com/googleapis/enterprise-certificate-proxy v0.3.16/go.mod 
h1:9Yb0eAkH/Xqhvv3zbeKf/+wMJqCeocWc6KIhDvEAuYE=
 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=
@@ -262,18 +262,18 @@
 go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc 
v0.67.0/go.mod h1:NoUCKYWK+3ecatC4HjkRktREheMeEtrXoQxrqYFeHSc=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0 
h1:OyrsyzuttWTSur2qN/Lm0m2a8yqyIjUVBZcxFPuXq2o=
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.67.0/go.mod 
h1:C2NGBr+kAB4bk3xtMXfZ94gqFDtg/GkI7e9zqGh5Beg=
-go.opentelemetry.io/otel v1.43.0 
h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
-go.opentelemetry.io/otel v1.43.0/go.mod 
h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
-go.opentelemetry.io/otel/metric v1.43.0 
h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
-go.opentelemetry.io/otel/metric v1.43.0/go.mod 
h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
+go.opentelemetry.io/otel v1.44.0 
h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
+go.opentelemetry.io/otel v1.44.0/go.mod 
h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
+go.opentelemetry.io/otel/metric v1.44.0 
h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
+go.opentelemetry.io/otel/metric v1.44.0/go.mod 
h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
 go.opentelemetry.io/otel/sdk v1.43.0 
h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
 go.opentelemetry.io/otel/sdk v1.43.0/go.mod 
h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
 go.opentelemetry.io/otel/sdk/metric v1.43.0 
h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
 go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod 
h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
-go.opentelemetry.io/otel/trace v1.43.0 
h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
-go.opentelemetry.io/otel/trace v1.43.0/go.mod 
h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
-go.step.sm/crypto v0.81.0 h1:e+ouzpNt3Xm4dp7HGXhgYB5y4iFik3vh3phHKWmvugU=
-go.step.sm/crypto v0.81.0/go.mod 
h1:fsTizqQeASjTXnbv9O00XtRlIuXRkCdoRiJNyXGQujc=
+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.82.0 h1:JOT8b/7Jh4My3mxE4U7UkuaN2sUGkZ8fnjznXaTGoRE=
+go.step.sm/crypto v0.82.0/go.mod 
h1:qyLTv666WJ6ImFPUjljux+684Y/GGYUjAZcKCnc6yBs=
 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=
@@ -285,8 +285,8 @@
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod 
h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod 
h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
 golang.org/x/crypto v0.19.0/go.mod 
h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
-golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
-golang.org/x/crypto v0.51.0/go.mod 
h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
+golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
+golang.org/x/crypto v0.52.0/go.mod 
h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f 
h1:W3F4c+6OLc6H2lb//N1q4WpJkhzJCK5J6kUi1NTVXfM=
 golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f/go.mod 
h1:J1xhfL/vlindoeF/aINzNzt2Bket5bjo9sdOYzOsU80=
 golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod 
h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
@@ -299,15 +299,15 @@
 golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod 
h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
 golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
 golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
-golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
-golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
+golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8=
+golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww=
 golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs=
 golang.org/x/oauth2 v0.36.0/go.mod 
h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q=
 golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod 
h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
 golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
-golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
-golang.org/x/sync v0.20.0/go.mod 
h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
+golang.org/x/sync v0.21.0 h1:HLII4xRRTtCRkxYp4HNFF0Js/Og6q2i++KXbg0gHCwM=
+golang.org/x/sync v0.21.0/go.mod 
h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
 golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod 
h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
 golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
 golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod 
h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -322,8 +322,8 @@
 golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
-golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
-golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
+golang.org/x/sys v0.46.0 h1:noSf2Fq6F8DBgS+LysIkx7rIExoNHJsxOAtPp4rthXw=
+golang.org/x/sys v0.46.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod 
h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod 
h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
@@ -352,14 +352,14 @@
 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.280.0 h1:F4OfEHZhZh6a7uTufJAXXVd/2TQ8EjM4vZH+jX/vFYk=
-google.golang.org/api v0.280.0/go.mod 
h1:oGKmPZRDoD3vdkf6MA7F4VNkR1rxCiuaPSkhsf3EolU=
+google.golang.org/api v0.283.0 h1:0lkp8u0MPwJVHqRL+nJlMAoZVVzbmiXmFHXMOTmSPik=
+google.golang.org/api v0.283.0/go.mod 
h1:6Wssta4c5n9qHq5CBhmlai5h/PUa1djdDAIhYEHyvcM=
 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-20260511170946-3700d4141b60 
h1:seT2EwLWM78plQ7wcDfuWBc/4FAEAXDDiaSol4ku4qo=
-google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260511170946-3700d4141b60/go.mod 
h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20260523011958-0a33c5d7ca68 
h1:PvEgGJf9C/1u5CHkInMg7UFYYUoiaQmW2LbtH0pjB78=
+google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260523011958-0a33c5d7ca68/go.mod 
h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
 google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
 google.golang.org/grpc v1.81.1/go.mod 
h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
 google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af 
h1:+5/Sw3GsDNlEmu7TfklWKPdQ0Ykja5VEmq2i817+jbI=
@@ -368,8 +368,8 @@
 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=
 gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod 
h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
-gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss=
-gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
+gopkg.in/ini.v1 v1.67.3 h1:iM9Lhz5MRSGhHVGGwCuzG9KO8PoirCXj/m/qTmOJJQw=
+gopkg.in/ini.v1 v1.67.3/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
 gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME=
 gopkg.in/warnings.v0 v0.1.2/go.mod 
h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI=
 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.16/pkg/apk/apk/repo.go 
new/apko-1.2.17/pkg/apk/apk/repo.go
--- old/apko-1.2.16/pkg/apk/apk/repo.go 2026-06-04 07:01:25.000000000 +0200
+++ new/apko-1.2.17/pkg/apk/apk/repo.go 2026-06-13 03:07:30.000000000 +0200
@@ -1012,7 +1012,47 @@
                        return 1
                }
 
-               // check provider priority
+               // The remaining steps follow apk-tools' compare_providers 
ordering
+               // (latest by requested name, then latest by principal name, 
then the
+               // highest declared provider priority), with one deliberate 
divergence
+               // in the final tiebreak below.
+               // 
https://github.com/alpinelinux/apk-tools/blob/20fe3dccc423bd401b9828958124664704dedb00/src/solver.c#L641-L688
+
+               // Latest by requested name: compare the versions the 
candidates carry
+               // for the name being resolved. An unversioned provide carries 
no
+               // version for the name and ties with anything.
+               if iVersionStr != "" && jVersionStr != "" && iVersionStr != 
jVersionStr {
+                       iVersion, err := cachedParseVersion(iVersionStr)
+                       if err != nil {
+                               return 1
+                       }
+                       jVersion, err := cachedParseVersion(jVersionStr)
+                       if err != nil {
+                               // If j fails to parse, prefer i.
+                               return -1
+                       }
+                       if versions := CompareVersions(iVersion, jVersion); 
versions != equal {
+                               return -1 * versions
+                       }
+               }
+
+               // Latest by principal name.
+               if a.Name == b.Name && a.Version != b.Version {
+                       iVersion, err := cachedParseVersion(a.Version)
+                       if err != nil {
+                               return 1
+                       }
+                       jVersion, err := cachedParseVersion(b.Version)
+                       if err != nil {
+                               // If j fails to parse, prefer i.
+                               return -1
+                       }
+                       if versions := CompareVersions(iVersion, jVersion); 
versions != equal {
+                               return -1 * versions
+                       }
+               }
+
+               // Highest declared provider priority.
                if a.ProviderPriority != b.ProviderPriority {
                        if a.ProviderPriority > b.ProviderPriority {
                                return -1
@@ -1021,23 +1061,17 @@
                        // a < b
                        return 1
                }
-               // both matched or both did not, so just compare versions
-               // version priority
-               iVersion, err := cachedParseVersion(iVersionStr)
-               if err != nil {
-                       return 1
-               }
-               jVersion, err := cachedParseVersion(jVersionStr)
-               if err != nil {
-                       // If j fails to parse, prefer i.
-                       return -1
-               }
-               versions := CompareVersions(iVersion, jVersion)
-               if versions != equal {
-                       return -1 * versions
-               }
-               // if versions are equal, they might not be the same as the 
package versions
-               if iVersionStr != a.Version || jVersionStr != b.Version {
+
+               // Prefer the more recent build, regardless of which repository 
carries
+               // it. This is where we deliberately diverge from apk-tools, 
which
+               // prefers the lowest available repository. Image 
configurations can
+               // layer a variant repository ahead of the main one, with 
packages
+               // providing the same virtual names, such as sonames, as the 
main
+               // repository's packages. Preferring the earlier repository 
would let
+               // those builds capture shared provides from every package in 
later
+               // repositories. Preferring the higher version also stops a 
stale
+               // build lingering in any index from winning the tie.
+               if a.Version != b.Version {
                        iVersion, err := cachedParseVersion(a.Version)
                        if err != nil {
                                return 1
@@ -1066,7 +1100,8 @@
 
 // getDepVersionForName get the version of the package that provides the given 
name.
 // If the name matches the package name, then the version of the package is 
used;
-// if it does not, then the version of the provides is used.
+// if it does not, then the version of the provides is used. An unversioned
+// provide carries no version for the name, so it returns "".
 //
 // For example, if pkg foo v2.3 provides bar=1.2, and we look for name=bar 
then it returns
 // 1.2 (from the provides); else it return 2.3 (from the package itself).
@@ -1079,12 +1114,8 @@
        }
        for _, prov := range pkg.Provides {
                constraint := cachedResolvePackageNameVersionPin(prov)
-               pName, pVersion := constraint.Name, constraint.Version
-               if pVersion == "" {
-                       pVersion = pkg.Version
-               }
-               if pName == name {
-                       return pVersion
+               if constraint.Name == name {
+                       return constraint.Version
                }
        }
        return ""
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.16/pkg/apk/apk/repo_test.go 
new/apko-1.2.17/pkg/apk/apk/repo_test.go
--- old/apko-1.2.16/pkg/apk/apk/repo_test.go    2026-06-04 07:01:25.000000000 
+0200
+++ new/apko-1.2.17/pkg/apk/apk/repo_test.go    2026-06-13 03:07:30.000000000 
+0200
@@ -937,6 +937,152 @@
        require.Contains(t, got, "glibc-2.apk")
 }
 
+// An index can retain old builds of a package whose provider priority is
+// higher than the current build's, for example when a priority assignment bug
+// was fixed in between. Provider priority is compared before version, so a
+// direct dependency on the package name would select the stale build. The
+// same-origin heuristic must rescue this: once the origin is pulled in at the
+// current version (here via the meta package), the candidate matching that
+// origin version wins over the stale higher-priority build.
+//
+// This is the py3-pybind11 case from the wolfi index: py3.10-pybind11
+// 2.13.6-r1 lingers with k=312 while the current 3.0.4-r0 build has k=310.
+func TestSameOriginVersionBeatsStaleProviderPriority(t *testing.T) {
+       repo := Repository{}
+       index := repo.WithIndex(&APKIndex{
+               Packages: []*Package{
+                       {Name: "py3.10-pybind11", Version: "2.13.6-r1", Origin: 
"py3-pybind11",
+                               Provides: []string{"py3-pybind11", 
"py3-pybind11-dev"}, ProviderPriority: 312},
+                       {Name: "py3.10-pybind11", Version: "3.0.4-r0", Origin: 
"py3-pybind11",
+                               Provides: []string{"py3-pybind11", 
"py3-pybind11-dev"}, ProviderPriority: 310},
+                       {Name: "py3-supported-pybind11", Version: "3.0.4-r0", 
Origin: "py3-pybind11",
+                               Dependencies: []string{"py3.10-pybind11"}},
+               },
+       })
+       resolver := NewPkgResolver(context.Background(), 
testNamedRepositoryFromIndexes([]*RepositoryWithIndex{index}))
+       pkgs, _, err := 
resolver.GetPackagesWithDependencies(context.Background(), 
[]string{"py3-supported-pybind11"}, nil)
+       require.NoError(t, err)
+
+       got := make([]string, 0, len(pkgs))
+       for _, p := range pkgs {
+               got = append(got, p.Filename())
+       }
+       require.Contains(t, got, "py3.10-pybind11-3.0.4-r0.apk", "should select 
the current build of py3.10-pybind11")
+       require.NotContains(t, got, "py3.10-pybind11-2.13.6-r1.apk", "should 
not select the stale higher-priority build")
+}
+
+// Provider priority arbitrates between different packages providing the same
+// virtual name. It must not arbitrate between builds of the named package
+// itself: there, the higher version wins, regardless of what priority each
+// build declared for its virtual provides.
+//
+// This is the py3-pybind11 case again, but with a direct dependency on the
+// package name, so the same-origin heuristic cannot help.
+func TestDirectDependencyPrefersVersionOverProviderPriority(t *testing.T) {
+       repo := Repository{}
+       index := repo.WithIndex(&APKIndex{
+               Packages: []*Package{
+                       {Name: "py3.10-pybind11", Version: "2.13.6-r1", Origin: 
"py3-pybind11",
+                               Provides: []string{"py3-pybind11", 
"py3-pybind11-dev"}, ProviderPriority: 312},
+                       {Name: "py3.10-pybind11", Version: "3.0.4-r0", Origin: 
"py3-pybind11",
+                               Provides: []string{"py3-pybind11", 
"py3-pybind11-dev"}, ProviderPriority: 310},
+               },
+       })
+       resolver := NewPkgResolver(context.Background(), 
testNamedRepositoryFromIndexes([]*RepositoryWithIndex{index}))
+       pkgs, _, err := 
resolver.GetPackagesWithDependencies(context.Background(), 
[]string{"py3.10-pybind11"}, nil)
+       require.NoError(t, err)
+
+       got := make([]string, 0, len(pkgs))
+       for _, p := range pkgs {
+               got = append(got, p.Filename())
+       }
+       require.Equal(t, []string{"py3.10-pybind11-3.0.4-r0.apk"}, got)
+}
+
+// Between different packages providing the same unversioned virtual name,
+// provider priority decides, as in apk-tools' compare_providers:
+// 
https://github.com/alpinelinux/apk-tools/blob/20fe3dccc423bd401b9828958124664704dedb00/src/solver.c#L641-L667
+func TestProviderPriorityArbitratesVirtualProvides(t *testing.T) {
+       repo := Repository{}
+       index := repo.WithIndex(&APKIndex{
+               Packages: []*Package{
+                       {Name: "py3.10-pybind11", Version: "3.0.4-r0", Origin: 
"py3-pybind11",
+                               Provides: []string{"py3-pybind11-dev"}, 
ProviderPriority: 310},
+                       {Name: "py3.13-pybind11", Version: "3.0.4-r0", Origin: 
"py3-pybind11",
+                               Provides: []string{"py3-pybind11-dev"}, 
ProviderPriority: 313},
+               },
+       })
+       resolver := NewPkgResolver(context.Background(), 
testNamedRepositoryFromIndexes([]*RepositoryWithIndex{index}))
+       pkgs, _, err := 
resolver.GetPackagesWithDependencies(context.Background(), 
[]string{"py3-pybind11-dev"}, nil)
+       require.NoError(t, err)
+
+       got := make([]string, 0, len(pkgs))
+       for _, p := range pkgs {
+               got = append(got, p.Filename())
+       }
+       require.Equal(t, []string{"py3.13-pybind11-3.0.4-r0.apk"}, got)
+}
+
+// Between providers of a versioned virtual name, the version each provides
+// for that name decides before provider priority, as in apk-tools'
+// compare_providers:
+// 
https://github.com/alpinelinux/apk-tools/blob/20fe3dccc423bd401b9828958124664704dedb00/src/solver.c#L641-L667
+func TestVersionedProvideBeatsProviderPriority(t *testing.T) {
+       repo := Repository{}
+       index := repo.WithIndex(&APKIndex{
+               Packages: []*Package{
+                       {Name: "prov-a", Version: "1.0-r0", Origin: "prov-a",
+                               Provides: []string{"virt=2.0"}, 
ProviderPriority: 1},
+                       {Name: "prov-b", Version: "9.0-r0", Origin: "prov-b",
+                               Provides: []string{"virt=1.0"}, 
ProviderPriority: 999},
+               },
+       })
+       resolver := NewPkgResolver(context.Background(), 
testNamedRepositoryFromIndexes([]*RepositoryWithIndex{index}))
+       pkgs, _, err := 
resolver.GetPackagesWithDependencies(context.Background(), []string{"virt"}, 
nil)
+       require.NoError(t, err)
+
+       got := make([]string, 0, len(pkgs))
+       for _, p := range pkgs {
+               got = append(got, p.Filename())
+       }
+       require.Equal(t, []string{"prov-a-1.0-r0.apk"}, got)
+}
+
+// When providers of a virtual name tie on the version they provide for it and
+// on priority, the higher build version wins, regardless of repository order.
+//
+// This is a deliberate divergence from apk-tools, which prefers the lowest
+// available repository here. Image configurations can layer a variant
+// repository ahead of the main one, with packages providing the same sonames
+// as the main repository's packages. An image depending on such a soname must
+// not resolve it to the variant build just because the variant repository is
+// configured first.
+func TestHigherVersionBreaksProviderTieAcrossRepositories(t *testing.T) {
+       repoA := Repository{URI: "https://example.invalid/a"}
+       indexA := repoA.WithIndex(&APKIndex{
+               Packages: []*Package{
+                       {Name: "prov-variant", Version: "1.0-r1", Origin: 
"prov-variant",
+                               Provides: []string{"so:libprov.so.12=12"}},
+               },
+       })
+       repoB := Repository{URI: "https://example.invalid/b"}
+       indexB := repoB.WithIndex(&APKIndex{
+               Packages: []*Package{
+                       {Name: "prov", Version: "1.0-r2", Origin: "prov",
+                               Provides: []string{"so:libprov.so.12=12"}},
+               },
+       })
+       resolver := NewPkgResolver(context.Background(), 
testNamedRepositoryFromIndexes([]*RepositoryWithIndex{indexA, indexB}))
+       pkgs, _, err := 
resolver.GetPackagesWithDependencies(context.Background(), 
[]string{"so:libprov.so.12"}, nil)
+       require.NoError(t, err)
+
+       got := make([]string, 0, len(pkgs))
+       for _, p := range pkgs {
+               got = append(got, p.Filename())
+       }
+       require.Equal(t, []string{"prov-1.0-r2.apk"}, got)
+}
+
 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.16/pkg/apk/auth/auth.go 
new/apko-1.2.17/pkg/apk/auth/auth.go
--- old/apko-1.2.16/pkg/apk/auth/auth.go        2026-06-04 07:01:25.000000000 
+0200
+++ new/apko-1.2.17/pkg/apk/auth/auth.go        2026-06-13 03:07:30.000000000 
+0200
@@ -12,6 +12,7 @@
 
        "github.com/chainguard-dev/clog"
        "golang.org/x/oauth2"
+       "golang.org/x/term"
        "golang.org/x/time/rate"
 )
 
@@ -102,7 +103,14 @@
 
        sometimes.Do(func() {
                cmd := exec.CommandContext(ctx, "chainctl", "auth", "token", 
"--audience", host) //#nosec G702 -- host is from env/default, passed as argv 
(no shell)
-               cmd.Stderr = io.Discard                                         
                 // Don't pollute logs when things fail
+               // If stderr is a terminal, let chainctl write to it so it can 
prompt
+               // interactively (e.g. emit a device-login URL when running 
headless).
+               // Otherwise discard it to avoid polluting logs when things 
fail.
+               if term.IsTerminal(int(os.Stderr.Fd())) {
+                       cmd.Stderr = os.Stderr
+               } else {
+                       cmd.Stderr = io.Discard
+               }
                out, err := cmd.Output()
                if err != nil {
                        // Document that automatic auth failed and how to 
reproduce.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.16/pkg/apk/expandapk/atomic.go 
new/apko-1.2.17/pkg/apk/expandapk/atomic.go
--- old/apko-1.2.16/pkg/apk/expandapk/atomic.go 1970-01-01 01:00:00.000000000 
+0100
+++ new/apko-1.2.17/pkg/apk/expandapk/atomic.go 2026-06-13 03:07:30.000000000 
+0200
@@ -0,0 +1,31 @@
+package expandapk
+
+import (
+       "fmt"
+       "io"
+       "os"
+       "path/filepath"
+)
+
+// writeFileAtomic streams r into path by writing to a temporary file in the
+// same directory and atomically renaming it into place.
+func writeFileAtomic(path string, r io.Reader) error {
+       tmp, err := os.CreateTemp(filepath.Dir(path), 
filepath.Base(path)+".tmp-*")
+       if err != nil {
+               return fmt.Errorf("creating temp file for %q: %w", path, err)
+       }
+       tmpName := tmp.Name()
+       defer os.Remove(tmpName) // no-op once the rename below succeeds
+
+       if _, err := io.Copy(tmp, r); err != nil {
+               tmp.Close()
+               return err
+       }
+       if err := tmp.Close(); err != nil {
+               return fmt.Errorf("closing %q: %w", tmpName, err)
+       }
+       if err := os.Rename(tmpName, path); err != nil {
+               return fmt.Errorf("renaming %q onto %q: %w", tmpName, path, err)
+       }
+       return nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.16/pkg/apk/expandapk/atomic_test.go 
new/apko-1.2.17/pkg/apk/expandapk/atomic_test.go
--- old/apko-1.2.16/pkg/apk/expandapk/atomic_test.go    1970-01-01 
01:00:00.000000000 +0100
+++ new/apko-1.2.17/pkg/apk/expandapk/atomic_test.go    2026-06-13 
03:07:30.000000000 +0200
@@ -0,0 +1,114 @@
+package expandapk
+
+import (
+       "bytes"
+       "errors"
+       "io"
+       "io/fs"
+       "os"
+       "path/filepath"
+       "testing"
+)
+
+// midWriteChecker is an io.Reader that, after the first chunk it returns has
+// been written by the copier, asserts that the destination path does not yet
+// exist. If the write were done in place (os.Create on the destination), the
+// destination would already hold a partial file at this point.
+type midWriteChecker struct {
+       t       *testing.T
+       dst     string
+       data    []byte
+       off     int
+       checked bool
+}
+
+func (r *midWriteChecker) Read(p []byte) (int, error) {
+       if r.off > 0 && !r.checked {
+               r.checked = true
+               if _, err := os.Stat(r.dst); !errors.Is(err, fs.ErrNotExist) {
+                       r.t.Errorf("destination %q is observable mid-write 
(stat err=%v); write is not atomic", r.dst, err)
+               }
+       }
+       if r.off >= len(r.data) {
+               return 0, io.EOF
+       }
+       // Cap each Read at 64 KiB to force several Read calls, so a chunk is
+       // flushed before the next destination check.
+       n := min(copy(p, r.data[r.off:]), 64*1024)
+       r.off += n
+       return n, nil
+}
+
+func TestWriteFileAtomic_DestinationNotVisibleUntilComplete(t *testing.T) {
+       dir := t.TempDir()
+       dst := filepath.Join(dir, "data.tar")
+
+       payload := bytes.Repeat([]byte("x"), 256*1024)
+       r := &midWriteChecker{t: t, dst: dst, data: payload}
+
+       if err := writeFileAtomic(dst, r); err != nil {
+               t.Fatalf("writeFileAtomic: %v", err)
+       }
+       if !r.checked {
+               t.Fatal("mid-write check never ran; the test would not catch a 
regression")
+       }
+
+       got, err := os.ReadFile(dst)
+       if err != nil {
+               t.Fatalf("reading destination: %v", err)
+       }
+       if !bytes.Equal(got, payload) {
+               t.Fatalf("destination content mismatch: got %d bytes, want %d", 
len(got), len(payload))
+       }
+
+       // The temp file must have been renamed, not left behind.
+       if ents, err := os.ReadDir(dir); err != nil {
+               t.Fatal(err)
+       } else if len(ents) != 1 {
+               t.Fatalf("expected only the destination file, found %v (temp 
not cleaned up?)", entryNames(ents))
+       }
+}
+
+// errAfter yields its data once and then returns a fixed error.
+type errAfter struct {
+       data []byte
+       err  error
+       done bool
+}
+
+func (r *errAfter) Read(p []byte) (int, error) {
+       if !r.done {
+               r.done = true
+               return copy(p, r.data), nil
+       }
+       return 0, r.err
+}
+
+func TestWriteFileAtomic_FailedWriteLeavesNoDestination(t *testing.T) {
+       dir := t.TempDir()
+       dst := filepath.Join(dir, "data.tar")
+
+       payload := bytes.Repeat([]byte("y"), 256*1024)
+       outOfReadsError := errors.New("single-use reader has no reads left")
+       r := &errAfter{data: payload, err: outOfReadsError}
+
+       if err := writeFileAtomic(dst, r); !errors.Is(err, outOfReadsError) {
+               t.Fatalf("expected error wrapping %v, got %v", outOfReadsError, 
err)
+       }
+       if _, err := os.Stat(dst); !errors.Is(err, fs.ErrNotExist) {
+               t.Fatalf("destination must not exist after a failed write (stat 
err=%v)", err)
+       }
+       if ents, err := os.ReadDir(dir); err != nil {
+               t.Fatal(err)
+       } else if len(ents) != 0 {
+               t.Fatalf("temp file not cleaned up after failed write: %v", 
entryNames(ents))
+       }
+}
+
+func entryNames(ents []os.DirEntry) []string {
+       names := make([]string, len(ents))
+       for i, e := range ents {
+               names[i] = e.Name()
+       }
+       return names
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.16/pkg/apk/expandapk/expandapk.go 
new/apko-1.2.17/pkg/apk/expandapk/expandapk.go
--- old/apko-1.2.16/pkg/apk/expandapk/expandapk.go      2026-06-04 
07:01:25.000000000 +0200
+++ new/apko-1.2.17/pkg/apk/expandapk/expandapk.go      2026-06-13 
03:07:30.000000000 +0200
@@ -30,16 +30,6 @@
        "go.opentelemetry.io/otel"
 )
 
-var slicePool = sync.Pool{
-       New: func() interface{} {
-               return make([]byte, 1<<20)
-       },
-}
-
-func pooledSlice() []byte {
-       return slicePool.Get().([]byte)
-}
-
 var readerPool = sync.Pool{
        New: func() interface{} {
                return bufio.NewReaderSize(nil, 1<<20)
@@ -186,14 +176,6 @@
                return nil, fmt.Errorf("parsing %q: %w", a.PackageFile, err)
        }
 
-       uf, err = os.Create(a.TarFile)
-       if err != nil {
-               return nil, fmt.Errorf("opening tar file %q: %w", a.TarFile, 
err)
-       }
-
-       buf := pooledSlice()
-       defer slicePool.Put(buf)
-
        // Wrap the gzip reader with a limit to protect against decompression 
bombs
        var maxSize int64
        if a.opts != nil {
@@ -201,14 +183,12 @@
        }
        limitedZr := limitio.NewLimitedReaderWithDefault(zr, maxSize, 
DefaultMaxDataSize)
 
-       if _, err := io.CopyBuffer(uf, limitedZr, buf); err != nil {
+       // Write the decompressed tar file atomically to avoid a concurrent
+       // reader of the cache seeing an empty or truncated file.
+       if err := writeFileAtomic(a.TarFile, limitedZr); err != nil {
                return nil, fmt.Errorf("decompressing %q: %w", a.PackageFile, 
err)
        }
 
-       if err := uf.Close(); err != nil {
-               return nil, fmt.Errorf("closing %q: %w", a.TarFile, err)
-       }
-
        return os.Open(a.TarFile)
 }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.16/pkg/build/paths.go 
new/apko-1.2.17/pkg/build/paths.go
--- old/apko-1.2.16/pkg/build/paths.go  2026-06-04 07:01:25.000000000 +0200
+++ new/apko-1.2.17/pkg/build/paths.go  2026-06-13 03:07:30.000000000 +0200
@@ -40,10 +40,27 @@
        return mutatePermissionsDirect(fsys, mut.Path, mut.Permissions, 
mut.UID, mut.GID)
 }
 
+// unixModeToFsMode converts raw unix permission bits (as written in apko
+// configs, e.g. 0o2775) to fs.FileMode, mapping setuid/setgid/sticky to
+// their Go fs.FileMode equivalents.
+func unixModeToFsMode(perms uint32) fs.FileMode {
+       mode := fs.FileMode(perms & 0o777)
+       if perms&0o4000 != 0 {
+               mode |= fs.ModeSetuid
+       }
+       if perms&0o2000 != 0 {
+               mode |= fs.ModeSetgid
+       }
+       if perms&0o1000 != 0 {
+               mode |= fs.ModeSticky
+       }
+       return mode
+}
+
 func mutatePermissionsDirect(fsys apkfs.FullFS, path string, perms, uid, gid 
uint32) error {
        target := path
 
-       if err := fsys.Chmod(target, fs.FileMode(perms)); err != nil {
+       if err := fsys.Chmod(target, unixModeToFsMode(perms)); err != nil {
                return fmt.Errorf("chmod %q: %w", target, err)
        }
        if err := fsys.Chown(target, int(uid), int(gid)); err != nil {
@@ -53,7 +70,7 @@
 }
 
 func mutateDirectory(fsys apkfs.FullFS, o *options.Options, mut 
types.PathMutation) error {
-       perms := fs.FileMode(mut.Permissions)
+       perms := unixModeToFsMode(mut.Permissions)
 
        if err := fsys.MkdirAll(mut.Path, perms); err != nil {
                return err
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/apko-1.2.16/pkg/build/paths_test.go 
new/apko-1.2.17/pkg/build/paths_test.go
--- old/apko-1.2.16/pkg/build/paths_test.go     1970-01-01 01:00:00.000000000 
+0100
+++ new/apko-1.2.17/pkg/build/paths_test.go     2026-06-13 03:07:30.000000000 
+0200
@@ -0,0 +1,174 @@
+// Copyright 2026 Chainguard, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package build
+
+import (
+       "archive/tar"
+       "bytes"
+       "context"
+       "io/fs"
+       "testing"
+
+       "github.com/stretchr/testify/require"
+
+       apkfs "chainguard.dev/apko/pkg/apk/fs"
+       "chainguard.dev/apko/pkg/build/types"
+)
+
+func TestMutateDirectorySpecialBits(t *testing.T) {
+       for _, test := range []struct {
+               desc         string
+               permissions  uint32
+               expectedMode fs.FileMode
+       }{
+               {
+                       desc:         "plain permissions",
+                       permissions:  0o755,
+                       expectedMode: 0o755,
+               },
+               {
+                       desc:         "setgid",
+                       permissions:  0o2775,
+                       expectedMode: 0o775 | fs.ModeSetgid,
+               },
+               {
+                       desc:         "setuid",
+                       permissions:  0o4755,
+                       expectedMode: 0o755 | fs.ModeSetuid,
+               },
+               {
+                       desc:         "sticky",
+                       permissions:  0o1777,
+                       expectedMode: 0o777 | fs.ModeSticky,
+               },
+               {
+                       desc:         "all special bits",
+                       permissions:  0o7775,
+                       expectedMode: 0o775 | fs.ModeSetuid | fs.ModeSetgid | 
fs.ModeSticky,
+               },
+               {
+                       desc: "bits above 0o7777 are dropped, not 
misinterpreted",
+                       // 1<<22 is fs.ModeSetgid; passing it raw must not 
smuggle in
+                       // setgid (or any other fs.FileMode flag).
+                       permissions:  1<<22 | 0o755,
+                       expectedMode: 0o755,
+               },
+               {
+                       desc: "decimal-vs-octal typo only yields its actual 
mode bits",
+                       // "permissions: 775" (decimal, missing 0o) = 0o1407: 
sticky + 407.
+                       permissions:  775,
+                       expectedMode: 0o407 | fs.ModeSticky,
+               },
+       } {
+               t.Run(test.desc, func(t *testing.T) {
+                       fsys := apkfs.NewMemFS()
+                       mut := types.PathMutation{
+                               Path:        "/var/log/kolla",
+                               Type:        "directory",
+                               Permissions: test.permissions,
+                       }
+                       require.NoError(t, mutateDirectory(fsys, nil, mut))
+                       // mutatePaths follows up non-permissions mutations with
+                       // mutatePermissions; exercise that path too.
+                       require.NoError(t, mutatePermissions(fsys, nil, mut))
+
+                       fi, err := fsys.Stat("/var/log/kolla")
+                       require.NoError(t, err)
+                       require.Equal(t, test.expectedMode, 
fi.Mode().Perm()|fi.Mode()&(fs.ModeSetuid|fs.ModeSetgid|fs.ModeSticky))
+               })
+       }
+}
+
+func TestMutateDirectoryRecursiveSpecialBits(t *testing.T) {
+       fsys := apkfs.NewMemFS()
+       require.NoError(t, fsys.MkdirAll("/var/log/kolla/glance", 0o755))
+
+       mut := types.PathMutation{
+               Path:        "/var/log/kolla",
+               Type:        "directory",
+               Permissions: 0o2775,
+               Recursive:   true,
+       }
+       require.NoError(t, mutateDirectory(fsys, nil, mut))
+
+       for _, path := range []string{"/var/log/kolla", 
"/var/log/kolla/glance"} {
+               fi, err := fsys.Stat(path)
+               require.NoError(t, err)
+               require.Equal(t, fs.ModeSetgid, fi.Mode()&fs.ModeSetgid, 
"setgid missing on %s", path)
+               require.Equal(t, fs.FileMode(0o775), fi.Mode().Perm(), "perms 
wrong on %s", path)
+       }
+}
+
+func TestMutatePermissionsMissingPath(t *testing.T) {
+       fsys := apkfs.NewMemFS()
+       require.Error(t, mutatePermissionsDirect(fsys, "/does/not/exist", 
0o2775, 0, 0))
+}
+
+// TestPathMutationSpecialBitsReachTar covers the full chain: paths mutation →
+// FS → tar header. This is where the dropped bits were observable in images.
+func TestPathMutationSpecialBitsReachTar(t *testing.T) {
+       fsys := apkfs.NewMemFS()
+       ic := &types.ImageConfiguration{
+               Paths: []types.PathMutation{{
+                       Path:        "var",
+                       Type:        "directory",
+                       Permissions: 0o755,
+               }, {
+                       Path:        "var/log",
+                       Type:        "directory",
+                       Permissions: 0o755,
+               }, {
+                       Path:        "var/log/kolla",
+                       Type:        "directory",
+                       Permissions: 0o2775,
+                       UID:         0,
+                       GID:         42400,
+               }},
+       }
+       require.NoError(t, mutatePaths(fsys, nil, ic))
+
+       var buf bytes.Buffer
+       tw := tar.NewWriter(&buf)
+       require.NoError(t, writeTar(context.Background(), tw, fsys))
+       require.NoError(t, tw.Close())
+
+       tr := tar.NewReader(&buf)
+       for {
+               hdr, err := tr.Next()
+               require.NoError(t, err, "tar must contain var/log/kolla")
+               if hdr.Name != "var/log/kolla" {
+                       continue
+               }
+               require.Equal(t, int64(0o2775), hdr.Mode&0o7777, "tar header 
mode")
+               require.Equal(t, fs.ModeSetgid, 
hdr.FileInfo().Mode()&fs.ModeSetgid)
+               require.Equal(t, 42400, hdr.Gid)
+               return
+       }
+}
+
+func TestMutatePermissionsSpecialBitsOnFile(t *testing.T) {
+       fsys := apkfs.NewMemFS()
+       require.NoError(t, fsys.MkdirAll("/usr/bin", 0o755))
+       f, err := fsys.Create("/usr/bin/suidtool")
+       require.NoError(t, err)
+       require.NoError(t, f.Close())
+
+       require.NoError(t, mutatePermissionsDirect(fsys, "/usr/bin/suidtool", 
0o4755, 0, 0))
+
+       fi, err := fsys.Stat("/usr/bin/suidtool")
+       require.NoError(t, err)
+       require.Equal(t, fs.ModeSetuid, fi.Mode()&fs.ModeSetuid)
+       require.Equal(t, fs.FileMode(0o755), fi.Mode().Perm())
+}

++++++ apko.obsinfo ++++++
--- /var/tmp/diff_new_pack.cAZF1v/_old  2026-06-15 19:49:00.097400043 +0200
+++ /var/tmp/diff_new_pack.cAZF1v/_new  2026-06-15 19:49:00.117400884 +0200
@@ -1,5 +1,5 @@
 name: apko
-version: 1.2.16
-mtime: 1780549285
-commit: 8bf905593d457345beafe51490dd28a619d0690a
+version: 1.2.17
+mtime: 1781312850
+commit: 301fd0d625c79684d29b2de779b7fd882e8fd822
 

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

Reply via email to