From ef810cc71158e7e1614ff6e16bc61a7539e674e5 Mon Sep 17 00:00:00 2001 From: Laura Armitage Date: Wed, 14 Feb 2024 14:49:27 +0000 Subject: [PATCH] GH-240 ability to display workflow inputs and outputs --- .../action/all_fields_readme.input.crlf | 14 +- .../action/all_fields_readme.output.crlf | 64 +++---- .../action/all_fields_readme_header.output | 2 + .../action/default-with-header.output | 1 + __tests__/fixtures/workflow/default.output | 5 +- __tests__/fixtures/workflow/workflow.yml | 2 +- src/action-docs.ts | 163 ++++++++++++------ 7 files changed, 152 insertions(+), 99 deletions(-) diff --git a/__tests__/fixtures/action/all_fields_readme.input.crlf b/__tests__/fixtures/action/all_fields_readme.input.crlf index 4b168b7e..2491c1b9 100644 --- a/__tests__/fixtures/action/all_fields_readme.input.crlf +++ b/__tests__/fixtures/action/all_fields_readme.input.crlf @@ -1,7 +1,7 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/__tests__/fixtures/action/all_fields_readme.output.crlf b/__tests__/fixtures/action/all_fields_readme.output.crlf index 44f3c510..20539304 100644 --- a/__tests__/fixtures/action/all_fields_readme.output.crlf +++ b/__tests__/fixtures/action/all_fields_readme.output.crlf @@ -1,32 +1,32 @@ - -## Description - -Default test - - - -## Inputs - -| name | description | required | default | -| --- | --- | --- | --- | -| `inputA` |

A description A

| `false` | `""` | -| `inputB` |

A description B

| `true` | `""` | -| `inputC` |

A description C

| `true` | `C` | -| `inputD` |

A description D

| `false` | `D` | -| `inputE` |

A description E

| `false` | `false` | - - - -## Outputs - -| name | description | -| --- | --- | -| `outputA` |

A description A

| -| `outputB` |

A description B

| - - - -## Runs - -This action is a `node12` action. - + +## Description + +Default test + + + +## Inputs + +| name | description | required | default | +| --- | --- | --- | --- | +| `inputA` |

A description A

| `false` | `""` | +| `inputB` |

A description B

| `true` | `""` | +| `inputC` |

A description C

| `true` | `C` | +| `inputD` |

A description D

| `false` | `D` | +| `inputE` |

A description E

| `false` | `false` | + + + +## Outputs + +| name | description | +| --- | --- | +| `outputA` |

A description A

| +| `outputB` |

A description B

| + + + +## Runs + +This action is a `node12` action. + \ No newline at end of file diff --git a/__tests__/fixtures/action/all_fields_readme_header.output b/__tests__/fixtures/action/all_fields_readme_header.output index acd53ed0..dbd28cde 100644 --- a/__tests__/fixtures/action/all_fields_readme_header.output +++ b/__tests__/fixtures/action/all_fields_readme_header.output @@ -44,6 +44,8 @@ Default test # Required: false # Default: false ``` + + diff --git a/__tests__/fixtures/action/default-with-header.output b/__tests__/fixtures/action/default-with-header.output index 4a5c8e82..e863447e 100644 --- a/__tests__/fixtures/action/default-with-header.output +++ b/__tests__/fixtures/action/default-with-header.output @@ -47,3 +47,4 @@ This action is a `node12` action. # Default: test ``` + diff --git a/__tests__/fixtures/workflow/default.output b/__tests__/fixtures/workflow/default.output index be77491c..36161973 100644 --- a/__tests__/fixtures/workflow/default.output +++ b/__tests__/fixtures/workflow/default.output @@ -3,9 +3,9 @@ ### Inputs | name | description | type | required | default | -| --- | --- | --- | --- | +| --- | --- | --- | --- | --- | | `inputA` | | `string` | `false` | `This is a default` | -| `inputB` |

This is a multiline description

| `number` | `true` | | +| `inputB` |

This is a multiline description

| `number` | `true` | `""` | ### Outputs @@ -14,3 +14,4 @@ | --- | --- | | `outputA` |

This is output A

