Skip to content
Open
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
72 changes: 48 additions & 24 deletions .github/scripts/generate_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@
GITHUB_TOKEN - GitHub personal access token or Actions token
REPOS - Comma-separated list of repositories in "owner/repo" format
(e.g. "mattermost/mattermost,mattermost/enterprise")
MILESTONE - Milestone title (e.g. "v10.6")
VERSION - Version label for the changelog entry (e.g. "10.6")
MILESTONE - Milestone title (e.g. "v11.7.0")
VERSION - Version label for the changelog entry (e.g. "v11.7.0")
RELEASE_DATE - (optional) Release day date (e.g. "2026-05-15"). Defaults to today.
BLOG_POST_URL - (optional) Blog post URL for the Improvements section.
Auto-constructed from VERSION if not provided.
ANTHROPIC_API_KEY - (optional) If set, release notes are polished by Claude
before being written to the changelog.
"""
Expand All @@ -28,26 +31,41 @@
"Accept": "application/vnd.github.v3+json",
}

# Shared timeout for all GitHub API requests (seconds).
# Prevents the workflow from hanging indefinitely if the API stalls.
API_TIMEOUT = 30

SYSTEM_PROMPT = """You are an expert technical writer and copyeditor for Mattermost software release notes. Your task is to transform raw, unstructured release notes from pull requests into a clean, categorized, and grammatically correct changelog entry that matches Mattermost's established changelog format exactly.

Here are your instructions:

1. **Section structure:** Use `###` for top-level sections and `####` for subsections. Only include sections that have relevant content — do not output empty sections.
1. **Section structure:** Use `###` for top-level sections and `####` for subsections. Only include sections that have relevant content — do not output empty sections. Do NOT add horizontal rules or line separators between sections.

Top-level sections and their subsections:
Top-level sections and their subsections, in this order:

- `### Upgrade Impact` — for changes that affect upgrading, with subsections as applicable:
- `#### Database Schema Changes` — new tables, columns, indexes, or migrations
- `#### config.json` — new or changed configuration settings; group by plan (e.g. "Changes to Enterprise plans")
- `#### Compatibility` — browser, OS, or minimum version requirement changes
- `### Improvements` — for new features and enhancements. Begin this section with the line `See [this blog post](BLOG_POST_URL) on the highlights in our latest release.` (use the exact placeholder `BLOG_POST_URL` — it will be replaced automatically). Then add subsections as applicable:
- `#### User Interface` — UI/UX changes, new visual features, pre-packaged plugin version updates
- `#### Administration` — System Console features, mmctl additions, logging, support packet changes
- `#### Database Schema Changes` — schema migrations such as new tables, new columns, changed columns, or new indexes. Example items: "Added a new ``Watermarks`` table.", "Added a new column ``DeleteAt`` to the ``ChannelMembers`` table."
- `#### config.json` — new or changed configuration settings. Use this exact block format for each plan grouping:

New setting options were added to ``config.json``. Below is a list of the additions and their default values on install. The settings can be modified in ``config.json``, or the System Console when available.

- **Changes to Enterprise Advanced plan:**
- Under ``ExperimentalSettings`` in ``config.json``, added ``EnableWatermark`` configuration setting to add watermarking toggle in the server.

Adapt the plan name (e.g. "Changes to All plans:", "Changes to Enterprise plan:", "Changes to Enterprise Advanced plan:") and list each setting change as a bullet under the appropriate plan heading.
- `#### Compatibility` — minimum version requirement changes for browsers, OS, or clients. Example: "Updated minimum Edge and Chrome versions to 146+."
- `### Improvements` — for new features and enhancements only. Do NOT place items beginning with "Fixed..." here — those belong in Bug Fixes. Begin this section with the line `See [this blog post](BLOG_POST_URL) on the highlights in our latest release.` (use the exact placeholder `BLOG_POST_URL` — it will be replaced automatically). Then add subsections as applicable:
- `#### User Interface` — UI/UX changes and new visual features. Pre-packaged plugin version updates go at the TOP of this subsection, before other items.
- `#### Plugins/Integrations` — plugin and integration improvements (use as a separate subsection when there are enough items to warrant it)
- `#### Administration` — System Console features, logging, support packet changes
- `#### mmctl` — mmctl command additions or changes (use as a separate subsection when there are enough items to warrant it)
- `#### Performance` — performance improvements
- `### Bug Fixes` — corrections to defects
- `### Bug Fixes` — corrections to defects. All items starting with "Fixed an issue..." go here, even if the raw note appeared in an Improvements context.
- `### API Changes` — API additions, changes, or deprecations
- `### WebSocket Event Changes` — new or changed WebSocket events, if applicable
- `### Audit Log Event Changes` — new or changed audit log events
- `### Go Version` — Go version updates
Comment thread
amyblais marked this conversation as resolved.
- `### Open Source Components` — open source component additions or removals, if applicable
- `### Security` — security-related fixes not already covered under Bug Fixes

2. **Sentence patterns:** Follow these conventions consistently:
Expand All @@ -65,15 +83,15 @@
- File names (e.g., ``config.json``)
- Feature flags (e.g., ``MM_FEATUREFLAGS_CJKSEARCH``)

4. **Markdown formatting:** Use `- ` bullet points for individual items within sections. Ensure correct and clean Markdown syntax throughout.
4. **Markdown formatting:** Use `- ` bullet points for individual items within sections. Ensure correct and clean Markdown syntax throughout. Do not insert horizontal rules (`---`) or any other separators between sections.

5. **License requirements:** When a feature requires a specific Mattermost license, note it inline at the end of the bullet point (e.g., "Requires Enterprise Advanced license" or "Requires Enterprise license").

7. **Proofreading:** Correct any typos, grammatical errors, awkward phrasing, or inconsistencies. Aim for clear, concise, and professional language.
6. **Proofreading:** Correct any typos, grammatical errors, awkward phrasing, or inconsistencies. Aim for clear, concise, and professional language.

8. **Tone:** Maintain a neutral, informative, and professional tone consistent with technical documentation.
7. **Tone:** Maintain a neutral, informative, and professional tone consistent with technical documentation.

9. **Focus:** Output only the section content (headings and bullet points). Do not include the release version header line or any introductory or concluding remarks from yourself."""
8. **Focus:** Output only the section content (headings and bullet points). Do not include the release version header line or any introductory or concluding remarks from yourself."""


def get_milestone_number(repo: str, title: str) -> int | None:
Expand All @@ -89,7 +107,7 @@ def get_milestone_number(repo: str, title: str) -> int | None:
"sort": "due_on", # sort by due date
"direction": "desc", # most recently due first, so active milestones are found quickly
}
resp = requests.get(url, headers=HEADERS, params=params, timeout=30)
resp = requests.get(url, headers=HEADERS, params=params, timeout=API_TIMEOUT)
resp.raise_for_status()
milestones = resp.json()
Comment thread
amyblais marked this conversation as resolved.
if not milestones:
Expand Down Expand Up @@ -118,7 +136,7 @@ def get_merged_prs(repo: str, milestone_number: int) -> list:
"per_page": 100,
"page": page,
}
resp = requests.get(url, headers=HEADERS, params=params)
resp = requests.get(url, headers=HEADERS, params=params, timeout=API_TIMEOUT)
resp.raise_for_status()
items = resp.json()
if not items:
Expand All @@ -131,7 +149,6 @@ def get_merged_prs(repo: str, milestone_number: int) -> list:
return prs



def extract_release_notes(body: str) -> list[str] | None:
"""
Extract release note text from a PR body.
Expand Down Expand Up @@ -210,15 +227,22 @@ def polish_with_ai(raw_notes: list[str]) -> str:
return response.content[0].text.strip()


def prepend_to_changelog(entry: str, changelog_path: str = "CHANGELOG.md") -> None:
"""Prepend a new version entry to the changelog file."""
def insert_changelog_entry(entry: str, changelog_path: str = "CHANGELOG.md") -> None:
"""Insert a new version entry into the changelog after the static file header block."""
existing = ""
if os.path.exists(changelog_path):
with open(changelog_path, "r") as f:
existing = f.read()

# If the file starts with a top-level heading, insert the new entry below it
if existing.startswith("# "):
# The v11 changelog file has a static header block ending with the platform scope note.
# New entries are inserted immediately after this block so the header is preserved.
HEADER_END_MARKER = "may not represent all affected configurations.\n```"

if HEADER_END_MARKER in existing:
idx = existing.index(HEADER_END_MARKER) + len(HEADER_END_MARKER)
new_content = existing[:idx] + "\n\n\n" + entry + existing[idx:]
elif existing.startswith("# "):
# Fallback: insert after the first top-level heading line
parts = existing.split("\n", 2)
new_content = parts[0] + "\n\n" + entry + ("\n" + parts[2] if len(parts) > 2 else "")
else:
Expand Down Expand Up @@ -284,10 +308,10 @@ def main():
entry += "_No release notes for this version._\n"

changelog_path = os.environ.get("CHANGELOG_PATH", "CHANGELOG.md")
prepend_to_changelog(entry, changelog_path)
insert_changelog_entry(entry, changelog_path)

prs_with_notes = total_prs - len(no_notes_prs)
print(f"✅ CHANGELOG.md updated with notes from {prs_with_notes} PR(s) across {len(REPOS)} repo(s)")
print(f"✅ Changelog updated with notes from {prs_with_notes} PR(s) across {len(REPOS)} repo(s)")

if no_notes_prs:
print(f"\n⚠️ {len(no_notes_prs)} PR(s) had no release notes (marked NONE or section missing):")
Expand Down
Loading