From 4149ffb40af32c536aae6933e0d296c8431030c4 Mon Sep 17 00:00:00 2001 From: Victor Plakyda Date: Sun, 2 Nov 2025 18:18:34 +0200 Subject: [PATCH 1/7] feat: gitops-runtime-helm-releases index file builde (github action) --- .github/scripts/release-index/.gitignore | 2 + .../scripts/release-index/build-releases.js | 285 ++++++++++++++++++ .../scripts/release-index/package-lock.json | 233 ++++++++++++++ .github/scripts/release-index/package.json | 31 ++ .github/workflows/release-index.yaml | 58 ++++ 5 files changed, 609 insertions(+) create mode 100644 .github/scripts/release-index/.gitignore create mode 100644 .github/scripts/release-index/build-releases.js create mode 100644 .github/scripts/release-index/package-lock.json create mode 100644 .github/scripts/release-index/package.json create mode 100644 .github/workflows/release-index.yaml diff --git a/.github/scripts/release-index/.gitignore b/.github/scripts/release-index/.gitignore new file mode 100644 index 000000000..de4e522de --- /dev/null +++ b/.github/scripts/release-index/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +releases/ \ No newline at end of file diff --git a/.github/scripts/release-index/build-releases.js b/.github/scripts/release-index/build-releases.js new file mode 100644 index 000000000..1fe89f029 --- /dev/null +++ b/.github/scripts/release-index/build-releases.js @@ -0,0 +1,285 @@ +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() \ No newline at end of file diff --git a/.github/scripts/release-index/package-lock.json b/.github/scripts/release-index/package-lock.json new file mode 100644 index 000000000..26af070b2 --- /dev/null +++ b/.github/scripts/release-index/package-lock.json @@ -0,0 +1,233 @@ +{ + "name": "gitops-runtime-helm-releases", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "gitops-runtime-helm-releases", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@octokit/rest": "^22.0.1", + "js-yaml": "^4.1.0", + "semver": "^7.7.3" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.2", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.2.tgz", + "integrity": "sha512-4zCpzP1fWc7QlqunZ5bSEjxc6yLAlRTnDwKtgXfcI/FxxGoqedDG8V2+xJ60bV2kODqcGB+nATdtap/XYq2NZQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-14.0.0.tgz", + "integrity": "sha512-fNVRE7ufJiAA3XUrha2omTA39M6IXIc6GIZLvlbsm8QOQCYvpq/LkMNGyFlB1d8hTDzsAXa3OKtybdMAYsV/fw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-request-log": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-request-log/-/plugin-request-log-6.0.0.tgz", + "integrity": "sha512-UkOzeEN3W91/eBq9sPZNQ7sUBvYCqYbrrD8gTbBuGtHEuycE4/awMXcYvx6sVYo7LypPhmQwwpUe4Yyu4QZN5Q==", + "license": "MIT", + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-rest-endpoint-methods": { + "version": "17.0.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-17.0.0.tgz", + "integrity": "sha512-B5yCyIlOJFPqUUeiD0cnBJwWJO8lkJs5d8+ze9QDP6SvfiXSz1BF+91+0MeI1d2yxgOhU/O+CvtiZ9jSkHhFAw==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.6.tgz", + "integrity": "sha512-FO+UgZCUu+pPnZAR+iKdUt64kPE7QW7ciqpldaMXaNzixz5Jld8dJ31LAUewk0cfSRkNSRKyqG438ba9c/qDlQ==", + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.2", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.0.2.tgz", + "integrity": "sha512-U8piOROoQQUyExw5c6dTkU3GKxts5/ERRThIauNL7yaRoeXW0q/5bgHWT7JfWBw1UyrbK8ERId2wVkcB32n0uQ==", + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/rest": { + "version": "22.0.1", + "resolved": "https://registry.npmjs.org/@octokit/rest/-/rest-22.0.1.tgz", + "integrity": "sha512-Jzbhzl3CEexhnivb1iQ0KJ7s5vvjMWcmRtq5aUsKmKDrRW6z3r84ngmiFKFvpZjpiU/9/S6ITPFRpn5s/3uQJw==", + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.6", + "@octokit/plugin-paginate-rest": "^14.0.0", + "@octokit/plugin-request-log": "^6.0.0", + "@octokit/plugin-rest-endpoint-methods": "^17.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "license": "Apache-2.0" + }, + "node_modules/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/semver": { + "version": "7.7.3", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz", + "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "license": "ISC" + } + } +} diff --git a/.github/scripts/release-index/package.json b/.github/scripts/release-index/package.json new file mode 100644 index 000000000..4a79a0c79 --- /dev/null +++ b/.github/scripts/release-index/package.json @@ -0,0 +1,31 @@ +{ + "name": "gitops-runtime-helm-releases", + "version": "1.0.0", + "description": "Automated release index builder for Codefresh GitOps Runtime Helm charts", + "main": "build-releases.js", + "scripts": { + "build": "node build-releases.js", + "start": "npm run build" + }, + "repository": { + "type": "git", + "url": "https://github.com/codefresh-io/gitops-runtime-helm.git" + }, + "keywords": [ + "codefresh", + "gitops", + "helm", + "releases", + "kubernetes" + ], + "author": "Codefresh", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "dependencies": { + "@octokit/rest": "^22.0.1", + "js-yaml": "^4.1.0", + "semver": "^7.7.3" + } +} diff --git a/.github/workflows/release-index.yaml b/.github/workflows/release-index.yaml new file mode 100644 index 000000000..1b1e2e9a7 --- /dev/null +++ b/.github/workflows/release-index.yaml @@ -0,0 +1,58 @@ +name: Update Release Index + +on: + release: + types: [published, edited] + workflow_dispatch: + schedule: + - cron: '0 */6 * * *' + +concurrency: + group: release-index + cancel-in-progress: false + +jobs: + build-index: + runs-on: ubuntu-latest + permissions: + contents: write + pages: write + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + + - name: Install dependencies + run: npm ci + + - name: Build Release Index + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: node build-releases.js + timeout-minutes: 2 + + - name: Verify output + run: | + if [ ! -f releases/releases.json ]; then + echo "Error: releases.json not generated" + exit 1 + fi + echo "āœ“ Release index generated successfully" + + - name: Deploy to GitHub Pages + uses: peaceiris/actions-gh-pages@v4 + if: success() + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./releases + publish_branch: gh-pages + user_name: 'github-actions[bot]' + user_email: 'github-actions[bot]@users.noreply.github.com' + commit_message: 'Update release index - ${{ github.run_number }}' + enable_jekyll: false \ No newline at end of file From b192e48a36a829ba9d9a6854d2b50d722fcbfa70 Mon Sep 17 00:00:00 2001 From: Victor Plakyda Date: Sun, 2 Nov 2025 18:24:12 +0200 Subject: [PATCH 2/7] refactor: prettier --- .github/scripts/release-index/.gitignore | 2 +- .../scripts/release-index/build-releases.js | 340 +++++++++--------- .github/workflows/release-index.yaml | 26 +- 3 files changed, 193 insertions(+), 175 deletions(-) diff --git a/.github/scripts/release-index/.gitignore b/.github/scripts/release-index/.gitignore index de4e522de..411c5987b 100644 --- a/.github/scripts/release-index/.gitignore +++ b/.github/scripts/release-index/.gitignore @@ -1,2 +1,2 @@ node_modules/ -releases/ \ No newline at end of file +releases/ diff --git a/.github/scripts/release-index/build-releases.js b/.github/scripts/release-index/build-releases.js index 1fe89f029..0fe2912d9 100644 --- a/.github/scripts/release-index/build-releases.js +++ b/.github/scripts/release-index/build-releases.js @@ -1,35 +1,36 @@ -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' +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) + console.error("āŒ GITHUB_TOKEN environment variable is required"); + process.exit(1); } -const octokit = new Octokit({ auth: TOKEN }) +const octokit = new Octokit({ auth: TOKEN }); function detectChannel(version) { - const match = version.match(LATEST_PATTERN) + const match = version.match(LATEST_PATTERN); if (match) { - const month = Number(match[2]) + const month = Number(match[2]); if (month >= 1 && month <= 12) { - return 'latest' + return "latest"; } } - return 'stable' + return "stable"; } /** @@ -37,29 +38,28 @@ function detectChannel(version) { * Converts: 2025.01-1 → 2025.1.1 */ function normalizeVersion(version, channel) { - if (channel === 'latest') { - const match = version.match(LATEST_PATTERN) + 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}` + const year = match[1]; + const month = Number(match[2]); + const patch = match[3]; + return `${year}.${month}.${patch}`; } } - return version + return version; } - function isValidVersion(normalized) { - return !!semver.valid(normalized) + return !!semver.valid(normalized); } function compareVersions(normA, normB) { try { - return semver.compare(normA, normB) + return semver.compare(normA, normB); } catch (error) { - console.warn(`Failed to compare versions:`, error.message) - return 0 + console.warn(`Failed to compare versions:`, error.message); + return 0; } } @@ -71,24 +71,24 @@ async function getAppVersionFromChart(tag) { path: CHART_PATH, ref: tag, mediaType: { - format: 'raw', + format: "raw", }, - }) - - const chart = load(data) - return chart.appVersion || DEFAULT_APP_VERSION + }); + + 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 + 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 - + 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, @@ -98,60 +98,65 @@ async function fetchReleases() { per_page: 100, } )) { - page++ - const releases = response.data - - allReleases.push(...releases) - console.log(` Fetched page ${page} (${releases.length} releases)`) - + 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 + console.log( + ` Reached ${MAX_GITHUB_RELEASES} releases limit, stopping...` + ); + break; } } } catch (error) { - console.error('Error fetching releases:', error.message) - throw error + console.error("Error fetching releases:", error.message); + throw error; } - - console.log(`āœ… Fetched ${allReleases.length} total releases`) - return allReleases + + 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 - + 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 + skipped++; + console.log(` āš ļø Skipping draft or prerelease: ${release.tag_name}`); + continue; } - - const version = release.tag_name || release.name + + const version = release.tag_name || release.name; if (!version) { - skipped++ - console.log(` āš ļø Skipping release without version: ${release.tag_name}`) - continue + skipped++; + console.log( + ` āš ļø Skipping release without version: ${release.tag_name}` + ); + continue; } - - const channel = detectChannel(version) - - const normalized = normalizeVersion(version, channel) - + + const channel = detectChannel(version); + + const normalized = normalizeVersion(version, channel); + if (!isValidVersion(normalized)) { - console.log(` āš ļø Skipping invalid version: ${version}`) - skipped++ - continue + console.log(` āš ļø Skipping invalid version: ${version}`); + skipped++; + continue; } - - const hasSecurityFixes = release.body?.includes(SECURITY_FIXES_STRING) || false - + + const hasSecurityFixes = + release.body?.includes(SECURITY_FIXES_STRING) || false; + const releaseData = { version, normalized, @@ -160,71 +165,77 @@ function processReleases(rawReleases) { publishedAt: release.published_at, url: release.html_url, createdAt: release.created_at, - } - - releases.push(releaseData) - channels[channel].push(releaseData) + }; + + 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 } + + 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...`) + 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) + release.appVersion = await getAppVersionFromChart(release.version); } - - const latestVersion = sorted[0]?.version - const latestSecureIndex = latestWithSecurityFixes - ? sorted.findIndex(r => r.version === latestWithSecurityFixes) - : -1 - + + 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 - }) - + 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`) - + 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'}`) + 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}`) + console.log(` šŸ”’ Stable security: ${stable.latestWithSecurityFixes}`); } if (latest.latestWithSecurityFixes) { - console.log(` šŸ”’ Latest security: ${latest.latestWithSecurityFixes}`) + console.log(` šŸ”’ Latest security: ${latest.latestWithSecurityFixes}`); } - + const index = { generatedAt: new Date().toISOString(), repository: `${OWNER}/${REPO}`, @@ -244,42 +255,49 @@ async function buildIndex() { totalReleases: releases.length, stableSecure: stable.latestWithSecurityFixes || null, latestSecure: latest.latestWithSecurityFixes || null, - } - } - - console.log('\nšŸ’¾ Writing index file...') - const outDir = path.join(process.cwd(), 'releases') + }, + }; + + console.log("\nšŸ’¾ Writing index file..."); + const outDir = path.join(process.cwd(), "releases"); if (!fs.existsSync(outDir)) { - fs.mkdirSync(outDir, { recursive: true }) + 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}`) - + + 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) + console.error("\nāŒ Error building index:", error.message); if (error.status) { - console.error(` GitHub API Status: ${error.status}`) + console.error(` GitHub API Status: ${error.status}`); } - console.error(error.stack) - process.exit(1) + console.error(error.stack); + process.exit(1); } } - -buildIndex() \ No newline at end of file +buildIndex(); diff --git a/.github/workflows/release-index.yaml b/.github/workflows/release-index.yaml index 1b1e2e9a7..81d246112 100644 --- a/.github/workflows/release-index.yaml +++ b/.github/workflows/release-index.yaml @@ -5,7 +5,7 @@ on: types: [published, edited] workflow_dispatch: schedule: - - cron: '0 */6 * * *' + - cron: "0 */6 * * *" concurrency: group: release-index @@ -17,26 +17,26 @@ jobs: permissions: contents: write pages: write - + steps: - name: Checkout uses: actions/checkout@v4 - + - name: Setup Node uses: actions/setup-node@v4 with: - node-version: '20' - cache: 'npm' - + node-version: "20" + cache: "npm" + - name: Install dependencies run: npm ci - + - name: Build Release Index env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: node build-releases.js timeout-minutes: 2 - + - name: Verify output run: | if [ ! -f releases/releases.json ]; then @@ -44,7 +44,7 @@ jobs: exit 1 fi echo "āœ“ Release index generated successfully" - + - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 if: success() @@ -52,7 +52,7 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./releases publish_branch: gh-pages - user_name: 'github-actions[bot]' - user_email: 'github-actions[bot]@users.noreply.github.com' - commit_message: 'Update release index - ${{ github.run_number }}' - enable_jekyll: false \ No newline at end of file + user_name: "github-actions[bot]" + user_email: "github-actions[bot]@users.noreply.github.com" + commit_message: "Update release index - ${{ github.run_number }}" + enable_jekyll: false From 82a5c55d89ef24dbe49d2e5bc52cbd3df3783bbb Mon Sep 17 00:00:00 2001 From: Victor Plakyda Date: Sun, 2 Nov 2025 18:26:31 +0200 Subject: [PATCH 3/7] fix: update GitHub token reference in release index workflow --- .github/workflows/release-index.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/release-index.yaml b/.github/workflows/release-index.yaml index 81d246112..5c0284349 100644 --- a/.github/workflows/release-index.yaml +++ b/.github/workflows/release-index.yaml @@ -33,7 +33,7 @@ jobs: - name: Build Release Index env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} run: node build-releases.js timeout-minutes: 2 @@ -49,7 +49,7 @@ jobs: uses: peaceiris/actions-gh-pages@v4 if: success() with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.TOKEN_GITHUB }} publish_dir: ./releases publish_branch: gh-pages user_name: "github-actions[bot]" From 095cd06731f9b62528bdfb072080e702ced23cd1 Mon Sep 17 00:00:00 2001 From: Victor Plakyda Date: Sun, 2 Nov 2025 18:37:32 +0200 Subject: [PATCH 4/7] chore: update release index workflow to specify working directory and cache dependency path --- .github/workflows/release-index.yaml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-index.yaml b/.github/workflows/release-index.yaml index 5c0284349..9cd6667ea 100644 --- a/.github/workflows/release-index.yaml +++ b/.github/workflows/release-index.yaml @@ -27,17 +27,21 @@ jobs: with: node-version: "20" cache: "npm" + cache-dependency-path: .github/scripts/release-index/package-lock.json - name: Install dependencies + working-directory: .github/scripts/release-index run: npm ci - name: Build Release Index + working-directory: .github/scripts/release-index env: GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} run: node build-releases.js timeout-minutes: 2 - name: Verify output + working-directory: .github/scripts/release-index run: | if [ ! -f releases/releases.json ]; then echo "Error: releases.json not generated" @@ -50,7 +54,7 @@ jobs: if: success() with: github_token: ${{ secrets.TOKEN_GITHUB }} - publish_dir: ./releases + publish_dir: .github/scripts/release-index/releases publish_branch: gh-pages user_name: "github-actions[bot]" user_email: "github-actions[bot]@users.noreply.github.com" From eae33f22b616e79a1d427b4a79ddca297f16847d Mon Sep 17 00:00:00 2001 From: Victor Plakyda Date: Sun, 2 Nov 2025 18:43:24 +0200 Subject: [PATCH 5/7] trigger --- .github/workflows/release-index.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/release-index.yaml b/.github/workflows/release-index.yaml index 9cd6667ea..1562dcbea 100644 --- a/.github/workflows/release-index.yaml +++ b/.github/workflows/release-index.yaml @@ -1,6 +1,12 @@ name: Update Release Index on: + push: + branches: + - 'CR-*' + paths: + - '.github/workflows/release-index.yaml' + - '.github/scripts/release-index/**' release: types: [published, edited] workflow_dispatch: From 108fafb721cf50eb4c5d9377d076940dfd9a92dd Mon Sep 17 00:00:00 2001 From: Victor Plakyda Date: Sun, 2 Nov 2025 19:19:16 +0200 Subject: [PATCH 6/7] feat: add artifact upload step to release index workflow --- .github/workflows/release-index.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/release-index.yaml b/.github/workflows/release-index.yaml index 1562dcbea..784540002 100644 --- a/.github/workflows/release-index.yaml +++ b/.github/workflows/release-index.yaml @@ -55,6 +55,13 @@ jobs: fi echo "āœ“ Release index generated successfully" + - name: Upload artifact + uses: actions/upload-artifact@v4 + with: + name: release-index + path: .github/scripts/release-index/releases/releases.json + retention-days: 30 + - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 if: success() From eaa7b5f0cafbc67fa2fd0ff88b148e948e11702f Mon Sep 17 00:00:00 2001 From: Victor Plakyda Date: Mon, 3 Nov 2025 13:34:05 +0200 Subject: [PATCH 7/7] chore: removing unused triggers and artifact upload step release index workflow --- .github/workflows/release-index.yaml | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/.github/workflows/release-index.yaml b/.github/workflows/release-index.yaml index 784540002..b3ca8e6f6 100644 --- a/.github/workflows/release-index.yaml +++ b/.github/workflows/release-index.yaml @@ -1,17 +1,9 @@ name: Update Release Index on: - push: - branches: - - 'CR-*' - paths: - - '.github/workflows/release-index.yaml' - - '.github/scripts/release-index/**' release: types: [published, edited] workflow_dispatch: - schedule: - - cron: "0 */6 * * *" concurrency: group: release-index @@ -42,7 +34,7 @@ jobs: - name: Build Release Index working-directory: .github/scripts/release-index env: - GITHUB_TOKEN: ${{ secrets.TOKEN_GITHUB }} + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: node build-releases.js timeout-minutes: 2 @@ -55,18 +47,11 @@ jobs: fi echo "āœ“ Release index generated successfully" - - name: Upload artifact - uses: actions/upload-artifact@v4 - with: - name: release-index - path: .github/scripts/release-index/releases/releases.json - retention-days: 30 - - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v4 if: success() with: - github_token: ${{ secrets.TOKEN_GITHUB }} + github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: .github/scripts/release-index/releases publish_branch: gh-pages user_name: "github-actions[bot]"