Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file added .github/bitflight-devops.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added .github/hero.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
70 changes: 65 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
<div align="center">

<img src=".github/hero.png" alt="GitHub Action README Generator" width="800" />

</div>

<div align="center" >
<!-- start title -->

Expand All @@ -14,15 +20,43 @@
</div>
<!-- start description -->

📓 Effortlessly sync action.yml to README.md. Auto-generates inputs, outputs & usage docs, ensuring docs match code.
📓 The docs generator for GitHub Actions. Auto-syncs action.yml README.md with 8 sections including inputs, outputs, usage, badges & branding. Sensible defaults, highly configurable.

<!-- end description -->

📓 Keep your action's README.md up to date with the `title` and `description` from the [`action.yml`](./action.yml) file, while also automatically generating sections for the inputs, outputs, and a usage example for the action. Additionally the Action's usage example is updated to match the Action's current release.
## Quick Start

```sh
npx github-action-readme-generator
```

That's it. Run this in your GitHub Action repository and your README.md is updated.

## Features

| | Feature | Description |
| :----------------: | ------------------- | ------------------------------------------------------------ |
| :white_check_mark: | **Inputs Table** | Auto-generates markdown table from `action.yml` inputs |
| :white_check_mark: | **Outputs Table** | Auto-generates markdown table from `action.yml` outputs |
| :white_check_mark: | **Usage Example** | Creates ready-to-copy YAML workflow snippet |
| :white_check_mark: | **Auto-Versioning** | Updates `uses: owner/repo@v1.2.3` on every release |
| :white_check_mark: | **GitHub Badges** | Adds release, commit, issues, and download badges |
| :white_check_mark: | **SVG Branding** | Generates icon from action.yml branding (100+ icons) |
| :white_check_mark: | **Easy Setup** | Add section markers to README, configure via `.ghadocs.json` |
| :white_check_mark: | **Dual Mode** | Use as CLI (`npx`) or GitHub Action in workflows |

## How It Works

This tool uses markdown comments as section markers in your README:

This is both a CLI tool and GitHub Action that will read the details from a GitHub Action's [`action.yml`](./action.yml) file. Configuration can be provided through a [`.ghadocs.json`](./.ghadocs.json) file stored in the root directory of the Action's repository, via the command line when using the CLI, or through the `with:` section of this Action.
```markdown
<!-- start inputs -->
<!-- end inputs -->
```

**_HOW_** 📝 This tool uses markdown comments like this `<!-- start section --><!-- stop section -->` as delimiting tokens within your README.md file to determine where to place the generated content. You can find an example README template with all fields filled-in in the [`README.example.md`](./README.example.md) file.\*\*\*
Run the generator, and content between these markers is automatically updated from your `action.yml`. See [`README.example.md`](./README.example.md) for a complete template.

**Works as both CLI and GitHub Action** - configure via [`.ghadocs.json`](./.ghadocs.json), command line args, or the Action's `with:` section.

## CLI Usage

Expand Down Expand Up @@ -120,14 +154,30 @@ This configuration will automatically regenerate your README whenever `action.ym
```

<!-- start contents -->

## Table of Contents

