Skip to content

Commit 4149ffb

Browse files
feat: gitops-runtime-helm-releases index file builde (github action)
1 parent 9beab32 commit 4149ffb

File tree

5 files changed

+609
-0
lines changed

5 files changed

+609
-0
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
node_modules/
2+
releases/
Lines changed: 285 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,285 @@
1+
const { Octokit } = require('@octokit/rest')
2+
const semver = require('semver')
3+
const fs = require('fs')
4+
const path = require('path')
5+
const { load } = require('js-yaml')
6+
7+
const OWNER = 'codefresh-io'
8+
const REPO = 'gitops-runtime-helm'
9+
const LATEST_PATTERN = /^(\d{4})\.(\d{1,2})-(\d+)$/
10+
const TOKEN = process.env.GITHUB_TOKEN
11+
const SECURITY_FIXES_STRING = process.env.SECURITY_FIXES_STRING || '### Security Fixes:'
12+
const MAX_RELEASES_PER_CHANNEL = 10
13+
const MAX_GITHUB_RELEASES = 1000
14+
const CHART_PATH = 'charts/gitops-runtime/Chart.yaml'
15+
const DEFAULT_APP_VERSION = '0.0.0'
16+
17+
if (!TOKEN) {
18+
console.error('❌ GITHUB_TOKEN environment variable is required')
19+
process.exit(1)
20+
}
21+
22+
const octokit = new Octokit({ auth: TOKEN })
23+
24+
function detectChannel(version) {
25+
const match = version.match(LATEST_PATTERN)
26+
if (match) {
27+
const month = Number(match[2])
28+
if (month >= 1 && month <= 12) {
29+
return 'latest'
30+
}
31+
}
32+
return 'stable'
33+
}
34+
35+
/**
36+
* Normalize version for semver validation
37+
* Converts: 2025.01-1 → 2025.1.1
38+
*/
39+
function normalizeVersion(version, channel) {
40+
if (channel === 'latest') {
41+
const match = version.match(LATEST_PATTERN)
42+
if (match) {
43+
const year = match[1]
44+
const month = Number(match[2])
45+
const patch = match[3]
46+
return `${year}.${month}.${patch}`
47+
}
48+
}
49+
return version
50+
}
51+
52+
53+
function isValidVersion(normalized) {
54+
return !!semver.valid(normalized)
55+
}
56+
57+
function compareVersions(normA, normB) {
58+
try {
59+
return semver.compare(normA, normB)
60+
} catch (error) {
61+
console.warn(`Failed to compare versions:`, error.message)
62+
return 0
63+
}
64+
}
65+
66+
async function getAppVersionFromChart(tag) {
67+
try {
68+
const { data } = await octokit.repos.getContent({
69+
owner: OWNER,
70+
repo: REPO,
71+
path: CHART_PATH,
72+
ref: tag,
73+
mediaType: {
74+
format: 'raw',
75+
},
76+
})
77+
78+
const chart = load(data)
79+
return chart.appVersion || DEFAULT_APP_VERSION
80+
} catch (error) {
81+
console.warn(` ⚠️ Failed to get appVersion for ${tag}:`, error.message)
82+
return DEFAULT_APP_VERSION
83+
}
84+
}
85+
86+
async function fetchReleases() {
87+
console.log('📦 Fetching releases from GitHub using Octokit...')
88+
89+
const allReleases = []
90+
let page = 0
91+
92+
try {
93+
for await (const response of octokit.paginate.iterator(
94+
octokit.rest.repos.listReleases,
95+
{
96+
owner: OWNER,
97+
repo: REPO,
98+
per_page: 100,
99+
}
100+
)) {
101+
page++
102+
const releases = response.data
103+
104+
allReleases.push(...releases)
105+
console.log(` Fetched page ${page} (${releases.length} releases)`)
106+
107+
if (allReleases.length >= MAX_GITHUB_RELEASES) {
108+
console.log(` Reached ${MAX_GITHUB_RELEASES} releases limit, stopping...`)
109+
break
110+
}
111+
}
112+
} catch (error) {
113+
console.error('Error fetching releases:', error.message)
114+
throw error
115+
}
116+
117+
console.log(`✅ Fetched ${allReleases.length} total releases`)
118+
return allReleases
119+
}
120+
121+
function processReleases(rawReleases) {
122+
console.log('\n🔍 Processing releases...')
123+
124+
const releases = []
125+
const channels = { stable: [], latest: [] }
126+
127+
let skipped = 0
128+
129+
for (const release of rawReleases) {
130+
if (release.draft || release.prerelease) {
131+
skipped++
132+
console.log(` ⚠️ Skipping draft or prerelease: ${release.tag_name}`)
133+
continue
134+
}
135+
136+
const version = release.tag_name || release.name
137+
if (!version) {
138+
skipped++
139+
console.log(` ⚠️ Skipping release without version: ${release.tag_name}`)
140+
continue
141+
}
142+
143+
const channel = detectChannel(version)
144+
145+
const normalized = normalizeVersion(version, channel)
146+
147+
if (!isValidVersion(normalized)) {
148+
console.log(` ⚠️ Skipping invalid version: ${version}`)
149+
skipped++
150+
continue
151+
}
152+
153+
const hasSecurityFixes = release.body?.includes(SECURITY_FIXES_STRING) || false
154+
155+
const releaseData = {
156+
version,
157+
normalized,
158+
channel,
159+
hasSecurityFixes,
160+
publishedAt: release.published_at,
161+
url: release.html_url,
162+
createdAt: release.created_at,
163+
}
164+
165+
releases.push(releaseData)
166+
channels[channel].push(releaseData)
167+
}
168+
169+
console.log(`✅ Processed ${releases.length} valid releases (skipped ${skipped})`)
170+
console.log(` Stable: ${channels.stable.length}`)
171+
console.log(` Latest: ${channels.latest.length}`)
172+
173+
return { releases, channels }
174+
}
175+
176+
async function buildChannelData(channelReleases, channelName) {
177+
const sorted = channelReleases.sort((a, b) => {
178+
return compareVersions(b.normalized, a.normalized)
179+
})
180+
181+
const latestWithSecurityFixes = sorted.find(r => r.hasSecurityFixes)?.version || null
182+
const topReleases = sorted.slice(0, MAX_RELEASES_PER_CHANNEL)
183+
184+
console.log(` Fetching appVersion for ${topReleases.length} ${channelName} releases...`)
185+
for (const release of topReleases) {
186+
release.appVersion = await getAppVersionFromChart(release.version)
187+
}
188+
189+
const latestVersion = sorted[0]?.version
190+
const latestSecureIndex = latestWithSecurityFixes
191+
? sorted.findIndex(r => r.version === latestWithSecurityFixes)
192+
: -1
193+
194+
topReleases.forEach((release, index) => {
195+
release.upgradeAvailable = release.version !== latestVersion
196+
release.hasSecurityVulnerabilities = latestSecureIndex >= 0 && index > latestSecureIndex
197+
})
198+
199+
return {
200+
releases: topReleases,
201+
latestChartVersion: sorted[0]?.version || null,
202+
latestWithSecurityFixes,
203+
}
204+
}
205+
206+
async function buildIndex() {
207+
console.log('🚀 Building release index...\n')
208+
console.log(`📍 Repository: ${OWNER}/${REPO}\n`)
209+
210+
try {
211+
const rawReleases = await fetchReleases()
212+
213+
const { releases, channels } = processReleases(rawReleases)
214+
215+
console.log('\n📊 Building channel data...')
216+
const stable = await buildChannelData(channels.stable, 'stable')
217+
const latest = await buildChannelData(channels.latest, 'latest')
218+
219+
console.log(` Stable latest: ${stable.latest || 'none'}`)
220+
console.log(` Latest latest: ${latest.latest || 'none'}`)
221+
if (stable.latestWithSecurityFixes) {
222+
console.log(` 🔒 Stable security: ${stable.latestWithSecurityFixes}`)
223+
}
224+
if (latest.latestWithSecurityFixes) {
225+
console.log(` 🔒 Latest security: ${latest.latestWithSecurityFixes}`)
226+
}
227+
228+
const index = {
229+
generatedAt: new Date().toISOString(),
230+
repository: `${OWNER}/${REPO}`,
231+
channels: {
232+
stable: {
233+
releases: stable.releases,
234+
latestChartVersion: stable.latestChartVersion,
235+
latestWithSecurityFixes: stable.latestWithSecurityFixes,
236+
},
237+
latest: {
238+
releases: latest.releases,
239+
latestChartVersion: latest.latestChartVersion,
240+
latestWithSecurityFixes: latest.latestWithSecurityFixes,
241+
},
242+
},
243+
stats: {
244+
totalReleases: releases.length,
245+
stableSecure: stable.latestWithSecurityFixes || null,
246+
latestSecure: latest.latestWithSecurityFixes || null,
247+
}
248+
}
249+
250+
console.log('\n💾 Writing index file...')
251+
const outDir = path.join(process.cwd(), 'releases')
252+
if (!fs.existsSync(outDir)) {
253+
fs.mkdirSync(outDir, { recursive: true })
254+
}
255+
256+
const outputPath = path.join(outDir, 'releases.json')
257+
fs.writeFileSync(
258+
outputPath,
259+
JSON.stringify(index, null, 2)
260+
)
261+
262+
console.log('\n✅ Release index built successfully!')
263+
console.log('\n📋 Summary:')
264+
console.log(` Total releases: ${index.stats.totalReleases}`)
265+
console.log(`\n 🟢 Stable Channel:`)
266+
console.log(` Latest: ${index.channels.stable.latestChartVersion || 'none'}`)
267+
console.log(` Latest secure: ${index.channels.stable.latestWithSecurityFixes || 'none'}`)
268+
console.log(`\n 🔵 Latest Channel:`)
269+
console.log(` Latest: ${index.channels.latest.latestChartVersion || 'none'}`)
270+
console.log(` Latest secure: ${index.channels.latest.latestWithSecurityFixes || 'none'}`)
271+
console.log(`\n📁 Files created:`)
272+
console.log(` ${outputPath}`)
273+
274+
} catch (error) {
275+
console.error('\n❌ Error building index:', error.message)
276+
if (error.status) {
277+
console.error(` GitHub API Status: ${error.status}`)
278+
}
279+
console.error(error.stack)
280+
process.exit(1)
281+
}
282+
}
283+
284+
285+
buildIndex()

0 commit comments

Comments
 (0)