@@ -79,6 +79,22 @@ func NewBuilder(
7979 }
8080}
8181
82+ // PayloadHash returns the SBOM cache key for the given schematic/version/arch.
83+ //
84+ // It fetches the schematic to extract the extension list, then computes a
85+ // content hash that reflects only the inputs that affect the SPDX bundle
86+ // content. Callers should use this hash as a cache key so that schematics
87+ // differing only in non-SBOM fields (kernel args, config, etc.) share
88+ // cached bundles.
89+ func (b * Builder ) PayloadHash (ctx context.Context , schematicID , versionTag string , arch artifacts.Arch ) (string , error ) {
90+ sc , err := b .schematicFactory .Get (ctx , schematicID , b .authProvider )
91+ if err != nil {
92+ return "" , fmt .Errorf ("failed to get schematic: %w" , err )
93+ }
94+
95+ return Hash (sc .Customization .SystemExtensions .OfficialExtensions , versionTag , string (arch )), nil
96+ }
97+
8298// Build returns an SPDX bundle, building and caching if necessary.
8399func (b * Builder ) Build (ctx context.Context , schematicID , versionTag string , arch artifacts.Arch ) (storage.Bundle , error ) {
84100 // Normalize version tag
@@ -91,29 +107,32 @@ func (b *Builder) Build(ctx context.Context, schematicID, versionTag string, arc
91107 return nil , fmt .Errorf ("invalid version: %w" , err )
92108 }
93109
94- // Check cache first
95- if err := b .storage .Head (ctx , schematicID , versionTag , string (arch )); err == nil {
96- ctxlog .Logger (ctx , b .logger ).Debug ("SPDX bundle cache hit" , zap .String ("schematic" , schematicID ), zap .String ("version" , versionTag ), zap .String ("arch" , string (arch )))
97-
98- return b .storage .Get (ctx , schematicID , versionTag , string (arch ))
99- }
100-
101- // Verify access and fetch schematic data before entering singleflight.
102- // buildBundle runs with context.Background() (request context may be canceled),
103- // so ownership enforcement must happen here with the live request context.
110+ // Fetch schematic first: we need the extension list to derive the cache key.
111+ // Ownership enforcement happens here with the live request context, before
112+ // entering singleflight which uses a detached context.
104113 sc , err := b .schematicFactory .Get (ctx , schematicID , b .authProvider )
105114 if err != nil {
106115 return nil , fmt .Errorf ("failed to get schematic: %w" , err )
107116 }
108117
109- // Build the bundle using singleflight to prevent duplicate work
110- cacheKey := CacheTag (schematicID , versionTag , string (arch ))
118+ // Compute cache key from only the inputs that affect the SBOM content
119+ // (extensions list, version, architecture), so that schematics differing
120+ // in other fields share the same cached bundle.
121+ sbomHash := Hash (sc .Customization .SystemExtensions .OfficialExtensions , versionTag , string (arch ))
111122
123+ // Check cache first
124+ if err := b .storage .Head (ctx , sbomHash ); err == nil {
125+ ctxlog .Logger (ctx , b .logger ).Debug ("SPDX bundle cache hit" , zap .String ("schematic" , schematicID ), zap .String ("version" , versionTag ), zap .String ("arch" , string (arch )))
126+
127+ return b .storage .Get (ctx , sbomHash )
128+ }
129+
130+ // Build the bundle using singleflight to prevent duplicate work
112131 // carry the request ID into the detached build so its logs keep the request_id.
113132 reqID := ctxlog .RequestID (ctx )
114133
115- resultCh := b .sf .DoChan (cacheKey , func () (any , error ) { //nolint:contextcheck
116- return nil , b .buildBundle (reqID , sc , schematicID , versionTag , arch )
134+ resultCh := b .sf .DoChan (sbomHash , func () (any , error ) { //nolint:contextcheck
135+ return nil , b .buildBundle (reqID , sc , schematicID , sbomHash , versionTag , arch )
117136 })
118137
119138 select {
@@ -125,14 +144,14 @@ func (b *Builder) Build(ctx context.Context, schematicID, versionTag string, arc
125144 }
126145
127146 // Retrieve from cache after building
128- return b .storage .Get (ctx , schematicID , versionTag , string ( arch ) )
147+ return b .storage .Get (ctx , sbomHash )
129148 }
130149}
131150
132151// buildBundle creates and stores an SPDX bundle for a single architecture.
133152// sc must be pre-fetched by the caller (Build) using the live request context,
134153// since this function runs inside singleflight with context.Background().
135- func (b * Builder ) buildBundle (reqID string , sc * schematicpkg.Schematic , schematicID , versionTag string , arch artifacts.Arch ) error {
154+ func (b * Builder ) buildBundle (reqID string , sc * schematicpkg.Schematic , schematicID , sbomHash , versionTag string , arch artifacts.Arch ) error {
136155 // Use a fresh context since we're in singleflight, but carry the
137156 // request ID so build logs keep the request_id.
138157 ctx := ctxlog .WithRequestID (context .Background (), reqID )
@@ -173,8 +192,8 @@ func (b *Builder) buildBundle(reqID string, sc *schematicpkg.Schematic, schemati
173192 return fmt .Errorf ("failed to create SPDX JSON document: %w" , err )
174193 }
175194
176- // Store the bundle
177- if err := b .storage .Put (ctx , schematicID , versionTag , string ( arch ) , jsonReader , size ); err != nil {
195+ // Store the bundle keyed by the SBOM content hash
196+ if err := b .storage .Put (ctx , sbomHash , jsonReader , size ); err != nil {
178197 return fmt .Errorf ("failed to store SPDX bundle: %w" , err )
179198 }
180199
0 commit comments