- [Quick Start](#quick-start)
- [Features](#features)
- [How It Works](#how-it-works)
- [CLI Usage](#cli-usage)
- [Stand Alone Usage - if you have a Docker Action](#stand-alone-usage-if-you-have-a-docker-action)
- [Install with Yarn or NPM as a dev dependency](#install-with-yarn-or-npm-as-a-dev-dependency)
- [Add a script to your project file](#add-a-script-to-your-project-file)
- [Using as a Pre-commit Hook](#using-as-a-pre-commit-hook)
- [Configuration](#configuration)
- [Example `.ghadocs.json` with all possible values](#example-ghadocsjson-with-all-possible-values)
- [Usage](#usage)
- [Inputs](#inputs)

<!-- end contents -->

## Usage

<!-- start usage -->

```yaml
- uses: bitflight-devops/github-action-readme-generator@v1.8.9
- uses: bitflight-devops/github-action-readme-generator@v1.8.10
with:
# Description: The absolute or relative path to the `action.yml` file to read in
# from.
Expand Down Expand Up @@ -260,3 +310,13 @@ This configuration will automatically regenerate your README whenever `action.ym

<!-- start [.github/ghadocs/examples/] -->
<!-- end [.github/ghadocs/examples/] -->

---

<div align="center">

<img src=".github/bitflight-devops.png" alt="Bitflight DevOps" width="400" />

**Built by [Bitflight DevOps](https://github.com/bitflight-devops)**

</div>
2 changes: 1 addition & 1 deletion action.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
name: GitHub Action's Readme Generator
author: Jamie Nelson <jamie@bitflight.io>
description: 📓 Effortlessly sync action.yml to README.md. Auto-generates inputs, outputs & usage docs, ensuring docs match code.
description: 📓 The docs generator for GitHub Actions. Auto-syncs action.yml README.md with 8 sections including inputs, outputs, usage, badges & branding. Sensible defaults, highly configurable.

branding:
icon: book-open
Expand Down
17 changes: 10 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,20 @@
"name": "github-action-readme-generator",
"displayName": "bitflight-devops/github-action-readme-generator",
"version": "1.8.10",
"description": "This is a CLI tool and GitHub Action that reads in the details from a \nGitHub Action's `action.yml` file and updates the `README.md` file\nwith the `name`, `description`, `usage`, `inputs`, `outputs`, and\nexamples of the action.\nConfiguration can be provided via a `.ghadocs.json` file stored in the\nroot directory of the Action's repository, via the command line when\nusing the cli, or via the `with:` section of this Action.\n\n\nThis tool uses markdown comments as delimiting tokens within the `README.md`\nfile to determine where to place the generated content.\n\n[`README.example.md`](README.example.md) example with all fields filled in, and no other free-form content.",
"description": "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.",
"keywords": [
"github-actions",
"actions",
"github",
"node20",
"documentation",
"github-actions",
"generator",
"readme-generator",
"action-metadata",
"markdown-generator"
"action-yml",
"markdown-generator",
"github-action-docs",
"auto-docs",
"devops",
"cli",
"inputs-outputs",
"usage-generator"
],
"homepage": "https://github.com/bitflight-devops/github-action-readme-generator#readme",
"bugs": {
Expand Down
8 changes: 8 additions & 0 deletions src/readme-editor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,14 @@ export default class ReadmeEditor {
}
}

/**
* Gets the current README content.
* @returns {string} - The README file content.
*/
getReadmeContent(): string {
return this.fileContent;
}

/**
* Gets the indexes of the start and end tokens for a given section.
* @param {string} token - The section token.
Expand Down
4 changes: 4 additions & 0 deletions src/sections/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type Inputs from '../inputs.js';
import LogTask from '../logtask/index.js';
import updateBadges from './update-badges.js';
import updateBranding from './update-branding.js';
import updateContents from './update-contents.js';
import updateDescription from './update-description.js';
import updateInputs from './update-inputs.js';
import updateOutputs from './update-outputs.js';
Expand Down Expand Up @@ -52,6 +53,9 @@ export default async function updateSection(
case 'outputs': {
return updateOutputs(section, inputs);
}
case 'contents': {
return updateContents(section, inputs);
}
default: {
log.debug(`unknown section found <!-- start ${section} -->. No updates were made.`);
return {};
Expand Down
113 changes: 113 additions & 0 deletions src/sections/update-contents.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
/**
* This TypeScript code exports a function named 'updateContents' which generates
* a table of contents from the README.md headers.
* @param {ReadmeSection} sectionToken - The sectionToken representing the section of the README to update.
* @param {Inputs} inputs - The Inputs class instance.
*/
import { ReadmeSection } from '../constants.js';
import type Inputs from '../inputs.js';
import LogTask from '../logtask/index.js';

/**
* Converts a header text to a GitHub-compatible anchor link.
* @param {string} text - The header text to convert.
* @returns {string} The anchor link.
*/
function headerToAnchor(text: string): string {
return text
.toLowerCase()
.replace(/[^\w\s-]/g, '') // Remove special characters except hyphens
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <unicorn/prefer-string-replace-all> reported by reviewdog 🐶
Prefer String#replaceAll() over String#replace().

Suggested change
.replace(/[^\w\s-]/g, '') // Remove special characters except hyphens
.replaceAll(/[^\w\s-]/g, '') // Remove special characters except hyphens

.replace(/\s+/g, '-') // Replace spaces with hyphens
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <unicorn/prefer-string-replace-all> reported by reviewdog 🐶
Prefer String#replaceAll() over String#replace().

Suggested change
.replace(/\s+/g, '-') // Replace spaces with hyphens
.replaceAll(/\s+/g, '-') // Replace spaces with hyphens

.replace(/-+/g, '-') // Collapse multiple hyphens
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <unicorn/prefer-string-replace-all> reported by reviewdog 🐶
Prefer String#replaceAll() over String#replace().

Suggested change
.replace(/-+/g, '-') // Collapse multiple hyphens
.replaceAll(/-+/g, '-') // Collapse multiple hyphens

.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <unicorn/prefer-string-replace-all> reported by reviewdog 🐶
Prefer String#replaceAll() over String#replace().

Suggested change
.replace(/^-|-$/g, ''); // Remove leading/trailing hyphens
.replaceAll(/^-|-$/g, ''); // Remove leading/trailing hyphens

}

/**
* Extracts headers from markdown content, excluding those in code blocks.
* @param {string} content - The markdown content.
* @returns {Array<{level: number, text: string}>} Array of header objects.
*/
function extractHeaders(content: string): Array<{ level: number; text: string }> {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/array-type> reported by reviewdog 🐶
Array type using 'Array' is forbidden. Use 'T[]' instead.

Suggested change
function extractHeaders(content: string): Array<{ level: number; text: string }> {
function extractHeaders(content: string): { level: number; text: string }[] {

const headers: Array<{ level: number; text: string }> = [];
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <@typescript-eslint/array-type> reported by reviewdog 🐶
Array type using 'Array' is forbidden. Use 'T[]' instead.

Suggested change
const headers: Array<{ level: number; text: string }> = [];
const headers: { level: number; text: string }[] = [];

const lines = content.split('\n');
let inCodeBlock = false;

for (const line of lines) {
// Track code block state
if (line.trim().startsWith('```')) {
inCodeBlock = !inCodeBlock;
continue;
}

// Skip if inside code block
if (inCodeBlock) {
continue;
}

// Match markdown headers (## Header)
const headerMatch = /^(#{2,6})\s+(.+)$/.exec(line);
if (headerMatch) {
const level = headerMatch[1].length;
let text = headerMatch[2].trim();

// Remove inline images and other markdown formatting from header text
text = text.replace(/<img[^>]*>/g, '').trim();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <unicorn/prefer-string-replace-all> reported by reviewdog 🐶
Prefer String#replaceAll() over String#replace().

Suggested change
text = text.replace(/<img[^>]*>/g, '').trim();
text = text.replaceAll(/<img[^>]*>/g, '').trim();

// Remove markdown links but keep the text
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <unicorn/prefer-string-replace-all> reported by reviewdog 🐶
Prefer String#replaceAll() over String#replace().

Suggested change
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
text = text.replaceAll(/\[([^\]]+)\]\([^)]+\)/g, '$1');

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [eslint] <optimize-regex/optimize-regex> reported by reviewdog 🐶
/[([^\]]+)]([^)]+)/g can be optimized to /[([^\]]+)]([^)]+)/g

Suggested change
text = text.replace(/\[([^\]]+)\]\([^)]+\)/g, '$1');
text = text.replace(/\[([^\]]+)]\([^)]+\)/g, '$1');


if (text) {
headers.push({ level, text });
}
}
}

return headers;
}

export default function updateContents(
sectionToken: ReadmeSection,
inputs: Inputs,
): Record<string, string> {
const log = new LogTask(sectionToken);
log.start();

const content: string[] = [];
const readmeContent = inputs.readmeEditor.getReadmeContent();

// Extract headers from README
const headers = extractHeaders(readmeContent);

// Filter out the title (h1) and contents section itself
const tocHeaders = headers.filter(
(h) => h.level >= 2 && !h.text.toLowerCase().includes('contents'),
);

if (tocHeaders.length === 0) {
log.info('No headers found for table of contents');
const ret: Record<string, string> = {};
ret[sectionToken] = '';
return ret;
}
Comment on lines +85 to +90
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Clear the TOC section when no headers are found.
Right now you return early without updating the README, which leaves stale TOC content in place.

🐛 Proposed fix
   if (tocHeaders.length === 0) {
     log.info('No headers found for table of contents');
+    inputs.readmeEditor.updateSection(sectionToken, '');
     const ret: Record<string, string> = {};
     ret[sectionToken] = '';
     return ret;
   }
🤖 Prompt for AI Agents
In `@src/sections/update-contents.ts` around lines 85 - 90, The function currently
returns early when tocHeaders.length === 0 which leaves the README's TOC
unchanged; instead, set ret[sectionToken] = '' and do not return early—allow the
function to follow the same code path that writes/updates sections to the README
(i.e., use the existing README update/write logic used when headers exist) so
the empty TOC is persisted; remove the early return and ensure the existing
write/update call is invoked with the ret map so the stale TOC is cleared.


log.info(`Generating table of contents with ${tocHeaders.length} entries`);

// Find minimum header level for proper indentation
const minLevel = Math.min(...tocHeaders.map((h) => h.level));

// Generate TOC entries
content.push('## Table of Contents');
content.push('');
Comment on lines +98 to +99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚫 [eslint] <unicorn/prefer-single-call> reported by reviewdog 🐶
Do not call Array#push() multiple times.

Suggested change
content.push('## Table of Contents');
content.push('');
content.push('## Table of Contents', '');


for (const header of tocHeaders) {
const indent = ' '.repeat(header.level - minLevel);
const anchor = headerToAnchor(header.text);
content.push(`${indent}- [${header.text}](#${anchor})`);
}
Comment on lines +97 to +105
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Handle duplicate headings to avoid broken TOC links.
GitHub disambiguates duplicate anchors with numeric suffixes; current logic produces duplicate anchors.

🐛 Proposed fix
   // Generate TOC entries
   content.push('## Table of Contents');
   content.push('');
 
+  const slugCounts = new Map<string, number>();
   for (const header of tocHeaders) {
     const indent = '  '.repeat(header.level - minLevel);
-    const anchor = headerToAnchor(header.text);
-    content.push(`${indent}- [${header.text}](#${anchor})`);
+    const baseAnchor = headerToAnchor(header.text);
+    const count = slugCounts.get(baseAnchor) ?? 0;
+    slugCounts.set(baseAnchor, count + 1);
+    const anchor = count === 0 ? baseAnchor : `${baseAnchor}-${count}`;
+    content.push(`${indent}- [${header.text}](#${anchor})`);
   }
🤖 Prompt for AI Agents
In `@src/sections/update-contents.ts` around lines 97 - 105, The TOC generation
must disambiguate duplicate anchors like GitHub; update the loop that builds
anchors for tocHeaders to track occurrences (e.g., a Map keyed by the base
anchor from headerToAnchor) and for each header compute base =
headerToAnchor(header.text), increment its count, and if count > 1 append a
numeric suffix of `-${count - 1}` to the anchor so subsequent duplicates become
anchor-1, anchor-2, etc., then push the TOC line using that unique anchor; apply
this logic where tocHeaders, headerToAnchor, minLevel and content are used to
build the TOC.


inputs.readmeEditor.updateSection(sectionToken, content);
log.success();

const ret: Record<string, string> = {};
ret[sectionToken] = content.join('\n');
return ret;
}
Loading