chore: streamline release workflows by adding version bump and built files commit step; remove redundant asset upload steps

This commit is contained in:
Luke Hagar
2025-08-14 22:29:38 -05:00
parent 1b694ebe63
commit 5cf3629bc8
14 changed files with 2031 additions and 107 deletions

View File

@@ -112,6 +112,19 @@ jobs:
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
- name: Commit version bump and built files
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json dist/
git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }} and build action"
git push
- name: Create and push tag
run: |
git tag v${{ steps.bump.outputs.new_version }}
git push origin v${{ steps.bump.outputs.new_version }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
@@ -133,56 +146,3 @@ jobs:
```
draft: false
prerelease: false
- name: Upload Release Assets
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/action.js
asset_name: action.js
asset_content_type: application/javascript
- name: Upload Release Assets - Collectors
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/collectors/
asset_name: collectors.zip
asset_content_type: application/zip
- name: Upload Release Assets - Summaries
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/summaries/
asset_name: summaries.zip
asset_content_type: application/zip
- name: Upload Release Assets - Utils
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/utils.js
asset_name: utils.js
asset_content_type: application/javascript
- name: Commit version bump
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json
git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }}"
git push
- name: Create and push tag
run: |
git tag v${{ steps.bump.outputs.new_version }}
git push origin v${{ steps.bump.outputs.new_version }}

View File

@@ -95,6 +95,19 @@ jobs:
echo "new_version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT
- name: Commit version bump and built files
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json dist/
git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }} and build action"
git push
- name: Create and push tag
run: |
git tag v${{ steps.bump.outputs.new_version }}
git push origin v${{ steps.bump.outputs.new_version }}
- name: Create Release
id: create_release
uses: actions/create-release@v1
@@ -106,59 +119,6 @@ jobs:
draft: false
prerelease: false
- name: Upload Release Assets
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/action.js
asset_name: action.js
asset_content_type: application/javascript
- name: Upload Release Assets - Collectors
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/collectors/
asset_name: collectors.zip
asset_content_type: application/zip
- name: Upload Release Assets - Summaries
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/summaries/
asset_name: summaries.zip
asset_content_type: application/zip
- name: Upload Release Assets - Utils
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ./dist/utils.js
asset_name: utils.js
asset_content_type: application/javascript
- name: Commit version bump
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add package.json
git commit -m "chore: bump version to ${{ steps.bump.outputs.new_version }}"
git push
- name: Create and push tag
run: |
git tag v${{ steps.bump.outputs.new_version }}
git push origin v${{ steps.bump.outputs.new_version }}
- name: Update action.yml version reference
run: |
# Update the action.yml to reference the new version

1
.gitignore vendored
View File

@@ -7,7 +7,6 @@ yarn-error.log*
# Build outputs
build/
*.tsbuildinfo
dist/
# Environment variables
.env

94
dist/action.js vendored Normal file
View File

@@ -0,0 +1,94 @@
import * as core from '@actions/core';
import { collectNpmBatch } from './collectors/npm.js';
import { collectGithubBatch } from './collectors/github.js';
import { collectPowerShellBatch } from './collectors/powershell.js';
import { collectPypiBatch } from './collectors/pypi.js';
import { getInputs, updateRepositoryReadme } from './utils.js';
import { writeFile } from 'fs/promises';
try {
const { npmPackages, githubRepositories, pypiPackages, powershellModules, jsonOutputPath, updateReadme, commitMessage, readmePath, } = getInputs();
// Debug logs are only output if the `ACTIONS_STEP_DEBUG` secret is true
core.debug(`NPM Packages: ${npmPackages.join(', ')}`);
core.debug(`GitHub Repositories: ${githubRepositories.join(', ')}`);
core.debug(`PyPI Packages: ${pypiPackages.join(', ')}`);
core.debug(`PowerShell Modules: ${powershellModules.join(', ')}`);
core.debug(``);
core.debug(`JSON Output Path: ${jsonOutputPath}`);
core.debug(`Update README: ${updateReadme}`);
core.debug(`Commit Message: ${commitMessage}`);
// Track which platforms are being used
const platformsTracked = [];
if (npmPackages.length > 0)
platformsTracked.push('NPM');
if (githubRepositories.length > 0)
platformsTracked.push('GitHub');
if (pypiPackages.length > 0)
platformsTracked.push('PyPI');
if (powershellModules.length > 0)
platformsTracked.push('PowerShell');
core.debug(`Platforms to track: ${platformsTracked.join(', ')}`);
core.info(`Successfully configured usage statistics tracker for ${platformsTracked.length} platforms`);
const metricPromises = [];
const metrics = [];
for (const platform of platformsTracked) {
core.info(`Collecting ${platform} metrics...`);
switch (platform) {
case 'NPM':
console.log(`Collecting NPM metrics for ${npmPackages.join(', ')}`);
console.time(`Collecting NPM metrics`);
metricPromises.push(collectNpmBatch(npmPackages).then(results => {
console.timeEnd(`Collecting NPM metrics`);
return results;
}));
break;
case 'GitHub':
console.log(`Collecting GitHub metrics for ${githubRepositories.join(', ')}`);
console.time(`Collecting GitHub metrics`);
metricPromises.push(collectGithubBatch(githubRepositories).then(results => {
console.timeEnd(`Collecting GitHub metrics`);
return results;
}));
break;
case 'PyPI':
console.log(`Collecting PyPI metrics for ${pypiPackages.join(', ')}`);
console.time(`Collecting PyPI metrics`);
metricPromises.push(collectPypiBatch(pypiPackages).then(results => {
console.timeEnd(`Collecting PyPI metrics`);
return results;
}));
break;
case 'PowerShell':
console.log(`Collecting PowerShell metrics for ${powershellModules.join(', ')}`);
console.time(`Collecting PowerShell metrics`);
metricPromises.push(collectPowerShellBatch(powershellModules).then(results => {
console.timeEnd(`Collecting PowerShell metrics`);
return results;
}));
break;
}
}
console.log('All metrics collecting started');
const metricResults = await Promise.all(metricPromises);
metrics.push(...metricResults.flat());
console.log('All metrics collecting completed');
if (updateReadme) {
console.log('Updating repository readme...');
await updateRepositoryReadme(metrics, readmePath);
}
console.log('Repository readme updated');
// Persist full result set to JSON for downstream consumption
try {
await writeFile(jsonOutputPath, JSON.stringify(metrics, null, 2), 'utf8');
core.setOutput('json-output', jsonOutputPath);
console.log(`Wrote metrics JSON to ${jsonOutputPath}`);
}
catch (writeErr) {
console.warn(`Failed to write metrics JSON to ${jsonOutputPath}:`, writeErr);
}
core.setOutput('commit-message', commitMessage);
}
catch (error) {
// Fail the workflow run if an error occurs
if (error instanceof Error)
core.setFailed(error.message);
}

268
dist/collectors/github.js vendored Normal file
View File

@@ -0,0 +1,268 @@
/**
* GitHub repository statistics collector with enhanced metrics using Octokit SDK and GraphQL
*/
import { Octokit } from '@octokit/rest';
import { graphql } from '@octokit/graphql';
const PlatformSettings = {
name: 'GitHub',
};
// GraphQL query for basic repository data (without releases)
const REPOSITORY_BASIC_QUERY = `
query RepositoryBasicData($owner: String!, $name: String!) {
repository(owner: $owner, name: $name) {
id
name
description
homepageUrl
stargazerCount
forkCount
watchers {
totalCount
}
openIssues: issues(states: OPEN) {
totalCount
}
closedIssues: issues(states: CLOSED) {
totalCount
}
primaryLanguage {
name
}
diskUsage
createdAt
updatedAt
pushedAt
defaultBranchRef {
name
}
repositoryTopics(first: 10) {
nodes {
topic {
name
}
}
}
licenseInfo {
name
spdxId
}
}
}
`;
// GraphQL query for releases with download data
const RELEASES_QUERY = `
query RepositoryReleases($owner: String!, $name: String!, $first: Int!) {
repository(owner: $owner, name: $name) {
releases(first: $first, orderBy: {field: CREATED_AT, direction: DESC}) {
nodes {
id
tagName
name
description
createdAt
publishedAt
releaseAssets(first: 100) {
nodes {
id
name
size
downloadCount
downloadUrl
}
}
}
}
}
}
`;
export async function collectGithub(repository) {
try {
const [owner, repo] = repository.split('/');
if (!owner || !repo) {
throw new Error(`Invalid repository format: ${repository}. Expected "owner/repo"`);
}
// Initialize Octokit for REST API calls
const token = process.env.GITHUB_TOKEN || process.env.INPUT_GITHUB_TOKEN || '';
const octokit = new Octokit({
auth: token,
userAgent: 'usage-statistics-tracker'
});
if (!token) {
console.warn('No GitHub token provided. Using unauthenticated requests (rate limited).');
}
// Step 1: Fetch basic repository data using GraphQL
let graphqlData = null;
try {
const graphqlClient = graphql.defaults({
headers: {
authorization: token ? `token ${token}` : undefined,
},
});
// Fetch basic repository data (without releases)
const basicResponse = await graphqlClient(REPOSITORY_BASIC_QUERY, {
owner,
name: repo
});
if (basicResponse.repository) {
graphqlData = basicResponse.repository;
}
}
catch (error) {
console.warn(`Could not fetch GitHub GraphQL basic data for ${repository}:`, error);
}
// Step 2: Fetch releases data separately using GraphQL
let totalReleaseDownloads = 0;
let latestReleaseDownloads = 0;
let releaseCount = 0;
let downloadRange = [];
let latestRelease = null;
try {
const graphqlClient = graphql.defaults({
headers: {
authorization: token ? `token ${token}` : undefined,
},
});
// Fetch releases data
const releasesResponse = await graphqlClient(RELEASES_QUERY, {
owner,
name: repo,
first: 100
});
if (releasesResponse.repository?.releases?.nodes) {
const releases = releasesResponse.repository.releases.nodes.filter(Boolean);
releaseCount = releases.length;
for (const release of releases) {
let releaseDownloads = 0;
if (release?.releaseAssets?.nodes) {
for (const asset of release.releaseAssets.nodes) {
if (asset) {
releaseDownloads += asset.downloadCount || 0;
}
}
}
totalReleaseDownloads += releaseDownloads;
// Latest release is the first one in the list
if (release && release === releases[0]) {
latestReleaseDownloads = releaseDownloads;
latestRelease = release.tagName;
}
// Add to download range with proper date format for charts
if (release?.publishedAt) {
downloadRange.push({
day: release.publishedAt,
downloads: releaseDownloads,
tagName: release.tagName
});
}
}
}
}
catch (error) {
console.warn(`Could not fetch GitHub GraphQL releases data for ${repository}:`, error);
}
// Fallback to REST API if GraphQL fails or for additional data
let restData = null;
try {
const { data: repoData } = await octokit.repos.get({
owner,
repo
});
restData = repoData;
}
catch (error) {
console.warn(`Could not fetch GitHub REST data for ${repository}:`, error);
}
// Use the best available data (GraphQL preferred, REST as fallback)
const finalData = graphqlData || restData;
if (!finalData) {
throw new Error('Could not fetch repository data from either GraphQL or REST API');
}
// Get traffic statistics using REST API (requires authentication)
let viewsCount = 0;
let uniqueVisitors = 0;
let clonesCount = 0;
if (token) {
try {
// Get views data
const { data: viewsData } = await octokit.repos.getViews({
owner,
repo
});
if (viewsData) {
viewsCount = viewsData.count || 0;
uniqueVisitors = viewsData.uniques || 0;
}
// Get clones data
const { data: clonesData } = await octokit.repos.getClones({
owner,
repo
});
if (clonesData) {
clonesCount = clonesData.count || 0;
}
}
catch (error) {
console.warn(`Could not fetch GitHub traffic data for ${repository}:`, error);
}
}
// Calculate repository age
let repositoryAge = 0;
if (finalData.createdAt) {
const created = new Date(finalData.createdAt);
const now = new Date();
repositoryAge = Math.floor((now.getTime() - created.getTime()) / (1000 * 60 * 60 * 24)); // days
}
// Calculate activity metrics
let lastActivity = 0;
if (finalData.pushedAt) {
const pushed = new Date(finalData.pushedAt);
const now = new Date();
lastActivity = Math.floor((now.getTime() - pushed.getTime()) / (1000 * 60 * 60 * 24)); // days
}
return {
platform: PlatformSettings.name,
name: repository,
timestamp: new Date().toISOString(),
metrics: {
stars: finalData.stargazerCount || finalData.stargazers_count || 0,
forks: finalData.forkCount || finalData.forks_count || 0,
watchers: finalData.watchers?.totalCount || finalData.watchers_count || 0,
totalIssues: finalData.openIssues?.totalCount + finalData.closedIssues?.totalCount || 0,
openIssues: finalData.openIssues?.totalCount || 0,
closedIssues: finalData.closedIssues?.totalCount || 0,
language: finalData.primaryLanguage?.name || finalData.language || null,
size: finalData.diskUsage || finalData.size || null,
repositoryAge,
lastActivity,
releaseCount,
totalReleaseDownloads,
latestReleaseDownloads,
viewsCount,
uniqueVisitors,
latestRelease,
clonesCount,
topics: finalData.repositoryTopics?.nodes?.length || finalData.topics?.length || 0,
license: finalData.licenseInfo?.name || finalData.license?.name || null,
defaultBranch: finalData.defaultBranchRef?.name || finalData.default_branch || null,
downloadsTotal: totalReleaseDownloads || 0,
downloadRange,
}
};
}
catch (error) {
return {
platform: PlatformSettings.name,
name: repository,
timestamp: new Date().toISOString(),
metrics: {},
error: error instanceof Error ? error.message : String(error)
};
}
}
export async function collectGithubBatch(repositories) {
const results = [];
for (const repo of repositories) {
results.push(collectGithub(repo));
}
return Promise.all(results);
}

112
dist/collectors/npm.js vendored Normal file
View File

@@ -0,0 +1,112 @@
/**
* NPM package statistics collector with enhanced metrics
*/
const PlatformSettings = {
name: 'NPM',
};
const BASE_URL = 'https://api.npmjs.org/downloads/range';
const CHUNK_DAYS = 540; // 18 months max per request
const START_DATE = new Date('2015-01-10'); // Earliest NPM data
function formatDate(date) {
return date.toISOString().split('T')[0];
}
function addDays(date, days) {
const result = new Date(date);
result.setDate(result.getDate() + days);
return result;
}
async function fetchChunk(start, end, packageName) {
const url = `${BASE_URL}/${formatDate(start)}:${formatDate(end)}/${packageName}`;
const res = await fetch(url);
if (!res.ok) {
throw new Error(`Failed to fetch data: ${res.status} ${res.statusText}`);
}
const json = await res.json();
return json.downloads;
}
async function getFullDownloadHistory(packageName, startDate) {
const today = new Date();
let currentStart = new Date(startDate);
let allDownloads = [];
while (currentStart < today) {
const currentEnd = addDays(currentStart, CHUNK_DAYS - 1);
const end = currentEnd > today ? today : currentEnd;
console.log(`Fetching ${formatDate(currentStart)} to ${formatDate(end)}...`);
const chunk = await fetchChunk(currentStart, end, packageName);
allDownloads = allDownloads.concat(chunk);
currentStart = addDays(end, 1); // move to next chunk
}
return Array.from(new Set(allDownloads));
}
export async function collectNpm(packageName) {
try {
// Get package info from npm registry
const packageUrl = `https://registry.npmjs.org/${packageName}`;
const packageResponse = await fetch(packageUrl);
const packageData = await packageResponse.json();
// Get download statistics
let downloadsMonthly;
let downloadsWeekly;
let downloadsDaily;
try {
// Monthly downloads
const monthlyUrl = `https://api.npmjs.org/downloads/point/last-month/${packageName}`;
const monthlyResponse = await fetch(monthlyUrl);
const monthlyData = await monthlyResponse.json();
downloadsMonthly = monthlyData.downloads || null;
}
catch (error) {
console.warn(`Could not fetch NPM monthly downloads for ${packageName}:`, error);
}
try {
// Weekly downloads
const weeklyUrl = `https://api.npmjs.org/downloads/point/last-week/${packageName}`;
const weeklyResponse = await fetch(weeklyUrl);
const weeklyData = await weeklyResponse.json();
downloadsWeekly = weeklyData.downloads || null;
}
catch (error) {
console.warn(`Could not fetch NPM weekly downloads for ${packageName}:`, error);
}
try {
// Daily downloads
const dailyUrl = `https://api.npmjs.org/downloads/point/last-day/${packageName}`;
const dailyResponse = await fetch(dailyUrl);
const dailyData = await dailyResponse.json();
downloadsDaily = dailyData.downloads || null;
}
catch (error) {
console.warn(`Could not fetch NPM daily downloads for ${packageName}:`, error);
}
const downloadsRange = await getFullDownloadHistory(packageName, new Date(packageData.time?.created || START_DATE));
const downloadsTotal = downloadsRange.reduce((acc, curr) => acc + curr.downloads, 0);
return {
platform: PlatformSettings.name,
name: packageName,
timestamp: new Date().toISOString(),
metrics: {
downloadsTotal,
downloadsMonthly,
downloadsWeekly,
downloadsDaily,
downloadsRange,
}
};
}
catch (error) {
return {
platform: PlatformSettings.name,
name: packageName,
timestamp: new Date().toISOString(),
metrics: {},
error: error instanceof Error ? error.message : String(error)
};
}
}
export async function collectNpmBatch(packageNames) {
const resultPromises = [];
for (const packageName of packageNames) {
resultPromises.push(collectNpm(packageName));
}
return Promise.all(resultPromises);
}

