Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package melange for openSUSE:Factory checked 
in at 2026-08-06 16:22:11
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/melange (Old)
 and      /work/SRC/openSUSE:Factory/.melange.new.16738 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "melange"

Thu Aug  6 16:22:11 2026 rev:174 rq:1369644 version:0.57.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/melange/melange.changes  2026-08-03 
17:57:20.062922616 +0200
+++ /work/SRC/openSUSE:Factory/.melange.new.16738/melange.changes       
2026-08-06 16:24:05.816409445 +0200
@@ -1,0 +2,9 @@
+Wed Aug 05 05:22:17 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.57.0:
+  * build(deps): bump the gomod group with 3 updates (#2609)
+  * feat(build): cache `uses:` pipeline definitions; allow script
+    normalization to be skipped (#2608)
+  * build(deps): bump the gomod group with 3 updates (#2606)
+
+-------------------------------------------------------------------

Old:
----
  melange-0.56.5.obscpio

New:
----
  melange-0.57.0.obscpio

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

Other differences:
------------------
++++++ melange.spec ++++++
--- /var/tmp/diff_new_pack.UQBAKL/_old  2026-08-06 16:24:07.472467281 +0200
+++ /var/tmp/diff_new_pack.UQBAKL/_new  2026-08-06 16:24:07.472467281 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           melange
-Version:        0.56.5
+Version:        0.57.0
 Release:        0
 Summary:        Build APKs from source code
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.UQBAKL/_old  2026-08-06 16:24:07.512468678 +0200
+++ /var/tmp/diff_new_pack.UQBAKL/_new  2026-08-06 16:24:07.516468818 +0200
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/chainguard-dev/melange.git</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">refs/tags/v0.56.5</param>
+    <param name="revision">refs/tags/v0.57.0</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.UQBAKL/_old  2026-08-06 16:24:07.540469655 +0200
+++ /var/tmp/diff_new_pack.UQBAKL/_new  2026-08-06 16:24:07.552470075 +0200
@@ -3,6 +3,6 @@
                 <param 
name="url">https://github.com/chainguard-dev/melange</param>
               <param 
name="changesrevision">3f6115b820985d70ca3c93cdf8519c1b3b4cfe81</param></service><service
 name="tar_scm">
                 <param 
name="url">https://github.com/chainguard-dev/melange.git</param>
-              <param 
name="changesrevision">5550831e913a4604b85b1cfd4d2786103f6d7311</param></service></servicedata>
+              <param 
name="changesrevision">17d552b98407e1ae29ff2c6c0db1ecdccdaf16ca</param></service></servicedata>
 (No newline at EOF)
 

++++++ melange-0.56.5.obscpio -> melange-0.57.0.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/melange-0.56.5/go.mod new/melange-0.57.0/go.mod
--- old/melange-0.56.5/go.mod   2026-07-28 15:38:49.000000000 +0200
+++ new/melange-0.57.0/go.mod   2026-08-04 14:36:12.000000000 +0200
@@ -3,18 +3,18 @@
 go 1.26.2
 
 require (
-       chainguard.dev/apko v1.2.29
+       chainguard.dev/apko v1.2.31
        github.com/chainguard-dev/clog v1.8.1
        github.com/chainguard-dev/go-pkgconfig 
v0.0.0-20240404163941-6351b37b2a10
        github.com/chainguard-dev/yam v0.2.65
        github.com/charmbracelet/log v1.0.0
-       github.com/docker/cli v29.6.2+incompatible
+       github.com/docker/cli v29.7.1+incompatible
        github.com/docker/docker v28.5.2+incompatible
        github.com/dprotaso/go-yit v0.0.0-20250513224043-18a80f8f6df4
        github.com/github/go-spdx/v2 v2.7.0
-       github.com/go-git/go-git/v5 v5.19.1
+       github.com/go-git/go-git/v5 v5.19.2
        github.com/google/go-cmp v0.7.0
-       github.com/google/go-containerregistry v0.21.7
+       github.com/google/go-containerregistry v0.21.8
        github.com/google/licenseclassifier/v2 v2.0.0
        github.com/in-toto/attestation v1.2.0
        github.com/invopop/jsonschema v0.14.0
@@ -62,7 +62,7 @@
        github.com/ijt/goparsify v0.0.0-20221203142333-3a5276334b8d // indirect
        github.com/klauspost/cpuid/v2 v2.4.0 // indirect
        github.com/moby/moby/api v1.55.0 // indirect
-       github.com/moby/moby/client v0.5.0 // indirect
+       github.com/moby/moby/client v0.5.1 // indirect
        github.com/moby/sys/atomicwriter v0.1.0 // indirect
        github.com/morikuni/aec v1.1.0 // indirect
        github.com/pavlo-v-chernykh/keystore-go/v4 v4.5.0 // indirect
@@ -82,7 +82,7 @@
 
 require (
        chainguard.dev/go-grpc-kit v0.17.17 // indirect
-       chainguard.dev/sdk v0.1.140 // indirect
+       chainguard.dev/sdk v0.1.158 // indirect
        cloud.google.com/go/auth v0.21.0 // indirect
        cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect
        cloud.google.com/go/compute/metadata v0.9.0 // indirect
@@ -164,15 +164,15 @@
        go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.69.0 
// indirect
        go.opentelemetry.io/otel/metric v1.44.0 // indirect
        go.opentelemetry.io/otel/trace v1.44.0 // indirect
-       go.step.sm/crypto v0.85.0 // indirect
+       go.step.sm/crypto v0.86.0 // indirect
        golang.org/x/mod v0.38.0 // indirect
        golang.org/x/net v0.57.0 // indirect
        golang.org/x/oauth2 v0.36.0 // indirect
-       google.golang.org/api v0.289.0 // indirect
+       google.golang.org/api v0.290.0 // indirect
        google.golang.org/genproto/googleapis/api 
v0.0.0-20260706201446-f0a921348800 // indirect
        google.golang.org/genproto/googleapis/rpc 
v0.0.0-20260706201446-f0a921348800 // indirect
        google.golang.org/grpc v1.82.1 // indirect
        google.golang.org/protobuf v1.36.12-0.20260120151049-f2248ac996af
        gopkg.in/warnings.v0 v0.1.2 // indirect
-       k8s.io/apimachinery v0.36.2 // indirect
+       k8s.io/apimachinery v0.36.3 // indirect
 )
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/melange-0.56.5/go.sum new/melange-0.57.0/go.sum
--- old/melange-0.56.5/go.sum   2026-07-28 15:38:49.000000000 +0200
+++ new/melange-0.57.0/go.sum   2026-08-04 14:36:12.000000000 +0200
@@ -1,9 +1,9 @@
-chainguard.dev/apko v1.2.29 h1:qB8mP6qhKpKOCvTGPjs0eX/9AiNc+ty+DU6qbG2wtDs=
-chainguard.dev/apko v1.2.29/go.mod 
h1:6FLHpIBilf8M7mruL11sTZ8gvMiFKHTOLhU2ss1eJEs=
+chainguard.dev/apko v1.2.31 h1:CqtX2A+EOySioCr5y+QvWIVxa++MhLnctfU6DnXEJx8=
+chainguard.dev/apko v1.2.31/go.mod 
h1:f54ZMrLLF2crumDOmkIA4MVPpgcLvB5oZopx6MhlizI=
 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.140 h1:9rV+7BG70uP+yqFC5IvU/f/fAleJ6kRdALXYMu29xZ0=
-chainguard.dev/sdk v0.1.140/go.mod 
h1:hwsZl3o558o1R4d8hXrIYHZ6E9lCAXJgeVXQMnV5bRM=
+chainguard.dev/sdk v0.1.158 h1:aOWATQ4zxYKQ8D+CCUj+hOU18wNQkbHw6tmj+/+GDAk=
+chainguard.dev/sdk v0.1.158/go.mod 
h1:WIaM60bR6qU84RvE45u4QkOh+BN2uc1nfHuZYLnRfvU=
 cloud.google.com/go v0.26.0/go.mod 
h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
 cloud.google.com/go/auth v0.21.0 
h1:g/QwYfYb2Ai6HH8oomAOyBaIHLbscZ4+T/F/f5JZHkE=
 cloud.google.com/go/auth v0.21.0/go.mod 
h1:M9o2Oz+YI2jAfxewJgb1vyI3vceHF+eohmxyzmrl+9s=
@@ -87,8 +87,8 @@
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/distribution/reference v0.6.0 
h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
 github.com/distribution/reference v0.6.0/go.mod 
h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
-github.com/docker/cli v29.6.2+incompatible 
h1:/bjePvcbbFTnRrMfWJBY7AjfICdsiLVgHn6LwTVOcqw=
-github.com/docker/cli v29.6.2+incompatible/go.mod 
h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
+github.com/docker/cli v29.7.1+incompatible 
h1:ILZpP6B7fedIr6ANy824QkDp1WMJuouIq0O2SrBkB2w=
+github.com/docker/cli v29.7.1+incompatible/go.mod 
h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
 github.com/docker/docker v28.5.2+incompatible 
h1:DBX0Y0zAjZbSrm1uzOkdr1onVghKaftjlSWt4AFexzM=
 github.com/docker/docker v28.5.2+incompatible/go.mod 
h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
 github.com/docker/docker-credential-helpers v0.9.8 
h1:bIREROb7So6PRlq6KTtdS9MPEjC29OQRkFNlvK2OX8Q=
@@ -125,8 +125,8 @@
 github.com/go-git/go-billy/v5 v5.9.0/go.mod 
h1:jCnQMLj9eUgGU7+ludSTYoZL/GGmii14RxKFj7ROgHw=
 github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20231010084843-55a94097c399 
h1:eMje31YglSBqCdIqdhKBW8lokaMrL3uTkpGYlE2OOT4=
 github.com/go-git/go-git-fixtures/v4 
v4.3.2-0.20231010084843-55a94097c399/go.mod 
h1:1OCfN199q1Jm3HZlxleg+Dw/mwps2Wbk9frAWm+4FII=
-github.com/go-git/go-git/v5 v5.19.1 
h1:nX27AnaU43/K5bKktKwgBmR9lawoYVe1Ckg0rgzzN00=
-github.com/go-git/go-git/v5 v5.19.1/go.mod 
h1:Pb1v0c7/g8aGQJwx9Us09W85yGoyvSwuhEGMH7zjDKQ=
+github.com/go-git/go-git/v5 v5.19.2 
h1:wkfn7vOlUBu8ivAWKBWisTiwJK4jYHzTF8Ndv1LyGqY=
+github.com/go-git/go-git/v5 v5.19.2/go.mod 
h1:QqCBE1EFN5ddFmrliLQ3/ntRCUjZU3EJuwuB/jWEHjk=
 github.com/go-jose/go-jose/v3 v3.0.5 
h1:BLLJWbC4nMZOfuPVxoZIxeYsn6Nl2r1fITaJ78UQlVQ=
 github.com/go-jose/go-jose/v3 v3.0.5/go.mod 
h1:5b+7YgP7ZICgJDBdfjZaIt+H/9L9T/YQrVfLAMboGkQ=
 github.com/go-logfmt/logfmt v0.6.1 
h1:4hvbpePJKnIzH1B+8OR/JPbTx37NktoI9LE2QZBBkvE=
@@ -165,8 +165,8 @@
 github.com/google/go-cmp v0.5.9/go.mod 
h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
 github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
 github.com/google/go-cmp v0.7.0/go.mod 
h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
-github.com/google/go-containerregistry v0.21.7 
h1:/vPFuVXDjtFREsVArW+0h1CIl5urnOhzei4X2DMW9IU=
-github.com/google/go-containerregistry v0.21.7/go.mod 
h1:kjSbt7/zMsKLWfnHrIvKvhXHUw91jbe9DNjPPJ32gXE=
+github.com/google/go-containerregistry v0.21.8 
h1:Ig/zIsnztdCUNaiNNczE+MoP5xcyUMfvpvfOr1xyMLE=
+github.com/google/go-containerregistry v0.21.8/go.mod 
h1:dP5XNKcL7kMFF/TB3LfvWmVhAcv7iqkHb3oDK8aauTo=
 github.com/google/go-licenses/v2 v2.0.1 
h1:ti+9bi5o7DKbeeg5eBb/uZTgsaPNoJaLCh93cRcXsW8=
 github.com/google/go-licenses/v2 v2.0.1/go.mod 
h1:efibo0EDNGkau6AIMOViGW+rTNPudhxX9rCxtfw5zKE=
 github.com/google/go-replayers/httpreplay v1.2.0 
h1:VM1wEyyjaoU53BwrOnaf9VhAyQQEEioJvFYxYcLRKzk=
@@ -243,8 +243,8 @@
 github.com/moby/docker-image-spec v1.3.1/go.mod 
h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo=
 github.com/moby/moby/api v1.55.0 
h1:2/sexvQyqIWS8pRSCFddBfpW2qE7vR7FCL+vN8pxwMc=
 github.com/moby/moby/api v1.55.0/go.mod 
h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs=
-github.com/moby/moby/client v0.5.0 
h1:5XhyPk2fuOWf6RlSFa3MkIIgDZkF25xToXW8Q/BH7cc=
-github.com/moby/moby/client v0.5.0/go.mod 
h1:rcVpF8ncl9vo5gaIBdol6CnbEtSj1uxMvEV/UrykF/s=
+github.com/moby/moby/client v0.5.1 
h1:tYNaJno4c0HXz12y5BiqEDy0rVTYkWzI26lGvnTMiJw=
+github.com/moby/moby/client v0.5.1/go.mod 
h1:odLstlZ6uSnfvAgVxMpvgmb8SUdd+siH2T0GBuxVAlM=
 github.com/moby/sys/atomicwriter v0.1.0 
h1:kw5D/EqkBwsBFi0ss9v1VG3wIkVhzGvLklJ+w3A14Sw=
 github.com/moby/sys/atomicwriter v0.1.0/go.mod 
h1:Ul8oqv2ZMNHOceF643P6FKPXeCmYtlQMvpizfsSoaWs=
 github.com/moby/sys/sequential v0.6.0 
h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU=
@@ -374,8 +374,8 @@
 go.opentelemetry.io/otel/trace v1.44.0/go.mod 
h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
 go.opentelemetry.io/proto/otlp v1.10.0 
h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
 go.opentelemetry.io/proto/otlp v1.10.0/go.mod 
h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
-go.step.sm/crypto v0.85.0 h1:h3U9gzCcxP1fWbseqy+CwsTxzHFNuoblefy7YP4jBHk=
-go.step.sm/crypto v0.85.0/go.mod 
h1:Rp/BdP0/ZJ7eYREhXcE4izCABLp0e8ziFqb0LBNG7Cc=
+go.step.sm/crypto v0.86.0 h1:ORItETVOEAo0+rfCm0VKSAolLtoxMSL+pOKfvUeR4MM=
+go.step.sm/crypto v0.86.0/go.mod 
h1:G+SbDNGA8ZYZBOXYJnxZLJnVCWp3RIKMF+ECS8G0Qug=
 go.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=
@@ -475,8 +475,8 @@
 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.289.0 h1:DmH0c6NigNFmsvsohM9bxv+MzVhag3aGHnojA5fFQjc=
-google.golang.org/api v0.289.0/go.mod 
h1:weJZ3lldHFYI0DBFNKpJelUDNnusTt5YaOEgxvt8ci8=
+google.golang.org/api v0.290.0 h1:eMw0Xo+IfbbMlKmW7aHvpyQRv9RCXuWx/vs8AD+0x9A=
+google.golang.org/api v0.290.0/go.mod 
h1:weJZ3lldHFYI0DBFNKpJelUDNnusTt5YaOEgxvt8ci8=
 google.golang.org/appengine v1.1.0/go.mod 
h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
 google.golang.org/appengine v1.4.0/go.mod 
h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
 google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod 
h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
@@ -526,8 +526,8 @@
 gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod 
h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod 
h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
-k8s.io/apimachinery v0.36.2 h1:0PE/W/WNy1UX61NLbXY5TMbJ6UwLL6E6lAPkYrKFxbQ=
-k8s.io/apimachinery v0.36.2/go.mod 
h1:fvf/HOLXq9RId0rnDIbN1OEBvHXdQbLMM8nu0LcBUf4=
+k8s.io/apimachinery v0.36.3 h1:PkzMRBRG8joFD8EhCuQAtNPvJlxb82FwplP26HIzvAM=
+k8s.io/apimachinery v0.36.3/go.mod 
h1:cTSjBWgPe/6CQyBKzY/hDIRWCQQQeK0mfLbml0UYFHE=
 k8s.io/klog/v2 v2.140.0 h1:Tf+J3AH7xnUzZyVVXhTgGhEKnFqye14aadWv7bzXdzc=
 k8s.io/klog/v2 v2.140.0/go.mod h1:o+/RWfJ6PwpnFn7OyAG3QnO47BFsymfEfrz6XyYSSp0=
 mvdan.cc/sh/v3 v3.13.1 h1:DP3TfgZhDkT7lerUdnp6PTGKyxxzz6T+cOlY/xEvfWk=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/melange-0.56.5/pkg/build/compile.go 
new/melange-0.57.0/pkg/build/compile.go
--- old/melange-0.56.5/pkg/build/compile.go     2026-07-28 15:38:49.000000000 
+0200
+++ new/melange-0.57.0/pkg/build/compile.go     2026-08-04 14:36:12.000000000 
+0200
@@ -25,7 +25,6 @@
        "strings"
 
        "github.com/chainguard-dev/clog"
-       "gopkg.in/yaml.v3"
        "mvdan.cc/sh/v3/syntax"
 
        "chainguard.dev/melange/pkg/cond"
@@ -35,6 +34,27 @@
 
 const unidentifiablePipeline = "???"
 
+// CompileOption adjusts what a Compile produces.
+type CompileOption func(*Compiled)
+
+// WithDependenciesOnly narrows compilation to what a caller resolving build
+// dependencies needs, which is the `uses:` tree and the packages it pulls in.
+//
+// The compiled pipelines are not runnable: each step's `runs:` keeps the
+// comments and formatting it was written with, because normalizing it is the
+// single most expensive part of compiling and a caller that only reads
+// .environment.contents.packages discards it. Everything else — substitution,
+// validation, conditionals and the resolved dependency list — is unchanged, so
+// a definition that fails to compile still fails the same way.
+//
+// Do not use this to produce a configuration that will be built or recorded:
+// use a plain Compile for that.
+func WithDependenciesOnly() CompileOption {
+       return func(c *Compiled) {
+               c.dependenciesOnly = true
+       }
+}
+
 func (t *Test) Compile(ctx context.Context) error {
        cfg := t.Configuration
 
@@ -122,16 +142,14 @@
 }
 
 // Compile compiles all configuration, including tests, by loading any 
pipelines and substituting all variables.
-func (b *Build) Compile(ctx context.Context) error {
+func (b *Build) Compile(ctx context.Context, opts ...CompileOption) error {
        cfg := b.Configuration
        sm, err := NewSubstitutionMap(cfg, b.Arch, b.buildFlavor(), 
b.EnabledBuildOptions)
        if err != nil {
                return err
        }
 
-       c := &Compiled{
-               PipelineDirs: b.PipelineDirs,
-       }
+       c := newCompiled(b.PipelineDirs, opts)
 
        if err := c.CompilePipelines(ctx, sm, cfg.Pipeline); err != nil {
                return fmt.Errorf("compiling %q pipelines: %w", 
cfg.Package.Name, err)
@@ -155,9 +173,7 @@
                        continue
                }
 
-               tc := &Compiled{
-                       PipelineDirs: b.PipelineDirs,
-               }
+               tc := newCompiled(b.PipelineDirs, opts)
                if err := tc.CompilePipelines(ctx, sm, sp.Test.Pipeline); err 
!= nil {
                        return fmt.Errorf("compiling subpackage %q tests: %w", 
sp.Name, err)
                }
@@ -178,9 +194,7 @@
        ic.Packages = append(ic.Packages, c.Needs...)
 
        if cfg.Test != nil {
-               tc := &Compiled{
-                       PipelineDirs: b.PipelineDirs,
-               }
+               tc := newCompiled(b.PipelineDirs, opts)
 
                if err := tc.CompilePipelines(ctx, sm, cfg.Test.Pipeline); err 
!= nil {
                        return fmt.Errorf("compiling %q test pipelines: %w", 
cfg.Package.Name, err)
@@ -202,6 +216,19 @@
 type Compiled struct {
        PipelineDirs []string
        Needs        []string
+
+       // dependenciesOnly skips producing runnable `runs:` bodies. See
+       // WithDependenciesOnly.
+       dependenciesOnly bool
+}
+
+func newCompiled(pipelineDirs []string, opts []CompileOption) *Compiled {
+       c := &Compiled{PipelineDirs: pipelineDirs}
+       for _, opt := range opts {
+               opt(c)
+       }
+
+       return c
 }
 
 func (c *Compiled) CompilePipelines(ctx context.Context, sm *SubstitutionMap, 
pipelines []config.Pipeline) error {
@@ -219,7 +246,6 @@
 }
 
 func (c *Compiled) compilePipeline(ctx context.Context, sm *SubstitutionMap, 
pipeline *config.Pipeline, parent map[string]string) error {
-       log := clog.FromContext(ctx)
        name, uses, with := pipeline.Name, pipeline.Uses, 
maps.Clone(pipeline.With)
 
        // When compiling an already-compiled config, `uses` will be redundant 
and FYI only,
@@ -230,34 +256,18 @@
                        return fmt.Errorf("invalid pipeline 'uses' value %q: 
must not contain absolute paths or '..' sequences", uses)
                }
 
-               var data []byte
-               // Set this to fail up front in case there are no pipeline dirs 
specified
-               // and we can't find them.
-               err := fmt.Errorf("could not find 'uses' pipeline %q", uses)
-
-               for _, pd := range c.PipelineDirs {
-                       log.Debugf("trying to load pipeline %q from %q", uses, 
pd)
-                       target := filepath.Join(pd, uses+".yaml")
-                       // Verify the resolved path is still within the 
pipeline directory.
-                       if rel, err := filepath.Rel(pd, 
filepath.Clean(target)); err != nil || strings.HasPrefix(rel, "..") {
-                               return fmt.Errorf("pipeline 'uses' value %q 
resolves outside pipeline directory %q", uses, pd)
-                       }
-                       data, err = os.ReadFile(target) // #nosec G304 - 
Loading pipeline definition from configured directory
-                       if err == nil {
-                               log.Debugf("Found pipeline %s", string(data))
-                               break
-                       }
-               }
+               // Reading and parsing the definition only happens the first 
time it
+               // is used; see usesDocument.
+               doc, err := usesDocument(c.PipelineDirs, uses, func() ([]byte, 
error) {
+                       return c.readUses(ctx, uses)
+               })
                if err != nil {
-                       log.Debugf("trying to load pipeline %q from embedded fs 
pipelines/%q.yaml", uses, uses)
-                       data, err = PipelinesFS.ReadFile("pipelines/" + uses + 
".yaml")
-                       if err != nil {
-                               return fmt.Errorf("unable to load pipeline: 
%w", err)
-                       }
+                       return err
                }
-
-               if err := yaml.Unmarshal(data, pipeline); err != nil {
-                       return fmt.Errorf("unable to parse pipeline %q: %w", 
uses, err)
+               if doc != nil {
+                       if err := doc.Decode(pipeline); err != nil {
+                               return fmt.Errorf("unable to parse pipeline %q: 
%w", uses, err)
+                       }
                }
 
                for k := range with {
@@ -308,10 +318,14 @@
                return fmt.Errorf("mutating runs: %w", err)
        }
 
-       // Drop any comments to avoid leaking things into .melange.json.
-       pipeline.Runs, err = stripComments(pipeline.Runs)
-       if err != nil {
-               return fmt.Errorf("stripping runs comments: %w", err)
+       // Drop any comments to avoid leaking things into .melange.json. Parsing
+       // the script to do it costs more than the rest of compiling the step, 
so
+       // a caller that is only resolving dependencies skips it.
+       if !c.dependenciesOnly {
+               pipeline.Runs, err = stripComments(pipeline.Runs)
+               if err != nil {
+                       return fmt.Errorf("stripping runs comments: %w", err)
+               }
        }
 
        if pipeline.If != "" {
@@ -356,6 +370,40 @@
        return nil
 }
 
+// readUses reads the definition named by uses from the configured pipeline
+// directories, falling back to the ones built into melange.
+func (c *Compiled) readUses(ctx context.Context, uses string) ([]byte, error) {
+       log := clog.FromContext(ctx)
+
+       var data []byte
+       // Set this to fail up front in case there are no pipeline dirs 
specified
+       // and we can't find them.
+       err := fmt.Errorf("could not find 'uses' pipeline %q", uses)
+
+       for _, pd := range c.PipelineDirs {
+               log.Debugf("trying to load pipeline %q from %q", uses, pd)
+               target := filepath.Join(pd, uses+".yaml")
+               // Verify the resolved path is still within the pipeline 
directory.
+               if rel, err := filepath.Rel(pd, filepath.Clean(target)); err != 
nil || strings.HasPrefix(rel, "..") {
+                       return nil, fmt.Errorf("pipeline 'uses' value %q 
resolves outside pipeline directory %q", uses, pd)
+               }
+               data, err = os.ReadFile(target) // #nosec G304 - Loading 
pipeline definition from configured directory
+               if err == nil {
+                       log.Debugf("Found pipeline %s", string(data))
+                       break
+               }
+       }
+       if err != nil {
+               log.Debugf("trying to load pipeline %q from embedded fs 
pipelines/%q.yaml", uses, uses)
+               data, err = PipelinesFS.ReadFile("pipelines/" + uses + ".yaml")
+               if err != nil {
+                       return nil, fmt.Errorf("unable to load pipeline: %w", 
err)
+               }
+       }
+
+       return data, nil
+}
+
 func identity(p *config.Pipeline) string {
        if p.Name != "" {
                return p.Name
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/melange-0.56.5/pkg/build/usescache.go 
new/melange-0.57.0/pkg/build/usescache.go
--- old/melange-0.56.5/pkg/build/usescache.go   1970-01-01 01:00:00.000000000 
+0100
+++ new/melange-0.57.0/pkg/build/usescache.go   2026-08-04 14:36:12.000000000 
+0200
@@ -0,0 +1,96 @@
+/*
+Copyright 2026 Chainguard, Inc.
+SPDX-License-Identifier: Apache-2.0
+
+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 (
+       "fmt"
+       "strings"
+       "sync"
+
+       "gopkg.in/yaml.v3"
+)
+
+// usesCache memoizes the parsed form of the pipeline definitions loaded by
+// `uses:`.
+//
+// A definition is resolved from the same pipeline directories every time, and
+// compiling a tree of package definitions asks for the same few of them over
+// and over: a repository whose packages mostly `uses: go/build` parses that
+// one file once per package that names it. Parsing the YAML is the expensive
+// half of loading it, so the parsed document is what is kept.
+//
+// What is cached is the document, not a compiled pipeline. Compilation
+// substitutes each package's own variables into the definition, so the result
+// differs per package and only the parse is shared.
+//
+// The cache lives for the life of the process and assumes the pipeline
+// directories do not change underneath it, which holds for a melange build and
+// for the tools that compile a checked-out tree. Nothing invalidates it.
+//
+// sync.Map rather than a guarded map: once the definitions in use have been
+// seen, every access is a read, and callers compile packages in parallel. A
+// mutex — even an RWMutex, whose read path still writes a shared counter —
+// would put every one of those reads through the same cache line.
+var usesCache sync.Map // map[string]*yaml.Node, nil for an empty definition
+
+// usesKey identifies a pipeline definition by the directories it is looked up
+// in and the name it is looked up by, which is everything that determines
+// which file wins.
+func usesKey(pipelineDirs []string, uses string) string {
+       return strings.Join(pipelineDirs, "\x00") + "\x00\x00" + uses
+}
+
+// usesDocument returns the parsed document for the pipeline definition named
+// by uses, calling load to read it only the first time it is asked for.
+//
+// A nil document means the definition was empty, which yaml.Unmarshal treats
+// as a no-op and so does the caller.
+//
+// Callers decode the returned document onto their own pipeline rather than
+// receiving a copy. Decoding a document onto an existing value applies the 
keys
+// the document sets and leaves the rest alone — the same as yaml.Unmarshal, 
and
+// what the caller depends on, since a step's own `with`, `if` and `workdir` 
have
+// to survive loading the definition it names.
+func usesDocument(pipelineDirs []string, uses string, load func() ([]byte, 
error)) (*yaml.Node, error) {
+       key := usesKey(pipelineDirs, uses)
+
+       if cached, ok := usesCache.Load(key); ok {
+               return cached.(*yaml.Node), nil
+       }
+
+       data, err := load()
+       if err != nil {
+               return nil, err
+       }
+
+       var doc yaml.Node
+       if err := yaml.Unmarshal(data, &doc); err != nil {
+               return nil, fmt.Errorf("unable to parse pipeline %q: %w", uses, 
err)
+       }
+
+       // An empty definition parses to a document with no content, which 
Decode
+       // rejects. Record it so it is not read again, and let the caller skip 
it.
+       if doc.Kind == 0 || (doc.Kind == yaml.DocumentNode && len(doc.Content) 
== 0) {
+               usesCache.Store(key, (*yaml.Node)(nil))
+               return nil, nil
+       }
+
+       usesCache.Store(key, &doc)
+
+       return &doc, nil
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/melange-0.56.5/pkg/build/usescache_test.go 
new/melange-0.57.0/pkg/build/usescache_test.go
--- old/melange-0.56.5/pkg/build/usescache_test.go      1970-01-01 
01:00:00.000000000 +0100
+++ new/melange-0.57.0/pkg/build/usescache_test.go      2026-08-04 
14:36:12.000000000 +0200
@@ -0,0 +1,249 @@
+/*
+Copyright 2026 Chainguard, Inc.
+SPDX-License-Identifier: Apache-2.0
+
+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 (
+       "errors"
+       "sync"
+       "sync/atomic"
+       "testing"
+
+       "gopkg.in/yaml.v3"
+
+       "chainguard.dev/melange/pkg/config"
+)
+
+// definition is a pipeline definition of the shape `uses:` loads: it names
+// some keys and leaves others out.
+const definition = `name: definition name
+inputs:
+  version:
+    description: the version
+    default: "1.0"
+needs:
+  packages:
+    - busybox
+pipeline:
+  - runs: echo hi
+`
+
+// loadUses does what compilePipeline does with a document: fetch it, then
+// decode it onto the pipeline.
+func loadUses(dirs []string, uses string, load func() ([]byte, error), 
pipeline *config.Pipeline) error {
+       doc, err := usesDocument(dirs, uses, load)
+       if err != nil {
+               return err
+       }
+       if doc == nil {
+               return nil
+       }
+
+       return doc.Decode(pipeline)
+}
+
+func serve(data string, calls *atomic.Int64) func() ([]byte, error) {
+       return func() ([]byte, error) {
+               if calls != nil {
+                       calls.Add(1)
+               }
+
+               return []byte(data), nil
+       }
+}
+
+// TestUsesDocumentMatchesUnmarshal pins that loading a definition through the
+// cache leaves the target in the state yaml.Unmarshal would, on both the parse
+// and the cached path, and that the definition is only read once. Keys the
+// definition sets are applied; the ones it omits keep the values the step
+// brought with it.
+func TestUsesDocumentMatchesUnmarshal(t *testing.T) {
+       step := func() *config.Pipeline {
+               return &config.Pipeline{
+                       Uses:    "some/pipeline",
+                       With:    map[string]string{"version": "2.0"},
+                       If:      "true",
+                       WorkDir: "/home/build",
+               }
+       }
+
+       want := step()
+       if err := yaml.Unmarshal([]byte(definition), want); err != nil {
+               t.Fatalf("yaml.Unmarshal: %v", err)
+       }
+
+       dirs := []string{t.TempDir()}
+       var calls atomic.Int64
+
+       for _, when := range []string{"parsed", "cached"} {
+               got := step()
+               if err := loadUses(dirs, "some/pipeline", serve(definition, 
&calls), got); err != nil {
+                       t.Fatalf("%s: %v", when, err)
+               }
+
+               if got.Name != want.Name {
+                       t.Errorf("%s: name: got = %q, want = %q", when, 
got.Name, want.Name)
+               }
+               if got.WorkDir != want.WorkDir {
+                       t.Errorf("%s: workdir: got = %q, want = %q", when, 
got.WorkDir, want.WorkDir)
+               }
+               if got.If != want.If {
+                       t.Errorf("%s: if: got = %q, want = %q", when, got.If, 
want.If)
+               }
+               if len(got.With) != len(want.With) || got.With["version"] != 
want.With["version"] {
+                       t.Errorf("%s: with: got = %v, want = %v", when, 
got.With, want.With)
+               }
+               if len(got.Inputs) != len(want.Inputs) {
+                       t.Errorf("%s: inputs: got = %d, want = %d", when, 
len(got.Inputs), len(want.Inputs))
+               }
+               if got.Needs == nil || len(got.Needs.Packages) != 1 || 
got.Needs.Packages[0] != "busybox" {
+                       t.Errorf("%s: needs: got = %v, want = [busybox]", when, 
got.Needs)
+               }
+               if len(got.Pipeline) != 1 || got.Pipeline[0].Runs != "echo hi" {
+                       t.Errorf("%s: pipeline: got = %v", when, got.Pipeline)
+               }
+       }
+
+       // The point of the cache: the definition is read from disk once, not 
once
+       // per package that names it.
+       if got := calls.Load(); got != 1 {
+               t.Errorf("loads: got = %d, want = 1", got)
+       }
+}
+
+// TestUsesDocumentKeyedByPipelineDirs checks that two pipeline directory sets
+// resolving the same name to different definitions do not share an entry.
+func TestUsesDocumentKeyedByPipelineDirs(t *testing.T) {
+       first := []string{t.TempDir()}
+       second := []string{t.TempDir()}
+
+       one := &config.Pipeline{}
+       if err := loadUses(first, "shared", serve("name: one\n", nil), one); 
err != nil {
+               t.Fatal(err)
+       }
+
+       two := &config.Pipeline{}
+       if err := loadUses(second, "shared", serve("name: two\n", nil), two); 
err != nil {
+               t.Fatal(err)
+       }
+
+       if one.Name != "one" {
+               t.Errorf("first dirs: got = %q, want = %q", one.Name, "one")
+       }
+       if two.Name != "two" {
+               t.Errorf("second dirs: got = %q, want = %q", two.Name, "two")
+       }
+}
+
+// TestUsesDocumentEmptyDefinition checks that an empty definition is the no-op
+// yaml.Unmarshal makes of it rather than an error, and that it is not read a
+// second time.
+func TestUsesDocumentEmptyDefinition(t *testing.T) {
+       for _, data := range []string{"", "\n", "# just a comment\n"} {
+               want := &config.Pipeline{Uses: "empty"}
+               if err := yaml.Unmarshal([]byte(data), want); err != nil {
+                       t.Fatalf("yaml.Unmarshal(%q): %v", data, err)
+               }
+
+               dirs := []string{t.TempDir()}
+               var calls atomic.Int64
+
+               for range 2 {
+                       got := &config.Pipeline{Uses: "empty"}
+                       if err := loadUses(dirs, "empty", serve(data, &calls), 
got); err != nil {
+                               t.Errorf("%q: %v", data, err)
+                               continue
+                       }
+                       if got.Uses != want.Uses {
+                               t.Errorf("%q: uses: got = %q, want = %q", data, 
got.Uses, want.Uses)
+                       }
+               }
+
+               if got := calls.Load(); got != 1 {
+                       t.Errorf("%q: loads: got = %d, want = 1", data, got)
+               }
+       }
+}
+
+// TestUsesDocumentLoadError checks that a definition that cannot be read
+// reports the loader's own error and is not cached.
+func TestUsesDocumentLoadError(t *testing.T) {
+       dirs := []string{t.TempDir()}
+       want := errors.New("could not find 'uses' pipeline")
+
+       for range 2 {
+               _, err := usesDocument(dirs, "missing", func() ([]byte, error) 
{ return nil, want })
+               if !errors.Is(err, want) {
+                       t.Fatalf("got = %v, want = %v", err, want)
+               }
+       }
+
+       // A failure must not be remembered as an empty definition.
+       got := &config.Pipeline{}
+       if err := loadUses(dirs, "missing", serve("name: recovered\n", nil), 
got); err != nil {
+               t.Fatal(err)
+       }
+       if got.Name != "recovered" {
+               t.Errorf("name: got = %q, want = %q", got.Name, "recovered")
+       }
+}
+
+// TestUsesDocumentConcurrent decodes one cached definition from many 
goroutines
+// at once. Cached documents are shared across the goroutines that compile a
+// tree of packages in parallel, so decoding must not write to the document it
+// decodes from. Run under -race for this to mean anything.
+func TestUsesDocumentConcurrent(t *testing.T) {
+       dirs := []string{t.TempDir()}
+
+       // Seed the entry so every goroutine below takes the cached path.
+       if err := loadUses(dirs, "concurrent", serve(definition, nil), 
&config.Pipeline{}); err != nil {
+               t.Fatal(err)
+       }
+
+       const goroutines = 32
+       results := make([]*config.Pipeline, goroutines)
+       errs := make([]error, goroutines)
+
+       var wg sync.WaitGroup
+       start := make(chan struct{})
+       for i := range goroutines {
+               wg.Go(func() {
+                       <-start
+                       p := &config.Pipeline{Uses: "concurrent", WorkDir: 
"/home/build"}
+                       errs[i] = loadUses(dirs, "concurrent", 
serve(definition, nil), p)
+                       results[i] = p
+               })
+       }
+       close(start)
+       wg.Wait()
+
+       for i := range goroutines {
+               if errs[i] != nil {
+                       t.Fatalf("goroutine %d: %v", i, errs[i])
+               }
+               got := results[i]
+               if got.Name != "definition name" || got.WorkDir != 
"/home/build" {
+                       t.Errorf("goroutine %d: got name = %q workdir = %q", i, 
got.Name, got.WorkDir)
+               }
+               if got.Needs == nil || len(got.Needs.Packages) != 1 {
+                       t.Errorf("goroutine %d: needs: got = %v", i, got.Needs)
+               }
+               if len(got.Pipeline) != 1 || got.Pipeline[0].Runs != "echo hi" {
+                       t.Errorf("goroutine %d: pipeline: got = %v", i, 
got.Pipeline)
+               }
+       }
+}

++++++ melange.obsinfo ++++++
--- /var/tmp/diff_new_pack.UQBAKL/_old  2026-08-06 16:24:09.452536433 +0200
+++ /var/tmp/diff_new_pack.UQBAKL/_new  2026-08-06 16:24:09.460536712 +0200
@@ -1,5 +1,5 @@
 name: melange
-version: 0.56.5
-mtime: 1785245929
-commit: 5550831e913a4604b85b1cfd4d2786103f6d7311
+version: 0.57.0
+mtime: 1785846972
+commit: 17d552b98407e1ae29ff2c6c0db1ecdccdaf16ca
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/melange/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.melange.new.16738/vendor.tar.gz differ: char 32, 
line 2

Reply via email to