Skip to content

Commit c9c3898

Browse files
leodidoona-agent
andcommitted
feat!: bump provenance version and remove tar.gz fallback
BREAKING CHANGE: Bumped provenanceProcessVersion to 4. Provenance is now stored exclusively outside tar.gz as <artifact>.provenance.jsonl. Removed backward compatibility fallback to read from inside tar.gz. This ensures artifacts remain deterministic and cache invalidation works correctly. Co-authored-by: Ona <no-reply@ona.com>
1 parent b039a47 commit c9c3898

File tree

1 file changed

+60
-49
lines changed

1 file changed

+60
-49
lines changed

pkg/leeway/provenance.go

Lines changed: 60 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
package leeway
22

33
import (
4-
"archive/tar"
54
"bufio"
6-
"compress/gzip"
75
"crypto/sha256"
86
"encoding/base64"
97
"encoding/hex"
@@ -37,20 +35,56 @@ const (
3735
// provenanceProcessVersion is the version of the provenance generating process.
3836
// If provenance is enabled in a workspace, this version becomes part of the manifest,
3937
// hence changing it will invalidate previously built packages.
40-
provenanceProcessVersion = 3
38+
//
39+
// Version 4: Provenance stored exclusively outside tar.gz as <artifact>.provenance.jsonl
40+
// Removed backward compatibility fallback to read from inside tar.gz.
41+
// This ensures artifacts remain deterministic and cache invalidation works correctly.
42+
provenanceProcessVersion = 4
4143

4244
// ProvenanceBuilderID is the prefix we use as Builder ID when issuing provenance
4345
ProvenanceBuilderID = "github.com/gitpod-io/leeway"
4446
)
4547

46-
// writeProvenance produces a provenanceWriter which ought to be used during package builds
48+
// writeProvenance produces a provenance bundle and writes it alongside the artifact in the cache.
49+
// The provenance is written to <artifact>.provenance.jsonl (outside the tar.gz) to maintain artifact determinism.
50+
// This function should be called AFTER the artifact tar.gz has been created.
4751
func writeProvenance(p *Package, buildctx *buildContext, builddir string, subjects []in_toto.Subject, buildStarted time.Time) (err error) {
4852
if !p.C.W.Provenance.Enabled {
4953
return nil
5054
}
5155

52-
fn := filepath.Join(builddir, provenanceBundleFilename)
53-
f, err := os.OpenFile(fn, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
56+
// Get the artifact path in cache
57+
// Location() returns (path, exists) - during build it returns .tar path even if file doesn't exist yet
58+
artifactPath, exists := buildctx.LocalCache.Location(p)
59+
if artifactPath == "" {
60+
return fmt.Errorf("cannot determine cache location for %s", p.FullName())
61+
}
62+
63+
// Determine the actual artifact path (.tar.gz)
64+
// Location() returns .tar.gz if it exists, otherwise .tar
65+
if !exists {
66+
// Artifact doesn't exist yet - this shouldn't happen as provenance should be written after packaging
67+
log.WithField("package", p.FullName()).WithField("path", artifactPath).Warn("Writing provenance before artifact exists")
68+
}
69+
70+
// Ensure we use the .tar.gz extension
71+
if strings.HasSuffix(artifactPath, ".tar") && !strings.HasSuffix(artifactPath, ".tar.gz") {
72+
artifactPath = artifactPath + ".gz"
73+
} else if !strings.HasSuffix(artifactPath, ".tar.gz") && !strings.HasSuffix(artifactPath, ".tar") {
74+
artifactPath = artifactPath + ".tar.gz"
75+
}
76+
77+
// Write provenance alongside artifact: <artifact>.provenance.jsonl
78+
// This keeps provenance metadata separate from the artifact for determinism
79+
provenancePath := artifactPath + ".provenance.jsonl"
80+
81+
// Ensure directory exists
82+
dir := filepath.Dir(provenancePath)
83+
if err := os.MkdirAll(dir, 0755); err != nil {
84+
return fmt.Errorf("cannot create provenance directory for %s: %w", p.FullName(), err)
85+
}
86+
87+
f, err := os.OpenFile(provenancePath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
5488
if err != nil {
5589
return fmt.Errorf("cannot write provenance for %s: %w", p.FullName(), err)
5690
}
@@ -74,7 +108,7 @@ func writeProvenance(p *Package, buildctx *buildContext, builddir string, subjec
74108
}
75109
}
76110

77-
log.WithField("fn", fn).WithField("package", p.FullName()).Debug("wrote provenance bundle")
111+
log.WithField("path", provenancePath).WithField("package", p.FullName()).Debug("wrote provenance bundle to cache (outside tar.gz)")
78112

79113
return nil
80114
}
@@ -102,60 +136,37 @@ func (p *Package) getDependenciesProvenanceBundles(buildctx *buildContext, dst *
102136

103137
var ErrNoAttestationBundle error = fmt.Errorf("no attestation bundle found")
104138

105-
// AccessAttestationBundleInCachedArchive provides access to the attestation bundle in a cached build artifact.
106-
// If no such bundle exists, ErrNoAttestationBundle is returned.
139+
// fileExists checks if a file exists and is not a directory
140+
func fileExists(filename string) bool {
141+
info, err := os.Stat(filename)
142+
if err != nil {
143+
return false
144+
}
145+
return !info.IsDir()
146+
}
147+
148+
// AccessAttestationBundleInCachedArchive provides access to the attestation bundle for a cached build artifact.
149+
// Reads from <artifact>.provenance.jsonl (outside tar.gz).
150+
// If no bundle exists, ErrNoAttestationBundle is returned.
107151
func AccessAttestationBundleInCachedArchive(fn string, handler func(bundle io.Reader) error) (err error) {
108152
defer func() {
109153
if err != nil {
110-
err = fmt.Errorf("error extracting provenance bundle from %s: %w", fn, err)
154+
err = fmt.Errorf("error accessing provenance bundle for %s: %w", fn, err)
111155
}
112156
}()
113157

114-
f, err := os.Open(fn)
115-
if err != nil {
116-
return err
158+
provenancePath := fn + ".provenance.jsonl"
159+
if !fileExists(provenancePath) {
160+
return ErrNoAttestationBundle
117161
}
118-
defer f.Close()
119162

120-
g, err := gzip.NewReader(f)
163+
f, err := os.Open(provenancePath)
121164
if err != nil {
122165
return err
123166
}
124-
defer g.Close()
125-
126-
var bundleFound bool
127-
a := tar.NewReader(g)
128-
var hdr *tar.Header
129-
for {
130-
hdr, err = a.Next()
131-
if err == io.EOF {
132-
err = nil
133-
break
134-
}
135-
if err != nil {
136-
break
137-
}
138-
139-
if hdr.Name != "./"+provenanceBundleFilename && hdr.Name != "package/"+provenanceBundleFilename {
140-
continue
141-
}
142-
143-
err = handler(io.LimitReader(a, hdr.Size))
144-
if err != nil {
145-
return err
146-
}
147-
bundleFound = true
148-
break
149-
}
150-
if err != nil {
151-
return
152-
}
153-
154-
if !bundleFound {
155-
return ErrNoAttestationBundle
156-
}
167+
defer f.Close()
157168

158-
return nil
169+
return handler(f)
159170
}
160171

161172
func (p *Package) produceSLSAEnvelope(buildctx *buildContext, subjects []in_toto.Subject, buildStarted time.Time) (res *provenance.Envelope, err error) {

0 commit comments

Comments
 (0)