| + diff --git a/__tests__/fixtures/workflow/workflow.yml b/__tests__/fixtures/workflow/workflow.yml index 9f4253b5..fcd789e8 100644 --- a/__tests__/fixtures/workflow/workflow.yml +++ b/__tests__/fixtures/workflow/workflow.yml @@ -18,7 +18,7 @@ on: type: string default: 'This is a default' required: false - InputB: + inputB: description: | This is a multiline description diff --git a/src/action-docs.ts b/src/action-docs.ts index 24375120..12a91f56 100644 --- a/src/action-docs.ts +++ b/src/action-docs.ts @@ -33,8 +33,8 @@ interface WorkflowTriggerEvent { types: string[]; branches: string[]; cron: string[]; - inputs: Record; - outputs: Record; + inputs: ActionInputsOutputs; + outputs: ActionInputsOutputs; } interface DefaultOptions { @@ -55,7 +55,7 @@ export const defaultOptions: DefaultOptions = { includeNameHeader: false, }; -type ActionInputsOutputs = Record; +type ActionInputsOutputs = Record; enum InputType { number, @@ -63,21 +63,42 @@ enum InputType { boolean, } -interface ActionInput { +enum InputOutputType { + actionInput, + workflowInput, + actionOutput, +} + +const inputOutputHeaders: Record = { + [InputOutputType.actionInput]: ["name", "description", "required", "default"], + [InputOutputType.workflowInput]: [ + "name", + "description", + "type", + "required", + "default", + ], + [InputOutputType.actionOutput]: ["name", "description"], +}; + +const inputOutputDefaults: Record = { + 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 { const tableData = getInputOutput(data, type); @@ -85,17 +106,7 @@ function createMdTable( const filler = Array(tableData.headers.length).fill("---"); 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) .map((x) => `| ${x.join(" | ")} |${getLineBreak(options.lineBreaks)}`) .join(""); @@ -111,19 +122,19 @@ function createMdCodeBlock( codeBlockArray.push(`- uses: ***PROJECT***@***VERSION***`); codeBlockArray.push(" with:"); - const inputs = getInputOutput(data, "input"); - for (const input of inputs.rows) { - const inputBlock = [`${input[0]}:`]; + const inputs = getInputOutput(data, InputOutputType.actionInput, false); + for (const row of inputs.rows) { + const inputBlock = [`${row[0]}:`]; inputBlock.push( - ...input[1] + ...row[1] .split(/(\r\n|\n|\r)/gm) .filter((l) => !["", "\r", "\n", "\r\n"].includes(l)) .map((l) => `# ${l}`), ); inputBlock.push(`#`); - inputBlock.push(`# Required: ${input[2].replace(/`/g, "")}`); - if (input[3]) { - inputBlock.push(`# Default: ${input[3]}`); + inputBlock.push(`# Required: ${row[2]}`); //.replace(/`/g, "") + if (row[3]) { + inputBlock.push(`# Default: ${row[3]}`); } codeBlockArray.push(...inputBlock.map((l) => ` ${l}`)); @@ -192,8 +203,8 @@ function generateActionDocs( return { header: generateHeader(yml, options), description: createMarkdownSection(options, yml.description, "Description"), - inputs: generateInputs(yml.inputs, options), - outputs: generateOutputs(yml, options), + inputs: generateInputs(yml.inputs, options, InputOutputType.actionInput), + outputs: generateOutputs(yml.outputs, options), runs: createMarkdownSection( options, // eslint-disable-next-line i18n-text/no-en @@ -210,10 +221,14 @@ function generateWorkflowDocs( ): Record { return { header: generateHeader(yml, options), - inputs: generateInputs(yml.on.workflow_call.inputs, options), - outputs: generateOutputs(yml, options), + inputs: generateInputs( + yml.on.workflow_call.inputs, + options, + InputOutputType.workflowInput, + ), + outputs: generateOutputs(yml.on.workflow_call.outputs, options), runs: "", - usage: generateUsage(yml, options), + usage: "", // todo }; } @@ -230,13 +245,21 @@ function generateHeader(yml: ActionYml, options: DefaultOptions): string { function generateInputs( data: ActionInputsOutputs, options: DefaultOptions, + type: InputOutputType, ): string { - const inputMdTable = createMdTable(data, options, "input"); + const inputMdTable = createMdTable(data, options, type); return createMarkdownSection(options, inputMdTable, "Inputs"); } -function generateOutputs(yml: ActionYml, options: DefaultOptions): string { - const outputMdTable = createMdTable(yml.outputs, options, "output"); +function generateOutputs( + data: ActionInputsOutputs, + options: DefaultOptions, +): string { + const outputMdTable = createMdTable( + data, + options, + InputOutputType.actionOutput, + ); return createMarkdownSection(options, outputMdTable, "Outputs"); } @@ -255,6 +278,7 @@ async function updateReadme( section: string, actionFile: string, ): Promise { + const lineBreak = getLineBreak(options.lineBreaks); if (section === "usage") { const readmeFileText = String(readFileSync(options.readmeFile, "utf-8")); const match = readmeFileText.match( @@ -269,15 +293,20 @@ async function updateReadme( `${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}${getLineBreak(options.lineBreaks)}${text - .trim() - .replace("***PROJECT***", match[1]) - .replace("***VERSION***", match[2])}${getLineBreak( - options.lineBreaks, - )}${commentExpression}`, + to: + commentExpression + + lineBreak + + processedText + + lineBreak + + commentExpression, }); } } else { @@ -289,9 +318,7 @@ async function updateReadme( await replaceInFile.replaceInFile({ files: options.readmeFile, from: regexp, - to: `${commentExpression}${getLineBreak( - options.lineBreaks, - )}${text.trim()}${getLineBreak(options.lineBreaks)}${commentExpression}`, + to: `${commentExpression}${lineBreak}${text.trim()}${lineBreak}${commentExpression}`, }); } } @@ -316,36 +343,58 @@ function createMarkdownHeader(options: DefaultOptions, header: string): string { return `${getToc(options.tocLevel)} ${header}${lineBreak}${lineBreak}`; } +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) { 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; 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); } } return { headers, rows };