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-03-11 20:55:49
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/melange (Old)
 and      /work/SRC/openSUSE:Factory/.melange.new.8177 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "melange"

Wed Mar 11 20:55:49 2026 rev:145 rq:1338204 version:0.45.3

Changes:
--------
--- /work/SRC/openSUSE:Factory/melange/melange.changes  2026-03-10 
17:58:31.512460502 +0100
+++ /work/SRC/openSUSE:Factory/.melange.new.8177/melange.changes        
2026-03-11 20:57:22.627015435 +0100
@@ -1,0 +2,13 @@
+Wed Mar 11 06:00:38 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.45.3:
+  * qemu: add SSH keepalives, handshake timeouts, and error
+    wrapping (#2412)
+  * qemu: set conservative guest MTU to prevent hangs in nested
+    environments (#2411)
+- Update to version 0.45.2:
+  * chore: Defer initramfs deletion until after SSH client is set
+    up (#2410)
+  * go: upgrade to mody/mody/v2 (#2408)
+
+-------------------------------------------------------------------

Old:
----
  melange-0.45.1.obscpio

New:
----
  melange-0.45.3.obscpio

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

Other differences:
------------------
++++++ melange.spec ++++++
--- /var/tmp/diff_new_pack.Sh5Z5m/_old  2026-03-11 20:57:24.251082439 +0100
+++ /var/tmp/diff_new_pack.Sh5Z5m/_new  2026-03-11 20:57:24.251082439 +0100
@@ -17,7 +17,7 @@
 
 
 Name:           melange
-Version:        0.45.1
+Version:        0.45.3
 Release:        0
 Summary:        Build APKs from source code
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.Sh5Z5m/_old  2026-03-11 20:57:24.287083924 +0100
+++ /var/tmp/diff_new_pack.Sh5Z5m/_new  2026-03-11 20:57:24.291084089 +0100
@@ -3,7 +3,7 @@
     <param name="url">https://github.com/chainguard-dev/melange</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
-    <param name="revision">v0.45.1</param>
+    <param name="revision">v0.45.3</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
     <param name="changesgenerate">enable</param>

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.Sh5Z5m/_old  2026-03-11 20:57:24.343086235 +0100
+++ /var/tmp/diff_new_pack.Sh5Z5m/_new  2026-03-11 20:57:24.347086400 +0100
@@ -1,6 +1,6 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/chainguard-dev/melange</param>
-              <param 
name="changesrevision">049664c63b543602ba8e344d2ebf0bdd950f3428</param></service></servicedata>
+              <param 
name="changesrevision">2a226faac364a05b188a3afcbbdc45b35b395ff3</param></service></servicedata>
 (No newline at EOF)
 

++++++ melange-0.45.1.obscpio -> melange-0.45.3.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/melange-0.45.1/go.mod new/melange-0.45.3/go.mod
--- old/melange-0.45.1/go.mod   2026-03-09 18:41:30.000000000 +0100
+++ new/melange-0.45.3/go.mod   2026-03-11 00:53:12.000000000 +0100
@@ -22,7 +22,7 @@
        github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
        github.com/klauspost/compress v1.18.4
        github.com/klauspost/pgzip v1.2.6
-       github.com/moby/moby v28.5.2+incompatible
+       github.com/moby/moby/v2 v2.0.0-beta.7
        github.com/opencontainers/image-spec v1.1.1
        github.com/package-url/packageurl-go v0.1.4
        github.com/pkg/errors v0.9.1
@@ -96,11 +96,11 @@
        github.com/charmbracelet/x/term v0.2.1 // indirect
        github.com/cloudflare/circl v1.6.3 // indirect
        github.com/common-nighthawk/go-figure 
v0.0.0-20210622060536-734e95fb86be // indirect
-       github.com/containerd/containerd/v2 v2.1.5 // indirect
+       github.com/containerd/containerd/v2 v2.2.1 // indirect
        github.com/containerd/log v0.1.0 // indirect
        github.com/containerd/stargz-snapshotter/estargz v0.18.2 // indirect
        github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
-       github.com/cyphar/filepath-securejoin v0.4.1 // indirect
+       github.com/cyphar/filepath-securejoin v0.6.0 // indirect
        github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // 
indirect
        github.com/distribution/reference v0.6.0 // indirect
        github.com/docker/distribution v2.8.3+incompatible // indirect
@@ -122,7 +122,7 @@
        github.com/google/uuid v1.6.0 // indirect
        github.com/googleapis/enterprise-certificate-proxy v0.3.12 // indirect
        github.com/googleapis/gax-go/v2 v2.17.0 // indirect
-       github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 // indirect
+       github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
        github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
        github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
        github.com/inconshreveable/mousetrap v1.1.0 // indirect
@@ -168,7 +168,7 @@
        go.opentelemetry.io/otel/trace v1.41.0 // indirect
        go.step.sm/crypto v0.76.2 // indirect
        golang.org/x/mod v0.33.0 // indirect
-       golang.org/x/net v0.50.0 // indirect
+       golang.org/x/net v0.51.0 // indirect
        golang.org/x/oauth2 v0.35.0 // indirect
        google.golang.org/api v0.269.0 // indirect
        google.golang.org/genproto/googleapis/api 
v0.0.0-20260128011058-8636f8732409 // indirect
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/melange-0.45.1/go.sum new/melange-0.45.3/go.sum
--- old/melange-0.45.1/go.sum   2026-03-09 18:41:30.000000000 +0100
+++ new/melange-0.45.3/go.sum   2026-03-11 00:53:12.000000000 +0100
@@ -35,8 +35,8 @@
 github.com/beorn7/perks v1.0.1/go.mod 
h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
 github.com/buger/jsonparser v1.1.1 
h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs=
 github.com/buger/jsonparser v1.1.1/go.mod 
h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0=
-github.com/cenkalti/backoff/v4 v4.3.0 
h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
-github.com/cenkalti/backoff/v4 v4.3.0/go.mod 
h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
+github.com/cenkalti/backoff/v5 v5.0.3 
h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
+github.com/cenkalti/backoff/v5 v5.0.3/go.mod 
h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
 github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod 
h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
 github.com/cespare/xxhash/v2 v2.3.0 
h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
 github.com/cespare/xxhash/v2 v2.3.0/go.mod 
h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
@@ -68,8 +68,8 @@
 github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod 
h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
 github.com/common-nighthawk/go-figure v0.0.0-20210622060536-734e95fb86be 
h1:J5BL2kskAlV9ckgEsNQXscjIaLiOYiZ75d4e94E6dcQ=
 github.com/common-nighthawk/go-figure 
v0.0.0-20210622060536-734e95fb86be/go.mod 
h1:mk5IQ+Y0ZeO87b858TlA645sVcEcbiX6YqP98kt+7+w=
-github.com/containerd/containerd/v2 v2.1.5 
h1:pWSmPxUszaLZKQPvOx27iD4iH+aM6o0BoN9+hg77cro=
-github.com/containerd/containerd/v2 v2.1.5/go.mod 
h1:8C5QV9djwsYDNhxfTCFjWtTBZrqjditQ4/ghHSYjnHM=
+github.com/containerd/containerd/v2 v2.2.1 
h1:TpyxcY4AL5A+07dxETevunVS5zxqzuq7ZqJXknM11yk=
+github.com/containerd/containerd/v2 v2.2.1/go.mod 
h1:NR70yW1iDxe84F2iFWbR9xfAN0N2F0NcjTi1OVth4nU=
 github.com/containerd/errdefs v1.0.0 
h1:tg5yIfIlQIrxYtu9ajqY42W3lpS19XqdxRQeEwYG8PI=
 github.com/containerd/errdefs v1.0.0/go.mod 
h1:+YBYIdtsnF4Iw6nWZhJcqGSg/dwvV7tyJ/kCkyJ2k+M=
 github.com/containerd/errdefs/pkg v0.3.0 
h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151Xdx3ZPPE=
@@ -83,8 +83,8 @@
 github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod 
h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
 github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s=
 github.com/creack/pty v1.1.24/go.mod 
h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE=
-github.com/cyphar/filepath-securejoin v0.4.1 
h1:JyxxyPEaktOD+GAnqIqTf9A8tHyAG22rowi7HkoSU1s=
-github.com/cyphar/filepath-securejoin v0.4.1/go.mod 
h1:Sdj7gXlvMcPZsbhwhQ33GguGLDGQL7h7bg04C/+u9jI=
+github.com/cyphar/filepath-securejoin v0.6.0 
h1:BtGB77njd6SVO6VztOHfPxKitJvd/VPT+OFBFMOi1Is=
+github.com/cyphar/filepath-securejoin v0.6.0/go.mod 
h1:A8hd4EnAeyujCJRrICiOWqjS1AX0a9kM5XL+NwKoYSc=
 github.com/davecgh/go-spew v1.1.0/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.1/go.mod 
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
 github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc 
h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
@@ -196,8 +196,8 @@
 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=
 github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.3.3/go.mod 
h1:NbCUVmiS4foBGBHOYlCT25+YmGpJ32dZPi75pGEUpj4=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3 
h1:NmZ1PKzSTQbuGHw9DGPFomqkkLWMC+vZCkfs+FHv1Vg=
-github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.3/go.mod 
h1:zQrxl1YP88HQlA6i9c63DSVPFklWpGX4OWAc9bFuaH4=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 
h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
+github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod 
h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
 github.com/hashicorp/go-cleanhttp v0.5.2 
h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
 github.com/hashicorp/go-cleanhttp v0.5.2/go.mod 
h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
 github.com/hashicorp/go-hclog v1.6.3 
h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
@@ -251,16 +251,16 @@
 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 v28.5.2+incompatible 
h1:hIn6qcenb3JY1E3STwqEbBvJ8bha+u1LpqjX4CBvNCk=
-github.com/moby/moby v28.5.2+incompatible/go.mod 
h1:fDXVQ6+S340veQPv35CzDahGBmHsiclFwfEygB/TWMc=
+github.com/moby/moby/v2 v2.0.0-beta.7 
h1:f+Sc06mGJ5eBskZlSEb8+2MgVPTn74Y6xG04xS3VNLw=
+github.com/moby/moby/v2 v2.0.0-beta.7/go.mod 
h1:rKqTDjHJippfr+QpXtKL8G0tZDDc7vanyxaVBL43o8s=
 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=
 github.com/moby/sys/sequential v0.6.0/go.mod 
h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko=
 github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ=
 github.com/moby/term v0.5.2/go.mod 
h1:d3djjFCrjnB+fl8NJux+EJzu0msscUP+f8it8hPkFLc=
-github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
-github.com/morikuni/aec v1.0.0/go.mod 
h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
+github.com/morikuni/aec v1.1.0 h1:vBBl0pUnvi/Je71dsRrhMBtreIqNMYErSAbEeb8jrXQ=
+github.com/morikuni/aec v1.1.0/go.mod 
h1:xDRgiq/iw5l+zkao76YTKzKttOp2cwPEne25HDkJnBw=
 github.com/muesli/termenv v0.16.0 
h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
 github.com/muesli/termenv v0.16.0/go.mod 
h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 
h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
@@ -368,10 +368,10 @@
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.63.0/go.mod 
h1:h06DGIukJOevXaj/xrNjhi/2098RZzcLTbc0jDAUbsg=
 go.opentelemetry.io/otel v1.41.0 
h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
 go.opentelemetry.io/otel v1.41.0/go.mod 
h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0 
h1:GqRJVj7UmLjCVyVJ3ZFLdPRmhDUp2zFmQe3RHIOsw24=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.38.0/go.mod 
h1:ri3aaHSmCTVYu2AWv44YMauwAQc0aqI9gHKIcSbI1pU=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0 
h1:xJ2qHD0C1BeYVTLLR9sX12+Qb95kfeD/byKj6Ky1pXg=
-go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.35.0/go.mod 
h1:u5BF1xyjstDowA1R5QAO9JHzqK+ublenEW/dyqTjBVk=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 
h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod 
h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 
h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
+go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod 
h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0 
h1:61oRQmYGMW7pXmFjPg1Muy84ndqMxQ6SH2L8fBG8fSY=
 go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.41.0/go.mod 
h1:c0z2ubK4RQL+kSDuuFu9WnuXimObon3IiKjJf4NACvU=
 go.opentelemetry.io/otel/metric v1.41.0 
h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
@@ -382,8 +382,8 @@
 go.opentelemetry.io/otel/sdk/metric v1.41.0/go.mod 
h1:HNBuSvT7ROaGtGI50ArdRLUnvRTRGniSUZbxiWxSO8Y=
 go.opentelemetry.io/otel/trace v1.41.0 
h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
 go.opentelemetry.io/otel/trace v1.41.0/go.mod 
h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
-go.opentelemetry.io/proto/otlp v1.7.1 
h1:gTOMpGDb0WTBOP8JaO72iL3auEZhVmAQg4ipjOVAtj4=
-go.opentelemetry.io/proto/otlp v1.7.1/go.mod 
h1:b2rVh6rfI/s2pHWNlB7ILJcRALpcNDzKhACevjI+ZnE=
+go.opentelemetry.io/proto/otlp v1.9.0 
h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
+go.opentelemetry.io/proto/otlp v1.9.0/go.mod 
h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
 go.step.sm/crypto v0.76.2 h1:JJ/yMcs/rmcCAwlo+afrHjq74XBFRTJw5B2y4Q4Z4c4=
 go.step.sm/crypto v0.76.2/go.mod 
h1:m6KlB/HzIuGFep0UWI5e0SYi38UxpoKeCg6qUaHV6/Q=
 go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -421,8 +421,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.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
-golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
+golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
+golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
 golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod 
h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
 golang.org/x/oauth2 v0.35.0 h1:Mv2mzuHuZuY2+bkyWXIHMfhNdJAdwW3FuWeCPYN5GVQ=
 golang.org/x/oauth2 v0.35.0/go.mod 
h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/melange-0.45.1/pkg/container/bubblewrap_runner.go 
new/melange-0.45.3/pkg/container/bubblewrap_runner.go
--- old/melange-0.45.1/pkg/container/bubblewrap_runner.go       2026-03-09 
18:41:30.000000000 +0100
+++ new/melange-0.45.3/pkg/container/bubblewrap_runner.go       2026-03-11 
00:53:12.000000000 +0100
@@ -34,7 +34,7 @@
        apko_types "chainguard.dev/apko/pkg/build/types"
        "github.com/chainguard-dev/clog"
        v1 "github.com/google/go-containerregistry/pkg/v1"
-       moby "github.com/moby/moby/oci/caps"
+       moby "github.com/moby/moby/v2/daemon/pkg/oci/caps"
        "go.opentelemetry.io/otel"
 )
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/melange-0.45.1/pkg/container/qemu_runner.go 
new/melange-0.45.3/pkg/container/qemu_runner.go
--- old/melange-0.45.1/pkg/container/qemu_runner.go     2026-03-09 
18:41:30.000000000 +0100
+++ new/melange-0.45.3/pkg/container/qemu_runner.go     2026-03-11 
00:53:12.000000000 +0100
@@ -68,6 +68,20 @@
 
 const (
        defaultDiskSize = "50Gi"
+
+       // sshDialTimeout is the maximum time allowed for TCP connect + SSH 
handshake.
+       sshDialTimeout = 30 * time.Second
+
+       // sshKeepaliveInterval is how often keepalive requests are sent.
+       sshKeepaliveInterval = 30 * time.Second
+
+       // sshKeepaliveMaxMissed is the number of consecutive missed keepalives
+       // before the connection is closed (equivalent to ServerAliveCountMax).
+       sshKeepaliveMaxMissed = 3
+
+       // sshKeepaliveRequestTimeout is how long to wait for a single keepalive
+       // response before counting it as missed.
+       sshKeepaliveRequestTimeout = 10 * time.Second
 )
 
 type qemu struct{}
@@ -185,7 +199,7 @@
        }
 
        // Connect to the SSH server
-       client, err := ssh.Dial("tcp", cfg.SSHAddress, config)
+       client, err := sshDialWithTimeout(cfg.SSHAddress, config, 
sshDialTimeout)
        if err != nil {
                clog.FromContext(ctx).Errorf("Failed to dial: %s", err)
                return err
@@ -742,7 +756,12 @@
        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")
-       baseargs = append(baseargs, "-device", 
"virtio-net-pci,netdev=id1,romfile=")
+       // 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
+       // lower). The guest kernel picks up host_mtu via virtio feature 
negotiation
+       // and configures the interface MTU automatically at boot.
+       baseargs = append(baseargs, "-device", 
"virtio-net-pci,netdev=id1,romfile=,host_mtu=1400")
        // add random generator via pci, improve ssh startup time
        baseargs = append(baseargs, "-device", "virtio-rng-pci,rng=rng0", 
"-object", "rng-random,filename=/dev/urandom,id=rng0")
        // panic=-1 ensures that if the init fails, we immediately exit the 
machine
@@ -931,8 +950,6 @@
        select {
        case <-started:
                log.Info("qemu: VM started successfully, SSH server is up")
-               secureDelete(ctx, cfg.InitramfsPath)
-               cfg.InitramfsPath = ""
        case err := <-qemuExit:
                defer os.Remove(cfg.ImgRef)
                defer os.Remove(cfg.Disk)
@@ -950,16 +967,17 @@
                return fmt.Errorf("qemu: context canceled while waiting for VM 
to start: %w", context.Cause(ctx))
        }
 
-       err = getHostKey(ctx, cfg)
-       if err != nil {
-               return fmt.Errorf("qemu: could not get VM host key")
+       if err := getHostKey(ctx, cfg); err != nil {
+               return fmt.Errorf("qemu: could not get VM host key: %w", err)
        }
 
-       err = setupSSHClients(ctx, cfg)
-       if err != nil {
-               return fmt.Errorf("qemu: could not setup SSH client")
+       if err := setupSSHClients(ctx, cfg); err != nil {
+               return fmt.Errorf("qemu: could not setup SSH client: %w", err)
        }
 
+       secureDelete(ctx, cfg.InitramfsPath)
+       cfg.InitramfsPath = ""
+
        // Zero out sensitive private key material now that all SSH connections 
are established
        // The public key is retained for verification in setupSSHClients
        zeroSensitiveFields(ctx, cfg)
@@ -970,7 +988,7 @@
 
        kv, err := getGuestKernelVersion(ctx, cfg)
        if err != nil {
-               return fmt.Errorf("qemu: unable to query guest kernel version")
+               return fmt.Errorf("qemu: unable to query guest kernel version: 
%w", err)
        }
        clog.FromContext(ctx).Infof("qemu: running kernel version: %s", kv)
 
@@ -1141,29 +1159,125 @@
        }
 
        // Connect to the SSH server
-       cfg.SSHBuildClient, err = ssh.Dial("tcp", cfg.SSHAddress, buildConfig)
+       cfg.SSHBuildClient, err = sshDialWithTimeout(cfg.SSHAddress, 
buildConfig, sshDialTimeout)
        if err != nil {
                clog.FromContext(ctx).Errorf("Failed to dial: %s", err)
                return err
        }
 
        // Connect to the SSH server on the control (unchrooted) port
-       cfg.SSHControlClient, err = ssh.Dial("tcp", cfg.SSHControlAddress, 
controlConfig)
+       cfg.SSHControlClient, err = sshDialWithTimeout(cfg.SSHControlAddress, 
controlConfig, sshDialTimeout)
        if err != nil {
                clog.FromContext(ctx).Errorf("Failed to dial: %s", err)
                return err
        }
 
        // Connect to the SSH server on the chrooted port with privilege
-       cfg.SSHControlBuildClient, err = ssh.Dial("tcp", cfg.SSHAddress, 
controlConfig)
+       cfg.SSHControlBuildClient, err = sshDialWithTimeout(cfg.SSHAddress, 
controlConfig, sshDialTimeout)
        if err != nil {
                clog.FromContext(ctx).Errorf("Failed to dial: %s", err)
                return err
        }
 
+       // Start SSH keepalives for all clients to prevent idle disconnects
+       startSSHKeepalive(ctx, cfg.SSHBuildClient, "build")
+       startSSHKeepalive(ctx, cfg.SSHControlClient, "control")
+       startSSHKeepalive(ctx, cfg.SSHControlBuildClient, "control-build")
+
        return nil
 }
 
+// sshDialWithTimeout dials an SSH connection with a deadline covering both the
+// TCP connect and the SSH handshake. This prevents hangs when the remote end
+// accepts the TCP connection but stalls during the handshake 
(golang/go#19338).
+func sshDialWithTimeout(addr string, config *ssh.ClientConfig, timeout 
time.Duration) (*ssh.Client, error) {
+       conn, err := net.DialTimeout("tcp", addr, timeout)
+       if err != nil {
+               return nil, err
+       }
+
+       // Set a deadline covering the entire SSH handshake
+       if err := conn.SetDeadline(time.Now().Add(timeout)); err != nil {
+               conn.Close()
+               return nil, err
+       }
+
+       c, chans, reqs, err := ssh.NewClientConn(conn, addr, config)
+       if err != nil {
+               conn.Close()
+               return nil, err
+       }
+
+       // Clear the deadline so it doesn't affect future operations
+       if err := conn.SetDeadline(time.Time{}); err != nil {
+               c.Close()
+               return nil, err
+       }
+
+       return ssh.NewClient(c, chans, reqs), nil
+}
+
+// sshKeepaliveClient is the interface needed for SSH keepalive operations.
+type sshKeepaliveClient interface {
+       SendRequest(name string, wantReply bool, payload []byte) (bool, []byte, 
error)
+       Close() error
+}
+
+// startSSHKeepalive sends periodic keepalive requests to an SSH client to
+// prevent idle connection timeouts. After sshKeepaliveMaxMissed consecutive
+// missed keepalives (equivalent to ServerAliveCountMax), the client connection
+// is closed. The goroutine stops when the context is canceled or the 
connection
+// is closed.
+func startSSHKeepalive(ctx context.Context, client sshKeepaliveClient, name 
string) {
+       go runSSHKeepalive(ctx, client, name)
+}
+
+// runSSHKeepalive is the keepalive loop, separated from startSSHKeepalive for
+// testability (synctest requires the goroutine to be started inside the 
bubble).
+func runSSHKeepalive(ctx context.Context, client sshKeepaliveClient, name 
string) {
+       t := time.NewTicker(sshKeepaliveInterval)
+       defer t.Stop()
+
+       missed := 0
+       for {
+               select {
+               case <-ctx.Done():
+                       return
+               case <-t.C:
+                       if err := sshSendKeepalive(ctx, client); err != nil {
+                               missed++
+                               clog.FromContext(ctx).Debugf("ssh keepalive 
missed for %s client (%d/%d): %v", name, missed, sshKeepaliveMaxMissed, err)
+                               if missed >= sshKeepaliveMaxMissed {
+                                       clog.FromContext(ctx).Warnf("ssh 
keepalive: closing %s client after %d consecutive missed keepalives", name, 
missed)
+                                       client.Close()
+                                       return
+                               }
+                               continue
+                       }
+                       missed = 0
+               }
+       }
+}
+
+// sshSendKeepalive sends a single keepalive request with a timeout to prevent
+// blocking indefinitely on a hung connection.
+func sshSendKeepalive(ctx context.Context, client sshKeepaliveClient) error {
+       done := make(chan error, 1)
+       go func() {
+               _, _, err := client.SendRequest("[email protected]", true, 
nil)
+               done <- err
+       }()
+
+       select {
+       case err := <-done:
+               return err
+       case <-time.After(sshKeepaliveRequestTimeout):
+               return fmt.Errorf("keepalive request timed out after %s", 
sshKeepaliveRequestTimeout)
+       case <-ctx.Done():
+               return ctx.Err()
+       }
+}
+
 // getWorkspaceLicenseFiles returns a list of possible license files from the
 // workspace
 func getWorkspaceLicenseFiles(ctx context.Context, cfg *Config, extraFiles 
[]string) ([]string, error) {
@@ -1285,7 +1399,7 @@
                        kernel = kernelVar
                }
        } else if _, err := os.Stat(kernel); err != nil {
-               return "", fmt.Errorf("qemu: /boot/vmlinuz not found, specify a 
kernel path with env variable QEMU_KERNEL_IMAGE")
+               return "", fmt.Errorf("qemu: /boot/vmlinuz not found, specify a 
kernel path with env variable QEMU_KERNEL_IMAGE: %w", err)
        }
 
        return kernel, nil
@@ -1390,7 +1504,7 @@
                HostKeyCallback: ssh.FixedHostKey(cfg.VMHostKeyPublic),
        }
 
-       client, err := ssh.Dial("tcp", cfg.SSHAddress, config)
+       client, err := sshDialWithTimeout(cfg.SSHAddress, config, 
sshDialTimeout)
        if err != nil {
                clog.FromContext(ctx).Errorf("Failed to verify and connect to 
VM: %s", err)
                return fmt.Errorf("vm host key verification failed (possible 
security issue): %w", err)
@@ -1758,7 +1872,7 @@
 func randomPortN() (int, error) {
        l, err := net.Listen("tcp", "localhost:0")
        if err != nil {
-               return 0, fmt.Errorf("no open port found")
+               return 0, fmt.Errorf("no open port found: %w", err)
        }
        defer l.Close()
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/melange-0.45.1/pkg/container/qemu_ssh_test.go 
new/melange-0.45.3/pkg/container/qemu_ssh_test.go
--- old/melange-0.45.1/pkg/container/qemu_ssh_test.go   1970-01-01 
01:00:00.000000000 +0100
+++ new/melange-0.45.3/pkg/container/qemu_ssh_test.go   2026-03-11 
00:53:12.000000000 +0100
@@ -0,0 +1,438 @@
+// Copyright 2026 Chainguard, Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//     http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package container
+
+import (
+       "context"
+       "crypto/ecdsa"
+       "crypto/elliptic"
+       "crypto/rand"
+       "errors"
+       "net"
+       "sync/atomic"
+       "testing"
+       "testing/synctest"
+       "time"
+
+       "golang.org/x/crypto/ssh"
+)
+
+// mockKeepaliveClient implements sshKeepaliveClient for testing.
+type mockKeepaliveClient struct {
+       sendFunc    func() error
+       closed      atomic.Bool
+       closeCalled atomic.Int32
+}
+
+func (m *mockKeepaliveClient) SendRequest(name string, wantReply bool, payload 
[]byte) (bool, []byte, error) {
+       if m.closed.Load() {
+               return false, nil, errors.New("client closed")
+       }
+       if m.sendFunc != nil {
+               return false, nil, m.sendFunc()
+       }
+       return true, nil, nil
+}
+
+func (m *mockKeepaliveClient) Close() error {
+       m.closed.Store(true)
+       m.closeCalled.Add(1)
+       return nil
+}
+
+// newTestSSHServer starts an in-process SSH server that accepts connections 
and
+// completes the handshake. Returns the listener address and a cleanup 
function.
+func newTestSSHServer(t *testing.T) (string, func()) {
+       t.Helper()
+
+       hostKey, err := ecdsa.GenerateKey(elliptic.P256(), rand.Reader)
+       if err != nil {
+               t.Fatalf("failed to generate host key: %v", err)
+       }
+       hostSigner, err := ssh.NewSignerFromKey(hostKey)
+       if err != nil {
+               t.Fatalf("failed to create signer: %v", err)
+       }
+
+       serverConfig := &ssh.ServerConfig{
+               NoClientAuth: true,
+       }
+       serverConfig.AddHostKey(hostSigner)
+
+       listener, err := net.Listen("tcp", "127.0.0.1:0")
+       if err != nil {
+               t.Fatalf("failed to listen: %v", err)
+       }
+
+       done := make(chan struct{})
+       go func() {
+               defer close(done)
+               for {
+                       conn, err := listener.Accept()
+                       if err != nil {
+                               return
+                       }
+                       go func(c net.Conn) {
+                               defer c.Close()
+                               _, chans, reqs, err := ssh.NewServerConn(c, 
serverConfig)
+                               if err != nil {
+                                       return
+                               }
+                               go ssh.DiscardRequests(reqs)
+                               for newChan := range chans {
+                                       newChan.Reject(ssh.Prohibited, "no 
channels allowed")
+                               }
+                       }(conn)
+               }
+       }()
+
+       cleanup := func() {
+               listener.Close()
+               <-done
+       }
+
+       return listener.Addr().String(), cleanup
+}
+
+func TestSSHDialWithTimeout_Success(t *testing.T) {
+       addr, cleanup := newTestSSHServer(t)
+       defer cleanup()
+
+       config := &ssh.ClientConfig{
+               User:            "test",
+               HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+       }
+
+       client, err := sshDialWithTimeout(addr, config, 5*time.Second)
+       if err != nil {
+               t.Fatalf("sshDialWithTimeout failed: %v", err)
+       }
+       defer client.Close()
+}
+
+func TestSSHDialWithTimeout_ConnectRefused(t *testing.T) {
+       l, err := net.Listen("tcp", "127.0.0.1:0")
+       if err != nil {
+               t.Fatal(err)
+       }
+       addr := l.Addr().String()
+       l.Close()
+
+       config := &ssh.ClientConfig{
+               User:            "test",
+               HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+       }
+
+       _, err = sshDialWithTimeout(addr, config, 2*time.Second)
+       if err == nil {
+               t.Fatal("expected error dialing closed port")
+       }
+}
+
+func TestSSHDialWithTimeout_HandshakeHang(t *testing.T) {
+       // TCP listener that accepts but never speaks SSH
+       listener, err := net.Listen("tcp", "127.0.0.1:0")
+       if err != nil {
+               t.Fatal(err)
+       }
+       defer listener.Close()
+
+       go func() {
+               for {
+                       conn, err := listener.Accept()
+                       if err != nil {
+                               return
+                       }
+                       go func(c net.Conn) {
+                               buf := make([]byte, 1024)
+                               for {
+                                       if _, err := c.Read(buf); err != nil {
+                                               return
+                                       }
+                               }
+                       }(conn)
+               }
+       }()
+
+       config := &ssh.ClientConfig{
+               User:            "test",
+               HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+       }
+
+       timeout := 500 * time.Millisecond
+       start := time.Now()
+       _, err = sshDialWithTimeout(listener.Addr().String(), config, timeout)
+       elapsed := time.Since(start)
+
+       if err == nil {
+               t.Fatal("expected error on handshake hang")
+       }
+       if elapsed > 5*time.Second {
+               t.Errorf("sshDialWithTimeout took %v, expected ~%v", elapsed, 
timeout)
+       }
+}
+
+func TestSSHDialWithTimeout_ReturnsClient(t *testing.T) {
+       addr, cleanup := newTestSSHServer(t)
+       defer cleanup()
+
+       config := &ssh.ClientConfig{
+               User:            "test",
+               HostKeyCallback: ssh.InsecureIgnoreHostKey(),
+       }
+
+       client, err := sshDialWithTimeout(addr, config, 5*time.Second)
+       if err != nil {
+               t.Fatalf("dial failed: %v", err)
+       }
+       defer client.Close()
+
+       // Session open will fail (server rejects channels) but proves client 
works
+       _, err = client.NewSession()
+       if err == nil {
+               t.Fatal("expected session rejection from test server")
+       }
+}
+
+func TestSSHSendKeepalive_Success(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               mock := &mockKeepaliveClient{}
+               ctx := t.Context()
+
+               if err := sshSendKeepalive(ctx, mock); err != nil {
+                       t.Fatalf("sshSendKeepalive failed on healthy client: 
%v", err)
+               }
+       })
+}
+
+func TestSSHSendKeepalive_Error(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               mock := &mockKeepaliveClient{
+                       sendFunc: func() error {
+                               return errors.New("connection reset")
+                       },
+               }
+               ctx := t.Context()
+
+               if err := sshSendKeepalive(ctx, mock); err == nil {
+                       t.Fatal("expected error from failing client")
+               }
+       })
+}
+
+func TestSSHSendKeepalive_Timeout(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               // SendRequest blocks until released, simulating a hung 
connection
+               block := make(chan struct{})
+               mock := &mockKeepaliveClient{
+                       sendFunc: func() error {
+                               <-block
+                               return errors.New("unblocked")
+                       },
+               }
+               ctx := t.Context()
+
+               err := sshSendKeepalive(ctx, mock)
+               if err == nil {
+                       t.Fatal("expected timeout error")
+               }
+               t.Logf("got expected error: %v", err)
+
+               // Unblock the leaked goroutine so synctest can clean up
+               close(block)
+               synctest.Wait()
+       })
+}
+
+func TestSSHSendKeepalive_ContextCanceled(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               // SendRequest blocks until released
+               block := make(chan struct{})
+               mock := &mockKeepaliveClient{
+                       sendFunc: func() error {
+                               <-block
+                               return errors.New("unblocked")
+                       },
+               }
+               ctx, cancel := context.WithCancel(t.Context())
+
+               // Cancel after 1s (before the 10s request timeout)
+               go func() {
+                       time.Sleep(1 * time.Second)
+                       cancel()
+               }()
+
+               err := sshSendKeepalive(ctx, mock)
+               if !errors.Is(err, context.Canceled) {
+                       t.Fatalf("expected context.Canceled, got: %v", err)
+               }
+
+               // Unblock the leaked goroutine so synctest can clean up
+               close(block)
+               synctest.Wait()
+       })
+}
+
+func TestRunSSHKeepalive_SendsAtInterval(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               var count atomic.Int32
+               mock := &mockKeepaliveClient{
+                       sendFunc: func() error {
+                               count.Add(1)
+                               return nil
+                       },
+               }
+
+               ctx, cancel := context.WithCancel(t.Context())
+               go runSSHKeepalive(ctx, mock, "test")
+
+               // Advance past 3 intervals (30s each = 90s)
+               time.Sleep(sshKeepaliveInterval*3 + time.Nanosecond)
+               synctest.Wait()
+
+               got := count.Load()
+               if got != 3 {
+                       t.Errorf("expected 3 keepalives after 3 intervals, got 
%d", got)
+               }
+
+               cancel()
+               synctest.Wait()
+       })
+}
+
+func TestRunSSHKeepalive_ClosesAfterMaxMissed(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               mock := &mockKeepaliveClient{
+                       sendFunc: func() error {
+                               return errors.New("connection reset")
+                       },
+               }
+
+               go runSSHKeepalive(t.Context(), mock, "test")
+
+               // After sshKeepaliveMaxMissed intervals, the client should be 
closed.
+               // Each tick is 30s; need 3 ticks for 3 misses.
+               
time.Sleep(sshKeepaliveInterval*time.Duration(sshKeepaliveMaxMissed) + 
time.Nanosecond)
+               synctest.Wait()
+
+               if !mock.closed.Load() {
+                       t.Fatal("expected client to be closed after max missed 
keepalives")
+               }
+               if got := mock.closeCalled.Load(); got != 1 {
+                       t.Errorf("expected Close called once, got %d", got)
+               }
+       })
+}
+
+func TestRunSSHKeepalive_ResetsOnSuccess(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               var callCount atomic.Int32
+               mock := &mockKeepaliveClient{
+                       sendFunc: func() error {
+                               n := callCount.Add(1)
+                               // Fail on calls 1 and 2, succeed on 3, fail on 
4 and 5, succeed on 6...
+                               if n%3 != 0 {
+                                       return errors.New("temporary failure")
+                               }
+                               return nil
+                       },
+               }
+
+               ctx, cancel := context.WithCancel(t.Context())
+               go runSSHKeepalive(ctx, mock, "test")
+
+               // After 6 intervals: fail, fail, succeed, fail, fail, succeed
+               // The missed counter resets each time a keepalive succeeds,
+               // so we never hit maxMissed (3).
+               time.Sleep(sshKeepaliveInterval*6 + time.Nanosecond)
+               synctest.Wait()
+
+               if mock.closed.Load() {
+                       t.Fatal("client should not be closed — missed counter 
resets on success")
+               }
+
+               cancel()
+               synctest.Wait()
+       })
+}
+
+func TestRunSSHKeepalive_StopsOnContextCancel(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               var count atomic.Int32
+               mock := &mockKeepaliveClient{
+                       sendFunc: func() error {
+                               count.Add(1)
+                               return nil
+                       },
+               }
+
+               ctx, cancel := context.WithCancel(t.Context())
+               go runSSHKeepalive(ctx, mock, "test")
+
+               // Let one keepalive fire
+               time.Sleep(sshKeepaliveInterval + time.Nanosecond)
+               synctest.Wait()
+
+               if got := count.Load(); got != 1 {
+                       t.Fatalf("expected 1 keepalive before cancel, got %d", 
got)
+               }
+
+               // Cancel and verify no more keepalives fire
+               cancel()
+               synctest.Wait()
+
+               countAfterCancel := count.Load()
+               time.Sleep(sshKeepaliveInterval * 3)
+               synctest.Wait()
+
+               if got := count.Load(); got != countAfterCancel {
+                       t.Errorf("keepalives continued after cancel: had %d, 
now %d", countAfterCancel, got)
+               }
+
+               if mock.closed.Load() {
+                       t.Error("client should not be closed on context cancel")
+               }
+       })
+}
+
+func TestRunSSHKeepalive_HungRequestCountsAsMiss(t *testing.T) {
+       synctest.Test(t, func(t *testing.T) {
+               // SendRequest blocks until released — each keepalive should 
time out
+               // after sshKeepaliveRequestTimeout and count as a miss.
+               block := make(chan struct{})
+               mock := &mockKeepaliveClient{
+                       sendFunc: func() error {
+                               <-block
+                               return errors.New("unblocked")
+                       },
+               }
+
+               go runSSHKeepalive(t.Context(), mock, "test")
+
+               // Each keepalive tick (30s) starts a request that times out 
after 10s,
+               // so each tick takes ~30s from the ticker's perspective (the 
timeout
+               // completes within the interval). After 3 ticks, client should 
close.
+               
time.Sleep(sshKeepaliveInterval*time.Duration(sshKeepaliveMaxMissed) + 
sshKeepaliveRequestTimeout + time.Nanosecond)
+               synctest.Wait()
+
+               if !mock.closed.Load() {
+                       t.Fatal("expected client to be closed after hung 
requests exceed max missed")
+               }
+
+               // Unblock leaked goroutines so synctest can clean up
+               close(block)
+               synctest.Wait()
+       })
+}

++++++ melange.obsinfo ++++++
--- /var/tmp/diff_new_pack.Sh5Z5m/_old  2026-03-11 20:57:28.987277840 +0100
+++ /var/tmp/diff_new_pack.Sh5Z5m/_new  2026-03-11 20:57:29.031279655 +0100
@@ -1,5 +1,5 @@
 name: melange
-version: 0.45.1
-mtime: 1773078090
-commit: 049664c63b543602ba8e344d2ebf0bdd950f3428
+version: 0.45.3
+mtime: 1773186792
+commit: 2a226faac364a05b188a3afcbbdc45b35b395ff3
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/melange/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.melange.new.8177/vendor.tar.gz differ: char 133, 
line 1

Reply via email to