Script 'mail_helper' called by obssrc Hello community, here is the log from the commit of package werf for openSUSE:Factory checked in at 2026-07-31 15:29:42 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Comparing /work/SRC/openSUSE:Factory/werf (Old) and /work/SRC/openSUSE:Factory/.werf.new.2004 (New) ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Package is "werf" Fri Jul 31 15:29:42 2026 rev:80 rq:1368472 version:2.74.0 Changes: -------- --- /work/SRC/openSUSE:Factory/werf/werf.changes 2026-07-21 23:11:41.260938246 +0200 +++ /work/SRC/openSUSE:Factory/.werf.new.2004/werf.changes 2026-07-31 16:10:55.811990786 +0200 @@ -1,0 +2,17 @@ +Thu Jul 30 05:35:05 UTC 2026 - Johannes Kastl <[email protected]> + +- Update to version 2.74.0: + * Features + - deploy: add werf.io/resource-policy annotation (#7613) + (72ae0f2) + * Bug Fixes + - build: forward include.path to submodule sync/update commands + (#7660) (5ab32d2) + - build: send valid empty tar for docker from:scratch import + (#7609) (1c59585) + - bundle: update .Values.global.werf.images during bundle copy + (#7600) (cf2fb96) + - deploy: werf.io/resource-policy should only respect + skip-delete from live (#7623) (f261028) + +------------------------------------------------------------------- Old: ---- werf-2.73.2.obscpio New: ---- werf-2.74.0.obscpio ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ Other differences: ------------------ ++++++ werf.spec ++++++ --- /var/tmp/diff_new_pack.CYJ1Gd/_old 2026-07-31 16:10:56.544016251 +0200 +++ /var/tmp/diff_new_pack.CYJ1Gd/_new 2026-07-31 16:10:56.548016389 +0200 @@ -17,7 +17,7 @@ Name: werf -Version: 2.73.2 +Version: 2.74.0 Release: 0 Summary: CLI for the Werf CI/CD system License: Apache-2.0 ++++++ _service ++++++ --- /var/tmp/diff_new_pack.CYJ1Gd/_old 2026-07-31 16:10:56.592017920 +0200 +++ /var/tmp/diff_new_pack.CYJ1Gd/_new 2026-07-31 16:10:56.604018338 +0200 @@ -3,7 +3,7 @@ <param name="url">https://github.com/werf/werf.git</param> <param name="scm">git</param> <param name="exclude">.git</param> - <param name="revision">refs/tags/v2.73.2</param> + <param name="revision">refs/tags/v2.74.0</param> <param name="versionformat">@PARENT_TAG@</param> <param name="versionrewrite-pattern">v(.*)</param> <param name="changesgenerate">enable</param> ++++++ _servicedata ++++++ --- /var/tmp/diff_new_pack.CYJ1Gd/_old 2026-07-31 16:10:56.636019451 +0200 +++ /var/tmp/diff_new_pack.CYJ1Gd/_new 2026-07-31 16:10:56.648019868 +0200 @@ -3,6 +3,6 @@ <param name="url">https://github.com/werf/werf</param> <param name="changesrevision">508fb9aee29baca228674ba7a3d2d4fa1f5844d5</param></service><service name="tar_scm"> <param name="url">https://github.com/werf/werf.git</param> - <param name="changesrevision">0b7ec198dac3e05320e8d77cd17492ed5fc18750</param></service></servicedata> + <param name="changesrevision">7585cfbf15796fbe86e33df695a3c5259aa40382</param></service></servicedata> (No newline at EOF) ++++++ vendor.tar.gz ++++++ /work/SRC/openSUSE:Factory/werf/vendor.tar.gz /work/SRC/openSUSE:Factory/.werf.new.2004/vendor.tar.gz differ: char 13, line 1 ++++++ werf-2.73.2.obscpio -> werf-2.74.0.obscpio ++++++ diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/.agents/skills/pull-request-name/SKILL.md new/werf-2.74.0/.agents/skills/pull-request-name/SKILL.md --- old/werf-2.73.2/.agents/skills/pull-request-name/SKILL.md 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/.agents/skills/pull-request-name/SKILL.md 2026-07-14 22:07:35.000000000 +0200 @@ -1,15 +1,56 @@ --- name: pull-request-name -description: Generates Pull Request titles according to werf conventions. Use when creating or updating a PR. +description: Generates Pull Request titles and descriptions according to werf conventions. Use when creating or updating a PR. --- -# Pull Request Naming +# Pull Request Conventions -## Instructions +## Defaults + +- Always create PRs as draft (`gh pr create --draft`). The author marks it ready for review manually. + +## Title 1. Read types, scopes, and formatting rules from `CONTRIBUTING.md#conventions`. 2. The PR title should mirror the header of the main commit in the PR. 3. Format the title as `<type>(<scope>): <subject>`. 4. Keep the total length ≤ 72 characters. 5. Nested scopes are allowed and encouraged. -6. Output ONLY the title, with no additional text, quotes, or formatting. \ No newline at end of file + +## Description + +Use the following structure. Every section is **mandatory** — omit a section only when it genuinely does not apply (e.g. single-line typo fix may skip *Review focus / risks*). + +``` +## Summary + +<1-3 sentence high-level overview of what the PR does and why it exists.> + +## Key changes + +- <concrete change 1> +- <concrete change 2> +- … + +## Why + +<Motivation: what problem this solves, what maintenance/UX/perf gain it brings.> + +## Review focus / risks + +- <area or file that deserves careful review> +- <potential risk or side-effect> +``` + +### Rules + +- Language: match the project's primary language (English by default). +- Be specific: name files, modules, functions — not "updated some code". +- *Key changes*: group related items; use sub-bullets for detail when helpful. +- *Why*: explain the reason, not what changed (that's *Key changes*). +- *Review focus / risks*: guide the reviewer — call out non-obvious consequences, large generated diffs, breaking changes. +- No AI-slop filler ("This PR improves the codebase…"). Every sentence must carry information. + +## Output + +When generating only the title (e.g. for `gh pr edit --title`), output ONLY the title, with no additional text, quotes, or formatting. \ No newline at end of file diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/CHANGELOG.md new/werf-2.74.0/CHANGELOG.md --- old/werf-2.73.2/CHANGELOG.md 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/CHANGELOG.md 2026-07-14 22:07:35.000000000 +0200 @@ -1,5 +1,20 @@ # Changelog +## [2.74.0](https://github.com/werf/werf/compare/v2.73.2...v2.74.0) (2026-07-14) + + +### Features + +* **deploy:** add `werf.io/resource-policy` annotation ([#7613](https://github.com/werf/werf/issues/7613)) ([72ae0f2](https://github.com/werf/werf/commit/72ae0f24ea745a81c61656d1c25df8dace2538f6)) + + +### Bug Fixes + +* **build:** forward include.path to submodule sync/update commands ([#7660](https://github.com/werf/werf/issues/7660)) ([5ab32d2](https://github.com/werf/werf/commit/5ab32d2fd7598bfcdb598f1f1217138e15a9e4b2)) +* **build:** send valid empty tar for docker from:scratch import ([#7609](https://github.com/werf/werf/issues/7609)) ([1c59585](https://github.com/werf/werf/commit/1c5958506a960167c76dede2cfa88ed130db1d12)) +* **bundle:** update .Values.global.werf.images during bundle copy ([#7600](https://github.com/werf/werf/issues/7600)) ([cf2fb96](https://github.com/werf/werf/commit/cf2fb96d2de01c13d8c733589cb02262eb9521b1)) +* **deploy:** `werf.io/resource-policy` should only respect skip-delete from live ([#7623](https://github.com/werf/werf/issues/7623)) ([f261028](https://github.com/werf/werf/commit/f261028ca4b92b48ef9e26c353c0adade1e331ea)) + ## [2.73.2](https://github.com/werf/werf/compare/v2.73.1...v2.73.2) (2026-07-01) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/go.mod new/werf-2.74.0/go.mod --- old/werf-2.73.2/go.mod 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/go.mod 2026-07-14 22:07:35.000000000 +0200 @@ -65,7 +65,7 @@ github.com/werf/kubedog-for-werf-helm v0.0.0-20241217155728-9d45c48b82b6 github.com/werf/lockgate v0.1.1 github.com/werf/logboek v0.7.1 - github.com/werf/nelm v1.24.4-0.20260618090926-7f5713b27219 + github.com/werf/nelm v1.26.1-0.20260706091654-ddfb620f23e2 github.com/werf/nelm-for-werf-helm v0.0.0-20241217155925-b0e6734d1dbf go.opentelemetry.io/otel v1.24.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.24.0 diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/go.sum new/werf-2.74.0/go.sum --- old/werf-2.73.2/go.sum 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/go.sum 2026-07-14 22:07:35.000000000 +0200 @@ -1416,8 +1416,8 @@ github.com/werf/lockgate v0.1.1/go.mod h1:0yIFSLq9ausy6ejNxF5uUBf/Ib6daMAfXuCaTMZJzIE= github.com/werf/logboek v0.7.1 h1:Ck8L7LVmj/wyrW/jk+EkkpGwdmpkOrsWAwoIgLHupc8= github.com/werf/logboek v0.7.1/go.mod h1:Gez5J4bxekyr6MxTmIJyId1F61rpO+0/V4vjCIEIZmk= -github.com/werf/nelm v1.24.4-0.20260618090926-7f5713b27219 h1:wZjBQJnQPMZRdhtS1/4vOQfuoFSybY1BHtbkY+AUg+E= -github.com/werf/nelm v1.24.4-0.20260618090926-7f5713b27219/go.mod h1:D3uU5e1dSBc0l0oo/iB6SjM2sLaXICqRDSWXAYF+vpk= +github.com/werf/nelm v1.26.1-0.20260706091654-ddfb620f23e2 h1:k72lZ9gEm459QHVhdDfz7Uxp8FJdlp9sBJjPKfJFISc= +github.com/werf/nelm v1.26.1-0.20260706091654-ddfb620f23e2/go.mod h1:D3uU5e1dSBc0l0oo/iB6SjM2sLaXICqRDSWXAYF+vpk= github.com/werf/nelm-for-werf-helm v0.0.0-20241217155925-b0e6734d1dbf h1:K51qz209c1yJgKzPw8AeS72T21F/ACp0VI3RJvT4THA= github.com/werf/nelm-for-werf-helm v0.0.0-20241217155925-b0e6734d1dbf/go.mod h1:7RJXSGPKKPEvfPqrTwNA8jT7y52O0ebwhSbSn29ESMA= github.com/xanzy/go-gitlab v0.31.0/go.mod h1:sPLojNBn68fMUWSxIJtdVVIP8uSBYqesTfDUseX11Ug= diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/pkg/deploy/bundles/copy_test.go new/werf-2.74.0/pkg/deploy/bundles/copy_test.go --- old/werf-2.73.2/pkg/deploy/bundles/copy_test.go 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/pkg/deploy/bundles/copy_test.go 2026-07-14 22:07:35.000000000 +0200 @@ -442,6 +442,118 @@ Expect(registryClient.ImagesByReference["registry2.example.com/group2/testproject2:tag-3"]).To(Equal([]byte(`image-3-bytes`))) } }) + + It("should copy remote to remote updating global werf values", func() { + ch := &chart.Chart{ + Metadata: &chart.Metadata{ + APIVersion: "v2", + Name: "testproject", + Version: "1.2.3", + Type: "application", + }, + Values: map[string]interface{}{ + "werf": map[string]interface{}{ + "image": map[string]interface{}{ + "myapp": "registry.example.com/group/testproject:tag-1", + }, + "repo": "registry.example.com/group/testproject", + }, + "global": map[string]interface{}{ + "werf": map[string]interface{}{ + "repo": "registry.example.com/group/testproject", + "images": map[string]interface{}{ + "myapp": map[string]interface{}{ + "registry": "registry.example.com", + "namespace": "group", + "name": "testproject", + "tag": "tag-1", + "digest": "sha256:abcdef1234567890", + "tag_digest": "tag-1@sha256:abcdef1234567890", + "image": "registry.example.com/group/testproject", + "repository": "group/testproject", + "ref": "registry.example.com/group/testproject:tag-1@sha256:abcdef1234567890", + "ref_tag": "registry.example.com/group/testproject:tag-1", + "repository_ref": "group/testproject:tag-1@sha256:abcdef1234567890", + "repository_tag": "group/testproject:tag-1", + "name_ref": "testproject:tag-1@sha256:abcdef1234567890", + "name_tag": "testproject:tag-1", + }, + }, + }, + }, + }, + Raw: []*chart.File{ + { + Name: "values.yaml", + Data: []byte(` +werf: + image: + myapp: registry.example.com/group/testproject:tag-1 + repo: registry.example.com/group/testproject +global: + werf: + repo: registry.example.com/group/testproject + images: + myapp: + registry: registry.example.com + namespace: group + name: testproject + tag: tag-1 + digest: "sha256:abcdef1234567890" + tag_digest: "tag-1@sha256:abcdef1234567890" + image: registry.example.com/group/testproject + repository: group/testproject + ref: "registry.example.com/group/testproject:tag-1@sha256:abcdef1234567890" + ref_tag: "registry.example.com/group/testproject:tag-1" + repository_ref: "group/testproject:tag-1@sha256:abcdef1234567890" + repository_tag: "group/testproject:tag-1" + name_ref: "testproject:tag-1@sha256:abcdef1234567890" + name_tag: "testproject:tag-1" +`), + }, + }, + } + + bundlesRegistryClient := NewBundlesRegistryClientStub() + registryClient := NewDockerRegistryStub() + + fromAddr, err := bundles_registry.ParseAddr("registry.example.com/group/testproject:1.2.3") + Expect(err).NotTo(HaveOccurred()) + from := NewRemoteBundle(fromAddr.RegistryAddress, bundlesRegistryClient, registryClient) + bundlesRegistryClient.StubCharts[fromAddr.RegistryAddress.FullName()] = ch + registryClient.ImagesByReference["registry.example.com/group/testproject:tag-1"] = []byte(`image-1-bytes`) + + toAddr, err := bundles_registry.ParseAddr("registry2.example.com/org/newproject:4.5.6") + Expect(err).NotTo(HaveOccurred()) + to := NewRemoteBundle(toAddr.RegistryAddress, bundlesRegistryClient, registryClient) + + Expect(from.CopyTo(ctx, to, copyToOptions{})).To(Succeed()) + + newCh := bundlesRegistryClient.StubCharts[toAddr.RegistryAddress.FullName()] + Expect(newCh).NotTo(BeNil()) + + globalVals := newCh.Values["global"].(map[string]interface{}) + globalWerfVals := globalVals["werf"].(map[string]interface{}) + Expect(globalWerfVals["repo"]).To(Equal("registry2.example.com/org/newproject")) + + imagesVals := globalWerfVals["images"].(map[string]interface{}) + myappVals := imagesVals["myapp"].(map[string]interface{}) + + Expect(myappVals["registry"]).To(Equal("registry2.example.com")) + Expect(myappVals["namespace"]).To(Equal("org")) + Expect(myappVals["name"]).To(Equal("newproject")) + Expect(myappVals["tag"]).To(Equal("tag-1")) + Expect(myappVals["digest"]).To(Equal("sha256:abcdef1234567890")) + Expect(myappVals["tag_digest"]).To(Equal("tag-1@sha256:abcdef1234567890")) + Expect(myappVals["image"]).To(Equal("registry2.example.com/org/newproject")) + Expect(myappVals["repository"]).To(Equal("org/newproject")) + Expect(myappVals["ref"]).To(Equal("registry2.example.com/org/newproject:tag-1@sha256:abcdef1234567890")) + Expect(myappVals["ref_tag"]).To(Equal("registry2.example.com/org/newproject:tag-1")) + Expect(myappVals["repository_ref"]).To(Equal("org/newproject:tag-1@sha256:abcdef1234567890")) + Expect(myappVals["repository_tag"]).To(Equal("org/newproject:tag-1")) + Expect(myappVals["name_ref"]).To(Equal("newproject:tag-1@sha256:abcdef1234567890")) + Expect(myappVals["name_tag"]).To(Equal("newproject:tag-1")) + }) }) type BundleArchiveStubReader struct { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/pkg/deploy/bundles/global_values.go new/werf-2.74.0/pkg/deploy/bundles/global_values.go --- old/werf-2.73.2/pkg/deploy/bundles/global_values.go 1970-01-01 01:00:00.000000000 +0100 +++ new/werf-2.74.0/pkg/deploy/bundles/global_values.go 2026-07-14 22:07:35.000000000 +0200 @@ -0,0 +1,49 @@ +package bundles + +import ( + "fmt" + + "github.com/werf/werf/v2/pkg/image" +) + +func updateGlobalWerfValues(values map[string]interface{}, newRepo string, newImageRefs map[string]string) error { + globalVals, ok := values["global"].(map[string]interface{}) + if !ok { + return nil + } + + werfVals, ok := globalVals["werf"].(map[string]interface{}) + if !ok { + return nil + } + + werfVals["repo"] = newRepo + + imagesVals, ok := werfVals["images"].(map[string]interface{}) + if !ok { + return nil + } + + for imageName, imageVals := range imagesVals { + vals, ok := imageVals.(map[string]interface{}) + if !ok { + continue + } + + newRef, hasRef := newImageRefs[imageName] + if !hasRef { + continue + } + + digest, _ := vals["digest"].(string) + + rebuilt, err := image.RebuildImageValuesMap(newRef, digest) + if err != nil { + return fmt.Errorf("update global werf image values for %q: %w", imageName, err) + } + + imagesVals[imageName] = rebuilt + } + + return nil +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/pkg/deploy/bundles/remote_bundle.go new/werf-2.74.0/pkg/deploy/bundles/remote_bundle.go --- old/werf-2.73.2/pkg/deploy/bundles/remote_bundle.go 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/pkg/deploy/bundles/remote_bundle.go 2026-07-14 22:07:35.000000000 +0200 @@ -89,6 +89,8 @@ return fmt.Errorf("unable to read chart from the bundle archive %q: %w", fromArchive.Reader.String(), err) } + newImageRefs := make(map[string]string) + if err := logboek.Context(ctx).LogProcess("Copy images from bundle archive").DoError(func() error { if werfVals, ok := ch.Values["werf"].(map[string]interface{}); ok { if imageVals, ok := werfVals["image"].(map[string]interface{}); ok { @@ -113,6 +115,7 @@ } newImageVals[imageName] = ref.FullName() + newImageRefs[imageName] = ref.FullName() } else { return fmt.Errorf("unexpected value .Values.werf.image.%s=%v", imageName, v) } @@ -129,6 +132,10 @@ return err } + if err := updateGlobalWerfValues(ch.Values, bundle.RegistryAddress.Repo, newImageRefs); err != nil { + return err + } + if err := SaveChartValues(ctx, ch); err != nil { return err } @@ -161,6 +168,8 @@ logboek.Context(ctx).Debug().LogF("Values before change (%v):\n%s\n---\n", err, d) } + newImageRefs := make(map[string]string) + if err := logboek.Context(ctx).LogProcess("Copy images from remote bundle").DoError(func() error { if werfVals, ok := ch.Values["werf"].(map[string]interface{}); ok { if imageVals, ok := werfVals["image"].(map[string]interface{}); ok { @@ -186,6 +195,7 @@ } newImageVals[imageName] = ref.FullName() + newImageRefs[imageName] = ref.FullName() } else { return fmt.Errorf("unexpected value .Values.werf.image.%s=%v", imageName, v) } @@ -201,6 +211,10 @@ return err } + if err := updateGlobalWerfValues(ch.Values, bundle.RegistryAddress.Repo, newImageRefs); err != nil { + return err + } + if err := SaveChartValues(ctx, ch); err != nil { return err } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/pkg/docker/image.go new/werf-2.74.0/pkg/docker/image.go --- old/werf-2.73.2/pkg/docker/image.go 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/pkg/docker/image.go 2026-07-14 22:07:35.000000000 +0200 @@ -1,6 +1,7 @@ package docker import ( + "archive/tar" "bytes" "fmt" "io" @@ -43,8 +44,42 @@ if opts.TargetPlatform != "" { importOpts.Platform = opts.TargetPlatform } - _, err := apiCli(ctx).ImageImport(ctx, types.ImageImportSource{SourceName: "-"}, ref, importOpts) - return err + + // The Docker daemon reads the request body as the rootfs tar when fromSrc is "-". + // A nil/empty body makes the daemon register a malformed empty layer that some + // consumers (e.g. `dive`, `docker save` readers) fail to parse on Linux, so we + // send a valid empty tar archive instead. This yields a single empty rootfs + // layer, which is fine for `from: scratch`: later stapel stages copy imports on + // top of it, and the empty layer is deterministic so it does not shift caching. + emptyTar, err := emptyTarArchive() + if err != nil { + return fmt.Errorf("unable to build empty rootfs tar for image %q: %w", ref, err) + } + + resp, err := apiCli(ctx).ImageImport(ctx, types.ImageImportSource{Source: emptyTar, SourceName: "-"}, ref, importOpts) + if err != nil { + return fmt.Errorf("unable to import image %q: %w", ref, err) + } + defer resp.Close() + + // Drain the response body so the daemon fully finalizes image creation before we return. + if _, err := io.Copy(io.Discard, resp); err != nil { + return fmt.Errorf("unable to read import response for image %q: %w", ref, err) + } + + return nil +} + +// emptyTarArchive returns a reader over a valid, empty tar archive (containing +// only the end-of-archive marker). It is used as the rootfs body for scratch +// images created via the Docker Engine import API. +func emptyTarArchive() (io.Reader, error) { + var buf bytes.Buffer + tw := tar.NewWriter(&buf) + if err := tw.Close(); err != nil { + return nil, fmt.Errorf("unable to close tar writer: %w", err) + } + return &buf, nil } func Images(ctx context.Context, options types.ImageListOptions) ([]types.ImageSummary, error) { diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/pkg/docker/image_test.go new/werf-2.74.0/pkg/docker/image_test.go --- old/werf-2.73.2/pkg/docker/image_test.go 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/pkg/docker/image_test.go 2026-07-14 22:07:35.000000000 +0200 @@ -1,6 +1,10 @@ package docker import ( + "archive/tar" + "bytes" + "io" + "github.com/docker/docker/api/types/filters" . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" @@ -41,4 +45,25 @@ "dff4d2bea666c3cdf6c24b6da06118a8a4f1658b1def9e50c55a1eb0c77eeaba", ), ) + + // emptyTarArchive provides the rootfs body sent to the Docker Engine import + // API for `from: scratch` images. A malformed/empty body was the root cause + // of unreadable scratch images on Linux, so the body must be a valid tar. + Describe("emptyTarArchive", func() { + It("returns a valid, non-nil, empty tar archive", func() { + r, err := emptyTarArchive() + Expect(err).NotTo(HaveOccurred()) + Expect(r).NotTo(BeNil()) + + data, err := io.ReadAll(r) + Expect(err).NotTo(HaveOccurred()) + // A valid empty tar is not a zero-byte stream: it contains the + // end-of-archive marker (two 512-byte zero blocks). + Expect(len(data)).To(BeNumerically(">", 0)) + + tr := tar.NewReader(bytes.NewReader(data)) + _, err = tr.Next() + Expect(err).To(Equal(io.EOF), "expected a well-formed empty tar with no entries") + }) + }) }) diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/pkg/image/image_values.go new/werf-2.74.0/pkg/image/image_values.go --- old/werf-2.73.2/pkg/image/image_values.go 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/pkg/image/image_values.go 2026-07-14 22:07:35.000000000 +0200 @@ -24,6 +24,23 @@ ), nil } +func RebuildImageValuesMap(newRef, digest string) (map[string]interface{}, error) { + tag, err := name.NewTag(newRef) + if err != nil { + return nil, fmt.Errorf("rebuild image values map: %w", err) + } + + return buildValuesMap( + tag.RegistryStr(), + tag.RepositoryStr(), + path.Base(tag.RepositoryStr()), + tag.TagStr(), + digest, + tag.Context().Name(), + newRef, + ), nil +} + func BuildStubImageValuesMap(repo, tag string) map[string]interface{} { return buildValuesMap( "REGISTRY", diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/pkg/true_git/common.go new/werf-2.74.0/pkg/true_git/common.go --- old/werf-2.73.2/pkg/true_git/common.go 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/pkg/true_git/common.go 2026-07-14 22:07:35.000000000 +0200 @@ -1,13 +1,63 @@ package true_git import ( + "context" + "errors" + "fmt" "os" + "os/exec" + "path/filepath" + "strings" ) func getCommonGitOptions() []string { return []string{"-c", "core.autocrlf=false", "-c", "gc.auto=0", "-c", "commit.gpgsign=false", "-c", "core.untrackedCache=false", "-c", "core.splitIndex=false"} } +func getIncludePathOptions(ctx context.Context, repoDir string) ([]string, error) { + configCmd := NewGitCmd(ctx, &GitCmdOptions{RepoDir: repoDir}, "config", "--show-origin", "--get-all", "include.path") + if err := configCmd.Run(ctx); err != nil { + var exitErr *exec.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() == 1 && configCmd.OutBuf.String() == "" && configCmd.ErrBuf.String() == "" { + return nil, nil + } + return nil, fmt.Errorf("get git include.path from %s: %w", repoDir, err) + } + + var opts []string + for _, line := range strings.Split(configCmd.OutBuf.String(), "\n") { + line = strings.TrimSpace(line) + if line == "" { + continue + } + + origin, includePath, found := strings.Cut(line, "\t") + if !found { + continue + } + + // include.path passed via -c has no declaring config file, so relative values are resolved against their origin up front + if !strings.HasPrefix(includePath, "~") && !filepath.IsAbs(includePath) { + if originFile, ok := strings.CutPrefix(origin, "file:"); ok { + if !filepath.IsAbs(originFile) { + originFile = filepath.Join(repoDir, originFile) + } + includePath = filepath.Join(filepath.Dir(originFile), includePath) + } else { + absIncludePath, err := filepath.Abs(includePath) + if err != nil { + return nil, fmt.Errorf("resolve include.path %q: %w", includePath, err) + } + includePath = absIncludePath + } + } + + opts = append(opts, "-c", "include.path="+includePath) + } + + return opts, nil +} + func debug() bool { return os.Getenv("WERF_DEBUG_TRUE_GIT") == "1" } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/pkg/true_git/helpers_ai_test.go new/werf-2.74.0/pkg/true_git/helpers_ai_test.go --- old/werf-2.73.2/pkg/true_git/helpers_ai_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/werf-2.74.0/pkg/true_git/helpers_ai_test.go 2026-07-14 22:07:35.000000000 +0200 @@ -0,0 +1,24 @@ +package true_git + +import ( + "os/exec" + "testing" + + "github.com/stretchr/testify/require" +) + +func runGitAI(t *testing.T, dir string, args ...string) string { + t.Helper() + cmd := exec.Command("git", append([]string{"-C", dir}, args...)...) + out, err := cmd.CombinedOutput() + require.NoError(t, err, "git %v: %s", args, out) + return string(out) +} + +func initGitRepoAI(t *testing.T, dir string) { + t.Helper() + runGitAI(t, dir, "init") + runGitAI(t, dir, "config", "user.email", "[email protected]") + runGitAI(t, dir, "config", "user.name", "test") + runGitAI(t, dir, "commit", "--allow-empty", "-m", "init") +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/pkg/true_git/submodule.go new/werf-2.74.0/pkg/true_git/submodule.go --- old/werf-2.73.2/pkg/true_git/submodule.go 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/pkg/true_git/submodule.go 2026-07-14 22:07:35.000000000 +0200 @@ -10,7 +10,12 @@ func syncSubmodules(ctx context.Context, repoDir, workTreeDir string) error { logProcessMsg := fmt.Sprintf("Sync submodules in work tree %q", workTreeDir) return logboek.Context(ctx).Info().LogProcess(logProcessMsg).DoError(func() error { - submSyncCmd := NewGitCmd(ctx, &GitCmdOptions{RepoDir: workTreeDir}, "submodule", "sync", "--recursive") + includePathOpts, err := getIncludePathOptions(ctx, repoDir) + if err != nil { + return err + } + + submSyncCmd := NewGitCmd(ctx, &GitCmdOptions{RepoDir: workTreeDir}, append(includePathOpts, "submodule", "sync", "--recursive")...) if err := submSyncCmd.Run(ctx); err != nil { return fmt.Errorf("submodule sync command failed: %w", err) } @@ -22,7 +27,12 @@ func updateSubmodules(ctx context.Context, repoDir, workTreeDir string) error { logProcessMsg := fmt.Sprintf("Update submodules in work tree %q", workTreeDir) return logboek.Context(ctx).Info().LogProcess(logProcessMsg).DoError(func() error { - submUpdateCmd := NewGitCmd(ctx, &GitCmdOptions{RepoDir: workTreeDir}, "submodule", "update", "--checkout", "--force", "--init", "--recursive") + includePathOpts, err := getIncludePathOptions(ctx, repoDir) + if err != nil { + return err + } + + submUpdateCmd := NewGitCmd(ctx, &GitCmdOptions{RepoDir: workTreeDir}, append(includePathOpts, "submodule", "update", "--checkout", "--force", "--init", "--recursive")...) if err := submUpdateCmd.Run(ctx); err != nil { return fmt.Errorf("submodule update command failed: %w", err) } diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/pkg/true_git/submodule_ai_test.go new/werf-2.74.0/pkg/true_git/submodule_ai_test.go --- old/werf-2.73.2/pkg/true_git/submodule_ai_test.go 1970-01-01 01:00:00.000000000 +0100 +++ new/werf-2.74.0/pkg/true_git/submodule_ai_test.go 2026-07-14 22:07:35.000000000 +0200 @@ -0,0 +1,80 @@ +package true_git + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" + + "github.com/stretchr/testify/require" +) + +func TestAI_GetIncludePathOptions(t *testing.T) { + ctx := context.Background() + + repoDir := t.TempDir() + initGitRepoAI(t, repoDir) + gitDir := filepath.Join(repoDir, ".git") + + opts, err := getIncludePathOptions(ctx, gitDir) + require.NoError(t, err) + require.Empty(t, opts) + + runGitAI(t, repoDir, "config", "--add", "include.path", "/abs/ext.conf") + runGitAI(t, repoDir, "config", "--add", "include.path", "rel/ext.conf") + runGitAI(t, repoDir, "config", "--add", "include.path", "~/tilde-ext.conf") + + opts, err = getIncludePathOptions(ctx, gitDir) + require.NoError(t, err) + require.Len(t, opts, 6) + require.Equal(t, "-c", opts[0]) + require.Equal(t, "include.path=/abs/ext.conf", opts[1]) + require.Equal(t, "-c", opts[2]) + require.True(t, strings.HasPrefix(opts[3], "include.path=")) + relResolved := strings.TrimPrefix(opts[3], "include.path=") + require.True(t, filepath.IsAbs(relResolved)) + require.True(t, strings.HasSuffix(relResolved, filepath.Join(".git", "rel", "ext.conf"))) + require.Equal(t, "-c", opts[4]) + require.Equal(t, "include.path=~/tilde-ext.conf", opts[5]) +} + +func TestAI_UpdateSubmodulesForwardsIncludePath(t *testing.T) { + ctx := context.Background() + + repoDir := t.TempDir() + initGitRepoAI(t, repoDir) + headSHA := strings.TrimSpace(runGitAI(t, repoDir, "rev-parse", "HEAD")) + + gitmodules := "[submodule \"sub\"]\n\tpath = sub\n\turl = https://werf-test-nonexistent.invalid/sub.git\n" + require.NoError(t, os.WriteFile(filepath.Join(repoDir, ".gitmodules"), []byte(gitmodules), 0o644)) + runGitAI(t, repoDir, "update-index", "--add", "--cacheinfo", "160000,"+headSHA+",sub") + runGitAI(t, repoDir, "add", ".gitmodules") + runGitAI(t, repoDir, "commit", "-m", "add submodule") + + stubPath := filepath.Join(t.TempDir(), "ext.conf") + stub := "[url \"werf-test-marker://rewritten/\"]\n\tinsteadOf = https://\n" + require.NoError(t, os.WriteFile(stubPath, []byte(stub), 0o644)) + runGitAI(t, repoDir, "config", "include.path", stubPath) + + err := updateSubmodules(ctx, filepath.Join(repoDir, ".git"), repoDir) + require.Error(t, err) + require.Contains(t, err.Error(), "werf-test-marker") +} + +func TestAI_SwitchWorkTreeNonSubmodulesUnaffectedByIncludePath(t *testing.T) { + ctx := context.Background() + + repoDir := t.TempDir() + initGitRepoAI(t, repoDir) + headSHA := strings.TrimSpace(runGitAI(t, repoDir, "rev-parse", "HEAD")) + + stubPath := filepath.Join(t.TempDir(), "ext.conf") + stub := "[url \"werf-test-marker://rewritten/\"]\n\tinsteadOf = https://\n" + require.NoError(t, os.WriteFile(stubPath, []byte(stub), 0o644)) + runGitAI(t, repoDir, "config", "include.path", stubPath) + + workTreeDir := filepath.Join(t.TempDir(), "worktree") + err := switchWorkTree(ctx, filepath.Join(repoDir, ".git"), workTreeDir, headSHA, false) + require.NoError(t, err) +} diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/test/e2e/build/scratch_test.go new/werf-2.74.0/test/e2e/build/scratch_test.go --- old/werf-2.73.2/test/e2e/build/scratch_test.go 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/test/e2e/build/scratch_test.go 2026-07-14 22:07:35.000000000 +0200 @@ -42,6 +42,7 @@ By("checking scratch image contents and labels") imageName := buildReport.Images["stapel-scratch"].DockerImageName + utils.ExpectImageIsReadable(ctx, testOpts.ContainerBackendMode, imageName) utils.ExpectFileContentInImage(ctx, testOpts.ContainerBackendMode, imageName, "etc/werf-test-scratch-import", "werf-test-scratch-import\n") utils.ExpectImageHasNonEmptyLabels(ctx, testOpts.ContainerBackendMode, imageName, image.WerfLabel, diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/test/pkg/utils/image.go new/werf-2.74.0/test/pkg/utils/image.go --- old/werf-2.73.2/test/pkg/utils/image.go 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/test/pkg/utils/image.go 2026-07-14 22:07:35.000000000 +0200 @@ -41,6 +41,38 @@ Expect(fileContent).To(Equal(expectedContent), "expected image file content to match for %s", filePath) } +// ExpectImageIsReadable asserts that the image can be saved and fully parsed by +// go-containerregistry: its manifest, config and every layer must be readable +// and the whole rootfs must extract without EOF/malformed-tar errors. This +// guards against malformed images (e.g. a broken `from: scratch` base layer) +// that Docker can still list/tag but that tools like `dive` fail to read. +func ExpectImageIsReadable(ctx context.Context, backendMode, imageName string) { + img, cleanup := loadLocalImage(ctx, backendMode, imageName) + defer cleanup() + + _, err := img.ConfigFile() + Expect(err).NotTo(HaveOccurred(), "expected image config to be readable for %s", imageName) + + _, err = img.Manifest() + Expect(err).NotTo(HaveOccurred(), "expected image manifest to be readable for %s", imageName) + + layers, err := img.Layers() + Expect(err).NotTo(HaveOccurred(), "expected image layers to be readable for %s", imageName) + + for i, layer := range layers { + rc, err := layer.Uncompressed() + Expect(err).NotTo(HaveOccurred(), "expected layer %d to be readable for %s", i, imageName) + _, err = io.Copy(io.Discard, rc) + Expect(err).NotTo(HaveOccurred(), "expected layer %d to be fully readable for %s", i, imageName) + Expect(rc.Close()).To(Succeed()) + } + + rc := mutate.Extract(img) + defer rc.Close() + _, err = io.Copy(io.Discard, rc) + Expect(err).NotTo(HaveOccurred(), "expected image rootfs to fully extract for %s", imageName) +} + func ExpectImageHasNonEmptyLabels(ctx context.Context, backendMode, imageName string, labelKeys ...string) { img, cleanup := loadLocalImage(ctx, backendMode, imageName) defer cleanup() diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' '--exclude=.svnignore' old/werf-2.73.2/trdl_channels.yaml new/werf-2.74.0/trdl_channels.yaml --- old/werf-2.73.2/trdl_channels.yaml 2026-07-02 10:37:28.000000000 +0200 +++ new/werf-2.74.0/trdl_channels.yaml 2026-07-14 22:07:35.000000000 +0200 @@ -38,12 +38,16 @@ - name: "2" channels: - name: alpha - version: 2.73.1 + version: 2.73.2 - name: beta - version: 2.73.0 + version: 2.73.1 - name: ea - version: 2.72.2 + version: 2.73.1 - name: stable - version: 2.71.0 + version: 2.72.2 - name: rock-solid - version: 2.70.0 + version: 2.71.0 + - name: "3" + channels: + - name: dev + version: 3.0.0-alpha.1 ++++++ werf.obsinfo ++++++ --- /var/tmp/diff_new_pack.CYJ1Gd/_old 2026-07-31 16:10:59.476118249 +0200 +++ /var/tmp/diff_new_pack.CYJ1Gd/_new 2026-07-31 16:10:59.480118388 +0200 @@ -1,5 +1,5 @@ name: werf -version: 2.73.2 -mtime: 1782981448 -commit: 0b7ec198dac3e05320e8d77cd17492ed5fc18750 +version: 2.74.0 +mtime: 1784059655 +commit: 7585cfbf15796fbe86e33df695a3c5259aa40382
