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

Reply via email to