LCOV - code coverage report
Current view: top level - lib - gh-act.js (source / functions) Hit Total Coverage
Test: lcov.info Lines: 365 433 84.3 %
Date: 2025-02-13 17:12:20 Functions: 14 19 73.7 %
Branches: 25 49 51.0 %

           Branch data     Line data    Source code
       1            [ + ]:         57 : /**
       2                 :         57 :  * @module  github-activity
       3                 :         57 :  * @desc    A simple command line interface (CLI) utility to fetch the recent activities of a GitHub user and display it in the terminal.
       4                 :         57 :  * @version 1.0.0
       5                 :         57 :  * @author  Essam A. El-Sherif
       6                 :         57 :  */
       7                 :         57 : 
       8                 :         57 : /* Import node.js core modules */
       9                 :         57 : import fs                from 'node:fs';
      10                 :         57 : import https             from 'node:https';
      11                 :         57 : import os                from 'node:os';
      12                 :         57 : import path              from 'node:path';
      13                 :         57 : import process           from 'node:process';
      14                 :         57 : import { fileURLToPath } from 'node:url';
      15                 :         57 : 
      16                 :         57 : /* Import from local modules */
      17                 :         57 : import { parseCmdLine, getError } from './gh-act.cmd.js';
      18                 :         57 : import { cmdOptions }             from './gh-act.cmd.js';
      19                 :         57 : import { githubUser }             from './gh-act.cmd.js';
      20                 :         57 : import { GitHubEvent }            from './gh-event.js';
      21                 :         57 : 
      22                 :         57 : /* Emulate commonJS __filename and __dirname constants */
      23                 :         57 : const __filename = fileURLToPath(import.meta.url);
      24                 :         57 : const __dirname = path.dirname(__filename);
      25                 :         57 : 
      26                 :         57 : /** @const {boolean} testMode - Run the program in test mode. */
      27                 :         57 : const testMode = 'GH_ACT_TEST' in process.env;
      28                 :         57 : 
      29                 :         57 : /** @var {string} authToken - Authorization token required in test mode. */
      30                 :         57 : let authToken = '';
      31                 :         57 : 
      32                 :         57 : /**
      33                 :         57 :  *     function main
      34                 :         57 :  * function processCmd(test)
      35                 :         57 :  * function requestAPI(endpoint, etag)
      36                 :         57 :  * function logRequestHeaders(res)
      37                 :         57 :  * function logResponse(res)
      38                 :         57 :  * function output(githubUserEvents)
      39                 :         57 :  *
      40                 :         57 :  * @func Main
      41                 :         57 :  * @desc The application entry point function
      42                 :         57 :  */
      43            [ + ]:         57 : (() => {
      44                 :         57 : 
      45                 :         57 :         const authTokenFile = path.join(__dirname, '../', cmdOptions.authTokenFile);
      46                 :         57 : 
      47                 :         57 :         // read the authorization token if exists
      48                 :         57 :         try{
      49                 :         57 :                 authToken = fs.readFileSync(authTokenFile, 'utf8');
      50                 :         57 :         }
      51            [ - ]:         57 :         catch(e){}
      52                 :         57 : 
      53                 :         57 :         // run the CLI in test mode
      54                 :         57 :         if('GH_ACT_TEST' in process.env){
      55            [ - ]:         57 :                 if(!authToken){
      56                 :          0 :                         process.stderr.write(`${getError(8)}\n`);
      57                 :          0 :                         process.exit(1);
      58                 :          0 :                 }
      59                 :         57 :                 else
      60                 :         57 :                 if(process.env['GH_ACT_TEST'] === 'parse'){
      61                 :         57 :                         parseCmdLine();
      62                 :         57 :                         process.stdout.write(JSON.stringify(cmdOptions));
      63                 :         57 :                 }
      64                 :         57 :                 else
      65                 :         57 :                 if(process.env['GH_ACT_TEST'] === 'fetchUser'){
      66                 :         57 :                         parseCmdLine();
      67                 :         57 : 
      68                 :         57 :                         try{
      69                 :         57 :                                 fs.mkdirSync(cmdOptions.cacheDir);
      70                 :         57 :                         }
      71                 :         57 :                         catch(e){}
      72                 :         57 : 
      73                 :         57 :                         processCmd('fetchUser');
      74                 :         57 :                 }
      75                 :         57 :                 else
      76                 :         57 :                 if(process.env['GH_ACT_TEST'] === 'fetchAct'){
      77                 :         57 :                         parseCmdLine();
      78                 :         57 : 
      79                 :         57 :                         try{
      80                 :         57 :                                 fs.mkdirSync(cmdOptions.cacheDir);
      81                 :         57 :                         }
      82                 :         57 :                         catch(e){}
      83                 :         57 : 
      84                 :         57 :                         processCmd('fetchAct');
      85                 :         57 :                 }
      86                 :         57 :         }
      87                 :         57 :         // run the CLI in normal mode
      88                 :         57 :         else{
      89                 :         57 :                 parseCmdLine();
      90                 :         57 : 
      91                 :         57 :                 try{
      92                 :         57 :                         fs.mkdirSync(cmdOptions.cacheDir);
      93                 :         57 :                 }
      94                 :         57 :                 catch(e){}
      95                 :         57 : 
      96            [ + ]:         57 :                 processCmd().then(githubUserEvents => {
      97                 :          4 :                         output(githubUserEvents);
      98                 :         57 :                 });
      99                 :         57 :         }
     100                 :         57 : })('Main Function');
     101                 :         57 : 
     102                 :         57 : /**
     103                 :         57 :  * function main
     104                 :         57 :  *     function processCmd(test)
     105                 :         57 :  * function requestAPI(endpoint, etag)
     106                 :         57 :  * function logRequestHeaders(res)
     107                 :         57 :  * function logResponse(res)
     108                 :         57 :  * function output(githubUserEvents)
     109                 :         57 :  *
     110                 :         57 :  * @func  processCmd
     111                 :         57 :  * @async
     112                 :         57 :  * @param {string} test - Test descriptor.
     113                 :         57 :  * @desc  Command processer function.
     114                 :         57 :  */
     115            [ + ]:         22 : async function processCmd(test){
     116                 :         22 : 
     117                 :         22 :         const githubUserObjFile = path.join(cmdOptions.cacheDir, githubUser + '.user.json');
     118                 :         22 :         const githubUserEventsFile =  path.join(cmdOptions.cacheDir, githubUser + '.events.json');
     119                 :         22 : 
     120                 :         22 :         let githubUserObj = null, githubUserEvents = null;
     121                 :         22 : 
     122                 :         22 :         // Retreive User JSON
     123                 :         22 :         try{
     124                 :         22 :                 let res = fs.readFileSync(githubUserObjFile, 'utf8');
     125                 :         22 :                 let cacheUserObj = JSON.parse(res);
     126                 :         22 : 
     127                 :         22 :                 try{
     128                 :         22 :                         githubUserObj = await requestAPI(`https://api.github.com/orgs/${githubUser}`, cacheUserObj.etag);
     129                 :         22 : 
     130            [ - ]:         22 :                         if(githubUserObj.url){
     131                 :          0 :                                 fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));
     132                 :          0 :                         }
     133                 :         22 :                         else{
     134                 :         22 :                                 githubUserObj = cacheUserObj;
     135                 :         22 :                         }
     136                 :         22 : 
     137                 :         22 :                         (test === 'fetchUser') && process.stdout.write(githubUserObj.url);
     138                 :         22 :                         (test === 'fetchUser') && process.exit(0);
     139                 :         22 : 
     140            [ - ]:         22 :                         if(cmdOptions.user){
     141                 :          0 :                                 ('etag' in githubUserObj) && delete githubUserObj.etag;
     142                 :          0 :                                 console.log(githubUserObj);
     143                 :          0 :                                 process.exit(0);
     144                 :          0 :                         }
     145                 :         22 :                 }
     146                 :         22 :                 catch(e){
     147                 :         22 :                         try{
     148                 :         22 :                                 githubUserObj = await requestAPI(`https://api.github.com/users/${githubUser}`, cacheUserObj.etag);
     149                 :         22 : 
     150            [ - ]:         22 :                                 if(githubUserObj.url){
     151                 :          0 :                                         fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));
     152                 :          0 :                                 }
     153                 :         22 :                                 else{
     154                 :         22 :                                         githubUserObj = cacheUserObj;
     155                 :         22 :                                 }
     156                 :         22 : 
     157                 :         22 :                                 (test === 'fetchUser') && process.stdout.write(githubUserObj.url);
     158                 :         22 :                                 (test === 'fetchUser') && process.exit(0);
     159                 :         22 : 
     160            [ - ]:         22 :                                 if(cmdOptions.user){
     161                 :          0 :                                         ('etag' in githubUserObj) && delete githubUserObj.etag;
     162                 :          0 :                                         console.log(githubUserObj);
     163                 :          0 :                                         process.exit(0);
     164                 :          0 :                                 }
     165                 :         22 :                         }
     166            [ - ]:         22 :                         catch(e){
     167                 :          0 :                                 fs.unlinkSync(githubUserObjFile);
     168                 :          0 : 
     169                 :          0 :                                 process.stderr.write(`${getError(3).replace('_', githubUser)}\n`);
     170                 :          0 :                                 process.exit(1);
     171                 :          0 :                         }
     172                 :         22 :                 }
     173                 :         22 :         }
     174                 :         22 :         catch(e){
     175                 :         22 :                 try{
     176                 :         22 :                         githubUserObj = await requestAPI(`https://api.github.com/orgs/${githubUser}`);
     177                 :         22 :                         fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));
     178                 :         22 : 
     179                 :         22 :                         (test === 'fetchUser') && process.stdout.write(githubUserObj.url);
     180                 :         22 :                         (test === 'fetchUser') && process.exit(0);
     181                 :         22 : 
     182                 :         22 :                         if(cmdOptions.user){
     183                 :         22 :                                 ('etag' in githubUserObj) && delete githubUserObj.etag;
     184                 :         22 :                                 console.log(githubUserObj);
     185                 :         22 :                                 process.exit(0);
     186                 :         22 :                         }
     187                 :         22 :                 }
     188                 :         22 :                 catch(e){
     189                 :         22 :                         try{
     190                 :         22 :                                 githubUserObj = await requestAPI(`https://api.github.com/users/${githubUser}`);
     191                 :         22 :                                 fs.writeFileSync(githubUserObjFile, JSON.stringify(githubUserObj, null, 0));
     192                 :         22 : 
     193                 :         22 :                                 (test === 'fetchUser') && process.stdout.write(githubUserObj.url);
     194                 :         22 :                                 (test === 'fetchUser') && process.exit(0);
     195                 :         22 : 
     196                 :         22 :                                 if(cmdOptions.user){
     197                 :         22 :                                         ('etag' in githubUserObj) && delete githubUserObj.etag;
     198                 :         22 :                                         console.log(githubUserObj);
     199                 :         22 :                                         process.exit(0);
     200                 :         22 :                                 }
     201                 :         22 :                         }
     202                 :         22 :                         catch(e){
     203                 :         22 :                                 process.stderr.write(`${getError(3).replace('_', githubUser)}\n`);
     204                 :         22 :                                 process.exit(1);
     205                 :         22 :                         }
     206                 :         22 :                 }
     207                 :         22 :         }
     208                 :         22 : 
     209                 :         22 :         // Retreive User Events JSON
     210                 :         22 :         try{
     211                 :         22 :                 let res = fs.readFileSync(githubUserEventsFile, 'utf8');
     212                 :         22 :                 let cacheUserEvents = JSON.parse(res);
     213                 :         22 : 
     214                 :         22 :                 try{
     215            [ - ]:         22 :                         let etag = cacheUserEvents.length ? cacheUserEvents[cacheUserEvents.length - 1].etag : undefined;
     216                 :         22 :                         githubUserEvents = await requestAPI(`${githubUserObj.events_url.replace(/\{.*\}/g, '')}`, etag);
     217                 :         22 : 
     218            [ - ]:         22 :                         if(Array.isArray(githubUserEvents)){
     219                 :          0 :                                 fs.writeFileSync(githubUserEventsFile, JSON.stringify(githubUserEvents, null, 0));
     220                 :          0 :                         }
     221                 :         22 :                         else{
     222                 :         22 :                                 githubUserEvents = cacheUserEvents;
     223                 :         22 :                         }
     224                 :         22 : 
     225                 :         22 :                         (test === 'fetchAct') && process.stdout.write(Object.prototype.toString.call(githubUserEvents));
     226                 :         22 :                         (test === 'fetchAct') && process.exit(0);
     227                 :         22 :                 }
     228            [ - ]:         22 :                 catch(e){
     229                 :          0 :                         fs.unlinkSync(githubUserEventsFile);
     230                 :          0 : 
     231                 :          0 :                         process.stderr.write(`${getError(7).replace('_', githubUser)}\n`);
     232                 :          0 :                         process.exit(1);
     233                 :          0 :                 }
     234                 :         22 :         }
     235                 :         22 :         catch(e){
     236                 :         22 :                 try{
     237                 :         22 :                         githubUserEvents = await requestAPI(`${githubUserObj.events_url.replace(/\{.*\}/g, '')}`);
     238                 :         22 :                         fs.writeFileSync(githubUserEventsFile, JSON.stringify(githubUserEvents, null, 0));
     239                 :         22 : 
     240                 :         22 :                         (test === 'fetchAct') && process.stdout.write(Object.prototype.toString.call(githubUserEvents));
     241                 :         22 :                         (test === 'fetchAct') && process.exit(0);
     242                 :         22 :                 }
     243            [ - ]:         22 :                 catch(e){
     244                 :          0 :                         process.stderr.write(`${getError(7).replace('_', githubUser)}\n`);
     245                 :          0 :                         process.exit(1);
     246                 :          0 :                 }
     247                 :         22 :         }
     248                 :         22 : 
     249                 :         22 :         return Promise.resolve(githubUserEvents);
     250                 :         22 : }
     251                 :         57 : 
     252                 :         57 : /**
     253                 :         57 :  * function main
     254                 :         57 :  * function processCmd(test)
     255                 :         57 :  *     function requestAPI(endpoint, etag)
     256                 :         57 :  * function logRequestHeaders(res)
     257                 :         57 :  * function logResponse(res)
     258                 :         57 :  * function output(githubUserEvents)
     259                 :         57 :  *
     260                 :         57 :  * @func   requestAPI
     261                 :         57 :  * @param  {string} endpoint - GitHub endpoint url.
     262                 :         57 :  * @param  {string} etag - Etag header used to make conditional request.
     263                 :         57 :  * @return {Promise} Promise object that will fulfill upon processing the request.
     264                 :         57 :  * @desc   Call to GitHub Rest API.
     265                 :         57 :  */
     266            [ + ]:         44 : function requestAPI(endpoint, etag){
     267                 :         44 : 
     268            [ + ]:         44 :         return new Promise((resolve, reject) => {
     269                 :         44 : 
     270                 :         44 :                 const reqOpt = {};
     271                 :         44 :                 const req = https.request(endpoint, reqOpt);
     272                 :         44 : 
     273                 :         44 :                 req.setHeader('User-Agent', cmdOptions.userAgent);
     274                 :         44 :                 req.setHeader('X-GitHub-Api-Version', cmdOptions.apiVersion);
     275                 :         44 :                 authToken && req.setHeader('Authorization', `token ${authToken}`);
     276                 :         44 : 
     277            [ + ]:         44 :                 etag && req.setHeader('If-None-Match', `"${etag}"`);
     278                 :         44 : 
     279            [ + ]:         44 :                 req.on('response', (res) => { resolve(res) });
     280                 :         44 :                 req.end();
     281                 :         44 : 
     282                 :         44 :         })
     283       [ + ][ + ]:         44 :         .then((res) => new Promise((resolve, reject) => {
     284                 :         44 : 
     285                 :         44 :                 cmdOptions.debug && logRequestHeaders(res);
     286                 :         44 : 
     287                 :         44 :                 let etag = res.headers.etag;
     288            [ + ]:         44 :                 etag = etag && etag.replace(/[^a-fA-F0-9]/g, '');
     289                 :         44 : 
     290                 :         44 :                 let resBody = '';
     291            [ + ]:         44 :                 res.on('data', (chunk) => { resBody += chunk });
     292            [ + ]:         44 :                 res.on('end', () => {
     293                 :         44 : 
     294                 :         44 :                         res.resBody = resBody;
     295                 :         44 :                         cmdOptions.debug && logResponse(res);
     296                 :         44 : 
     297       [ + ][ + ]:         44 :                         resBody = resBody ? JSON.parse(resBody) : {};
     298                 :         44 : 
     299            [ + ]:         44 :                         if(etag){
     300                 :         16 :                                 if(Array.isArray(resBody))
     301            [ + ]:         16 :                                         resBody.push({'etag': etag});
     302                 :          8 :                                 else
     303                 :          8 :                                 if(typeof resBody === 'object')
     304                 :          8 :                                         resBody.etag = etag;
     305                 :         16 :                         }
     306                 :         44 : 
     307                 :         44 :                         if(res.statusCode >= 200 && res.statusCode < 400)
     308  [ + ][ + ][ + ]:         44 :                                 resolve(resBody);
     309                 :          6 :                         else
     310                 :          6 :                                 reject(resBody);
     311                 :         44 :                 });
     312                 :         44 :         }));
     313                 :         44 : }
     314                 :         57 : 
     315                 :         57 : /**
     316                 :         57 :  * function main
     317                 :         57 :  * function processCmd(test)
     318                 :         57 :  * function requestAPI(endpoint, etag)
     319                 :         57 :  *     function logRequestHeaders(res)
     320                 :         57 :  * function logResponse(res)
     321                 :         57 :  * function output(githubUserEvents)
     322                 :         57 :  *
     323                 :         57 :  * @func   logRequestHeaders
     324                 :         57 :  * @param  {object} res - The response object.
     325                 :         57 :  * @desc   Log request headers to stderr.
     326                 :         57 :  */
     327            [ + ]:          2 : function logRequestHeaders(res){
     328                 :          2 : 
     329                 :          2 :         if(res.req._header){
     330                 :          2 :                 const headers = res.req._header.split('\r\n');
     331                 :          2 :                 if(testMode){
     332                 :          2 :                         process.stderr.write(headers[0]);
     333            [ - ]:          2 :                 }
     334                 :          0 :                 else{
     335                 :          0 :                         headers.forEach((e) => e && console.error(`> ${e}`));
     336                 :          0 :                         console.error('> ');
     337                 :          0 :                 }
     338            [ - ]:          2 :         }
     339                 :          0 :         else{
     340                 :          0 :                 if(testMode){
     341                 :          0 :                         process.stderr.write(`${res.req.method} ${res.req.path} HTTP/${res.httpVersion}`);
     342                 :          0 :                 }
     343                 :          0 :                 else{
     344                 :          0 :                         console.error(`> ${res.req.method} ${res.req.path} HTTP/${res.httpVersion}`);
     345                 :          0 : 
     346                 :          0 :                         const headers = res.req.getHeaders();
     347                 :          0 :                         for(let headerName of res.req.getRawHeaderNames())
     348                 :          0 :                                 console.error(`> ${headerName}: ${headers[headerName.toLowerCase()]}`);
     349                 :          0 : 
     350                 :          0 :                         console.error('> ');
     351                 :          0 :                 }
     352                 :          0 :         }
     353                 :          2 : 
     354            [ - ]:          2 :         !testMode && console.error();
     355                 :          2 : }
     356                 :         57 : 
     357                 :         57 : /**
     358                 :         57 :  * function main
     359                 :         57 :  * function processCmd(test)
     360                 :         57 :  * function requestAPI(endpoint, etag)
     361                 :         57 :  * function logRequestHeaders(res)
     362                 :         57 :  *     function logResponse(res)
     363                 :         57 :  * function output(githubUserEvents)
     364                 :         57 :  *
     365                 :         57 :  * @func   logResponse
     366                 :         57 :  * @param  {object} res - The response object.
     367                 :         57 :  * @desc   Log response headers and body to stderr.
     368                 :         57 :  */
     369            [ + ]:          2 : function logResponse(res){
     370                 :          2 : 
     371            [ - ]:          2 :         !testMode && console.error(`< HTTP/${res.httpVersion} ${res.statusCode} ${res.statusMessage}`);
     372                 :          2 : 
     373                 :          2 :         for(let i = 0; i < res.rawHeaders.length; i += 2)
     374       [ + ][ - ]:          2 :                 !testMode && console.error(`< ${res.rawHeaders[i]}: ${res.rawHeaders[i+1]}`)
     375                 :          2 : 
     376            [ - ]:          2 :         !testMode && console.error('< ');
     377            [ - ]:          2 :         !testMode && console.error(res.resBody);
     378            [ - ]:          2 :         !testMode && console.error();
     379                 :          2 : }
     380                 :         57 : 
     381                 :         57 : /**
     382                 :         57 :  * function main
     383                 :         57 :  * function processCmd(test)
     384                 :         57 :  * function requestAPI(endpoint, etag)
     385                 :         57 :  * function logRequestHeaders(res)
     386                 :         57 :  * function logResponse(res)
     387                 :         57 :  *     function output(githubUserEvents)
     388                 :         57 :  *
     389                 :         57 :  * @func  output
     390                 :         57 :  * @param {Array} githubUserEvents - Output GitHub user events.
     391                 :         57 :  * @desc  Display output in terminal.
     392                 :         57 :  */
     393            [ + ]:          4 : function output(githubUserEvents){
     394                 :          4 : 
     395                 :          4 :         if('etag' in githubUserEvents[githubUserEvents.length - 1])
     396                 :          4 :                 githubUserEvents.pop();
     397                 :          4 : 
     398                 :          4 :         // cmdOptions.type !== ''
     399            [ - ]:          4 :         if(cmdOptions.type){
     400                 :          0 :                 githubUserEvents = githubUserEvents.filter(event => event.type === cmdOptions.type);
     401                 :          0 :         }
     402                 :          4 : 
     403            [ - ]:          4 :         if(cmdOptions.output === 'j'){
     404                 :          0 :                 console.log(githubUserEvents);
     405                 :          0 :         }
     406                 :          4 :         else
     407            [ - ]:          4 :         if(cmdOptions.output === 'b'){
     408                 :          0 :                 let maxTypeLength = 5, maxRepoLength = 10, maxDateLength = 20;
     409                 :          0 : 
     410                 :          0 :                 githubUserEvents.forEach(event => {
     411                 :          0 :                         maxTypeLength = Math.max(event.type.length, maxTypeLength);
     412                 :          0 :                         maxRepoLength = Math.max(event.repo.name.length, maxRepoLength);
     413                 :          0 :                 });
     414                 :          0 : 
     415                 :          0 :                 console.log(`${'Event'.padEnd(maxTypeLength)}    ${'Created at'.padEnd(maxDateLength)}    Repository`);
     416                 :          0 :                 console.log(`${'='.repeat(maxTypeLength)}    ${'='.repeat(maxDateLength)}    ${'='.repeat(maxRepoLength)}`);
     417                 :          0 : 
     418                 :          0 :                 githubUserEvents.forEach(event => { console.log(`${event.type.padEnd(maxTypeLength)}    ${event.created_at}    ${event.repo.name}`); });
     419                 :          0 :         }
     420                 :          4 :         else
     421            [ - ]:          4 :         if(cmdOptions.output === 'c'){
     422                 :          0 :                 githubUserEvents.forEach(event => { console.log(`${event.type},${event.created_at},${event.repo.name}`); });
     423                 :          0 :         }
     424                 :          4 :         else
     425                 :          4 :         if(cmdOptions.output === 'v'){
     426            [ - ]:          4 :                 if(githubUserEvents.length === 0){
     427                 :          0 :                         console.log('no user activities found');
     428                 :          0 :                 }
     429                 :          4 :                 else{
     430            [ + ]:          4 :                         githubUserEvents.forEach(event => { console.log(new GitHubEvent(event).phrase) });
     431                 :          4 :                 }
     432                 :          4 :         }
     433                 :          4 : }

Generated by: LCOV version 1.14