Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 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
2 changes: 2 additions & 0 deletions .github/scripts/release-index/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
releases/
303 changes: 303 additions & 0 deletions .github/scripts/release-index/build-releases.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,303 @@
const { Octokit } = require("@octokit/rest");
const semver = require("semver");
const fs = require("fs");
const path = require("path");
const { load } = require("js-yaml");

const OWNER = "codefresh-io";
const REPO = "gitops-runtime-helm";
const LATEST_PATTERN = /^(\d{4})\.(\d{1,2})-(\d+)$/;
const TOKEN = process.env.GITHUB_TOKEN;
const SECURITY_FIXES_STRING =
process.env.SECURITY_FIXES_STRING || "### Security Fixes:";
const MAX_RELEASES_PER_CHANNEL = 10;
const MAX_GITHUB_RELEASES = 1000;
const CHART_PATH = "charts/gitops-runtime/Chart.yaml";
const DEFAULT_APP_VERSION = "0.0.0";

if (!TOKEN) {
console.error("❌ GITHUB_TOKEN environment variable is required");
process.exit(1);
}

const octokit = new Octokit({ auth: TOKEN });

function detectChannel(version) {
const match = version.match(LATEST_PATTERN);
if (match) {
const month = Number(match[2]);
if (month >= 1 && month <= 12) {
return "latest";
}
}
return "stable";
}

/**
* Normalize version for semver validation
* Converts: 2025.01-1 → 2025.1.1
*/
function normalizeVersion(version, channel) {
if (channel === "latest") {
const match = version.match(LATEST_PATTERN);
if (match) {
const year = match[1];
const month = Number(match[2]);
const patch = match[3];
return `${year}.${month}.${patch}`;
}
}
return version;
}

function isValidVersion(normalized) {
return !!semver.valid(normalized);
}

function compareVersions(normA, normB) {
try {
return semver.compare(normA, normB);
} catch (error) {
console.warn(`Failed to compare versions:`, error.message);
return 0;
}
}

async function getAppVersionFromChart(tag) {
try {
const { data } = await octokit.repos.getContent({
owner: OWNER,
repo: REPO,
path: CHART_PATH,
ref: tag,
mediaType: {
format: "raw",
},
});

const chart = load(data);
return chart.appVersion || DEFAULT_APP_VERSION;
} catch (error) {
console.warn(` ⚠️ Failed to get appVersion for ${tag}:`, error.message);
return DEFAULT_APP_VERSION;
}
}

async function fetchReleases() {
console.log("📦 Fetching releases from GitHub using Octokit...");

const allReleases = [];
let page = 0;

try {
for await (const response of octokit.paginate.iterator(
octokit.rest.repos.listReleases,
{
owner: OWNER,
repo: REPO,
per_page: 100,
}
)) {
page++;
const releases = response.data;

allReleases.push(...releases);
console.log(` Fetched page ${page} (${releases.length} releases)`);

if (allReleases.length >= MAX_GITHUB_RELEASES) {
console.log(
` Reached ${MAX_GITHUB_RELEASES} releases limit, stopping...`
);
break;
}
}
} catch (error) {
console.error("Error fetching releases:", error.message);
throw error;
}

console.log(`✅ Fetched ${allReleases.length} total releases`);
return allReleases;
}

function processReleases(rawReleases) {
console.log("\n🔍 Processing releases...");

const releases = [];
const channels = { stable: [], latest: [] };

let skipped = 0;

for (const release of rawReleases) {
if (release.draft || release.prerelease) {
skipped++;
console.log(` ⚠️ Skipping draft or prerelease: ${release.tag_name}`);
continue;
}

const version = release.tag_name || release.name;
if (!version) {
skipped++;
console.log(
` ⚠️ Skipping release without version: ${release.tag_name}`
);
continue;
}

const channel = detectChannel(version);

const normalized = normalizeVersion(version, channel);

if (!isValidVersion(normalized)) {
console.log(` ⚠️ Skipping invalid version: ${version}`);
skipped++;
continue;
}

const hasSecurityFixes =
release.body?.includes(SECURITY_FIXES_STRING) || false;

const releaseData = {
version,
normalized,
channel,
hasSecurityFixes,
publishedAt: release.published_at,
url: release.html_url,
createdAt: release.created_at,
};

releases.push(releaseData);
channels[channel].push(releaseData);
}

console.log(
`✅ Processed ${releases.length} valid releases (skipped ${skipped})`
);
console.log(` Stable: ${channels.stable.length}`);
console.log(` Latest: ${channels.latest.length}`);

return { releases, channels };
}

