Skip to content

Commit bb5f4ed

Browse files
authored
chore: copy upgrade-dashboard scripts from backstage/community-plugins (#1334)
Copies the Backstage Upgrade Dashboard scripts from `backstage/community-plugins`, updating the issue number as appropriate. Refs: #1333 Signed-off-by: Beth Griggs <bethanyngriggs@gmail.com>
1 parent df7e964 commit bb5f4ed

5 files changed

Lines changed: 287 additions & 1 deletion

File tree

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: Backstage Upgrade Dashboard
2+
3+
permissions:
4+
contents: read
5+
issues: write
6+
7+
on:
8+
schedule:
9+
- cron: '0 6 * * *' # run at 6 AM UTC every day
10+
11+
jobs:
12+
update-dashboard:
13+
name: Generate Upgrade Dashboard
14+
runs-on: ubuntu-latest
15+
steps:
16+
- name: Harden Runner
17+
uses: step-security/harden-runner@63c24ba6bd7ba022e95695ff85de572c04a18142 # v2.7.0
18+
with:
19+
egress-policy: audit
20+
21+
- uses: actions/checkout@v4
22+
23+
- name: Set up Node
24+
uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4
25+
with:
26+
node-version: 22
27+
registry-url: https://registry.npmjs.org/
28+
29+
- name: Install dependencies
30+
run: yarn install --immutable
31+
32+
- name: Update Issue
33+
uses: actions/github-script@v7
34+
with:
35+
script: |
36+
const { execSync } = require('child_process');
37+
const dashboardContent = execSync('node scripts/generate-upgrade-dashboard.js', { encoding: 'utf8' });
38+
39+
await github.rest.issues.update({
40+
owner: context.repo.owner,
41+
repo: context.repo.repo,
42+
issue_number: 1333,
43+
body: dashboardContent
44+
});

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"lodash": "^4.17.21",
3737
"lodash.escaperegexp": "^4.1.2",
3838
"node-fetch": "^2.6.7",
39-
"prettier": "^3.4.2"
39+
"prettier": "^3.4.2",
40+
"semver": "^7.7.2"
4041
},
4142
"prettier": "@spotify/prettier-config",
4243
"lint-staged": {
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
import fs from 'fs';
18+
import path from 'path';
19+
import { fileURLToPath } from 'url';
20+
import semver from 'semver';
21+
import { listWorkspaces } from './list-workspaces.js';
22+
23+
const __filename = fileURLToPath(import.meta.url);
24+
const __dirname = path.dirname(__filename);
25+
26+
// get latest stable Backstage release
27+
async function getLatestBackstageVersion() {
28+
const response = await fetch(
29+
'https://versions.backstage.io/v1/tags/main/manifest.json',
30+
);
31+
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
32+
33+
const manifest = await response.json();
34+
return manifest.releaseVersion;
35+
}
36+
37+
// get all workspace versions
38+
async function getWorkspaceVersions() {
39+
const workspaceNames = await listWorkspaces();
40+
const workspacesDir = path.join(__dirname, '..', 'workspaces');
41+
const workspaces = [];
42+
43+
for (const workspaceName of workspaceNames) {
44+
const backstageJsonPath = path.join(
45+
workspacesDir,
46+
workspaceName,
47+
'backstage.json',
48+
);
49+
50+
if (fs.existsSync(backstageJsonPath)) {
51+
try {
52+
const backstageJson = JSON.parse(
53+
fs.readFileSync(backstageJsonPath, 'utf8'),
54+
);
55+
if (backstageJson.version) {
56+
workspaces.push({
57+
name: workspaceName,
58+
version: backstageJson.version,
59+
});
60+
}
61+
} catch (error) {
62+
console.warn(
63+
`Warning: Could not read version from ${workspaceName}/backstage.json:`,
64+
error.message,
65+
);
66+
}
67+
}
68+
}
69+
70+
return workspaces;
71+
}
72+
73+
// calculate version difference
74+
function getVersionDifference(currentVersion, latestVersion) {
75+
const current = semver.clean(currentVersion);
76+
const latest = semver.clean(latestVersion);
77+
78+
if (!current || !latest) return 0;
79+
80+
// if major versions differ, treat as significantly outdated
81+
if (semver.major(current) !== semver.major(latest)) {
82+
return semver.major(latest) > semver.major(current) ? 10 : 0;
83+
}
84+
85+
return semver.minor(latest) - semver.minor(current);
86+
}
87+
88+
// categorize workspaces by how outdated they are
89+
function categorizeWorkspaces(workspaces, latestVersion) {
90+
const tiers = { tier1: [], tier2: [], tier3: [] };
91+
92+
workspaces.forEach(workspace => {
93+
const versionDiff = getVersionDifference(workspace.version, latestVersion);
94+
if (versionDiff >= 3) tiers.tier1.push(workspace);
95+
else if (versionDiff === 2) tiers.tier2.push(workspace);
96+
else if (versionDiff === 1) tiers.tier3.push(workspace);
97+
});
98+
99+
// sort each tier by workspace name
100+
Object.values(tiers).forEach(tier =>
101+
tier.sort((a, b) => a.name.localeCompare(b.name)),
102+
);
103+
104+
return tiers;
105+
}
106+
107+
// generate markdown table for a tier
108+
function generateTierTable(workspaces, emoji, title, description) {
109+
if (workspaces.length === 0) return '';
110+
111+
let output = `## ${emoji} ${title}${
112+
description ? ` – ${description}` : ''
113+
}\n\n`;
114+
output += '| Workspace | Current Version |\n';
115+
output += '|-----------|----------------|\n';
116+
workspaces.forEach(workspace => {
117+
output += `| ${workspace.name} | ${workspace.version} |\n`;
118+
});
119+
return `${output}\n`;
120+
}
121+
122+
// generate the dashboard output
123+
function generateDashboard(tiers, latestVersion) {
124+
let output =
125+
'Tracking workspaces not on the latest Backstage minor version\n\n';
126+
output += `**Latest Version:** ${latestVersion}\n\n`;
127+
128+
output += generateTierTable(
129+
tiers.tier1,
130+
'🔴',
131+
'≥ 3 minor versions behind',
132+
'',
133+
);
134+
output += generateTierTable(tiers.tier2, '🟠', '2 minor versions behind', '');
135+
output += generateTierTable(tiers.tier3, '🟡', '1 minor version behind', '');
136+
137+
const totalOutdated =
138+
tiers.tier1.length + tiers.tier2.length + tiers.tier3.length;
139+
if (totalOutdated === 0) {
140+
output += '## 🎉 All workspaces are up to date!\n\n';
141+
}
142+
143+
output += '---\n\n### Summary\n\n';
144+
output += `- **Total outdated workspaces:** ${totalOutdated}\n`;
145+
output += `- **≥ 3 minor versions behind:** ${tiers.tier1.length}\n`;
146+
output += `- **2 minor versions behind:** ${tiers.tier2.length}\n`;
147+
output += `- **1 minor version behind:** ${tiers.tier3.length}\n\n`;
148+
149+
output += `*Dashboard generated on ${
150+
new Date().toISOString().split('T')[0]
151+
}*\n`;
152+
153+
return output.trim();
154+
}
155+
156+
// main function
157+
async function main() {
158+
const latestVersion = await getLatestBackstageVersion();
159+
const workspaces = await getWorkspaceVersions();
160+
const tiers = categorizeWorkspaces(workspaces, latestVersion);
161+
const dashboard = generateDashboard(tiers, latestVersion);
162+
163+
console.log(dashboard);
164+
}
165+
166+
// run the script
167+
main().catch(error => {
168+
console.error('Error generating dashboard:', error);
169+
process.exit(1);
170+
});
171+
172+
export {
173+
getLatestBackstageVersion,
174+
getWorkspaceVersions,
175+
categorizeWorkspaces,
176+
generateDashboard,
177+
};

scripts/list-workspaces.js

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/*
2+
* Copyright Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
/*
18+
* Copyright 2024 The Backstage Authors
19+
*
20+
* Licensed under the Apache License, Version 2.0 (the "License");
21+
* you may not use this file except in compliance with the License.
22+
* You may obtain a copy of the License at
23+
*
24+
* http://www.apache.org/licenses/LICENSE-2.0
25+
*
26+
* Unless required by applicable law or agreed to in writing, software
27+
* distributed under the License is distributed on an "AS IS" BASIS,
28+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
29+
* See the License for the specific language governing permissions and
30+
* limitations under the License.
31+
*/
32+
33+
import fs from 'fs-extra';
34+
import { resolve } from 'path';
35+
import * as url from 'url';
36+
37+
const __dirname = url.fileURLToPath(new URL('.', import.meta.url));
38+
const EXCLUDED_WORKSPACES = ['noop', 'repo-tools'];
39+
40+
/**
41+
* Retrieves a list of workspace directory names from the 'workspaces' folder.
42+
*
43+
* @returns {Promise<string[]>} A promise that resolves to an array of workspace directory names,
44+
* excluding directories listed in EXCLUDED_WORKSPACES.
45+
* @throws {Error} If there are filesystem errors reading the directory
46+
*/
47+
export async function listWorkspaces() {
48+
const rootPath = resolve(__dirname, '..');
49+
const workspacePath = resolve(rootPath, 'workspaces');
50+
51+
return (await fs.readdir(workspacePath, { withFileTypes: true }))
52+
.filter(w => w.isDirectory() && !EXCLUDED_WORKSPACES.includes(w.name))
53+
.map(w => w.name);
54+
}

yarn.lock

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3909,6 +3909,7 @@ __metadata:
39093909
lodash.escaperegexp: ^4.1.2
39103910
node-fetch: ^2.6.7
39113911
prettier: ^3.4.2
3912+
semver: ^7.7.2
39123913
languageName: unknown
39133914
linkType: soft
39143915

@@ -14934,6 +14935,15 @@ __metadata:
1493414935
languageName: node
1493514936
linkType: hard
1493614937

14938+
"semver@npm:^7.7.2":
14939+
version: 7.7.2
14940+
resolution: "semver@npm:7.7.2"
14941+
bin:
14942+
semver: bin/semver.js
14943+
checksum: dd94ba8f1cbc903d8eeb4dd8bf19f46b3deb14262b6717d0de3c804b594058ae785ef2e4b46c5c3b58733c99c83339068203002f9e37cfe44f7e2cc5e3d2f621
14944+
languageName: node
14945+
linkType: hard
14946+
1493714947
"send@npm:0.19.0":
1493814948
version: 0.19.0
1493914949
resolution: "send@npm:0.19.0"

0 commit comments

Comments
 (0)