Skip to content

Commit c397178

Browse files
feat: add automated release index generator and GitHub Pages deployment (#879)
* feat: gitops-runtime-helm-releases index file builde (github action) * refactor: prettier * fix: update GitHub token reference in release index workflow * chore: update release index workflow to specify working directory and cache dependency path * trigger * feat: add artifact upload step to release index workflow * chore: removing unused triggers and artifact upload step release index workflow
1 parent cef38e3 commit c397178

File tree

5 files changed

+629
-0
lines changed

5 files changed

+629
-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: 303 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,303 @@
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 =
12+
process.env.SECURITY_FIXES_STRING || "### Security Fixes:";
13+
const MAX_RELEASES_PER_CHANNEL = 10;
14+
const MAX_GITHUB_RELEASES = 1000;
15+
const CHART_PATH = "charts/gitops-runtime/Chart.yaml";
16+
const DEFAULT_APP_VERSION = "0.0.0";
17+
18+
if (!TOKEN) {
19+
console.error("❌ GITHUB_TOKEN environment variable is required");
20+
process.exit(1);
21+
}
22+
23+
const octokit = new Octokit({ auth: TOKEN });
24+
25+
function detectChannel(version) {
26+
const match = version.match(LATEST_PATTERN);
27+
if (match) {
28+
const month = Number(match[2]);
29+
if (month >= 1 && month <= 12) {
30+
return "latest";
31+
}
32+
}
33+
return "stable";
34+
}
35+
36+
/**
37+
* Normalize version for semver validation
38+
* Converts: 2025.01-1 → 2025.1.1
39+
*/
40+
function normalizeVersion(version, channel) {
41+
if (channel === "latest") {
42+
const match = version.match(LATEST_PATTERN);
43+
if (match) {
44+
const year = match[1];
45+
const month = Number(match[2]);
46+
const patch = match[3];
47+
return `${year}.${month}.${patch}`;
48+
}
49+
}
50+
return version;
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(
109+
` Reached ${MAX_GITHUB_RELEASES} releases limit, stopping...`
110+
);
111+
break;
112+
}
113+
}
114+
} catch (error) {
115+
console.error("Error fetching releases:", error.message);
116+
throw error;
117+
}
118+
119+
console.log(`✅ Fetched ${allReleases.length} total releases`);
120+
return allReleases;
121+
}
122+
123+
function processReleases(rawReleases) {
124+
console.log("\n🔍 Processing releases...");
125+
126+
const releases = [];
127+
const channels = { stable: [], latest: [] };
128+
129+
let skipped = 0;
130+
131+
for (const release of rawReleases) {
132+
if (release.draft || release.prerelease) {
133+
skipped++;
134+
console.log(` ⚠️ Skipping draft or prerelease: ${release.tag_name}`);
135+
continue;
136+
}
137+
138+
const version = release.tag_name || release.name;
139+
if (!version) {
140+
skipped++;
141+
console.log(
142+
` ⚠️ Skipping release without version: ${release.tag_name}`
143+
);
144+
continue;
145+
}
146+
147+
const channel = detectChannel(version);
148+
149+
const normalized = normalizeVersion(version, channel);
150+
151+
if (!isValidVersion(normalized)) {
152+
console.log(` ⚠️ Skipping invalid version: ${version}`);
153+
skipped++;
154+
continue;
155+
}
156+
157+
const hasSecurityFixes =
158+
release.body?.includes(SECURITY_FIXES_STRING) || false;
159+
160+
const releaseData = {
161+
version,
162+
normalized,
163+
channel,
164+
hasSecurityFixes,
165+
publishedAt: release.published_at,
166+
url: release.html_url,
167+
createdAt: release.created_at,
168+
};
169+
170+
releases.push(releaseData);
171+
channels[channel].push(releaseData);
172+
}
173+
174+
console.log(
175+
`✅ Processed ${releases.length} valid releases (skipped ${skipped})`
176+
);
177+
console.log(` Stable: ${channels.stable.length}`);
178+
console.log(` Latest: ${channels.latest.length}`);
179+
180+
return { releases, channels };
181+
}
182+
183+
async function buildChannelData(channelReleases, channelName) {
184+
const sorted = channelReleases.sort((a, b) => {
185+
return compareVersions(b.normalized, a.normalized);
186+
});
187+
188+
const latestWithSecurityFixes =
189+
sorted.find((r) => r.hasSecurityFixes)?.version || null;
190+
const topReleases = sorted.slice(0, MAX_RELEASES_PER_CHANNEL);
191+
192+
console.log(
193+
` Fetching appVersion for ${topReleases.length} ${channelName} releases...`
194+
);
195+
for (const release of topReleases) {
196+
release.appVersion = await getAppVersionFromChart(release.version);
197+
}
198+
199+
const latestVersion = sorted[0]?.version;
200+
const latestSecureIndex = latestWithSecurityFixes
201+
? sorted.findIndex((r) => r.version === latestWithSecurityFixes)
202+
: -1;
203+
204+
topReleases.forEach((release, index) => {
205+
release.upgradeAvailable = release.version !== latestVersion;
206+
release.hasSecurityVulnerabilities =
207+
latestSecureIndex >= 0 && index > latestSecureIndex;
208+
});
209+
210+
return {
211+
releases: topReleases,
212+
latestChartVersion: sorted[0]?.version || null,
213+
latestWithSecurityFixes,
214+
};
215+
}
216+
217+
async function buildIndex() {
218+
console.log("🚀 Building release index...\n");
219+
console.log(`📍 Repository: ${OWNER}/${REPO}\n`);
220+
221+
try {
222+
const rawReleases = await fetchReleases();
223+
224+
const { releases, channels } = processReleases(rawReleases);
225+
226+
console.log("\n📊 Building channel data...");
227+
const stable = await buildChannelData(channels.stable, "stable");
228+
const latest = await buildChannelData(channels.latest, "latest");
229+
230+
console.log(` Stable latest: ${stable.latest || "none"}`);
231+
console.log(` Latest latest: ${latest.latest || "none"}`);
232+
if (stable.latestWithSecurityFixes) {
233+
console.log(` 🔒 Stable security: ${stable.latestWithSecurityFixes}`);
234+
}
235+
if (latest.latestWithSecurityFixes) {
236+
console.log(` 🔒 Latest security: ${latest.latestWithSecurityFixes}`);
237+
}
238+
239+
const index = {
240+
generatedAt: new Date().toISOString(),
241+
repository: `${OWNER}/${REPO}`,
242+
channels: {
243+
stable: {
244+
releases: stable.releases,
245+
latestChartVersion: stable.latestChartVersion,
246+
latestWithSecurityFixes: stable.latestWithSecurityFixes,
247+
},
248+
latest: {
249+
releases: latest.releases,
250+
latestChartVersion: latest.latestChartVersion,
251+
latestWithSecurityFixes: latest.latestWithSecurityFixes,
252+
},
253+
},
254+
stats: {
255+
totalReleases: releases.length,
256+
stableSecure: stable.latestWithSecurityFixes || null,
257+
latestSecure: latest.latestWithSecurityFixes || null,
258+
},
259+
};
260+
261+
console.log("\n💾 Writing index file...");
262+
const outDir = path.join(process.cwd(), "releases");
263+
if (!fs.existsSync(outDir)) {
264+
fs.mkdirSync(outDir, { recursive: true });
265+
}
266+
267+
const outputPath = path.join(outDir, "releases.json");
268+
fs.writeFileSync(outputPath, JSON.stringify(index, null, 2));
269+
270+
console.log("\n✅ Release index built successfully!");
271+
console.log("\n📋 Summary:");
272+
console.log(` Total releases: ${index.stats.totalReleases}`);
273+
console.log(`\n 🟢 Stable Channel:`);
274+
console.log(
275+
` Latest: ${index.channels.stable.latestChartVersion || "none"}`
276+
);
277+
console.log(
278+
` Latest secure: ${
279+
index.channels.stable.latestWithSecurityFixes || "none"
280+
}`
281+
);
282+
console.log(`\n 🔵 Latest Channel:`);
283+
console.log(
284+
` Latest: ${index.channels.latest.latestChartVersion || "none"}`
285+
);
286+
console.log(
287+
` Latest secure: ${
288+
index.channels.latest.latestWithSecurityFixes || "none"
289+
}`
290+
);
291+
console.log(`\n📁 Files created:`);
292+
console.log(` ${outputPath}`);
293+
} catch (error) {
294+
console.error("\n❌ Error building index:", error.message);
295+
if (error.status) {
296+
console.error(` GitHub API Status: ${error.status}`);
297+
}
298+
console.error(error.stack);
299+
process.exit(1);
300+
}
301+
}
302+
303+
buildIndex();

0 commit comments

Comments
 (0)