generate-docs.ts
1 import * as fs from 'fs' 2 import * as os from 'os' 3 import * as path from 'path' 4 import * as yaml from 'js-yaml' 5 6 // 7 // SUMMARY 8 // 9 // This script rebuilds the usage section in the README.md to be consistent with the action.yml 10 11 function updateUsage( 12 actionReference: string, 13 actionYamlPath: string = 'action.yml', 14 readmePath: string = 'README.md', 15 startToken: string = '<!-- start usage -->', 16 endToken: string = '<!-- end usage -->' 17 ): void { 18 if (!actionReference) { 19 throw new Error('Parameter actionReference must not be empty') 20 } 21 22 // Load the action.yml 23 const actionYaml = yaml.safeLoad(fs.readFileSync(actionYamlPath).toString()) 24 25 // Load the README 26 const originalReadme = fs.readFileSync(readmePath).toString() 27 28 // Find the start token 29 const startTokenIndex = originalReadme.indexOf(startToken) 30 if (startTokenIndex < 0) { 31 throw new Error(`Start token '${startToken}' not found`) 32 } 33 34 // Find the end token 35 const endTokenIndex = originalReadme.indexOf(endToken) 36 if (endTokenIndex < 0) { 37 throw new Error(`End token '${endToken}' not found`) 38 } else if (endTokenIndex < startTokenIndex) { 39 throw new Error('Start token must appear before end token') 40 } 41 42 // Build the new README 43 const newReadme: string[] = [] 44 45 // Append the beginning 46 newReadme.push(originalReadme.substr(0, startTokenIndex + startToken.length)) 47 48 // Build the new usage section 49 newReadme.push('```yaml', `- uses: ${actionReference}`, ' with:') 50 const inputs = actionYaml.inputs 51 let firstInput = true 52 for (const key of Object.keys(inputs)) { 53 const input = inputs[key] 54 55 // Line break between inputs 56 if (!firstInput) { 57 newReadme.push('') 58 } 59 60 // Constrain the width of the description 61 const width = 80 62 let description = (input.description as string) 63 .trimRight() 64 .replace(/\r\n/g, '\n') // Convert CR to LF 65 .replace(/ +/g, ' ') // Squash consecutive spaces 66 .replace(/ \n/g, '\n') // Squash space followed by newline 67 while (description) { 68 // Longer than width? Find a space to break apart 69 let segment: string = description 70 if (description.length > width) { 71 segment = description.substr(0, width + 1) 72 while (!segment.endsWith(' ') && !segment.endsWith('\n') && segment) { 73 segment = segment.substr(0, segment.length - 1) 74 } 75 76 // Trimmed too much? 77 if (segment.length < width * 0.67) { 78 segment = description 79 } 80 } else { 81 segment = description 82 } 83 84 // Check for newline 85 const newlineIndex = segment.indexOf('\n') 86 if (newlineIndex >= 0) { 87 segment = segment.substr(0, newlineIndex + 1) 88 } 89 90 // Append segment 91 newReadme.push(` # ${segment}`.trimRight()) 92 93 // Remaining 94 description = description.substr(segment.length) 95 } 96 97 if (input.default !== undefined) { 98 // Append blank line if description had paragraphs 99 if ((input.description as string).trimRight().match(/\n[ ]*\r?\n/)) { 100 newReadme.push(` #`) 101 } 102 103 // Default 104 newReadme.push(` # Default: ${input.default}`) 105 } 106 107 // Input name 108 newReadme.push(` ${key}: ''`) 109 110 firstInput = false 111 } 112 113 newReadme.push('```') 114 115 // Append the end 116 newReadme.push(originalReadme.substr(endTokenIndex)) 117 118 // Write the new README 119 fs.writeFileSync(readmePath, newReadme.join(os.EOL)) 120 } 121 122 updateUsage( 123 'actions/checkout@v2', 124 path.join(__dirname, '..', '..', 'action.yml'), 125 path.join(__dirname, '..', '..', 'README.md') 126 )