Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

2503 doc autogen #2539

Closed
wants to merge 33 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
1702d98
doc autogen
owenwahlgren Jan 8, 2025
ae30533
update on PR
owenwahlgren Jan 8, 2025
b5c7370
update script path
owenwahlgren Jan 8, 2025
c27db7d
update yml
owenwahlgren Jan 8, 2025
f499ed1
checkout branch before building
owenwahlgren Jan 8, 2025
b3dd17e
fix md workflow
owenwahlgren Jan 8, 2025
3b37113
fix md workflow
owenwahlgren Jan 8, 2025
ec29931
please
owenwahlgren Jan 8, 2025
14225b6
fix
owenwahlgren Jan 8, 2025
2e11bf1
chore: Update MD file [skip ci]
invalid-email-address Jan 8, 2025
ec37e5f
Bump google.golang.org/api from 0.214.0 to 0.215.0 (#2529)
dependabot[bot] Jan 9, 2025
d701f9c
Bump google.golang.org/protobuf from 1.36.1 to 1.36.2 (#2540)
dependabot[bot] Jan 9, 2025
9491aa9
update output path
owenwahlgren Jan 13, 2025
7d8407c
format fix
owenwahlgren Jan 13, 2025
ec20dbf
chore: Update MD file [skip ci]
invalid-email-address Jan 13, 2025
dae01ed
improve evm proposer vm activation error handling (#2534)
felipemadero Jan 10, 2025
5f9c834
support contract owners for custom subnet evm binary (#2536)
felipemadero Jan 10, 2025
54a0dca
Bump github.com/aws/aws-sdk-go-v2 from 1.32.7 to 1.32.8 (#2546)
dependabot[bot] Jan 10, 2025
ea23a45
Bump github.com/spf13/afero from 1.11.0 to 1.12.0 (#2547)
dependabot[bot] Jan 10, 2025
7da695c
chore: Update MD file [skip ci]
invalid-email-address Jan 13, 2025
aea1c99
test
owenwahlgren Jan 14, 2025
6be6b7b
fix: commit action
owenwahlgren Jan 23, 2025
d0c3936
Bump google.golang.org/api from 0.215.0 to 0.216.0 (#2550)
dependabot[bot] Jan 13, 2025
3122675
Bump github.com/aws/aws-sdk-go-v2/config from 1.28.7 to 1.28.10 (#2551)
dependabot[bot] Jan 13, 2025
9428410
Improve change weight (#2545)
felipemadero Jan 14, 2025
de11ecc
refactor: using slices.Contains to simplify the code (#2552)
dashangcun Jan 14, 2025
54813cd
chore: Update MD file [skip ci]
invalid-email-address Jan 15, 2025
a6c81b8
Use local machine to join as validator on any L1 (#2548)
sukantoraymond Jan 17, 2025
2fd91b1
Bump github.com/aws/aws-sdk-go-v2/service/ec2 from 1.198.1 to 1.200.0…
dependabot[bot] Jan 20, 2025
6454a79
Bump github.com/docker/docker (#2554)
dependabot[bot] Jan 20, 2025
595cfa4
chore: Update MD file [skip ci]
invalid-email-address Jan 21, 2025
179a819
try another way
owenwahlgren Jan 23, 2025
fffa819
chore: Update MD file [skip ci]
invalid-email-address Jan 8, 2025
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
262 changes: 262 additions & 0 deletions .github/scripts/cli_scraper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,262 @@
import subprocess
import json
import re

def replace_angle_brackets(text):
"""
Replace any text within angle brackets with backticks to prevent Markdown rendering issues.
Example: "<snapshotName>" becomes "`snapshotName`"
"""
return re.sub(r'<(.*?)>', r'`\1`', text)

def generate_anchor_id(cli_tool, command_chain):
"""
Generate a unique anchor ID based on the entire command chain.

Example:
cli_tool = "avalanche"
command_chain = ["blockchain", "create"]
-> anchor_id = "avalanche-blockchain-create"
"""
full_chain = [cli_tool] + command_chain
anchor_str = '-'.join(full_chain)
# Remove invalid characters for anchors, and lowercase
anchor_str = re.sub(r'[^\w\-]', '', anchor_str.lower())
return anchor_str

def get_command_structure(cli_tool, command_chain=None, max_depth=10, current_depth=0, processed_commands=None):
"""
Recursively get a dictionary of commands, subcommands, flags (with descriptions),
and descriptions for a given CLI tool by parsing its --help output.
"""
if command_chain is None:
command_chain = []
if processed_commands is None:
processed_commands = {}

current_command = [cli_tool] + command_chain
command_key = ' '.join(current_command)

# Prevent re-processing of the same command
if command_key in processed_commands:
return processed_commands[command_key]

# Prevent going too deep
if current_depth > max_depth:
return None

command_structure = {
"description": "",
"flags": [],
"subcommands": {}
}

print(f"Processing command: {' '.join(current_command)}")

# Run `<command> --help`
try:
help_output = subprocess.run(
current_command + ["--help"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
timeout=10,
stdin=subprocess.DEVNULL
)
output = help_output.stdout
# Some CLIs return a non-zero exit code but still provide help text, so no strict check here
except subprocess.TimeoutExpired:
print(f"[ERROR] Timeout expired for command: {' '.join(current_command)}")
return None
except Exception as e:
print(f"[ERROR] Exception while running: {' '.join(current_command)} -> {e}")
return None

if not output.strip():
print(f"[WARNING] No output for command: {' '.join(current_command)}")
return None

# --- Extract Description ------------------------------------------------------
description_match = re.search(r"(?s)^\s*(.*?)\n\s*Usage:", output)
if description_match:
description = description_match.group(1).strip()
command_structure['description'] = replace_angle_brackets(description)

# --- Extract Flags (including Global Flags) -----------------------------------
flags = []
# "Flags:" section
flags_match = re.search(r"(?sm)^Flags:\n(.*?)(?:\n\n|^\S|\Z)", output)
if flags_match:
flags_text = flags_match.group(1)
flags.extend(re.findall(
r"^\s+(-{1,2}[^\s,]+(?:,\s*-{1,2}[^\s,]+)*)\s+(.*)$",
flags_text,
re.MULTILINE
))

# "Global Flags:" section
global_flags_match = re.search(r"(?sm)^Global Flags:\n(.*?)(?:\n\n|^\S|\Z)", output)
if global_flags_match:
global_flags_text = global_flags_match.group(1)
flags.extend(re.findall(
r"^\s+(-{1,2}[^\s,]+(?:,\s*-{1,2}[^\s,]+)*)\s+(.*)$",
global_flags_text,
re.MULTILINE
))

if flags:
command_structure["flags"] = [
{
"flag": f[0].strip(),
"description": replace_angle_brackets(f[1].strip())
}
for f in flags
]

# --- Extract Subcommands ------------------------------------------------------
subcommands_match = re.search(
r"(?sm)(?:^Available Commands?:\n|^Commands?:\n)(.*?)(?:\n\n|^\S|\Z)",
output
)
if subcommands_match:
subcommands_text = subcommands_match.group(1)
# Lines like: " create Create a new something"
subcommand_lines = re.findall(r"^\s+([^\s]+)\s+(.*)$", subcommands_text, re.MULTILINE)

for subcmd, sub_desc in sorted(set(subcommand_lines)):
sub_desc_clean = replace_angle_brackets(sub_desc.strip())
sub_structure = get_command_structure(
cli_tool,
command_chain + [subcmd],
max_depth,
current_depth + 1,
processed_commands
)
if sub_structure is not None:
if not sub_structure.get('description'):
sub_structure['description'] = sub_desc_clean
command_structure["subcommands"][subcmd] = sub_structure
else:
command_structure["subcommands"][subcmd] = {
"description": sub_desc_clean,
"flags": [],
"subcommands": {}
}

processed_commands[command_key] = command_structure
return command_structure

def generate_markdown(cli_structure, cli_tool, file_path):
"""
Generate a Markdown file from the CLI structure JSON object in a developer-friendly format.
No top-level subcommand bullet list.
"""
# Define a set of known type keywords. Adjust as needed.
known_types = {
"string", "bool", "int", "uint", "float", "duration",
"strings", "uint16", "uint32", "uint64", "int16", "int32", "int64",
"float32", "float64"
}

def write_section(structure, file, command_chain=None):
if command_chain is None:
command_chain = []

# If at root level, do not print a heading or bullet list, just go straight
# to recursing through subcommands.
if command_chain:
# Determine heading level (but max out at H6)
heading_level = min(1 + len(command_chain), 6)

# Build heading text:
if len(command_chain) == 1:
heading_text = f"{cli_tool} {command_chain[0]}"
else:
heading_text = ' '.join(command_chain[1:])

# Insert a single anchor before writing the heading
anchor = generate_anchor_id(cli_tool, command_chain)
file.write(f'<a id="{anchor}"></a>\n')
file.write(f"{'#' * heading_level} {heading_text}\n\n")

# Write description
if structure.get('description'):
file.write(f"{structure['description']}\n\n")

# Write usage
full_command = f"{cli_tool} {' '.join(command_chain)}"
file.write("**Usage:**\n")
file.write(f"```bash\n{full_command} [subcommand] [flags]\n```\n\n")

# Subcommands index
subcommands = structure.get('subcommands', {})
if subcommands:
file.write("**Subcommands:**\n\n")
for subcmd in sorted(subcommands.keys()):
sub_desc = subcommands[subcmd].get('description', '')
sub_anchor = generate_anchor_id(cli_tool, command_chain + [subcmd])
file.write(f"- [`{subcmd}`](#{sub_anchor}): {sub_desc}\n")
file.write("\n")
else:
subcommands = structure.get('subcommands', {})

# Flags (only if we have a command chain)
if command_chain and structure.get('flags'):
file.write("**Flags:**\n\n")
flag_lines = []
for flag_dict in structure['flags']:
flag_names = flag_dict['flag']
description = flag_dict['description'].strip()

# Attempt to parse a recognized "type" from the first word.
desc_parts = description.split(None, 1) # Split once on whitespace
if len(desc_parts) == 2:
first_word, rest = desc_parts
# Check if the first word is in known_types
if first_word.lower() in known_types:
flag_type = first_word
flag_desc = rest
else:
flag_type = ""
flag_desc = description
else:
flag_type = ""
flag_desc = description

if flag_type:
flag_line = f"{flag_names} {flag_type}"
else:
flag_line = flag_names

flag_lines.append((flag_line, flag_desc))

# Determine formatting width
max_len = max(len(fl[0]) for fl in flag_lines) if flag_lines else 0
file.write("```bash\n")
for fl, fd in flag_lines:
file.write(f"{fl.ljust(max_len)} {fd}\n")
file.write("```\n\n")

# Recurse into subcommands
subcommands = structure.get('subcommands', {})
for subcmd in sorted(subcommands.keys()):
write_section(subcommands[subcmd], file, command_chain + [subcmd])

with open(file_path, "w", encoding="utf-8") as f:
write_section(cli_structure, f)

def main():
cli_tool = "avalanche" # Adjust if needed
max_depth = 10

# Build the nested command structure
cli_structure = get_command_structure(cli_tool, max_depth=max_depth)
if cli_structure:
# Generate Markdown
generate_markdown(cli_structure, cli_tool, "cmd/commands.md")
print("Markdown documentation saved to cmd/commands.md")
else:
print("[ERROR] Failed to retrieve CLI structure")

if __name__ == "__main__":
main()
60 changes: 60 additions & 0 deletions .github/workflows/update-markdown.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
name: Update Markdown

on:
push:
branches:
- main
pull_request:
branches:
- main

jobs:
update-md:
runs-on: ubuntu-latest

steps:
- name: Check out the repo
uses: actions/checkout@v3
with:
ref: ${{ github.head_ref }}
fetch-depth: 0

- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.21'

- name: Build avalanche-cli
run: |
chmod +x ./scripts/build.sh
./scripts/build.sh

- name: Add avalanche to PATH
run: echo "${{ github.workspace }}/bin" >> $GITHUB_PATH

- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: "3.9"

- name: Install dependencies
run: |
pip install --upgrade pip
# If you need additional dependencies, install them here
# pip install -r requirements.txt

- name: Generate MD
id: generate_md
run: |
python .github/scripts/cli_scraper.py

- name: Commit files
uses: ChromeQ/[email protected]
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
files: |
cmd/commands.md
commit-message: "chore: Update MD file [skip ci]"
ref: ${{ github.head_ref }}

Loading