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-10-27 16:59:09 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/melange (Old) and /work/SRC/openSUSE:Factory/.melange.new.2252680 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "melange" Tue Oct 27 16:59:09 2026 rev:187 rq:1392963 version:0.63.2 Changes: -------- --- /work/SRC/openSUSE:Factory/melange/melange.changes 2026-10-24 22:06:55.826404475 +0200 +++ /work/SRC/openSUSE:Factory/.melange.new.2252680/melange.changes 2026-10-27 17:00:00.091219098 +0100 @@ -1,0 +2,11 @@ +Tue Oct 27 06:31:28 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 0.63.2: + * perf(git-checkout): read verify_worktree's hashes in step with + the index (#2718) + * fix(pipelines): strip the whole userinfo from recorded source + URIs (#2717) + * fix(qemu): wait for SSH on a wall-clock deadline instead of a + try count (#2714) + +------------------------------------------------------------------- Old: ---- melange-0.63.1.obscpio New: ---- melange-0.63.2.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ melange.spec ++++++ --- /var/tmp/diff_new_pack.d65f8O/_old 2026-10-27 17:00:02.295311400 +0100 +++ /var/tmp/diff_new_pack.d65f8O/_new 2026-10-27 17:00:02.297311483 +0100 @@ -17,7 +17,7 @@ Name: melange -Version: 0.63.1 +Version: 0.63.2 Release: 0 Summary: Build APKs from source code License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.d65f8O/_old 2026-10-27 17:00:02.341313326 +0100 +++ /var/tmp/diff_new_pack.d65f8O/_new 2026-10-27 17:00:02.344313452 +0100 @@ -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.63.1</param> + <param name="revision">refs/tags/v0.63.2</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.d65f8O/_old 2026-10-27 17:00:02.379314917 +0100 +++ /var/tmp/diff_new_pack.d65f8O/_new 2026-10-27 17:00:02.383315085 +0100 @@ -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">04c0a4045867d1462842d383487f1f02f80ff7ac</param></service></servicedata> + <param name="changesrevision">402004649f817966440662612d0ea7302b95a858</param></service></servicedata> (No newline at EOF) ++++++ melange-0.63.1.obscpio -> melange-0.63.2.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.63.1/pkg/build/pipelines/fetch.yaml new/melange-0.63.2/pkg/build/pipelines/fetch.yaml --- old/melange-0.63.1/pkg/build/pipelines/fetch.yaml 2026-10-21 00:14:41.000000000 +0200 +++ new/melange-0.63.2/pkg/build/pipelines/fetch.yaml 2026-10-23 17:44:26.000000000 +0200 @@ -103,7 +103,7 @@ # bytes, so only a build that asked to restore from a companion -- # whose cache the pin in the binary package has verified -- looks; # an ordinary build with a cache it did not vet fetches as before. - uri=$(printf '%s\n' '${{inputs.uri}}' | sed -e 's|://[^/@]*@|://|' -e 's|[?#].*$||') + uri=$(printf '%s\n' '${{inputs.uri}}' | sed -e 's|://[^/?#]*@|://|' -e 's|[?#].*$||') for u in /var/cache/melange/sha256:*.uri; do [ -f "$u" ] || continue [ "$(head -n 1 "$u")" = "$uri" ] || continue @@ -153,7 +153,7 @@ cp "$bn" "$MELANGE_SOURCE_STASH/$key" # the URL as provenance: scheme, host and path only, since userinfo, # query and fragment are where credentials travel - printf '%s\n' '${{inputs.uri}}' | sed -e 's|://[^/@]*@|://|' -e 's|[?#].*$||' > "$MELANGE_SOURCE_STASH/$key.uri" + printf '%s\n' '${{inputs.uri}}' | sed -e 's|://[^/?#]*@|://|' -e 's|[?#].*$||' > "$MELANGE_SOURCE_STASH/$key.uri" printf "fetch: kept $key for the -source package\n" fi diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.63.1/pkg/build/pipelines/git-checkout.yaml new/melange-0.63.2/pkg/build/pipelines/git-checkout.yaml --- old/melange-0.63.1/pkg/build/pipelines/git-checkout.yaml 2026-10-21 00:14:41.000000000 +0200 +++ new/melange-0.63.2/pkg/build/pipelines/git-checkout.yaml 2026-10-23 17:44:26.000000000 +0200 @@ -349,7 +349,7 @@ # when detached, as a tag checkout leaves it): builds read .git/HEAD # the URL as provenance: scheme, host and path only, since userinfo, # query and fragment are where credentials travel - printf '%s\n%s\n%s\n%s\n' "$(printf '%s' "$repo" | sed -e 's|://[^/@]*@|://|' -e 's|[?#].*$||')" "$head" "$(git -C "$dest" log -1 --format=%ct)" \ + printf '%s\n%s\n%s\n%s\n' "$(printf '%s' "$repo" | sed -e 's|://[^/?#]*@|://|' -e 's|[?#].*$||')" "$head" "$(git -C "$dest" log -1 --format=%ct)" \ "$(git -C "$dest" symbolic-ref --short -q HEAD || true)" > "$stash/$key.uri" git -C "$dest" cat-file commit "$head" > "$stash/$key.commit" || rm -f "$stash/$key.commit" # the commits the checkout stacked on the expected one (cherry-picks) @@ -538,7 +538,7 @@ # would leave them, so status and describe --dirty read clean. Gitlinks # are checked by verify_submodules. verify_worktree() { - local entries="" paths="" raw="" clean="" tab="" bad="" assume="" n=0 line="" mode="" sha="" path="" r="" c="" + local entries="" paths="" raw="" clean="" tab="" bad="" assume="" line="" mode="" sha="" path="" r="" c="" tab=$(printf '\t') entries=$(mktemp); paths=$(mktemp); raw=$(mktemp); clean=$(mktemp); bad=$(mktemp); assume=$(mktemp) git -c core.quotePath=false ls-files -s > "$entries" @@ -547,13 +547,13 @@ git hash-object -w --no-filters --stdin-paths < "$paths" > "$raw" || return 1 git hash-object -w --stdin-paths < "$paths" > "$clean" || return 1 fi - n=0 + # line i of raw and clean hashes the i-th regular file, so read + # them in step with the entries while IFS="$tab" read -r meta path; do mode=${meta%% *}; sha=${meta#* }; sha=${sha%% *} case "$mode" in 100644|100755) - n=$((n + 1)) - r=$(sed -n "${n}p" "$raw"); c=$(sed -n "${n}p" "$clean") + r=""; c=""; read -r r <&3 || :; read -r c <&4 || : if [ "$r" = "$sha" ]; then [ "$c" = "$sha" ] || printf '%s\n' "$path" >> "$assume" elif [ "$c" != "$sha" ]; then @@ -563,7 +563,7 @@ [ "$(printf '%s' "$(readlink "$path")" | git hash-object -w --stdin)" = "$sha" ] || printf '%s\n' "$path" >> "$bad" ;; esac - done < "$entries" + done < "$entries" 3< "$raw" 4< "$clean" # and nothing the tree does not name may be present: a git archive # holds exactly the tree, so anything else was put there git -c core.quotePath=false ls-files -o > "$paths" diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.63.1/pkg/build/source_uri_credentials_test.go new/melange-0.63.2/pkg/build/source_uri_credentials_test.go --- old/melange-0.63.1/pkg/build/source_uri_credentials_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/melange-0.63.2/pkg/build/source_uri_credentials_test.go 2026-10-23 17:44:26.000000000 +0200 @@ -0,0 +1,72 @@ +// 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 build + +import ( + "os/exec" + "regexp" + "strings" + "testing" + + "github.com/stretchr/testify/require" + + "chainguard.dev/melange/pkg/config" +) + +var uriCredentialStrip = regexp.MustCompile(`sed -e 's\|://[^']*\|://\|' -e 's\|\[\?#\]\.\*\$\|\|'`) + +// The by-URL lookup and the .uri records must normalise a URL identically, or +// a record stops matching its own lookup. A userinfo may contain '@', so the +// strip has to run to the last one in the authority. +func TestSourceURICredentialStrip(t *testing.T) { + t.Parallel() + if testing.Short() { + t.Skip("runs shell commands") + } + for _, tool := range []string{"sh", "sed"} { + if _, err := exec.LookPath(tool); err != nil { + t.Skipf("%s not available", tool) + } + } + + found := make([]string, 0, 3) + for _, p := range []config.Pipeline{ + {Uses: "fetch", With: map[string]string{"uri": "https://h/x", "expected-sha256": strings.Repeat("0", 64)}}, + {Uses: "git-checkout", With: map[string]string{"repository": "https://h/r", "destination": "d"}}, + } { + sm := &SubstitutionMap{Substitutions: map[string]string{ + config.SubstitutionPackageName: "hello", config.SubstitutionPackageVersion: "1.0", + }} + require.NoError(t, (&Compiled{}).compilePipeline(t.Context(), sm, &p, nil)) + found = append(found, uriCredentialStrip.FindAllString(p.Pipeline[0].Runs, -1)...) + } + require.Len(t, found, 3, "fetch lookup, fetch record, git-checkout record") + require.Equal(t, found[0], found[1]) + require.Equal(t, found[0], found[2]) + + for in, want := range map[string]string{ + "https://u:p@ss@h/x?t=1": "https://h/x", + "https://u:pw@h/x": "https://h/x", + "https://h/@scope/pkg.tgz": "https://h/@scope/pkg.tgz", + "https://h?x=a@b": "https://h", + "https://u:p@[::1]:8080/x": "https://[::1]:8080/x", + "ssh://git@h:22/r": "ssh://h:22/r", + "git@h:o/r": "git@h:o/r", + } { + out, err := exec.CommandContext(t.Context(), "sh", "-c", `printf '%s\n' "$1" | `+found[0], "sh", in).CombinedOutput() + require.NoError(t, err, string(out)) + require.Equal(t, want+"\n", string(out), in) + } +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.63.1/pkg/container/qemu_runner.go new/melange-0.63.2/pkg/container/qemu_runner.go --- old/melange-0.63.1/pkg/container/qemu_runner.go 2026-10-21 00:14:41.000000000 +0200 +++ new/melange-0.63.2/pkg/container/qemu_runner.go 2026-10-23 17:44:26.000000000 +0200 @@ -83,6 +83,13 @@ // sshKeepaliveRequestTimeout is how long to wait for a single keepalive // response before counting it as missed. sshKeepaliveRequestTimeout = 10 * time.Second + + // defaultSSHWaitTimeout is how long to wait for the guest's SSH server + // to come up after starting the VM. Before sshd starts, the guest + // extracts the whole rootfs tarball onto its scratch disk, so large + // build/test environments take well over a minute to boot. Can be + // overridden with QEMU_SSH_WAIT_TIMEOUT. + defaultSSHWaitTimeout = 5 * time.Minute ) type qemu struct{} @@ -633,6 +640,15 @@ func createMicroVM(ctx context.Context, cfg *Config) error { log := clog.FromContext(ctx) + + // Validate the SSH wait timeout before any side effects, so a bad + // QEMU_SSH_WAIT_TIMEOUT fails the VM start instead of leaking a + // running VM and its disks. + sshWaitTimeout, err := sshWaitTimeoutFromEnv() + if err != nil { + return err + } + log.Debug("qemu: ssh - create ssh key pair") pubKey, err := generateSSHKeys(ctx, cfg) if err != nil { @@ -957,7 +973,11 @@ // qemu-system-x86_64 or qemu-system-aarch64... // #nosec G204 - Architecture is from validated configuration, not user input qemuCmd := exec.CommandContext(ctx, fmt.Sprintf("qemu-system-%s", cfg.Arch.ToAPK()), baseargs...) - clog.FromContext(ctx).Infof("qemu: VM resources: arch=%s cpus=%d memory=%dMiB", cfg.Arch.ToAPK(), nproc, mem/1024) + rootfsSize := int64(0) + if fi, err := os.Stat(cfg.ImgRef); err == nil { + rootfsSize = fi.Size() + } + clog.FromContext(ctx).Infof("qemu: VM resources: arch=%s cpus=%d memory=%dMiB rootfs=%dMiB", cfg.Arch.ToAPK(), nproc, mem/1024, rootfsSize/1024/1024) clog.FromContext(ctx).Info("qemu: starting VM") clog.FromContext(ctx).Debugf("qemu: executing - %s", strings.Join(qemuCmd.Args, " ")) @@ -1011,12 +1031,14 @@ started := make(chan struct{}) - clog.FromContext(ctx).Info("qemu: waiting for SSH") + log.Infof("qemu: waiting for SSH (timeout %s)", sshWaitTimeout) + sshWaitStart := time.Now() go func() { - // 100ms timeout with a 100ms sleep - retries := 60 + deadline := sshWaitStart.Add(sshWaitTimeout) try := 0 - for try < retries { + var lastErr error + lastHeartbeat := sshWaitStart + for time.Now().Before(deadline) { if logCtx.Err() != nil { return } @@ -1024,22 +1046,31 @@ try++ time.Sleep(time.Millisecond * 100) - log.Debugf("qemu: waiting for ssh to come up, try %d of %d", try, retries) - err = checkSSHServer(cfg.SSHAddress) - if err == nil { + log.Debugf("qemu: waiting for ssh to come up, try %d", try) + if err := checkSSHServer(cfg.SSHAddress); err == nil { close(started) return } else { - log.Info(err.Error()) + log.Debug(err.Error()) + lastErr = err + } + + if time.Since(lastHeartbeat) >= 30*time.Second { + log.Infof("qemu: still waiting for SSH after %.0fs (last probe error: %s)", time.Since(sshWaitStart).Seconds(), lastErr) + lastHeartbeat = time.Now() } } - cancel(fmt.Errorf("ssh server never came up")) + if lastErr != nil { + cancel(fmt.Errorf("ssh server never came up within %s (last probe error: %w)", sshWaitTimeout, lastErr)) + } else { + cancel(fmt.Errorf("ssh server never came up within %s", sshWaitTimeout)) + } }() select { case <-started: - log.Info("qemu: VM started successfully, SSH server is up") + log.Infof("qemu: VM started successfully, SSH server is up after %.1fs", time.Since(sshWaitStart).Seconds()) case err := <-qemuExit: defer os.Remove(cfg.ImgRef) defer os.Remove(cfg.Disk) @@ -1522,6 +1553,24 @@ return diskName.Name(), os.Truncate(diskName.Name(), size) } +// sshWaitTimeoutFromEnv returns how long to wait for the guest's SSH +// server to come up, honoring the QEMU_SSH_WAIT_TIMEOUT environment +// variable (a time.ParseDuration string, e.g. "90s" or "10m"). +func sshWaitTimeoutFromEnv() (time.Duration, error) { + v, ok := os.LookupEnv("QEMU_SSH_WAIT_TIMEOUT") + if !ok || v == "" { + return defaultSSHWaitTimeout, nil + } + d, err := time.ParseDuration(v) + if err == nil && d <= 0 { + err = errors.New("must be positive") + } + if err != nil { + return 0, fmt.Errorf("invalid QEMU_SSH_WAIT_TIMEOUT value %q: %w", v, err) + } + return d, nil +} + // qemu will open the port way before the ssh daemon is ready to listen // so we need to check if we get the SSH banner in order to value if the server // is up or not. diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/melange-0.63.1/pkg/container/qemu_runner_test.go new/melange-0.63.2/pkg/container/qemu_runner_test.go --- old/melange-0.63.1/pkg/container/qemu_runner_test.go 2026-10-21 00:14:41.000000000 +0200 +++ new/melange-0.63.2/pkg/container/qemu_runner_test.go 2026-10-23 17:44:26.000000000 +0200 @@ -25,6 +25,7 @@ "path/filepath" "runtime" "testing" + "time" "github.com/chainguard-dev/clog" "github.com/chainguard-dev/clog/slogtest" @@ -1233,4 +1234,87 @@ } }) } +} + +func TestSSHWaitTimeoutFromEnv(t *testing.T) { + tests := []struct { + name string + envValue string + envSet bool + expectTimeout time.Duration + expectError bool + }{ + { + name: "env not set", + envSet: false, + expectTimeout: defaultSSHWaitTimeout, + }, + { + name: "env set to empty string", + envValue: "", + envSet: true, + expectTimeout: defaultSSHWaitTimeout, + }, + { + name: "env set to seconds", + envValue: "90s", + envSet: true, + expectTimeout: 90 * time.Second, + }, + { + name: "env set to minutes", + envValue: "10m", + envSet: true, + expectTimeout: 10 * time.Minute, + }, + { + name: "env set to invalid value", + envValue: "invalid", + envSet: true, + expectError: true, + }, + { + name: "env set to bare number", + envValue: "300", + envSet: true, + expectError: true, + }, + { + name: "env set to zero", + envValue: "0s", + envSet: true, + expectError: true, + }, + { + name: "env set to negative duration", + envValue: "-30s", + envSet: true, + expectError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if tt.envSet { + t.Setenv("QEMU_SSH_WAIT_TIMEOUT", tt.envValue) + } else { + // t.Setenv registers the cleanup that restores the + // original value after the subtest. + t.Setenv("QEMU_SSH_WAIT_TIMEOUT", "") + os.Unsetenv("QEMU_SSH_WAIT_TIMEOUT") + } + + timeout, err := sshWaitTimeoutFromEnv() + + if tt.expectError && err == nil { + t.Error("expected error but got nil") + } + if !tt.expectError && err != nil { + t.Errorf("unexpected error: %v", err) + } + if !tt.expectError && timeout != tt.expectTimeout { + t.Errorf("sshWaitTimeoutFromEnv() = %v, expected %v", timeout, tt.expectTimeout) + } + }) + } } ++++++ melange.obsinfo ++++++ --- /var/tmp/diff_new_pack.d65f8O/_old 2026-10-27 17:00:04.320396205 +0100 +++ /var/tmp/diff_new_pack.d65f8O/_new 2026-10-27 17:00:04.329396582 +0100 @@ -1,5 +1,5 @@ name: melange -version: 0.63.1 -mtime: 1792534481 -commit: 04c0a4045867d1462842d383487f1f02f80ff7ac +version: 0.63.2 +mtime: 1792770266 +commit: 402004649f817966440662612d0ea7302b95a858 ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/melange/vendor.tar.gz /work/SRC/openSUSE:Factory/.melange.new.2252680/vendor.tar.gz differ: char 30, line 1
