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
