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-04-18 21:35:20 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/melange (Old) and /work/SRC/openSUSE:Factory/.melange.new.11940 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "melange" Sat Apr 18 21:35:20 2026 rev:153 rq:1347906 version:0.50.1 Changes: -------- --- /work/SRC/openSUSE:Factory/melange/melange.changes 2026-04-13 23:19:58.122803369 +0200 +++ /work/SRC/openSUSE:Factory/.melange.new.11940/melange.changes 2026-04-18 21:35:38.527771522 +0200 @@ -1,0 +2,25 @@ +Sat Apr 18 07:22:42 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.50.1: + * fix(qemu): fix CPU/Memory resource precedence (#2489) + +------------------------------------------------------------------- +Fri Apr 17 20:00:38 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.50.0: + * build(deps): bump github.com/github/go-spdx/v2 from 2.4.0 to + 2.5.0 in the gomod group (#2485) + * build(deps): bump step-security/harden-runner from 2.17.0 to + 2.18.0 in the actions group (#2486) + * fix(observability): probe only when observability is installed + (#2482) + * feat(qemu): add DNS search domains (#2481) + * build(deps): bump zizmorcore/zizmor-action from 0.5.2 to 0.5.3 + in the actions group (#2483) + * feat(pipelines): add reason fields to fetch and git-checkout + (#2480) + * fix(qemu): improve VM shutdown with graceful timeouts and PID + safety (#2479) + * build(deps): bump the gomod group with 3 updates (#2477) + +------------------------------------------------------------------- Old: ---- melange-0.49.0.obscpio New: ---- melange-0.50.1.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ melange.spec ++++++ --- /var/tmp/diff_new_pack.6czRRO/_old 2026-04-18 21:35:41.007872599 +0200 +++ /var/tmp/diff_new_pack.6czRRO/_new 2026-04-18 21:35:41.011872762 +0200 @@ -17,7 +17,7 @@ Name: melange -Version: 0.49.0 +Version: 0.50.1 Release: 0 Summary: Build APKs from source code License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.6czRRO/_old 2026-04-18 21:35:41.051874392 +0200 +++ /var/tmp/diff_new_pack.6czRRO/_new 2026-04-18 21:35:41.055874555 +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.49.0</param> + <param name="revision">refs/tags/v0.50.1</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.6czRRO/_old 2026-04-18 21:35:41.079875534 +0200 +++ /var/tmp/diff_new_pack.6czRRO/_new 2026-04-18 21:35:41.083875696 +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">4121ddfd1c96ecefe8644566858072682828c1ac</param></service></servicedata> + <param name="changesrevision">738880da281d8df5c37a56c70a4f39386ca90ef4</param></service></servicedata> (No newline at EOF) ++++++ melange-0.49.0.obscpio -> melange-0.50.1.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/e2e-tests/git-checkout-build.yaml new/melange-0.50.1/e2e-tests/git-checkout-build.yaml --- old/melange-0.49.0/e2e-tests/git-checkout-build.yaml 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/e2e-tests/git-checkout-build.yaml 2026-04-17 22:03:54.000000000 +0200 @@ -126,6 +126,7 @@ with: repository: ${{vars.giturl}} branch: 1.x + reason: testing mutable git-checkout reference - name: "check branch without expected" working-directory: branch-no-expected diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/go.mod new/melange-0.50.1/go.mod --- old/melange-0.49.0/go.mod 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/go.mod 2026-04-17 22:03:54.000000000 +0200 @@ -3,18 +3,18 @@ go 1.25.7 require ( - chainguard.dev/apko v1.2.2 + chainguard.dev/apko v1.2.3 github.com/chainguard-dev/clog v1.8.0 github.com/chainguard-dev/go-pkgconfig v0.0.0-20240404163941-6351b37b2a10 - github.com/chainguard-dev/yam v0.2.54 + github.com/chainguard-dev/yam v0.2.55 github.com/charmbracelet/log v1.0.0 github.com/docker/cli v29.4.0+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.4.0 + github.com/github/go-spdx/v2 v2.5.0 github.com/go-git/go-git/v5 v5.17.2 github.com/google/go-cmp v0.7.0 - github.com/google/go-containerregistry v0.21.4 + github.com/google/go-containerregistry v0.21.5 github.com/google/licenseclassifier/v2 v2.0.0 github.com/in-toto/attestation v1.2.0 github.com/invopop/jsonschema v0.13.0 @@ -60,15 +60,15 @@ github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 // indirect github.com/ijt/goparsify v0.0.0-20221203142333-3a5276334b8d // indirect github.com/klauspost/cpuid/v2 v2.3.0 // indirect - github.com/moby/moby/api v1.54.0 // indirect - github.com/moby/moby/client v0.3.0 // indirect + github.com/moby/moby/api v1.54.1 // indirect + github.com/moby/moby/client v0.4.0 // 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 go.opencensus.io v0.24.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.43.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/tools v0.43.0 // indirect + golang.org/x/tools v0.44.0 // indirect k8s.io/klog/v2 v2.130.1 // indirect ) @@ -80,7 +80,7 @@ require ( chainguard.dev/go-grpc-kit v0.17.16 // indirect chainguard.dev/sdk v0.1.52 // indirect - cloud.google.com/go/auth v0.18.2 // indirect + cloud.google.com/go/auth v0.20.0 // indirect cloud.google.com/go/auth/oauth2adapt v0.2.8 // indirect cloud.google.com/go/compute/metadata v0.9.0 // indirect filippo.io/edwards25519 v1.2.0 // indirect @@ -122,7 +122,7 @@ github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect github.com/google/uuid v1.6.0 // indirect github.com/googleapis/enterprise-certificate-proxy v0.3.14 // indirect - github.com/googleapis/gax-go/v2 v2.19.0 // indirect + github.com/googleapis/gax-go/v2 v2.21.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.28.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-retryablehttp v0.7.8 // indirect @@ -163,14 +163,14 @@ go.lsp.dev/uri v0.3.0 // indirect 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.65.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/trace v1.43.0 // indirect go.step.sm/crypto v0.77.2 // indirect - golang.org/x/mod v0.34.0 // indirect - golang.org/x/net v0.52.0 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.53.0 // indirect golang.org/x/oauth2 v0.36.0 // indirect - google.golang.org/api v0.273.1 // indirect + google.golang.org/api v0.275.0 // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 // indirect google.golang.org/grpc v1.80.0 // indirect diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/go.sum new/melange-0.50.1/go.sum --- old/melange-0.49.0/go.sum 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/go.sum 2026-04-17 22:03:54.000000000 +0200 @@ -1,12 +1,12 @@ -chainguard.dev/apko v1.2.2 h1:6WGvPASk3VXK1D+m4HkYX0PixQkP6NDGwP201KovTLE= -chainguard.dev/apko v1.2.2/go.mod h1:Q2bBBulVerKw0Jq9id7oeEuj0cLGh2q2vb8xu0mrFSA= +chainguard.dev/apko v1.2.3 h1:VW6Xf5Xy3pB5FkE0EYMU9JvaeGE+nY/ix2F+pLbI2t8= +chainguard.dev/apko v1.2.3/go.mod h1:yZ4odDgm5O3fp4CSw6SoToCbbfjMvznnIkJi+IJfVPw= chainguard.dev/go-grpc-kit v0.17.16 h1:Y9RKwZCnrYR3S0K8BiazyOoBrZF+Q7bJWDacfKXz2zg= chainguard.dev/go-grpc-kit v0.17.16/go.mod h1:0vrfIBJguXNa+EKOUEhx1Fj2aBp8o6A8gAHoidiF8ps= chainguard.dev/sdk v0.1.52 h1:G1wmZHU8v5E78YlCHuwQH0Hwt4NBBCvCNAFad5FUanQ= chainguard.dev/sdk v0.1.52/go.mod h1:IZIiUyuNaxTao6mpC/BROsw/dwjl/DmCR/raIT7eK4c= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go/auth v0.18.2 h1:+Nbt5Ev0xEqxlNjd6c+yYUeosQ5TtEUaNcN/3FozlaM= -cloud.google.com/go/auth v0.18.2/go.mod h1:xD+oY7gcahcu7G2SG2DsBerfFxgPAJz17zz2joOFF3M= +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= cloud.google.com/go/auth/oauth2adapt v0.2.8/go.mod h1:XQ9y31RkqZCcwJWNSx2Xvric3RrU88hAYYbjDWYDL+c= cloud.google.com/go/compute/metadata v0.9.0 h1:pDUj4QMoPejqq20dK0Pg2N4yG9zIkYGdBtwLoEkH9Zs= @@ -44,8 +44,8 @@ github.com/chainguard-dev/clog v1.8.0/go.mod h1:5MQOZi+Iu7fV7GcJG8ag8rCB5elEOpqRMKEASgnGVdo= github.com/chainguard-dev/go-pkgconfig v0.0.0-20240404163941-6351b37b2a10 h1:XR2vgQC024I9/boh9r1ihVv8Z14+pbvWqXeYMCnZJpc= github.com/chainguard-dev/go-pkgconfig v0.0.0-20240404163941-6351b37b2a10/go.mod h1:1p6+MesLcjKeON5BRWa7I87mvAY0QmKjgginIM3w6BI= -github.com/chainguard-dev/yam v0.2.54 h1:QIzSAllLHUCx0s3Ot1PpdcY7c6jhiWAEP6SAqOIN6xs= -github.com/chainguard-dev/yam v0.2.54/go.mod h1:Sbt8pVO8DbHoVly44oF5gg03NRxl9AhEImOkqGyoQCs= +github.com/chainguard-dev/yam v0.2.55 h1:Uc0yovNO9fVEFMgFGSL6UUc/v7UUpsssrZl90OeX1eE= +github.com/chainguard-dev/yam v0.2.55/go.mod h1:Sbt8pVO8DbHoVly44oF5gg03NRxl9AhEImOkqGyoQCs= github.com/charmbracelet/colorprofile v0.3.2 h1:9J27WdztfJQVAQKX2WOlSSRB+5gaKqqITmrvb1uTIiI= github.com/charmbracelet/colorprofile v0.3.2/go.mod h1:mTD5XzNeWHj8oqHb+S1bssQb7vIHbepiebQ2kPKVKbI= github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY= @@ -115,8 +115,8 @@ github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k= github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= -github.com/github/go-spdx/v2 v2.4.0 h1:+4IwVwJJbm3rzvrQ6P1nI9BDMcy3la4RchRy5uehV/M= -github.com/github/go-spdx/v2 v2.4.0/go.mod h1:/5rwgS0txhGtRdUZwc02bTglzg6HK3FfuEbECKlK2Sg= +github.com/github/go-spdx/v2 v2.5.0 h1:ULLZ0ZEBOspdLD4PAmADzLYhh9GXLFEalKx3wpp3iFY= +github.com/github/go-spdx/v2 v2.5.0/go.mod h1:Ftc45YYG1WzpzwEPKRVm9Jv8vDqOrN4gWoCkK+bHer0= github.com/gliderlabs/ssh v0.3.8 h1:a4YXD1V7xMF9g5nTkdfnja3Sxy1PVDCj1Zg4Wb8vY6c= github.com/gliderlabs/ssh v0.3.8/go.mod h1:xYoytBv1sV0aL3CavoDuJIQNURXkkfPA/wxQ1pL1fAU= github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= @@ -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.4 h1:VrhlIQtdhE6riZW//MjPrcJ1snAjPoCCpPHqGOygrv8= -github.com/google/go-containerregistry v0.21.4/go.mod h1:kxgc23zQ2qMY/hAKt0wCbB/7tkeovAP2mE2ienynJUw= +github.com/google/go-containerregistry v0.21.5 h1:KTJG9Pn/jC0VdZR6ctV3/jcN+q6/Iqlx0sTVz3ywZlM= +github.com/google/go-containerregistry v0.21.5/go.mod h1:ySvMuiWg+dOsRW0Hw8GYwfMwBlNRTmpYBFJPlkco5zU= 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= @@ -184,8 +184,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/enterprise-certificate-proxy v0.3.14 h1:yh8ncqsbUY4shRD5dA6RlzjJaT4hi3kII+zYw8wmLb8= github.com/googleapis/enterprise-certificate-proxy v0.3.14/go.mod h1:vqVt9yG9480NtzREnTlmGSBmFrA+bzb0yl0TxoBQXOg= -github.com/googleapis/gax-go/v2 v2.19.0 h1:fYQaUOiGwll0cGj7jmHT/0nPlcrZDFPrZRhTsoCr8hE= -github.com/googleapis/gax-go/v2 v2.19.0/go.mod h1:w2ROXVdfGEVFXzmlciUU4EdjHgWvB5h2n6x/8XSTTJA= +github.com/googleapis/gax-go/v2 v2.21.0 h1:h45NjjzEO3faG9Lg/cFrBh2PgegVVgzqKzuZl/wMbiI= +github.com/googleapis/gax-go/v2 v2.21.0/go.mod h1:But/NJU6TnZsrLai/xBAQLLz+Hc7fHZJt/hsCz3Fih4= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0 h1:QGLs/O40yoNK9vmy4rhUGBVyMf1lISBGtXRpsu/Qu/o= github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.1.0/go.mod h1:hM2alZsMUni80N33RBe6J0e423LB+odMj7d3EMP9l20= github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3 h1:B+8ClL/kCQkRiU82d9xajRPKYMrB7E0MbtzWVi1K4ns= @@ -245,10 +245,10 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/moby/api v1.54.0 h1:7kbUgyiKcoBhm0UrWbdrMs7RX8dnwzURKVbZGy2GnL0= -github.com/moby/moby/api v1.54.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= -github.com/moby/moby/client v0.3.0 h1:UUGL5okry+Aomj3WhGt9Aigl3ZOxZGqR7XPo+RLPlKs= -github.com/moby/moby/client v0.3.0/go.mod h1:HJgFbJRvogDQjbM8fqc1MCEm4mIAGMLjXbgwoZp6jCQ= +github.com/moby/moby/api v1.54.1 h1:TqVzuJkOLsgLDDwNLmYqACUuTehOHRGKiPhvH8V3Nn4= +github.com/moby/moby/api v1.54.1/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.0 h1:S+2XegzHQrrvTCvF6s5HFzcrywWQmuVnhOXe2kiWjIw= +github.com/moby/moby/client v0.4.0/go.mod h1:QWPbvWchQbxBNdaLSpoKpCdf5E+WxFAgNHogCWDoa7g= 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= @@ -360,8 +360,8 @@ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.67.0 h1:yI1/OhfEPy7J9eoa6Sj051C7n5dvpj0QX8g4sRchg04= 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.65.0 h1:7iP2uCb7sGddAr30RRS6xjKy7AZ2JtTOPA3oolgVSw8= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.65.0/go.mod h1:c7hN3ddxs/z6q9xwvfLPk+UHlWRQyaeR1LdgfL/66l0= +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/exporters/otlp/otlptrace v1.43.0 h1:88Y4s2C8oTui1LGM6bTWkw0ICGcOLCAI5l6zsD1j20k= @@ -403,8 +403,8 @@ golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= -golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -417,8 +417,8 @@ 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.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= @@ -474,19 +474,21 @@ golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= -golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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.273.1 h1:L7G/TmpAMz0nKx/ciAVssVmWQiOF6+pOuXeKrWVsquY= -google.golang.org/api v0.273.1/go.mod h1:JbAt7mF+XVmWu6xNP8/+CTiGH30ofmCmk9nM8d8fHew= +google.golang.org/api v0.275.0 h1:vfY5d9vFVJeWEZT65QDd9hbndr7FyZ2+6mIzGAh71NI= +google.golang.org/api v0.275.0/go.mod h1:Fnag/EWUPIcJXuIkP1pjoTgS5vdxlk3eeemL7Do6bvw= 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= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7 h1:XzmzkmB14QhVhgnawEVsOn6OFsnpyxNPRY9QV01dNB0= +google.golang.org/genproto v0.0.0-20260319201613-d00831a3d3e7/go.mod h1:L43LFes82YgSonw6iTXTxXUX1OlULt4AQtkik4ULL/I= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9 h1:VPWxll4HlMw1Vs/qXtN7BvhZqsS9cdAittCNvVENElA= google.golang.org/genproto/googleapis/api v0.0.0-20260401024825-9d38bb4040a9/go.mod h1:7QBABkRtR8z+TEnmXTqIqwJLlzrZKVfAUm7tY3yGv0M= google.golang.org/genproto/googleapis/rpc v0.0.0-20260401024825-9d38bb4040a9 h1:m8qni9SQFH0tJc1X0vmnpw/0t+AImlSvp30sEupozUg= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/build/build.go new/melange-0.50.1/pkg/build/build.go --- old/melange-0.49.0/pkg/build/build.go 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/build/build.go 2026-04-17 22:03:54.000000000 +0200 @@ -774,7 +774,7 @@ // is installed. Only applicable to QEMU builds which run in a full VM. if b.Runner.Name() == container.QemuName { if obsEvents, err := container.RetrieveObservabilityEvents(ctx, cfg); err != nil { - log.Warnf("failed to retrieve observability events: %v", err) + log.Errorf("failed to retrieve observability events: %v", err) } else if obsEvents != nil { container.LogObservabilityEvents(ctx, obsEvents) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/build/build_test.go new/melange-0.50.1/pkg/build/build_test.go --- old/melange-0.49.0/pkg/build/build_test.go 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/build/build_test.go 2026-04-17 22:03:54.000000000 +0200 @@ -50,7 +50,7 @@ Package: config.Package{ Name: "hello", Version: "world", - Resources: &config.Resources{CPU: "2", Memory: "4Gi"}, + Resources: &config.Resources{}, }, Pipeline: []config.Pipeline{ { @@ -127,7 +127,7 @@ Package: config.Package{ Name: "cosign", Version: "2.0.0", - Resources: &config.Resources{CPU: "2", Memory: "4Gi"}, + Resources: &config.Resources{}, }, Update: config.Update{ Enabled: true, @@ -144,7 +144,7 @@ name: "release-monitor", requireErr: require.NoError, expected: &config.Configuration{ - Package: config.Package{Name: "bison", Version: "3.8.2", Resources: &config.Resources{CPU: "2", Memory: "4Gi"}}, + Package: config.Package{Name: "bison", Version: "3.8.2", Resources: &config.Resources{}}, Update: config.Update{ Enabled: true, Shared: false, @@ -199,7 +199,7 @@ Name: "cosign", Version: "2.0.0", Epoch: 0, - Resources: &config.Resources{CPU: "2", Memory: "4Gi"}, + Resources: &config.Resources{}, }, Environment: apko_types.ImageConfiguration{ Environment: map[string]string{ @@ -282,7 +282,7 @@ Package: config.Package{ Name: "nginx", Version: "100", - Resources: &config.Resources{CPU: "2", Memory: "4Gi"}, + Resources: &config.Resources{}, }, Subpackages: []config.Subpackage{}, } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/build/pipelines/README.md new/melange-0.50.1/pkg/build/pipelines/README.md --- old/melange-0.49.0/pkg/build/pipelines/README.md 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/build/pipelines/README.md 2026-04-17 22:03:54.000000000 +0200 @@ -30,6 +30,7 @@ | extract | false | Whether to extract the downloaded artifact as a source tarball. | true | | purl-name | false | package-URL (PURL) name for use in SPDX SBOM External References | ${{package.name}} | | purl-version | false | package-URL (PURL) version for use in SPDX SBOM External References | ${{package.version}} | +| reason | false | Provide reason why fetch is used, instead of git-checkout | | | retry-limit | false | The number of times to retry fetching before failing. | 5 | | strip-components | false | The number of path components to strip while extracting. | 1 | | timeout | false | The timeout (in seconds) to use for connecting and reading. The fetch will fail if the timeout is hit. | 5 | @@ -61,6 +62,7 @@ | initial-backoff | false | Initial backoff duration in seconds before first retry. | 2 | | max-backoff | false | Maximum backoff duration in seconds between retries. | 60 | | max-retries | false | Maximum number of retry attempts for git clone operation on failure. | 3 | +| reason | false | Provide reason for a mutable reference. | | | recurse-submodules | false | Indicates whether --recurse-submodules should be passed to git clone. | false | | repository | true | The repository to check out sources from. | | | shallow-submodules | false | Whether to use --shallow-submodules when recurse-submodules is true. Ignored if recurse-submodules is false. | false | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/build/pipelines/fetch.yaml new/melange-0.50.1/pkg/build/pipelines/fetch.yaml --- old/melange-0.49.0/pkg/build/pipelines/fetch.yaml 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/build/pipelines/fetch.yaml 2026-04-17 22:03:54.000000000 +0200 @@ -69,6 +69,10 @@ Whether to delete the fetched artifact after unpacking. default: false + reason: + description: | + Provide reason why fetch is used, instead of git-checkout + pipeline: - runs: | if [ "${{inputs.expected-sha256}}" == "" ] && [ "${{inputs.expected-sha512}}" == "" ] && [ "${{inputs.expected-none}}" == "" ]; then diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/build/pipelines/git-checkout.yaml new/melange-0.50.1/pkg/build/pipelines/git-checkout.yaml --- old/melange-0.49.0/pkg/build/pipelines/git-checkout.yaml 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/build/pipelines/git-checkout.yaml 2026-04-17 22:03:54.000000000 +0200 @@ -89,6 +89,9 @@ description: | Maximum backoff duration in seconds between retries. default: "60" + reason: + description: | + Provide reason for a mutable reference. pipeline: - runs: | diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/build/test_test.go new/melange-0.50.1/pkg/build/test_test.go --- old/melange-0.49.0/pkg/build/test_test.go 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/build/test_test.go 2026-04-17 22:03:54.000000000 +0200 @@ -179,7 +179,7 @@ Package: config.Package{ Name: "hello", Version: "world", - Resources: &config.Resources{CPU: "2", Memory: "4Gi"}, + Resources: &config.Resources{}, }, Test: &config.Test{ Environment: defaultEnv(), @@ -291,7 +291,7 @@ Package: config.Package{ Name: "py3-pandas", Version: "2.1.3", - Resources: &config.Resources{CPU: "2", Memory: "4Gi"}, + Resources: &config.Resources{}, }, Test: &config.Test{ Environment: defaultEnv(func(env *apko_types.ImageConfiguration) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/config/config.go new/melange-0.50.1/pkg/config/config.go --- old/melange-0.49.0/pkg/config/config.go 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/config/config.go 2026-04-17 22:03:54.000000000 +0200 @@ -1861,16 +1861,11 @@ cfg.Package.Resources.Disk = options.disk } - // Apply reasonable defaults for CPU and memory if still unset after YAML and CLI - // flag processing. Without these, the QEMU runner defaults to all host CPUs - // and 85% of host memory, causing resource contention when multiple builds - // share a node. - if cfg.Package.Resources.CPU == "" { - cfg.Package.Resources.CPU = "2" - } - if cfg.Package.Resources.Memory == "" { - cfg.Package.Resources.Memory = "4Gi" - } + // Host-exhaustion safeguards for CPU and memory live in the QEMU runner + // (see effectiveCPU / effectiveMemoryKB in pkg/container/qemu_runner.go), + // not here. Defaulting Package.Resources at parse time would clobber + // empty-value signals that downstream consumers rely on to decide when + // to apply their own, often larger, build-specific defaults. // Finally, validate the configuration we ended up with before returning it for use downstream. if err = cfg.validate(ctx); err != nil { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/config/config_test.go new/melange-0.50.1/pkg/config/config_test.go --- old/melange-0.49.0/pkg/config/config_test.go 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/config/config_test.go 2026-04-17 22:03:54.000000000 +0200 @@ -1504,8 +1504,8 @@ memory: 8Gi `, expectResources: &Resources{ - CPU: "2", - Memory: "4Gi", + CPU: "", + Memory: "", }, expectTestResources: &Resources{ CPU: "4", @@ -1540,8 +1540,8 @@ epoch: 0 `, expectResources: &Resources{ - CPU: "2", - Memory: "4Gi", + CPU: "", + Memory: "", }, expectTestResources: nil, expectParseError: false, @@ -1585,8 +1585,8 @@ memory: 8Gi `, expectResources: &Resources{ - CPU: "2", - Memory: "4Gi", + CPU: "", + Memory: "", }, expectTestResources: &Resources{ CPU: "4", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/container/config.go new/melange-0.50.1/pkg/container/config.go --- old/melange-0.49.0/pkg/container/config.go 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/container/config.go 2026-04-17 22:03:54.000000000 +0200 @@ -16,6 +16,7 @@ import ( "crypto/ed25519" + "os" "time" apko_types "chainguard.dev/apko/pkg/build/types" @@ -73,11 +74,16 @@ SSHBuildClient *ssh.Client // SSH client for the build environment, may not have privileges SSHControlBuildClient *ssh.Client // SSH client for control operations in the build environment, has privileges SSHControlClient *ssh.Client // SSH client for unrestricted control environment, has privileges - QemuPID int + QemuProcess *os.Process // QEMU process handle (not just PID, to avoid PID reuse issues) RunAsGID string // Virtiofs-related fields for cache directory VirtiofsEnabled bool // Whether virtiofs is enabled for cache VirtiofsdPID int // PID of virtiofsd daemon for cleanup VirtiofsdSocketPath string // Path to Unix socket for virtiofsd + + // ObservabilityHook is true when the observability hook's sentinel file + // was found in the initramfs CPIO during VM setup. When false, + // RetrieveObservabilityEvents returns immediately without probing the VM. + ObservabilityHook bool } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/container/observability.go new/melange-0.50.1/pkg/container/observability.go --- old/melange-0.49.0/pkg/container/observability.go 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/container/observability.go 2026-04-17 22:03:54.000000000 +0200 @@ -31,6 +31,12 @@ "/tmp/observability/events.log", } +// observabilityHookSentinel is a file installed exclusively by the +// observability hook package. Its presence in the initramfs CPIO confirms +// the hook is installed, regardless of how it was included. +// CPIO record names are stored without a leading slash. +const observabilityHookSentinel = "etc/tetragon/tetragon.tp.d/network-monitor.yaml" + // ObservabilityEvents holds parsed event data retrieved from the build VM. type ObservabilityEvents struct { // RawData is the raw NDJSON event data. @@ -60,18 +66,19 @@ // via the SSHControlClient (port 2223, unchrooted root access). This should // be called after the build completes but before TerminatePod. // -// Returns nil with no error if the observability hook is not installed or no -// events were generated. This makes the feature fully optional — default -// builds without the hook are completely unaffected. +// If cfg.ObservabilityHook is false the function returns immediately without +// probing the VM. If true, a missing events file is treated as an error. func RetrieveObservabilityEvents(ctx context.Context, cfg *Config) (*ObservabilityEvents, error) { + if !cfg.ObservabilityHook { + return nil, nil + } if cfg.SSHControlClient == nil { return nil, nil } log := clog.FromContext(ctx) - // Probe known event file locations. If none exist, the observability - // hook is not installed and we silently return nil. + // Probe known event file locations. eventsPath := "" for _, path := range observabilityEventPaths { err := sendSSHCommand(ctx, cfg.SSHControlClient, cfg, nil, nil, nil, false, @@ -83,9 +90,7 @@ } if eventsPath == "" { - // No events file found — observability hook is not installed. - // This is the normal case for default builds; return silently. - return nil, nil + return nil, fmt.Errorf("observability hook is installed but no events file found at any known path: %v", observabilityEventPaths) } log.Infof("qemu: found observability events at %s", eventsPath) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/container/qemu_runner.go new/melange-0.50.1/pkg/container/qemu_runner.go --- old/melange-0.49.0/pkg/container/qemu_runner.go 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/container/qemu_runner.go 2026-04-17 22:03:54.000000000 +0200 @@ -26,6 +26,7 @@ _ "embed" "encoding/base64" "encoding/pem" + "errors" "fmt" "io" "io/fs" @@ -394,6 +395,24 @@ return createMicroVM(ctx, cfg) } +// waitForProcessExit polls until a process exits or the timeout is reached. +// Returns true if the process exited, false if timeout exceeded. +func waitForProcessExit(proc *os.Process, timeout time.Duration) bool { + if proc == nil { + return true + } + deadline := time.Now().Add(timeout) + for time.Now().Before(deadline) { + // Signal 0 tests if the process exists without sending a signal + err := proc.Signal(syscall.Signal(0)) + if err != nil { + return true // process is gone + } + time.Sleep(100 * time.Millisecond) + } + return false +} + // TerminatePod terminates a pod if necessary. For Qemu runners, shuts // down the guest VM. func (bw *qemu) TerminatePod(ctx context.Context, cfg *Config) error { @@ -405,7 +424,8 @@ defer secureDelete(ctx, cfg.InitramfsPath) defer stopVirtiofsd(ctx, cfg) - clog.FromContext(ctx).Info("qemu: sending shutdown signal") + log := clog.FromContext(ctx) + log.Info("qemu: sending shutdown signal") err := sendSSHCommand(ctx, cfg.SSHControlClient, cfg, @@ -416,12 +436,29 @@ []string{"sh", "-c", "echo s > /proc/sysrq-trigger && echo o > /proc/sysrq-trigger&"}, ) if err != nil { - clog.FromContext(ctx).Warnf("failed to gracefully shutdown vm, killing it: %v", err) - // in case of graceful shutdown failure, axe it with pkill - return syscall.Kill(cfg.QemuPID, syscall.SIGKILL) + // ExitMissingError is expected when the VM powers off abruptly before + // the SSH channel can return a clean exit status. Don't log this as an error. + var missingErr *ssh.ExitMissingError + if !errors.As(err, &missingErr) { + log.Warnf("qemu: graceful shutdown command failed: %v", err) + } } - return nil + // Wait up to 5 seconds for the VM process to exit + if waitForProcessExit(cfg.QemuProcess, 5*time.Second) { + return nil + } + + // VM didn't exit, try SIGTERM and wait another 5 seconds + log.Warn("qemu: VM did not exit after shutdown signal, sending SIGTERM") + _ = cfg.QemuProcess.Signal(syscall.SIGTERM) + if waitForProcessExit(cfg.QemuProcess, 5*time.Second) { + return nil + } + + // VM still didn't exit, send SIGKILL + log.Warn("qemu: VM did not exit after SIGTERM, sending SIGKILL") + return cfg.QemuProcess.Signal(syscall.SIGKILL) } // WorkspaceTar implements Runner @@ -675,18 +712,22 @@ return fmt.Errorf("unknown architecture: %s", cfg.Arch.ToAPK()) } - // default to use 85% of available memory, if a mem limit is set, respect it. - mem := int64(float64(getAvailableMemoryKB()) * 0.85) + // Memory: start from 85% of available memory (cgroup-aware via + // getAvailableMemoryKB), then apply precedence cfg.Memory > cgroup > fallback + // through effectiveMemoryKB. The fallback caps shared-runner VMs at 4 GiB + // when neither a user-supplied value nor a cgroup limit is present. + hostScaledKB := int64(float64(getAvailableMemoryKB()) * 0.85) + cgroupMemKB := int64(getCgroupMemoryLimitKB()) + var cfgMemKB int64 if cfg.Memory != "" { - memKb, err := convertHumanToKB(cfg.Memory) + var err error + cfgMemKB, err = convertHumanToKB(cfg.Memory) if err != nil { return err } - - if mem > memKb { - mem = memKb - } } + mem := effectiveMemoryKB(cfgMemKB, hostScaledKB, cgroupMemKB) + // Memory configuration - virtiofs requires shared memory backend if cfg.VirtiofsEnabled { // Round up memory to MiB boundary for memory-backend-memfd alignment @@ -697,19 +738,20 @@ baseargs = append(baseargs, "-m", fmt.Sprintf("%dk", mem)) } - // default to use all CPUs, if a cgroup or config limit is set, respect it. + // CPU: apply precedence cfg.CPU > cgroup > fallback through effectiveCPU. // In a container (e.g. Kubernetes pod), runtime.NumCPU() returns the host's - // total CPUs, not the pod's allocation. Check cgroup limits first. - nproc := runtime.NumCPU() - if cgroupCPU := getCgroupCPULimitCores(); cgroupCPU > 0 && cgroupCPU < nproc { - nproc = cgroupCPU - } + // total CPUs, not the pod's allocation; the cgroup check below covers that. + // The fallback caps shared-runner VMs at 2 vCPUs when neither a user- + // supplied value nor a cgroup limit is present. + hostCPU := runtime.NumCPU() + cgroupCPU := getCgroupCPULimitCores() + var cfgCPU int if cfg.CPU != "" { - cpu, err := strconv.Atoi(cfg.CPU) - if err == nil && nproc > cpu { - nproc = cpu + if parsed, err := strconv.Atoi(cfg.CPU); err == nil && parsed > 0 { + cfgCPU = parsed } } + nproc := effectiveCPU(cfgCPU, cgroupCPU, hostCPU) baseargs = append(baseargs, "-smp", fmt.Sprintf("%d,dies=1,sockets=1,cores=%d,threads=1", nproc, nproc)) // use kvm on linux, Hypervisor.framework on macOS, and software for cross-arch @@ -755,7 +797,23 @@ baseargs = append(baseargs, "-nodefaults") baseargs = append(baseargs, serialArgs...) // use -netdev + -device instead of -nic, as this is better supported by microvm machine type - baseargs = append(baseargs, "-netdev", "user,id=id1,hostfwd=tcp:"+cfg.SSHAddress+"-:22,hostfwd=tcp:"+cfg.SSHControlAddress+"-:2223") + netdevArgs := "user,id=id1,hostfwd=tcp:" + cfg.SSHAddress + "-:22,hostfwd=tcp:" + cfg.SSHControlAddress + "-:2223" + // QEMU_DNS_SEARCH allows configuring DNS search domains inside the guest VM. + // This is useful for builds that need to resolve short hostnames via search + // domains, or when the build environment requires specific DNS resolution + // behavior. The search domains are passed to SLIRP which includes them in DHCP + // responses, so the guest receives them naturally via DHCP. + // Multiple domains should be comma-separated. + // Example: QEMU_DNS_SEARCH="example.com,my.domain.org" + if dnsSearch, ok := os.LookupEnv("QEMU_DNS_SEARCH"); ok { + domains, err := parseDNSSearchDomains(dnsSearch) + if err != nil { + return fmt.Errorf("invalid QEMU_DNS_SEARCH value %q: %w", dnsSearch, err) + } + log.Infof("qemu: QEMU_DNS_SEARCH set to %v, adding %d domain(s) to SLIRP network config", domains, len(domains)) + netdevArgs += buildDNSSearchNetdevArgs(domains) + } + baseargs = append(baseargs, "-netdev", netdevArgs) // Set host_mtu to avoid silent packet drops in nested environments (e.g., // QEMU inside GKE pods). SLIRP defaults to 1500 MTU but the host path MTU // may be lower due to encapsulation (GCP VPC uses 1460, pod networks can be @@ -1118,7 +1176,7 @@ // don't fail the build because of this. } - cfg.QemuPID = qemuCmd.Process.Pid + cfg.QemuProcess = qemuCmd.Process return nil } @@ -1599,7 +1657,12 @@ clog.FromContext(ctx).Debugf("running (%d) %v", len(command), cmd) err = session.Run(cmd) if err != nil { - clog.FromContext(ctx).Errorf("Failed to run command %q: %v", cmd, err) + // ExitMissingError is expected when the SSH channel closes abruptly + // (e.g., when the VM powers off). Don't log it as an error. + var missingErr *ssh.ExitMissingError + if !errors.As(err, &missingErr) { + clog.FromContext(ctx).Errorf("Failed to run command %q: %v", cmd, err) + } return err } @@ -1765,6 +1828,88 @@ return num * multiplier / 1024, nil } +// cpuFallbackCap is the host-exhaustion safeguard cap (in vCPUs) applied +// only when neither cfg.CPU nor a cgroup CPU limit is present. +const cpuFallbackCap = 2 + +// memFallbackCapKB is the host-exhaustion safeguard cap (in KB) applied +// only when neither cfg.Memory nor a cgroup memory limit is present. +// 4 GiB in KB. +const memFallbackCapKB int64 = 4 * 1024 * 1024 + +// effectiveCPU returns the vCPU count the VM should be pinned to, enforcing +// the precedence: +// +// cfgCPU (user flag / YAML) > cgroupCPU (container runtime) > fallback cap +// +// Arguments: +// - cfgCPU: value parsed from cfg.CPU, or 0 if empty/invalid. +// - cgroupCPU: value from getCgroupCPULimitCores(), or 0 if no limit. +// - hostCPU: runtime.NumCPU() on the invoking host. +// +// Precedence and capping: +// - hostCPU is first narrowed by the cgroup limit (when present and < host). +// - A user-supplied cfgCPU is then capped at that cgroup-narrowed host so +// the user request is honoured only when it fits on the effective host. +// - When cfgCPU is zero AND no cgroup limit is driving the budget, the +// min(cap, host) fallback fires. This safeguard prevents shared GKE / +// CI runners with empty YAML from claiming every host core. +func effectiveCPU(cfgCPU, cgroupCPU, hostCPU int) int { + effHost := hostCPU + cgroupNarrows := cgroupCPU > 0 && cgroupCPU < hostCPU + if cgroupNarrows { + effHost = cgroupCPU + } + if cfgCPU > 0 { + if effHost > 0 && effHost < cfgCPU { + return effHost + } + return cfgCPU + } + if cgroupNarrows { + return cgroupCPU + } + if hostCPU < cpuFallbackCap { + return hostCPU + } + return cpuFallbackCap +} + +// effectiveMemoryKB returns the memory (in KB) the VM should be allocated, +// enforcing the precedence: +// +// cfgMemoryKB (user flag / YAML) > cgroup (reflected in hostScaledKB) > fallback cap +// +// Arguments: +// - cfgMemoryKB: value parsed from cfg.Memory, or 0 if empty. +// - hostScaledKB: the runner's 85%-of-available-memory budget. This value +// is already cgroup-aware because getAvailableMemoryKB() consults the +// cgroup limit before falling back to /proc/meminfo. +// - cgroupMemoryKB: value from getCgroupMemoryLimitKB(), or 0 if no limit. +// Used only to distinguish "cgroup is driving the budget" (no extra cap) +// from "no cgroup present" (apply host-exhaustion cap). +// +// When cfgMemoryKB is set it takes precedence but is capped at hostScaledKB, +// so a user request larger than host availability does not oversubscribe. +// When cfgMemoryKB is unset and cgroupMemoryKB is present, hostScaledKB is +// used as-is (cgroup already narrows it). When both are unset, the fallback +// cap fires so a 128 GiB host does not hand its VM ~108 GiB. +func effectiveMemoryKB(cfgMemoryKB, hostScaledKB, cgroupMemoryKB int64) int64 { + if cfgMemoryKB > 0 { + if hostScaledKB > 0 && hostScaledKB < cfgMemoryKB { + return hostScaledKB + } + return cfgMemoryKB + } + if cgroupMemoryKB > 0 { + return hostScaledKB + } + if hostScaledKB < memFallbackCapKB { + return hostScaledKB + } + return memFallbackCapKB +} + // getCgroupCPULimitCores reads the cgroup CPU quota for the current process and // returns the number of whole CPU cores available. It checks cgroup v2 first, // then falls back to cgroup v1. Returns 0 if no cgroup limit is found. @@ -2173,6 +2318,11 @@ } } + // Detect whether the observability hook is present. We cache the result in + // a sidecar file (<cpio>.observability) so we only scan the archive once + // per cached initramfs rather than on every melange invocation. + cfg.ObservabilityHook = observabilityHookPresent(ctx, baseInitramfs) + // Inject SSH host keys and modules return injectRuntimeData(ctx, cfg, os.Getenv("QEMU_KERNEL_MODULES"), baseInitramfs) } @@ -2259,6 +2409,68 @@ return nil } +// cpioContainsPath reports whether target exists as a record name in the CPIO +// archive at cpioFile. CPIO record names are stored without a leading slash. +func cpioContainsPath(cpioFile, target string) (bool, error) { + f, err := os.Open(cpioFile) + if err != nil { + return false, err + } + defer f.Close() + + rr, err := cpio.Newc.NewFileReader(f) + if err != nil { + return false, err + } + + errFound := errors.New("found") + err = cpio.ForEachRecord(rr, func(r cpio.Record) error { + if r.Name == target { + return errFound + } + return nil + }) + if errors.Is(err, errFound) { + return true, nil + } + return false, err +} + +// observabilityHookPresent reports whether the observability hook is present in +// the CPIO archive at cpioFile. The result is cached in a sidecar file +// (<cpioFile>.observability) so the archive is only scanned once per cached +// initramfs. Subsequent calls just read the tiny sidecar. +func observabilityHookPresent(ctx context.Context, cpioFile string) bool { + sidecar := cpioFile + ".observability" + + // Use the cached result when the sidecar is at least as new as the CPIO. + cpioInfo, cpioErr := os.Stat(cpioFile) + sidecarInfo, sidecarErr := os.Stat(sidecar) + if cpioErr == nil && sidecarErr == nil && !sidecarInfo.ModTime().Before(cpioInfo.ModTime()) { + data, err := os.ReadFile(sidecar) + if err == nil { + return strings.TrimSpace(string(data)) == "true" + } + } + + // Sidecar missing or stale — scan the archive. + present, err := cpioContainsPath(cpioFile, observabilityHookSentinel) + if err != nil { + clog.FromContext(ctx).Debugf("qemu: could not inspect initramfs for observability hook: %v", err) + return false + } + + // Write the sidecar so future invocations skip the scan. + val := "false" + if present { + val = "true" + } + if err := os.WriteFile(sidecar, []byte(val+"\n"), 0o600); err != nil { + clog.FromContext(ctx).Debugf("qemu: could not write observability sidecar: %v", err) + } + return present +} + // getAdditionalPackages parses and validates the QEMU_ADDITIONAL_PACKAGES environment variable. // Returns a list of package names to add to the initramfs, or empty slice if none/invalid. func getAdditionalPackages(ctx context.Context) []string { @@ -2288,6 +2500,65 @@ return packages } +// dnsSearchDomainRegex matches valid DNS search domain characters. +// Only allows alphanumeric characters, dots, and hyphens. +// This prevents injection of QEMU netdev options via malicious domain names. +var ( + // dnsSearchDomainRegex matches valid DNS search domain characters. + // Only allows alphanumeric characters, dots, and hyphens. + // This prevents injection of QEMU netdev options via malicious domain names. + dnsSearchDomainRegex = regexp.MustCompile(`^[a-zA-Z0-9.-]+$`) +) + +// parseDNSSearchDomains parses and validates DNS search domains from a comma-separated string. +// Returns an error if the input is empty or contains invalid domain characters. +func parseDNSSearchDomains(input string) ([]string, error) { + input = strings.TrimSpace(input) + if input == "" { + return nil, fmt.Errorf("empty input") + } + + // Only split on commas + parts := strings.Split(input, ",") + + domains := make([]string, 0, len(parts)) + for _, part := range parts { + part = strings.TrimSpace(part) + if part == "" { + continue + } + + // Validate domain: only allow [a-zA-Z0-9.-] + if !dnsSearchDomainRegex.MatchString(part) { + return nil, fmt.Errorf("invalid characters in domain %q: only alphanumeric, dots, and hyphens are allowed", part) + } + + domains = append(domains, part) + } + + if len(domains) == 0 { + return nil, fmt.Errorf("no valid domains found") + } + + return domains, nil +} + +// buildDNSSearchNetdevArgs constructs the QEMU netdev dnssearch options string. +// Returns empty string if no domains provided. +// Each domain produces a separate ",dnssearch=<domain>" option. +func buildDNSSearchNetdevArgs(domains []string) string { + if len(domains) == 0 { + return "" + } + + var builder strings.Builder + for _, domain := range domains { + builder.WriteString(",dnssearch=") + builder.WriteString(domain) + } + return builder.String() +} + // getPackageCacheSuffix generates a deterministic cache suffix based on the package list. // Uses SHA256 hash (first 12 chars) to avoid collisions and keep filenames reasonable. // Returns empty string if packages list is empty. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/container/qemu_runner_test.go new/melange-0.50.1/pkg/container/qemu_runner_test.go --- old/melange-0.49.0/pkg/container/qemu_runner_test.go 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/container/qemu_runner_test.go 2026-04-17 22:03:54.000000000 +0200 @@ -231,6 +231,267 @@ } } +func TestParseDNSSearchDomains(t *testing.T) { + tests := []struct { + name string + input string + expected []string + wantErr bool + }{ + // Valid single domain + { + name: "single valid domain", + input: "example.com", + expected: []string{"example.com"}, + }, + // Valid multiple domains - comma separated + { + name: "comma separated domains", + input: "example.com,test.org", + expected: []string{"example.com", "test.org"}, + }, + // Multiple commas collapsed + { + name: "multiple commas collapsed", + input: "a.com,,b.org", + expected: []string{"a.com", "b.org"}, + }, + // Comma with spaces around domains (trimmed) + { + name: "comma with spaces trimmed", + input: "a.com, b.org , c.net", + expected: []string{"a.com", "b.org", "c.net"}, + }, + // Hyphenated domain + { + name: "hyphenated domain", + input: "my-domain.example.com", + expected: []string{"my-domain.example.com"}, + }, + // Nested subdomains + { + name: "nested subdomains", + input: "a.b.c.d.example.com", + expected: []string{"a.b.c.d.example.com"}, + }, + // Numeric domain parts + { + name: "numeric domain parts", + input: "123.example.com", + expected: []string{"123.example.com"}, + }, + // Empty input + { + name: "empty string", + input: "", + wantErr: true, + }, + // Only whitespace + { + name: "only whitespace", + input: " ", + wantErr: true, + }, + // Only commas + { + name: "only commas", + input: ",,,", + wantErr: true, + }, + // Space-separated domains (not allowed) + { + name: "space separated domains rejected", + input: "example.com test.org", + wantErr: true, + }, + // Newline in domain (not allowed) + { + name: "newline rejected", + input: "foo\nbar", + wantErr: true, + }, + // Tab in domain (not allowed) + { + name: "tab rejected", + input: "foo\tbar", + wantErr: true, + }, + // Injection with equals sign (netdev option injection) + { + name: "injection attempt with equals", + input: "evil=value", + wantErr: true, + }, + // Injection with hostfwd attempt + { + name: "hostfwd injection attempt", + input: "foo,hostfwd=tcp::8080-:22", + wantErr: true, + }, + // Injection with colon + { + name: "colon injection (port-like)", + input: "domain:8080", + wantErr: true, + }, + // Semicolon injection (command separator) + { + name: "semicolon injection", + input: "foo;rm -rf /", + wantErr: true, + }, + // Pipe injection + { + name: "pipe injection", + input: "foo|cat /etc/passwd", + wantErr: true, + }, + // Backtick injection + { + name: "backtick injection", + input: "foo`whoami`", + wantErr: true, + }, + // Dollar sign injection + { + name: "dollar sign injection", + input: "foo$HOME", + wantErr: true, + }, + // Quote injection + { + name: "double quote injection", + input: `foo"bar`, + wantErr: true, + }, + // Single quote injection + { + name: "single quote injection", + input: "foo'bar", + wantErr: true, + }, + // Ampersand injection + { + name: "ampersand injection", + input: "foo&bar", + wantErr: true, + }, + // Parentheses injection + { + name: "parentheses injection", + input: "foo(bar)", + wantErr: true, + }, + // Bracket injection + { + name: "bracket injection", + input: "foo[bar]", + wantErr: true, + }, + // Brace injection + { + name: "brace injection", + input: "foo{bar}", + wantErr: true, + }, + // Angle bracket injection + { + name: "angle bracket injection", + input: "foo<bar>", + wantErr: true, + }, + // Backslash injection + { + name: "backslash injection", + input: "foo\\bar", + wantErr: true, + }, + // Forward slash (path-like) + { + name: "forward slash injection", + input: "foo/bar", + wantErr: true, + }, + // One valid, one invalid domain + { + name: "mixed valid and invalid domains", + input: "good.com,evil=bad", + wantErr: true, + }, + // QEMU dnssearch option injection attempt + { + name: "dnssearch option injection", + input: "foo,dnssearch=evil.com", + wantErr: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseDNSSearchDomains(tt.input) + + if tt.wantErr { + if err == nil { + t.Errorf("parseDNSSearchDomains(%q) expected error, got nil with result %v", tt.input, result) + } + return + } + + if err != nil { + t.Errorf("parseDNSSearchDomains(%q) unexpected %v", tt.input, err) + return + } + + if len(result) != len(tt.expected) { + t.Errorf("parseDNSSearchDomains(%q) returned %d domains, expected %d: got %v, want %v", + tt.input, len(result), len(tt.expected), result, tt.expected) + return + } + + for i, domain := range result { + if domain != tt.expected[i] { + t.Errorf("parseDNSSearchDomains(%q)[%d] = %q, expected %q", + tt.input, i, domain, tt.expected[i]) + } + } + }) + } +} + +func TestBuildDNSSearchNetdevArgs(t *testing.T) { + tests := []struct { + name string + domains []string + expected string + }{ + { + name: "empty domains", + domains: nil, + expected: "", + }, + { + name: "single domain", + domains: []string{"example.com"}, + expected: ",dnssearch=example.com", + }, + { + name: "multiple domains", + domains: []string{"a.com", "b.org", "c.net"}, + expected: ",dnssearch=a.com,dnssearch=b.org,dnssearch=c.net", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := buildDNSSearchNetdevArgs(tt.domains) + if result != tt.expected { + t.Errorf("buildDNSSearchNetdevArgs(%v) = %q, expected %q", + tt.domains, result, tt.expected) + } + }) + } +} + func TestGetPackageCacheSuffix(t *testing.T) { tests := []struct { name string @@ -663,3 +924,90 @@ } return false } + +func TestEffectiveCPU(t *testing.T) { + tests := []struct { + name string + cfgCPU int + cgroupCPU int + hostCPU int + want int + }{ + // Flag / YAML precedence (Invariants 1 & 2): + // cfg wins over the fallback (never returns 2 when cfgCPU is set) + // but is still capped at the cgroup-narrowed host. + {name: "flag wins under host", cfgCPU: 4, cgroupCPU: 0, hostCPU: 8, want: 4}, + {name: "flag wins over cgroup when smaller", cfgCPU: 4, cgroupCPU: 8, hostCPU: 16, want: 4}, + {name: "flag wins at host boundary", cfgCPU: 8, cgroupCPU: 0, hostCPU: 8, want: 8}, + {name: "flag capped at host when larger", cfgCPU: 16, cgroupCPU: 0, hostCPU: 8, want: 8}, + {name: "flag capped at cgroup when cgroup narrower", cfgCPU: 16, cgroupCPU: 4, hostCPU: 8, want: 4}, + {name: "flag capped at cgroup on big host", cfgCPU: 16, cgroupCPU: 4, hostCPU: 32, want: 4}, + + // Cgroup precedence (Invariant 3) + {name: "cgroup wins when cfg empty", cfgCPU: 0, cgroupCPU: 3, hostCPU: 8, want: 3}, + {name: "cgroup at host boundary falls to fallback", cfgCPU: 0, cgroupCPU: 8, hostCPU: 8, want: 2}, + // cgroupCPU >= hostCPU means cgroup didn't actually narrow — fallback applies. + + // Fallback (Invariant 4) + {name: "regression: no cfg no cgroup big host", cfgCPU: 0, cgroupCPU: 0, hostCPU: 16, want: 2}, + {name: "fallback caps at 2 on 32-core host", cfgCPU: 0, cgroupCPU: 0, hostCPU: 32, want: 2}, + {name: "fallback small host", cfgCPU: 0, cgroupCPU: 0, hostCPU: 1, want: 1}, + {name: "fallback exact 2 host", cfgCPU: 0, cgroupCPU: 0, hostCPU: 2, want: 2}, + + // Edge: zero / degenerate inputs + {name: "all zero (degenerate)", cfgCPU: 0, cgroupCPU: 0, hostCPU: 0, want: 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := effectiveCPU(tt.cfgCPU, tt.cgroupCPU, tt.hostCPU) + if got != tt.want { + t.Errorf("effectiveCPU(cfg=%d, cgroup=%d, host=%d) = %d, want %d", + tt.cfgCPU, tt.cgroupCPU, tt.hostCPU, got, tt.want) + } + }) + } +} + +func TestEffectiveMemoryKB(t *testing.T) { + const ( + gib = int64(1024 * 1024) // 1 GiB in KB + fallback = int64(4 * 1024 * 1024) + bigHost = int64(108 * 1024 * 1024) // 108 GiB in KB (85% of 128 GiB) + smallHost = int64(2 * 1024 * 1024) + ) + + tests := []struct { + name string + cfgKB int64 + hostKB int64 // the already-scaled (85%) host-available value + cgroupKB int64 + want int64 + }{ + // Flag / YAML precedence + {name: "cfg wins under host", cfgKB: 8 * gib, hostKB: bigHost, cgroupKB: 0, want: 8 * gib}, + {name: "cfg wins over cgroup", cfgKB: 8 * gib, hostKB: 16 * gib, cgroupKB: 32 * gib, want: 8 * gib}, + {name: "cfg capped at host", cfgKB: 32 * gib, hostKB: smallHost, cgroupKB: 0, want: smallHost}, + + // Cgroup precedence: host is already cgroup-aware, so pass through. + {name: "cgroup wins when cfg empty", cfgKB: 0, hostKB: 8 * gib, cgroupKB: 16 * gib, want: 8 * gib}, + + // Fallback cap at 4Gi when no cfg and no cgroup + {name: "regression: 128GiB host capped at 4GiB", cfgKB: 0, hostKB: bigHost, cgroupKB: 0, want: fallback}, + {name: "fallback exact 4GiB host", cfgKB: 0, hostKB: fallback, cgroupKB: 0, want: fallback}, + {name: "fallback small host below cap", cfgKB: 0, hostKB: 2 * gib, cgroupKB: 0, want: 2 * gib}, + + // Edge + {name: "cfg zero host zero", cfgKB: 0, hostKB: 0, cgroupKB: 0, want: 0}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got := effectiveMemoryKB(tt.cfgKB, tt.hostKB, tt.cgroupKB) + if got != tt.want { + t.Errorf("effectiveMemoryKB(cfg=%d, host=%d, cgroup=%d) = %d, want %d", + tt.cfgKB, tt.hostKB, tt.cgroupKB, got, tt.want) + } + }) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.49.0/pkg/source/testdata/fetch.yaml new/melange-0.50.1/pkg/source/testdata/fetch.yaml --- old/melange-0.49.0/pkg/source/testdata/fetch.yaml 2026-04-11 04:29:31.000000000 +0200 +++ new/melange-0.50.1/pkg/source/testdata/fetch.yaml 2026-04-17 22:03:54.000000000 +0200 @@ -21,5 +21,6 @@ - uses: fetch with: uri: https://unofficial-builds.nodejs.org/download/release/v22.9.0/node-v22.9.0-linux-x64-glibc-217.tar.gz + reason: this is a test of the fetch pipeline - runs: echo "steps after" ++++++ melange.obsinfo ++++++ --- /var/tmp/diff_new_pack.6czRRO/_old 2026-04-18 21:35:42.895949548 +0200 +++ /var/tmp/diff_new_pack.6czRRO/_new 2026-04-18 21:35:42.899949711 +0200 @@ -1,5 +1,5 @@ name: melange -version: 0.49.0 -mtime: 1775874571 -commit: 4121ddfd1c96ecefe8644566858072682828c1ac +version: 0.50.1 +mtime: 1776456234 +commit: 738880da281d8df5c37a56c70a4f39386ca90ef4 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/melange/vendor.tar.gz /work/SRC/openSUSE:Factory/.melange.new.11940/vendor.tar.gz differ: char 132, line 1
