Automate Ironic release version detection#87
Automate Ironic release version detection#87elfosardo wants to merge 1 commit intoOpenStackweb:masterfrom
Conversation
📝 WalkthroughWalkthroughAdds Gatsby source logic that detects the latest OpenStack Ironic series, fetches and parses deliverable YAML to create an IronicRelease GraphQL node, surfaces that node to the index template and CMS preview, and includes a CLI test script plus npm script entries to validate the detection/fetch flow. Changes
Sequence DiagramsequenceDiagram
participant Build as Gatsby Build
participant GitHub as GitHub Raw Content
participant Parser as Parser/Regex/YAML
participant GraphQL as Gatsby GraphQL
participant Template as Index Template
Build->>GitHub: GET /releases/.../series_status.yaml
GitHub-->>Build: series_status.yaml
Build->>Parser: extract series order & mapping
loop detect series
Build->>GitHub: HEAD deliverables/<series>/ironic.yaml
GitHub-->>Build: 200 / 404
end
alt deliverable found
Build->>GitHub: GET deliverables/<detected>/ironic.yaml
GitHub-->>Build: ironic.yaml (YAML)
Build->>Parser: parse versions (- version: x.y.z) & extract release hash
Parser-->>Build: latest version, hash
Build->>GitHub: GET commits/<hash> (optional)
GitHub-->>Build: commit metadata (date)
Build->>GraphQL: createNode(IronicRelease {version, releaseNotesUrl, publishedAt, htmlUrl})
else fallback / error
Build->>GraphQL: createNode(IronicRelease {fallback data})
end
Template->>GraphQL: query ironicRelease
GraphQL-->>Template: return IronicRelease data
Template->>Template: compute dynamicHeader and render
Estimated Code Review Effort🎯 4 (Complex) | ⏱️ ~45 minutes Poem
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
📝 Coding Plan
Comment |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@gatsby-node.js`:
- Around line 148-157: The current regex and selection logic (yamlContent,
releaseMatches, latestVersionMatch, version) only capture numeric parts and
blindly picks the last match, which drops prerelease suffixes; change the regex
to capture the full token (e.g., /- version: ([^\s]+)/g) so you get complete
version strings, then build an array of those captured versions and iterate from
newest to oldest to pick the first entry that is not a prerelease (detect
prereleases with a simple pattern like /[A-Za-z-]/ or a semver prerelease check
/-(alpha|beta|rc|dev)|[A-Za-z]/i) and assign that to version; this ensures
prerelease tags (e.g., 34.0.0rc1) aren’t truncated and are skipped in favor of
the latest non-prerelease.
- Around line 139-145: The current catch around the axios.get for the
`ironic.yaml` fetch unconditionally assumes a 404 and switches `releaseSeries`
to 'epoxy' and retries, which misattributes timeouts/5xx/DNS errors and issues
an unbounded second request; update the catch in the try/axios.get for
`ironic.yaml` to check the error's HTTP status (e.g. error.response &&
error.response.status === 404) before setting `releaseSeries = 'epoxy'` and
retrying, otherwise rethrow or surface the original error; also add a bounded
timeout/options object to both axios.get calls (pass a timeout in the config) so
the requests are limited.
In `@package.json`:
- Around line 59-60: Remove the live GitHub/network-dependent check from the
default npm test script so running `npm test` remains deterministic: change the
"test" npm script to a local/deterministic test runner (or a harmless
placeholder) and keep the networked smoke check only under "test:release" which
should run `node test-release-fetch.js`; update the package.json scripts so
"test" does not invoke test-release-fetch.js but "test:release" still does, and
verify test tooling (the "test" script) runs the intended local checks.
In `@test-release-fetch.js`:
- Around line 13-39: The test duplicates parsing and unconditionally fails on
gazpacho instead of using the same fallback logic as gatsby-node.js; update
test-release-fetch.js to import and use the shared helper (or replicate its
behavior) so the code that fetches/parses the YAML uses the same
fallback-from-gazpacho-to-epoxy logic (only fall back on a 404) instead of
immediately throwing. Locate the current fetch/parse variables (response,
yamlContent, releaseMatches, latestVersion and the epoxy equivalents) and
replace their direct axios/get + regex flow with a call to the shared helper
used by gatsby-node.js (or at minimum add a 404 check after the initial
axios.get and only then fetch epoxy) so test behavior matches production.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 63a62524-20e3-4c60-b018-10d96988ee0a
📒 Files selected for processing (6)
gatsby-node.jspackage.jsonsrc/cms/preview-templates/IndexPagePreview.jssrc/pages/index.mdsrc/templates/index-page.jstest-release-fetch.js
| "test": "node test-release-fetch.js", | ||
| "test:release": "node test-release-fetch.js" |
There was a problem hiding this comment.
Keep the live release smoke check out of the default npm test.
Making npm test depend on a live GitHub request will make local and CI runs flaky on transient network failures or rate limits, even though the site build already has a fallback node. I’d keep this behind npm run test:release and reserve test for deterministic checks.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@package.json` around lines 59 - 60, Remove the live GitHub/network-dependent
check from the default npm test script so running `npm test` remains
deterministic: change the "test" npm script to a local/deterministic test runner
(or a harmless placeholder) and keep the networked smoke check only under
"test:release" which should run `node test-release-fetch.js`; update the
package.json scripts so "test" does not invoke test-release-fetch.js but
"test:release" still does, and verify test tooling (the "test" script) runs the
intended local checks.
ade0c3e to
60d2d2e
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
gatsby-node.js (1)
156-157:⚠️ Potential issue | 🟠 MajorBound outbound calls and only skip series on 404.
Line 180 and Line 211 currently swallow all request failures as “try next series”. Timeouts/5xx/DNS issues can silently downgrade to an older series. Also, all these axios calls are unbounded, which can stall builds.
Suggested hardening
+ const requestConfig = { timeout: 10000 } - const response = await axios.get('https://api.github.com/repos/openstack/releases/contents/deliverables') + const response = await axios.get( + 'https://api.github.com/repos/openstack/releases/contents/deliverables', + requestConfig + ) - await axios.head(`https://raw.githubusercontent.com/openstack/releases/master/deliverables/${series}/ironic.yaml`) + await axios.head( + `https://raw.githubusercontent.com/openstack/releases/master/deliverables/${series}/ironic.yaml`, + requestConfig + ) console.log(`✅ Found Ironic releases in series: ${series}`) return series } catch (error) { - // Series doesn't have ironic.yaml, try next - continue + if (error.response?.status === 404) continue + throw error } - response = await axios.get(`https://raw.githubusercontent.com/openstack/releases/master/deliverables/${series}/ironic.yaml`) + response = await axios.get( + `https://raw.githubusercontent.com/openstack/releases/master/deliverables/${series}/ironic.yaml`, + requestConfig + ) releaseSeries = series break } catch (error) { - continue + if (error.response?.status === 404) continue + throw error } - response = await axios.get(`https://raw.githubusercontent.com/openstack/releases/master/deliverables/${releaseSeries}/ironic.yaml`) + response = await axios.get( + `https://raw.githubusercontent.com/openstack/releases/master/deliverables/${releaseSeries}/ironic.yaml`, + requestConfig + )#!/bin/bash # Verify outbound request config and catch behavior in gatsby-node.js rg -n 'axios\.(get|head)\(' gatsby-node.js -C2 rg -n 'catch \(error\)' gatsby-node.js -C3Also applies to: 176-183, 208-213, 221-221
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@gatsby-node.js` around lines 156 - 157, The axios calls (e.g., the const response = await axios.get(...) that sets discoveredSeries and other axios.get/axios.head usages) must be bounded and must only treat HTTP 404 as "skip series" — add a request timeout (via axios.create or per-request timeout) to avoid unbounded hangs, and in each catch inspect error.response?.status === 404 to continue, otherwise rethrow or fail the build (for timeouts, 5xx, DNS/network errors check error.code or missing error.response and propagate). Update all axios.get/axios.head call sites so they use the timeout-configured client and the described error handling instead of swallowing all errors.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@gatsby-node.js`:
- Around line 163-167: The code is incorrectly truncating discoveredSeries to
the first three entries; update the assignment that builds allSeries so it uses
the full discoveredSeries array instead of discoveredSeries.slice(0, 3) (i.e.,
change allSeries = [...discoveredSeries.slice(0, 3), ...knownSeries] to include
all discoveredSeries) and remove the console message truncation so it logs the
full discoveredSeries (or a sensible untruncated representation); apply the same
change in test-release-fetch.js where discoveredSeries is being sliced at entry
41 to ensure consistency.
---
Duplicate comments:
In `@gatsby-node.js`:
- Around line 156-157: The axios calls (e.g., the const response = await
axios.get(...) that sets discoveredSeries and other axios.get/axios.head usages)
must be bounded and must only treat HTTP 404 as "skip series" — add a request
timeout (via axios.create or per-request timeout) to avoid unbounded hangs, and
in each catch inspect error.response?.status === 404 to continue, otherwise
rethrow or fail the build (for timeouts, 5xx, DNS/network errors check
error.code or missing error.response and propagate). Update all
axios.get/axios.head call sites so they use the timeout-configured client and
the described error handling instead of swallowing all errors.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2f64512a-259d-41fc-97b1-5b69505e8d12
📒 Files selected for processing (6)
gatsby-node.jspackage.jsonsrc/cms/preview-templates/IndexPagePreview.jssrc/pages/index.mdsrc/templates/index-page.jstest-release-fetch.js
🚧 Files skipped from review as they are similar to previous changes (3)
- src/pages/index.md
- package.json
- test-release-fetch.js
60d2d2e to
c2bac9b
Compare
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
test-release-fetch.js (1)
41-44:⚠️ Potential issue | 🟡 MinorFallback series list is inconsistent with
gatsby-node.js.The test's fallback (
['gazpacho', 'epoxy', 'dalmatian', 'caracal']) is missing'hibiscus'and'flamingo'that are present ingatsby-node.js(lines 165-167). This means the test can fail in scenarios where the build would succeed, defeating the purpose of validating the production code path.Sync the fallback lists or, better, extract them into a shared module.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test-release-fetch.js` around lines 41 - 44, The test's fallback arrays (fallbackOrder and fallbackData) are missing series present in the production list, causing mismatch with gatsby-node.js; update test-release-fetch.js to use the same canonical fallback series (add 'hibiscus' and 'flamingo' into fallbackOrder and include their versions in fallbackData) or better yet import the shared FALLBACK_SERIES/FALLBACK_DATA constant from a common module used by gatsby-node.js so both test and production use the identical source of truth.
🧹 Nitpick comments (4)
gatsby-node.js (2)
297-304: Fallback version will become stale.The hardcoded fallback
version: '34.0.0'will drift behind actual releases over time. Since this only activates when all network fetches fail, staleness may go unnoticed.Consider logging a warning at a higher severity level or adding a comment with the date this fallback was last updated to prompt periodic review.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@gatsby-node.js` around lines 297 - 304, The hardcoded fallbackData object (version '34.0.0') in gatsby-node.js will become stale; update the fallback handling by adding a clear timestamped comment (e.g., "last updated YYYY-MM-DD") next to the fallbackData declaration and emit a warning-level log (or throw/report) when the fallback is used so failures are visible; reference the fallbackData constant and the code path that uses it so the log message includes context like "using fallbackData version" to prompt periodic review.
135-135: Add request timeouts to all axios calls.Network requests to GitHub (lines 135, 192, 231, 245) have no timeout configured. A slow or unresponsive upstream can stall the Gatsby build indefinitely.
Suggested fix
+const AXIOS_CONFIG = { timeout: 15000 }; + async function getSeriesStatusData() { try { console.log('📋 Fetching OpenStack series status data...'); - const response = await axios.get('https://raw.githubusercontent.com/openstack/releases/master/data/series_status.yaml'); + const response = await axios.get('https://raw.githubusercontent.com/openstack/releases/master/data/series_status.yaml', AXIOS_CONFIG);Apply the same config to
axios.head(line 192) andaxios.getcalls (lines 231, 245).Also applies to: 192-192, 231-231, 245-245
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@gatsby-node.js` at line 135, The axios calls (e.g., the call assigned to response: const response = await axios.get(...), the axios.head(...) call, and the other axios.get(...) calls) lack timeouts and can hang builds; add a timeout option (for example { timeout: 10000 }) to each axios request or create a shared axios instance with a default timeout and use that instance for all requests so every call includes a timeout configuration (apply to the axios.get used for series_status.yaml, the axios.head call, and the other axios.get calls referenced).src/templates/index-page.js (1)
207-212:publishedAtandhtmlUrlare fetched but unused.The GraphQL query retrieves
publishedAtandhtmlUrlfromironicRelease, but neither is referenced in the template. If these are reserved for future use, that's fine—otherwise remove them to keep the query minimal.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/templates/index-page.js` around lines 207 - 212, The GraphQL selection set for ironicRelease includes fields publishedAt and htmlUrl that are not used in the template; either remove these two fields from the ironicRelease selection in the GraphQL query in index-page.js to keep the query minimal, or if you intend to display them, wire them into the template where the ironicRelease data is rendered (e.g., use publishedAt for a release date display and htmlUrl for a link). Ensure the change targets the ironicRelease selection block so unused fields are dropped or consumed.test-release-fetch.js (1)
12-12: Add request timeouts to prevent indefinite hangs.All
axios.getandaxios.headcalls lack timeouts. If GitHub is slow or unresponsive, the test will hang indefinitely.Suggested fix
+const REQUEST_CONFIG = { timeout: 15000 }; + async function getSeriesStatusData() { try { console.log('📋 Fetching OpenStack series status data...'); - const response = await axios.get('https://raw.githubusercontent.com/openstack/releases/master/data/series_status.yaml'); + const response = await axios.get('https://raw.githubusercontent.com/openstack/releases/master/data/series_status.yaml', REQUEST_CONFIG);Apply similarly to
axios.head(line 62) andaxios.getcalls (lines 96, 116).Also applies to: 62-62, 96-96, 116-116
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@test-release-fetch.js` at line 12, The axios requests in test-release-fetch.js (the axios.get that assigns to response and the axios.head/other axios.get invocations) lack timeouts and can hang; add a timeout option (e.g., { timeout: 10000 }) to each axios.get and axios.head call or create an axios instance with a default timeout and use it throughout (locate the calls where response is assigned from axios.get and the axios.head at line ~62 plus the other axios.get calls around lines ~96 and ~116 and apply the timeout option or swap to the configured axios instance).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@gatsby-node.js`:
- Line 273: The publishedAt field is being set to the build timestamp
(publishedAt: new Date().toISOString()) which misrepresents the release date;
update the node creation logic (where publishedAt is assigned) to instead: parse
the real release date from the source YAML/frontmatter if present and assign
that value to publishedAt, or if no release date exists set publishedAt to null,
or rename the field to fetchedAt (and update any downstream consumers) to
clearly indicate it's the build fetch time—make the change at the location that
constructs the node/object containing publishedAt so consumers get correct or
explicitly-null/renamed semantics.
---
Duplicate comments:
In `@test-release-fetch.js`:
- Around line 41-44: The test's fallback arrays (fallbackOrder and fallbackData)
are missing series present in the production list, causing mismatch with
gatsby-node.js; update test-release-fetch.js to use the same canonical fallback
series (add 'hibiscus' and 'flamingo' into fallbackOrder and include their
versions in fallbackData) or better yet import the shared
FALLBACK_SERIES/FALLBACK_DATA constant from a common module used by
gatsby-node.js so both test and production use the identical source of truth.
---
Nitpick comments:
In `@gatsby-node.js`:
- Around line 297-304: The hardcoded fallbackData object (version '34.0.0') in
gatsby-node.js will become stale; update the fallback handling by adding a clear
timestamped comment (e.g., "last updated YYYY-MM-DD") next to the fallbackData
declaration and emit a warning-level log (or throw/report) when the fallback is
used so failures are visible; reference the fallbackData constant and the code
path that uses it so the log message includes context like "using fallbackData
version" to prompt periodic review.
- Line 135: The axios calls (e.g., the call assigned to response: const response
= await axios.get(...), the axios.head(...) call, and the other axios.get(...)
calls) lack timeouts and can hang builds; add a timeout option (for example {
timeout: 10000 }) to each axios request or create a shared axios instance with a
default timeout and use that instance for all requests so every call includes a
timeout configuration (apply to the axios.get used for series_status.yaml, the
axios.head call, and the other axios.get calls referenced).
In `@src/templates/index-page.js`:
- Around line 207-212: The GraphQL selection set for ironicRelease includes
fields publishedAt and htmlUrl that are not used in the template; either remove
these two fields from the ironicRelease selection in the GraphQL query in
index-page.js to keep the query minimal, or if you intend to display them, wire
them into the template where the ironicRelease data is rendered (e.g., use
publishedAt for a release date display and htmlUrl for a link). Ensure the
change targets the ironicRelease selection block so unused fields are dropped or
consumed.
In `@test-release-fetch.js`:
- Line 12: The axios requests in test-release-fetch.js (the axios.get that
assigns to response and the axios.head/other axios.get invocations) lack
timeouts and can hang; add a timeout option (e.g., { timeout: 10000 }) to each
axios.get and axios.head call or create an axios instance with a default timeout
and use it throughout (locate the calls where response is assigned from
axios.get and the axios.head at line ~62 plus the other axios.get calls around
lines ~96 and ~116 and apply the timeout option or swap to the configured axios
instance).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 5d5132ca-dbc0-47ff-982d-ea1d3732237f
📒 Files selected for processing (6)
gatsby-node.jspackage.jsonsrc/cms/preview-templates/IndexPagePreview.jssrc/pages/index.mdsrc/templates/index-page.jstest-release-fetch.js
🚧 Files skipped from review as they are similar to previous changes (3)
- src/pages/index.md
- package.json
- src/cms/preview-templates/IndexPagePreview.js
Replace static version hardcoding with fully dynamic fetching from OpenStack releases repository at build time. The website now automatically displays the latest Ironic version with actual release dates and handles new release series without any manual updates. - Add dynamic series detection from OpenStack series_status.yaml - Implement automatic series-to-version mapping (e.g., gazpacho → 2026.1) - Add sourceNodes function to fetch latest release at build time - Fetch actual release dates from git commit timestamps via GitHub API - Update index page template to use dynamic release data - Add comprehensive fallback mechanisms for API failures - Update CMS preview template with mock release data - Add test script to verify dynamic detection and date fetching - Remove all hardcoded series names and version mappings The system now fetches series list, version mappings, and actual release dates dynamically, making it completely self-maintaining for future OpenStack releases. Version and date are fetched once per build and baked into static HTML for optimal performance. Assisted-By: Claude 4.6 Opus High
c2bac9b to
2f22570
Compare
There was a problem hiding this comment.
🧹 Nitpick comments (2)
src/cms/preview-templates/IndexPagePreview.js (1)
9-16: Mock data includesreleaseSerieswhich is not in the actual GraphQL query.The mock includes
releaseSeries: "gazpacho", but theironicReleaseGraphQL query inindex-page.jsdoesn't fetch this field. This inconsistency could cause confusion if someone adds code that usesreleaseSeriesafter testing in the CMS preview—it would work in preview but fail in production.Consider aligning the mock data fields with the actual GraphQL query, or add
releaseSeriesto the query if it's intended for future use.♻️ Suggested fix to align with GraphQL query
const mockReleaseData = { version: "34.0.0", releaseNotesUrl: "https://docs.openstack.org/releasenotes/ironic/2026.1.html#relnotes-34-0-0", publishedAt: null, // Actual release date not available htmlUrl: "https://github.com/openstack/releases/blob/master/deliverables/gazpacho/ironic.yaml", - releaseSeries: "gazpacho", };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/cms/preview-templates/IndexPagePreview.js` around lines 9 - 16, The mockReleaseData object in IndexPagePreview.js includes releaseSeries which is not requested by the ironicRelease GraphQL query in index-page.js; either remove releaseSeries from the mock to match the actual schema or add releaseSeries to the ironicRelease query (and associated type/resolver) if you intend to use it in production. Locate mockReleaseData in IndexPagePreview.js and either drop the releaseSeries property or update the ironicRelease query in index-page.js to request releaseSeries so both preview and production data shapes match.src/templates/index-page.js (1)
207-212:publishedAtandhtmlUrlare fetched but unused.The GraphQL query fetches
publishedAtandhtmlUrl, but onlyversionandreleaseNotesUrlare used in thedynamicHeaderlogic. If these fields are reserved for future features, this is fine. Otherwise, consider removing them to keep the query minimal.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/templates/index-page.js` around lines 207 - 212, The GraphQL query includes unused fields publishedAt and htmlUrl on the ironicRelease selection while the dynamicHeader logic only uses version and releaseNotesUrl; remove publishedAt and htmlUrl from the query (or alternatively use them in dynamicHeader if intended) so the query only requests necessary fields—locate the ironicRelease selection in the GraphQL query within index-page.js and delete publishedAt and htmlUrl (or update dynamicHeader to reference those fields if they are required).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Nitpick comments:
In `@src/cms/preview-templates/IndexPagePreview.js`:
- Around line 9-16: The mockReleaseData object in IndexPagePreview.js includes
releaseSeries which is not requested by the ironicRelease GraphQL query in
index-page.js; either remove releaseSeries from the mock to match the actual
schema or add releaseSeries to the ironicRelease query (and associated
type/resolver) if you intend to use it in production. Locate mockReleaseData in
IndexPagePreview.js and either drop the releaseSeries property or update the
ironicRelease query in index-page.js to request releaseSeries so both preview
and production data shapes match.
In `@src/templates/index-page.js`:
- Around line 207-212: The GraphQL query includes unused fields publishedAt and
htmlUrl on the ironicRelease selection while the dynamicHeader logic only uses
version and releaseNotesUrl; remove publishedAt and htmlUrl from the query (or
alternatively use them in dynamicHeader if intended) so the query only requests
necessary fields—locate the ironicRelease selection in the GraphQL query within
index-page.js and delete publishedAt and htmlUrl (or update dynamicHeader to
reference those fields if they are required).
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 2357dbfc-0bef-4152-a84c-68636e0b0a7d
📒 Files selected for processing (6)
gatsby-node.jspackage.jsonsrc/cms/preview-templates/IndexPagePreview.jssrc/pages/index.mdsrc/templates/index-page.jstest-release-fetch.js
✅ Files skipped from review due to trivial changes (1)
- test-release-fetch.js
🚧 Files skipped from review as they are similar to previous changes (3)
- src/pages/index.md
- package.json
- gatsby-node.js
Replace static version hardcoding with fully dynamic fetching from OpenStack
releases repository at build time. The website now automatically displays
the latest Ironic version and handles new release series without any manual
updates.
The system fetches both series list and version mappings dynamically,
making it completely self-maintaining for future OpenStack releases.
Version is fetched once per build and baked into static HTML for optimal
performance.
Assisted-By: Claude 4.6 Opus High
Summary by CodeRabbit
New Features
Content Changes
Tests
Chores