diff --git a/misc/scripts/create-change-note.py b/misc/scripts/create-change-note.py index 1de42126c90c..acaf0794c867 100755 --- a/misc/scripts/create-change-note.py +++ b/misc/scripts/create-change-note.py @@ -9,6 +9,8 @@ # - 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 @@ -17,11 +19,111 @@ import sys import os -# Read the given arguments -language = sys.argv[1] -subdir = sys.argv[2] -change_note_name = sys.argv[3] -change_category = sys.argv[4] +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__))))