mirror of
https://github.com/LukeHagar/usage-statistics.git
synced 2025-12-06 04:21:55 +00:00
chore: bump version to 1.0.11 and build action
This commit is contained in:
286
dist/summaries/github.js
vendored
286
dist/summaries/github.js
vendored
@@ -10,22 +10,27 @@ Chart.register(...registerables);
|
||||
*/
|
||||
function safeSemverCompare(a, b) {
|
||||
try {
|
||||
// Clean and validate versions
|
||||
const cleanA = a.trim();
|
||||
const cleanB = b.trim();
|
||||
// Check if versions are valid semver
|
||||
if (!semver.valid(cleanA) || !semver.valid(cleanB)) {
|
||||
// Fall back to string comparison for invalid semver
|
||||
return cleanA.localeCompare(cleanB);
|
||||
}
|
||||
return semver.compare(cleanA, cleanB);
|
||||
}
|
||||
catch (error) {
|
||||
console.warn(`Semver comparison failed for "${a}" vs "${b}":`, error);
|
||||
// Fall back to string comparison
|
||||
catch {
|
||||
return a.trim().localeCompare(b.trim());
|
||||
}
|
||||
}
|
||||
/**
|
||||
* Prettify a normalized asset key for charts/summaries
|
||||
* Example: "sail|linux|amd64|yaml" -> "Sail / Linux / Amd64 / Yaml"
|
||||
*/
|
||||
function formatAssetLabel(key) {
|
||||
return key
|
||||
.split("|")
|
||||
.map(part => part.charAt(0).toUpperCase() + part.slice(1))
|
||||
.join(" / ");
|
||||
}
|
||||
export function formatGitHubSummary(summary, platformMetrics) {
|
||||
let totalStars = 0;
|
||||
let totalForks = 0;
|
||||
@@ -74,6 +79,48 @@ export async function addRepoDetails(summary, metrics) {
|
||||
summary += `- Views: ${metric.metrics?.viewsCount?.toLocaleString() || 0}\n`;
|
||||
summary += `- Unique Visitors: ${metric.metrics?.uniqueVisitors?.toLocaleString() || 0}\n`;
|
||||
summary += `- Clones: ${metric.metrics?.clonesCount?.toLocaleString() || 0}\n`;
|
||||
// Include top assets using parsed grouping keys for better aggregation
|
||||
const assetTotalsByParsedKey = metric.metrics?.assetTotalsByParsedKey;
|
||||
if (assetTotalsByParsedKey && Object.keys(assetTotalsByParsedKey).length > 0) {
|
||||
const topAssets = Object.entries(assetTotalsByParsedKey)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 5);
|
||||
if (topAssets.length > 0) {
|
||||
summary += `- Top Assets (by downloads):\n`;
|
||||
for (const [assetName, count] of topAssets) {
|
||||
summary += ` - ${assetName}: ${count.toLocaleString()}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
// OS/Arch/Format/Variant breakdowns
|
||||
const totalsByOs = metric.metrics?.totalsByOs;
|
||||
const totalsByArch = metric.metrics?.totalsByArch;
|
||||
const totalsByFormat = metric.metrics?.totalsByFormat;
|
||||
const totalsByVariant = metric.metrics?.totalsByVariant;
|
||||
if (totalsByOs && Object.keys(totalsByOs).length > 0) {
|
||||
const ordered = Object.entries(totalsByOs).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
||||
summary += `- OS Breakdown:\n`;
|
||||
for (const [k, v] of ordered)
|
||||
summary += ` - ${k}: ${v.toLocaleString()}\n`;
|
||||
}
|
||||
if (totalsByArch && Object.keys(totalsByArch).length > 0) {
|
||||
const ordered = Object.entries(totalsByArch).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
||||
summary += `- Arch Breakdown:\n`;
|
||||
for (const [k, v] of ordered)
|
||||
summary += ` - ${k}: ${v.toLocaleString()}\n`;
|
||||
}
|
||||
if (totalsByFormat && Object.keys(totalsByFormat).length > 0) {
|
||||
const ordered = Object.entries(totalsByFormat).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
||||
summary += `- Format Breakdown:\n`;
|
||||
for (const [k, v] of ordered)
|
||||
summary += ` - ${k}: ${v.toLocaleString()}\n`;
|
||||
}
|
||||
if (totalsByVariant && Object.keys(totalsByVariant).length > 0) {
|
||||
const ordered = Object.entries(totalsByVariant).sort((a, b) => b[1] - a[1]).slice(0, 5);
|
||||
summary += `- Variant Breakdown:\n`;
|
||||
for (const [k, v] of ordered)
|
||||
summary += ` - ${k}: ${v.toLocaleString()}\n`;
|
||||
}
|
||||
summary += `\n`;
|
||||
}
|
||||
summary += `\n\n`;
|
||||
@@ -94,31 +141,29 @@ export async function createGitHubReleaseChart(platformMetrics, outputPath) {
|
||||
svgOutputPathList.push(svgOutputPath);
|
||||
const svgOutputPathCumulative = await createCumulativeDownloadsChart(metric, outputPath);
|
||||
svgOutputPathList.push(svgOutputPathCumulative);
|
||||
const svgOutputPathReleases = await createReleaseDownloadsChart(metric, outputPath);
|
||||
svgOutputPathList.push(svgOutputPathReleases);
|
||||
// Create trend charts by dimensions if parsed attributes exist
|
||||
if (metric.metrics?.assetReleaseSeries && metric.metrics?.assetAttributesByKey) {
|
||||
const trends = await createParsedAttributeTrendCharts(metric, outputPath);
|
||||
svgOutputPathList.push(...trends);
|
||||
}
|
||||
}
|
||||
}
|
||||
return svgOutputPathList;
|
||||
}
|
||||
function groupByReleaseCumulative(releaseRange) {
|
||||
const releases = {};
|
||||
for (const release of releaseRange.sort((a, b) => {
|
||||
return safeSemverCompare(a.tagName || '0.0.0', b.tagName || '0.0.0');
|
||||
})) {
|
||||
if (!release.tagName) {
|
||||
for (const release of releaseRange.sort((a, b) => safeSemverCompare(a.tagName || '0.0.0', b.tagName || '0.0.0'))) {
|
||||
if (!release.tagName)
|
||||
continue;
|
||||
}
|
||||
if (!releases[release.tagName]) {
|
||||
releases[release.tagName] = { downloads: release.downloads, tagName: release.tagName || '' };
|
||||
releases[release.tagName] = { downloads: release.downloads, tagName: release.tagName };
|
||||
}
|
||||
else {
|
||||
releases[release.tagName].downloads += release.downloads;
|
||||
}
|
||||
}
|
||||
let cumulativeDownloads = 0;
|
||||
for (const release of Object.keys(releases).sort((a, b) => {
|
||||
return safeSemverCompare(a, b);
|
||||
})) {
|
||||
for (const release of Object.keys(releases).sort((a, b) => safeSemverCompare(a, b))) {
|
||||
cumulativeDownloads += releases[release].downloads;
|
||||
releases[release].downloads = cumulativeDownloads;
|
||||
}
|
||||
@@ -127,17 +172,15 @@ function groupByReleaseCumulative(releaseRange) {
|
||||
export async function createDownloadsPerReleaseChart(metric, outputPath) {
|
||||
const downloadsRange = metric.metrics?.downloadRange || [];
|
||||
const svgOutputPath = `${outputPath}/${metric.name.replace('/', '-')}-release-downloads.svg`;
|
||||
const sortedReleases = downloadsRange.sort((a, b) => {
|
||||
return safeSemverCompare(a.tagName || '0.0.0', b.tagName || '0.0.0');
|
||||
});
|
||||
const sortedReleases = downloadsRange.sort((a, b) => safeSemverCompare(a.tagName || '0.0.0', b.tagName || '0.0.0'));
|
||||
const canvas = new Canvas(1000, 800);
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: sortedReleases.map((release) => release.tagName),
|
||||
labels: sortedReleases.map((r) => r.tagName),
|
||||
datasets: [{
|
||||
label: `${metric.name} Release Downloads`,
|
||||
data: sortedReleases.map((release) => release.downloads),
|
||||
data: sortedReleases.map((r) => r.downloads),
|
||||
backgroundColor: 'rgba(54, 162, 235, 0.8)',
|
||||
borderColor: 'rgba(54, 162, 235, 1)',
|
||||
borderWidth: 1,
|
||||
@@ -146,31 +189,12 @@ export async function createDownloadsPerReleaseChart(metric, outputPath) {
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `${metric.name} - Release Downloads`,
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
title: { display: true, text: `${metric.name} - Release Downloads`, font: { size: 16 } },
|
||||
legend: { display: true }
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Release'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Downloads'
|
||||
},
|
||||
beginAtZero: true
|
||||
}
|
||||
x: { title: { display: true, text: 'Release' } },
|
||||
y: { title: { display: true, text: 'Downloads' }, beginAtZero: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -183,10 +207,7 @@ export async function createCumulativeDownloadsChart(metric, outputPath) {
|
||||
const downloadsRange = metric.metrics?.downloadRange || [];
|
||||
const svgOutputPath = `${outputPath}/${metric.name.replace('/', '-')}-cumulative-release-downloads.svg`;
|
||||
const groupedDownloads = groupByReleaseCumulative(downloadsRange);
|
||||
// Sort months chronologically
|
||||
const semVerSortedReleases = Object.keys(groupedDownloads).sort((a, b) => {
|
||||
return safeSemverCompare(a, b);
|
||||
});
|
||||
const semVerSortedReleases = Object.keys(groupedDownloads).sort((a, b) => safeSemverCompare(a, b));
|
||||
const canvas = new Canvas(1000, 800);
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
@@ -194,7 +215,7 @@ export async function createCumulativeDownloadsChart(metric, outputPath) {
|
||||
labels: semVerSortedReleases,
|
||||
datasets: [{
|
||||
label: `${metric.name} Cumulative Downloads`,
|
||||
data: semVerSortedReleases.map(release => groupedDownloads[release].downloads),
|
||||
data: semVerSortedReleases.map((release) => groupedDownloads[release].downloads),
|
||||
backgroundColor: 'rgba(75, 192, 192, 0.2)',
|
||||
borderColor: 'rgba(75, 192, 192, 1)',
|
||||
borderWidth: 3,
|
||||
@@ -205,31 +226,12 @@ export async function createCumulativeDownloadsChart(metric, outputPath) {
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `${metric.name} - Cumulative Release Downloads`,
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
title: { display: true, text: `${metric.name} - Cumulative Release Downloads`, font: { size: 16 } },
|
||||
legend: { display: true }
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Release'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Downloads'
|
||||
},
|
||||
beginAtZero: true
|
||||
}
|
||||
x: { title: { display: true, text: 'Release' } },
|
||||
y: { title: { display: true, text: 'Downloads' }, beginAtZero: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -238,60 +240,106 @@ export async function createCumulativeDownloadsChart(metric, outputPath) {
|
||||
chart.destroy();
|
||||
return svgOutputPath;
|
||||
}
|
||||
export async function createReleaseDownloadsChart(metric, outputPath) {
|
||||
const downloadsRange = metric.metrics?.downloadRange || [];
|
||||
const svgOutputPath = `${outputPath}/${metric.name.replace('/', '-')}-top-release-downloads.svg`;
|
||||
// Sort releases by date (newest first for display)
|
||||
const sortedReleases = downloadsRange
|
||||
.filter((release) => release.tagName && release.downloads > 0)
|
||||
.sort((a, b) => b.downloads - a.downloads)
|
||||
.slice(0, 10) // Show top 10 releases
|
||||
.sort((a, b) => safeSemverCompare(a.tagName || '0.0.0', b.tagName || '0.0.0'));
|
||||
if (sortedReleases.length === 0) {
|
||||
// Return empty chart if no releases
|
||||
return svgOutputPath;
|
||||
export async function createParsedAttributeTrendCharts(metric, outputPath) {
|
||||
const outputPaths = [];
|
||||
const attrSeries = metric.metrics?.attributeReleaseSeries || { os: {}, arch: {}, format: {}, variant: {}, version: {} };
|
||||
const dims = ['os', 'arch', 'format', 'variant', 'version'];
|
||||
for (const dim of dims) {
|
||||
const groups = attrSeries[dim] || {};
|
||||
const groupNames = Object.keys(groups);
|
||||
if (groupNames.length === 0)
|
||||
continue;
|
||||
// Build union of release tags for this dim
|
||||
const allReleaseTags = Array.from(new Set(groupNames.flatMap(name => Object.keys(groups[name]))))
|
||||
.sort((a, b) => safeSemverCompare(a || '0.0.0', b || '0.0.0'));
|
||||
const svgOutputPath = `${outputPath}/${metric.name.replace('/', '-')}-trend-${dim}.svg`;
|
||||
const datasets = groupNames.map((name, idx) => {
|
||||
const colorHue = (idx * 53) % 360;
|
||||
const borderColor = `hsl(${colorHue}, 70%, 45%)`;
|
||||
const backgroundColor = `hsla(${colorHue}, 70%, 45%, 0.2)`;
|
||||
const byTag = groups[name];
|
||||
return {
|
||||
label: name,
|
||||
data: allReleaseTags.map(tag => byTag[tag] || 0),
|
||||
borderColor,
|
||||
backgroundColor,
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
tension: 0.1
|
||||
};
|
||||
});
|
||||
const canvas = new Canvas(1200, 800);
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'line',
|
||||
data: { labels: allReleaseTags, datasets },
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
title: { display: true, text: `${metric.name} - ${dim.toUpperCase()} Download Trends`, font: { size: 16 } },
|
||||
legend: { display: true }
|
||||
},
|
||||
scales: {
|
||||
x: { title: { display: true, text: 'Release Tag' } },
|
||||
y: {
|
||||
title: { display: true, text: 'Downloads' },
|
||||
beginAtZero: true,
|
||||
ticks: { precision: 0 },
|
||||
suggestedMin: 0,
|
||||
suggestedMax: Math.max(10, ...datasets.map((ds) => Math.max(...ds.data)))
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
|
||||
writeFileSync(svgOutputPath, svgBuffer);
|
||||
chart.destroy();
|
||||
outputPaths.push(svgOutputPath);
|
||||
}
|
||||
return outputPaths;
|
||||
}
|
||||
export async function createAssetDownloadsAcrossReleasesChart(metric, outputPath) {
|
||||
const assetSeries = metric.metrics?.assetReleaseSeries || {};
|
||||
const svgOutputPath = `${outputPath}/${metric.name.replace('/', '-')}-asset-downloads-over-releases.svg`;
|
||||
const assetNames = Object.keys(assetSeries);
|
||||
if (assetNames.length === 0)
|
||||
return svgOutputPath;
|
||||
const allReleaseTagsSet = new Set();
|
||||
for (const name of assetNames) {
|
||||
for (const point of assetSeries[name]) {
|
||||
allReleaseTagsSet.add(point.tagName);
|
||||
}
|
||||
}
|
||||
const allReleaseTags = Array.from(allReleaseTagsSet).sort((a, b) => safeSemverCompare(a || '0.0.0', b || '0.0.0'));
|
||||
const datasets = assetNames.map((name, idx) => {
|
||||
const points = assetSeries[name];
|
||||
const byTag = {};
|
||||
for (const p of points) {
|
||||
byTag[p.tagName] = (byTag[p.tagName] || 0) + (p.downloads || 0);
|
||||
}
|
||||
const colorHue = (idx * 53) % 360;
|
||||
return {
|
||||
label: formatAssetLabel(name),
|
||||
data: allReleaseTags.map(tag => byTag[tag] || 0),
|
||||
borderColor: `hsl(${colorHue}, 70%, 45%)`,
|
||||
backgroundColor: `hsla(${colorHue}, 70%, 45%, 0.2)`,
|
||||
borderWidth: 2,
|
||||
fill: false,
|
||||
tension: 0.1
|
||||
};
|
||||
});
|
||||
const canvas = new Canvas(1200, 800);
|
||||
const chart = new Chart(canvas, {
|
||||
type: 'bar',
|
||||
data: {
|
||||
labels: sortedReleases.map((release) => release.tagName),
|
||||
datasets: [{
|
||||
label: `${metric.name} Release Downloads`,
|
||||
data: sortedReleases.map((release) => release.downloads),
|
||||
backgroundColor: 'rgba(255, 99, 132, 0.8)',
|
||||
borderColor: 'rgba(255, 99, 132, 1)',
|
||||
borderWidth: 1,
|
||||
}]
|
||||
},
|
||||
type: 'line',
|
||||
data: { labels: allReleaseTags, datasets },
|
||||
options: {
|
||||
responsive: true,
|
||||
plugins: {
|
||||
title: {
|
||||
display: true,
|
||||
text: `${metric.name} - Top Release Downloads`,
|
||||
font: {
|
||||
size: 16
|
||||
}
|
||||
},
|
||||
legend: {
|
||||
display: true
|
||||
}
|
||||
title: { display: true, text: `${metric.name} - Asset Downloads Across Releases`, font: { size: 16 } },
|
||||
legend: { display: true }
|
||||
},
|
||||
scales: {
|
||||
x: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Release Tag'
|
||||
}
|
||||
},
|
||||
y: {
|
||||
title: {
|
||||
display: true,
|
||||
text: 'Downloads'
|
||||
},
|
||||
beginAtZero: true
|
||||
}
|
||||
x: { title: { display: true, text: 'Release Tag' } },
|
||||
y: { title: { display: true, text: 'Downloads' }, beginAtZero: true }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user