Photo by Markus Spiske on Unsplash
Automating Codebase Visualization with Go
automating documentation—readme.md file.
While learning and improving my problem-solving skills(DSA), I got frustrated by the thought of manually updating the README.md file of my dsa in golang repo to reflect all the learning (topic-wise) I’ve been doing over the past few weeks. Then it hit me—what’s the point of learning about automation if I’m not applying it where needed? So, I took a few minutes and wrote a script audited by an LLM to handle the updates automatically.
Not too long ago, I worked on a similar automation project where my latest articles on Hashnode and Dev.to were automatically updated on my GitHub profile using a cron job. This saved me the hassle of manually updating my articles every time I published something new—a cool and time-saving solution.
And now that my GitHub profile has been 10% automated, I no longer need a portfolio page!
Problem Statement
I wanted a way to represent my learning journey visually about solved Data Structures and algorithms (DSA) problems in a tree-like view. The idea was simple: instead of searching through multiple files, I (or anyone reading my repository) could quickly skim the README and locate an implementation at a glance.
Last year, I did this manually, but despite actively working on DSA this year, I just never found the time to keep the README updated. This wasn’t just a one-time issue—it’s part of a larger challenge for me. I’ve never been a fan of writing documentation; in fact, I often find it tedious. So, instead of forcing myself to manually maintain updates, I figured—why not automate it?
That’s what led me here. 🚀
Solution Statement
To solve this problem, I built an automated system using Go that dynamically generates and updates the README file with a structured tree representation of my codebase.
The solution involves:
Building a Go script that recursively scans my project directory and constructs a tree-like visualization of the file structure.
Formatting the output to clearly differentiate between directories and files, making it easy to navigate.
Automating updates using a GitHub Action that runs the script whenever there’s a change in the repository (on push or pull requests).
Committing and pushing changes back to the repository ensures that the README is always up to date without manual intervention.
With this approach, I no longer need to update the file structure manually, and anyone exploring my DSA repository can instantly see a well-organized view of the codebase. This automation not only saves time but also removes the burden of documentation, allowing me to focus on solving more DSA problems efficiently.
Final Scope
GitHub Workflow for Automation
To ensure that my README file maintains the latest file structure, I integrated a GitHub Actions workflow. This workflow automates the execution of my Go script and commits the updated tree structure to my repository whenever it changes.
How the GitHub Workflow Works
Triggers: The workflow runs on every push and pull request to the
main
branch.Checkout Code: It pulls the latest version of the repository.
Set Up Go Environment: It installs and configures Go (version 1.20 in this case).
Run the Go Script: The script scans the project directory and updates the
README.md
file with the latest tree structure.Commit and Push Changes: If the
README.md
file has been updated, the workflow automatically commits and pushes the changes back to the repository.
Step 1: Writing the Go Script
First, we need to create a Go script that scans our project directory and generates a tree-like structure.
1.1 Create a generate
package
Inside your project, create a folder named generate/
and add a generate.go
file:
generate/generate.go
package generate
import (
"flag" // For command-line flag parsing
"fmt" // For formatted I/O operations
"os" // For file and directory operations
"path/filepath" // For manipulating file paths
"strings" // For string manipulation
)
var (
outputFileName string // Name of the output file (default: README.md)
maxDepth int // Maximum depth of directory traversal (0 means no limit)
ignoredDirs = map[string]bool{ // Directories to ignore while generating the tree
".git": true,
"node_modules": true,
}
)
// TreeNode represents a node in the directory tree structure.
type TreeNode struct {
Name string // Name of the file or directory
Children []*TreeNode // List of child nodes (for directories)
IsDir bool // Indicates whether the node is a directory
}
// GenerateTree recursively walks through the directory and builds the tree structure.
func GenerateTree(root string, currentDepth int) (*TreeNode, error) {
info, err := os.Stat(root) // Get file/directory information
if err != nil {
return nil, err // Return error if unable to retrieve info
}
// Create a new TreeNode for the current directory/file
rootNode := &TreeNode{
Name: info.Name(),
IsDir: info.IsDir(),
}
// If it's a file or depth limit is reached, return the node
if !info.IsDir() || (maxDepth > 0 && currentDepth >= maxDepth) {
return rootNode, nil
}
// Read the contents of the directory
entries, err := os.ReadDir(root)
if err != nil {
return nil, err
}
// Iterate over directory entries and recursively generate tree nodes
for _, entry := range entries {
if ignoredDirs[entry.Name()] { // Skip ignored directories
continue
}
childPath := filepath.Join(root, entry.Name()) // Construct full path of the child
childNode, err := GenerateTree(childPath, currentDepth+1) // Recursively generate child node
if err != nil {
return nil, err
}
rootNode.Children = append(rootNode.Children, childNode) // Append child node to parent's children list
}
return rootNode, nil
}
// RenderTree recursively generates the directory tree as a formatted string.
func RenderTree(node *TreeNode, prefix string) string {
var sb strings.Builder // String builder for efficient string concatenation
// Append appropriate symbol (📂 for directories, 📄 for files)
if node.IsDir {
sb.WriteString(fmt.Sprintf("%s📂 %s\n", prefix, node.Name))
} else {
sb.WriteString(fmt.Sprintf("%s📄 %s\n", prefix, node.Name))
}
// Iterate over child nodes and render them with indentation
for i, child := range node.Children {
newPrefix := prefix + "│ " // Default prefix for children
if i == len(node.Children)-1 {
newPrefix = prefix + " " // Adjust last child prefix to avoid an extra line
}
sb.WriteString(RenderTree(child, newPrefix)) // Recursively render child nodes
}
return sb.String()
}
// UpdateReadme writes the generated tree structure to the specified output file.
func UpdateReadme(tree string) error {
// Format the tree as a markdown code block and write to file
readmeContent := fmt.Sprintf("# Codebase Structure\n\n```\n%s```\n", tree)
return os.WriteFile(outputFileName, []byte(readmeContent), 0644)
}
// Generate is the main entry point that handles flag parsing and initiates tree generation.
func Generate() error {
// Define command-line flags
flag.StringVar(&outputFileName, "output", "README.md", "Output file name")
flag.IntVar(&maxDepth, "depth", 0, "Maximum depth of the tree (0 for no limit)")
flag.Parse() // Parse command-line arguments
rootDir := "." // Default root directory (current directory)
if flag.NArg() > 0 {
rootDir = flag.Arg(0) // If an argument is provided, use it as the root directory
}
// Generate the directory tree
tree, err := GenerateTree(rootDir, 0)
if err != nil {
return fmt.Errorf("Error generating tree: %v", err)
}
// Render the tree as a string
treeString := RenderTree(tree, "")
// Write the tree structure to the output file
if err := UpdateReadme(treeString); err != nil {
return fmt.Errorf("Error updating README.md: %v", err)
}
// Print success message
fmt.Println("Tree structure successfully written to", outputFileName)
return nil
}
Step 2: Connecting the Go Script to main.go
Now, let's create a main.go
file to call the Generate() function from our script.
main.go
package main
import (
"log"
"generate"
)
func main() {
if err := generate.Generate(); err != nil {
log.Fatalf("Error: %v", err)
}
}
Step 3: Automating Updates with GitHub Actions
3.1 Create a GitHub Actions Workflow
Now that we have our Go script ready, let’s automate its execution using GitHub Actions.
Inside your project, create the directory .github/workflows/
and add a new YAML file:
.github/workflows/generate-codebase-tree.yml
name: Generate Codebase Tree
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
generate-tree:
runs-on: ubuntu-latest
steps:
# Step 1: Check out the code
- name: Checkout code
uses: actions/checkout@v3
# Step 2: Set up Go environment
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: "1.23"
# Step 3: Run the Go script
- name: Run generate tree script for codebase
run: go run main.go
# Step 4: Commit and push changes (if README.md was updated)
- name: Commit and push changes
run: |
git config --global user.name "github-actions[bot]"
git config --global user.email "github-actions[bot]@users.noreply.github.com"
if [ "$(git status --porcelain)" ]; then
git add README.md
git commit -m "Update codebase tree structure [skip ci]"
git push
fi
Step 4: How the Workflow Works
✅ Triggering the Workflow
- The workflow runs automatically when you push or merge changes into the
main
branch.
✅ Execution Steps
Checkout Code – Pulls the latest version of the repository.
Set Up Go – Installs and configures Go (version 1.23).
Run Script – Executes the Go script to generate the project tree.
Commit & Push – Updates
README.md
and commits the changes if needed.
Step 5: Testing the Automation
- Push Changes to GitHub
git add .
git commit -m "Added automation for codebase structure"
git push origin main
Check GitHub Actions
Navigate to your GitHub repository
Click on Actions
Select Generate Codebase Tree to see the workflow logs
View the Updated
README.md
Once the workflow completes successfully, open your
README.md
file.It should now contain the latest project structure.
Final Thoughts
With this automation in place, your README.md
will always reflect your latest project structure without manual updates. This improves documentation and helps you navigate your File structure efficiently.
🔹 No more manual updates!
🔹 Keep your repository structured and organized!
🔹 Let GitHub Actions do the work for you! 🚀
I Will be working next on an automated documentation in readme which would be provided as a plugin or not, till then :). stay tuned.