feat: Add a script for bulk GitHub issue creation and simplify the client settings profile header UI.
This commit is contained in:
207
scripts/create_issues.py
Normal file
207
scripts/create_issues.py
Normal file
@@ -0,0 +1,207 @@
|
||||
#!/usr/bin/env python3
|
||||
import subprocess
|
||||
import os
|
||||
import re
|
||||
import argparse
|
||||
|
||||
# --- Configuration ---
|
||||
INPUT_FILE = "issues-to-create.md"
|
||||
DEFAULT_PROJECT_TITLE = None
|
||||
DEFAULT_MILESTONE = "Milestone 4"
|
||||
# ---
|
||||
|
||||
def parse_issues(content):
|
||||
"""Parse issue blocks from markdown content.
|
||||
|
||||
Each issue block starts with a '# Title' line, followed by an optional
|
||||
'Labels:' metadata line, then the body. Milestone is set globally, not per-issue.
|
||||
"""
|
||||
issue_blocks = re.split(r'\n(?=#\s)', content)
|
||||
issues = []
|
||||
|
||||
for block in issue_blocks:
|
||||
if not block.strip():
|
||||
continue
|
||||
|
||||
lines = block.strip().split('\n')
|
||||
|
||||
# Title: strip leading '#' characters and whitespace
|
||||
title = re.sub(r'^#+\s*', '', lines[0]).strip()
|
||||
|
||||
labels_line = ""
|
||||
body_start_index = len(lines) # default: no body
|
||||
|
||||
# Only 'Labels:' is parsed from the markdown; milestone is global
|
||||
for i, line in enumerate(lines[1:], start=1):
|
||||
stripped = line.strip()
|
||||
if stripped.lower().startswith('labels:'):
|
||||
labels_line = stripped.split(':', 1)[1].strip()
|
||||
elif stripped == "":
|
||||
continue # skip blank separator lines in the header
|
||||
else:
|
||||
body_start_index = i
|
||||
break
|
||||
|
||||
body = "\n".join(lines[body_start_index:]).strip()
|
||||
labels = [label.strip() for label in labels_line.split(',') if label.strip()]
|
||||
|
||||
if not title:
|
||||
print("⚠️ Skipping block with no title.")
|
||||
continue
|
||||
|
||||
issues.append({
|
||||
"title": title,
|
||||
"body": body,
|
||||
"labels": labels,
|
||||
})
|
||||
|
||||
return issues
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Bulk create GitHub issues from a markdown file.",
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Input file format (issues-to-create.md):
|
||||
-----------------------------------------
|
||||
# Issue Title One
|
||||
Labels: bug, enhancement
|
||||
|
||||
This is the body of the first issue.
|
||||
It can span multiple lines.
|
||||
|
||||
# Issue Title Two
|
||||
Labels: documentation
|
||||
|
||||
Body of the second issue.
|
||||
-----------------------------------------
|
||||
All issues share the same project and milestone, configured at the top of this script
|
||||
or passed via --project and --milestone flags.
|
||||
"""
|
||||
)
|
||||
parser.add_argument(
|
||||
"--file", "-f",
|
||||
default=INPUT_FILE,
|
||||
help=f"Path to the markdown input file (default: {INPUT_FILE})"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--project", "-p",
|
||||
default=DEFAULT_PROJECT_TITLE,
|
||||
help=f"GitHub Project title for all issues (default: {DEFAULT_PROJECT_TITLE})"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--milestone", "-m",
|
||||
default=DEFAULT_MILESTONE,
|
||||
help=f"Milestone to assign to all issues (default: {DEFAULT_MILESTONE})"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-project",
|
||||
action="store_true",
|
||||
help="Do not add issues to any project."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--no-milestone",
|
||||
action="store_true",
|
||||
help="Do not assign a milestone to any issue."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--repo", "-r",
|
||||
default=None,
|
||||
help="Target GitHub repo in OWNER/REPO format (uses gh default if not set)."
|
||||
)
|
||||
parser.add_argument(
|
||||
"--dry-run",
|
||||
action="store_true",
|
||||
help="Parse the file and print issues without creating them."
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
input_file = args.file
|
||||
project_title = args.project if not args.no_project else None
|
||||
milestone = args.milestone if not args.no_milestone else None
|
||||
|
||||
print("🚀 Bulk GitHub Issue Creator")
|
||||
print("=" * 40)
|
||||
print(f" Input file: {input_file}")
|
||||
print(f" Project: {project_title or '(none)'}")
|
||||
print(f" Milestone: {milestone or '(none)'}")
|
||||
if args.repo:
|
||||
print(f" Repo: {args.repo}")
|
||||
if args.dry_run:
|
||||
print(" Mode: DRY RUN (no issues will be created)")
|
||||
print("=" * 40)
|
||||
|
||||
# --- Preflight checks ---
|
||||
if subprocess.run(["which", "gh"], capture_output=True).returncode != 0:
|
||||
print("❌ ERROR: GitHub CLI ('gh') is not installed or not in PATH.")
|
||||
print(" Install it from: https://cli.github.com/")
|
||||
exit(1)
|
||||
|
||||
if not os.path.exists(input_file):
|
||||
print(f"❌ ERROR: Input file '{input_file}' not found.")
|
||||
exit(1)
|
||||
|
||||
print("✅ Preflight checks passed.\n")
|
||||
|
||||
# --- Parse ---
|
||||
print(f"📄 Parsing '{input_file}'...")
|
||||
with open(input_file, 'r') as f:
|
||||
content = f.read()
|
||||
|
||||
issues = parse_issues(content)
|
||||
|
||||
if not issues:
|
||||
print("⚠️ No issues found in the input file. Check the format.")
|
||||
exit(0)
|
||||
|
||||
print(f" Found {len(issues)} issue(s) to create.\n")
|
||||
|
||||
# --- Create ---
|
||||
success_count = 0
|
||||
fail_count = 0
|
||||
|
||||
for idx, issue in enumerate(issues, start=1):
|
||||
print(f"[{idx}/{len(issues)}] {issue['title']}")
|
||||
if issue['labels']:
|
||||
print(f" Labels: {', '.join(issue['labels'])}")
|
||||
print(f" Milestone: {milestone or '(none)'}")
|
||||
print(f" Project: {project_title or '(none)'}")
|
||||
|
||||
if args.dry_run:
|
||||
print(" (dry-run — skipping creation)\n")
|
||||
continue
|
||||
|
||||
command = ["gh", "issue", "create"]
|
||||
if args.repo:
|
||||
command.extend(["--repo", args.repo])
|
||||
command.extend(["--title", issue["title"]])
|
||||
command.extend(["--body", issue["body"] or " "]) # gh requires non-empty body
|
||||
|
||||
if project_title:
|
||||
command.extend(["--project", project_title])
|
||||
if milestone:
|
||||
command.extend(["--milestone", milestone])
|
||||
for label in issue["labels"]:
|
||||
command.extend(["--label", label])
|
||||
|
||||
try:
|
||||
result = subprocess.run(command, check=True, text=True, capture_output=True)
|
||||
print(f" ✅ Created: {result.stdout.strip()}")
|
||||
success_count += 1
|
||||
except subprocess.CalledProcessError as e:
|
||||
print(f" ❌ Failed: {e.stderr.strip()}")
|
||||
fail_count += 1
|
||||
|
||||
print()
|
||||
|
||||
# --- Summary ---
|
||||
print("=" * 40)
|
||||
if args.dry_run:
|
||||
print(f"🔍 Dry run complete. {len(issues)} issue(s) parsed, none created.")
|
||||
else:
|
||||
print(f"🎉 Done! {success_count} created, {fail_count} failed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user