11package leeway
22
33import (
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.
4751func 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
103137var 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.
107151func 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
161172func (p * Package ) produceSLSAEnvelope (buildctx * buildContext , subjects []in_toto.Subject , buildStarted time.Time ) (res * provenance.Envelope , err error ) {
0 commit comments