Skip to content

Commit de1d183

Browse files
committed
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.)
1 parent c666fc7 commit de1d183

File tree

1 file changed

+107
-5
lines changed

1 file changed

+107
-5
lines changed

misc/scripts/create-change-note.py

Lines changed: 107 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99
# - The name of the change note (in kebab-case)
1010
# - The category of the change (see https://github.com/github/codeql/blob/main/docs/change-notes.md#change-categories).
1111

12+
# Alternatively, run without arguments for interactive mode.
13+
1214
# The change note will be created in the `{language}/ql/{subdir}/change-notes` directory, where `subdir` is either `src` or `lib`.
1315

1416
# The format of the change note filename is `{current_date}-{change_note_name}.md` with the date in
@@ -17,11 +19,111 @@
1719
import sys
1820
import os
1921

20-
# Read the given arguments
21-
language = sys.argv[1]
22-
subdir = sys.argv[2]
23-
change_note_name = sys.argv[3]
24-
change_category = sys.argv[4]
22+
LANGUAGES = [
23+
"actions",
24+
"cpp",
25+
"csharp",
26+
"go",
27+
"java",
28+
"javascript",
29+
"python",
30+
"ruby",
31+
"rust",
32+
"swift",
33+
]
34+
35+
SUBDIRS = {
36+
"src": "query",
37+
"lib": "library",
38+
}
39+
40+
CATEGORIES_QUERY = [
41+
"breaking",
42+
"deprecated",
43+
"newQuery",
44+
"queryMetadata",
45+
"majorAnalysis",
46+
"minorAnalysis",
47+
"fix",
48+
]
49+
50+
CATEGORIES_LIBRARY = [
51+
"breaking",
52+
"deprecated",
53+
"feature",
54+
"majorAnalysis",
55+
"minorAnalysis",
56+
"fix",
57+
]
58+
59+
60+
def is_subsequence(needle: str, haystack: str) -> bool:
61+
"""Check if needle is a subsequence of haystack (case-insensitive)."""
62+
it = iter(haystack.lower())
63+
return all(c in it for c in needle.lower())
64+
65+
66+
def pick_option(prompt: str, options: list[str]) -> str:
67+
"""Display options and let the user pick by subsequence match."""
68+
print(f"\n{prompt}")
69+
print(f" Options: {', '.join(options)}")
70+
while True:
71+
choice = input("Choice: ").strip()
72+
if not choice:
73+
continue
74+
# Try exact match first
75+
for o in options:
76+
if o.lower() == choice.lower():
77+
return o
78+
# Try subsequence match
79+
matches = [o for o in options if is_subsequence(choice, o)]
80+
if len(matches) == 1:
81+
return matches[0]
82+
if len(matches) > 1:
83+
print(f" Ambiguous: {', '.join(matches)}")
84+
continue
85+
print(f" No match for '{choice}'. Try again.")
86+
87+
88+
def prompt_string(prompt: str) -> str:
89+
"""Prompt the user for a string value."""
90+
while True:
91+
value = input(f"\n{prompt}: ").strip()
92+
if value:
93+
return value
94+
print("Value cannot be empty.")
95+
96+
97+
def interactive_mode() -> tuple[str, str, str, str]:
98+
"""Run interactive mode to gather all required inputs."""
99+
print("=== Create Change Note (Interactive Mode) ===")
100+
101+
language = pick_option("Select language:", LANGUAGES)
102+
subdir = pick_option("Change type:", list(SUBDIRS.keys()))
103+
104+
change_note_name = prompt_string("Short name (kebab-case)")
105+
106+
if subdir == "src":
107+
categories = CATEGORIES_QUERY
108+
else:
109+
categories = CATEGORIES_LIBRARY
110+
change_category = pick_option("Select category:", categories)
111+
112+
return language, subdir, change_note_name, change_category
113+
114+
115+
# Check if running in interactive mode (no arguments) or with arguments
116+
if len(sys.argv) == 1:
117+
language, subdir, change_note_name, change_category = interactive_mode()
118+
elif len(sys.argv) == 5:
119+
language = sys.argv[1]
120+
subdir = sys.argv[2]
121+
change_note_name = sys.argv[3]
122+
change_category = sys.argv[4]
123+
else:
124+
print("Usage: create-change-note.py [language subdir name category]")
125+
print(" Run without arguments for interactive mode.")
126+
sys.exit(1)
25127

26128
# Find the root of the repository. The current script should be located in `misc/scripts`.
27129
root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))

0 commit comments

Comments
 (0)