action-docs
Advanced tools
Comparing version 2.1.0 to 2.2.0
# Changelog | ||
## [2.2.0](https://github.com/npalm/action-docs/compare/v2.1.0...v2.2.0) (2024-03-06) | ||
### Features | ||
* support document generation for workflows ([#523](https://github.com/npalm/action-docs/issues/523)) ([f043f7f](https://github.com/npalm/action-docs/commit/f043f7f0e017821cad293ebd71293127c462663b)) | ||
### Bug Fixes | ||
* **deps:** bump yaml from 2.3.4 to 2.4.0 ([#543](https://github.com/npalm/action-docs/issues/543)) ([0c76a5e](https://github.com/npalm/action-docs/commit/0c76a5e8468fc82d71f3a70b2b277b5d366877e3)) | ||
## [2.1.0](https://github.com/npalm/action-docs/compare/v2.0.1...v2.1.0) (2024-02-15) | ||
@@ -4,0 +16,0 @@ |
import { LineBreakType } from "./linebreak.js"; | ||
export interface Options { | ||
tocLevel?: number; | ||
actionFile?: string; | ||
sourceFile?: string; | ||
updateReadme?: boolean; | ||
@@ -12,7 +12,7 @@ readmeFile?: string; | ||
tocLevel: number; | ||
actionFile: string; | ||
sourceFile: string; | ||
updateReadme: boolean; | ||
readmeFile: string; | ||
lineBreaks: LineBreakType; | ||
includeNameHeader?: boolean; | ||
includeNameHeader: boolean; | ||
} | ||
@@ -19,0 +19,0 @@ export declare const defaultOptions: DefaultOptions; |
@@ -10,3 +10,3 @@ import { getLineBreak } from "./linebreak.js"; | ||
tocLevel: 2, | ||
actionFile: "action.yml", | ||
sourceFile: "action.yml", | ||
updateReadme: false, | ||
@@ -17,2 +17,31 @@ readmeFile: "README.md", | ||
}; | ||
var InputType; | ||
(function (InputType) { | ||
InputType[InputType["number"] = 0] = "number"; | ||
InputType[InputType["string"] = 1] = "string"; | ||
InputType[InputType["boolean"] = 2] = "boolean"; | ||
})(InputType || (InputType = {})); | ||
var InputOutputType; | ||
(function (InputOutputType) { | ||
InputOutputType[InputOutputType["actionInput"] = 0] = "actionInput"; | ||
InputOutputType[InputOutputType["workflowInput"] = 1] = "workflowInput"; | ||
InputOutputType[InputOutputType["actionOutput"] = 2] = "actionOutput"; | ||
})(InputOutputType || (InputOutputType = {})); | ||
const inputOutputHeaders = { | ||
[InputOutputType.actionInput]: ["name", "description", "required", "default"], | ||
[InputOutputType.workflowInput]: [ | ||
"name", | ||
"description", | ||
"type", | ||
"required", | ||
"default", | ||
], | ||
[InputOutputType.actionOutput]: ["name", "description"], | ||
}; | ||
const inputOutputDefaults = { | ||
description: "", | ||
type: "", | ||
required: "false", | ||
default: '""', | ||
}; | ||
function createMdTable(data, options, type) { | ||
@@ -23,10 +52,3 @@ const tableData = getInputOutput(data, type); | ||
const result = [headers, filler] | ||
.concat(tableData.rows.map((line) => { | ||
return line.map((elem, i) => { | ||
const pretty = i === 0 || i === 2 || i === 3 ? `\`${line[i]}\`` : elem; | ||
const html = i === 1 ? converter.makeHtml(pretty) : pretty; | ||
const htmlNoNewlines = html.replace(/(\r\n|\n|\r)/gm, " ").trim(); | ||
return htmlNoNewlines; | ||
}); | ||
})) | ||
.concat(tableData.rows) | ||
.filter((x) => x.length > 0) | ||
@@ -37,19 +59,39 @@ .map((x) => `| ${x.join(" | ")} |${getLineBreak(options.lineBreaks)}`) | ||
} | ||
function createMdCodeBlock(data, options) { | ||
function createMdCodeBlock(data, options, isAction = true) { | ||
let codeBlockArray = ["```yaml"]; | ||
codeBlockArray.push("- uses: ***PROJECT***@***VERSION***"); | ||
codeBlockArray.push(" with:"); | ||
const inputs = getInputOutput(data, "input"); | ||
for (const input of inputs.rows) { | ||
const inputBlock = [`${input[0]}:`]; | ||
inputBlock.push(...input[1] | ||
let indent = ""; | ||
if (isAction) { | ||
codeBlockArray.push("- uses: ***PROJECT***@***VERSION***"); | ||
indent += " "; | ||
} | ||
else { | ||
codeBlockArray.push("jobs:"); | ||
indent += " "; | ||
codeBlockArray.push(`${indent}job1:`); | ||
indent += " "; | ||
codeBlockArray.push(`${indent}uses: ***PROJECT***@***VERSION***`); | ||
} | ||
codeBlockArray.push(`${indent}with:`); | ||
indent += " "; | ||
const inputs = getInputOutput(data, isAction ? InputOutputType.actionInput : InputOutputType.workflowInput, false); | ||
for (const row of inputs.rows) { | ||
const inputName = row[0]; | ||
const inputDescCommented = row[1] | ||
.split(/(\r\n|\n|\r)/gm) | ||
.filter((l) => !["", "\r", "\n", "\r\n"].includes(l)) | ||
.map((l) => `# ${l}`)); | ||
.map((l) => `# ${l}`); | ||
const type = isAction ? undefined : row[2]; | ||
const isRequired = isAction ? row[2] : row[3]; | ||
const defaultVal = isAction ? row[3] : row[4]; | ||
const inputBlock = [`${inputName}:`]; | ||
inputBlock.push(...inputDescCommented); | ||
inputBlock.push("#"); | ||
inputBlock.push(`# Required: ${input[2].replace(/`/g, "")}`); | ||
if (input[3]) { | ||
inputBlock.push(`# Default: ${input[3]}`); | ||
if (type) { | ||
inputBlock.push(`# Type: ${type}`); | ||
} | ||
codeBlockArray.push(...inputBlock.map((l) => ` ${l}`)); | ||
inputBlock.push(`# Required: ${isRequired}`); | ||
if (defaultVal) { | ||
inputBlock.push(`# Default: ${defaultVal}`); | ||
} | ||
codeBlockArray.push(...inputBlock.map((l) => `${indent}${l}`)); | ||
codeBlockArray.push(""); | ||
@@ -80,43 +122,94 @@ } | ||
}; | ||
const docs = generateActionDocs(options); | ||
if (options.updateReadme) { | ||
await updateReadme(options, docs.header, "header", options.actionFile); | ||
await updateReadme(options, docs.description, "description", options.actionFile); | ||
await updateReadme(options, docs.inputs, "inputs", options.actionFile); | ||
await updateReadme(options, docs.outputs, "outputs", options.actionFile); | ||
await updateReadme(options, docs.runs, "runs", options.actionFile); | ||
await updateReadme(options, docs.usage, "usage", options.actionFile); | ||
const docs = generateDocs(options); | ||
let outputString = ""; | ||
for (const key in docs) { | ||
const value = docs[key]; | ||
if (options.updateReadme) { | ||
await updateReadme(options, value, key, options.sourceFile); | ||
} | ||
outputString += value; | ||
} | ||
return `${docs.header + docs.description + docs.inputs + docs.outputs + docs.runs}`; | ||
return outputString; | ||
} | ||
function generateActionDocs(options) { | ||
const yml = parse(readFileSync(options.actionFile, "utf-8")); | ||
const inputMdTable = createMdTable(yml.inputs, options, "input"); | ||
const usageMdCodeBlock = createMdCodeBlock(yml.inputs, options); | ||
const outputMdTable = createMdTable(yml.outputs, options, "output"); | ||
let header = ""; | ||
if (options.includeNameHeader) { | ||
header = createMarkdownHeader(options, yml.name); | ||
options.tocLevel++; | ||
function generateDocs(options) { | ||
const yml = parse(readFileSync(options.sourceFile, "utf-8")); | ||
if (yml.runs === undefined) { | ||
return generateWorkflowDocs(yml, options); | ||
} | ||
else { | ||
return generateActionDocs(yml, options); | ||
} | ||
} | ||
function generateActionDocs(yml, options) { | ||
return { | ||
header, | ||
header: generateHeader(yml, options), | ||
description: createMarkdownSection(options, yml.description, "Description"), | ||
inputs: createMarkdownSection(options, inputMdTable, "Inputs"), | ||
outputs: createMarkdownSection(options, outputMdTable, "Outputs"), | ||
inputs: generateInputs(yml.inputs, options, InputOutputType.actionInput), | ||
outputs: generateOutputs(yml.outputs, options), | ||
runs: createMarkdownSection(options, | ||
// eslint-disable-next-line i18n-text/no-en | ||
`This action is a \`${yml.runs.using}\` action.`, "Runs"), | ||
usage: createMarkdownSection(options, usageMdCodeBlock, "Usage"), | ||
usage: generateUsage(yml.inputs, options), | ||
}; | ||
} | ||
function generateWorkflowDocs(yml, options) { | ||
return { | ||
header: generateHeader(yml, options), | ||
inputs: generateInputs(yml.on.workflow_call?.inputs, options, InputOutputType.workflowInput), | ||
outputs: generateOutputs(yml.on.workflow_call?.outputs, options), | ||
runs: "", | ||
usage: generateUsage(yml.on.workflow_call?.inputs, options, false), | ||
}; | ||
} | ||
function generateHeader(yml, options) { | ||
let header = ""; | ||
if (options.includeNameHeader) { | ||
header = createMarkdownHeader(options, yml.name); | ||
options.tocLevel++; | ||
} | ||
return header; | ||
} | ||
function generateInputs(data, options, type) { | ||
const inputMdTable = createMdTable(data, options, type); | ||
return createMarkdownSection(options, inputMdTable, "Inputs"); | ||
} | ||
function generateOutputs(data, options) { | ||
const outputMdTable = createMdTable(data, options, InputOutputType.actionOutput); | ||
return createMarkdownSection(options, outputMdTable, "Outputs"); | ||
} | ||
function generateUsage(data, options, isAction = true) { | ||
const usageMdCodeBlock = createMdCodeBlock(data, options, isAction); | ||
return createMarkdownSection(options, usageMdCodeBlock, "Usage"); | ||
} | ||
function escapeRegExp(x) { | ||
return x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string | ||
} | ||
async function updateReadme(options, text, section, actionFile) { | ||
if (section === "usage") { | ||
const readmeFileText = String(readFileSync(options.readmeFile, "utf-8")); | ||
const match = readmeFileText.match(new RegExp(`<!-- action-docs-usage action="${escapeRegExp(actionFile)}" project="(.*)" version="(.*)" -->.?`)); | ||
if (match) { | ||
const commentExpression = `<!-- action-docs-usage action="${actionFile}" project="${match[1]}" version="${match[2]}" -->`; | ||
async function updateReadme(options, text, section, sourceFile) { | ||
const lineBreak = getLineBreak(options.lineBreaks); | ||
const readmeFileText = String(readFileSync(options.readmeFile, "utf-8")); | ||
const sourceOrActionMatches = readmeFileText.match(new RegExp(`<!-- action-docs-${section} (source|action)`)); | ||
if (sourceOrActionMatches) { | ||
const sourceOrAction = sourceOrActionMatches[1]; | ||
if (section === "usage") { | ||
const match = readmeFileText.match(new RegExp(`<!-- action-docs-usage ${sourceOrAction}="${escapeRegExp(sourceFile)}" project="(.*)" version="(.*)" -->.?`)); | ||
if (match) { | ||
const commentExpression = `<!-- action-docs-usage ${sourceOrAction}="${sourceFile}" project="${match[1]}" version="${match[2]}" -->`; | ||
const regexp = new RegExp(`${escapeRegExp(commentExpression)}(?:(?:\r\n|\r|\n.*)+${escapeRegExp(commentExpression)})?`); | ||
const processedText = text | ||
.trim() | ||
.replace("***PROJECT***", match[1]) | ||
.replace("***VERSION***", match[2]); | ||
await replaceInFile.replaceInFile({ | ||
files: options.readmeFile, | ||
from: regexp, | ||
to: commentExpression + | ||
lineBreak + | ||
processedText + | ||
lineBreak + | ||
commentExpression, | ||
}); | ||
} | ||
} | ||
else { | ||
const commentExpression = `<!-- action-docs-${section} ${sourceOrAction}="${sourceFile}" -->`; | ||
const regexp = new RegExp(`${escapeRegExp(commentExpression)}(?:(?:\r\n|\r|\n.*)+${escapeRegExp(commentExpression)})?`); | ||
@@ -126,18 +219,10 @@ await replaceInFile.replaceInFile({ | ||
from: regexp, | ||
to: `${commentExpression}${getLineBreak(options.lineBreaks)}${text | ||
.trim() | ||
.replace("***PROJECT***", match[1]) | ||
.replace("***VERSION***", match[2])}${getLineBreak(options.lineBreaks)}${commentExpression}`, | ||
to: commentExpression + | ||
lineBreak + | ||
text.trim() + | ||
lineBreak + | ||
commentExpression, | ||
}); | ||
} | ||
} | ||
else { | ||
const commentExpression = `<!-- action-docs-${section} action="${actionFile}" -->`; | ||
const regexp = new RegExp(`${escapeRegExp(commentExpression)}(?:(?:\r\n|\r|\n.*)+${escapeRegExp(commentExpression)})?`); | ||
await replaceInFile.replaceInFile({ | ||
files: options.readmeFile, | ||
from: regexp, | ||
to: `${commentExpression}${getLineBreak(options.lineBreaks)}${text.trim()}${getLineBreak(options.lineBreaks)}${commentExpression}`, | ||
}); | ||
} | ||
} | ||
@@ -156,3 +241,9 @@ function createMarkdownSection(options, data, header) { | ||
} | ||
function getInputOutput(data, type) { | ||
function isHtmlColumn(columnName) { | ||
return columnName === "description"; | ||
} | ||
function stripNewLines(value) { | ||
return value.replace(/\r\n|\r|\n/g, " "); | ||
} | ||
function getInputOutput(data, type, format = true) { | ||
let headers = []; | ||
@@ -163,6 +254,3 @@ const rows = []; | ||
} | ||
headers = | ||
type === "input" | ||
? ["name", "description", "required", "default"] | ||
: ["name", "description"]; | ||
headers = inputOutputHeaders[type]; | ||
for (let i = 0; i < Object.keys(data).length; i++) { | ||
@@ -172,12 +260,27 @@ const key = Object.keys(data)[i]; | ||
rows[i] = []; | ||
rows[i].push(key); | ||
rows[i].push(value.description ? value.description : ""); | ||
if (type === "input") { | ||
rows[i].push(value.required ? String(value.required) : "false"); | ||
if (value.default !== undefined && value.default !== "") { | ||
rows[i].push(value.default.toString().replace(/\r\n|\r|\n/g, " ")); | ||
for (const columnName of headers) { | ||
let rowValue = ""; | ||
if (columnName === "name") { | ||
rowValue = key; | ||
} | ||
else if (columnName === "default") { | ||
rowValue = | ||
value[columnName] !== undefined && value[columnName] !== "" | ||
? stripNewLines(String(value[columnName])) | ||
: inputOutputDefaults[columnName]; | ||
} | ||
else { | ||
rows[i].push('""'); | ||
rowValue = value[columnName] | ||
? value[columnName] | ||
: inputOutputDefaults[columnName]; | ||
} | ||
if (format) { | ||
if (isHtmlColumn(columnName)) { | ||
rowValue = stripNewLines(converter.makeHtml(rowValue)).trim(); | ||
} | ||
else { | ||
rowValue = `\`${rowValue}\``; | ||
} | ||
} | ||
rows[i].push(rowValue); | ||
} | ||
@@ -184,0 +287,0 @@ } |
@@ -19,6 +19,14 @@ #!/usr/bin/env node | ||
type: "string", | ||
default: defaultOptions.actionFile, | ||
default: defaultOptions.sourceFile, | ||
demandOption: false, | ||
alias: "a", | ||
deprecated: 'use "source" instead', | ||
}, | ||
source: { | ||
description: "GitHub source file", | ||
type: "string", | ||
default: defaultOptions.sourceFile, | ||
demandOption: false, | ||
alias: "s", | ||
}, | ||
"no-banner": { | ||
@@ -53,3 +61,3 @@ description: "Print no banner", | ||
const options = { | ||
actionFile: args.action, | ||
sourceFile: args.source ?? args.action, | ||
tocLevel: args["toc-level"], | ||
@@ -56,0 +64,0 @@ updateReadme, |
{ | ||
"name": "action-docs", | ||
"version": "2.1.0", | ||
"version": "2.2.0", | ||
"description": "Generate GitHub action docs based on action.yml", | ||
@@ -20,4 +20,7 @@ "main": "lib/index.js", | ||
"test-cli": "nyc jest --testTimeout=10000 --silent --testMatch=**/cli*test.ts && nyc report --reporter=lcov --reporter=html --report-dir=./coverage_nyc", | ||
"test-action": "nyc jest --testTimeout=10000 --silent --testMatch=**/action-docs.test.ts --coverage=false", | ||
"dev-action": "node lib/cli.js -a __tests__/fixtures/action.yml", | ||
"test-action": "nyc jest --testTimeout=10000 --silent --testMatch=**/action-docs-action.test.ts --coverage=false", | ||
"test-workflow": "nyc jest --testTimeout=10000 --silent --testMatch=**/action-docs-workflow.test.ts --coverage=false", | ||
"dev-action": "yarn run build && node lib/cli.js -s __tests__/fixtures/action/action.yml", | ||
"dev-workflow": "yarn run build && node lib/cli.js -s __tests__/fixtures/workflow/workflow.yml", | ||
"help": "yarn run build && node lib/cli.js --help", | ||
"all": "yarn run build && yarn run format && yarn run lint && yarn test" | ||
@@ -40,3 +43,2 @@ }, | ||
"chalk": "^5.3.0", | ||
"eslint-import-resolver-typescript": "^3.6.1", | ||
"figlet": "^1.7.0", | ||
@@ -60,2 +62,3 @@ "replace-in-file": "^7.1.0", | ||
"eslint": "^8.56.0", | ||
"eslint-import-resolver-typescript": "^3.6.1", | ||
"eslint-plugin-github": "^4.10.0", | ||
@@ -62,0 +65,0 @@ "eslint-plugin-jest": "^27.6.0", |
@@ -9,3 +9,3 @@ <!-- BADGES/ --> | ||
A CLI to generate and update documentation for GitHub actions, based on the action definition `.yml`. To update your README in a GitHub workflow you can use the [action-docs-action](https://github.com/npalm/action-docs-action). | ||
A CLI to generate and update documentation for GitHub actions or workflows, based on the definition `.yml`. To update your README in a GitHub workflow you can use the [action-docs-action](https://github.com/npalm/action-docs-action). | ||
@@ -17,9 +17,11 @@ ## TL;DR | ||
```md | ||
<!-- action-docs-description action="action.yml" --> | ||
<!-- action-docs-header source="action.yml" --> | ||
<!-- action-docs-inputs action="action.yml" --> | ||
<!-- action-docs-description source="action.yml" --> # applicable for actions only | ||
<!-- action-docs-outputs action="action.yml" --> | ||
<!-- action-docs-inputs source="action.yml" --> | ||
<!-- action-docs-runs action="action.yml" --> | ||
<!-- action-docs-outputs source="action.yml" --> | ||
<!-- action-docs-runs source="action.yml" --> # applicable for actions only | ||
``` | ||
@@ -30,3 +32,3 @@ | ||
```md | ||
<!-- action-docs-usage action="action.yml" project="<project>" version="<version>" --> | ||
<!-- action-docs-usage source="action.yml" project="<project>" version="<version>" --> | ||
``` | ||
@@ -61,11 +63,14 @@ | ||
Options: | ||
--help Show help [boolean] | ||
--version Show version number [boolean] | ||
-t, --toc-level TOC level used for markdown [number] [default: 2] | ||
-a, --action GitHub action file [string] [default: "action.yml"] | ||
--no-banner Print no banner | ||
-u, --update-readme Update readme file. [string] | ||
-l, --line-breaks Used line breaks in the generated docs. | ||
[string] [choices: "CR", "LF", "CRLF"] [default: "LF"] | ||
-n, --include-name-header Include a header with the action/workflow name. | ||
--version Show version number [boolean] | ||
-t, --toc-level TOC level used for markdown [number] [default: 2] | ||
-a, --action GitHub action file | ||
[deprecated: use "source" instead] [string] [default: "action.yml"] | ||
-s, --source GitHub source file [string] [default: "action.yml"] | ||
--no-banner Print no banner | ||
-u, --update-readme Update readme file. [string] | ||
-l, --line-breaks Used line breaks in the generated docs. | ||
[string] [choices: "CR", "LF", "CRLF"] [default: "LF"] | ||
-n, --include-name-header Include a header with the action/workflow name | ||
[boolean] | ||
--help Show help [boolean] | ||
``` | ||
@@ -78,11 +83,11 @@ | ||
```md | ||
<!-- action-docs-header action="action.yml" --> | ||
<!-- action-docs-header source="action.yml" --> | ||
<!-- action-docs-description action="action.yml" --> | ||
<!-- action-docs-description source="action.yml" --> | ||
<!-- action-docs-inputs action="action.yml" --> | ||
<!-- action-docs-inputs source="action.yml" --> | ||
<!-- action-docs-outputs action="action.yml" --> | ||
<!-- action-docs-outputs source="action.yml" --> | ||
<!-- action-docs-runs action="action.yml" --> | ||
<!-- action-docs-runs source="action.yml" --> | ||
``` | ||
@@ -94,4 +99,4 @@ | ||
1. write it in tags like `action="another/action.yml"`; | ||
1. specify in a command via the `-a` option like `action-docs -a another/action.yml` | ||
1. write it in tags like `source="another/action.yml"`; | ||
2. specify in a command via the `-s` option like `action-docs -s another/action.yml` | ||
@@ -115,3 +120,3 @@ ### Examples | ||
```bash | ||
action-docs --action another/action.yaml | ||
action-docs --source another/action.yaml | ||
``` | ||
@@ -122,3 +127,3 @@ | ||
```bash | ||
action-docs --action ./some-dir/action.yml --toc-level 3 --update-readme docs.md | ||
action-docs --source ./some-dir/action.yml --toc-level 3 --update-readme docs.md | ||
``` | ||
@@ -132,3 +137,3 @@ | ||
await generateActionMarkdownDocs({ | ||
actionFile: 'action.yml' | ||
sourceFile: 'action.yml' | ||
tocLevel: 2 | ||
@@ -135,0 +140,0 @@ updateReadme: true |
@@ -11,3 +11,3 @@ import { LineBreakType, getLineBreak } from "./linebreak.js"; | ||
tocLevel?: number; | ||
actionFile?: string; | ||
sourceFile?: string; | ||
updateReadme?: boolean; | ||
@@ -19,14 +19,6 @@ readmeFile?: string; | ||
interface ActionMarkdown { | ||
header: string; | ||
description: string; | ||
inputs: string; | ||
outputs: string; | ||
runs: string; | ||
usage: string; | ||
} | ||
interface ActionYml { | ||
interface YmlStructure { | ||
name: string; | ||
description: string; | ||
on: Record<string, WorkflowTriggerEvent>; | ||
inputs: ActionInputsOutputs; | ||
@@ -42,9 +34,17 @@ outputs: ActionInputsOutputs; | ||
interface WorkflowTriggerEvent { | ||
types: string[]; | ||
branches: string[]; | ||
cron: string[]; | ||
inputs: ActionInputsOutputs; | ||
outputs: ActionInputsOutputs; | ||
} | ||
interface DefaultOptions { | ||
tocLevel: number; | ||
actionFile: string; | ||
sourceFile: string; | ||
updateReadme: boolean; | ||
readmeFile: string; | ||
lineBreaks: LineBreakType; | ||
includeNameHeader?: boolean; | ||
includeNameHeader: boolean; | ||
} | ||
@@ -54,3 +54,3 @@ | ||
tocLevel: 2, | ||
actionFile: "action.yml", | ||
sourceFile: "action.yml", | ||
updateReadme: false, | ||
@@ -62,18 +62,46 @@ readmeFile: "README.md", | ||
type ActionInputsOutputs = Record<string, ActionInput | ActionOutput>; | ||
type ActionInputsOutputs = Record<string, InputOutput>; | ||
interface ActionInput { | ||
enum InputType { | ||
number, | ||
string, | ||
boolean, | ||
} | ||
enum InputOutputType { | ||
actionInput, | ||
workflowInput, | ||
actionOutput, | ||
} | ||
const inputOutputHeaders: Record<InputOutputType, string[]> = { | ||
[InputOutputType.actionInput]: ["name", "description", "required", "default"], | ||
[InputOutputType.workflowInput]: [ | ||
"name", | ||
"description", | ||
"type", | ||
"required", | ||
"default", | ||
], | ||
[InputOutputType.actionOutput]: ["name", "description"], | ||
}; | ||
const inputOutputDefaults: Record<string, string> = { | ||
description: "", | ||
type: "", | ||
required: "false", | ||
default: '""', | ||
}; | ||
interface InputOutput { | ||
required?: boolean; | ||
description?: string; | ||
default?: string; | ||
type?: InputType; | ||
} | ||
interface ActionOutput { | ||
description: string; | ||
} | ||
function createMdTable( | ||
data: ActionInputsOutputs, | ||
options: DefaultOptions, | ||
type: "input" | "output", | ||
type: InputOutputType, | ||
): string { | ||
@@ -86,13 +114,3 @@ const tableData = getInputOutput(data, type); | ||
const result = [headers, filler] | ||
.concat( | ||
tableData.rows.map((line) => { | ||
return line.map((elem, i) => { | ||
const pretty = | ||
i === 0 || i === 2 || i === 3 ? `\`${line[i]}\`` : elem; | ||
const html = i === 1 ? converter.makeHtml(pretty) : pretty; | ||
const htmlNoNewlines = html.replace(/(\r\n|\n|\r)/gm, " ").trim(); | ||
return htmlNoNewlines; | ||
}); | ||
}), | ||
) | ||
.concat(tableData.rows) | ||
.filter((x) => x.length > 0) | ||
@@ -108,23 +126,49 @@ .map((x) => `| ${x.join(" | ")} |${getLineBreak(options.lineBreaks)}`) | ||
options: DefaultOptions, | ||
isAction = true, | ||
): string { | ||
let codeBlockArray = ["```yaml"]; | ||
codeBlockArray.push("- uses: ***PROJECT***@***VERSION***"); | ||
codeBlockArray.push(" with:"); | ||
const inputs = getInputOutput(data, "input"); | ||
for (const input of inputs.rows) { | ||
const inputBlock = [`${input[0]}:`]; | ||
inputBlock.push( | ||
...input[1] | ||
.split(/(\r\n|\n|\r)/gm) | ||
.filter((l) => !["", "\r", "\n", "\r\n"].includes(l)) | ||
.map((l) => `# ${l}`), | ||
); | ||
let indent = ""; | ||
if (isAction) { | ||
codeBlockArray.push("- uses: ***PROJECT***@***VERSION***"); | ||
indent += " "; | ||
} else { | ||
codeBlockArray.push("jobs:"); | ||
indent += " "; | ||
codeBlockArray.push(`${indent}job1:`); | ||
indent += " "; | ||
codeBlockArray.push(`${indent}uses: ***PROJECT***@***VERSION***`); | ||
} | ||
codeBlockArray.push(`${indent}with:`); | ||
indent += " "; | ||
const inputs = getInputOutput( | ||
data, | ||
isAction ? InputOutputType.actionInput : InputOutputType.workflowInput, | ||
false, | ||
); | ||
for (const row of inputs.rows) { | ||
const inputName = row[0]; | ||
const inputDescCommented = row[1] | ||
.split(/(\r\n|\n|\r)/gm) | ||
.filter((l) => !["", "\r", "\n", "\r\n"].includes(l)) | ||
.map((l) => `# ${l}`); | ||
const type = isAction ? undefined : row[2]; | ||
const isRequired = isAction ? row[2] : row[3]; | ||
const defaultVal = isAction ? row[3] : row[4]; | ||
const inputBlock = [`${inputName}:`]; | ||
inputBlock.push(...inputDescCommented); | ||
inputBlock.push("#"); | ||
inputBlock.push(`# Required: ${input[2].replace(/`/g, "")}`); | ||
if (input[3]) { | ||
inputBlock.push(`# Default: ${input[3]}`); | ||
if (type) { | ||
inputBlock.push(`# Type: ${type}`); | ||
} | ||
inputBlock.push(`# Required: ${isRequired}`); | ||
if (defaultVal) { | ||
inputBlock.push(`# Default: ${defaultVal}`); | ||
} | ||
codeBlockArray.push(...inputBlock.map((l) => ` ${l}`)); | ||
codeBlockArray.push(...inputBlock.map((l) => `${indent}${l}`)); | ||
codeBlockArray.push(""); | ||
@@ -162,38 +206,37 @@ } | ||
const docs = generateActionDocs(options); | ||
if (options.updateReadme) { | ||
await updateReadme(options, docs.header, "header", options.actionFile); | ||
await updateReadme( | ||
options, | ||
docs.description, | ||
"description", | ||
options.actionFile, | ||
); | ||
await updateReadme(options, docs.inputs, "inputs", options.actionFile); | ||
await updateReadme(options, docs.outputs, "outputs", options.actionFile); | ||
await updateReadme(options, docs.runs, "runs", options.actionFile); | ||
await updateReadme(options, docs.usage, "usage", options.actionFile); | ||
const docs = generateDocs(options); | ||
let outputString = ""; | ||
for (const key in docs) { | ||
const value = docs[key]; | ||
if (options.updateReadme) { | ||
await updateReadme(options, value, key, options.sourceFile); | ||
} | ||
outputString += value; | ||
} | ||
return `${docs.header + docs.description + docs.inputs + docs.outputs + docs.runs}`; | ||
return outputString; | ||
} | ||
function generateActionDocs(options: DefaultOptions): ActionMarkdown { | ||
const yml = parse(readFileSync(options.actionFile, "utf-8")) as ActionYml; | ||
function generateDocs(options: DefaultOptions): Record<string, string> { | ||
const yml = parse(readFileSync(options.sourceFile, "utf-8")) as YmlStructure; | ||
const inputMdTable = createMdTable(yml.inputs, options, "input"); | ||
const usageMdCodeBlock = createMdCodeBlock(yml.inputs, options); | ||
const outputMdTable = createMdTable(yml.outputs, options, "output"); | ||
let header = ""; | ||
if (options.includeNameHeader) { | ||
header = createMarkdownHeader(options, yml.name); | ||
options.tocLevel++; | ||
if (yml.runs === undefined) { | ||
return generateWorkflowDocs(yml, options); | ||
} else { | ||
return generateActionDocs(yml, options); | ||
} | ||
} | ||
function generateActionDocs( | ||
yml: YmlStructure, | ||
options: DefaultOptions, | ||
): Record<string, string> { | ||
return { | ||
header, | ||
header: generateHeader(yml, options), | ||
description: createMarkdownSection(options, yml.description, "Description"), | ||
inputs: createMarkdownSection(options, inputMdTable, "Inputs"), | ||
outputs: createMarkdownSection(options, outputMdTable, "Outputs"), | ||
inputs: generateInputs(yml.inputs, options, InputOutputType.actionInput), | ||
outputs: generateOutputs(yml.outputs, options), | ||
runs: createMarkdownSection( | ||
@@ -205,6 +248,63 @@ options, | ||
), | ||
usage: createMarkdownSection(options, usageMdCodeBlock, "Usage"), | ||
usage: generateUsage(yml.inputs, options), | ||
}; | ||
} | ||
function generateWorkflowDocs( | ||
yml: YmlStructure, | ||
options: DefaultOptions, | ||
): Record<string, string> { | ||
return { | ||
header: generateHeader(yml, options), | ||
inputs: generateInputs( | ||
yml.on.workflow_call?.inputs, | ||
options, | ||
InputOutputType.workflowInput, | ||
), | ||
outputs: generateOutputs(yml.on.workflow_call?.outputs, options), | ||
runs: "", | ||
usage: generateUsage(yml.on.workflow_call?.inputs, options, false), | ||
}; | ||
} | ||
function generateHeader(yml: YmlStructure, options: DefaultOptions): string { | ||
let header = ""; | ||
if (options.includeNameHeader) { | ||
header = createMarkdownHeader(options, yml.name); | ||
options.tocLevel++; | ||
} | ||
return header; | ||
} | ||
function generateInputs( | ||
data: ActionInputsOutputs, | ||
options: DefaultOptions, | ||
type: InputOutputType, | ||
): string { | ||
const inputMdTable = createMdTable(data, options, type); | ||
return createMarkdownSection(options, inputMdTable, "Inputs"); | ||
} | ||
function generateOutputs( | ||
data: ActionInputsOutputs, | ||
options: DefaultOptions, | ||
): string { | ||
const outputMdTable = createMdTable( | ||
data, | ||
options, | ||
InputOutputType.actionOutput, | ||
); | ||
return createMarkdownSection(options, outputMdTable, "Outputs"); | ||
} | ||
function generateUsage( | ||
data: ActionInputsOutputs, | ||
options: DefaultOptions, | ||
isAction = true, | ||
): string { | ||
const usageMdCodeBlock = createMdCodeBlock(data, options, isAction); | ||
return createMarkdownSection(options, usageMdCodeBlock, "Usage"); | ||
} | ||
function escapeRegExp(x: string): string { | ||
@@ -218,14 +318,45 @@ return x.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string | ||
section: string, | ||
actionFile: string, | ||
sourceFile: string, | ||
): Promise<void> { | ||
if (section === "usage") { | ||
const readmeFileText = String(readFileSync(options.readmeFile, "utf-8")); | ||
const match = readmeFileText.match( | ||
new RegExp( | ||
`<!-- action-docs-usage action="${escapeRegExp(actionFile)}" project="(.*)" version="(.*)" -->.?`, | ||
), | ||
) as string[]; | ||
const lineBreak = getLineBreak(options.lineBreaks); | ||
if (match) { | ||
const commentExpression = `<!-- action-docs-usage action="${actionFile}" project="${match[1]}" version="${match[2]}" -->`; | ||
const readmeFileText = String(readFileSync(options.readmeFile, "utf-8")); | ||
const sourceOrActionMatches = readmeFileText.match( | ||
new RegExp(`<!-- action-docs-${section} (source|action)`), | ||
) as string[]; | ||
if (sourceOrActionMatches) { | ||
const sourceOrAction = sourceOrActionMatches[1]; | ||
if (section === "usage") { | ||
const match = readmeFileText.match( | ||
new RegExp( | ||
`<!-- action-docs-usage ${sourceOrAction}="${escapeRegExp(sourceFile)}" project="(.*)" version="(.*)" -->.?`, | ||
), | ||
) as string[]; | ||
if (match) { | ||
const commentExpression = `<!-- action-docs-usage ${sourceOrAction}="${sourceFile}" project="${match[1]}" version="${match[2]}" -->`; | ||
const regexp = new RegExp( | ||
`${escapeRegExp(commentExpression)}(?:(?:\r\n|\r|\n.*)+${escapeRegExp(commentExpression)})?`, | ||
); | ||
const processedText = text | ||
.trim() | ||
.replace("***PROJECT***", match[1]) | ||
.replace("***VERSION***", match[2]); | ||
await replaceInFile.replaceInFile({ | ||
files: options.readmeFile, | ||
from: regexp, | ||
to: | ||
commentExpression + | ||
lineBreak + | ||
processedText + | ||
lineBreak + | ||
commentExpression, | ||
}); | ||
} | ||
} else { | ||
const commentExpression = `<!-- action-docs-${section} ${sourceOrAction}="${sourceFile}" -->`; | ||
const regexp = new RegExp( | ||
@@ -238,23 +369,10 @@ `${escapeRegExp(commentExpression)}(?:(?:\r\n|\r|\n.*)+${escapeRegExp(commentExpression)})?`, | ||
from: regexp, | ||
to: `${commentExpression}${getLineBreak(options.lineBreaks)}${text | ||
.trim() | ||
.replace("***PROJECT***", match[1]) | ||
.replace("***VERSION***", match[2])}${getLineBreak( | ||
options.lineBreaks, | ||
)}${commentExpression}`, | ||
to: | ||
commentExpression + | ||
lineBreak + | ||
text.trim() + | ||
lineBreak + | ||
commentExpression, | ||
}); | ||
} | ||
} else { | ||
const commentExpression = `<!-- action-docs-${section} action="${actionFile}" -->`; | ||
const regexp = new RegExp( | ||
`${escapeRegExp(commentExpression)}(?:(?:\r\n|\r|\n.*)+${escapeRegExp(commentExpression)})?`, | ||
); | ||
await replaceInFile.replaceInFile({ | ||
files: options.readmeFile, | ||
from: regexp, | ||
to: `${commentExpression}${getLineBreak( | ||
options.lineBreaks, | ||
)}${text.trim()}${getLineBreak(options.lineBreaks)}${commentExpression}`, | ||
}); | ||
} | ||
@@ -283,8 +401,18 @@ } | ||
function isHtmlColumn(columnName: string): boolean { | ||
return columnName === "description"; | ||
} | ||
function stripNewLines(value: string): string { | ||
return value.replace(/\r\n|\r|\n/g, " "); | ||
} | ||
function getInputOutput( | ||
data: ActionInputsOutputs, | ||
type: "input" | "output", | ||
type: InputOutputType, | ||
format = true, | ||
): { headers: string[]; rows: string[][] } { | ||
let headers: string[] = []; | ||
const rows: string[][] = []; | ||
if (data === undefined) { | ||
@@ -294,22 +422,34 @@ return { headers, rows }; | ||
headers = | ||
type === "input" | ||
? ["name", "description", "required", "default"] | ||
: ["name", "description"]; | ||
headers = inputOutputHeaders[type]; | ||
for (let i = 0; i < Object.keys(data).length; i++) { | ||
const key = Object.keys(data)[i]; | ||
const value = data[key] as ActionInput; | ||
const value = data[key] as Record<string, string>; | ||
rows[i] = []; | ||
rows[i].push(key); | ||
rows[i].push(value.description ? value.description : ""); | ||
if (type === "input") { | ||
rows[i].push(value.required ? String(value.required) : "false"); | ||
for (const columnName of headers) { | ||
let rowValue = ""; | ||
if (value.default !== undefined && value.default !== "") { | ||
rows[i].push(value.default.toString().replace(/\r\n|\r|\n/g, " ")); | ||
if (columnName === "name") { | ||
rowValue = key; | ||
} else if (columnName === "default") { | ||
rowValue = | ||
value[columnName] !== undefined && value[columnName] !== "" | ||
? stripNewLines(String(value[columnName])) | ||
: inputOutputDefaults[columnName]; | ||
} else { | ||
rows[i].push('""'); | ||
rowValue = value[columnName] | ||
? value[columnName] | ||
: inputOutputDefaults[columnName]; | ||
} | ||
if (format) { | ||
if (isHtmlColumn(columnName)) { | ||
rowValue = stripNewLines(converter.makeHtml(rowValue)).trim(); | ||
} else { | ||
rowValue = `\`${rowValue}\``; | ||
} | ||
} | ||
rows[i].push(rowValue); | ||
} | ||
@@ -316,0 +456,0 @@ } |
@@ -21,6 +21,14 @@ #!/usr/bin/env node | ||
type: "string", | ||
default: defaultOptions.actionFile, | ||
default: defaultOptions.sourceFile, | ||
demandOption: false, | ||
alias: "a", | ||
deprecated: 'use "source" instead', | ||
}, | ||
source: { | ||
description: "GitHub source file", | ||
type: "string", | ||
default: defaultOptions.sourceFile, | ||
demandOption: false, | ||
alias: "s", | ||
}, | ||
"no-banner": { | ||
@@ -60,3 +68,3 @@ description: "Print no banner", | ||
const options = { | ||
actionFile: args.action, | ||
sourceFile: args.source ?? args.action, | ||
tocLevel: args["toc-level"], | ||
@@ -63,0 +71,0 @@ updateReadme, |
45338
6
911
143
16
- Removed@eslint-community/eslint-utils@4.4.1(transitive)
- Removed@eslint-community/regexpp@4.12.1(transitive)
- Removed@eslint/config-array@0.19.2(transitive)
- Removed@eslint/core@0.10.0(transitive)
- Removed@eslint/eslintrc@3.2.0(transitive)
- Removed@eslint/js@9.19.0(transitive)
- Removed@eslint/object-schema@2.1.6(transitive)
- Removed@eslint/plugin-kit@0.2.5(transitive)
- Removed@humanfs/core@0.19.1(transitive)
- Removed@humanfs/node@0.16.6(transitive)
- Removed@humanwhocodes/module-importer@1.0.1(transitive)
- Removed@humanwhocodes/retry@0.3.10.4.1(transitive)
- Removed@nodelib/fs.scandir@2.1.5(transitive)
- Removed@nodelib/fs.stat@2.0.5(transitive)
- Removed@nodelib/fs.walk@1.2.8(transitive)
- Removed@nolyfill/is-core-module@1.0.39(transitive)
- Removed@types/estree@1.0.6(transitive)
- Removed@types/json-schema@7.0.15(transitive)
- Removedacorn@8.14.0(transitive)
- Removedacorn-jsx@5.3.2(transitive)
- Removedajv@6.12.6(transitive)
- Removedargparse@2.0.1(transitive)
- Removedbrace-expansion@1.1.11(transitive)
- Removedbraces@3.0.3(transitive)
- Removedcallsites@3.1.0(transitive)
- Removedconcat-map@0.0.1(transitive)
- Removedcross-spawn@7.0.6(transitive)
- Removeddebug@4.4.0(transitive)
- Removeddeep-is@0.1.4(transitive)
- Removedenhanced-resolve@5.18.0(transitive)
- Removedescape-string-regexp@4.0.0(transitive)
- Removedeslint@9.19.0(transitive)
- Removedeslint-import-resolver-typescript@3.7.0(transitive)
- Removedeslint-scope@8.2.0(transitive)
- Removedeslint-visitor-keys@3.4.34.2.0(transitive)
- Removedespree@10.3.0(transitive)
- Removedesquery@1.6.0(transitive)
- Removedesrecurse@4.3.0(transitive)
- Removedestraverse@5.3.0(transitive)
- Removedesutils@2.0.3(transitive)
- Removedfast-deep-equal@3.1.3(transitive)
- Removedfast-glob@3.3.3(transitive)
- Removedfast-json-stable-stringify@2.1.0(transitive)
- Removedfast-levenshtein@2.0.6(transitive)
- Removedfastq@1.19.0(transitive)
- Removedfile-entry-cache@8.0.0(transitive)
- Removedfill-range@7.1.1(transitive)
- Removedfind-up@5.0.0(transitive)
- Removedflat-cache@4.0.1(transitive)
- Removedflatted@3.3.2(transitive)
- Removedget-tsconfig@4.10.0(transitive)
- Removedglob-parent@5.1.26.0.2(transitive)
- Removedglobals@14.0.0(transitive)
- Removedgraceful-fs@4.2.11(transitive)
- Removedignore@5.3.2(transitive)
- Removedimport-fresh@3.3.1(transitive)
- Removedimurmurhash@0.1.4(transitive)
- Removedis-bun-module@1.3.0(transitive)
- Removedis-extglob@2.1.1(transitive)
- Removedis-glob@4.0.3(transitive)
- Removedis-number@7.0.0(transitive)
- Removedisexe@2.0.0(transitive)
- Removedjs-yaml@4.1.0(transitive)
- Removedjson-buffer@3.0.1(transitive)
- Removedjson-schema-traverse@0.4.1(transitive)
- Removedjson-stable-stringify-without-jsonify@1.0.1(transitive)
- Removedkeyv@4.5.4(transitive)
- Removedlevn@0.4.1(transitive)
- Removedlocate-path@6.0.0(transitive)
- Removedlodash.merge@4.6.2(transitive)
- Removedmerge2@1.4.1(transitive)
- Removedmicromatch@4.0.8(transitive)
- Removedminimatch@3.1.2(transitive)
- Removedms@2.1.3(transitive)
- Removednatural-compare@1.4.0(transitive)
- Removedoptionator@0.9.4(transitive)
- Removedp-limit@3.1.0(transitive)
- Removedp-locate@5.0.0(transitive)
- Removedparent-module@1.0.1(transitive)
- Removedpath-exists@4.0.0(transitive)
- Removedpath-key@3.1.1(transitive)
- Removedpicomatch@2.3.1(transitive)
- Removedprelude-ls@1.2.1(transitive)
- Removedpunycode@2.3.1(transitive)
- Removedqueue-microtask@1.2.3(transitive)
- Removedresolve-from@4.0.0(transitive)
- Removedresolve-pkg-maps@1.0.0(transitive)
- Removedreusify@1.0.4(transitive)
- Removedrun-parallel@1.2.0(transitive)
- Removedsemver@7.7.0(transitive)
- Removedshebang-command@2.0.0(transitive)
- Removedshebang-regex@3.0.0(transitive)
- Removedstable-hash@0.0.4(transitive)
- Removedstrip-json-comments@3.1.1(transitive)
- Removedtapable@2.2.1(transitive)
- Removedto-regex-range@5.0.1(transitive)
- Removedtype-check@0.4.0(transitive)
- Removeduri-js@4.4.1(transitive)
- Removedwhich@2.0.2(transitive)
- Removedword-wrap@1.2.5(transitive)
- Removedyocto-queue@0.1.0(transitive)