github-action-readme-generator
Version:
This is a CLI tool and GitHub Action that reads in the details from a GitHub Action's `action.yml` file and updates the `README.md` file with the `name`, `description`, `usage`, `inputs`, `outputs`, and examples of the action. Configuration can be provid
320 lines • 11.4 kB
JavaScript
import { execSync } from 'node:child_process';
import { accessSync, readFileSync } from 'node:fs';
import * as path from 'node:path';
import { fileURLToPath } from 'node:url';
import LogTask from './logtask/index.js';
import { unicodeWordMatch } from './unicode-word-match.js';
import { notEmpty } from './util.js';
export const __filename = fileURLToPath(import.meta.url);
export const __dirname = path.dirname(__filename);
/**
* Returns the input value if it is not empty, otherwise returns undefined.
* @param value - The input value to check.
* @returns The input value if it is not empty, otherwise undefined.
*/
export function undefinedOnEmpty(value) {
if (!value || value === '') {
return undefined;
}
return value;
}
/**
* Returns the basename of the given path.
* @param pathStr - The path to extract the basename from.
* @returns The basename of the path.
*/
export function basename(pathStr) {
if (!pathStr)
return undefined;
const log = new LogTask('basename');
const result = path.basename(pathStr);
log.debug(`Basename passed ${pathStr} and returns ${result}`);
return result;
}
/**
* Removes the "refs/heads/" or "refs/tags/" prefix from the given path.
*
* @param pathStr - The path to remove the prefix from
* @returns The path without the prefix, or null if path is empty
*/
export function stripRefs(pathStr) {
if (!pathStr)
return null;
const log = new LogTask('stripRefs');
const result = pathStr.replace('refs/heads/', '').replace('refs/tags/', '');
log.debug(`stripRefs passed ${pathStr} and returns ${result}`);
return result;
}
/**
* Converts the given text to title case.
* @param text - The text to convert.
* @returns The text converted to title case.
* @throws {TypeError} If the input is not a string.
*/
export function titlecase(text) {
if (!text)
return undefined;
if (typeof text !== 'string') {
throw new TypeError(`Invalid argument type provided to titlecase(): ${typeof text}`);
}
return text.replaceAll(unicodeWordMatch, (txt) => txt[0] ? txt[0].toUpperCase() + txt.slice(1).toLowerCase() : txt);
}
/**
* Parses the given text and converts it to title case, replacing underscores and dashes with spaces.
* @param text - The text to parse and convert.
* @returns The parsed text converted to title case.
*/
export function prefixParser(text) {
if (!text)
return undefined;
if (typeof text !== 'string') {
throw new TypeError(`Invalid argument type provided to prefixParser(): ${typeof text}`);
}
return titlecase(text.replace(/[_-]+/, ' '));
}
/**
* Wraps the given text into multiple lines with a maximum width of 80 characters.
* @param text - The text to wrap.
* @param content - The array to store the wrapped lines.
* @param prepend - The string to prepend to each wrapped line.
* @returns The array of wrapped lines.
*/
export function wrapText(text, content, prepend = '') {
// Constrain the width of the description
if (!text)
return content;
const width = 80;
let description = text
.trim()
.replaceAll('\r\n', '\n') // Convert CR to LF
.replaceAll(/ +/g, ' ') // Squash consecutive spaces
.replaceAll(' \n', '\n'); // Squash space followed by newline
while (description) {
// Longer than width? Find a space to break apart
let segment;
if (description.length > width) {
segment = description.slice(0, Math.max(0, width + 1));
while (!segment.endsWith(' ') && !segment.endsWith('\n') && segment) {
segment = segment.slice(0, Math.max(0, segment.length - 1));
}
// Trimmed too much?
if (segment.length < width * 0.67) {
segment = description;
}
}
else {
segment = description;
}
// Check for newline
const newlineIndex = segment.indexOf('\n');
if (newlineIndex >= 0) {
segment = segment.slice(0, Math.max(0, newlineIndex + 1));
}
content.push(`${prepend}${segment}`.trimEnd());
// Remaining
description = description.slice(segment.length);
}
return content;
}
export function readFile(filename) {
try {
return readFileSync(filename, 'utf8');
}
catch (error) {
throw new Error(`Cannot read file ${filename}: ${error}`);
}
}
export function repoObjFromRepoName(repository, log, from) {
if (notEmpty(repository)) {
const [owner, repo] = repository.split('/');
if (owner && repo) {
log.debug(`repoObjFromRepoName using ${from} and returns ${JSON.stringify({ owner, repo })}`);
return { owner, repo };
}
}
return undefined;
}
export const remoteGitUrlPattern = /url( )?=( )?.*github\.com[/:](?<owner>.*)\/(?<repo>.*)\.git/;
/**
* Finds the repository information from the input, context, environment variables, or git configuration.
* @param inputRepo - The input repository string.
* @param context - The GitHub context object.
* @returns The repository information (owner and repo) or null if not found.
*/
export function repositoryFinder(inputRepo, context) {
const log = new LogTask('repositoryFinder');
/**
* Attempt to get git user and repo from input
*/
const repoObj = repoObjFromRepoName(inputRepo, log, 'inputRepo');
if (repoObj) {
return repoObj;
}
/**
* Attempt to get git user and repo from GitHub context,
* which includes checking for GITHUB_REPOSITORY environment variable
*/
if (context) {
try {
const result = { ...context.repo };
log.debug(`repositoryFinder using GitHub context and returns ${JSON.stringify(result)}`);
return result;
}
catch (error) {
log.debug(`repositoryFinder using GitHub context gives error ${JSON.stringify(error)}`);
}
}
/**
* Attempt to get git user and repo from .git/config
*/
try {
const fileContent = readFile('.git/config');
log.debug(`loading .git/config:\n***\n${fileContent}\n***`);
const results = remoteGitUrlPattern.exec(fileContent);
if (results?.groups?.owner && results?.groups?.repo) {
log.debug(`repositoryFinder using '.git/config' and returns ${JSON.stringify(results.groups)}`);
return {
owner: results.groups.owner,
repo: results.groups.repo,
};
}
}
catch (error) {
// can't find it
log.error(`Couldn't retrieve owner or repo in .git/config file: ${error}`);
}
throw new Error('No owner or repo found');
}
/**
* Returns the default branch of the git repository.
* @returns The default branch.
*/
/**
* Gets the default branch for the Git repository.
*
* @returns The name of the default branch.
*/
export function getDefaultGitBranch() {
let result;
try {
// Run git command to get default branch
result = execSync('git symbolic-ref HEAD | sed s@^refs/heads/@@');
}
catch (error) {
// If command fails, try alternative for MacOS
if (error) {
try {
result = execSync("git remote set-head origin -a;git remote show origin | head 50 sed -n 's/^.*default branch \\(.*\\)/\\1/p'");
}
catch {
result = execSync("git remote set-head origin -a;git remote show origin | sed -n 's/^s*HEAD branch: \\(.*\\)/\\1/p'");
}
}
}
return result?.toString().trim() ?? '';
}
/**
* Formats the given value as a column header.
* @param value - The value to format.
* @returns The formatted column header.
*/
export function columnHeader(value) {
if (!value) {
return '';
}
let text = value.replaceAll(/\*\*(.*?)\*\*/g, '$1');
// Remove italic formatting: *italic*
text = text.replaceAll(/\*(.*?)\*/g, '$1');
// Remove strikethrough formatting: ~~strikethrough~~
text = text.replaceAll(/~~(.*?)~~/g, '$1');
const normalisedHeader = titlecase(text.trim());
if (normalisedHeader) {
return `${normalisedHeader}`;
}
return '';
}
/**
* Formats the given value as a row header in HTML.
*
* Removes formatting from the string and converts it to code style.
*
* @param value - The string to format as a header
* @returns The formatted row header string
*/
export function rowHeader(value) {
if (!value) {
return '';
}
let text = value;
// Remove bold formatting
text = text.replaceAll(/\*\*(.*?)\*\*/g, '$1');
// Remove italic formatting: *italic*
text = text.replaceAll(/\*(.*?)\*/g, '$1');
// Remove strikethrough formatting: ~~strikethrough~~
text = text.replaceAll(/~~(.*?)~~/g, '$1');
// Normalize spacing
text = text.trim();
// Add code formatting
return `<code>${text}</code>`;
}
export function getCurrentVersionString(inputs) {
let versionString = '';
const log = new LogTask('getCurrentVersionString');
if (inputs.config.get('versioning:enabled')) {
log.debug('version string in generated example is enabled');
const oRide = inputs.config.get('versioning:override');
let packageVersion = process.env.npm_package_version;
log.debug(`version string in env:npm_package_version is ${packageVersion ?? 'not found'}`);
if (!packageVersion) {
log.debug('version string in env:npm_package_version is not found, trying to use git');
try {
accessSync('package.json');
const packageData = JSON.parse(readFileSync('package.json', 'utf8'));
packageVersion = packageData.version;
}
catch (error) {
log.debug(`package.json not found. ${error}`);
}
log.debug(`version string in package.json:version is ${packageVersion ?? 'not found'}`);
}
versionString = oRide && oRide.length > 0 ? oRide : packageVersion ?? '0.0.0';
if (versionString &&
!versionString.startsWith(inputs.config.get('versioning:prefix'))) {
versionString = `${inputs.config.get('versioning:prefix')}${versionString}`;
}
}
else {
versionString = inputs.config.get('versioning:branch');
}
log.debug(`version to use in generated example is ${versionString}`);
return versionString;
}
export function indexOfRegex(str, providedRegex) {
const regex = providedRegex.global
? providedRegex
: new RegExp(providedRegex.source, `${providedRegex.flags}g`);
let index = -1;
let match = regex.exec(str);
while (match) {
index = match.index;
match = regex.exec(str);
}
return index;
}
export function lastIndexOfRegex(str, providedRegex) {
const regex = providedRegex.global
? providedRegex
: new RegExp(providedRegex.source, `${providedRegex.flags}g`);
let index = -1;
let match = regex.exec(str);
while (match) {
index = match.index + match[0].length;
match = regex.exec(str);
}
return index;
}
export function isObject(value) {
const type = typeof value;
return type === 'object' && !!value;
}
//# sourceMappingURL=helpers.js.map