From bd555574f5f6f53dbd69d7141ffb4c78e34710ac Mon Sep 17 00:00:00 2001 From: maria-hambardzumian Date: Thu, 12 Feb 2026 03:02:14 +0400 Subject: [PATCH] EPMRPP-89449 || Load release versions from GitHub --- .github/workflows/deploy.yml | 2 +- .github/workflows/sync-releases.yml | 77 +++++++++++ scripts/sync-releases.js | 200 ++++++++++++++++++++++++++++ 3 files changed, 278 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/sync-releases.yml create mode 100644 scripts/sync-releases.js diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 9bbe3a6dbd..533a65761d 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -50,7 +50,7 @@ jobs: - name: Deploy uses: peaceiris/actions-gh-pages@v3 with: - github_token: ${{ secrets.GITHUB_TOKEN }} + github_token: ${{ secrets.GH_TOKEN }} publish_dir: ./${{ env.BUILD_DIR }} publish_branch: gh-pages commit_message: ${{ github.event.head_commit.message }} diff --git a/.github/workflows/sync-releases.yml b/.github/workflows/sync-releases.yml new file mode 100644 index 0000000000..bd4394f31c --- /dev/null +++ b/.github/workflows/sync-releases.yml @@ -0,0 +1,77 @@ +# Copyright 2026 EPAM Systems +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +name: Sync GitHub Releases + +on: + workflow_dispatch: + inputs: + scope: + description: 'How many releases to check' + required: false + default: 'latest' + type: choice + options: + - latest + - last-n + - last-month + - all + count: + description: 'Number of releases to check (only used with "last-n")' + required: false + default: '5' + type: string + +env: + RELEASES_URL: https://api.github.com/repos/reportportal/reportportal/releases + +jobs: + sync-releases: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: 20 + + - name: Sync release pages + run: node scripts/sync-releases.js + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + RELEASES_URL: ${{ env.RELEASES_URL }} + SYNC_SCOPE: ${{ inputs.scope == 'last-n' && format('last-{0}', inputs.count) || inputs.scope }} + + - name: Check for changes + id: changes + run: | + if [ -n "$(git status docs/releases/ --porcelain)" ]; then + echo "has_changes=true" >> "$GITHUB_OUTPUT" + else + echo "has_changes=false" >> "$GITHUB_OUTPUT" + echo "No new release pages to add." + fi + + - name: Commit and push new release pages + if: steps.changes.outputs.has_changes == 'true' + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + git add docs/releases/ + git commit -m "docs: sync release pages from GitHub Releases" + git push diff --git a/scripts/sync-releases.js b/scripts/sync-releases.js new file mode 100644 index 0000000000..e7becf68ee --- /dev/null +++ b/scripts/sync-releases.js @@ -0,0 +1,200 @@ +const fs = require('fs'); +const path = require('path'); + +const RELEASES_API_URL = + process.env.RELEASES_URL || + 'https://api.github.com/repos/reportportal/reportportal/releases'; +const RELEASES_PER_PAGE = 100; +const RELEASES_DIR = path.join(__dirname, '..', 'docs', 'releases'); + +async function main() { + const releases = await fetchAllReleases(); + + const published = releases.filter((r) => !r.draft); + + published.sort( + (a, b) => new Date(b.published_at) - new Date(a.published_at), + ); + + const scope = (process.env.SYNC_SCOPE || 'latest').trim().toLowerCase(); + const filtered = applyScope(published, scope); + console.log(`Scope: "${scope}" -> checking ${filtered.length} of ${published.length} releases.\n`); + + fs.mkdirSync(RELEASES_DIR, { recursive: true }); + + const existingFiles = new Set( + fs.readdirSync(RELEASES_DIR).map((f) => f.toLowerCase()), + ); + + let created = 0; + + for (let i = 0; i < filtered.length; i++) { + const release = filtered[i]; + const name = release.name?.trim(); + if (!name) { + console.log(`Skipping release id=${release.id} - empty name`); + continue; + } + + const fileName = buildFileName(name); + + if (existingFiles.has(fileName.toLowerCase())) { + console.log(`Already exists: ${fileName}`); + continue; + } + + const sidebarLabel = buildSidebarLabel(name); + const body = transformBody(release.body || ''); + const position = i + 1; + + const content = [ + '---', + `sidebar_position: ${position}`, + `sidebar_label: ${sidebarLabel}`, + '---', + '', + `# ${sidebarLabel}`, + '', + body, + '', + ].join('\n'); + + const filePath = path.join(RELEASES_DIR, fileName); + fs.writeFileSync(filePath, content, 'utf-8'); + console.log(`Created: ${fileName}`); + created++; + } + + console.log( + `\nDone. ${created} new file(s) created, ${filtered.length - created} already existed or skipped.`, + ); + + if (created > 0) { + fs.writeFileSync( + path.join(__dirname, '..', '.releases-updated'), + String(created), + ); + } +} + +function applyScope(releases, scope) { + if (scope === 'all') return releases; + + if (scope === 'latest') return releases.slice(0, 1); + + const lastN = scope.match(/^last-(\d+)$/); + if (lastN) return releases.slice(0, parseInt(lastN[1], 10)); + + if (scope === 'last-month') { + const cutoff = new Date(); + cutoff.setMonth(cutoff.getMonth() - 1); + return releases.filter((r) => new Date(r.published_at) >= cutoff); + } + + console.warn(`Unknown scope "${scope}", falling back to "latest".`); + return releases.slice(0, 1); +} + +async function fetchAllReleases() { + const all = []; + let page = 1; + + while (true) { + const url = `${RELEASES_API_URL}?per_page=${RELEASES_PER_PAGE}&page=${page}`; + const headers = { + Accept: 'application/vnd.github+json', + 'X-GitHub-Api-Version': '2022-11-28', + 'User-Agent': 'reportportal-docs-sync', + }; + + if (process.env.GH_TOKEN) { + headers.Authorization = `Bearer ${process.env.GH_TOKEN}`; + } else { + console.warn('GH_TOKEN not set - using unauthenticated API (60 req/hr limit)'); + } + + console.log(`Fetching releases page ${page}...`); + const res = await fetch(url, { headers }); + + if (!res.ok) { + throw new Error(`GitHub API ${res.status}: ${res.statusText}`); + } + + const data = await res.json(); + if (!Array.isArray(data) || data.length === 0) break; + + all.push(...data); + if (data.length < RELEASES_PER_PAGE) break; + page++; + } + + console.log(`Fetched ${all.length} releases total.\n`); + return all; +} + +function transformBody(body) { + let result = body; + + result = result.replace(/\r\n/g, '\n'); + + result = result.replace(/]*>/gi, (tag) => { + const srcMatch = tag.match(/src="([^"]+)"/i); + const altMatch = tag.match(/alt="([^"]*)"/i); + const src = srcMatch ? srcMatch[1] : ''; + const alt = altMatch ? altMatch[1] : 'image'; + return src ? `![${alt}](${src})` : ''; + }); + + result = result.replace( + /(?\]]+/g, + (url) => `[${extractLabel(url)}](${url})`, + ); + + return result; +} + +function extractLabel(url) { + try { + const parts = new URL(url).pathname.split('/').filter(Boolean); + return parts.length > 0 ? parts[parts.length - 1] : url; + } catch { + return url; + } +} + +function buildFileName(name) { + let v = stripPrefix(name); + + if (/^BETA/i.test(v)) { + const nums = v.match(/[\d.]+/); + return nums ? `Version${nums[0]}RC.md` : `Version${v.replace(/\s+/g, '')}.md`; + } + + v = v.replace(/\s+(Final|RC|Beta|Alpha)$/i, '').trim(); + + return `Version${v}.md`; +} + +function buildSidebarLabel(name) { + let v = stripPrefix(name); + + if (/^BETA/i.test(v)) { + const nums = v.match(/[\d.]+/); + return nums ? `Version ${nums[0]} RC` : `Version ${v}`; + } + + return `Version ${v}`; +} + +function stripPrefix(name) { + return name + .replace(/^Release\s+/i, '') + .replace(/^ReportPortal\s+/i, '') + .replace(/^v\.?\s*/i, '') + .trim(); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +});