diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 7c2fa4b..5b9113c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -61,10 +61,16 @@ Must be one of the following: * **feat**: new features or capabilities that enhance the user's experience. * **fix**: bug fixes that enhance the user's experience. +* **security**: CVE fixes, vulnerability mitigations, VEX updates. * **refactor**: a code changes that neither fixes a bug nor adds a feature. * **docs**: updates or improvements to documentation. * **test**: additions or corrections to tests. -* **chore**: updates that don't fit into other types. +* **chore**: other changes that still affect users in some way and don't fit into other types (e.g. dependency or base image updates). +* **internal**: developer-only changes that users never notice: CI, templates, tooling. Excluded from the changelog and release notes. + +Rule of thumb: if a user of the module can notice the change in any way — it is **chore** +(or a more specific type); if the change is visible only to developers of this +repository — it is **internal**. #### Scope diff --git a/cliff.toml b/cliff.toml index 8d191b2..92d76ad 100644 --- a/cliff.toml +++ b/cliff.toml @@ -10,10 +10,18 @@ trim = false # Emit the module changelog yaml: all four categories are always present, # empty ones are rendered as []. body = """ +{%- set breaking = commits | filter(attribute="group", value="breaking") -%} {%- set feats = commits | filter(attribute="group", value="features") -%} {%- set fixes = commits | filter(attribute="group", value="fixes") -%} {%- set security = commits | filter(attribute="group", value="security") -%} {%- set chores = commits | filter(attribute="group", value="chore") -%} +breaking: +{%- if breaking | length == 0 %} [] +{%- else %} +{%- for c in breaking %} + - "{{ c.message | split(pat="\n") | first | trim | replace(from="\\", to="\\\\") | replace(from='"', to='\\"') }}" +{%- endfor %} +{%- endif %} features: {%- if feats | length == 0 %} [] {%- else %} @@ -59,9 +67,12 @@ protect_breaking_commits = true tag_pattern = "^v[0-9]+\\.[0-9]+\\.[0-9]+$" commit_parsers = [ - # noise: merge commits and generated-changelog commits + # noise: merge commits and internal (developer-only) changes: ci, templates, tooling { message = "^Merge", skip = true }, - { message = "^internal\\(changelog\\)", skip = true }, + { message = "^internal", skip = true }, + # breaking changes: "type!: ..." or BREAKING CHANGE footer + { message = "^[a-z]+(\\([^)]*\\))?!:", group = "breaking" }, + { body = "(?s).*BREAKING CHANGE.*", group = "breaking" }, # security first: explicit type or CVE/vex mentions { message = "^security", group = "security" }, { message = "(?i)\\bcve-\\d{4}-\\d+", group = "security" }, diff --git a/hack/chlog.py b/hack/chlog.py index fd22de7..c5dbec6 100755 --- a/hack/chlog.py +++ b/hack/chlog.py @@ -29,6 +29,7 @@ "fixes": {"en": "Bug Fixes", "ru": "Исправления"}, "security": {"en": "Security Fixes", "ru": "Исправления безопасности"}, "chore": {"en": "Chore", "ru": "Прочее"}, + "breaking": {"en": "Breaking Changes", "ru": "Несовместимые изменения"}, } @@ -279,6 +280,7 @@ def release_notes_handler(changelog_dir: str, lang: str, output: str = "") -> in headers = { "title": TRANSLATIONS["title"][lang], + "breaking": TRANSLATIONS["breaking"][lang], "features": TRANSLATIONS["features"][lang], "fixes": TRANSLATIONS["fixes"][lang], "security": TRANSLATIONS["security"][lang], @@ -292,6 +294,7 @@ def release_notes_handler(changelog_dir: str, lang: str, output: str = "") -> in "version": os.path.basename(filename).replace( ".ru.yaml" if lang == "ru" else ".yaml", "" ), + "breaking": "", "features": "", "fixes": "", "security": "", @@ -301,10 +304,15 @@ def release_notes_handler(changelog_dir: str, lang: str, output: str = "") -> in for category, changes in changelog.items(): logging.debug("processing category: %s", category) if changes: + if lang == "en" and isinstance(changes, list): + changes = [ + to_past_tense(c) if isinstance(c, str) else c + for c in changes + ] entries[category] = yaml_to_markdown(changes) markdown += f"\n## {entries['version']}\n" - for category in ["features", "fixes", "security", "chore"]: + for category in ["breaking", "features", "fixes", "security", "chore"]: if entries[category]: markdown += f"\n### {headers[category]}\n{entries[category]}" @@ -317,6 +325,46 @@ def release_notes_handler(changelog_dir: str, lang: str, output: str = "") -> in return 0 +# Leading verbs converted to past tense when rendering release notes. +# Changelog yaml files keep the original (imperative) wording. +PAST_TENSE = { + "add": "added", + "allow": "allowed", + "apply": "applied", + "bump": "bumped", + "change": "changed", + "create": "created", + "delete": "deleted", + "disable": "disabled", + "drop": "dropped", + "enable": "enabled", + "enforce": "enforced", + "fix": "fixed", + "forbid": "forbidden", + "improve": "improved", + "introduce": "introduced", + "mark": "marked", + "move": "moved", + "remove": "removed", + "rename": "renamed", + "replace": "replaced", + "resolve": "resolved", + "rework": "reworked", + "update": "updated", + "upgrade": "upgraded", +} + + +def to_past_tense(entry: str) -> str: + words = entry.split(maxsplit=1) + if not words: + return entry + verb = PAST_TENSE.get(words[0].lower()) + if not verb: + return entry + return verb + (" " + words[1] if len(words) > 1 else "") + + def openwebui_translate_handler(file: str, lang: str, output: str = "") -> int: api_token = os.environ.get("GPT_API_TOKEN", None) if not api_token: @@ -339,7 +387,13 @@ def openwebui_translate_handler(file: str, lang: str, output: str = "") -> int: target = "Russian" if lang == "ru" else "English" - text = f"Translate the following YAML to {target}. Preserve YAML formatting.\n\n```{content}```" + text = ( + f"Translate the following YAML to {target}. Preserve YAML formatting.\n" + "Do not translate YAML keys. Keep technical terms, product names, CVE ids\n" + "and file paths in English. Describe changes as already done: use past\n" + "tense passive forms (e.g. \u00abисправлено\u00bb, \u00abдобавлена\u00bb, \u00abобновлены\u00bb).\n" + f"\n```{content}```" + ) data = { "model": "gpt-4o",