176
dist/collectors/powershell.js vendored Normal file
View File

@@ -0,0 +1,176 @@
/**
* PowerShell Gallery module statistics collector with enhanced metrics
*/
import { XMLParser } from 'fast-xml-parser';
const PlatformSettings = {
name: 'PowerShell',
};
const BASE_URL = 'https://www.powershellgallery.com/api/v2/';
const parser = new XMLParser({
ignoreAttributes: false,
attributeNamePrefix: '',
});
function parsePowerShellGalleryEntry(entry) {
const props = entry["m:properties"];
const getText = (field) => field?.["#text"];
const isTrue = (field) => field?.["#text"] === true;
const getNumber = (field) => field?.["#text"];
const getDate = (field) => {
const dateText = getText(field);
if (!dateText || dateText === '') {
return new Date(0); // Return epoch date for invalid dates
}
return new Date(dateText);
};
return {
id: entry.id,
name: props["d:Id"],
version: props["d:Version"],
normalizedVersion: props["d:NormalizedVersion"],
authors: props["d:Authors"],
description: props["d:Description"],
downloadCount: getNumber(props["d:DownloadCount"]),
versionDownloadCount: getNumber(props["d:VersionDownloadCount"]),
published: getDate(props["d:Published"]),
lastUpdated: getDate(props["d:LastUpdated"]),
created: getDate(props["d:Created"]),
isLatest: isTrue(props["d:IsLatestVersion"]),
isPrerelease: isTrue(props["d:IsPrerelease"]),
projectUrl: getText(props["d:ProjectUrl"]) ?? undefined,
reportAbuseUrl: props["d:ReportAbuseUrl"],
galleryDetailsUrl: props["d:GalleryDetailsUrl"],
packageSize: getNumber(props["d:PackageSize"]),
companyName: props["d:CompanyName"],
owners: props["d:Owners"],
};
}
/**
* Fetches all versions of a package.
* Equivalent to: FindPackagesById()?id='PackageName'
*/
export async function findPackagesById(id) {
const url = `${BASE_URL}FindPackagesById()?id='${encodeURIComponent(id)}'`;
const res = await fetch(url);
const xml = await res.text();
const json = parser.parse(xml);
return json.feed.entry ?? [];
}
/**
* Fetches metadata for a specific version of a package.
* Equivalent to: Packages(Id='Name',Version='x.y.z')
*/
export async function getPackageVersionInfo(id, version) {
const url = `${BASE_URL}Packages(Id='${encodeURIComponent(id)}',Version='${encodeURIComponent(version)}')`;
const res = await fetch(url);
const xml = await res.text();
const json = parser.parse(xml);
return json.entry;
}
/**
* Searches the PowerShell Gallery with a search term.
* Equivalent to: Search()?searchTerm='term'&includePrerelease=false
*/
export async function searchPackages(searchTerm, includePrerelease = false) {
const url = `${BASE_URL}Search()?searchTerm='${encodeURIComponent(searchTerm)}'&includePrerelease=${includePrerelease.toString()}`;
const res = await fetch(url);
const xml = await res.text();
const json = parser.parse(xml);
return json.feed?.entry ?? [];
}
/**
* Sum total download count for all versions of a package.
*/
export async function getTotalDownloadCount(id) {
const entries = await findPackagesById(id);
const versions = Array.isArray(entries) ? entries : [entries];
return versions.reduce((sum, entry) => {
const count = entry['m:properties']?.['d:DownloadCount']?.['#text'];
return sum + count;
}, 0);
}
export async function collectPowerShell(moduleName) {
try {
// Get all versions of the package
const allVersions = await findPackagesById(moduleName);
if (!allVersions || allVersions.length === 0) {
throw new Error(`Module ${moduleName} not found`);
}
const versions = [];
for (const version of allVersions) {
const parsedVersion = parsePowerShellGalleryEntry(version);
versions.push(parsedVersion);
}
// Sort versions by published date (newest first)
const sortedVersions = versions.sort((a, b) => b.published.getTime() - a.published.getTime());
let downloadsTotal = 0;
let latestVersionDownloads = 0;
let downloadRange = [];
let latestVersion = '';
let latestVersionDate = '';
// Process each version
for (const version of sortedVersions) {
// Use Created date if Published date is invalid (1900-01-01)
const effectiveDate = version.published.getTime() === new Date('1900-01-01T00:00:00').getTime()
? version.created
: version.published;
// Skip versions with invalid dates
if (effectiveDate.getTime() === 0) {
continue;
}
downloadsTotal += version.versionDownloadCount;
// Track latest version downloads
if (version.isLatest) {
latestVersionDownloads = version.versionDownloadCount;
latestVersion = version.version;
latestVersionDate = effectiveDate.toISOString();
}
const rangeEntry = {
day: effectiveDate.toISOString(),
downloads: version.versionDownloadCount,
version: version.version
};
// Add to download range for charts
downloadRange.push(rangeEntry);
}
// Get latest version metadata
const latestModuleData = sortedVersions[0];
const result = {
platform: PlatformSettings.name,
name: moduleName,
timestamp: new Date().toISOString(),
metrics: {
downloadsTotal,
downloadsRange: downloadRange,
latestVersionDownloads,
latestVersion,
latestVersionDate,
versionCount: versions.length,
lastUpdated: latestModuleData.lastUpdated.toISOString(),
// Additional metadata
authors: latestModuleData.authors,
description: latestModuleData.description,
projectUrl: latestModuleData.projectUrl,
packageSize: latestModuleData.packageSize,
companyName: latestModuleData.companyName,
owners: latestModuleData.owners,
}
};
return result;
}
catch (error) {
console.warn('Error collecting PowerShell module:', error);
return {
platform: PlatformSettings.name,
name: moduleName,
timestamp: new Date().toISOString(),
error: error instanceof Error ? error.message : String(error),
};
}
}
export async function collectPowerShellBatch(moduleNames) {
const resultPromises = [];
for (const moduleName of moduleNames) {
resultPromises.push(collectPowerShell(moduleName));
}
return Promise.all(resultPromises);
}