async function buildChannelData(channelReleases, channelName) {
const sorted = channelReleases.sort((a, b) => {
return compareVersions(b.normalized, a.normalized);
});

const latestWithSecurityFixes =
sorted.find((r) => r.hasSecurityFixes)?.version || null;
const topReleases = sorted.slice(0, MAX_RELEASES_PER_CHANNEL);

console.log(
` Fetching appVersion for ${topReleases.length} ${channelName} releases...`
);
for (const release of topReleases) {
release.appVersion = await getAppVersionFromChart(release.version);
}

const latestVersion = sorted[0]?.version;
const latestSecureIndex = latestWithSecurityFixes
? sorted.findIndex((r) => r.version === latestWithSecurityFixes)
: -1;

topReleases.forEach((release, index) => {
release.upgradeAvailable = release.version !== latestVersion;
release.hasSecurityVulnerabilities =
latestSecureIndex >= 0 && index > latestSecureIndex;
});

return {
releases: topReleases,
latestChartVersion: sorted[0]?.version || null,
latestWithSecurityFixes,
};
}

async function buildIndex() {
console.log("🚀 Building release index...\n");
console.log(`📍 Repository: ${OWNER}/${REPO}\n`);

try {
const rawReleases = await fetchReleases();

const { releases, channels } = processReleases(rawReleases);

console.log("\n📊 Building channel data...");
const stable = await buildChannelData(channels.stable, "stable");
const latest = await buildChannelData(channels.latest, "latest");

console.log(` Stable latest: ${stable.latest || "none"}`);
console.log(` Latest latest: ${latest.latest || "none"}`);
if (stable.latestWithSecurityFixes) {
console.log(` 🔒 Stable security: ${stable.latestWithSecurityFixes}`);
}
if (latest.latestWithSecurityFixes) {
console.log(` 🔒 Latest security: ${latest.latestWithSecurityFixes}`);
}

const index = {
generatedAt: new Date().toISOString(),
repository: `${OWNER}/${REPO}`,
channels: {
stable: {
releases: stable.releases,
latestChartVersion: stable.latestChartVersion,
latestWithSecurityFixes: stable.latestWithSecurityFixes,
},
latest: {
releases: latest.releases,
latestChartVersion: latest.latestChartVersion,
latestWithSecurityFixes: latest.latestWithSecurityFixes,
},
},
stats: {
totalReleases: releases.length,
stableSecure: stable.latestWithSecurityFixes || null,
latestSecure: latest.latestWithSecurityFixes || null,
},
};

console.log("\n💾 Writing index file...");
const outDir = path.join(process.cwd(), "releases");
if (!fs.existsSync(outDir)) {
fs.mkdirSync(outDir, { recursive: true });
}

const outputPath = path.join(outDir, "releases.json");
fs.writeFileSync(outputPath, JSON.stringify(index, null, 2));

console.log("\n✅ Release index built successfully!");
console.log("\n📋 Summary:");
console.log(` Total releases: ${index.stats.totalReleases}`);
console.log(`\n 🟢 Stable Channel:`);
console.log(
` Latest: ${index.channels.stable.latestChartVersion || "none"}`
);
console.log(
` Latest secure: ${
index.channels.stable.latestWithSecurityFixes || "none"
}`
);
console.log(`\n 🔵 Latest Channel:`);
console.log(
` Latest: ${index.channels.latest.latestChartVersion || "none"}`
);
console.log(
` Latest secure: ${
index.channels.latest.latestWithSecurityFixes || "none"
}`
);
console.log(`\n📁 Files created:`);
console.log(` ${outputPath}`);
} catch (error) {
console.error("\n❌ Error building index:", error.message);
if (error.status) {
console.error(` GitHub API Status: ${error.status}`);
}
console.error(error.stack);
process.exit(1);
}
}

buildIndex();
Loading