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
