Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package copacetic for openSUSE:Factory 
checked in at 2026-07-03 16:05:48
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/copacetic (Old)
 and      /work/SRC/openSUSE:Factory/.copacetic.new.1982 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "copacetic"

Fri Jul  3 16:05:48 2026 rev:9 rq:1363600 version:0.14.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/copacetic/copacetic.changes      2026-05-18 
17:47:30.051023522 +0200
+++ /work/SRC/openSUSE:Factory/.copacetic.new.1982/copacetic.changes    
2026-07-03 16:09:00.409792918 +0200
@@ -1,0 +2,6 @@
+Fri Jul 03 05:11:02 UTC 2026 - Johannes Kastl 
<[email protected]>
+
+- Update to version 0.14.2:
+  * fix: avoid forced recompression on local export (#1637) (#1640)
+
+-------------------------------------------------------------------

Old:
----
  copacetic-0.14.1.obscpio

New:
----
  copacetic-0.14.2.obscpio

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

Other differences:
------------------
++++++ copacetic.spec ++++++
--- /var/tmp/diff_new_pack.TpfMrU/_old  2026-07-03 16:09:03.905914169 +0200
+++ /var/tmp/diff_new_pack.TpfMrU/_new  2026-07-03 16:09:03.925914863 +0200
@@ -18,7 +18,7 @@
 
 %define executable_name copa
 Name:           copacetic
-Version:        0.14.1
+Version:        0.14.2
 Release:        0
 Summary:        CLI tool for directly patching container images using reports 
from vulnerability scanners
 License:        Apache-2.0

++++++ _service ++++++
--- /var/tmp/diff_new_pack.TpfMrU/_old  2026-07-03 16:09:04.325928737 +0200
+++ /var/tmp/diff_new_pack.TpfMrU/_new  2026-07-03 16:09:04.401931372 +0200
@@ -1,11 +1,11 @@
 <services>
   <service name="obs_scm" mode="manual">
-    <param name="url">https://github.com/project-copacetic/copacetic</param>
+    <param 
name="url">https://github.com/project-copacetic/copacetic.git</param>
     <param name="scm">git</param>
     <param name="exclude">.git</param>
     <param name="versionformat">@PARENT_TAG@</param>
     <param name="versionrewrite-pattern">v(.*)</param>
-    <param name="revision">v0.14.1</param>
+    <param name="revision">refs/tags/v0.14.2</param>
     <param name="changesgenerate">enable</param>
   </service>
   <service name="set_version" mode="manual">

++++++ _servicedata ++++++
--- /var/tmp/diff_new_pack.TpfMrU/_old  2026-07-03 16:09:04.629939280 +0200
+++ /var/tmp/diff_new_pack.TpfMrU/_new  2026-07-03 16:09:04.689941361 +0200
@@ -1,6 +1,8 @@
 <servicedata>
 <service name="tar_scm">
                 <param 
name="url">https://github.com/project-copacetic/copacetic</param>
-              <param 
name="changesrevision">f262411b1b8788772dee4d47256aa0ed670adf84</param></service></servicedata>
+              <param 
name="changesrevision">f262411b1b8788772dee4d47256aa0ed670adf84</param></service><service
 name="tar_scm">
+                <param 
name="url">https://github.com/project-copacetic/copacetic.git</param>
+              <param 
name="changesrevision">875ab453477a1e863a41c44cf02c98b8557f758b</param></service></servicedata>
 (No newline at EOF)
 

++++++ copacetic-0.14.1.obscpio -> copacetic-0.14.2.obscpio ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/pkg/buildkit/buildkit.go 
new/copacetic-0.14.2/pkg/buildkit/buildkit.go
--- old/copacetic-0.14.1/pkg/buildkit/buildkit.go       2026-05-15 
15:28:24.000000000 +0200
+++ new/copacetic-0.14.2/pkg/buildkit/buildkit.go       2026-07-03 
00:50:18.000000000 +0200
@@ -56,6 +56,14 @@
        KeyPath    string
 }
 