151
dist/collectors/pypi.js vendored Normal file
View File

@@ -0,0 +1,151 @@
/**
* PyPI package statistics collector using external PyPI Stats API
*/
const PlatformSettings = {
name: 'PyPI',
};
// (no BigQuery historical metrics; all data comes from the external API)
// External PyPI Stats API base URL
const PYPI_STATS_BASE_URL = process.env.PYPI_STATS_BASE_URL || 'https://pypistats.dev';
function normalizePackageName(name) {
return name.replace(/[._]/g, '-').toLowerCase();
}
async function fetchJson(url) {
const res = await fetch(url);
if (!res.ok)
throw new Error(`Request failed ${res.status}: ${url}`);
return res.json();
}
export async function collectPypi(packageName) {
const normalized = normalizePackageName(packageName);
try {
// Package metadata
const packageDataPromise = fetchJson(`https://pypi.org/pypi/${normalized}/json`);
const recentPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/recent`);
const summaryPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/summary`);
const overallPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/overall`);
const pythonMajorPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/python_major`);
const pythonMinorPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/python_minor`);
const systemPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/system`);
const installerPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/installer`);
const overallChartPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/chart/overall?format=json`);
const pythonMajorChartPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/chart/python_major?format=json`);
const pythonMinorChartPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/chart/python_minor?format=json`);
const systemChartPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/chart/system?format=json`);
const installerChartPromise = fetchJson(`${PYPI_STATS_BASE_URL}/api/packages/${normalized}/chart/installer?format=json`);
const [packageData, recent, summary, overall, pythonMajor, pythonMinor, system, installer, overallChart, pythonMajorChart, pythonMinorChart, systemChart, installerChart] = await Promise.all([
packageDataPromise,
recentPromise,
summaryPromise,
overallPromise,
pythonMajorPromise,
pythonMinorPromise,
systemPromise,
installerPromise,
overallChartPromise,
pythonMajorChartPromise,
pythonMinorChartPromise,
systemChartPromise,
installerChartPromise,
]);
// All time-series and breakdowns are provided by the external API
const overallSeries = (overall.data || []).filter(p => p.category === 'without_mirrors');
const systemBreakdown = summary.totals?.system || null;
const pythonVersionBreakdown = summary.totals?.python_major
? Object.fromEntries(Object.entries(summary.totals.python_major).filter(([k]) => /^\d+$/.test(k)).map(([k, v]) => [`python${k}`, v]))
: null;
const pythonMinorBreakdown = summary.totals?.python_minor
? Object.fromEntries(Object.entries(summary.totals.python_minor).filter(([k]) => /^\d+(?:\.\d+)?$/.test(k)).map(([k, v]) => [`python${k}`, v]))
: null;
// Derive popular system and installer from totals/series
let popularSystem;
if (systemBreakdown && Object.keys(systemBreakdown).length > 0) {
popularSystem = Object.entries(systemBreakdown).sort((a, b) => b[1] - a[1])[0]?.[0];
}
else if (system.data && system.data.length > 0) {
const totals = {};
for (const p of system.data)
totals[p.category] = (totals[p.category] || 0) + p.downloads;
popularSystem = Object.entries(totals).sort((a, b) => b[1] - a[1])[0]?.[0];
}
let popularInstaller;
if (installer.data && installer.data.length > 0) {
const totals = {};
for (const p of installer.data)
totals[p.category] = (totals[p.category] || 0) + p.downloads;
popularInstaller = Object.entries(totals).sort((a, b) => b[1] - a[1])[0]?.[0];
}
// Latest release date for current version
let latestReleaseDate;
try {
const currentVersion = packageData.info?.version;
const files = currentVersion ? (packageData.releases?.[currentVersion] || []) : [];
const latestUpload = files.reduce((max, f) => {
const t = f.upload_time;
if (!t)
return max;
if (!max)
return t;
return new Date(t) > new Date(max) ? t : max;
}, undefined);
if (latestUpload) {
const d = new Date(latestUpload);
if (!isNaN(d.getTime()))
latestReleaseDate = d.toISOString().slice(0, 10);
}
}
catch { }
return {
platform: PlatformSettings.name,
name: packageName,
timestamp: new Date().toISOString(),
metrics: {
downloadsTotal: summary.totals?.overall,
downloadsMonthly: recent.data?.last_month,
downloadsWeekly: recent.data?.last_week,
downloadsDaily: recent.data?.last_day,
version: packageData.info?.version,
latestReleaseDate,
description: packageData.info?.summary,
homepage: packageData.info?.home_page,
author: packageData.info?.author,
license: packageData.info?.license,
requiresPython: packageData.info?.requires_python,
releases: Object.keys(packageData.releases || {}).length,
downloadsRange: overallSeries.map(p => ({ day: p.date, downloads: p.downloads })),
overallSeries,
pythonMajorSeries: (pythonMajor.data || []).filter(p => p.category?.toLowerCase?.() !== 'unknown'),
pythonMinorSeries: (pythonMinor.data || []).filter(p => p.category?.toLowerCase?.() !== 'unknown'),
systemSeries: system.data || [],
installerSeries: installer.data || [],
popularSystem,
popularInstaller,
// Server-prepared chart JSON (preferred for rendering)
overallChart,
pythonMajorChart: { ...pythonMajorChart, datasets: (pythonMajorChart.datasets || []).filter(ds => !/unknown/i.test(ds.label)) },
pythonMinorChart: { ...pythonMinorChart, datasets: (pythonMinorChart.datasets || []).filter(ds => !/unknown/i.test(ds.label)) },
systemChart,
installerChart,
pythonVersionBreakdown,
pythonMinorBreakdown,
systemBreakdown,
}
};
}
catch (error) {
return {
platform: PlatformSettings.name,
name: packageName,
timestamp: new Date().toISOString(),
metrics: {},
error: error instanceof Error ? error.message : String(error)
};
}
}
export async function collectPypiBatch(packageNames) {
const results = [];
for (const packageName of packageNames) {
results.push(collectPypi(packageName));
}
return Promise.all(results);
}

4
dist/collectors/types.js vendored Normal file
View File

@@ -0,0 +1,4 @@
/**
* Core types for the simplified usage statistics system
*/
export {};

280
dist/summaries/github.js vendored Normal file
View File

@@ -0,0 +1,280 @@
import { mkdirSync, writeFileSync } from "fs";
import { Chart, registerables } from 'chart.js';
import { Canvas } from 'skia-canvas';
import semver from "semver";
// Register all Chart.js controllers
Chart.register(...registerables);
export function formatGitHubSummary(summary, platformMetrics) {
let totalStars = 0;
let totalForks = 0;
let totalWatchers = 0;
let totalIssues = 0;
let totalOpenIssues = 0;
let totalClosedIssues = 0;
let totalDownloads = 0;
let totalReleases = 0;
summary += `| Repository | Stars | Forks | Watchers | Open Issues | Closed Issues | Total Issues | Release Downloads | Releases | Latest Release | Language |\n`;
summary += `| --- | --- | --- | --- | --- | --- | --- | --- | --- | --- | --- |\n`;
for (const metric of platformMetrics) {
const stars = metric.metrics?.stars || 0;
const forks = metric.metrics?.forks || 0;
const watchers = metric.metrics?.watchers || 0;
const issues = metric.metrics?.totalIssues || 0;
const openIssues = metric.metrics?.openIssues || 0;
const closedIssues = metric.metrics?.closedIssues || 0;
const downloads = metric.metrics?.totalReleaseDownloads || 0;
const releases = metric.metrics?.releaseCount || 0;
const latestRelease = metric.metrics?.latestRelease || 'N/A';
const language = metric.metrics?.language || 'N/A';
totalStars += stars;
totalForks += forks;
totalWatchers += watchers;
totalIssues += issues;
totalOpenIssues += openIssues;
totalClosedIssues += closedIssues;
totalDownloads += downloads;
totalReleases += releases;
summary += `| ${metric.name} | ${stars.toLocaleString()} | ${forks.toLocaleString()} | ${watchers.toLocaleString()} | ${openIssues.toLocaleString()} | ${closedIssues.toLocaleString()} | ${issues.toLocaleString()} | ${downloads.toLocaleString()} | ${releases.toLocaleString()} | ${latestRelease} | ${language} |\n`;
}
summary += `| **Total** | **${totalStars.toLocaleString()}** | **${totalForks.toLocaleString()}** | **${totalWatchers.toLocaleString()}** | **${totalOpenIssues.toLocaleString()}** | **${totalClosedIssues.toLocaleString()}** | **${totalIssues.toLocaleString()}** | **${totalDownloads.toLocaleString()}** | **${totalReleases.toLocaleString()}** | | |\n`;
return summary;
}
export async function addRepoDetails(summary, metrics) {
summary += `#### Repository Details:\n\n`;
for (const metric of metrics) {
summary += `**${metric.name}**:\n`;
summary += `- Last Activity: ${metric.metrics?.lastActivity?.toLocaleString() || 0} days ago\n`;
summary += `- Repository Age: ${metric.metrics?.repositoryAge?.toLocaleString() || 0} days\n`;
summary += `- Release Count: ${metric.metrics?.releaseCount?.toLocaleString() || 0}\n`;
summary += `- Total Release Downloads: ${metric.metrics?.totalReleaseDownloads?.toLocaleString() || 0}\n`;
summary += `- Latest Release: ${metric.metrics?.latestRelease || 'N/A'}\n`;
summary += `- Latest Release Downloads: ${metric.metrics?.latestReleaseDownloads?.toLocaleString() || 0}\n`;
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`;
summary += `\n`;
}
summary += `\n\n`;
const chatOutputPath = './charts/github';
mkdirSync(chatOutputPath, { recursive: true });
const svgOutputPathList = await createGitHubReleaseChart(metrics, chatOutputPath);
for (const svgOutputPath of svgOutputPathList) {
summary += `![${svgOutputPath}](${svgOutputPath})\n`;
}
return summary;
}
export async function createGitHubReleaseChart(platformMetrics, outputPath) {
const svgOutputPathList = [];
for (const metric of platformMetrics) {
// Only create charts if there's download data
if (metric.metrics?.downloadRange && metric.metrics.downloadRange.length > 0) {
const svgOutputPath = await createDownloadsPerReleaseChart(metric, outputPath);
svgOutputPathList.push(svgOutputPath);
const svgOutputPathCumulative = await createCumulativeDownloadsChart(metric, outputPath);
svgOutputPathList.push(svgOutputPathCumulative);
const svgOutputPathReleases = await createReleaseDownloadsChart(metric, outputPath);
svgOutputPathList.push(svgOutputPathReleases);
}
}
return svgOutputPathList;
}
function groupByReleaseCumulative(releaseRange) {
const releases = {};
for (const release of releaseRange.sort((a, b) => {
return semver.compare(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 || '' };
}
else {
releases[release.tagName].downloads += release.downloads;
}
}
let cumulativeDownloads = 0;
for (const release of Object.keys(releases).sort((a, b) => {
return semver.compare(a, b);
})) {
cumulativeDownloads += releases[release].downloads;
releases[release].downloads = cumulativeDownloads;
}
return releases;
}
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 semver.compare(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),
datasets: [{
label: `${metric.name} Release Downloads`,
data: sortedReleases.map((release) => release.downloads),
backgroundColor: 'rgba(54, 162, 235, 0.8)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 1,
}]
},
options: {
responsive: true,
plugins: {
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
}
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath;
}
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 semver.compare(a, b);
});
const canvas = new Canvas(1000, 800);
const chart = new Chart(canvas, {
type: 'line',
data: {
labels: semVerSortedReleases,
datasets: [{
label: `${metric.name} Cumulative Downloads`,
data: semVerSortedReleases.map(release => groupedDownloads[release].downloads),
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
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
}
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
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) => semver.compare(a.tagName || '0.0.0', b.tagName || '0.0.0'));
if (sortedReleases.length === 0) {
// Return empty chart if no releases
return svgOutputPath;
}
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,
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: `${metric.name} - Top Release Downloads`,
font: {
size: 16
}
},
legend: {
display: true
}
},
scales: {
x: {
title: {
display: true,
text: 'Release Tag'
}
},
y: {
title: {
display: true,
text: 'Downloads'
},
beginAtZero: true
}
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath;
}

171
dist/summaries/npm.js vendored Normal file
View File

@@ -0,0 +1,171 @@
import { mkdirSync, writeFileSync } from 'node:fs';
import { Chart, registerables } from 'chart.js';
import { Canvas } from 'skia-canvas';
// Register all Chart.js controllers
Chart.register(...registerables);
export function formatNpmSummary(summary, platformMetrics) {
let totalDownloads = 0;
let totalMonthlyDownloads = 0;
let totalWeeklyDownloads = 0;
let totalDailyDownloads = 0;
summary += `| Package | Downloads | Monthly Downloads | Weekly Downloads | Daily Downloads |\n`;
summary += `| --- | --- | --- | --- | --- |\n`;
for (const metric of platformMetrics) {
const downloads = metric.metrics?.downloadsTotal || 0;
const monthlyDownloads = metric.metrics?.downloadsMonthly || 0;
const weeklyDownloads = metric.metrics?.downloadsWeekly || 0;
const dailyDownloads = metric.metrics?.downloadsDaily || 0;
totalDownloads += downloads;
totalMonthlyDownloads += monthlyDownloads;
totalWeeklyDownloads += weeklyDownloads;
totalDailyDownloads += dailyDownloads;
summary += `| ${metric.name} | ${downloads.toLocaleString()} | ${monthlyDownloads.toLocaleString()} | ${weeklyDownloads.toLocaleString()} | ${dailyDownloads.toLocaleString()} |\n`;
}
summary += `| **Total** | **${totalDownloads.toLocaleString()}** | **${totalMonthlyDownloads.toLocaleString()}** | **${totalWeeklyDownloads.toLocaleString()}** | **${totalDailyDownloads.toLocaleString()}** | | | | |\n`;
return summary;
}
// Convert a list of dates into a list of Months
function groupByMonth(dateRange) {
const months = {};
for (const range of dateRange) {
const month = new Date(range.day).toLocaleDateString('en-US', { month: 'short', year: '2-digit' });
if (!months[month]) {
months[month] = range.downloads;
}
else {
months[month] += range.downloads;
}
}
return months;
}
function groupByMonthCumulative(dateRange) {
const months = {};
for (const range of dateRange) {
const month = new Date(range.day).toLocaleDateString('en-US', { month: 'short', year: '2-digit' });
if (!months[month]) {
months[month] = range.downloads;
}
else {
months[month] += range.downloads;
}
}
let cumulativeDownloads = 0;
for (const month in months) {
cumulativeDownloads += months[month];
months[month] = cumulativeDownloads;
}
return months;
}
export async function createDownloadsPerMonthChart(metric, outputPath) {
const downloadsRange = metric.metrics?.downloadsRange || [];
const svgOutputPath = `${outputPath}/${metric.name}-new-downloads-by-month.svg`;
const groupedDownloads = groupByMonth(downloadsRange);
const canvas = new Canvas(1000, 800);
const chart = new Chart(canvas, {
type: 'line',
data: {
labels: Object.keys(groupedDownloads),
datasets: [{
label: metric.name,
data: Object.values(groupedDownloads),
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}]
},
options: {
scales: {
x: {
time: {
unit: 'month',
displayFormats: {
month: 'MMM DD'
}
},
title: {
display: true,
text: 'Date'
}
},
y: {
title: {
display: true,
text: 'Downloads per month'
},
}
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath;
}
export async function createCumulativeDownloadsChart(metric, outputPath) {
const downloadsRange = metric.metrics?.downloadsRange || [];
const svgOutputPath = `${outputPath}/${metric.name}-cumulative-downloads.svg`;
const groupedDownloads = groupByMonthCumulative(downloadsRange);
const canvas = new Canvas(1000, 800);
const chart = new Chart(canvas, {
type: 'line',
data: {
labels: Object.keys(groupedDownloads),
datasets: [{
label: metric.name,
data: Object.values(groupedDownloads),
backgroundColor: 'rgba(75, 192, 192, 0.2)',
borderColor: 'rgba(75, 192, 192, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}]
},
options: {
scales: {
x: {
time: {
unit: 'month',
displayFormats: {
month: 'MMM DD'
}
},
title: {
display: true,
text: 'Date'
}
},
y: {
title: {
display: true,
text: 'Downloads per month'
},
}
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath;
}
export async function createNpmChart(platformMetrics, outputPath) {
const svgOutputPathList = [];
for (const metric of platformMetrics) {
const svgOutputPath = await createDownloadsPerMonthChart(metric, outputPath);
svgOutputPathList.push(svgOutputPath);
const svgOutputPathCumulative = await createCumulativeDownloadsChart(metric, outputPath);
svgOutputPathList.push(svgOutputPathCumulative);
}
return svgOutputPathList;
}
export async function addNpmDetails(summary, platformMetrics) {
const outputPath = './charts/npm';
mkdirSync(outputPath, { recursive: true });
const svgOutputPathList = await createNpmChart(platformMetrics, outputPath);
for (const svgOutputPath of svgOutputPathList) {
summary += `![${svgOutputPath}](${svgOutputPath})\n`;
}
return summary;
}

238
dist/summaries/powershell.js vendored Normal file
View File

@@ -0,0 +1,238 @@
import { mkdirSync, writeFileSync } from "fs";
import { Chart, registerables } from 'chart.js';
import { Canvas } from 'skia-canvas';
// Register all Chart.js controllers
Chart.register(...registerables);
export function formatPowerShellSummary(summary, platformMetrics) {
let platformDownloadTotal = 0;
let totalVersions = 0;
summary += `| Module | Total Downloads | Latest Version | Version Downloads | Versions | Last Updated |\n`;
summary += `| --- | --- | --- | --- | --- | --- |\n`;
for (const metric of platformMetrics) {
const lastUpdated = metric.metrics?.lastUpdated ? new Date(metric.metrics.lastUpdated).toLocaleDateString() : 'N/A';
const latestVersion = metric.metrics?.latestVersion || 'N/A';
const latestVersionDownloads = metric.metrics?.latestVersionDownloads || 0;
const versionCount = metric.metrics?.versionCount || 0;
summary += `| ${metric.name} | ${metric.metrics?.downloadsTotal?.toLocaleString() || 0} | ${latestVersion} | ${latestVersionDownloads.toLocaleString()} | ${versionCount} | ${lastUpdated} |\n`;
platformDownloadTotal += metric.metrics?.downloadsTotal || 0;
totalVersions += versionCount;
}
summary += `| **Total** | **${platformDownloadTotal.toLocaleString()}** | | | **${totalVersions}** | |\n`;
return summary;
}
export async function addPowerShellDetails(summary, platformMetrics) {
summary += `#### PowerShell Module Details:\n\n`;
for (const metric of platformMetrics) {
summary += `**${metric.name}**:\n`;
summary += `- Total Downloads: ${metric.metrics?.downloadsTotal?.toLocaleString() || 0}\n`;
summary += `- Latest Version: ${metric.metrics?.latestVersion || 'N/A'}\n`;
summary += `- Latest Version Downloads: ${metric.metrics?.latestVersionDownloads?.toLocaleString() || 0}\n`;
summary += `- Version Count: ${metric.metrics?.versionCount || 0}\n`;
summary += `- Last Updated: ${metric.metrics?.lastUpdated ? new Date(metric.metrics.lastUpdated).toLocaleDateString() : 'N/A'}\n`;
summary += `- Package Size: ${metric.metrics?.packageSize ? `${Math.round(metric.metrics.packageSize / 1024)} KB` : 'N/A'}\n`;
summary += `\n`;
}
summary += `\n\n`;
const chartOutputPath = './charts/powershell';
mkdirSync(chartOutputPath, { recursive: true });
const svgOutputPathList = await createPowerShellCharts(platformMetrics, chartOutputPath);
for (const svgOutputPath of svgOutputPathList) {
summary += `![${svgOutputPath}](${svgOutputPath})\n`;
}
return summary;
}
export async function createPowerShellCharts(platformMetrics, outputPath) {
const svgOutputPathList = [];
// Only create charts if there's download data
const metricsWithData = platformMetrics.filter(metric => metric.metrics?.downloadsRange && metric.metrics.downloadsRange.length > 0);
if (metricsWithData.length > 0) {
const svgOutputPath = await createCombinedDownloadsChart(metricsWithData, outputPath);
svgOutputPathList.push(svgOutputPath);
const svgOutputPathCumulative = await createCombinedCumulativeDownloadsChart(metricsWithData, outputPath);
svgOutputPathList.push(svgOutputPathCumulative);
}
return svgOutputPathList;
}
// Color palette for different modules
const colors = [
'rgba(54, 162, 235, 0.8)',
'rgba(255, 99, 132, 0.8)',
'rgba(75, 192, 192, 0.8)',
'rgba(255, 205, 86, 0.8)',
'rgba(153, 102, 255, 0.8)',
'rgba(255, 159, 64, 0.8)',
'rgba(199, 199, 199, 0.8)',
'rgba(83, 102, 255, 0.8)',
'rgba(255, 99, 132, 0.8)',
'rgba(54, 162, 235, 0.8)'
];
const borderColors = [
'rgba(54, 162, 235, 1)',
'rgba(255, 99, 132, 1)',
'rgba(75, 192, 192, 1)',
'rgba(255, 205, 86, 1)',
'rgba(153, 102, 255, 1)',
'rgba(255, 159, 64, 1)',
'rgba(199, 199, 199, 1)',
'rgba(83, 102, 255, 1)',
'rgba(255, 99, 132, 1)',
'rgba(54, 162, 235, 1)'
];
export async function createCombinedDownloadsChart(metrics, outputPath) {
const svgOutputPath = `${outputPath}/powershell-combined-downloads.svg`;
// Get all unique dates across all modules for the x-axis
const allDates = new Set();
for (const metric of metrics) {
const downloadsRange = metric.metrics?.downloadsRange || [];
for (const download of downloadsRange) {
allDates.add(download.day);
}
}
// Sort all dates chronologically
const sortedAllDates = Array.from(allDates).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
// Create datasets for each module (one line per module)
const data = [];
for (const metric of metrics) {
const downloadsRange = metric.metrics?.downloadsRange || [];
for (const date of sortedAllDates) {
const downloads = downloadsRange.filter(d => d.day === date).reduce((sum, d) => sum + d.downloads, 0);
data.push(downloads);
}
}
const labels = sortedAllDates.map(date => new Date(date).toLocaleDateString('en-US', {
month: 'short',
year: '2-digit',
day: 'numeric'
}));
const canvas = new Canvas(1200, 800);
const chart = new Chart(canvas, {
type: 'line',
data: {
labels: labels,
datasets: [{
data,
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'PowerShell Modules - Downloads Over Time',
font: {
size: 16
}
},
legend: {
display: true
}
},
scales: {
x: {
title: {
display: true,
text: 'Release Date'
}
},
y: {
type: 'linear',
title: {
display: true,
text: 'Downloads'
},
beginAtZero: true,
ticks: {
stepSize: 1000
}
}
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath;
}
export async function createCombinedCumulativeDownloadsChart(metrics, outputPath) {
const svgOutputPath = `${outputPath}/powershell-cumulative-downloads.svg`;
// Get all unique dates across all modules for the x-axis
const allDates = new Set();
for (const metric of metrics) {
const downloadsRange = metric.metrics?.downloadsRange || [];
for (const download of downloadsRange) {
allDates.add(download.day);
}
}
// Sort all dates chronologically
const sortedAllDates = Array.from(allDates).sort((a, b) => new Date(a).getTime() - new Date(b).getTime());
const labels = sortedAllDates.map(date => new Date(date).toLocaleDateString('en-US', {
month: 'short',
year: '2-digit',
day: 'numeric'
}));
const data = [];
let runningTotal = 0;
for (const date of sortedAllDates) {
const downloads = metrics.reduce((sum, metric) => sum + (metric.metrics?.downloadsRange?.find(d => d.day === date)?.downloads || 0), 0);
runningTotal += downloads;
data.push(runningTotal);
}
const canvas = new Canvas(1200, 800);
const chart = new Chart(canvas, {
type: 'line',
data: {
labels: labels,
datasets: [{
label: 'Cumulative Downloads',
data,
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}]
},
options: {
responsive: true,
plugins: {
title: {
display: true,
text: 'PowerShell Modules - Cumulative Downloads',
font: {
size: 16
}
},
legend: {
display: true
}
},
scales: {
x: {
title: {
display: true,
text: 'Release Date'
}
},
y: {
title: {
display: true,
text: 'Cumulative Downloads'
},
beginAtZero: true,
ticks: {
stepSize: 5000
}
}
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
writeFileSync(svgOutputPath, svgBuffer);
chart.destroy();
return svgOutputPath;
}

388
dist/summaries/pypi.js vendored Normal file
View File

@@ -0,0 +1,388 @@
import { mkdirSync, writeFileSync } from 'node:fs';
import { Chart, registerables } from 'chart.js';
import { Canvas } from 'skia-canvas';
Chart.register(...registerables);
export function formatPypiSummary(summary, platformMetrics) {
summary += `| Package | Total Downloads | Monthly Downloads | Weekly Downloads | Daily Downloads | Version |\n`;
summary += `| --- | --- | --- | --- | --- | --- |\n`;
for (const metric of platformMetrics) {
summary += `| ${metric.name} | ${metric.metrics?.downloadsTotal?.toLocaleString() || 0} | ${metric.metrics?.downloadsMonthly?.toLocaleString() || 0} | ${metric.metrics?.downloadsWeekly?.toLocaleString() || 0} | ${metric.metrics?.downloadsDaily?.toLocaleString() || 0} | ${metric.metrics?.version || 'N/A'} |\n`;
}
summary += `| **Total** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsTotal || 0), 0).toLocaleString()}** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsMonthly || 0), 0).toLocaleString()}** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsWeekly || 0), 0).toLocaleString()}** | **${platformMetrics.reduce((sum, m) => sum + (m.metrics?.downloadsDaily || 0), 0).toLocaleString()}** | | |\n`;
return summary;
}
function toIsoMonth(dateStr) {
// input expected YYYY-MM-DD; fallback to Date parse if needed
const iso = dateStr?.slice(0, 7);
if (iso && /\d{4}-\d{2}/.test(iso))
return iso;
const d = new Date(dateStr);
const y = d.getFullYear();
const m = String(d.getMonth() + 1).padStart(2, '0');
return `${y}-${m}`;
}
function displayMonthLabel(isoMonth) {
const [y, m] = isoMonth.split('-');
const d = new Date(Number(y), Number(m) - 1, 1);
return d.toLocaleDateString('en-US', { month: 'short', year: '2-digit' });
}
function aggregateMonthlyTotals(points) {
const totals = {};
for (const p of points) {
const iso = toIsoMonth(p.date);
totals[iso] = (totals[iso] || 0) + p.downloads;
}
const labelsIso = Object.keys(totals).sort();
const labels = labelsIso.map(displayMonthLabel);
const data = labelsIso.map(l => totals[l]);
return { labels, data };
}
function aggregateMonthlyByCategory(points) {
const labelIsoSet = new Set();
const categoryMap = {};
for (const p of points) {
const iso = toIsoMonth(p.date);
labelIsoSet.add(iso);
if (!categoryMap[p.category])
categoryMap[p.category] = {};
categoryMap[p.category][iso] = (categoryMap[p.category][iso] || 0) + p.downloads;
}
const labelsIso = Array.from(labelIsoSet).sort();
const labels = labelsIso.map(displayMonthLabel);
return { labelsIso, labels, categoryMap };
}
async function createOverallDownloadsChart(metric, outputPath) {
// Prefer server-prepared chart JSON if present
const server = metric.metrics?.overallChart;
let labels;
let datasets;
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
labels = server.labels;
const colorFor = (label, idx) => {
const l = (label || '').toLowerCase();
if (l.includes('without'))
return { stroke: '#2563eb', fill: '#2563eb33' }; // blue
if (l.includes('with'))
return { stroke: '#64748b', fill: '#64748b33' }; // slate
const palette = ['#2563eb', '#16a34a', '#f59e0b', '#ef4444', '#7c3aed'];
const i = idx ?? 0;
return { stroke: palette[i % palette.length], fill: palette[i % palette.length] + '33' };
};
datasets = server.datasets.map((ds, i) => {
const c = colorFor(ds.label, i);
return {
...ds,
borderColor: c.stroke,
backgroundColor: c.fill,
borderWidth: 3,
fill: true,
tension: 0.1,
};
});
}
else {
const series = metric.metrics?.overallSeries || [];
const agg = aggregateMonthlyTotals(series.map(p => ({ date: p.date, downloads: p.downloads })));
labels = agg.labels;
datasets = [{
label: `${metric.name} downloads per month`,
data: agg.data,
backgroundColor: 'rgba(54, 162, 235, 0.2)',
borderColor: 'rgba(54, 162, 235, 1)',
borderWidth: 3,
fill: true,
tension: 0.1
}];
}
const canvas = new Canvas(1000, 800);
const chart = new Chart(canvas, {
type: 'line',
data: { labels, datasets },
options: {
plugins: {
legend: { display: true, position: 'bottom' },
title: { display: true, text: `${metric.name} overall downloads` }
},
scales: {
x: { title: { display: true, text: 'Month' } },
y: { title: { display: true, text: 'Downloads' } }
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
const svgPath = `${outputPath}/${metric.name}-pypi-overall.svg`;
writeFileSync(svgPath, svgBuffer);
chart.destroy();
return svgPath;
}
// Time-series: Python major over time (line)
async function createPythonMajorChart(metric, outputPath) {
// Prefer server chart JSON if present
const server = metric.metrics?.pythonMajorChart;
let labels;
let datasets;
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
const palette = ['#2563eb', '#16a34a', '#f59e0b', '#ef4444', '#7c3aed', '#0891b2', '#dc2626', '#0ea5e9'];
labels = server.labels;
datasets = server.datasets
.filter(ds => !/unknown/i.test(ds.label))
.map((ds, idx) => ({
...ds,
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}));
}
else {
const points = metric.metrics?.pythonMajorSeries || [];
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points);
labels = lbls;
const sortedCategories = Object.keys(categoryMap).filter(k => !/unknown/i.test(k)).sort((a, b) => Number(a) - Number(b));
const palette = ['#2563eb', '#16a34a', '#f59e0b', '#ef4444', '#7c3aed', '#0891b2', '#dc2626', '#0ea5e9'];
datasets = sortedCategories.map((category, idx) => ({
label: `Python ${category}`,
data: labelsIso.map(l => categoryMap[category][l] || 0),
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}));
}
const canvas = new Canvas(1000, 800);
const chart = new Chart(canvas, {
type: 'line',
data: { labels, datasets },
options: {
plugins: {
legend: { position: 'bottom' },
title: { display: true, text: `${metric.name} downloads by Python major version` }
},
scales: {
x: { title: { display: true, text: 'Month' } },
y: { title: { display: true, text: 'Downloads' } }
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
const svgPath = `${outputPath}/${metric.name}-pypi-python-major.svg`;
writeFileSync(svgPath, svgBuffer);
chart.destroy();
return svgPath;
}
// Time-series: Python minor over time (line)
async function createPythonMinorChart(metric, outputPath) {
// Prefer server chart JSON if present
const server = metric.metrics?.pythonMinorChart;
let labels;
let datasets;
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
const palette = ['#1d4ed8', '#059669', '#d97706', '#dc2626', '#6d28d9', '#0e7490', '#b91c1c', '#0284c7'];
labels = server.labels;
datasets = server.datasets
.filter(ds => !/unknown/i.test(ds.label))
.map((ds, idx) => ({
...ds,
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}));
}
else {
const points = metric.metrics?.pythonMinorSeries || [];
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points);
labels = lbls;
const sortedCategories = Object.keys(categoryMap).filter(k => !/unknown/i.test(k)).sort((a, b) => a.localeCompare(b, undefined, { numeric: true }));
const palette = ['#1d4ed8', '#059669', '#d97706', '#dc2626', '#6d28d9', '#0e7490', '#b91c1c', '#0284c7'];
datasets = sortedCategories.map((category, idx) => ({
label: `Python ${category}`,
data: labelsIso.map(l => categoryMap[category][l] || 0),
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}));
}
const canvas = new Canvas(1000, 800);
const chart = new Chart(canvas, {
type: 'line',
data: { labels, datasets },
options: {
plugins: {
legend: { position: 'bottom' },
title: { display: true, text: `${metric.name} downloads by Python minor version` }
},
scales: {
x: { title: { display: true, text: 'Month' } },
y: { title: { display: true, text: 'Downloads' } }
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
const svgPath = `${outputPath}/${metric.name}-pypi-python-minor.svg`;
writeFileSync(svgPath, svgBuffer);
chart.destroy();
return svgPath;
}
// Time-series: Installer over time (line) - prefer server JSON
async function createInstallerChart(metric, outputPath) {
const server = metric.metrics?.installerChart;
let labels;
let datasets;
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
const palette = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#a78bfa', '#22d3ee'];
labels = server.labels;
datasets = server.datasets.map((ds, idx) => ({
...ds,
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}));
}
else {
const points = metric.metrics?.installerSeries || [];
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points);
labels = lbls;
const categories = Object.keys(categoryMap);
const palette = ['#3b82f6', '#10b981', '#f59e0b', '#ef4444', '#a78bfa', '#22d3ee'];
datasets = categories.map((category, idx) => ({
label: category,
data: labelsIso.map(l => categoryMap[category][l] || 0),
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}));
}
const canvas = new Canvas(1000, 800);
const chart = new Chart(canvas, {
type: 'line',
data: { labels, datasets },
options: {
plugins: {
legend: { position: 'bottom' },
title: { display: true, text: `${metric.name} downloads by installer` }
},
scales: {
x: { title: { display: true, text: 'Month' } },
y: { title: { display: true, text: 'Downloads' } }
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
const svgPath = `${outputPath}/${metric.name}-pypi-installer.svg`;
writeFileSync(svgPath, svgBuffer);
chart.destroy();
return svgPath;
}
// Time-series: System over time (line) - prefer server JSON
async function createSystemChart(metric, outputPath) {
const server = metric.metrics?.systemChart;
let labels;
let datasets;
if (server && server.labels && server.labels.length && server.datasets && server.datasets.length) {
const palette = ['#0ea5e9', '#22c55e', '#f97316', '#e11d48', '#8b5cf6', '#06b6d4'];
labels = server.labels;
datasets = server.datasets.map((ds, idx) => ({
...ds,
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}));
}
else {
const points = metric.metrics?.systemSeries || [];
const { labelsIso, labels: lbls, categoryMap } = aggregateMonthlyByCategory(points);
labels = lbls;
const sortedCategories = Object.keys(categoryMap).sort();
const palette = ['#0ea5e9', '#22c55e', '#f97316', '#e11d48', '#8b5cf6', '#06b6d4'];
datasets = sortedCategories.map((category, idx) => ({
label: category,
data: labelsIso.map(l => categoryMap[category][l] || 0),
borderColor: palette[idx % palette.length],
backgroundColor: palette[idx % palette.length] + '33',
borderWidth: 2,
fill: false,
}));
}
const canvas = new Canvas(1000, 800);
const chart = new Chart(canvas, {
type: 'line',
data: { labels, datasets },
options: {
plugins: {
legend: { position: 'bottom' },
title: { display: true, text: `${metric.name} downloads by OS` }
},
scales: {
x: { title: { display: true, text: 'Month' } },
y: { title: { display: true, text: 'Downloads' } }
}
}
});
const svgBuffer = await canvas.toBuffer('svg', { matte: 'white' });
const svgPath = `${outputPath}/${metric.name}-pypi-system.svg`;
writeFileSync(svgPath, svgBuffer);
chart.destroy();
return svgPath;
}
// Removed static bar chart generators per request
async function createPypiCharts(metrics, basePath) {
const outputPaths = [];
for (const metric of metrics) {
const packagePath = `${basePath}`;
mkdirSync(packagePath, { recursive: true });
const overall = await createOverallDownloadsChart(metric, packagePath);
outputPaths.push(overall);
const pythonMajor = await createPythonMajorChart(metric, packagePath);
outputPaths.push(pythonMajor);
const pythonMinor = await createPythonMinorChart(metric, packagePath);
outputPaths.push(pythonMinor);
const installer = await createInstallerChart(metric, packagePath);
outputPaths.push(installer);
const system = await createSystemChart(metric, packagePath);
outputPaths.push(system);
// static bar charts removed
}
return outputPaths;
}
export function addPypiDetails(summary, metrics) {
summary += `#### Package Details:\n\n`;
for (const metric of metrics) {
summary += `**${metric.name}**:\n`;
summary += `- Version: ${metric.metrics?.version || 'N/A'}\n`;
if (metric.metrics?.latestReleaseDate)
summary += `- Released: ${metric.metrics.latestReleaseDate}\n`;
if (metric.metrics?.popularSystem)
summary += `- Popular system: ${metric.metrics.popularSystem}\n`;
if (metric.metrics?.popularInstaller)
summary += `- Popular installer: ${metric.metrics.popularInstaller}\n`;
summary += `- Releases: ${metric.metrics?.releases || 0}\n`;
if (metric.metrics?.systemBreakdown) {
summary += `- OS Usage Breakdown \n`;
for (const [key, value] of Object.entries(metric.metrics?.systemBreakdown)) {
summary += ` - ${key}: ${value}\n`;
}
}
if (metric.metrics?.pythonVersionBreakdown) {
summary += `- Python Version Breakdown \n`;
for (const [key, value] of Object.entries(metric.metrics?.pythonVersionBreakdown)) {
summary += ` - ${key}: ${value}\n`;
}
}
}
return summary;
}
export async function addPypiCharts(summary, platformMetrics) {
const outputPath = './charts/pypi';
mkdirSync(outputPath, { recursive: true });
summary += `\n\n`;
const svgPaths = await createPypiCharts(platformMetrics, outputPath);
for (const p of svgPaths) {
summary += `![${p}](${p})\n`;
}
return summary;
}

123
dist/utils.js vendored Normal file
View File

@@ -0,0 +1,123 @@
import * as core from '@actions/core';
import { CategoryScale, Chart, LinearScale, LineController, LineElement, PointElement, BarController, BarElement } from 'chart.js';
import { readFile, writeFile } from 'fs/promises';
import { addRepoDetails, formatGitHubSummary } from './summaries/github.js';
import { addNpmDetails, formatNpmSummary } from './summaries/npm.js';
import { formatPowerShellSummary, addPowerShellDetails } from './summaries/powershell.js';
import { addPypiDetails, addPypiCharts, formatPypiSummary } from './summaries/pypi.js';
Chart.register([
CategoryScale,
LineController,
LineElement,
LinearScale,
PointElement,
BarController,
BarElement
]);
/**
* Parse comma-separated inputs into arrays
* @param input - The input string to parse
* @returns An array of trimmed, non-empty items
*/
function parseCommaSeparatedInputs(input) {
return input ? input.split(',').map(item => item.trim()).filter(item => item) : [];
}
export function getInputs() {
// Get all inputs from action.yml
const npmPackages = core.getInput('npm-packages');
const githubRepositories = core.getInput('github-repositories');
const pypiPackages = core.getInput('pypi-packages');
const powershellModules = core.getInput('powershell-modules');
const jsonOutputPath = core.getInput('json-output-path');
const updateReadme = core.getBooleanInput('update-readme');
const commitMessage = core.getInput('commit-message');
const readmePath = core.getInput('readme-path');
return {
npmPackages: parseCommaSeparatedInputs(npmPackages),
githubRepositories: parseCommaSeparatedInputs(githubRepositories),
pypiPackages: parseCommaSeparatedInputs(pypiPackages),
powershellModules: parseCommaSeparatedInputs(powershellModules),
jsonOutputPath,
updateReadme,
commitMessage,
readmePath,
};
}
const MetricsPlaceHolderRegex = /<!-- METRICS_START -->[\s\S]*<!-- METRICS_END -->/;
function formatSummary(summary) {
return `<!-- METRICS_START -->\n${summary}\n<!-- METRICS_END -->`;
}
const PlatformMap = {
"NPM": "JavaScript/TypeScript",
"PyPI": "Python",
"PowerShell Gallery": undefined,
"GitHub": undefined,
};
export async function createSummary(metrics) {
const platforms = metrics.map(metric => metric.platform).filter((value, index, self) => self.indexOf(value) === index);
console.log(platforms);
console.log(metrics);
let summary = `# Usage Statistics
Last updated: ${new Date().toLocaleString()}
Below are stats from artifacts tracked across ${platforms.slice(0, -1).join(', ')} and ${platforms.slice(-1)}.
`;
for (const platform of platforms) {
const platformMetrics = metrics.filter(metric => metric.platform === platform);
const platformLanguage = PlatformMap[platform];
summary += `### ${platform}${platformLanguage ? ` (${platformLanguage})` : ''}: \n\n`;
switch (platform) {
case "NPM":
summary = formatNpmSummary(summary, platformMetrics);
break;
case "GitHub":
summary = formatGitHubSummary(summary, platformMetrics);
break;
case "PyPI":
summary = formatPypiSummary(summary, platformMetrics);
break;
case "PowerShell":
summary = formatPowerShellSummary(summary, platformMetrics);
break;
default:
let platformDownloadTotal = 0;
summary += `| Package | Downloads |\n`;
summary += `| --- | --- |\n`;
for (const metric of platformMetrics) {
summary += `| ${metric.name} | ${metric.metrics?.downloadCount?.toLocaleString() || 0} |\n`;
platformDownloadTotal += metric.metrics?.downloadCount || 0;
}
summary += `| **Total** | **${platformDownloadTotal.toLocaleString()}** |\n`;
break;
}
summary += `\n`;
// Add detailed information for each platform
switch (platform) {
case "GitHub":
summary = await addRepoDetails(summary, platformMetrics);
break;
case "PyPI":
summary = addPypiDetails(summary, platformMetrics);
summary = await addPypiCharts(summary, platformMetrics);
break;
case "NPM":
summary = await addNpmDetails(summary, platformMetrics);
break;
case "PowerShell":
summary = await addPowerShellDetails(summary, platformMetrics);
break;
default:
break;
}
summary += '\n';
}
return summary;
}
export async function updateRepositoryReadme(metrics, readmePath) {
const currentReadme = await readFile(readmePath, 'utf8');
const summary = await createSummary(metrics);
const updatedReadme = currentReadme.replace(MetricsPlaceHolderRegex, formatSummary(summary));
await writeFile(readmePath, updatedReadme);
}