-
Notifications
You must be signed in to change notification settings - Fork 82
EPMRPP-89449 || Load release versions from GitHub #1068
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: develop
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 | ||
|
Comment on lines
+29
to
+33
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. No validation that The 🤖 Prompt for AI Agents |
||
|
|
||
| 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 | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
|
Comment on lines
+46
to
+48
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
For example: Run 1 creates releases at positions 1, 2, 3. A new release is added. Run 2 assigns position 1 to the new release, but the old file at position 1 is never updated. Consider one of:
🤖 Prompt for AI Agents |
||
|
|
||
| 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); | ||
| } | ||
|
Comment on lines
+88
to
+92
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
A safer approach is to also clamp the day: Proposed fix if (scope === 'last-month') {
const cutoff = new Date();
- cutoff.setMonth(cutoff.getMonth() - 1);
+ const targetMonth = cutoff.getMonth() - 1;
+ cutoff.setDate(1); // avoid day overflow
+ cutoff.setMonth(targetMonth);
return releases.filter((r) => new Date(r.published_at) >= cutoff);
}🤖 Prompt for AI Agents |
||
|
|
||
| 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)'); | ||
| } | ||
|
Comment on lines
+110
to
+114
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major
The token check is inside the Proposed fix async function fetchAllReleases() {
const all = [];
let page = 1;
+ 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)');
+ }
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 });🤖 Prompt for AI Agents |
||
|
|
||
| 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(/<img\b[^>]*>/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 ? `` : ''; | ||
| }); | ||
|
|
||
| result = result.replace( | ||
| /(?<!["\(])(?<!\]\()https?:\/\/[^\s)<>\]]+/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`; | ||
| } | ||
|
Comment on lines
+165
to
+176
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. File-name collisions between release stages, and "BETA" mislabeled as "RC". Two issues:
🤖 Prompt for AI Agents |
||
|
|
||
| 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); | ||
| }); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🏁 Script executed:
Repository: reportportal/docs
Length of output: 43
🏁 Script executed:
Repository: reportportal/docs
Length of output: 43
🏁 Script executed:
Repository: reportportal/docs
Length of output: 43
🏁 Script executed:
Repository: reportportal/docs
Length of output: 139
🏁 Script executed:
Repository: reportportal/docs
Length of output: 89
🏁 Script executed:
Repository: reportportal/docs
Length of output: 43
🏁 Script executed:
Repository: reportportal/docs
Length of output: 183
🏁 Script executed:
# Read the deploy.yml file with line numbers cat -n .github/workflows/deploy.ymlRepository: reportportal/docs
Length of output: 1978
🏁 Script executed:
Repository: reportportal/docs
Length of output: 317
🏁 Script executed:
Repository: reportportal/docs
Length of output: 80
🏁 Script executed:
Repository: reportportal/docs
Length of output: 217
🏁 Script executed:
Repository: reportportal/docs
Length of output: 932
Ensure
GH_TOKENsecret is provisioned and properly scoped.The change from the built-in
GITHUB_TOKENto a customGH_TOKENsecret (used in bothdeploy.ymlandsync-releases.yml) requires manual configuration. If this secret is not set in the repository settings, the deploy step will fail.Verify:
GH_TOKENsecret is configured in repository settings.repoorcontents: write).🤖 Prompt for AI Agents