Repository: chainguard-dev/apko (go)

Issue:
`apko-publish` doesn't copy SBOM to local `sbomPath`
During the usage of [`apko-snapshot`](https://github.com/chainguard-images/actions/tree/main/apko-snapshot) action I experienced a problem at SBOM attestation step:

```console
...
Run function attest_sbom {
Unable to find SBOM on disk: sbom-index.spdx.json (arch: index). Exiting.
Error: Process completed with exit code 1.
```
I've reproduced it tweaking [`test-scan-apk.yaml`](https://github.com/andros21/actions/commit/df845b4a0bfe046d933ec6f71c5a529f75d0457e) workflow, [here its run with full logs](https://github.com/andros21/actions/actions/runs/5517957909).

It seems that since `apko` started to generate also SBOM in tempdir `apko-*`, SBOM are correctly copied in the requested `sbomPath` when using `apko-build`, thanks to this snip of code:

https://github.com/chainguard-dev/apko/blob/37b82f3a5bd85bde214df382fa974bc8dc88f692/internal/cli/build.go#L164-L170

but not when using `apko-publish`, I was wondering if this is the wanted behavior.
Thanks

Pull request description:
fix: publish cmd --sbom-path not writing files
fixes #804

quick fix based on the `--sbom-path` logic in the `build` cmd

Patch:
diff --git a/internal/cli/publish.go b/internal/cli/publish.go
index 60a3ece..b8b6649 100644
--- a/internal/cli/publish.go
+++ b/internal/cli/publish.go
@@ -19,6 +19,7 @@ import (
 	"fmt"
 	"io"
 	"os"
+	"path/filepath"
 	"regexp"
 	"sort"
 	"strings"
@@ -122,6 +123,7 @@ in a keychain.`,
 			remoteOpts = append(remoteOpts, remote.Reuse(puller))
 
 			if err := PublishCmd(cmd.Context(), imageRefs, archs, remoteOpts,
+				sbomPath,
 				[]build.Option{
 					build.WithLogger(logger),
 					build.WithConfig(args[0]),
@@ -186,7 +188,7 @@ in a keychain.`,
 	return cmd
 }
 
-func PublishCmd(ctx context.Context, outputRefs string, archs []types.Architecture, ropt []remote.Option, buildOpts []build.Option, publishOpts []PublishOption) error {
+func PublishCmd(ctx context.Context, outputRefs string, archs []types.Architecture, ropt []remote.Option, sbomPath string, buildOpts []build.Option, publishOpts []PublishOption) error {
 	ctx, span := otel.Tracer("apko").Start(ctx, "PublishCmd")
 	defer span.End()
 
@@ -353,6 +355,16 @@ func PublishCmd(ctx context.Context, outputRefs string, archs []types.Architectu
 		}
 	}
 
+	// copy sboms over to the sbomPath target directory
+	if sbomPath != "" {
+		for _, sbom := range sboms {
+			// because os.Rename fails across partitions, we do our own
+			if err := rename(sbom.Path, filepath.Join(sbomPath, filepath.Base(sbom.Path))); err != nil {
+				return fmt.Errorf("moving sbom: %w", err)
+			}
+		}
+	}
+
 	// Write the image digest to STDOUT in order to enable command
 	// composition e.g. kn service create --image=$(apko publish ...)
 	fmt.Println(finalDigest)


Test patch:
diff --git a/internal/cli/publish_test.go b/internal/cli/publish_test.go
index 5c43887..571dc0d 100644
--- a/internal/cli/publish_test.go
+++ b/internal/cli/publish_test.go
@@ -20,6 +20,7 @@ import (
 	"net/http"
 	"net/http/httptest"
 	"net/url"
+	"os"
 	"path/filepath"
 	"strings"
 	"testing"
@@ -38,6 +39,7 @@ import (
 
 func TestPublish(t *testing.T) {
 	ctx := context.Background()
+	tmp := t.TempDir()
 
 	// Set up a registry that requires we see a magic header.
 	// This allows us to make sure that remote options are getting passed
@@ -63,7 +65,11 @@ func TestPublish(t *testing.T) {
 	opts := []build.Option{build.WithConfig(config), build.WithTags(dst), build.WithSBOMFormats(sbom.DefaultOptions.Formats)}
 	publishOpts := []cli.PublishOption{cli.WithTags(dst)}
 
-	err = cli.PublishCmd(ctx, outputRefs, archs, ropt, opts, publishOpts)
+	sbomPath := filepath.Join(tmp, "sboms")
+	err = os.MkdirAll(sbomPath, 0o750)
+	require.NoError(t, err)
+
+	err = cli.PublishCmd(ctx, outputRefs, archs, ropt, sbomPath, opts, publishOpts)
 	require.NoError(t, err)
 
 	ref, err := name.ParseReference(dst)
@@ -125,6 +131,11 @@ func TestPublish(t *testing.T) {
 		got := m.Layers[0].Digest.String()
 		require.Equal(t, wantBoms[i], got)
 	}
+
+	// Check that the sbomPath is not empty.
+	sboms, err := os.ReadDir(sbomPath)
+	require.NoError(t, err)
+	require.NotEmpty(t, sboms)
 }
 
 type sentinel struct {