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