Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
223 changes: 222 additions & 1 deletion gatsby-node.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
const _ = require('lodash')
const path = require('path')
const { createFilePath } = require('gatsby-source-filesystem')
const axios = require('axios')

exports.createSchemaCustomization = ({ actions }) => {
const { createTypes } = actions
Expand All @@ -15,11 +16,18 @@ exports.createSchemaCustomization = ({ actions }) => {
subTitle: String
company: String
}

type BlogCategory {
label: String
id: String
}

type IronicRelease implements Node {
version: String!
releaseNotesUrl: String!
publishedAt: Date @dateformat
htmlUrl: String!
}
`

createTypes(typeDefs)
Expand Down Expand Up @@ -121,6 +129,219 @@ exports.createPages = ({ actions, graphql }) => {
})
}

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')

// Simple YAML parsing for series data
const yamlContent = response.data
const seriesMatches = yamlContent.match(/- name: ([^\s]+)\s+release-id: ([^\s]+)/g)

if (!seriesMatches) {
throw new Error('Could not parse series status data')
}

const seriesData = {}
const seriesOrder = []

seriesMatches.forEach(match => {
const nameMatch = match.match(/name: ([^\s]+)/)
const idMatch = match.match(/release-id: ([^\s]+)/)

if (nameMatch && idMatch) {
const name = nameMatch[1]
const releaseId = idMatch[1]
seriesData[name] = releaseId
seriesOrder.push(name) // This gives us newest-first order from the YAML
}
})

console.log(`✅ Found ${Object.keys(seriesData).length} series in OpenStack data`)
return { seriesData, seriesOrder }
} catch (error) {
console.log('⚠️ Could not fetch series status, using fallback data')
// Fallback to known series if API fails
const fallbackOrder = [
'hibiscus', 'gazpacho', 'flamingo', 'epoxy', 'dalmatian', 'caracal',
'bobcat', 'antelope', 'zed', 'yoga', 'xena', 'wallaby', 'victoria', 'ussuri'
]
const fallbackData = {
'hibiscus': '2026.2', 'gazpacho': '2026.1', 'flamingo': '2025.2', 'epoxy': '2025.1',
'dalmatian': '2024.2', 'caracal': '2024.1', 'bobcat': '2023.2', 'antelope': '2023.1',
'zed': '2022.2', 'yoga': '2022.1', 'xena': '2021.2', 'wallaby': '2021.1',
'victoria': '2020.2', 'ussuri': '2020.1'
}
return { seriesData: fallbackData, seriesOrder: fallbackOrder }
}
}

async function getLatestReleaseSeries() {
try {
console.log('🔍 Auto-detecting latest OpenStack release series...')

// Get dynamic series data from OpenStack
const { seriesData, seriesOrder } = await getSeriesStatusData()
let knownSeries = seriesOrder

console.log(`📋 Checking series in order: ${knownSeries.slice(0, 5).join(', ')}${knownSeries.length > 5 ? '...' : ''}`)

// Try each series until we find one with ironic.yaml
for (const series of knownSeries) {
try {
await axios.head(`https://raw.githubusercontent.com/openstack/releases/master/deliverables/${series}/ironic.yaml`)
console.log(`✅ Found Ironic releases in series: ${series}`)
return { series, seriesData }
} catch (error) {
// Series doesn't have ironic.yaml, try next
continue
}
}

throw new Error('No ironic.yaml found in any release series')
} catch (error) {
console.log('⚠️ Auto-detection failed, using known fallbacks')
// Return known good series as fallbacks with basic mapping
const fallbackData = {
'gazpacho': '2026.1', 'epoxy': '2025.1', 'dalmatian': '2024.2', 'caracal': '2024.1'
}
return {
series: ['gazpacho', 'epoxy', 'dalmatian', 'caracal'],
seriesData: fallbackData
}
}
}

