Misc: Add CLI interface to create-change-note.py

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.)
This commit is contained in:
Taus
2026-01-12 15:32:03 +00:00
parent c666fc71ca
commit de1d183ca6

View File

@@ -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__))))