diff --git a/gatsby-node.js b/gatsby-node.js index d89e8f2..6296fda 100644 --- a/gatsby-node.js +++ b/gatsby-node.js @@ -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 @@ -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) @@ -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 diff --git a/package.json b/package.json index 3d279ba..a4bed95 100644 --- a/package.json +++ b/package.json @@ -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" }, "devDependencies": { "prettier": "^3.3.3" diff --git a/src/cms/preview-templates/IndexPagePreview.js b/src/cms/preview-templates/IndexPagePreview.js index 02cd5d7..d1ac96f 100644 --- a/src/cms/preview-templates/IndexPagePreview.js +++ b/src/cms/preview-templates/IndexPagePreview.js @@ -6,6 +6,15 @@ 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 ( { promo={data.promo || {}} features={data.features || {}} review={data.review || {}} + latestRelease={mockReleaseData} /> ); } else { diff --git a/src/pages/index.md b/src/pages/index.md index c3750e6..9431e23 100644 --- a/src/pages/index.md +++ b/src/pages/index.md @@ -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 diff --git a/src/templates/index-page.js b/src/templates/index-page.js index 6441d52..f50e423 100644 --- a/src/templates/index-page.js +++ b/src/templates/index-page.js @@ -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 (
{seo && ( )}
-); + ); +}; IndexPageTemplate.propTypes = { seo: PropTypes.object, @@ -82,11 +95,13 @@ IndexPageTemplate.propTypes = { 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 ( @@ -96,6 +111,7 @@ const IndexPage = ({ data }) => { mainpitch={frontmatter?.mainpitch} promo={frontmatter?.promo} features={frontmatter?.features} + latestRelease={latestRelease} review={frontmatter?.review} /> @@ -109,6 +125,7 @@ IndexPage.propTypes = { markdownRemark: PropTypes.shape({ frontmatter: PropTypes.object, }), + ironicRelease: PropTypes.object, }), }; @@ -187,5 +204,11 @@ export const pageQuery = graphql` } } } + ironicRelease { + version + releaseNotesUrl + publishedAt + htmlUrl + } } `; diff --git a/test-release-fetch.js b/test-release-fetch.js new file mode 100644 index 0000000..c49806d --- /dev/null +++ b/test-release-fetch.js @@ -0,0 +1,170 @@ +#!/usr/bin/env node +/** + * Test script to verify automatic release series detection and fetching + * Run with: node test-release-fetch.js + */ + +const axios = require('axios'); + +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); + } + }); + + 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'); + const fallbackOrder = ['gazpacho', 'epoxy', 'dalmatian', 'caracal']; + const fallbackData = { + 'gazpacho': '2026.1', 'epoxy': '2025.1', 'dalmatian': '2024.2', 'caracal': '2024.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(); + const 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'); + const fallbackData = { + 'gazpacho': '2026.1', 'epoxy': '2025.1', 'dalmatian': '2024.2', 'caracal': '2024.1' + }; + return { + series: ['gazpacho', 'epoxy', 'dalmatian', 'caracal'], + seriesData: fallbackData + }; + } +} + +async function testReleaseFetch() { + console.log('๐Ÿงช Testing automatic Ironic release detection...\n'); + + try { + // Test auto-detection with dynamic series mapping + const detectionResult = await getLatestReleaseSeries(); + + if (Array.isArray(detectionResult.series)) { + // Fallback mode + console.log('\n๐Ÿ”„ Testing fallback mode...'); + for (const series of detectionResult.series) { + try { + const response = await axios.get(`https://raw.githubusercontent.com/openstack/releases/master/deliverables/${series}/ironic.yaml`); + const yamlContent = response.data; + const releaseMatches = yamlContent.match(/- version: ([\d.]+)/g); + + if (releaseMatches && releaseMatches.length > 0) { + const latestVersion = releaseMatches[releaseMatches.length - 1].replace('- version: ', ''); + const seriesVersion = detectionResult.seriesData[series] || 'unknown'; + + // Try to get the actual release date + let releaseDate = 'unknown'; + try { + const hashPattern = new RegExp(`- version: ${latestVersion.replace(/\./g, '\\.')}[\\s\\S]*?hash: ([a-f0-9]+)`, 'i'); + const hashMatch = yamlContent.match(hashPattern); + + if (hashMatch && hashMatch[1]) { + const gitHash = hashMatch[1]; + const commitResponse = await axios.get(`https://api.github.com/repos/openstack/ironic/commits/${gitHash}`); + releaseDate = commitResponse.data.commit.committer.date; + } + } catch (error) { + // Release date fetch failed, continue with 'unknown' + } + + console.log(`โœ… Latest ${series} (${seriesVersion}) version: ${latestVersion} (${releaseDate})`); + break; + } + } catch (error) { + console.log(`โš ๏ธ ${series} series failed, trying next...`); + continue; + } + } + } else { + // Auto-detection succeeded + const series = detectionResult.series; + const seriesVersion = detectionResult.seriesData[series] || 'unknown'; + console.log(`\n๐Ÿ“ก Testing detected series: ${series} (${seriesVersion})`); + const response = await axios.get(`https://raw.githubusercontent.com/openstack/releases/master/deliverables/${series}/ironic.yaml`); + + const yamlContent = response.data; + const releaseMatches = yamlContent.match(/- version: ([\d.]+)/g); + + if (!releaseMatches || releaseMatches.length === 0) { + throw new Error(`No releases found in ${series} YAML`); + } + + const latestVersion = releaseMatches[releaseMatches.length - 1].replace('- version: ', ''); + + // Try to get the actual release date + let releaseDate = 'unknown'; + try { + const hashPattern = new RegExp(`- version: ${latestVersion.replace(/\./g, '\\.')}[\\s\\S]*?hash: ([a-f0-9]+)`, 'i'); + const hashMatch = yamlContent.match(hashPattern); + + if (hashMatch && hashMatch[1]) { + const gitHash = hashMatch[1]; + const commitResponse = await axios.get(`https://api.github.com/repos/openstack/ironic/commits/${gitHash}`); + releaseDate = commitResponse.data.commit.committer.date; + } + } catch (error) { + // Release date fetch failed, continue with 'unknown' + } + + console.log(`โœ… Latest ${series} (${seriesVersion}) version: ${latestVersion} (${releaseDate})`); + } + + console.log('\n๐ŸŽ‰ All tests passed! Dynamic release detection is working correctly.'); + + } catch (error) { + console.error('โŒ Test failed:', error.message); + process.exit(1); + } +} + +// Run the test +testReleaseFetch(); \ No newline at end of file