Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 107 additions & 5 deletions misc/scripts/create-change-note.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)")
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The prompt_string function prompts for a "Short name (kebab-case)" but does not validate that the input is actually in kebab-case format. This could lead to incorrectly formatted filenames. Consider adding validation to ensure the input matches the expected kebab-case pattern (lowercase letters, numbers, and hyphens only, not starting or ending with a hyphen).

Copilot uses AI. Check for mistakes.

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]
Comment on lines +119 to +122
Copy link

Copilot AI Jan 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

When running in argument mode (len(sys.argv) == 5), there is no validation of the provided arguments. The language, subdir, and change_category values are not verified against the valid options defined in LANGUAGES, SUBDIRS, and CATEGORIES_* constants. Invalid values would only fail later when trying to create the output directory path. Consider adding validation to provide clearer error messages when invalid arguments are provided.

Suggested change
language = sys.argv[1]
subdir = sys.argv[2]
change_note_name = sys.argv[3]
change_category = sys.argv[4]
language, subdir, change_note_name, change_category = sys.argv[1:5]
# Validate language
if language not in LANGUAGES:
print(f"Invalid language: {language}")
print(f"Valid languages: {', '.join(LANGUAGES)}")
sys.exit(1)
# Validate subdir
if subdir not in SUBDIRS:
print(f"Invalid subdir: {subdir}")
print(f"Valid subdirs: {', '.join(SUBDIRS.keys())}")
sys.exit(1)
# Validate category based on subdir (query vs library)
if subdir == "src":
valid_categories = CATEGORIES_QUERY
else:
valid_categories = CATEGORIES_LIBRARY
if change_category not in valid_categories:
print(f"Invalid category: {change_category}")
print(f"Valid categories for {subdir}: {', '.join(valid_categories)}")
sys.exit(1)
# Basic validation for change note name (match interactive behavior)
if not change_note_name.strip():
print("Change note name must be non-empty.")
sys.exit(1)

Copilot uses AI. Check for mistakes.
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__))))
Expand Down
Loading