mirror of
https://github.com/github/codeql.git
synced 2026-02-11 20:51:06 +01:00
This may be useful when working in "the other place", where the VSCode task may not be easily accessible. (Also, some people may just prefer to use a CLI interface.)
160 lines
4.3 KiB
Python
Executable File
160 lines
4.3 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
# Creates a change note and opens it in $EDITOR (or VSCode if the environment
|
|
# variable is not set) for editing.
|
|
|
|
# Expects to receive the following arguments:
|
|
# - What language the change note is for
|
|
# - Whether it's a query or library change (the string `src` or `lib`)
|
|
# - The name of the change note (in kebab-case)
|
|
# - The category of the change (see https://github.com/github/codeql/blob/main/docs/change-notes.md#change-categories).
|
|
|
|
# Alternatively, run without arguments for interactive mode.
|
|
|
|
# The change note will be created in the `{language}/ql/{subdir}/change-notes` directory, where `subdir` is either `src` or `lib`.
|
|
|
|
# The format of the change note filename is `{current_date}-{change_note_name}.md` with the date in
|
|
# the format `YYYY-MM-DD`.
|
|
|
|
import sys
|
|
import os
|
|
|
|
LANGUAGES = [
|
|
"actions",
|
|
"cpp",
|
|
"csharp",
|
|
"go",
|
|
"java",
|
|
"javascript",
|
|
"python",
|
|
"ruby",
|
|
"rust",
|
|
"swift",
|
|
]
|
|
|
|
SUBDIRS = {
|
|
"src": "query",
|
|
"lib": "library",
|
|
}
|
|
|
|
CATEGORIES_QUERY = [
|
|
"breaking",
|
|
"deprecated",
|
|
"newQuery",
|
|
"queryMetadata",
|
|
"majorAnalysis",
|
|
"minorAnalysis",
|
|
"fix",
|
|
]
|
|
|
|
CATEGORIES_LIBRARY = [
|
|
"breaking",
|
|
"deprecated",
|
|
"feature",
|
|
"majorAnalysis",
|
|
"minorAnalysis",
|
|
"fix",
|
|
]
|
|
|
|
|
|
def is_subsequence(needle: str, haystack: str) -> bool:
|
|
"""Check if needle is a subsequence of haystack (case-insensitive)."""
|
|
it = iter(haystack.lower())
|
|
return all(c in it for c in needle.lower())
|
|
|
|
|
|
def pick_option(prompt: str, options: list[str]) -> str:
|
|
"""Display options and let the user pick by subsequence match."""
|
|
print(f"\n{prompt}")
|
|
print(f" Options: {', '.join(options)}")
|
|
while True:
|
|
choice = input("Choice: ").strip()
|
|
if not choice:
|
|
continue
|
|
# Try exact match first
|
|
for o in options:
|
|
if o.lower() == choice.lower():
|
|
return o
|
|
# Try subsequence match
|
|
matches = [o for o in options if is_subsequence(choice, o)]
|
|
if len(matches) == 1:
|
|
return matches[0]
|
|
if len(matches) > 1:
|
|
print(f" Ambiguous: {', '.join(matches)}")
|
|
continue
|
|
print(f" No match for '{choice}'. Try again.")
|
|
|
|
|
|
def prompt_string(prompt: str) -> str:
|
|
"""Prompt the user for a string value."""
|
|
while True:
|
|
value = input(f"\n{prompt}: ").strip()
|
|
if value:
|
|
return value
|
|
print("Value cannot be empty.")
|
|
|
|
|
|
def interactive_mode() -> tuple[str, str, str, str]:
|
|
"""Run interactive mode to gather all required inputs."""
|
|
print("=== Create Change Note (Interactive Mode) ===")
|
|
|
|
language = pick_option("Select language:", LANGUAGES)
|
|
subdir = pick_option("Change type:", list(SUBDIRS.keys()))
|
|
|
|
change_note_name = prompt_string("Short name (kebab-case)")
|
|
|
|
if subdir == "src":
|
|
categories = CATEGORIES_QUERY
|
|
else:
|
|
categories = CATEGORIES_LIBRARY
|
|
change_category = pick_option("Select category:", categories)
|
|
|
|
return language, subdir, change_note_name, change_category
|
|
|
|
|
|
# Check if running in interactive mode (no arguments) or with arguments
|
|
if len(sys.argv) == 1:
|
|
language, subdir, change_note_name, change_category = interactive_mode()
|
|
elif len(sys.argv) == 5:
|
|
language = sys.argv[1]
|
|
subdir = sys.argv[2]
|
|
change_note_name = sys.argv[3]
|
|
change_category = sys.argv[4]
|
|
else:
|
|
print("Usage: create-change-note.py [language subdir name category]")
|
|
print(" Run without arguments for interactive mode.")
|
|
sys.exit(1)
|
|
|
|
# Find the root of the repository. The current script should be located in `misc/scripts`.
|
|
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
|
|
|
# Go to the repo root
|
|
os.chdir(root)
|
|
|
|
output_dir = f"{language}/ql/{subdir}/change-notes"
|
|
|
|
# Abort if the output directory doesn't exist
|
|
if not os.path.exists(output_dir):
|
|
print(f"Output directory {output_dir} does not exist")
|
|
sys.exit(1)
|
|
|
|
# Get the current date
|
|
import datetime
|
|
current_date = datetime.datetime.now().strftime("%Y-%m-%d")
|
|
|
|
# Create the change note file
|
|
change_note_file = f"{output_dir}/{current_date}-{change_note_name}.md"
|
|
|
|
change_note = f"""
|
|
---
|
|
category: {change_category}
|
|
---
|
|
* """.lstrip()
|
|
|
|
with open(change_note_file, "w") as f:
|
|
f.write(change_note)
|
|
|
|
editor = os.environ.get('EDITOR', 'code -r')
|
|
|
|
os.system(f"{editor} {change_note_file}")
|