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
