Skip to content

Commit

Permalink
Contributors and Creation Modification time of examples
Browse files Browse the repository at this point in the history
  • Loading branch information
juanmaguitar committed Dec 13, 2024
1 parent 461ff94 commit a514369
Show file tree
Hide file tree
Showing 9 changed files with 559 additions and 48 deletions.
30 changes: 30 additions & 0 deletions .github/workflows/github-contributors.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Generate Contributors JSON

on:
workflow_dispatch:

jobs:
github-contributors:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Generate Contributors JSON
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: node .github/workflows/scripts/github-contributors.js

- name: Commit and Push Changes
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
git add _data/contributors.json
git commit -m "Update contributors.json" || echo "No changes to commit"
git push
101 changes: 101 additions & 0 deletions .github/workflows/scripts/github-contributors.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
const fs = require( 'fs' );
const https = require( 'https' );

// Read examples.json
const examples = JSON.parse( fs.readFileSync( '_data/examples.json', 'utf8' ) );

// Read existing contributors.json if it exists
let existingContributors = [];
try {
existingContributors = JSON.parse(
fs.readFileSync( '_data/contributors.json', 'utf8' )
);
} catch ( e ) {
// File doesn't exist yet, start with empty array
existingContributors = [];
}

// Extract unique contributor emails, excluding ones we already have
const existingEmails = new Set( existingContributors.map( ( c ) => c.mail ) );
const uniqueEmails = [
...new Set( examples.flatMap( ( example ) => example.contributors ) ),
].filter( ( email ) => ! existingEmails.has( email ) );

// Function to get GitHub user info
async function getGithubInfo( email ) {
return new Promise( ( resolve ) => {
const options = {
hostname: 'api.github.com',
path: `/search/users?q=${ encodeURIComponent( email ) }+in:email`,
headers: {
'User-Agent': 'Node.js',
Authorization: `Bearer ${ process.env.GITHUB_TOKEN }`,
},
};

https
.get( options, ( res ) => {
let data = '';
res.on( 'data', ( chunk ) => ( data += chunk ) );
res.on( 'end', () => {
try {
const result = JSON.parse( data );
if ( result.items && result.items.length > 0 ) {
const user = result.items[ 0 ];
resolve( {
mail: email,
github: user.login,
name: user.name || user.login,
} );
} else {
resolve( {
mail: email,
github: null,
name: null,
} );
}
} catch ( e ) {
resolve( {
mail: email,
github: null,
name: null,
} );
}
} );
} )
.on( 'error', () => {
resolve( {
mail: email,
github: null,
name: null,
} );
} );
} );
}

// Process all new emails
async function generateContributors() {
const newContributors = [];
for ( const email of uniqueEmails ) {
const info = await getGithubInfo( email );
newContributors.push( info );
// Respect GitHub API rate limiting
await new Promise( ( resolve ) => setTimeout( resolve, 1000 ) );
}

// Combine existing and new contributors
const allContributors = [ ...existingContributors, ...newContributors ];

// Sort by email to maintain consistent ordering
allContributors.sort( ( a, b ) => a.mail.localeCompare( b.mail ) );

return allContributors;
}

// Generate and save the file
generateContributors().then( ( contributors ) => {
fs.writeFileSync(
'_data/contributors.json',
JSON.stringify( contributors, null, 2 )
);
} );
29 changes: 21 additions & 8 deletions _bin/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,22 @@
import { generateTables } from './services/tableMarkdown';
import { PLUGINS_PATH, ROOT_README_PATH } from './constants';
import { ValidationError } from './types/errors';
import { validatePath } from './utils/errors';
import path from 'path';
import fs from 'fs';
import { withErrorHandling } from './utils/compose';
import { contributorsService } from './services/ContributorsService';
import { datesService } from './services/DatesService';

const command = process.argv[ 2 ];
const target = process.argv[ 3 ];