+// OCILayoutExportOptions controls BuildKit OCI exporter behavior when writing
+// patched platforms into an OCI image layout. Preserved platforms are copied
+// from the original image as-is to keep their descriptors and layer blobs 
unchanged.
+type OCILayoutExportOptions struct {
+       Compression      string
+       ForceCompression bool
+}
+
 const (
        linux = "linux"
        arm64 = "arm64"
@@ -871,6 +879,12 @@
 
 // CreateOCILayoutFromResults creates an OCI layout directory from patch 
results using BuildKit's OCI exporter.
 func CreateOCILayoutFromResults(outputDir string, results []types.PatchResult, 
platforms []types.PatchPlatform) error {
+       return CreateOCILayoutFromResultsWithOptions(outputDir, results, 
platforms, OCILayoutExportOptions{})
+}
+
+// CreateOCILayoutFromResultsWithOptions creates an OCI layout directory from 
patch results using
+// BuildKit's OCI exporter with the provided export options.
+func CreateOCILayoutFromResultsWithOptions(outputDir string, results 
[]types.PatchResult, platforms []types.PatchPlatform, exportOpts 
OCILayoutExportOptions) error {
        log.Infof("Creating multi-platform OCI layout in directory: %s with %d 
platforms", outputDir, len(platforms))
 
        // Create output directory
@@ -890,14 +904,14 @@
 
        if hasStates {
                log.Info("Using BuildKit states directly for OCI export")
-               return createOCILayoutFromStates(outputDir, results, platforms)
+               return createOCILayoutFromStates(outputDir, results, platforms, 
exportOpts)
        }
 
        return fmt.Errorf("no BuildKit states available for OCI export, cannot 
proceed")
 }
 
 // createOCILayoutFromStates creates OCI layout directly from BuildKit states.
-func createOCILayoutFromStates(outputDir string, results []types.PatchResult, 
platforms []types.PatchPlatform) error {
+func createOCILayoutFromStates(outputDir string, results []types.PatchResult, 
platforms []types.PatchPlatform, exportOpts OCILayoutExportOptions) error {
        log.Info("Creating OCI layout from preserved BuildKit states and 
preserved platforms")
 
        // Separate patched and preserved platforms
@@ -955,7 +969,7 @@
        switch {
        case hasPreservedPlatforms && hasPatchedPlatforms:
                log.Infof("Creating mixed OCI layout with %d patched and %d 
preserved platforms", len(platformStates), len(preservedPlatforms))
-               return createMixedOCILayout(outputDir, results, platformStates, 
platformSpecs, preservedPlatforms)
+               return createMixedOCILayout(outputDir, results, platformStates, 
platformSpecs, preservedPlatforms, exportOpts)
        case hasPatchedPlatforms:
                log.Infof("Creating OCI layout from %d patched platforms only", 
len(platformStates))
        case hasPreservedPlatforms:
@@ -980,7 +994,7 @@
                                log.Debug("Using buildx driver for OCI layout 
export")
                                defer c.Close()
 
-                               return solveMultiPlatformOCI(ctx, c, outputDir, 
platformStates, platformSpecs)
+                               return solveMultiPlatformOCI(ctx, c, outputDir, 
platformStates, platformSpecs, exportOpts)
                        }
                        c.Close()
                }
@@ -996,11 +1010,11 @@
        }
        defer c.Close()
 
-       return solveMultiPlatformOCI(ctx, c, outputDir, platformStates, 
platformSpecs)
+       return solveMultiPlatformOCI(ctx, c, outputDir, platformStates, 
platformSpecs, exportOpts)
 }
 
 // solveMultiPlatformOCI uses BuildKit client to solve multi-platform states 
and export to OCI layout.
-func solveMultiPlatformOCI(ctx context.Context, c *client.Client, outputDir 
string, platformStates []llb.State, platformSpecs []specs.Platform) error {
+func solveMultiPlatformOCI(ctx context.Context, c *client.Client, outputDir 
string, platformStates []llb.State, platformSpecs []specs.Platform, exportOpts 
OCILayoutExportOptions) error {
        if len(platformStates) == 0 {
                return fmt.Errorf("no platform states provided")
        }
@@ -1019,23 +1033,35 @@
 
        if len(platformStates) == 1 {
                // Single platform case - use output function to avoid diffcopy 
issues
-               return solveSinglePlatformOCI(ctx, c, outputDir, 
&platformStates[0], &platformSpecs[0])
+               return solveSinglePlatformOCI(ctx, c, outputDir, 
&platformStates[0], &platformSpecs[0], exportOpts)
        }
 
        // Multi-platform case - solve each platform and combine
-       return solveAndCombineAllPlatforms(ctx, c, outputDir, platformStates, 
platformSpecs)
+       return solveAndCombineAllPlatforms(ctx, c, outputDir, platformStates, 
platformSpecs, exportOpts)
+}
+
+func ociExporterAttrs(exportOpts OCILayoutExportOptions) map[string]string {
+       attrs := map[string]string{
+               "oci-mediatypes": "true",
+               "buildinfo":      "false",
+       }
+       if exportOpts.Compression != "" {
+               attrs["compression"] = exportOpts.Compression
+       }
+       if exportOpts.ForceCompression {
+               attrs["force-compression"] = "true"
+       }
+
+       return attrs
 }
 
 // solveSinglePlatformOCI handles single platform OCI export using output 
function.
-func solveSinglePlatformOCI(ctx context.Context, c *client.Client, outputDir 
string, state *llb.State, platformSpec *specs.Platform) error {
+func solveSinglePlatformOCI(ctx context.Context, c *client.Client, outputDir 
string, state *llb.State, platformSpec *specs.Platform, exportOpts 
OCILayoutExportOptions) error {
        // Create solve options with output function to avoid diffcopy issues
        solveOpt := client.SolveOpt{
                Exports: []client.ExportEntry{{
-                       Type: client.ExporterOCI,
-                       Attrs: map[string]string{
-                               "oci-mediatypes": "true",
-                               "buildinfo":      "false",
-                       },
+                       Type:  client.ExporterOCI,
+                       Attrs: ociExporterAttrs(exportOpts),
                        Output: func(_ map[string]string) (io.WriteCloser, 
error) {
                                tarPath := filepath.Join(outputDir, "image.tar")
                                return os.Create(tarPath)
@@ -1118,7 +1144,7 @@
 }
 
 // solveAndCombineAllPlatforms solves each platform and combines them into one 
OCI layout.
-func solveAndCombineAllPlatforms(ctx context.Context, c *client.Client, 
outputDir string, platformStates []llb.State, platformSpecs []specs.Platform) 
error {
+func solveAndCombineAllPlatforms(ctx context.Context, c *client.Client, 
outputDir string, platformStates []llb.State, platformSpecs []specs.Platform, 
exportOpts OCILayoutExportOptions) error {
        // Create temporary directory for platform tars
        tempDir, err := os.MkdirTemp("", "copa-platforms-*")
        if err != nil {
@@ -1136,11 +1162,8 @@
                // Create solve options with output function
                platformSolveOpt := client.SolveOpt{
                        Exports: []client.ExportEntry{{
-                               Type: client.ExporterOCI,
-                               Attrs: map[string]string{
-                                       "oci-mediatypes": "true",
-                                       "buildinfo":      "false",
-                               },
+                               Type:  client.ExporterOCI,
+                               Attrs: ociExporterAttrs(exportOpts),
                                Output: func(_ map[string]string) 
(io.WriteCloser, error) {
                                        return os.Create(platformTarPath)
                                },
@@ -1374,6 +1397,7 @@
        platformStates []llb.State,
        platformSpecs []specs.Platform,
        preservedPlatforms []types.PatchPlatform,
+       exportOpts OCILayoutExportOptions,
 ) error {
        log.Infof("Creating mixed OCI layout with %d patched platforms and %d 
preserved platforms", len(platformStates), len(preservedPlatforms))
 
@@ -1399,7 +1423,7 @@
                }
                defer c.Close()
 
-               patchedManifests, err = exportPatchedPlatformsToTemp(ctx, c, 
patchedTempDir, platformStates, platformSpecs)
+               patchedManifests, err = exportPatchedPlatformsToTemp(ctx, c, 
patchedTempDir, platformStates, platformSpecs, exportOpts)
                if err != nil {
                        return fmt.Errorf("failed to export patched platforms: 
%w", err)
                }
@@ -1425,6 +1449,8 @@
                if originalRef == nil {
                        log.Warn("Could not determine original image reference 
for preserved platforms, skipping preserved platforms export")
                } else {
+                       // Preserved platforms intentionally keep their 
original descriptors and
+                       // layer blobs, even when compression options are set 
for patched platforms.
                        var err error
                        preservedManifests, err = 
exportPreservedPlatformsToOutput(outputDir, originalRef, preservedPlatforms, 
allBlobs)
                        if err != nil {
@@ -1444,7 +1470,14 @@
 }
 
 // exportPatchedPlatformsToTemp exports patched platforms using BuildKit to a 
temporary directory.
-func exportPatchedPlatformsToTemp(ctx context.Context, c *client.Client, 
tempDir string, platformStates []llb.State, platformSpecs []specs.Platform) 
([]map[string]interface{}, error) {
+func exportPatchedPlatformsToTemp(
+       ctx context.Context,
+       c *client.Client,
+       tempDir string,
+       platformStates []llb.State,
+       platformSpecs []specs.Platform,
+       exportOpts OCILayoutExportOptions,
+) ([]map[string]interface{}, error) {
        var manifests []map[string]interface{}
 
        // Export each platform to its own tar file
@@ -1455,11 +1488,8 @@
                // Create solve options with output function
                solveOpt := client.SolveOpt{
                        Exports: []client.ExportEntry{{
-                               Type: client.ExporterOCI,
-                               Attrs: map[string]string{
-                                       "oci-mediatypes": "true",
-                                       "buildinfo":      "false",
-                               },
+                               Type:  client.ExporterOCI,
+                               Attrs: ociExporterAttrs(exportOpts),
                                Output: func(_ map[string]string) 
(io.WriteCloser, error) {
                                        return os.Create(platformTarPath)
                                },
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/pkg/buildkit/buildkit_test.go 
new/copacetic-0.14.2/pkg/buildkit/buildkit_test.go
--- old/copacetic-0.14.1/pkg/buildkit/buildkit_test.go  2026-05-15 
15:28:24.000000000 +0200
+++ new/copacetic-0.14.2/pkg/buildkit/buildkit_test.go  2026-07-03 
00:50:18.000000000 +0200
@@ -571,3 +571,48 @@
                })
        }
 }
+
+func TestOCIExporterAttrs(t *testing.T) {
+       tests := []struct {
+               name                 string
+               exportOpts           OCILayoutExportOptions
+               wantCompression      string
+               wantForceCompression bool
+       }{
+               {
+                       name: "no compression options",
+               },
+               {
+                       name: "compression without force compression",
+                       exportOpts: OCILayoutExportOptions{
+                               Compression: "zstd",
+                       },
+                       wantCompression: "zstd",
+               },
+               {
+                       name: "compression with force compression",
+                       exportOpts: OCILayoutExportOptions{
+                               Compression:      "gzip",
+                               ForceCompression: true,
+                       },
+                       wantCompression:      "gzip",
+                       wantForceCompression: true,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       attrs := ociExporterAttrs(tc.exportOpts)
+
+                       assert.Equal(t, "true", attrs["oci-mediatypes"])
+                       assert.Equal(t, "false", attrs["buildinfo"])
+                       if tc.wantCompression == "" {
+                               assert.NotContains(t, attrs, "compression")
+                       } else {
+                               assert.Equal(t, tc.wantCompression, 
attrs["compression"])
+                       }
+                       _, hasForceCompression := attrs["force-compression"]
+                       assert.Equal(t, tc.wantForceCompression, 
hasForceCompression)
+               })
+       }
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/pkg/cmd/cmd.go 
new/copacetic-0.14.2/pkg/cmd/cmd.go
--- old/copacetic-0.14.1/pkg/cmd/cmd.go 2026-05-15 15:28:24.000000000 +0200
+++ new/copacetic-0.14.2/pkg/cmd/cmd.go 2026-07-03 00:50:18.000000000 +0200
@@ -48,6 +48,8 @@
        goVCSURL            string
        progress            string
        ociDir              string
+       compression         string
+       forceCompression    bool
        eolAPIBaseURL       string
        exitOnEOL           bool
        configFile          string
@@ -107,6 +109,8 @@
                                GoVCSURL:            ua.goVCSURL,
                                Progress:            
progressui.DisplayMode(ua.progress),
                                OCIDir:              ua.ociDir,
+                               Compression:         ua.compression,
+                               ForceCompression:    ua.forceCompression,
                                EOLAPIBaseURL:       ua.eolAPIBaseURL,
                                ExitOnEOL:           ua.exitOnEOL,
                                ConfigFile:          ua.configFile,
@@ -161,6 +165,9 @@
        flags.StringVar(&ua.eolAPIBaseURL, "eol-api-url", "", "EOL API base 
URL, defaults to 'https://endoflife.date/api/v1/products'")
        flags.BoolVar(&ua.exitOnEOL, "exit-on-eol", false, "Exit with error 
when EOL (End of Life) operating system is detected")
        flags.StringVar(&ua.progress, "progress", "auto", "Set the buildkit 
display mode (auto, plain, tty, quiet or rawjson). Set to quiet to discard all 
output.")
+       flags.StringVar(&ua.compression, "compression", 
patch.DefaultLocalExportCompression,
+               "Layer compression for patched-platform local export (BuildKit 
values such as 'uncompressed', 'gzip', 'estargz', or 'zstd')")
+       flags.BoolVar(&ua.forceCompression, "force-compression", false, 
"Re-encode BuildKit-exported patched-platform layers to the selected 
compression on local export")
 
        // Experimental flags - only available when COPA_EXPERIMENTAL=1
        if os.Getenv("COPA_EXPERIMENTAL") == "1" {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/pkg/cmd/cmd_test.go 
new/copacetic-0.14.2/pkg/cmd/cmd_test.go
--- old/copacetic-0.14.1/pkg/cmd/cmd_test.go    2026-05-15 15:28:24.000000000 
+0200
+++ new/copacetic-0.14.2/pkg/cmd/cmd_test.go    2026-07-03 00:50:18.000000000 
+0200
@@ -33,6 +33,11 @@
                        expectValidationError: false, // This combination of 
flags is valid.
                },
                {
+                       name:                  "PASS: Local export compression 
flags",
+                       args:                  []string{"--image", 
"alpine:latest", "--compression", "gzip", "--force-compression"},
+                       expectValidationError: false, // This combination of 
flags is valid.
+               },
+               {
                        name:                  "PASS: Bulk mode validation",
                        args:                  []string{"--config", 
"config.yaml"},
                        expectValidationError: false, // This combination of 
flags is valid.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/pkg/patch/build.go 
new/copacetic-0.14.2/pkg/patch/build.go
--- old/copacetic-0.14.1/pkg/patch/build.go     2026-05-15 15:28:24.000000000 
+0200
+++ new/copacetic-0.14.2/pkg/patch/build.go     2026-07-03 00:50:18.000000000 
+0200
@@ -18,6 +18,10 @@
 
 const (
        attrValueTrue = "true"
+
+       // DefaultLocalExportCompression is the BuildKit compression used for 
newly
+       // created patch layers when loading patched images locally.
+       DefaultLocalExportCompression = "uncompressed"
 )
 
 // BuildConfig holds configuration for building and exporting images.
@@ -51,6 +55,8 @@
        pipeW io.WriteCloser,
        originalAnnotations map[string]string,
        patchedTag string,
+       compression string,
+       forceCompression bool,
 ) (*BuildConfig, error) {
        dockerConfig := config.LoadDefaultConfigFile(os.Stderr)
        cfg := authprovider.DockerAuthProviderConfig{AuthConfigProvider: 
authprovider.LoadAuthConfig(dockerConfig)}
@@ -95,10 +101,20 @@
                        },
                }
        } else {
-               // Use uncompressed layers for local export to ensure diff_id 
== blob digest
-               // This fixes Trivy scanning issues where compressed layers 
have mismatched hashes
-               attrs["compression"] = "uncompressed"
-               attrs["force-compression"] = attrValueTrue
+               // Local export compresses newly created patch layers with the 
selected
+               // compression (default: uncompressed). Uncompressed keeps
+               // diff_id == blob digest for the patch layer, which is what 
local
+               // scanners such as Trivy expect. Without force-compression, 
BuildKit
+               // applies the selected compression only to Copa's new patch 
layer and
+               // passes existing base layers through in their original 
compression;
+               // force-compression re-encodes every layer to the selected 
compression.
+               if compression == "" {
+                       compression = DefaultLocalExportCompression
+               }
+               attrs["compression"] = compression
+               if forceCompression {
+                       attrs["force-compression"] = attrValueTrue
+               }
 
                solveOpt.Exports = []client.ExportEntry{
                        {
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/pkg/patch/build_test.go 
new/copacetic-0.14.2/pkg/patch/build_test.go
--- old/copacetic-0.14.1/pkg/patch/build_test.go        2026-05-15 
15:28:24.000000000 +0200
+++ new/copacetic-0.14.2/pkg/patch/build_test.go        2026-07-03 
00:50:18.000000000 +0200
@@ -1,10 +1,13 @@
 package patch
 
 import (
+       "io"
        "testing"
 
+       "github.com/moby/buildkit/client"
        sourcepolicy "github.com/moby/buildkit/sourcepolicy/pb"
        "github.com/stretchr/testify/assert"
+       "github.com/stretchr/testify/require"
 )
 
 // TestValidateSourcePolicy tests the validateSourcePolicy function.
@@ -197,3 +200,76 @@
                })
        }
 }
+
+func TestCreateBuildConfigLocalExportCompression(t *testing.T) {
+       tests := []struct {
+               name                 string
+               compression          string
+               forceCompression     bool
+               wantCompression      string
+               wantForceCompression bool
+       }{
+               {
+                       name:            "default compression without force 
compression",
+                       wantCompression: DefaultLocalExportCompression,
+               },
+               {
+                       name:                 "custom compression with force 
compression",
+                       compression:          "gzip",
+                       forceCompression:     true,
+                       wantCompression:      "gzip",
+                       wantForceCompression: true,
+               },
+       }
+
+       for _, tc := range tests {
+               t.Run(tc.name, func(t *testing.T) {
+                       pipeR, pipeW := io.Pipe()
+                       defer pipeR.Close()
+                       defer pipeW.Close()
+
+                       buildConfig, err := createBuildConfig(
+                               "example.com/app:patched",
+                               false,
+                               false,
+                               pipeW,
+                               nil,
+                               "patched",
+                               tc.compression,
+                               tc.forceCompression,
+                       )
+                       require.NoError(t, err)
+                       require.Len(t, buildConfig.SolveOpt.Exports, 1)
+
+                       export := buildConfig.SolveOpt.Exports[0]
+                       assert.Equal(t, client.ExporterDocker, export.Type)
+                       assert.Equal(t, tc.wantCompression, 
export.Attrs["compression"])
+                       _, hasForceCompression := 
export.Attrs["force-compression"]
+                       assert.Equal(t, tc.wantForceCompression, 
hasForceCompression)
+               })
+       }
+}
+
+func TestCreateBuildConfigPushDoesNotSetLocalCompressionAttrs(t *testing.T) {
+       pipeR, pipeW := io.Pipe()
+       defer pipeR.Close()
+       defer pipeW.Close()
+
+       buildConfig, err := createBuildConfig(
+               "example.com/app:patched",
+               false,
+               true,
+               pipeW,
+               nil,
+               "patched",
+               "gzip",
+               true,
+       )
+       require.NoError(t, err)
+       require.Len(t, buildConfig.SolveOpt.Exports, 1)
+
+       export := buildConfig.SolveOpt.Exports[0]
+       assert.Equal(t, client.ExporterImage, export.Type)
+       assert.NotContains(t, export.Attrs, "compression")
+       assert.NotContains(t, export.Attrs, "force-compression")
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/pkg/patch/multi.go 
new/copacetic-0.14.2/pkg/patch/multi.go
--- old/copacetic-0.14.1/pkg/patch/multi.go     2026-05-15 15:28:24.000000000 
+0200
+++ new/copacetic-0.14.2/pkg/patch/multi.go     2026-07-03 00:50:18.000000000 
+0200
@@ -421,7 +421,19 @@
        }
        // Create OCI layout if requested and not pushing to registry
        if opts.OCIDir != "" && !opts.Push {
-               if err := buildkit.CreateOCILayoutFromResults(opts.OCIDir, 
patchResults, platforms); err != nil {
+               compression := opts.Compression
+               if compression == "" {
+                       compression = DefaultLocalExportCompression
+               }
+               if err := buildkit.CreateOCILayoutFromResultsWithOptions(
+                       opts.OCIDir,
+                       patchResults,
+                       platforms,
+                       buildkit.OCILayoutExportOptions{
+                               Compression:      compression,
+                               ForceCompression: opts.ForceCompression,
+                       },
+               ); err != nil {
                        log.Warnf("Failed to create OCI layout: %v", err)
                        return fmt.Errorf("failed to create OCI layout: %w", 
err)
                }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/pkg/patch/single.go 
new/copacetic-0.14.2/pkg/patch/single.go
--- old/copacetic-0.14.1/pkg/patch/single.go    2026-05-15 15:28:24.000000000 
+0200
+++ new/copacetic-0.14.2/pkg/patch/single.go    2026-07-03 00:50:18.000000000 
+0200
@@ -194,7 +194,16 @@
        }
 
        // Create build configuration
-       buildConfig, err := createBuildConfig(patchedImageName, 
shouldExportOCI, push, pipeW, originalAnnotations, patchedTag)
+       buildConfig, err := createBuildConfig(
+               patchedImageName,
+               shouldExportOCI,
+               push,
+               pipeW,
+               originalAnnotations,
+               patchedTag,
+               opts.Compression,
+               opts.ForceCompression,
+       )
        if err != nil {
                return nil, err
        }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/pkg/types/options.go 
new/copacetic-0.14.2/pkg/types/options.go
--- old/copacetic-0.14.1/pkg/types/options.go   2026-05-15 15:28:24.000000000 
+0200
+++ new/copacetic-0.14.2/pkg/types/options.go   2026-07-03 00:50:18.000000000 
+0200
@@ -37,10 +37,12 @@
        BkKeyPath    string
 
        // Platform and push
-       Push      bool
-       Platforms []string
-       Loader    string
-       OCIDir    string
+       Push             bool
+       Platforms        []string
+       Loader           string
+       OCIDir           string
+       Compression      string
+       ForceCompression bool
 
        // Package types and library patch level
        PkgTypes          string
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/test/e2e/nodejs/nodejs_test.go 
new/copacetic-0.14.2/test/e2e/nodejs/nodejs_test.go
--- old/copacetic-0.14.1/test/e2e/nodejs/nodejs_test.go 2026-05-15 
15:28:24.000000000 +0200
+++ new/copacetic-0.14.2/test/e2e/nodejs/nodejs_test.go 2026-07-03 
00:50:18.000000000 +0200
@@ -69,10 +69,14 @@
                                return
                        }
 
-                       // 2. Patch the image and capture its output.
+                       // 2. Patch the image and capture its output. Keep this 
Node.js e2e
+                       // scoped to node-pkg findings: Trivy can also report 
vulnerabilities
+                       // for Go helper binaries in the same images, which 
exercises Copa's Go
+                       // binary rebuilder instead of the Node.js language 
manager under test.
                        t.Log("patching image")
+                       nodeReportFile := filterTrivyReportByType(t, 
scanResultsFile, "node-pkg")
                        tagPatched := img.Tag + "-patched"
-                       copaOutput := patchImage(t, ref, tagPatched, 
scanResultsFile)
+                       copaOutput := patchImage(t, ref, tagPatched, 
nodeReportFile)
 
                        // 3. Scan the newly patched image.
                        t.Log("scanning patched image for verification")
@@ -164,6 +168,44 @@
        return vulns
 }
 
+func filterTrivyReportByType(t *testing.T, reportFile string, allowedTypes 
...string) string {
+       t.Helper()
+
+       allowed := make(map[string]struct{}, len(allowedTypes))
+       for _, typ := range allowedTypes {
+               allowed[typ] = struct{}{}
+       }
+
+       reportBytes, err := os.ReadFile(reportFile)
+       require.NoError(t, err, "failed to read trivy report file")
+
+       var report map[string]interface{}
+       require.NoError(t, json.Unmarshal(reportBytes, &report), "failed to 
unmarshal trivy report")
+
+       results, ok := report["Results"].([]interface{})
+       require.True(t, ok, "trivy report Results should be an array")
+
+       filteredResults := make([]interface{}, 0, len(results))
+       for _, result := range results {
+               resultMap, ok := result.(map[string]interface{})
+               if !ok {
+                       continue
+               }
+               resultType, _ := resultMap["Type"].(string)
+               if _, keep := allowed[resultType]; keep {
+                       filteredResults = append(filteredResults, result)
+               }
+       }
+       report["Results"] = filteredResults
+
+       filteredBytes, err := json.Marshal(report)
+       require.NoError(t, err, "failed to marshal filtered trivy report")
+
+       filteredFile := filepath.Join(t.TempDir(), "scan-filtered.json")
+       require.NoError(t, os.WriteFile(filteredFile, filteredBytes, 0o600), 
"failed to write filtered trivy report")
+       return filteredFile
+}
+
 // patchImage now returns the command output as a string.
 func patchImage(t *testing.T, image, tag, reportFile string) string {
        t.Helper()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/website/docs/bulk-image-patching.md 
new/copacetic-0.14.2/website/docs/bulk-image-patching.md
--- old/copacetic-0.14.1/website/docs/bulk-image-patching.md    2026-05-15 
15:28:24.000000000 +0200
+++ new/copacetic-0.14.2/website/docs/bulk-image-patching.md    2026-07-03 
00:50:18.000000000 +0200
@@ -96,11 +96,13 @@
 | `--timeout`             | Per‑job timeout (e.g., `15m`)                      
                      |
 | `--ignore-errors`       | Continue processing other jobs if one fails        
                   |
 | `--oci-dir`             | Export patched image(s) as an OCI layout instead 
of pushing              |
+| `--compression`         | Layer compression for patched-platform local 
export                     |
+| `--force-compression`   | Re-encode patched-platform layers to the selected 
compression on local export |
 
 Restrictions in bulk mode:
 
 - `--config` cannot be combined with `--image` or `--tag`.
-- Global flags like `--push`, `--timeout`, `--ignore-errors`, and `--oci-dir` 
apply to every job defined by the config.
+- Global flags like `--push`, `--timeout`, `--ignore-errors`, `--oci-dir`, 
`--compression`, and `--force-compression` apply to every job defined by the 
config.
 
 ## Behavior and Output
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/copacetic-0.14.1/website/docs/quick-start.md 
new/copacetic-0.14.2/website/docs/quick-start.md
--- old/copacetic-0.14.1/website/docs/quick-start.md    2026-05-15 
15:28:24.000000000 +0200
+++ new/copacetic-0.14.2/website/docs/quick-start.md    2026-07-03 
00:50:18.000000000 +0200
@@ -106,6 +106,8 @@
 
 Both methods create a new image tagged `nginx:1.21.6-patched` in your local 
registry.
 
+For local exports, Copa stores newly created patch layers with 
`--compression=uncompressed` by default so scanners can read the patched image 
reliably. Existing base layers are preserved in their original compression 
unless you use `--force-compression` to re-encode layers exported for patched 
platforms to the selected compression. Platforms preserved unchanged in a 
multi-platform OCI layout keep their original layer blobs and compression.
+
 :::tip
 
 The image used in this tutorial is a multi-platform image containing many 
different platforms. To learn more about multi-platform patching, see 
[multi-platform patching](./multiplatform-patching.md) docs.

++++++ copacetic.obsinfo ++++++
--- /var/tmp/diff_new_pack.TpfMrU/_old  2026-07-03 16:09:07.022022241 +0200
+++ /var/tmp/diff_new_pack.TpfMrU/_new  2026-07-03 16:09:07.078024183 +0200
@@ -1,5 +1,5 @@
 name: copacetic
-version: 0.14.1
-mtime: 1778851704
-commit: f262411b1b8788772dee4d47256aa0ed670adf84
+version: 0.14.2
+mtime: 1783032618
+commit: 875ab453477a1e863a41c44cf02c98b8557f758b
 

++++++ vendor.tar.gz ++++++
/work/SRC/openSUSE:Factory/copacetic/vendor.tar.gz 
/work/SRC/openSUSE:Factory/.copacetic.new.1982/vendor.tar.gz differ: char 150, 
line 2

Reply via email to