exports.sourceNodes = async ({ actions, createNodeId, createContentDigest }) => {
const { createNode } = actions

try {
// Auto-detect the latest release series with dynamic version mapping
const detectionResult = await getLatestReleaseSeries()
let response
let releaseSeries
let seriesVersionMap

if (Array.isArray(detectionResult.series)) {
// Fallback mode - try known series
console.log('🔄 Trying fallback series...')
seriesVersionMap = detectionResult.seriesData
for (const series of detectionResult.series) {
try {
response = await axios.get(`https://raw.githubusercontent.com/openstack/releases/master/deliverables/${series}/ironic.yaml`)
releaseSeries = series
break
} catch (error) {
continue
}
}
if (!response) {
throw new Error('All fallback series failed')
}
} else {
// Auto-detection succeeded
releaseSeries = detectionResult.series
seriesVersionMap = detectionResult.seriesData
response = await axios.get(`https://raw.githubusercontent.com/openstack/releases/master/deliverables/${releaseSeries}/ironic.yaml`)
}

// Parse YAML content (simple parsing for releases section)
const yamlContent = response.data
const releaseMatches = yamlContent.match(/- version: ([\d.]+)/g)
if (!releaseMatches || releaseMatches.length === 0) {
throw new Error('No releases found in YAML')
}

// Get the latest version (last one in the list)
const latestVersionMatch = releaseMatches[releaseMatches.length - 1]
const version = latestVersionMatch.replace('- version: ', '')

// Extract git hash for the latest version to get actual release date
let publishedAt = null
try {
// Find the git hash for this version
const hashPattern = new RegExp(`- version: ${version.replace(/\./g, '\\.')}[\\s\\S]*?hash: ([a-f0-9]+)`, 'i')
const hashMatch = yamlContent.match(hashPattern)

if (hashMatch && hashMatch[1]) {
const gitHash = hashMatch[1]
console.log(`🔍 Found git hash for ${version}: ${gitHash.substring(0, 8)}...`)

// Get commit date from GitHub API
const commitResponse = await axios.get(`https://api.github.com/repos/openstack/ironic/commits/${gitHash}`)
publishedAt = commitResponse.data.commit.committer.date
console.log(`📅 Release date for ${version}: ${publishedAt}`)
}
} catch (error) {
console.log(`⚠️ Could not fetch release date for ${version}: ${error.message}`)
}

// Generate release notes URL using dynamic series mapping
let seriesVersion = seriesVersionMap[releaseSeries]
if (!seriesVersion) {
console.log(`⚠️ Unknown series '${releaseSeries}', using generic release notes URL`)
seriesVersion = 'latest'
}

const releaseNotesUrl = seriesVersion === 'latest'
? `https://docs.openstack.org/releasenotes/ironic/latest.html#relnotes-${version.replace(/\./g, '-')}`
: `https://docs.openstack.org/releasenotes/ironic/${seriesVersion}.html#relnotes-${version.replace(/\./g, '-')}`

const nodeData = {
version,
releaseNotesUrl,
publishedAt,
htmlUrl: `https://github.com/openstack/releases/blob/master/deliverables/${releaseSeries}/ironic.yaml`,
releaseSeries,
}

const nodeContent = JSON.stringify(nodeData)

const nodeMeta = {
id: createNodeId('ironic-latest-release'),
parent: null,
children: [],
internal: {
type: 'IronicRelease',
content: nodeContent,
contentDigest: createContentDigest(nodeData),
},
}

const node = Object.assign({}, nodeData, nodeMeta)
createNode(node)

console.log(`✅ Fetched latest Ironic release: ${version} (${releaseSeries} series)`)
} catch (error) {
console.error('❌ Failed to fetch latest Ironic release:', error.message)
// Fallback to current known version if all APIs fail
const fallbackData = {
version: '34.0.0',
releaseNotesUrl: 'https://docs.openstack.org/releasenotes/ironic/latest.html#relnotes-34-0-0',
publishedAt: null,
htmlUrl: 'https://docs.openstack.org/releasenotes/ironic/',
releaseSeries: 'fallback',
}

const nodeContent = JSON.stringify(fallbackData)
const nodeMeta = {
id: createNodeId('ironic-latest-release'),
parent: null,
children: [],
internal: {
type: 'IronicRelease',
content: nodeContent,
contentDigest: createContentDigest(fallbackData),
},
}

const node = Object.assign({}, fallbackData, nodeMeta)
createNode(node)

console.log('⚠️ Using fallback version: 34.0.0')
}
}

