@stoe/action-reporting-cli
Version:
CLI to report on GitHub Actions
296 lines (265 loc) • 9.78 kB
JavaScript
/**
* @fileoverview GitHub Actions Reporting CLI - Generates reports on GitHub Actions usage across enterprises,
* organizations, users, and individual repositories. Supports CSV, JSON, and Markdown output formats.
*
* This CLI tool helps organizations audit their GitHub Actions usage by collecting data about workflows,
* secrets, variables, permissions, and action dependencies. It supports GitHub Enterprise Cloud/Server
* and provides caching for improved performance on large datasets.
*
* @author Stefan Stölzle
* @license MIT
*/
import chalk from 'chalk'
import meow from 'meow'
// Report class
import Report from './src/report/report.js'
// Utilities
import cacheInstance from './src/util/cache.js'
import log from './src/util/log.js'
const {blue, bold, dim, yellow} = chalk
/**
* Creates the help text for the CLI application.
* @returns {string} The formatted help text with usage, options, and examples
*/
function createHelpText() {
return `
${bold('Usage')}
${blue(`action-reporting-cli`)} ${yellow(`[options]`)}
${bold('Required options')} ${dim(`[one of]`)}
${yellow(`--enterprise`)}, ${yellow(`-e`)} GitHub Enterprise (Cloud|Server) account slug ${dim(
'(e.g. enterprise)',
)}.
${yellow(`--owner`)}, ${yellow(`-o`)} GitHub organization/user login ${dim('(e.g. owner)')}.
${dim(
`If ${yellow(`--owner`)} is a user, results for the authenticated user (${yellow(
`--token`,
)}) will be returned.`,
)}
${yellow(`--repository`)}, ${yellow(`-r`)} GitHub repository name with owner ${dim('(e.g. owner/repo)')}.
${bold('Additional options')}
${yellow(`--token`)}, ${yellow(`-t`)} GitHub Personal Access Token (PAT) ${dim('(default GITHUB_TOKEN)')}.
${yellow(`--hostname`)} GitHub Enterprise Server ${bold('hostname')} ${dim('(default api.github.com)')}.
${dim(`For example: ${yellow('github.example.com')}`)}
${bold('Report options')}
${yellow(`--all`)} Report all below or individually:
${yellow(`--listeners`)} Report ${bold('on')} listeners used.
${yellow(`--permissions`)} Report ${bold('permissions')} values for GITHUB_TOKEN.
${yellow(`--runs-on`)} Report ${bold('runs-on')} values.
${yellow(`--secrets`)} Report ${bold('secrets')} used.
${yellow(`--vars`)} Report ${bold('vars')} used.
${yellow(`--uses`)} Report ${bold('uses')} values.
${yellow(`--exclude`)} Exclude GitHub Actions created by GitHub. ${dim(`(can be used with ${yellow('--all')})`)}
${dim(`From https://github.com/actions and https://github.com/github organizations.
Only applies to ${yellow(`--uses`)}.`)}
${yellow(`--unique`)} List unique GitHub Actions.
${dim(
`Possible values are ${yellow('true')}, ${yellow('false')} and ${yellow('both')}.
Will create an additional ${bold('*-unique.{csv,json,md}')} report file when not ${yellow('false')}.
Resolves to ${yellow('both')} for ${yellow(`--all`)}.`,
)}
${bold('Output options')}
${yellow(`--csv`)} Path to save CSV output ${dim('(e.g. /path/to/reports/report.csv)')}.
${yellow(`--json`)} Path to save JSON output ${dim('(e.g. /path/to/reports/report.json)')}.
${yellow(`--md`)} Path to save markdown output ${dim('(e.g. /path/to/reports/report.md)')}.
${bold('Skip options')} ${dim(`[only applies to --enterprise and --owner options]`)}
${yellow(`--archived`)} Skip archived repositories ${dim('(default false)')}.
${yellow(`--forked`)} Skip forked repositories ${dim('(default false)')}.
${bold('Helper options')}
${yellow(`--debug`)}, ${yellow(`-d`)} Enable debug mode.
${yellow(`--skipCache`)} Disable caching.
${yellow(`--help`)}, ${yellow(`-h`)} Print action-reporting help.
${yellow(`--version`)}, ${yellow(`-v`)} Print action-reporting version.`
}
/**
* Creates the CLI flags configuration object.
* @returns {object} The CLI flags configuration for meow
*/
const CLI_FLAGS = {
// Required options
enterprise: {
type: 'string',
shortFlag: 'e',
},
owner: {
type: 'string',
shortFlag: 'o',
},
repository: {
type: 'string',
shortFlag: 'r',
},
// Additional options
token: {
type: 'string',
default: process.env.GITHUB_TOKEN || '',
shortFlag: 't',
},
hostname: {
type: 'string',
},
// Report options
all: {
type: 'boolean',
default: false,
},
listeners: {
type: 'boolean',
default: false,
},
permissions: {
type: 'boolean',
default: false,
},
runsOn: {
type: 'boolean',
default: false,
},
secrets: {
type: 'boolean',
default: false,
},
vars: {
type: 'boolean',
default: false,
},
uses: {
type: 'boolean',
default: false,
},
exclude: {
type: 'boolean',
default: false,
},
unique: {
type: 'string',
default: 'false',
},
// Output options
csv: {
type: 'string',
},
md: {
type: 'string',
},
json: {
type: 'string',
},
// Skip options
archived: {
type: 'boolean',
default: false,
},
forked: {
type: 'boolean',
default: false,
},
// Helper options
debug: {
type: 'boolean',
default: false,
shortFlag: 'd',
},
skipCache: {
type: 'boolean',
default: false,
},
help: {
type: 'boolean',
shortFlag: 'h',
},
version: {
type: 'boolean',
shortFlag: 'v',
},
}
const cli = meow(createHelpText(), {
booleanDefault: undefined,
description: false,
hardRejection: false,
allowUnknownFlags: false,
importMeta: import.meta,
inferType: false,
input: [],
flags: CLI_FLAGS,
})
/**
* Displays a structured status message showing what the CLI is scanning for and what options are enabled.
* @param {object} flags - The CLI flags object containing all user options
* @param {string} targetName - The name of the target (enterprise, owner, or repository)
* @param {string} [hostName] - Optional hostname for GitHub Enterprise Server
*/
const displayAnalysisSummary = (flags, targetName, hostName) => {
// Check if any report options are enabled
const {all, listeners, permissions, runsOn, secrets, vars, uses, exclude, unique, archived, forked} = flags
// Create readable list of enabled report types
const enabledReportTypes = []
if (all || listeners) enabledReportTypes.push('listeners')
if (all || permissions) enabledReportTypes.push('permissions')
if (all || runsOn) enabledReportTypes.push('runs-on')
if (all || secrets) enabledReportTypes.push('secrets')
if (all || vars) enabledReportTypes.push('vars')
if (all || uses) enabledReportTypes.push('uses')
// Create readable list of options
const options = []
if (all || uses) {
if (exclude) options.push('excluding actions created by GitHub')
const uniqueValue = all ? 'both' : unique
if (uniqueValue !== 'false') options.push(`unique report=${uniqueValue}`)
}
// Show repository filter information
if (archived) options.push('skip archived repos')
if (forked) options.push('skip forked repos')
// Create readable list of output formats
const outputs = []
if (flags.csv) outputs.push('csv')
if (flags.json) outputs.push('json')
if (flags.md) outputs.push('markdown')
// Build the structured message
console.log(
`Analyzing GitHub Actions in ${blue(targetName)}${hostName ? ` on ${blue(hostName)}` : ''}:
${yellow('→')} scanning:\t${enabledReportTypes.map(type => yellow(type)).join(', ')}
${yellow('→')} options:\t${options.length > 0 ? options.map(opt => yellow(opt)).join(', ') : 'none'}
${yellow('→')} outputs:\t${outputs.length > 0 ? outputs.map(output => yellow(output)).join(', ') : 'none'}
${dim('This can take a while...')}
`,
)
}
/**
* Main execution function that orchestrates the CLI application.
* Handles input validation, option processing, and delegates to appropriate processing functions.
* @async
* @returns {Promise<void>}
* @throws {Error} When validation fails or processing encounters errors
*/
async function main() {
console.log(`${bold('@stoe/action-reporting-cli')} ${dim(`v${cli.pkg.version}`)}\n`)
const {token, hostname, enterprise, owner, repository, debug, archived, forked, help, version} = cli.flags
const entity = enterprise || owner || repository
const logger = log(entity, token, debug)
const cache = cacheInstance(null, logger)
try {
// Handle help and version flags early exit
if (help) cli.showHelp(0)
if (version) cli.showVersion(0)
const report = new Report(cli.flags, logger, cache)
// Display analysis summary
displayAnalysisSummary(cli.flags, enterprise || owner || repository, hostname)
let results
if (enterprise) {
results = await report.processEnterprise(enterprise, token, hostname, debug, archived, forked)
} else if (owner) {
results = await report.processOwner(owner, token, hostname, debug, archived, forked)
} else if (repository) {
results = await report.processRepository(repository, token, hostname, debug, archived, forked)
}
const reportData = await report.createReport(results)
reportData.length && (await report.saveReports(reportData))
} catch (error) {
logger.fail(error.message)
// Log error stack trace in debug mode
debug && logger.error(error.stack)
}
}
// Execute the main function
main()