179 lines
5.3 KiB
JavaScript
Executable File
179 lines
5.3 KiB
JavaScript
Executable File
#!/usr/bin/env node
|
||
/*
|
||
* Copyright 2025 The Backstage Authors
|
||
*
|
||
* 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.
|
||
*/
|
||
|
||
import fs from 'fs';
|
||
import path from 'path';
|
||
import { fileURLToPath } from 'url';
|
||
import semver from 'semver';
|
||
import { listWorkspaces } from './list-workspaces.js';
|
||
|
||
const __filename = fileURLToPath(import.meta.url);
|
||
const __dirname = path.dirname(__filename);
|
||
|
||
// get latest stable Backstage release
|
||
async function getLatestBackstageVersion() {
|
||
const response = await fetch(
|
||
'https://versions.backstage.io/v1/tags/main/manifest.json',
|
||
);
|
||
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
|
||
|
||
const manifest = await response.json();
|
||
return manifest.releaseVersion;
|
||
}
|
||
|
||
// get all workspace versions
|
||
async function getWorkspaceVersions() {
|
||
const workspaceNames = await listWorkspaces();
|
||
const workspacesDir = path.join(__dirname, '..', 'workspaces');
|
||
const workspaces = [];
|
||
|
||
for (const workspaceName of workspaceNames) {
|
||
const backstageJsonPath = path.join(
|
||
workspacesDir,
|
||
workspaceName,
|
||
'backstage.json',
|
||
);
|
||
|
||
if (fs.existsSync(backstageJsonPath)) {
|
||
try {
|
||
const backstageJson = JSON.parse(
|
||
fs.readFileSync(backstageJsonPath, 'utf8'),
|
||
);
|
||
if (backstageJson.version) {
|
||
workspaces.push({
|
||
name: workspaceName,
|
||
version: backstageJson.version,
|
||
});
|
||
}
|
||
} catch (error) {
|
||
console.warn(
|
||
`Warning: Could not read version from ${workspaceName}/backstage.json:`,
|
||
error.message,
|
||
);
|
||
}
|
||
}
|
||
}
|
||
|
||
return workspaces;
|
||
}
|
||
|
||
// calculate version difference
|
||
function getVersionDifference(currentVersion, latestVersion) {
|
||
const current = semver.clean(currentVersion);
|
||
const latest = semver.clean(latestVersion);
|
||
|
||
if (!current || !latest) return 0;
|
||
|
||
// if major versions differ, treat as significantly outdated
|
||
if (semver.major(current) !== semver.major(latest)) {
|
||
return semver.major(latest) > semver.major(current) ? 10 : 0;
|
||
}
|
||
|
||
return semver.minor(latest) - semver.minor(current);
|
||
}
|
||
|
||
// categorize workspaces by how outdated they are
|
||
function categorizeWorkspaces(workspaces, latestVersion) {
|
||
const tiers = { tier1: [], tier2: [], tier3: [] };
|
||
|
||
workspaces.forEach(workspace => {
|
||
const versionDiff = getVersionDifference(workspace.version, latestVersion);
|
||
if (versionDiff >= 3) tiers.tier1.push(workspace);
|
||
else if (versionDiff === 2) tiers.tier2.push(workspace);
|
||
else if (versionDiff === 1) tiers.tier3.push(workspace);
|
||
});
|
||
|
||
// sort each tier by workspace name
|
||
Object.values(tiers).forEach(tier =>
|
||
tier.sort((a, b) => a.name.localeCompare(b.name)),
|
||
);
|
||
|
||
return tiers;
|
||
}
|
||
|
||
// generate markdown table for a tier
|
||
function generateTierTable(workspaces, emoji, title, description) {
|
||
if (workspaces.length === 0) return '';
|
||
|
||
let output = `## ${emoji} ${title}${
|
||
description ? ` – ${description}` : ''
|
||
}\n\n`;
|
||
output += '| Workspace | Current Version |\n';
|
||
output += '|-----------|----------------|\n';
|
||
workspaces.forEach(workspace => {
|
||
output += `| ${workspace.name} | ${workspace.version} |\n`;
|
||
});
|
||
return `${output}\n`;
|
||
}
|
||
|
||
// generate the dashboard output
|
||
function generateDashboard(tiers, latestVersion) {
|
||
let output =
|
||
'Tracking workspaces not on the latest Backstage minor version\n\n';
|
||
output += `**Latest Version:** ${latestVersion}\n\n`;
|
||
|
||
output += generateTierTable(
|
||
tiers.tier1,
|
||
'🔴',
|
||
'≥ 3 minor versions behind',
|
||
'',
|
||
);
|
||
output += generateTierTable(tiers.tier2, '🟠', '2 minor versions behind', '');
|
||
output += generateTierTable(tiers.tier3, '🟡', '1 minor version behind', '');
|
||
|
||
const totalOutdated =
|
||
tiers.tier1.length + tiers.tier2.length + tiers.tier3.length;
|
||
if (totalOutdated === 0) {
|
||
output += '## 🎉 All workspaces are up to date!\n\n';
|
||
}
|
||
|
||
output += '---\n\n### Summary\n\n';
|
||
output += `- **Total outdated workspaces:** ${totalOutdated}\n`;
|
||
output += `- **≥ 3 minor versions behind:** ${tiers.tier1.length}\n`;
|
||
output += `- **2 minor versions behind:** ${tiers.tier2.length}\n`;
|
||
output += `- **1 minor version behind:** ${tiers.tier3.length}\n\n`;
|
||
|
||
output += `*Dashboard generated on ${
|
||
new Date().toISOString().split('T')[0]
|
||
}*\n`;
|
||
|
||
return output.trim();
|
||
}
|
||
|
||
// main function
|
||
async function main() {
|
||
const latestVersion = await getLatestBackstageVersion();
|
||
const workspaces = await getWorkspaceVersions();
|
||
const tiers = categorizeWorkspaces(workspaces, latestVersion);
|
||
const dashboard = generateDashboard(tiers, latestVersion);
|
||
|
||
console.log(dashboard);
|
||
}
|
||
|
||
// run the script
|
||
main().catch(error => {
|
||
console.error('Error generating dashboard:', error);
|
||
process.exit(1);
|
||
});
|
||
|
||
export {
|
||
getLatestBackstageVersion,
|
||
getWorkspaceVersions,
|
||
categorizeWorkspaces,
|
||
generateDashboard,
|
||
};
|