mirror of
https://github.com/LukeHagar/usage-statistics.git
synced 2025-12-06 04:21:55 +00:00
chore: streamline release workflows by adding version bump and built files commit step; remove redundant asset upload steps
This commit is contained in:
66
.github/workflows/auto-release.yml
vendored
66
.github/workflows/auto-release.yml
vendored
@@ -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 }}
|
||||
|
||||
66
.github/workflows/release.yml
vendored
66
.github/workflows/release.yml
vendored
@@ -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
1
.gitignore
vendored
@@ -7,7 +7,6 @@ yarn-error.log*
|
||||
# Build outputs
|
||||
build/
|
||||
*.tsbuildinfo
|
||||
dist/
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
94
dist/action.js
vendored
Normal file
94
dist/action.js
vendored
Normal 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
268
dist/collectors/github.js
vendored
Normal 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
112
dist/collectors/npm.js
vendored
Normal 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
176
dist/collectors/powershell.js
vendored
Normal 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
151
dist/collectors/pypi.js
vendored
Normal 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
4
dist/collectors/types.js
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
/**
|
||||
* Core types for the simplified usage statistics system
|
||||
*/
|
||||
export {};
|
||||
280
dist/summaries/github.js
vendored
Normal file
280
dist/summaries/github.js
vendored
Normal 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 += `\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
171
dist/summaries/npm.js
vendored
Normal 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 += `\n`;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
238
dist/summaries/powershell.js
vendored
Normal file
238
dist/summaries/powershell.js
vendored
Normal 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 += `\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
388
dist/summaries/pypi.js
vendored
Normal 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 += `\n`;
|
||||
}
|
||||
return summary;
|
||||
}
|
||||
123
dist/utils.js
vendored
Normal file
123
dist/utils.js
vendored
Normal 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);
|
||||
}
|
||||
Reference in New Issue
Block a user