const mainBase = (): void => {
const mainBase = async (): Promise< void > => {
if ( command === 'generate' ) {
// Lazy load table generation functionality
const { generateTables } = await import( './services/tableMarkdown' );
const { validatePath } = await import( './utils/errors' );
const { PLUGINS_PATH, ROOT_README_PATH } = await import(
'./constants'
);
const path = await import( 'path' );
const fs = await import( 'fs' );

if ( ! target ) {
validatePath( ROOT_README_PATH );
generateTables( ROOT_README_PATH );
Expand Down Expand Up @@ -40,13 +46,20 @@ const mainBase = (): void => {
validatePath( pluginPath );
generateTables( pluginPath );
}
} else if ( command === 'contributors' ) {
await contributorsService.updateContributors();
} else if ( command === 'dates' ) {
await datesService.updateDates();
} else {
throw new ValidationError(
'Unknown command. Use "generate", "generate all", or "generate <plugin-name>"'
'Unknown command. Use "generate", "generate all", "generate <plugin-name>", "contributors", or "dates"'
);
}
};

const main = withErrorHandling( mainBase );

main();
main().catch( ( error: Error ) => {
console.error( 'Error:', error );
process.exit( 1 );
} );
66 changes: 66 additions & 0 deletions _bin/src/services/ContributorsService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { execSync } from 'child_process';
import * as fs from 'fs';
import * as path from 'path';
import { Example } from '../types/example';
import { withErrorHandling } from '../utils/compose';
import { readJsonFile, writeFile } from '../utils/fileOperations';
import { PLUGINS_PATH, EXAMPLES_DATA_PATH } from '../constants';

export class ContributorsService {
private examples: Example[];

constructor() {
this.examples = [];
}

private async loadExamples(): Promise< void > {
this.examples = await readJsonFile( EXAMPLES_DATA_PATH );
}

private getContributors( pluginPath: string ): string[] {
const gitCommand = `git log --format='%ae' -- ${ pluginPath } | sort -u`;
const output = execSync( gitCommand, { encoding: 'utf8' } );

return output
.split( '\n' )
.filter( Boolean )
.map( ( email ) => email.replace( /'/g, '' ) );
}

private async processExample( example: Example ): Promise< void > {
const pluginPath = path.join( PLUGINS_PATH, example.slug );

if ( fs.existsSync( pluginPath ) ) {
try {
example.contributors = this.getContributors( pluginPath );
} catch ( error ) {
console.error( `Error processing ${ example.slug }:`, error );
}
}
}

private async saveExamples(): Promise< void > {
await writeFile(
EXAMPLES_DATA_PATH,
JSON.stringify( this.examples, null, '\t' ) + '\n'
);
}

public async updateContributors(): Promise< void > {
await this.loadExamples();

for ( const example of this.examples ) {
await this.processExample( example );
}

await this.saveExamples();
}
}

// Create error-handled version of the service methods
export const contributorsService = {
updateContributors: withErrorHandling( async () => {
const service = new ContributorsService();
await service.updateContributors();
} ),
};
80 changes: 80 additions & 0 deletions _bin/src/services/DatesService.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import { execSync } from 'child_process';
import * as path from 'path';
import { Example } from '../types/example';
import { withErrorHandling } from '../utils/compose';
import { readJsonFile, writeFile } from '../utils/fileOperations';
import { PLUGINS_PATH, EXAMPLES_DATA_PATH } from '../constants';

export class DatesService {
private examples: Example[];

constructor() {
this.examples = [];
}

private async loadExamples(): Promise< void > {
this.examples = await readJsonFile( EXAMPLES_DATA_PATH );
}

private getGitDates( pluginPath: string ): {
created: string;
lastModified: string;
} {
// Get first commit date
const createdCommand = `git log --reverse --format=%aI -- ${ pluginPath } | head -n 1`;
const created = execSync( createdCommand, {
encoding: 'utf8',
} ).trim();

// Get last commit date
const lastModifiedCommand = `git log -1 --format=%aI -- ${ pluginPath }`;
const lastModified = execSync( lastModifiedCommand, {
encoding: 'utf8',
} ).trim();

return {
created,
lastModified,
};
}

private async processExample( example: Example ): Promise< void > {
const pluginPath = path.join( PLUGINS_PATH, example.slug );

try {
const dates = this.getGitDates( pluginPath );
example.created = dates.created;
example.lastModified = dates.lastModified;
} catch ( error ) {
console.error(
`Error processing dates for ${ example.slug }:`,
error
);
}
}

private async saveExamples(): Promise< void > {
await writeFile(
EXAMPLES_DATA_PATH,
JSON.stringify( this.examples, null, '\t' ) + '\n'
);
}

public async updateDates(): Promise< void > {
await this.loadExamples();

for ( const example of this.examples ) {
await this.processExample( example );
}

await this.saveExamples();
}
}

// Create error-handled version of the service methods
export const datesService = {
updateDates: withErrorHandling( async () => {
const service = new DatesService();
await service.updateDates();
} ),
};
6 changes: 5 additions & 1 deletion _bin/src/types/example.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,11 @@ export interface Example {
slug: string;
description: string;
tags: string[];
[ key: string ]: any;
contributors?: string[];
folder?: string;
id?: string;
created?: string;
lastModified?: string;
}

export interface Tag {
Expand Down
17 changes: 6 additions & 11 deletions _bin/tsconfig.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,14 @@
{
"compilerOptions": {
"target": "es2018",
"module": "commonjs",
"moduleResolution": "node",
"sourceMap": true,
"target": "ES2020",
"module": "CommonJS",
"outDir": "./dist",
"rootDir": ".",
"rootDir": "./",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"allowJs": true,
"declaration": true,
"lib": [ "ES2021" ]
"forceConsistentCasingInFileNames": true
},
"exclude": [ "node_modules" ]
"include": [ "./**/*.ts" ],
"exclude": [ "node_modules", "dist" ]
}
Loading

0 comments on commit a514369

Please sign in to comment.