github-action-readme-generator
Version:
The docs generator for GitHub Actions. Auto-syncs action.yml to README.md with 8 sections: inputs, outputs, usage, badges, branding & more. Works as CLI or GitHub Action.
494 lines • 19.8 kB
JavaScript
/**
* This class handles input configuration and manipulation.
* It imports various modules and packages for file operations, configuration parsing, and logging.
* The class has methods for initializing the input configuration, setting default values, and converting the configuration to a string.
* It also has properties for storing the configuration values, sections, readme path, action instance, and readme editor instance.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import * as core from '@actions/core';
import { context as githubContext } from '@actions/github';
import nconf from 'nconf';
import YAML from 'yaml';
const Context = githubContext.constructor;
import Action from './Action.js';
import { ConfigKeys, configFileName, README_SECTIONS } from './constants.js';
import { repositoryFinder } from './helpers.js';
import LogTask from './logtask/index.js';
import ReadmeEditor from './readme-editor.js';
const { Provider } = nconf;
/**
* Change working directory to output of workingDirectory()
*/
// process.chdir(workingDirectory());
export const metaActionPath = '../../action.yml';
/**
* Represents the command line argument options for the application.
*/
const argvOptions = {};
/**
* Save option configuration.
* @property {string} alias - Alias for the save option.
* @property {string} describe - Description for the save option.
* @property {boolean} parseValues - Specifies whether to parse values for the save option.
* @property {string} type - Type of the save option.
*/
argvOptions[ConfigKeys.Save] = {
alias: 'save',
describe: `Save this config to ${configFileName}`,
parseValues: true,
type: 'boolean',
};
/**
* Paths action option configuration.
* @property {string | string[]} alias - Alias(es) for the pathsAction option.
* @property {string} type - Type of the pathsAction option.
* @property {string} describe - Description for the pathsAction option.
*/
argvOptions[ConfigKeys.pathsAction] = {
alias: ['pathsAction', 'action'],
type: 'string',
describe: 'Path to the action.yml',
};
/**
* Paths readme option configuration.
* @property {string | string[]} alias - Alias(es) for the pathsReadme option.
* @property {string} type - Type of the pathsReadme option.
* @property {string} describe - Description for the pathsReadme option.
*/
argvOptions[ConfigKeys.pathsReadme] = {
alias: ['pathsReadme', 'readme'],
type: 'string',
describe: 'Path to the README file',
};
/**
* Branding SVG path option configuration.
* @property {string} alias - Alias for the svg option.
* @property {string} type - Type of the svg option.
* @property {string} describe - Description for the svg option.
*/
argvOptions[ConfigKeys.BrandingSvgPath] = {
alias: 'svg',
type: 'string',
describe: 'Save and load the branding svg image in the README from this path',
};
/**
* Branding as title prefix option configuration.
* @property {string} alias - Alias for the branding_prefix option.
* @property {string} type - Type of the branding_prefix option.
* @property {boolean} parseValues - Specifies whether to parse values for the branding_prefix option.
* @property {string} describe - Description for the branding_prefix option.
*/
argvOptions[ConfigKeys.BrandingAsTitlePrefix] = {
alias: 'branding_prefix',
type: 'boolean',
parseValues: true,
describe: 'Use the branding svg as a prefix for the README title',
};
/**
* Owner option configuration.
* @property {string} alias - Alias for the owner option.
* @property {string} type - Type of the owner option.
* @property {string} describe - Description for the owner option.
*/
argvOptions[ConfigKeys.Owner] = {
alias: 'owner',
type: 'string',
describe: 'The GitHub Action repository owner. i.e: `bitflight-devops`',
};
/**
* Repo option configuration.
* @property {string} alias - Alias for the repo option.
* @property {string} type - Type of the repo option.
* @property {string} describe - Description for the repo option.
*/
argvOptions[ConfigKeys.Repo] = {
alias: 'repo',
type: 'string',
describe: 'The GitHub Action repository name. i.e: `github-action-readme-generator`',
};
/**
* Prettier option configuration.
* @property {string | string[]} alias - Alias(es) for the prettier option.
* @property {string} type - Type of the prettier option.
* @property {boolean} parseValues - Specifies whether to parse values for the prettier option.
* @property {string} describe - Description for the prettier option.
*/
argvOptions[ConfigKeys.Prettier] = {
alias: ['pretty', 'prettier'],
type: 'boolean',
parseValues: true,
describe: 'Format the markdown using prettier formatter',
};
/**
* Versioning enabled option configuration.
* @property {string | string[]} alias - Alias(es) for the versioning_enabled option.
* @property {string} describe - Description for the versioning_enabled option.
* @property {boolean} parseValues - Specifies whether to parse values for the versioning_enabled option.
* @property {string} type - Type of the versioning_enabled option.
*/
argvOptions[ConfigKeys.VersioningEnabled] = {
alias: ['versioning', 'versioning_enabled'],
describe: 'Enable the update of the usage version to match the latest version in the package.json file',
parseValues: true,
type: 'boolean',
};
/**
* Versioning override option configuration.
* @property {string | string[]} alias - Alias(es) for the versioning_override option.
* @property {string} describe - Description for the versioning_override option.
* @property {boolean} parseValues - Specifies whether to parse values for the versioning_override option.
*/
argvOptions[ConfigKeys.VersioningOverride] = {
alias: ['setversion', 'versioning_override', 'version_override'],
describe: 'Set a specific version to display in the README.md',
parseValues: true,
};
/**
* Versioning prefix option configuration.
* @property {string | string[]} alias - Alias(es) for the version_prefix option.
* @property {string} describe - Description for the version_prefix option.
* @property {boolean} parseValues - Specifies whether to parse values for the version_prefix option.
*/
argvOptions[ConfigKeys.VersioningPrefix] = {
alias: ['vp', 'version_prefix'],
describe: "Prefix the version with this value (if it isn't already prefixed)",
parseValues: true,
};
/**
* Versioning branch option configuration.
* @property {string | string[]} alias - Alias(es) for the versioning_default_branch option.
* @property {string} describe - Description for the versioning_default_branch option.
* @property {boolean} parseValues - Specifies whether to parse values for the versioning_default_branch option.
*/
argvOptions[ConfigKeys.VersioningBranch] = {
alias: ['branch', 'versioning_default_branch'],
describe: 'If versioning is disabled show this branch instead',
parseValues: true,
};
/**
* Versioning source option configuration.
* @property {string | string[]} alias - Alias(es) for the version_source option.
* @property {string} describe - Description for the version_source option.
* @property {boolean} parseValues - Specifies whether to parse values for the version_source option.
* @property {string} type - Type of the version_source option.
*/
argvOptions[ConfigKeys.VersioningSource] = {
alias: ['version-source', 'version_source', 'versioning_source'],
describe: 'How to detect the action version (git-tag, git-branch, git-sha, package-json, explicit)',
parseValues: true,
type: 'string',
};
/**
* Include GitHub version badge option configuration.
* @property {string | string[]} alias - Alias(es) for the include_github_version_badge option.
* @property {string} describe - Description for the include_github_version_badge option.
* @property {boolean} parseValues - Specifies whether to parse values for the include_github_version_badge option.
* @property {string} type - Type of the include_github_version_badge option.
*/
argvOptions[ConfigKeys.IncludeGithubVersionBadge] = {
alias: ['version-badge', 'versioning_badge', 'include_github_version_badge'],
describe: 'Display the current version as a badge',
parseValues: true,
type: 'boolean',
};
/**
* Title prefix option configuration.
* @property {string | string[]} alias - Alias(es) for the title_prefix option.
* @property {string} describe - Description for the title_prefix option.
* @property {boolean} parseValues - Specifies whether to parse values for the title_prefix option.
*/
argvOptions[ConfigKeys.TitlePrefix] = {
alias: ['prefix', 'title_prefix'],
describe: 'Add a prefix to the README title',
parseValues: true,
};
/**
* Debug Nconf option configuration.
* @property {string} describe - Description for the debugNconf option.
* @property {boolean} parseValues - Specifies whether to parse values for the debugNconf option.
* @property {string} type - Type of the debugNconf option.
*/
argvOptions[ConfigKeys.DebugNconf] = {
alias: ['debug_nconf'],
describe: 'Print out the resolved nconf object with all values',
parseValues: true,
type: 'boolean',
};
/**
* Debug Config option configuration.
* @property {string} describe - Description for the debugConfig option.
* @property {boolean} parseValues - Specifies whether to parse values for the debugConfig option.
* @property {string} type - Type of the debugConfig option.
*/
argvOptions[ConfigKeys.DebugConfig] = {
alias: ['debug_config'],
describe: 'Print out the resolved nconf object with all values',
parseValues: true,
type: 'boolean',
};
/**
* Configuration inputs from the github action don't
* all match the input names when running on cli.
* This maps the action inputs to the cli.
*/
const ConfigKeysInputsMap = {
save: ConfigKeys.Save,
action: ConfigKeys.pathsAction,
readme: ConfigKeys.pathsReadme,
branding_svg_path: ConfigKeys.BrandingSvgPath,
branding_as_title_prefix: ConfigKeys.BrandingAsTitlePrefix,
versioning_enabled: ConfigKeys.VersioningEnabled,
version_prefix: ConfigKeys.VersioningPrefix,
versioning_default_branch: ConfigKeys.VersioningBranch,
version_override: ConfigKeys.VersioningOverride,
version_source: ConfigKeys.VersioningSource,
include_github_version_badge: ConfigKeys.IncludeGithubVersionBadge,
owner: ConfigKeys.Owner,
repo: ConfigKeys.Repo,
title_prefix: ConfigKeys.TitlePrefix,
pretty: ConfigKeys.Prettier,
};
export function transformGitHubInputsToArgv(log, _config, obj) {
/** The obj.key is always in lowercase, but it checks for it without case sensitivity */
if (/^(INPUT|input)_[A-Z_a-z]\w*$/.test(obj.key)) {
log.debug(`Parsing input: ${obj.key} with ith value: ${obj.value}`);
const keyParsed = obj.key.replace(/^(INPUT|input)_/, '').toLocaleLowerCase();
const key = ConfigKeysInputsMap[keyParsed] || keyParsed;
// Skip empty values for owner/repo to allow fallback detection from .git/config or GITHUB_REPOSITORY
if ((key === 'owner' || key === 'repo') && (!obj.value || obj.value === '')) {
log.debug(`Ignoring empty ${key} input to allow auto-detection`);
return undefined;
}
log.debug(`New input is ${key} with the value ${obj.value}`);
return { key, value: obj.value };
}
log.debug(`Ignoring input: ${obj.key} with ith value: ${obj.value}`);
return undefined;
}
/**
* Sets config value from action file default
*
* @param {Action} actionInstance - The action instance
* @param {string} inputName - The input name
* @returns {string | boolean | undefined} The default value
*/
export function setConfigValueFromActionFileDefault(log, actionInstance, inputName) {
if (ConfigKeysInputsMap[inputName] === undefined) {
log.error(`${inputName} from ${actionInstance.path} does not match a known input. Known inputs are: ${Object.keys(ConfigKeysInputsMap)}`);
return;
}
const configName = ConfigKeysInputsMap[inputName];
const defaultValue = actionInstance.inputDefault(inputName);
log.debug(`Default Value for action.yml: ${inputName} CLI: ${configName} = ${defaultValue}`);
return defaultValue;
}
/**
* Collects all default values from action file
*
* @returns {IOptions} The default values object
*/
export function collectAllDefaultValuesFromAction(log, providedMetaActionPath, providedDefaults = {}) {
log.debug('Collecting default values from action.yml');
// This loads the defaults from THIS action's own action.yml file (github-action-readme-generator's action.yml)
// NOT the user's action.yml file (which is loaded separately via the 'action' input parameter)
// Therefore, we use import.meta.dirname to find this package's action.yml regardless of where it's installed
const thisActionPath = path.join(import.meta.dirname, providedMetaActionPath ?? metaActionPath);
try {
const defaultValues = {};
const thisAction = new Action(thisActionPath);
const defaults = { ...thisAction.inputs, ...providedDefaults };
// Collect all of the default values from the action.yml file
if (defaults) {
for (const key of Object.keys(defaults)) {
const mappedKey = ConfigKeysInputsMap[key] ?? key;
defaultValues[mappedKey] = setConfigValueFromActionFileDefault(log, thisAction, key);
}
}
log.debug(JSON.stringify(defaultValues, null, 2));
return defaultValues;
}
catch (error) {
// When running as a CLI tool (e.g., via npx or yarn dlx), the tool's own action.yml
// may not be present in the node_modules. This is expected behavior, as the tool
// should still work to generate documentation for other actions.
log.debug(`Could not load defaults from this tool's action.yml at ${thisActionPath}: ${error}`);
log.debug('Continuing without default values from action.yml');
return {};
}
}
/**
* Loads the configuration
*
* @returns {ProviderInstance} The configuration instance
*/
export function loadConfig(log, providedConfig, configFilePath) {
log.debug('Loading config from env and argv');
const config = providedConfig ?? new Provider();
if (process.env.GITHUB_ACTION === 'true') {
log.info('Running in GitHub action');
}
if (configFilePath) {
if (fs.existsSync(configFilePath)) {
log.info(`Config file found: ${configFilePath}`);
config.file(configFilePath);
}
else {
log.debug(`Config file not found: ${configFilePath}`);
}
}
config
.env({
lowerCase: true,
parseValues: true,
transform: (obj) => {
return transformGitHubInputsToArgv(log, config, obj);
},
})
.argv(argvOptions);
return config;
}
/**
* Loads the default configuration
*
* @param {ProviderInstance} config - The config instance
* @returns {ProviderInstance} The updated config instance
*/
export function loadDefaultConfig(log, config, providedContext) {
log.debug('Loading default config');
const defaultValues = collectAllDefaultValuesFromAction(log);
const context = providedContext ?? new Context();
// Get owner/repo from config (which includes CLI args), falling back to env vars for GitHub Actions
const ownerFromConfig = config.get('owner');
const repoFromConfig = config.get('repo');
const ownerInput = ownerFromConfig ?? process.env.INPUT_OWNER ?? '';
const repoInput = repoFromConfig ?? process.env.INPUT_REPO ?? '';
// Get the action path to derive the target repo directory for .git/config lookup
const actionPath = config.get(ConfigKeys.pathsAction);
const actionDir = actionPath ? path.dirname(path.resolve(actionPath)) : undefined;
log.debug(`Action directory for repository detection: ${actionDir ?? 'not specified'}`);
const repositoryDetail = repositoryFinder(`${ownerInput}/${repoInput}`, context, actionDir);
log.debug(`repositoryDetail: ${repositoryDetail}`);
// Apply the default values from the action.yml file
return config.defaults({
...defaultValues,
owner: repositoryDetail?.owner,
repo: repositoryDetail?.repo,
sections: [...README_SECTIONS],
});
}
/**
* Represents the required inputs for the action.
*/
const RequiredInputs = [
ConfigKeys.pathsAction,
ConfigKeys.pathsReadme,
ConfigKeys.Owner,
ConfigKeys.Repo,
];
/**
* Loads the required configuration
*
* @param {ProviderInstance} config - The config instance
* @returns {ProviderInstance} The updated config instance
*/
export function loadRequiredConfig(log, config, requiredInputs = RequiredInputs) {
log.debug('Loading required config');
return config.required([...requiredInputs]);
}
/**
*
*/
export function loadAction(log, actionPath) {
log.debug(`Loading action from: ${actionPath}`);
if (actionPath) {
return new Action(path.resolve(actionPath));
}
throw new Error(`Action path not found: ${actionPath}`);
}
/**
* Main Inputs class that handles configuration
*/
export default class Inputs {
/**
* The configuration instance
*/
config;
/**
* The readme sections
*/
sections;
/**
* The readme file path
*/
readmePath;
/**
* The config file path
*/
configPath;
/**
* The action instance
*/
action;
/**
* The readme editor instance
*/
readmeEditor;
/**
* The repository owner
*/
owner;
/**
* The repository name
*/
repo;
/** The logger for this instance */
log;
/**
* Initializes a new instance of the Inputs class.
*/
constructor(providedInputContext = {}, log = new LogTask('inputs')) {
this.log = log ?? new LogTask('inputs');
this.log.debug('Initializing Inputs');
const inputContext = providedInputContext ?? {};
this.configPath = inputContext.configPath ?? path.resolve(configFileName);
this.config = inputContext.config ?? new Provider();
loadConfig(log, this.config, this.configPath);
loadDefaultConfig(log, this.config);
loadRequiredConfig(log, this.config);
this.action = inputContext.action ?? loadAction(log, this.config.get(ConfigKeys.pathsAction));
this.config.set('sections', inputContext.sections ?? this.config.get('sections'));
this.sections = this.config.get('sections');
this.readmePath =
inputContext.readmePath ?? path.resolve(this.config.get(ConfigKeys.pathsReadme));
this.readmeEditor = inputContext.readmeEditor ?? new ReadmeEditor(this.readmePath);
/**
* Output the readme path that is being parsed
*/
if (process.env.GITHUB_ACTIONS) {
core.setOutput('readme', this.readmePath);
}
/**
* owner is required, and if it doesn't exist it is handled by nconf which throws an error
*/
this.owner = inputContext.owner ?? this.config.get('owner');
/**
* repo is required, and if it doesn't exist it is handled by nconf which throws an error
*/
this.repo = inputContext.repo ?? this.config.get('repo');
}
stringify() {
if (this?.config) {
try {
return YAML.stringify(this.config.get());
}
catch (error) {
this.log.error(`${error}`);
// continue
}
}
// this is just for debug, no need to stop the process if it fails
return '';
}
}
//# sourceMappingURL=inputs.js.map