exports.onCreateNode = ({ node, actions, getNode }) => {
const { createNodeField } = actions

Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,8 @@
"build": "npm run clean && gatsby build",
"develop": "npm run clean && gatsby develop",
"format": "prettier --trailing-comma es5 --no-semi --single-quote --write \"{gatsby-*.js,src/**/*.js}\"",
"test": "echo \"Error: no test specified\" && exit 1"
"test": "node test-release-fetch.js",
"test:release": "node test-release-fetch.js"
Comment on lines +59 to +60
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

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.

},
"devDependencies": {
"prettier": "^3.3.3"
Expand Down
10 changes: 10 additions & 0 deletions src/cms/preview-templates/IndexPagePreview.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,23 @@ const IndexPagePreview = ({ entry, getAsset }) => {
const data = entry.getIn(["data"]).toJS();

if (data) {
// CMS preview doesn't have access to dynamic release data, so provide a fallback
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",
};

return (
<IndexPageTemplate
header={data.header || {}}
mainpitch={data.mainpitch || {}}
promo={data.promo || {}}
features={data.features || {}}
review={data.review || {}}
latestRelease={mockReleaseData}
/>
);
} else {
Expand Down
4 changes: 2 additions & 2 deletions src/pages/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ seo:
url: https://ironicbaremetal.org
header:
bottomtext:
title: 31.0.0 release available now
link: https://docs.openstack.org/releasenotes/ironic/unreleased.html#relnotes-31-0-0
title: Loading latest release...
link: https://docs.openstack.org/releasenotes/ironic/
linktext: See the release notes
buttons:
- link: https://docs.openstack.org/bifrost/latest/install/index.html
Expand Down
37 changes: 30 additions & 7 deletions src/templates/index-page.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,20 @@ export const IndexPageTemplate = ({
mainpitch,
promo,
features,
latestRelease,
// review
}) => (
}) => {
// Override header.bottomtext with dynamic release data if available
const dynamicHeader = latestRelease ? {
...header,
bottomtext: {
title: `${latestRelease.version} release available now`,
link: latestRelease.releaseNotesUrl,
linktext: "See the release notes"
}
} : header;

return (
<div>
{seo && (
<Helmet
Expand Down Expand Up @@ -64,29 +76,32 @@ export const IndexPageTemplate = ({
</Helmet>
)}
<Header
title={header.title}
subTitle={header.subTitle}
buttons={header.buttons}
bottomtext={header.bottomtext}
display={header.display}
title={dynamicHeader.title}
subTitle={dynamicHeader.subTitle}
buttons={dynamicHeader.buttons}
bottomtext={dynamicHeader.bottomtext}
display={dynamicHeader.display}
/>
<Mainpitch mainpitch={mainpitch} />
<Promo promo={promo} />
<Features features={features} />
</div>
);
);
};

IndexPageTemplate.propTypes = {
seo: PropTypes.object,
header: PropTypes.object,
mainpitch: PropTypes.object,
promo: PropTypes.object,
features: PropTypes.object,
latestRelease: PropTypes.object,
review: PropTypes.object,
};

const IndexPage = ({ data }) => {
const { frontmatter } = data.markdownRemark;
const latestRelease = data.ironicRelease;

return (
<Layout>
Expand All @@ -96,6 +111,7 @@ const IndexPage = ({ data }) => {
mainpitch={frontmatter?.mainpitch}
promo={frontmatter?.promo}
features={frontmatter?.features}
latestRelease={latestRelease}
review={frontmatter?.review}
/>
<NewsletterSubscribe />
Expand All @@ -109,6 +125,7 @@ IndexPage.propTypes = {
markdownRemark: PropTypes.shape({
frontmatter: PropTypes.object,
}),
ironicRelease: PropTypes.object,
}),
};

Expand Down Expand Up @@ -187,5 +204,11 @@ export const pageQuery = graphql`
}
}
}
ironicRelease {
version
releaseNotesUrl
publishedAt
htmlUrl
}
}
`;
Loading