@@ -10,17 +10,19 @@ module PuppetReferences
1010 # Base for the component "release contents" tables rendered on the OpenVox
1111 # component-versions page. Each subclass names the upstream repo whose releases
1212 # drive the table and resolves one row per stable release from authoritative,
13- # structured component pins, so the tables are never hand-maintained. The shared
14- # GitHub releases/contents plumbing lives here so the per-table classes stay small.
13+ # structured metadata, so the tables are never hand-maintained. The shared
14+ # GitHub releases/contents plumbing lives here so the per-table classes stay
15+ # small. Agent and OpenBolt rows come from openvox-sbom-tools SBOMs (see
16+ # SbomReleaseTable); server and OpenVoxDB rows are still resolved from upstream
17+ # pins until their SBOMs land.
1518 #
1619 # Authenticates with ENV GITHUB_TOKEN/GH_TOKEN, falling back to `gh auth token`
17- # for local runs. A release whose component layout can't be resolved (e.g. very
18- # old tags that predate these pins ) is skipped with a warning; transient API
20+ # for local runs. A release whose components can't be resolved (e.g. a tag that
21+ # predates the data this table reads ) is skipped with a warning; transient API
1922 # failures raise so a bad run never overwrites the committed data file.
2023 class ReleaseTable
2124 API_ROOT = 'https://api.github.com'
2225 USER_AGENT = 'openvox-docs-release-table'
23- RUNTIME_REPO = 'OpenVoxProject/puppet-runtime'
2426
2527 # A resolved component value must look like a dotted version. This screens out
2628 # boundary releases that pin a component by git SHA or whose layout yields a
@@ -96,24 +98,6 @@ def release_tags(series:, min_version:)
9698 . reverse
9799 end
98100
99- # Files under puppet-runtime's configs/components at a given runtime version.
100- def runtime_files ( runtime_ver )
101- list_dir ( RUNTIME_REPO , 'configs/components' , runtime_ver )
102- end
103-
104- # Component version from puppet-runtime's <name>.json (.version) or the vanagon
105- # <name>.rb DSL. Recent components pin in JSON; older ones use the .rb DSL.
106- def component_version ( pattern , runtime_ver , files )
107- if ( json = files . find { |n | n . match? ( /\A #{ pattern } \. json\z / ) } )
108- return json_at ( RUNTIME_REPO , "configs/components/#{ json } " , runtime_ver ) [ 'version' ]
109- end
110-
111- rb = files . find { |n | n . match? ( /\A #{ pattern } \. rb\z / ) }
112- raise NotFound , "component /#{ pattern } / in runtime #{ runtime_ver } " unless rb
113-
114- raw ( RUNTIME_REPO , "configs/components/#{ rb } " , runtime_ver ) [ /pkg\. version\s +['"]([^'"]+)['"]/ , 1 ]
115- end
116-
117101 def json_at ( repo , path , ref )
118102 JSON . parse ( raw ( repo , path , ref ) )
119103 end
@@ -122,11 +106,6 @@ def raw(repo, path, ref)
122106 api_get ( "/repos/#{ repo } /contents/#{ path } ?ref=#{ ref } " , accept : 'application/vnd.github.raw' )
123107 end
124108
125- def list_dir ( repo , path , ref )
126- body = api_get ( "/repos/#{ repo } /contents/#{ path } ?ref=#{ ref } " , accept : 'application/vnd.github+json' )
127- JSON . parse ( body ) . map { |entry | entry [ 'name' ] }
128- end
129-
130109 def api_get ( path , accept :)
131110 uri = URI ( "#{ API_ROOT } #{ path } " )
132111 req = Net ::HTTP ::Get . new ( uri )
@@ -147,42 +126,95 @@ def token
147126 end
148127 end
149128
150- # openvox-agent: bundled OpenFact plus Ruby/OpenSSL/curl from the agent runtime.
151- #
152- # Only components actually bundled by the agent runtime project (agent-runtime-8.x)
153- # are reported. r10k is intentionally absent: its pin exists in puppet-runtime, but
154- # it is bundled by openbolt-runtime, not the agent, so it does not ship in
155- # openvox-agent (see OpenboltReleaseTable).
156- class AgentReleaseTable < ReleaseTable
129+ # Base for tables sourced from openvox-sbom-tools CycloneDX SBOMs rather than
130+ # scraping upstream pkg-DSL. Each release ships an SBOM committed at
131+ # lib/openvox/sbom-tools/sbom/<package>_<version>.cdx.json whose `.components[]`
132+ # carry already-resolved `{name, version}` pairs; a subclass maps the component
133+ # names it cares about to the table's columns. A release whose SBOM has not been
134+ # committed yet is skipped (NotFound), so the table tracks SBOM availability:
135+ # a newly tagged release appears once its SBOM lands upstream.
136+ class SbomReleaseTable < ReleaseTable
137+ SBOM_REPO = 'OpenVoxProject/openvox-sbom-tools'
138+ SBOM_DIR = 'lib/openvox/sbom-tools/sbom'
139+
140+ # SBOM package name; the file for a release is "<package>_<tag>.cdx.json".
141+ def sbom_package
142+ raise NotImplementedError , "#{ self . class } must define #sbom_package"
143+ end
144+
145+ # Ordered { column => component-name } map pulled from each release's SBOM.
146+ # Insertion order sets the column order of the generated row.
147+ def columns
148+ raise NotImplementedError , "#{ self . class } must define #columns"
149+ end
150+
151+ def row_for ( tag , _cache )
152+ resolved = sbom_components ( tag )
153+ row = { 'release' => tag }
154+ columns . each do |column , name |
155+ version = resolved [ name ]
156+ raise NotFound , "component #{ name . inspect } in #{ sbom_package } #{ tag } SBOM" unless version
157+
158+ row [ column ] = version
159+ end
160+ row
161+ end
162+
163+ private
164+
165+ # { component-name => version } for every component in a release's SBOM. The
166+ # underlying GitHub contents fetch raises NotFound when the SBOM file is absent
167+ # (a release without a committed SBOM), which skips the release.
168+ def sbom_components ( tag )
169+ sbom = json_at ( SBOM_REPO , "#{ SBOM_DIR } /#{ sbom_package } _#{ tag } .cdx.json" , 'main' )
170+ sbom . fetch ( 'components' ) . to_h { |component | [ component [ 'name' ] , component [ 'version' ] ] }
171+ end
172+ end
173+
174+ # openvox-agent: bundled OpenFact plus Ruby/OpenSSL/curl from the agent runtime,
175+ # read from the openvox-agent SBOM. The SBOM lists both `openssl` and
176+ # `openssl-fips`; we report the `openssl` runtime version to match the column.
177+ class AgentReleaseTable < SbomReleaseTable
157178 REPO = 'OpenVoxProject/openvox'
158179
159180 def repo
160181 REPO
161182 end
162183
163- def row_for ( tag , cache )
164- openfact = json_at ( REPO , 'packaging/configs/components/openfact.json' , tag ) [ 'ref' ] . delete_prefix ( 'refs/tags/' )
165- runtime = json_at ( REPO , 'packaging/configs/components/puppet-runtime.json' , tag ) [ 'version' ]
166- rt = ( cache [ runtime ] ||= runtime_versions ( runtime ) )
167- { 'release' => tag , 'openfact' => openfact , 'ruby' => rt [ :ruby ] , 'openssl' => rt [ :openssl ] , 'curl' => rt [ :curl ] }
184+ def sbom_package
185+ 'openvox-agent'
168186 end
169187
170- private
188+ def columns
189+ { 'openfact' => 'openfact' , 'ruby' => 'ruby' , 'openssl' => 'openssl' , 'curl' => 'curl' }
190+ end
191+ end
171192
172- def runtime_versions ( runtime_ver )
173- files = runtime_files ( runtime_ver )
174- {
175- ruby : component_version ( 'ruby-\d+\.\d+' , runtime_ver , files ) ,
176- openssl : component_version ( 'openssl-\d+\.\d+' , runtime_ver , files ) ,
177- curl : component_version ( 'curl' , runtime_ver , files ) ,
178- }
193+ # OpenBolt ships on its own 5.x line and bundles its own runtime (Ruby/OpenSSL
194+ # plus r10k) along with OpenVox itself (for `bolt apply`). The SBOM reports the
195+ # exact bundled OpenVox version resolved at build time, so the `openvox` column
196+ # is the resolved version rather than the gemspec requirement range.
197+ class OpenboltReleaseTable < SbomReleaseTable
198+ REPO = 'OpenVoxProject/openbolt'
199+
200+ def repo
201+ REPO
202+ end
203+
204+ def sbom_package
205+ 'openbolt'
206+ end
207+
208+ def columns
209+ { 'openvox' => 'openvox' , 'ruby' => 'ruby' , 'openssl' => 'openssl' , 'r10k' => 'r10k' }
179210 end
180211 end
181212
182213 # openvox-server: bundled JRuby, resolved through the server's pinned
183214 # jruby-utils -> jruby-deps "9.4.12.1-3" (the trailing "-N" packaging suffix is
184215 # stripped). Java is a supported requirement, not a pin, so it is hand-maintained
185- # on the docs page rather than resolved here.
216+ # on the docs page rather than resolved here. Stays on the upstream-pin scraper
217+ # until an openvox-server SBOM is published.
186218 class ServerReleaseTable < ReleaseTable
187219 SERVER_REPO = 'OpenVoxProject/openvox-server'
188220 JRUBY_UTILS_REPO = 'OpenVoxProject/jruby-utils'
@@ -226,52 +258,4 @@ def row_for(tag, _cache)
226258 { 'release' => tag }
227259 end
228260 end
229-
230- # OpenBolt ships on its own 5.x line, independent of the OpenVox major, and
231- # bundles its own runtime (Ruby/OpenSSL plus r10k). r10k is bundled here, not by
232- # the agent, which is why it appears on the OpenBolt table. OpenBolt also bundles
233- # OpenVox itself (for `bolt apply`); its gemspec declares the requirement as a
234- # range (e.g. "~> 8.0") and the exact version is resolved at build time, so the
235- # table shows the declared requirement rather than a resolved version.
236- class OpenboltReleaseTable < ReleaseTable
237- REPO = 'OpenVoxProject/openbolt'
238-
239- def repo
240- REPO
241- end
242-
243- def row_for ( tag , cache )
244- runtime = json_at ( REPO , 'packaging/configs/components/puppet-runtime.json' , tag ) [ 'version' ]
245- rt = ( cache [ runtime ] ||= runtime_versions ( runtime ) )
246- {
247- 'release' => tag , 'openvox' => openvox_requirement ( tag ) ,
248- 'ruby' => rt [ :ruby ] , 'openssl' => rt [ :openssl ] , 'r10k' => rt [ :r10k ] ,
249- }
250- end
251-
252- private
253-
254- # "openvox" is a dependency requirement (e.g. "~> 8.0"), not a resolved version.
255- def freeform_fields
256- %w[ openvox ]
257- end
258-
259- # OpenBolt's bundled-OpenVox requirement, from its gemspec.
260- def openvox_requirement ( tag )
261- gemspec = raw ( REPO , 'openbolt.gemspec' , tag )
262- req = gemspec [ /add_dependency\s +["']openvox["']\s *,\s *["']([^"']+)["']/ , 1 ]
263- raise NotFound , "openvox dependency in openbolt.gemspec@#{ tag } " unless req
264-
265- req
266- end
267-
268- def runtime_versions ( runtime_ver )
269- files = runtime_files ( runtime_ver )
270- {
271- ruby : component_version ( 'ruby-\d+\.\d+' , runtime_ver , files ) ,
272- openssl : component_version ( 'openssl-\d+\.\d+' , runtime_ver , files ) ,
273- r10k : component_version ( 'rubygem-r10k' , runtime_ver , files ) ,
274- }
275- end
276- end
277261end
0 commit comments