Conflict-Free Changelog Management System (#1194)

* Added development plan

* Added and updated docs

* Added scripts

* Removed old YAML file

* Added some QoL changes to the scripts

* Added a way to provide a specific version to compile entries into

* Updated dry run code

* Fixed the compile script to not delete entries with different indentation

* Added support for multi-line entries

* Added trimming for multi-line

* Added multi-line support to New-ChangelogEntry.ps1

* Added a script for syncing the main CHANGELOG with that one for VS Code

* Updated the changelog compilation script to remove empty sections

* Added support for any MCP server in this repo

* Moved a couple of docs

* Updated other docs

* Updated new-command.md

* Updated behavior to ask for a CHANGELOG path instead of defaulting to the Azure MCP Server. Added an optional filename parameter in New-ChangelogEntry.ps1. Updated schema. Updated documentation.

* Updated Compile-Changelog.ps1 to accept files any name and using either the .yml or .yaml extension

* Applied GitHub Copilot feedback for PowerShell scripts

* Applied PR feedback to PS scripts

* Added sample file

* Refactored indentation and trimming logic

* Refactored a couple of scripts to use logic found in `eng/common/scripts/ChangeLog-Operations.ps1`

* Updated test file and added examples for other servers

* Added logic for extracting PR number from the commit that added an entry file
This commit is contained in:
vcolin7 authored and GitHub committed 2025-12-11 17:37:52 -08:00
1 parent c8e7e258ef
commit bd1ed664ed
12 files changed
+2390 -6

No files matched your search

+2 -2
View File
@@ -182,7 +182,7 @@ dotnet build
- Documentation: Update `/servers/Azure.Mcp.Server/docs/azmcp-commands.md` and add test prompts to `/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md`
- Tool validation: Run `ToolDescriptionEvaluator` for command descriptions (target: top 3 ranking, ≥0.4 confidence)
- Spelling check: `.\eng\common\spelling\Invoke-Cspell.ps1`
- Changelog: Update `CHANGELOG.md` with your changes
- Changelog: Create changelog entry YAML file if the change is a new feature, bug fix, or breaking change. See `docs/changelog-entries.md` for instructions. Always use the `-ChangelogPath` parameter (e.g., `servers/Azure.Mcp.Server/CHANGELOG.md` or `servers/Fabric.Mcp.Server/CHANGELOG.md`).
- One tool per PR: Submit single toolsets for faster review cycles
## Architecture and Project Structure
@@ -687,7 +687,7 @@ When adding new commands:
1. **Update `/servers/Azure.Mcp.Server/docs/azmcp-commands.md`** with new command details
2. **Add test prompts to `/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md`** (maintain alphabetical order)
3. **Update toolset README.md** with new functionality
4. **Update CHANGELOG.md** with changes
4. **Create changelog entry** if user-facing or critical change. See `docs/changelog-entries.md` for instructions. Always use the `-ChangelogPath` parameter (e.g., `servers/Azure.Mcp.Server/CHANGELOG.md` or `servers/Fabric.Mcp.Server/CHANGELOG.md`).
5. **Add CODEOWNERS entry** for new toolset
### Spelling and Content Validation
+20 -3
View File
@@ -18,9 +18,11 @@ If you are contributing significant changes, or if the issue is already assigned
- [Adding a New Command](#adding-a-new-command)
- [Testing](#testing)
- [Unit Tests](#unit-tests)
- [Cancellation plumbing](#cancellation-plumbing)
- [End-to-end Tests](#end-to-end-tests)
- [Testing Local Build with VS Code](#testing-local-build-with-vs-code)
- [Build the Server](#build-the-server)
- [Run the Azure MCP server in HTTP mode](#run-the-azure-mcp-server-in-http-mode)
- [Configure mcp.json](#configure-mcpjson)
- [Server Modes](#server-modes)
- [Start from IDE](#start-from-ide)
@@ -91,6 +93,7 @@ If you are contributing significant changes, or if the issue is already assigned
- `test-resources.bicep` - Infrastructure templates for testing
- `eng/` - Shared tools, templates, CLI helpers
- `docs/` - Central documentation and onboarding materials
## Development Workflow
### Development Process
@@ -115,6 +118,7 @@ If you are contributing significant changes, or if the issue is already assigned
> - **Incremental progress**: Get your first tool merged to establish baseline, then build upon it
>
> If you're planning to contribute multiple tools, please:
>
> 1. Submit your most important or representative tool as your first PR to establish the code patterns.
> 2. Use that baseline to inform your subsequent tool PRs.
@@ -140,9 +144,22 @@ If you are contributing significant changes, or if the issue is already assigned
- Add test prompts for the new command in [/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md](https://github.com/microsoft/mcp/blob/main/servers/Azure.Mcp.Server/docs/e2eTestPrompts.md)
- Update [README.md](https://github.com/microsoft/mcp/blob/main/README.md) to mention the new command
6. **Add CODEOWNERS entry** in [CODEOWNERS](https://github.com/microsoft/mcp/blob/main/.github/CODEOWNERS) [(example)](https://github.com/microsoft/mcp/commit/08f73efe826d5d47c0f93be5ed9e614740e82091)
6. **Create a changelog entry** (if your change is a new feature, bug fix, or breaking change):
- Use the generator script to create a changelog entry (see `docs/changelog-entries.md` for details):
```powershell
# Interactive mode (prompts for server)
./eng/scripts/New-ChangelogEntry.ps1
# Or with all parameters
./eng/scripts/New-ChangelogEntry.ps1 -ChangelogPath "servers/Azure.Mcp.Server/CHANGELOG.md" -Description <your-change-description> -Section <changelog-section> -PR <pr-number>
./eng/scripts/New-ChangelogEntry.ps1 -ChangelogPath "servers/Fabric.Mcp.Server/CHANGELOG.md" -Description <your-change-description> -Section <changelog-section> -PR <pr-number>
```
- Or manually create a YAML file in `servers/{ServerName}/changelog-entries/`
- Not every PR needs a changelog entry - skip for internal refactoring, test-only changes, or minor updates. If unsure, add to the "Other Changes" section or ask a maintainer.
7. **Add new tool to consolidated mode**:
7. **Add CODEOWNERS entry** in [CODEOWNERS](https://github.com/microsoft/mcp/blob/main/.github/CODEOWNERS) [(example)](https://github.com/microsoft/mcp/commit/08f73efe826d5d47c0f93be5ed9e614740e82091)
8. **Add new tool to consolidated mode**:
- Open `core/Azure.Mcp.Core/src/Areas/Server/Resources/consolidated-tools.json` file, where the tool grouping definition is stored for consolidated mode. In Agent mode, add it to the chat as context.
- Paste the follow prompt for Copilot to generate the change to add the new tool:
```txt
@@ -157,7 +174,7 @@ If you are contributing significant changes, or if the issue is already assigned
```
- Commit the change.
8. **Create Pull Request**:
9. **Create Pull Request**:
- Reference the issue you created
- Include tests in the `/tests` folder
- Ensure all tests pass
+321
View File
@@ -0,0 +1,321 @@
# Changelog Entries
## Overview
Each server's `/changelog-entries` directory contains individual YAML entry files that are compiled into the main `CHANGELOG.md` during the release process. This document describes how to create, manage, and compile said entries files.
**Why individual files?**
Using separate YAML files for each changelog entry **eliminates merge conflicts** on `CHANGELOG.md` when multiple contributors work on the same branch simultaneously, allowing for smoother collaboration on a repository with numerous active contributors.
## Quick Start
For most contributors, here's all you need:
```powershell
# Create a changelog entry for your PR
./eng/scripts/New-ChangelogEntry.ps1 `
-ChangelogPath "servers/Azure.Mcp.Server/CHANGELOG.md" `
-Description "Your change description here" `
-Section "Features Added"
```
That's it! The PR number will be **auto-detected** from the git commit when the changelog is compiled during release. Commit the YAML file with your code changes.
> **Tip:** Not every PR needs a changelog entry. Skip it for internal refactoring, test-only changes, or minor cleanup.
## Table of Contents
- [Changelog Entries](#changelog-entries)
- [Overview](#overview)
- [Quick Start](#quick-start)
- [Table of Contents](#table-of-contents)
- [Creating a Changelog Entry](#creating-a-changelog-entry)
- [When to Create an Entry](#when-to-create-an-entry)
- [File Format](#file-format)
- [Required Fields](#required-fields)
- [Optional Fields](#optional-fields)
- [Using the Generator Script](#using-the-generator-script)
- [Interactive mode](#interactive-mode)
- [One-line](#one-line)
- [With a subsection](#with-a-subsection)
- [With a multi-line entry](#with-a-multi-line-entry)
- [Manual Creation](#manual-creation)
- [Simple entry](#simple-entry)
- [Multi-line entry](#multi-line-entry)
- [Using a subsection](#using-a-subsection)
- [Multiple-entries](#multiple-entries)
- [Preparing a New Release](#preparing-a-new-release)
- [Compiled Output](#compiled-output)
- [Validation](#validation)
- [Tips](#tips)
- [FAQ](#faq)
- [Do I need to compile the changelog in my PR?](#do-i-need-to-compile-the-changelog-in-my-pr)
- [Can I edit an existing changelog entry?](#can-i-edit-an-existing-changelog-entry)
- [What if I forget to add a changelog entry?](#what-if-i-forget-to-add-a-changelog-entry)
- [What if two entries use the same filename?](#what-if-two-entries-use-the-same-filename)
- [Can I add multiple changelog entries in one PR?](#can-i-add-multiple-changelog-entries-in-one-pr)
- [Do I need to know the PR number when creating an entry?](#do-i-need-to-know-the-pr-number-when-creating-an-entry)
- [Does every PR need a changelog entry?](#does-every-pr-need-a-changelog-entry)
- [What happens to the YAML files after release?](#what-happens-to-the-yaml-files-after-release)
- [What if I have any other questions?](#what-if-i-have-any-other-questions)
## Creating a Changelog Entry
### When to Create an Entry
Create a changelog entry for:
- ✅ New user-facing features or tools
- ✅ Breaking changes that affect users or API consumers
- ✅ Important bug fixes
- ✅ Significant performance improvements
- ✅ Dependency updates that affect functionality
**Skip** changelog entries for:
- ❌ Internal refactoring with no user impact
- ❌ Documentation-only changes (unless major)
- ❌ Test-only changes
- ❌ Minor code cleanup or formatting
### File Format
**Structure**: One file per PR, supporting multiple changelog entries
```yaml
pr: <number> # Optional: PR number (auto-detected from git commit during compilation if omitted)
changes: # Required: Array of changes (minimum 1)
- section: <string> # Required
description: <string> # Required
subsection: <string> # Optional
```
#### Required Fields
- **changes**: Array of `change` objects (must have at least one). Each `change` requires:
- **section**: One of the following:
- `Features Added` - New features, tools, or capabilities
- `Breaking Changes` - Changes that break backward compatibility
- `Bugs Fixed` - Bug fixes
- `Other Changes` - Everything else (dependency updates, refactoring, etc.)
- **description**: Description of the change
#### Optional Fields
- **pr**: Pull request number (positive integer). If not provided, it can be auto-detected from the git commit message during compilation. GitHub squash merges include the PR number in the format `... (#1234)` which the compiler extracts automatically.
- **subsection**: Optional subsection to group changes under. Currently, the only valid subsection is `Dependency Updates` under the `Other Changes` section.
### Using the Generator Script
The easiest way to create a changelog entry is using the generator script located at `./eng/scripts/New-ChangelogEntry.ps1`. It supports both interactive and one-line modes:
#### Interactive mode
```powershell
./eng/scripts/New-ChangelogEntry.ps1
```
#### One-line
```powershell
./eng/scripts/New-ChangelogEntry.ps1 `
-ChangelogPath "servers/<server-name>/CHANGELOG.md" `
-Description "Added support for User-Assigned Managed Identity" `
-Section "Features Added"
```
> **Note:** The `-PR` parameter is optional. If omitted, the PR number will be auto-detected from the git commit during compilation.
##### With a subsection
```powershell
./eng/scripts/New-ChangelogEntry.ps1 `
-ChangelogPath "servers/<server-name>/CHANGELOG.md" `
-Description "Updated ModelContextProtocol.AspNetCore to version 0.4.0-preview.3" `
-Section "Other Changes" `
-Subsection "Dependency Updates"
```
##### With a multi-line entry
```powershell
$description = @"
Added new Foundry tools:
- foundry_agents_create: Create a new AI Foundry agent
- foundry_threads_create: Create a new AI Foundry Agent Thread
- foundry_threads_list: List all AI Foundry Agent Threads
"@
./eng/scripts/New-ChangelogEntry.ps1 `
-ChangelogPath "servers/<server-name>/CHANGELOG.md" `
-Description $description `
-Section "Features Added"
```
### Manual Creation
If you prefer to do things manually, create a new YAML file with your changes and save it in the appropriate `servers/<server-name>/changelog-entries` directory. **Use a unique name** made up of your alias or GitHub username and a brief description of the change, like this: `alias-brief-description.yaml`. For example: `servers/Azure.Mcp.Server/changelog-entries/vcolin7-fix-serialization.yaml`.
#### Simple entry
```yaml
changes:
- section: "Features Added"
description: "Added support for User-Assigned Managed Identity"
```
> **Note:** You can also specify the PR number explicitly if you know it: `pr: 1234`
#### Multi-line entry
```yaml
changes:
- section: "Features Added"
description: |
Added new AI Foundry tools:
- foundry_agents_create: Create a new AI Foundry agent
- foundry_threads_create: Create a new AI Foundry Agent Thread
- foundry_threads_list: List all AI Foundry Agent Threads
```
#### Using a subsection
```yaml
changes:
- section: "Other Changes"
subsection: "Dependency Updates"
description: "Updated ModelContextProtocol.AspNetCore to version 0.4.0-preview.3"
```
#### Multiple-entries
```yaml
changes:
- section: "Features Added"
description: "Added support for multiple changes per PR in changelog entries"
- section: "Bugs Fixed"
description: "Fixed issue with subsection title casing"
- section: "Other Changes"
subsection: "Dependency Updates"
description: "Updated ModelContextProtocol.AspNetCore to version 0.4.0-preview.3"
```
## Preparing a New Release
If you are a release manager, follow these steps before initiaiting a new release pipeline run:
1. Preview the changelog section for the version you are about to release:*
```powershell
# Compile to the default Unreleased section for Azure MCP Server
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -DryRun
# Or compile to a specific version
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -Version "<version>" -DryRun
```
2. Compile entries and delete files:
```powershell
# Compile to Unreleased section and delete YAML files for Azure MCP Server
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -DeleteFiles
# Or compile to a specific version
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -Version "<version>" -DeleteFiles
```
Notes:
- `-ChangelogPath`: Required parameter specifying which changelog file to compile changes for
- If `-Version` is specified: Entries are compiled into that version section (must exist in `CHANGELOG.md`)
- If no `-Version` is specified: Entries are compiled into the "Unreleased" section at the top
- If no "Unreleased" section exists and no `-Version` is specified: A new "Unreleased" section is created with the next version number
3. Sync the VS Code extension CHANGELOG (if applicable):
```powershell
# Preview the sync
./eng/scripts/Sync-VsCodeChangelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -DryRun
# Apply the sync
./eng/scripts/Sync-VsCodeChangelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md"
```
4. Update release date in CHANGELOG.md
5. Commit and initiate the release process
### Compiled Output
When compiled, entries are grouped by section and subsection. Empty sections will not be included.
```markdown
## 2.0.0-beta.3 (Unreleased)
### Features Added
- Added support for User-Assigned Managed Identity via the `AZURE_CLIENT_ID` environment variable. [[#1033](https://github.com/microsoft/mcp/pull/1033)]
- Added speech recognition support. [[#1054](https://github.com/microsoft/mcp/pull/1054)]
### Other Changes
#### Dependency Updates
- Updated the `ModelContextProtocol.AspNetCore` package from version `0.4.0-preview.2` to `0.4.0-preview.3`. [[#887](https://github.com/Azure/azure-mcp/pull/887)]
```
**Note:** When dealing with multi-line descriptions the PR link will be added to the last line. If the first line is followed by lines that are bullet items, they'll be automatically indented as sub-bullets and the PR link will be added to the first line instead.
### Validation
The scripts automatically validate YAML files against the schema at `eng/schemas/changelog-entry.schema.json`. To manually validate, you can use the `-DryRun` flag as follows:
```powershell
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -DryRun
```
## Tips
- **Multiple servers**: All scripts require the `-ChangelogPath` parameter
- Available servers: `Azure.Mcp.Server`, `Fabric.Mcp.Server`, `Template.Mcp.Server`, etc.
- Each server has its own `changelog-entries/` folder and `CHANGELOG.md`
- Example paths: `servers/Azure.Mcp.Server/CHANGELOG.md`, `servers/Fabric.Mcp.Server/CHANGELOG.md`
- **PR number auto-detection**: You don't need to know the PR number when creating an entry, it will be auto-detected from the git commit during compilation. You can still provide it explicitly if you want.
- **Edit an existing entry**: Just edit the YAML file and commit the change.
- **Multiple entries**: Create a single YAML file with multiple entries under the `changes` section.
## FAQ
### Do I need to compile the changelog in my PR?
No! Contributors only create YAML files. Release managers compile the changelog during the release process using the compilation script.
### Can I edit an existing changelog entry?
Yes! Just edit the YAML file and commit the change. It will be picked up in the next compilation.
### What if I forget to add a changelog entry?
Add it later in a follow-up PR or ask the maintainer to create one. Each entry is a separate file, so there's no conflict with other ongoing work.
### What if two entries use the same filename?
The filename is the unique identifier for each changelog entry. If two entries use the same filename, Git will show a conflict, and you can simply update the filename for your new change.
### Can I add multiple changelog entries in one PR?
Yes! Just create a single YAML file with multiple entries under the `changes` section. This is common when a PR includes several distinct user-facing changes.
### Do I need to know the PR number when creating an entry?
No! The PR number is **auto-detected** from the git commit message during compilation. When your PR is merged via GitHub's squash merge, the commit message includes the PR number in the format `... (#1234)`. The compilation script extracts this automatically.
If you prefer, you can still specify the PR number explicitly using `pr: 1234` in the YAML file.
### Does every PR need a changelog entry?
No! Only include entries for changes worth mentioning to users:
- ✅ New features, breaking changes, important bug fixes
- ❌ Internal refactoring, test-only changes, minor cleanup
### What happens to the YAML files after release?
They're deleted by the release manager using the `-DeleteFiles` flag when compiling entries into the main `CHANGELOG.md`.
### What if I have any other questions?
Reach out to the maintainers if you have questions or encounter issues with the changelog entry system.
+539
View File
@@ -0,0 +1,539 @@
# Changelog Management System
## Overview
This document describes the implementation of a conflict-free changelog management system inspired by [GitLab's approach](https://about.gitlab.com/blog/solving-gitlabs-changelog-conflict-crisis/). Instead of directly editing `CHANGELOG.md`, contributors create individual YAML files that are compiled during the release process.
**Key Benefits:**
- ✅ No merge conflicts on CHANGELOG.md
- ✅ Automated compilation and formatting
- ✅ Structured, validated data
- ✅ Flexible organization with sections and subsections
- ✅ Git history shows who added each entry
---
## Directory Structure
```
servers/Azure.Mcp.Server/
├── CHANGELOG.md # Main changelog
└── changelog-entries/ # Individual entry files
├── vcolin7-fix-changelog-system.yml
├── jfree-documentation-update.yml
├── ...
└── alzimmermsft-improve-performance.yml
```
**Why keep it simple with a single flat directory?**
- Each release train (i.e. beta, stable) lives in a separate branch with its own `CHANGELOG.md` and `changelog-entries/` folder, so there is no need for subdirectories.
- Simpler workflow: contributors only need to create files in a single known location
- Simplified file compilation and removal together on release
---
## YAML File Format
### Filename Convention
**Format:** `{username-brief-change-description}.yml`
**Example:** `jdoe-add-user-authentication.yml`
**Why this approach?**
- **Uniqueness**: Usernames and brief descriptions make filenames unique and descriptive
- **Sortable**: Files naturally sort alphabetically by username and description
- **Simple**: Easy to understand and manage
- **Pre-PR friendly**: Can create entries before opening a PR
### YAML Schema
```yaml
pr: <number> # Required: PR number (use 0 if not known yet)
changes: # Required: Array of changes (minimum 1)
- section: <string> # Required
description: <string> # Required
subsection: <string> # Optional
```
#### Required Fields
- **pr**: Pull request number at the top level (integer, use 0 if not known yet)
- **changes**: Array of `change` objects (must have at least one). Each `change` requires:
- **section**: One of the following:
- `Features Added` - New features, tools, or capabilities
- `Breaking Changes` - Changes that break backward compatibility
- `Bugs Fixed` - Bug fixes
- `Other Changes` - Everything else (dependency updates, refactoring, etc.)
- **description**: Description of the change
#### Optional Fields
- **subsection**: Optional subsection to group changes under. Currently, the only valid subsection is `Dependency Updates` under the `Other Changes` section.
**Note:** Not every PR needs a changelog entry! Only include entries for changes that are worth mentioning to users or maintainers (new features, breaking changes, important bug fixes, etc.). Minor internal refactoring, documentation updates, or test changes typically don't need entries.
**When to create a changelog entry:**
- ✅ New user-facing features or tools
- ✅ Breaking changes that affect users or API consumers
- ✅ Important bug fixes
- ✅ Significant performance improvements
- ✅ Dependency updates that affect functionality
- ❌ Internal refactoring with no user impact
- ❌ Documentation-only changes (unless major)
- ❌ Test-only changes
- ❌ Minor code cleanup or formatting
---
## Implementation Components
### 1. JSON Schema for Validation
**File:** `eng/schemas/changelog-entry.schema.json`
Validates YAML structure and ensures required fields are present.
```json
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Changelog Entry",
"description": "Schema for individual changelog entry YAML files. One file per PR, supporting multiple changes.",
"type": "object",
"required": ["pr", "changes"],
"properties": {
"pr": {
"type": "integer",
"description": "Pull request number (use 0 if not known yet)",
"minimum": 0
},
"changes": {
"type": "array",
"description": "List of changes in this PR (must have at least one)",
"minItems": 1,
"items": {
"type": "object",
"required": ["section", "description"],
"properties": {
"section": {
"type": "string",
"description": "The changelog section this entry belongs to",
"enum": [
"Features Added",
"Breaking Changes",
"Bugs Fixed",
"Other Changes"
]
},
"subsection": {
"type": "string",
"description": "Optional subsection for grouping related changes",
"enum": [
"Dependency Updates"
]
},
"description": {
"type": "string",
"description": "Description of the change",
"minLength": 10
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
```
### 2. Generator Script
**File:** `eng/scripts/New-ChangelogEntry.ps1`
Helper script to create properly formatted changelog entries. It supports both interactive and one-line modes.
**Usage:**
```powershell
# Interactive mode (prompts for all fields)
./eng/scripts/New-ChangelogEntry.ps1
# One-line with parameters
./eng/scripts/New-ChangelogEntry.ps1 `
-ChangelogPath "servers/<server-name>/CHANGELOG.md" `
-Description "Added support for User-Assigned Managed Identity" `
-Section "Features Added" `
-PR 1234
# With subsection
./eng/scripts/New-ChangelogEntry.ps1 `
-ChangelogPath "servers/<server-name>/CHANGELOG.md" `
-Description "Updated ModelContextProtocol.AspNetCore to version 0.4.0-preview.3" `
-Section "Other Changes" `
-Subsection "Dependency Updates" `
-PR 1234
# With a multi-line entry
$description = @"
Added new Foundry tools:
- foundry_agents_create: Create a new AI Foundry agent
- foundry_threads_create: Create a new AI Foundry Agent Thread
- foundry_threads_list: List all AI Foundry Agent Threads
"@
./eng/scripts/New-ChangelogEntry.ps1 `
-ChangelogPath "servers/<server-name>/CHANGELOG.md" `
-Description $description `
-Section "Features Added" `
-PR 1234
```
**Features:**
- Interactive prompts for section, description, PR number
- Auto-generates filename based on username and description
- Validates YAML against schema
- Places file in correct `changelog-entries/` directory
- Creates `changelog-entries/` directory if it doesn't exist
### 3. Compiler Script
**File:** `eng/scripts/Compile-Changelog.ps1`
Compiles all YAML entries into CHANGELOG.md.
**Usage:**
```powershell
# Preview what will be compiled (dry run)
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -DryRun
# Compile entries into CHANGELOG.md
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md"
# Compile to a specific version
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -Version "<version>"
# Compile and remove YAML files after successful compilation
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -DeleteFiles
```
**Parameters:**
- `-ChangelogPath`: Required. Path to the CHANGELOG.md file (e.g., `servers/Azure.Mcp.Server/CHANGELOG.md`)
- `-Version`: Optional. Target version section to compile into. If not specified, compiles to "Unreleased" section
- `-DryRun`: Preview compilation without modifying files
- `-DeleteFiles`: Remove YAML files after successful compilation
**Features:**
- Reads all YAML files from `changelog-entries/`
- Validates each file against schema
- Groups entries by section, then by subsection
- Formats entries as markdown with PR links
- Inserts compiled entries into CHANGELOG.md
- Optional deletion of YAML files after compilation
- Error handling for missing/invalid files
- Summary output
**Compilation Logic:**
1. Read all `.yml` and `.yaml` files from `changelog-entries/`
2. Validate each file against schema
3. Group by section (preserving order: Features, Breaking, Bugs, Other)
4. Within each section, group by subsection (if present)
5. Sort entries within groups
6. Generate markdown format with PR links
7. Find the target version section in CHANGELOG.md (or "Unreleased")
8. Insert compiled entries under appropriate section headers
9. Optionally delete YAML files if `-DeleteFiles` flag is set
---
## Example Workflow
### For Contributors
When making a change that needs a changelog entry:
1. **Make your code changes**
2. **Create a changelog entry:**
```powershell
./eng/scripts/New-ChangelogEntry.ps1 `
-ChangelogPath "servers/<server-name>/CHANGELOG.md" `
-Description "Your change description" `
-Section "Features Added" `
-PR <pr-number>
```
3. **Commit both your code AND the YAML file**
4. **Open your PR** - no conflicts on CHANGELOG.md!
### For Release Managers
Before tagging a release:
1. **Preview compilation:**
```powershell
# Preview to Unreleased section
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -DryRun
# Or preview to a specific version
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -Version "<version>" -DryRun
```
2. **Compile and clean up:**
```powershell
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -DeleteFiles
```
3. **Sync VS Code extension CHANGELOG (if applicable):**
```powershell
./eng/scripts/Sync-VsCodeChangelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md"
```
4. **Update version in CHANGELOG.md:**
- Change "Unreleased" to actual version number
- Add release date
5. **Commit the compiled changelog**
6. **Tag the release**
---
## Example Output
### Input YAML Files
**File:** `vcolin7-managed-identity.yaml`
```yaml
pr: 1033
changes:
- section: "Features Added"
description: "Added support for User-Assigned Managed Identity via the `AZURE_CLIENT_ID` environment variable."
```
**File:** `jdoe-speech-recognition.yaml`
```yaml
pr: 1054
changes:
- section: "Features Added"
description: "Added support for speech recognition from an audio file with Fast Transcription via the command `azmcp_speech_stt_recognize`."
```
**File:** `alzimmermsft-dependency-update.yaml`
```yaml
pr: 887
changes:
- section: "Other Changes"
subsection: "Dependency Updates"
description: "Updated the `ModelContextProtocol.AspNetCore` package from version `0.4.0-preview.2` to `0.4.0-preview.3`."
```
**File:** `jfree-multiple-changes.yaml` (multiple entries in one file)
```yaml
pr: 1234
changes:
- section: "Features Added"
description: "Added support for multiple changes per PR in changelog entries"
- section: "Bugs Fixed"
description: "Fixed issue with subsection title casing"
```
### Compiled Output in CHANGELOG.md
When compiled, entries are grouped by section and subsection. Empty sections will not be included.
```markdown
## 2.0.0-beta.3 (Unreleased)
### Features Added
- Added support for User-Assigned Managed Identity via the `AZURE_CLIENT_ID` environment variable. [[#1033](https://github.com/microsoft/mcp/pull/1033)]
- Added support for speech recognition from an audio file with Fast Transcription via the command `azmcp_speech_stt_recognize`. [[#1054](https://github.com/microsoft/mcp/pull/1054)]
- Added support for multiple changes per PR in changelog entries [[#1234](https://github.com/microsoft/mcp/pull/1234)]
### Bugs Fixed
- Fixed issue with subsection title casing [[#1234](https://github.com/microsoft/mcp/pull/1234)]
### Other Changes
#### Dependency Updates
- Updated the `ModelContextProtocol.AspNetCore` package from version `0.4.0-preview.2` to `0.4.0-preview.3`. [[#887](https://github.com/microsoft/mcp/pull/887)]
```
**Note:** When dealing with multi-line descriptions the PR link will be added to the last line. If the first line is followed by lines that are bullet items, they'll be automatically indented as sub-bullets and the PR link will be added to the first line instead.
---
## Validation & Quality Controls
### Schema Validation
All YAML files are validated against the JSON schema before compilation. Invalid files cause compilation to fail with clear error messages.
### Filename Validation
The compiler checks that filenames follow the expected pattern:
- Must end with `.yml` or `.yaml`
- Should follow `{username}-{brief-description}` format
- Ignores `README.md` and other non-YAML files
### CI Integration (Recommended)
Add validation to your CI pipeline:
```yaml
# Example GitHub Actions check
- name: Validate Changelog Entries
run: |
# Test compilation (dry run) - includes entry validation
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -DryRun
```
### Pre-commit Hooks (Optional)
- Validate YAML schema for changed `.yml` or `.yaml` files in `changelog-entries/`
- Ensure filename follows `{username}-{brief-description}` convention
- Warn if YAML file has `pr: 0` (can be updated later)
---
## Implementation Checklist
- [x] Create `changelog-entries/` directory
- [x] Create JSON schema: `eng/schemas/changelog-entry.schema.json`
- [x] Create generator script: `eng/scripts/New-ChangelogEntry.ps1`
- [x] Create compiler script: `eng/scripts/Compile-Changelog.ps1`
- [x] Create documentation: `docs/changelog-entries`
- [x] Update `CONTRIBUTING.md` with new changelog workflow
- [x] Update `docs/new-command.md` to mention changelog entries for AI coding agents
- [x] Update `AGENTS.md` to include changelog workflow for AI agents
- [ ] Add CI validation for YAML files (optional but recommended)
- [x] Create sample YAML files for testing
- [x] Test compilation with sample data
- [ ] Document in team wiki/onboarding materials
- [ ] Migrate any existing unreleased entries to YAML format
---
## Migration Strategy
For existing unreleased entries in CHANGELOG.md:
1. **Create YAML files** for each existing entry in the "Unreleased" section
2. **Run compilation** to verify output matches current format
3. **Remove unreleased entries** from CHANGELOG.md (will be re-added by compilation)
4. **Commit YAML files** to the repository
5. **Going forward**, all new entries use YAML files only
---
## Alternative Filename Strategies Considered
| Strategy | Pros | Cons | Verdict |
|----------|------|------|---------|
| **Username + description** (chosen) | Unique, descriptive, simple, pre-PR friendly | None significant | ✅ Best choice |
| Timestamp milliseconds | Unique, sortable | Opaque, not descriptive | ❌ Less readable |
| Timestamp + PR number | Guaranteed unique, traceable | Verbose, requires PR first | ❌ Unnecessary complexity |
| Git commit SHA | Unique, Git-native | Harder to read/sort, requires commit | ❌ Less convenient |
| GUID only | Guaranteed unique | Completely opaque, no sorting | ❌ Too opaque |
| Sequential counter | Simple, short | Requires coordination/locking | ❌ Conflict-prone |
| PR number only | Simple, short | Not unique, requires PR first | ❌ Defeats purpose |
---
## FAQ
### Q: What if I forget to add a changelog entry?
Add it later in a follow-up PR. Each entry is a separate file, so there's no conflict.
### Q: Can I edit an existing changelog entry?
Yes! Just edit the YAML file and commit the change. It will be picked up in the next compilation.
### Q: What if two entries use the same filename?
The filename is the unique identifier for each changelog entry. If two entries use the same filename, Git will show a conflict, and you can simply update the filename for your new change.
### Q: Do I need to compile the changelog in my PR?
No! Contributors only create YAML files. Release managers compile during the release process.
### Q: Can I add multiple changelog entries in one PR?
Yes! Just create a single YAML file with multiple entries under the `changes` section. This is common when a PR includes several distinct user-facing changes.
### Q: Do I need to know the PR number when creating an entry?
No! You can create the YAML file before opening a PR. Just add or update the `pr` field later when you know the PR number.
### Q: Does every PR need a changelog entry?
No. Only include entries for changes worth mentioning to users (new features, breaking changes, important bug fixes). Skip entries for internal refactoring, minor documentation updates, test-only changes, or code cleanup.
### Q: What happens to the YAML files after release?
They're deleted by the release manager using `Compile-Changelog.ps1 -DeleteFiles` after compilation.
---
## References
- [GitLab's Original Blog Post](https://about.gitlab.com/blog/solving-gitlabs-changelog-conflict-crisis/)
- [Azure SDK Changelog Guidelines](https://aka.ms/azsdk/guideline/changelogs)
- [Keep a Changelog](https://keepachangelog.com/)
---
## AI Agent Integration
When using AI coding agents like GitHub Copilot to work on features, ensure they are aware of the changelog workflow:
### Key Documents for AI Agents
1. **`docs/new-command.md`** - Should include step about creating changelog entry
2. **`AGENTS.md`** - Should reference this changelog system
3. **`CONTRIBUTING.md`** - Should explain when and how to create entries
### Recommended Addition to Agent Instructions
Add to relevant agent instruction files:
```markdown
## Changelog Entries
When adding user-facing changes (new features, breaking changes, important bug fixes):
1. Create a changelog entry YAML file:
```bash
./eng/scripts/New-ChangelogEntry.ps1 -ChangelogPath "servers/<server-name>/CHANGELOG.md" -Description "Your change" -Section "Features Added" -PR <number>
```
2. Or manually create `changelog-entries/{username}-{brief-description}.yaml`:
```yaml
pr: 1234 # Can use 0 if not known yet
changes:
- section: "Features Added"
description: "Your change description"
```
3. Not every change needs a changelog entry - skip for internal refactoring, test-only changes, or minor updates.
```
---
## Future Enhancements
Potential improvements to consider:
- [ ] Automatic PR number detection from git branch or PR context
- [ ] GitHub Action to auto-create YAML file from PR template
- [ ] Validation bot that comments on PRs when user-facing changes lack changelog entries
- [ ] Support for multiple changelog files (e.g., separate for different components)
- [ ] Integration with release notes generation
- [ ] Automatic backporting of changelog entries to stable branches
- [ ] AI agent templates that auto-generate changelog entries based on code changes
+49
View File
@@ -0,0 +1,49 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"title": "Changelog Entry",
"description": "Schema for individual changelog entry YAML files. One file per PR, supporting multiple changes.",
"type": "object",
"required": ["changes"],
"properties": {
"pr": {
"type": "integer",
"description": "Pull request number (optional - auto-detected from git commit during compilation if not provided)",
"minimum": 1
},
"changes": {
"type": "array",
"description": "List of changes in this PR (must have at least one)",
"minItems": 1,
"items": {
"type": "object",
"required": ["section", "description"],
"properties": {
"section": {
"type": "string",
"description": "The changelog section this entry belongs to",
"enum": [
"Features Added",
"Breaking Changes",
"Bugs Fixed",
"Other Changes"
]
},
"subsection": {
"type": "string",
"description": "Optional subsection for grouping related changes",
"enum": [
"Dependency Updates"
]
},
"description": {
"type": "string",
"description": "Description of the change",
"minLength": 10
}
},
"additionalProperties": false
}
}
},
"additionalProperties": false
}
+783
View File
@@ -0,0 +1,783 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Compiles changelog entries from YAML files into CHANGELOG.md.
.DESCRIPTION
This script reads all YAML files from the changelog-entries directory,
validates them against the schema, groups them by section and subsection,
and inserts the compiled entries into CHANGELOG.md under the specified version section.
If no version is specified, entries are added to the "Unreleased" section at the top.
If there is no "Unreleased" section and no version is specified, a new "Unreleased"
section is created using the next semantic version number.
.PARAMETER ChangelogPath
Path to the CHANGELOG.md file (required).
The changelog-entries directory is inferred from this path (same directory as CHANGELOG.md).
Examples: "servers/Azure.Mcp.Server/CHANGELOG.md", "servers/Fabric.Mcp.Server/CHANGELOG.md"
.PARAMETER Version
Target version section to compile entries into (e.g., "2.0.0-beta.3", "1.5.2").
If not specified, uses the "Unreleased" section or creates one.
.PARAMETER DryRun
Preview what will be compiled without modifying any files.
.PARAMETER DeleteFiles
Delete YAML files after successful compilation.
.EXAMPLE
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/Azure.Mcp.Server/CHANGELOG.md" -DryRun
Preview what will be compiled for Azure.Mcp.Server without making changes.
.EXAMPLE
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/Fabric.Mcp.Server/CHANGELOG.md"
Compile entries into the Unreleased section for Fabric.Mcp.Server.
.EXAMPLE
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/Azure.Mcp.Server/CHANGELOG.md" -Version "2.0.0-beta.3"
Compile entries into the 2.0.0-beta.3 version section for Azure.Mcp.Server.
.EXAMPLE
./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers/Fabric.Mcp.Server/CHANGELOG.md" -Version "1.0.0"
Compile entries into the 1.0.0 version section for Fabric.Mcp.Server.
.EXAMPLE
./eng/scripts/Compile-Changelog.ps1 -DeleteFiles
Compile entries and remove YAML files after successful compilation.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ChangelogPath,
[Parameter(Mandatory = $false)]
[string]$Version,
[Parameter(Mandatory = $false)]
[switch]$DryRun,
[Parameter(Mandatory = $false)]
[switch]$DeleteFiles
)
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot/../common/scripts/common.ps1"
. "$PSScriptRoot/../common/scripts/Helpers/PSModule-Helpers.ps1"
$RepoRoot = $RepoRoot.Path.Replace('\', '/')
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
# Infer changelog-entries path from CHANGELOG.md path
$changelogDir = Split-Path $ChangelogPath -Parent
$ChangelogEntriesPath = Join-Path $changelogDir "changelog-entries"
# Helper function to convert text to title case (capitalize first letter of each word)
function ConvertTo-TitleCase {
param([string]$Text)
if ([string]::IsNullOrWhiteSpace($Text)) {
return $Text
}
# Use TextInfo for proper title casing
$textInfo = (Get-Culture).TextInfo
return $textInfo.ToTitleCase($Text.ToLowerInvariant())
}
# Helper function to extract PR number from the git commit that introduced a file
# GitHub squash merges include PR number in format "... (#1234)" in commit message
function Get-PrFromGitCommit {
param([string]$FilePath)
try {
# Get the first (oldest) commit that touched this file
# Using --follow to track renames, and --reverse to get oldest first
$commitMessage = git log --follow --reverse --format="%s" -- $FilePath 2>$null | Select-Object -First 1
if ($commitMessage -and $commitMessage -match '\(#(\d+)\)\s*$') {
return [int]$matches[1]
}
}
catch {
# Git command failed, return 0
}
return 0
}
# Helper function to normalize indentation in multi-line text
# Converts tabs to spaces and standardizes indent levels to 2 spaces
function Normalize-Indentation {
param([string]$Line)
# Convert tabs to 2 spaces
$line = $Line -replace "`t", " "
# Count leading spaces and normalize to multiples of 2
if ($line -match '^(\s+)(.*)$') {
$leadingSpaces = $matches[1].Length
$content = $matches[2]
# Round to nearest multiple of 2 (minimum 2 if any indentation exists)
$normalizedSpaces = [Math]::Max(2, [Math]::Ceiling($leadingSpaces / 2) * 2)
return (" " * $normalizedSpaces) + $content
}
return $line
}
# Helper function to format a changelog entry with description and PR link
function Format-ChangelogEntry {
param(
[string]$Description,
[int]$PR
)
# Trim leading and trailing whitespace from the entire description
$Description = $Description.Trim()
# Normalize tabs to spaces throughout the description
$Description = $Description -replace "`t", " "
# Check if description contains multiple lines
if ($Description.Contains("`n")) {
# Remove trailing newlines from YAML block scalars
$cleanedDescription = $Description.TrimEnd("`n", "`r")
$lines = $cleanedDescription -split "`n"
$formattedLines = @()
# Check if this is a list (first line followed by bullet items)
$isList = $lines.Length -gt 1 -and $lines[1].TrimStart() -match '^-\s+'
$prLink = if ($PR -gt 0) { " [[#$PR](https://github.com/microsoft/mcp/pull/$PR)]" } else { "" }
for ($i = 0; $i -lt $lines.Length; $i++) {
$line = $lines[$i].TrimEnd() # Trim trailing spaces from each line
if ($i -eq 0) {
# First line: main bullet point - also trim leading spaces
$line = $line.TrimStart()
# Ensure it ends with colon if followed by a list, otherwise period
if ($isList) {
# Next line is a bullet, so first line should end with colon
if (-not $line.EndsWith(":")) {
$line += ":"
}
# For lists, add PR link to first line
$formattedLines += "- $line$prLink"
}
else {
# Regular multi-line text, ensure valid sentence ending
if ($line -notmatch '[.!?\)\]]$') {
$line += "."
}
$formattedLines += "- $line"
}
}
elseif ($i -eq $lines.Length - 1) {
# Last line: normalize indentation and add PR link for non-list entries
$normalizedLine = Normalize-Indentation $line.TrimEnd()
if ($isList) {
# For lists, PR link was added to first line, just add the last bullet
$formattedLines += " $normalizedLine"
}
else {
# For regular multi-line text, add PR link to last line
# Ensure valid sentence ending for non-bullet text
$content = $normalizedLine.TrimStart()
$needsPeriod = -not ($content -match '^-\s+') -and ($content -notmatch '[.!?\)\]]$')
if ($needsPeriod) {
$normalizedLine += "."
}
$formattedLines += " $normalizedLine$prLink"
}
}
else {
# Middle lines: normalize indentation and add 2 spaces for nesting
$normalizedLine = Normalize-Indentation $line.TrimEnd()
$formattedLines += " $normalizedLine"
}
}
return $formattedLines -join "`n"
}
else {
# Single-line description: original logic
$formattedDescription = $Description
# Ensure valid sentence ending (avoid double punctuation)
if ($formattedDescription -notmatch '[.!?\)\]]$') {
$formattedDescription += "."
}
# Add PR link if available
$prLink = if ($PR -gt 0) { " [[#$PR](https://github.com/microsoft/mcp/pull/$PR)]" } else { "" }
return "- $formattedDescription$prLink"
}
}
# Helper function to build subsection mapping (merging with case-insensitive matching)
function Build-SubsectionMapping {
param(
[hashtable]$ExistingSections,
[hashtable]$GroupedEntries,
[string]$Section
)
$subsectionMapping = @{}
$hasExisting = $ExistingSections.ContainsKey($Section)
$hasNew = $GroupedEntries.ContainsKey($Section)
if ($hasExisting) {
foreach ($key in $ExistingSections[$Section].Keys) {
if ($key -ne "") {
$titleCased = ConvertTo-TitleCase $key
if (-not $subsectionMapping.ContainsKey($titleCased)) {
$subsectionMapping[$titleCased] = @{
Existing = $key
New = $null
}
}
}
}
}
if ($hasNew) {
foreach ($key in $GroupedEntries[$Section].Keys) {
if ($key -ne "") {
$titleCased = ConvertTo-TitleCase $key
if ($subsectionMapping.ContainsKey($titleCased)) {
$subsectionMapping[$titleCased].New = $key
} else {
$subsectionMapping[$titleCased] = @{
Existing = $null
New = $key
}
}
}
}
}
return $subsectionMapping
}
# Set up paths using $RepoRoot from common.ps1
$changelogFile = Join-Path $RepoRoot $ChangelogPath
$changelogEntriesDir = Join-Path $RepoRoot $ChangelogEntriesPath
Write-Host "Changelog Compiler" -ForegroundColor Cyan
Write-Host "==================" -ForegroundColor Cyan
Write-Host ""
# Check if changelog-entries directory exists
if (-not (Test-Path $changelogEntriesDir)) {
Write-Error "Changelog entries directory not found: $changelogEntriesDir"
exit 1
}
# Check if CHANGELOG.md exists
if (-not (Test-Path $changelogFile)) {
Write-Error "CHANGELOG.md not found: $changelogFile"
exit 1
}
# Get all YAML files, excluding the example template file
# Note: Using -Filter twice and combining results since -Include requires -Recurse
$yamlFiles = @(Get-ChildItem -Path $changelogEntriesDir -Filter "*.yaml" -File)
$yamlFiles += @(Get-ChildItem -Path $changelogEntriesDir -Filter "*.yml" -File)
$yamlFiles = @($yamlFiles | Where-Object { $_.BaseName -ne 'username-example-brief-description' })
if ($yamlFiles.Count -eq 0) {
Write-Host "No changelog entries found in $changelogEntriesDir" -ForegroundColor Yellow
Write-Host "Nothing to compile." -ForegroundColor Yellow
exit 0
}
Write-Host "Found $($yamlFiles.Count) changelog entry file(s)" -ForegroundColor Green
Write-Host ""
Install-ModuleIfNotInstalled "powershell-yaml" "0.4.7" | Import-Module
# Load JSON schema and extract valid values
$schemaPath = Join-Path $RepoRoot "eng/schemas/changelog-entry.schema.json"
if (-not (Test-Path $schemaPath)) {
Write-Error "Schema file not found: $schemaPath"
exit 1
}
$schema = Get-Content -Path $schemaPath -Raw | ConvertFrom-Json
$validSections = $schema.properties.changes.items.properties.section.enum
$validSubsections = $schema.properties.changes.items.properties.subsection.enum
$minDescriptionLength = $schema.properties.changes.items.properties.description.minLength
Write-Host "Loaded validation rules from schema:" -ForegroundColor Gray
Write-Host " Valid sections: $($validSections -join ', ')" -ForegroundColor Gray
Write-Host " Valid subsections: $($validSubsections -join ', ')" -ForegroundColor Gray
Write-Host " Min description length: $minDescriptionLength" -ForegroundColor Gray
Write-Host ""
# Parse and validate YAML files
$entries = @()
foreach ($file in $yamlFiles) {
Write-Host "Processing: $($file.Name)" -ForegroundColor Gray
try {
$yamlContent = Get-Content -Path $file.FullName -Raw
$entry = $yamlContent | ConvertFrom-Yaml
# Handle PR field - it's optional, will be auto-detected from git if not provided
if (-not $entry.ContainsKey('pr') -or -not $entry['pr']) {
# Try to extract PR from git commit that introduced this file
$detectedPR = Get-PrFromGitCommit -FilePath $file.FullName
if ($detectedPR -gt 0) {
$entry['pr'] = $detectedPR
Write-Host " Auto-detected PR #$detectedPR from git history" -ForegroundColor Green
}
else {
$entry['pr'] = 0
Write-Warning " PR number not provided in $($file.Name) and could not be auto-detected from git"
}
}
elseif ($entry['pr'] -lt 1) {
Write-Error " Invalid PR number in $($file.Name) (must be a positive integer)"
continue
}
if (-not $entry.ContainsKey('changes') -or -not $entry['changes'] -or $entry['changes'].Count -eq 0) {
Write-Error " Missing or empty 'changes' array in $($file.Name)"
continue
}
# Process each change in the array
$changeCount = 0
foreach ($change in $entry.changes) {
# Validate required fields for each change
if (-not $change['section']) {
Write-Error " Missing required field 'section' in change #$($changeCount + 1) of $($file.Name)"
continue
}
# Validate and normalize section (case-insensitive)
$matchedSection = $validSections | Where-Object { $_ -ieq $change['section'] }
if ($matchedSection) {
$normalizedSection = $matchedSection
} else {
Write-Error " Invalid section '$($change['section'])' in change #$($changeCount + 1) of $($file.Name). Must be one of: $($validSections -join ', ')"
continue
}
if (-not $change['description']) {
Write-Error " Missing required field 'description' in change #$($changeCount + 1) of $($file.Name)"
continue
}
if ($change['description'].Length -lt $minDescriptionLength) {
Write-Error " Description too short in change #$($changeCount + 1) of $($file.Name) (minimum $minDescriptionLength characters)"
continue
}
# Validate subsection if provided
$subsection = $null
if ($change.ContainsKey('subsection') -and $change['subsection'] -and $change['subsection'] -ne "null") {
$providedSubsection = $change['subsection']
$matchedSubsection = $validSubsections | Where-Object { $_ -ieq $providedSubsection }
if (-not $matchedSubsection) {
Write-Error " Invalid subsection '$providedSubsection' in change #$($changeCount + 1) of $($file.Name). Valid subsections: $($validSubsections -join ', ')"
continue
}
$subsection = $matchedSubsection
}
# Add to entries collection with normalized section name
$entries += [PSCustomObject]@{
Section = $normalizedSection
Subsection = $subsection
Description = $change['description']
PR = $entry['pr']
Filename = $file.Name
}
$changeCount++
}
Write-Host " ✓ Valid ($changeCount change(s))" -ForegroundColor Green
}
catch {
Write-Error " Failed to parse $($file.Name): $_"
continue
}
}
if ($entries.Count -eq 0) {
Write-Error "No valid changelog entries found"
exit 1
}
Write-Host ""
Write-Host "Successfully validated $($entries.Count) entry/entries" -ForegroundColor Green
Write-Host ""
# Group entries by section and subsection
# Use $RecommendedSectionHeaders from ChangeLog-Operations.ps1 (imported via common.ps1)
$groupedEntries = @{}
foreach ($section in $RecommendedSectionHeaders) {
$sectionEntries = $entries | Where-Object { $_.Section -eq $section }
if ($sectionEntries) {
$groupedEntries[$section] = @{}
# Group by subsection
$withSubsection = $sectionEntries | Where-Object { $_.Subsection }
$withoutSubsection = $sectionEntries | Where-Object { -not $_.Subsection }
if ($withoutSubsection) {
$groupedEntries[$section][""] = $withoutSubsection
}
foreach ($entry in $withSubsection) {
# Normalize subsection name to title case for grouping
$normalizedSubsection = ConvertTo-TitleCase $entry.Subsection
if (-not $groupedEntries[$section].ContainsKey($normalizedSubsection)) {
$groupedEntries[$section][$normalizedSubsection] = @()
}
$groupedEntries[$section][$normalizedSubsection] += $entry
}
}
}
# Read existing CHANGELOG.md to determine target version
$changelogContent = Get-Content -Path $changelogFile -Raw
# Determine target version section
$targetVersionHeader = $null
if ($Version) {
# User specified a version - find it in the changelog
Write-Host "Looking for version section: $Version" -ForegroundColor Cyan
# Look for exact version match (with or without date)
$escapedVersion = [regex]::Escape($Version)
$versionPattern = "(?m)^##\s+$escapedVersion(\s+\(.*?\))?\s*$"
$versionMatch = [regex]::Match($changelogContent, $versionPattern)
if (-not $versionMatch.Success) {
Write-Error "Version '$Version' not found in CHANGELOG.md. Please ensure this version section exists before compiling entries."
exit 1
}
$targetVersionHeader = $versionMatch.Value
Write-Host "✓ Found version section: $targetVersionHeader" -ForegroundColor Green
Write-Host ""
} else {
# No version specified - look for Unreleased section at the top
Write-Host "No version specified - looking for Unreleased section..." -ForegroundColor Cyan
# Find the first ## header after any initial # headers
# This regex handles both formats: "## 2.0.0 (Unreleased)" and "## [0.0.1] - 2025-09-16"
$firstSectionPattern = '(?m)^##\s+(?:\[(.+?)\]|(\S+))(?:\s+-\s+|\s+\()'
$firstSectionMatch = [regex]::Match($changelogContent, $firstSectionPattern)
if ($firstSectionMatch.Success) {
$firstSectionFull = $firstSectionMatch.Value
# Check if it's Unreleased
if ($firstSectionFull -match '\(Unreleased\)') {
$targetVersionHeader = $firstSectionFull
Write-Host "✓ Found Unreleased section: $targetVersionHeader" -ForegroundColor Green
Write-Host ""
} else {
# First section is not Unreleased - create new Unreleased section
Write-Host "No Unreleased section found at the top of CHANGELOG.md" -ForegroundColor Yellow
Write-Host "Creating new Unreleased section with next version number..." -ForegroundColor Yellow
# Parse current version to determine next version
# Handle both bracketed and non-bracketed version formats
$currentVersion = if ($firstSectionMatch.Groups[1].Success) {
$firstSectionMatch.Groups[1].Value.Trim()
} else {
$firstSectionMatch.Groups[2].Value.Trim()
}
Write-Host "Current version: $currentVersion" -ForegroundColor Gray
# Parse semantic version using AzureEngSemanticVersion from SemVer.ps1 (imported via common.ps1)
$semVer = [AzureEngSemanticVersion]::ParseVersionString($currentVersion)
if ($semVer) {
# Increment to next prerelease version
$semVer.IncrementAndSetToPrerelease()
$nextVersion = $semVer.ToString()
$targetVersionHeader = "## $nextVersion (Unreleased)"
Write-Host "Next version: $nextVersion" -ForegroundColor Green
Write-Host ""
} else {
Write-Error "Unable to parse version number '$currentVersion' to determine next version. Please specify -Version parameter or ensure CHANGELOG.md has a valid version format."
exit 1
}
}
} else {
Write-Error "No version sections found in CHANGELOG.md. Please add a version section manually or specify -Version parameter."
exit 1
}
}
Write-Host "Target section: $targetVersionHeader" -ForegroundColor Cyan
Write-Host ""
# Generate markdown content from grouped entries (needed for new sections)
$newEntriesMarkdown = @()
foreach ($section in $RecommendedSectionHeaders) {
if ($groupedEntries.ContainsKey($section)) {
$newEntriesMarkdown += ""
$newEntriesMarkdown += "### $section"
$newEntriesMarkdown += ""
$subsections = $groupedEntries[$section]
# Process entries without subsection first
if ($subsections.ContainsKey("")) {
foreach ($entry in $subsections[""]) {
$newEntriesMarkdown += Format-ChangelogEntry -Description $entry.Description -PR $entry.PR
}
}
# Then process subsections
foreach ($subsectionName in ($subsections.Keys | Where-Object { $_ -ne "" } | Sort-Object)) {
$newEntriesMarkdown += ""
$newEntriesMarkdown += "#### $subsectionName"
$newEntriesMarkdown += ""
foreach ($entry in $subsections[$subsectionName]) {
$newEntriesMarkdown += Format-ChangelogEntry -Description $entry.Description -PR $entry.PR
}
}
}
}
# Find the target section in the changelog
$versionSectionPattern = '(?s)(' + [regex]::Escape($targetVersionHeader) + '\r?\n)(.*?)(?=##\s+\d+\.\d+\.\d+|##\s+[^\s]+\s+\(|$)'
$match = [regex]::Match($changelogContent, $versionSectionPattern)
# Build the merged content for preview
$mergedContent = @()
$mergedContent += $targetVersionHeader
if (-not $match.Success) {
# New section - just use the new entries
$mergedContent += $newEntriesMarkdown
} else {
# Existing section - merge with existing content
$versionHeader = $match.Groups[1].Value
$existingContent = $match.Groups[2].Value
# Parse existing content by sections
$existingSections = @{}
$currentSection = $null
$currentSubsection = $null
$lines = $existingContent -split "`r?`n"
foreach ($line in $lines) {
if ($line -match '^###\s+(.+?)\s*$') {
# Main section header - normalize to match valid sections (case-insensitive)
$rawSection = $matches[1]
$matchedSection = $validSections | Where-Object { $_ -ieq $rawSection }
$currentSection = if ($matchedSection) { $matchedSection } else { $rawSection }
$currentSubsection = $null
if (-not $existingSections.ContainsKey($currentSection)) {
$existingSections[$currentSection] = @{
"" = @() # Entries without subsection
}
}
}
elseif ($line -match '^####\s+(.+?)\s*$') {
# Subsection header - title case it
$rawSubsection = $matches[1]
$currentSubsection = ConvertTo-TitleCase $rawSubsection
if ($currentSection) {
# Check if this subsection already exists (case-insensitive)
$existingKey = $existingSections[$currentSection].Keys | Where-Object { $_ -ieq $currentSubsection -and $_ -ne "" }
if ($existingKey) {
$currentSubsection = $existingKey
} elseif (-not $existingSections[$currentSection].ContainsKey($currentSubsection)) {
$existingSections[$currentSection][$currentSubsection] = @()
}
}
}
elseif ($line -match '^-\s+' -or ($currentSection -and $line.Trim() -ne "" -and $line -match '^\s+')) {
# Entry line (starts with -) or continuation line (indented, non-empty)
# Skip if it's empty or if we're not in a section yet
if ($currentSection) {
if ($currentSubsection) {
$existingSections[$currentSection][$currentSubsection] += $line
} else {
$existingSections[$currentSection][""] += $line
}
}
}
}
# Build merged content by combining existing and new entries
$isFirstSection = $true
foreach ($section in $RecommendedSectionHeaders) {
# Check if we have entries (existing or new) for this section
$hasExisting = $existingSections.ContainsKey($section)
$hasNew = $groupedEntries.ContainsKey($section)
if (-not $hasExisting -and -not $hasNew) {
continue
}
# Collect entries for this section
$sectionContent = @()
# Merge entries without subsection
$existingMainEntries = @()
if ($hasExisting -and $existingSections[$section].ContainsKey("")) {
$existingMainEntries = @($existingSections[$section][""] | Where-Object { $_.Trim() -ne '' })
}
$newMainEntries = @()
if ($hasNew -and $groupedEntries[$section].ContainsKey("")) {
foreach ($entry in $groupedEntries[$section][""]) {
$newMainEntries += Format-ChangelogEntry -Description $entry.Description -PR $entry.PR
}
}
# Add main entries to section content
$totalMainEntries = $existingMainEntries.Count + $newMainEntries.Count
if ($totalMainEntries -gt 0) {
$sectionContent += "" # Empty line before entries
foreach ($line in $existingMainEntries) {
$sectionContent += $line
}
foreach ($line in $newMainEntries) {
$sectionContent += $line
}
}
# Merge subsections - normalize subsection names (case-insensitive, title case)
$subsectionMapping = Build-SubsectionMapping -ExistingSections $existingSections -GroupedEntries $groupedEntries -Section $section
$allSubsections = $subsectionMapping.Keys | Sort-Object
foreach ($subsectionTitleCased in $allSubsections) {
$mapping = $subsectionMapping[$subsectionTitleCased]
$subsectionEntries = @()
# Existing subsection entries (filter out empty lines)
if ($mapping.Existing -and $hasExisting -and $existingSections[$section].ContainsKey($mapping.Existing)) {
$subsectionEntries += @($existingSections[$section][$mapping.Existing] | Where-Object { $_.Trim() -ne '' })
}
# New subsection entries
if ($mapping.New -and $hasNew -and $groupedEntries[$section].ContainsKey($mapping.New)) {
foreach ($entry in $groupedEntries[$section][$mapping.New]) {
$subsectionEntries += Format-ChangelogEntry -Description $entry.Description -PR $entry.PR
}
}
# Only add subsection if it has content
if ($subsectionEntries.Count -gt 0) {
$sectionContent += ""
$sectionContent += "#### $subsectionTitleCased"
$sectionContent += "" # Empty line before entries
$sectionContent += $subsectionEntries
}
}
# Only add section to merged content if it has any content
if ($sectionContent.Count -gt 0) {
# Add empty line before section (but not before the very first section)
if (-not $isFirstSection) {
$mergedContent += ""
}
$isFirstSection = $false
$mergedContent += "### $section"
$mergedContent += $sectionContent
}
}
}
# Preview output
Write-Host "Compiled Output (as it will appear in CHANGELOG.md):" -ForegroundColor Cyan
Write-Host "=====================================================" -ForegroundColor Cyan
Write-Host ""
$mergedContent | ForEach-Object { Write-Host $_ -ForegroundColor Gray }
Write-Host ""
if ($DryRun) {
Write-Host "DRY RUN - No files were modified" -ForegroundColor Yellow
exit 0
}
# Now apply the changes to the file
if (-not $match.Success) {
Write-Host "Target section '$targetVersionHeader' not found in CHANGELOG.md" -ForegroundColor Yellow
Write-Host "Creating new section..." -ForegroundColor Yellow
# Create a new section using the merged content
$newVersionSection = ($mergedContent -join "`n") + "`n`n"
# Find the first ## section and insert before it
$firstSectionPattern = '(?m)^##\s+'
$firstSectionMatch = [regex]::Match($changelogContent, $firstSectionPattern)
if ($firstSectionMatch.Success) {
# Insert the new section right before the first ## section
$insertPosition = $firstSectionMatch.Index
$updatedChangelog = $changelogContent.Insert($insertPosition, "$newVersionSection")
} else {
# If no ## section found, append to the end of the file
$updatedChangelog = $changelogContent + "`n$newVersionSection"
}
} else {
# Replace existing section with merged content
$versionHeader = $match.Groups[1].Value
$existingContent = $match.Groups[2].Value
# Build the new content from merged array
$newContent = ($mergedContent -join "`n")
# Ensure there's an empty line at the end of the version section (two newlines total)
if (-not $newContent.EndsWith("`n`n")) {
if ($newContent.EndsWith("`n")) {
$newContent += "`n"
} else {
$newContent += "`n`n"
}
}
# Replace in changelog
$updatedChangelog = $changelogContent -replace [regex]::Escape($versionHeader + $existingContent), $newContent
}
# Write updated CHANGELOG.md
$updatedChangelog | Set-Content -Path $changelogFile -Encoding UTF8 -NoNewline
Write-Host "✓ Updated CHANGELOG.md" -ForegroundColor Green
Write-Host " Location: $changelogFile" -ForegroundColor Gray
Write-Host ""
# Delete YAML files if requested
if ($DeleteFiles) {
Write-Host "Deleting changelog entry files..." -ForegroundColor Yellow
foreach ($file in $yamlFiles) {
Remove-Item -Path $file.FullName -Force
Write-Host " Deleted: $($file.Name)" -ForegroundColor Gray
}
Write-Host "✓ Deleted $($yamlFiles.Count) file(s)" -ForegroundColor Green
Write-Host ""
}
Write-Host "Summary:" -ForegroundColor Cyan
Write-Host " Entries compiled: $($entries.Count)" -ForegroundColor Gray
Write-Host " Sections updated: $($groupedEntries.Keys.Count)" -ForegroundColor Gray
if ($DeleteFiles) {
Write-Host " Files deleted: $($yamlFiles.Count)" -ForegroundColor Gray
}
Write-Host ""
Write-Host "Done!" -ForegroundColor Green
+387
View File
@@ -0,0 +1,387 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Creates a new changelog entry YAML file.
.DESCRIPTION
This script helps create properly formatted changelog entry files.
Each entry is stored as a separate YAML file to avoid merge conflicts.
.PARAMETER Description
Description of the change (minimum 10 characters).
.PARAMETER Section
The changelog section. Valid values: "Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes".
.PARAMETER Subsection
Optional subsection for grouping related changes. Valid values: "Dependency Updates".
.PARAMETER PR
Pull request number (integer). Optional - if not provided, it will be auto-detected from the git commit
message during compilation. Must be a positive integer if provided.
.PARAMETER Filename
Optional custom filename for the changelog entry (without path).
If not provided, a timestamp-based filename will be generated.
Example: "vcolin7-fix-serialization.yaml"
.PARAMETER ChangelogPath
Path to the CHANGELOG.md file
The changelog-entries directory is inferred from this path (same directory as CHANGELOG.md).
Examples: "servers/Azure.Mcp.Server/CHANGELOG.md", "servers/Fabric.Mcp.Server/CHANGELOG.md"
If not provided, you will be prompted to select a server interactively.
.EXAMPLE
./eng/scripts/New-ChangelogEntry.ps1
Runs in fully interactive mode, prompting for server selection and all required fields.
.EXAMPLE
./eng/scripts/New-ChangelogEntry.ps1 -ChangelogPath "servers/Azure.Mcp.Server/CHANGELOG.md"
Runs in interactive mode for Azure.Mcp.Server, prompting for all required fields.
.EXAMPLE
./eng/scripts/New-ChangelogEntry.ps1 -ChangelogPath "servers/Fabric.Mcp.Server/CHANGELOG.md"
Runs in interactive mode for Fabric.Mcp.Server.
.EXAMPLE
./eng/scripts/New-ChangelogEntry.ps1 -ChangelogPath "servers/Azure.Mcp.Server/CHANGELOG.md" -Description "Added new feature" -Section "Features Added" -PR 1234
Creates a changelog entry for Azure.Mcp.Server with the specified parameters.
.EXAMPLE
./eng/scripts/New-ChangelogEntry.ps1 -ChangelogPath "servers/Fabric.Mcp.Server/CHANGELOG.md" -Description "Updated Azure.Core to 1.2.3" -Section "Other Changes" -Subsection "Dependency Updates" -PR 1234
Creates a changelog entry for Fabric.Mcp.Server with a subsection.
.EXAMPLE
./eng/scripts/New-ChangelogEntry.ps1 -ChangelogPath "servers/Azure.Mcp.Server/CHANGELOG.md" -Description "Fixed serialization bug" -Section "Bugs Fixed" -PR 1234 -Filename "vcolin7-fix-serialization"
Creates a changelog entry with a custom filename (vcolin7-fix-serialization.yaml).
.EXAMPLE
$description = @"
Added new AI Foundry tools:
- foundry_agents_create: Create a new AI Foundry agent
- foundry_threads_create: Create a new AI Foundry Agent Thread
- foundry_threads_list: List all AI Foundry Agent Threads
"@
./eng/scripts/New-ChangelogEntry.ps1 -Description $description -Section "Features Added" -PR 945
Creates a changelog entry with a multi-line description containing a list.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $false)]
[string]$ChangelogPath,
[Parameter(Mandatory = $false)]
[string]$Description,
[Parameter(Mandatory = $false)]
[string]$Section,
[Parameter(Mandatory = $false)]
[string]$Subsection,
[Parameter(Mandatory = $false)]
[int]$PR,
[Parameter(Mandatory = $false)]
[string]$Filename
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/../common/scripts/common.ps1"
$RepoRoot = $RepoRoot.Path.Replace('\', '/')
# Determine if we're in interactive mode (any required parameter is missing)
# Note: PR is optional - it will be auto-detected from git commit during compilation
$isInteractive = (-not $ChangelogPath) -or (-not $Description) -or (-not $Section)
# Show header once if in interactive mode
if ($isInteractive) {
LogInfo ""
LogInfo "Changelog Entry Creator"
LogInfo "======================="
LogInfo ""
}
# Helper function to convert text to title case (capitalize first letter of each word)
function ConvertTo-TitleCase {
param([string]$Text)
if ([string]::IsNullOrWhiteSpace($Text)) {
return $Text
}
# Use invariant culture TextInfo for proper title casing
$textInfo = [System.Globalization.CultureInfo]::InvariantCulture.TextInfo
return $textInfo.ToTitleCase($Text.ToLowerInvariant())
}
# Load JSON schema and extract valid values
$schemaPath = Join-Path $RepoRoot "eng/schemas/changelog-entry.schema.json"
if (-not (Test-Path $schemaPath)) {
LogError "Schema file not found: $schemaPath"
exit 1
}
$schema = Get-Content -Path $schemaPath -Raw | ConvertFrom-Json
$validSections = $schema.properties.changes.items.properties.section.enum
$validSubsections = $schema.properties.changes.items.properties.subsection.enum
$minDescriptionLength = $schema.properties.changes.items.properties.description.minLength
# Validate and normalize Section parameter if provided (case-insensitive)
if ($Section) {
$matchedSection = $validSections | Where-Object { $_ -ieq $Section }
if ($matchedSection) {
# Use the properly cased version
$Section = $matchedSection
} else {
LogError "Invalid section '$Section'. Valid sections are: $($validSections -join ', ')"
LogInfo ""
LogInfo "Example usage:"
LogInfo ' .\eng\scripts\New-ChangelogEntry.ps1 -Section "Features Added" -Description "..." -PR 1234'
LogInfo ""
exit 1
}
}
# Normalize subsection to title case if provided
if ($Subsection) {
$Subsection = ConvertTo-TitleCase -Text $Subsection
# Validate subsection against allowed values
$matchedSubsection = $validSubsections | Where-Object { $_ -ieq $Subsection }
if (-not $matchedSubsection) {
LogError "Invalid subsection '$Subsection'. Valid subsections are: $($validSubsections -join ', ')"
LogInfo "If you need a new subsection, please add it to the schema first."
LogInfo ""
exit 1
}
# Use the properly cased version
$Subsection = $matchedSubsection
}
# Interactive prompt for ChangelogPath if not provided
if (-not $ChangelogPath) {
Write-Host "Available servers:" -ForegroundColor Yellow
Write-Host " 1. Azure.Mcp.Server (servers/Azure.Mcp.Server/CHANGELOG.md)"
Write-Host " 2. Fabric.Mcp.Server (servers/Fabric.Mcp.Server/CHANGELOG.md)"
Write-Host " 3. Custom path"
Write-Host ""
$serverChoice = Read-Host "Select server (1-3)"
$ChangelogPath = switch ($serverChoice) {
"1" { "servers/Azure.Mcp.Server/CHANGELOG.md" }
"2" { "servers/Fabric.Mcp.Server/CHANGELOG.md" }
"3" {
$customPath = Read-Host "Enter custom CHANGELOG.md path"
$customPath.Trim()
}
default {
LogError "Invalid choice '$serverChoice'. Please select 1-3."
exit 1
}
}
Write-Host ""
Write-Host "Using: $ChangelogPath" -ForegroundColor Green
Write-Host ""
}
# Infer changelog-entries path from CHANGELOG.md path
$changelogDir = Split-Path $ChangelogPath -Parent
$ChangelogEntriesPath = Join-Path $changelogDir "changelog-entries"
# Set up paths (RepoRoot and schemaPath already defined above for schema loading)
$changelogEntriesDir = Join-Path $RepoRoot $ChangelogEntriesPath
# Create changelog-entries directory if it doesn't exist
if (-not (Test-Path $changelogEntriesDir)) {
Write-Host "Creating changelog-entries directory: $changelogEntriesDir" -ForegroundColor Yellow
New-Item -ItemType Directory -Path $changelogEntriesDir | Out-Null
}
# Interactive mode if parameters not provided
if (-not $Description) {
Write-Host "Note: For multi-line descriptions (e.g., with lists), use the -Description parameter with a here-string." -ForegroundColor Gray
Write-Host ""
$Description = Read-Host "Description (minimum $minDescriptionLength characters)"
# Trim whitespace from user input
$Description = $Description.Trim()
while ($Description.Length -lt $minDescriptionLength) {
Write-Host "Description must be at least $minDescriptionLength characters long." -ForegroundColor Red
$Description = Read-Host "Description (minimum $minDescriptionLength characters)"
$Description = $Description.Trim()
}
}
if (-not $Section) {
Write-Host "`nSelect a section:" -ForegroundColor Cyan
for ($i = 0; $i -lt $validSections.Count; $i++) {
Write-Host "$($i + 1). $($validSections[$i])"
}
$choice = Read-Host "`nEnter choice (1-$($validSections.Count))"
$choiceIndex = [int]$choice - 1
if ($choiceIndex -ge 0 -and $choiceIndex -lt $validSections.Count) {
$Section = $validSections[$choiceIndex]
} else {
LogError "Invalid choice '$choice'. Please select 1-$($validSections.Count)."
exit 1
}
}
# Allow subsection for any section in interactive mode
if (-not $PSBoundParameters.ContainsKey('Subsection') -and $isInteractive) {
$subsectionInput = Read-Host "`nSubsection (optional, press Enter to skip)"
if ($subsectionInput) {
# Trim and title case the subsection
$Subsection = ConvertTo-TitleCase -Text $subsectionInput.Trim()
# Validate subsection against allowed values
$matchedSubsection = $validSubsections | Where-Object { $_ -ieq $Subsection }
if (-not $matchedSubsection) {
LogError "Invalid subsection '$Subsection'. Valid subsections are: $($validSubsections -join ', ')"
LogInfo "If you need a new subsection, please add it to the schema first."
exit 1
}
# Use the properly cased version
$Subsection = $matchedSubsection
}
}
if (-not $PR -and $isInteractive) {
$prInput = Read-Host "`nPR number (press Enter to auto-detect from git during compilation)"
if ($prInput) {
$PR = [int]$prInput
}
}
# Trim whitespace from description if provided via parameter
$Description = $Description.Trim()
# Generate filename (use custom if provided, otherwise timestamp-based)
if ($Filename) {
# Ensure filename has proper extension
if (-not ($Filename.EndsWith('.yml') -or $Filename.EndsWith('.yaml'))) {
$Filename = "$Filename.yaml"
}
$filename = $Filename
} else {
$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds()
$filename = "$timestamp.yaml"
}
$filepath = Join-Path $changelogEntriesDir $filename
# Create YAML content in new format: pr at top level (optional), changes as an array
# If PR is not provided, it will be auto-detected from git commit during compilation
if ($PR) {
$yamlContent = "pr: $PR`n"
} else {
$yamlContent = ""
}
$yamlContent += "changes:`n"
# Create the change entry
$yamlContent += " - section: `"$Section`"`n"
# Use block scalar (|) for multi-line descriptions, quoted string for single-line
if ($Description.Contains("`n")) {
# Multi-line description - use block scalar with proper indentation
$descriptionLines = $Description -split "`n"
$yamlContent += " description: |`n"
foreach ($line in $descriptionLines) {
$yamlContent += " $line`n"
}
} else {
# Single-line description - use quoted string
# Escape backslashes and double quotes in the description
$escapedDescription = $Description -replace '\\', '\\\\' -replace '"', '\"'
$yamlContent += " description: `"$escapedDescription`"`n"
}
if ($Subsection) {
$yamlContent += " subsection: `"$Subsection`"`n"
}
# Remove trailing newline
$yamlContent = $yamlContent.TrimEnd("`n")
# Write YAML file
$yamlContent | Set-Content -Path $filepath -Encoding UTF8 -NoNewline
LogSuccess ""
LogSuccess "✓ Created changelog entry: $filename"
LogInfo " Location: $filepath"
LogInfo " Section: $Section"
if ($Subsection) {
LogInfo " Subsection: $Subsection"
}
LogInfo " Description: $Description"
if ($PR) {
LogInfo " PR: #$PR"
} else {
LogInfo " PR: Will be auto-detected from git commit"
}
# Validate against schema if available
if (Test-Path $schemaPath) {
LogInfo ""
LogInfo "Validating against schema..."
# Try to use PowerShell-Yaml module if available
$yamlModule = Get-Module -ListAvailable -Name "powershell-yaml"
if ($yamlModule) {
Import-Module powershell-yaml -ErrorAction SilentlyContinue
try {
$yamlData = Get-Content -Path $filepath -Raw | ConvertFrom-Yaml
# Validate PR if provided (must be positive integer)
if ($yamlData.ContainsKey('pr') -and $yamlData.pr -and $yamlData.pr -lt 1) {
LogError "PR must be a positive integer"
exit 1
}
if (-not $yamlData.changes -or $yamlData.changes.Count -eq 0) {
LogError "At least one change is required"
exit 1
}
foreach ($change in $yamlData.changes) {
if ($change.description.Length -lt $minDescriptionLength) {
LogError "Description must be at least $minDescriptionLength characters"
exit 1
}
}
LogSuccess "✓ Validation passed"
}
catch {
LogWarning "Could not validate YAML: $_"
}
}
else {
LogInfo " Note: Install 'powershell-yaml' module for automatic validation"
LogInfo " Run: Install-Module -Name powershell-yaml"
}
}
LogInfo ""
LogInfo "Next steps:"
LogInfo "1. Commit this file with your changes"
if (-not $PR) {
LogInfo "2. The PR number will be auto-detected from the git commit when compiled"
}
LogInfo ""
+273
View File
@@ -0,0 +1,273 @@
#!/usr/bin/env pwsh
<#
.SYNOPSIS
Syncs the Unreleased section from the main CHANGELOG to the VS Code extension CHANGELOG.
.DESCRIPTION
This script extracts the Unreleased section from the main server CHANGELOG
and creates a corresponding entry in the VS Code extension CHANGELOG with renamed sections:
- "Features Added" → "Added"
- "Breaking Changes" + "Other Changes" → "Changed"
- "Bugs Fixed" → "Fixed"
.PARAMETER ChangelogPath
Path to the main CHANGELOG.md file (required).
The VS Code CHANGELOG.md path is inferred from this path (vscode subdirectory).
Examples: "servers/Azure.Mcp.Server/CHANGELOG.md", "servers/Fabric.Mcp.Server/CHANGELOG.md"
.PARAMETER Version
The version number to use for the new VS Code changelog entry. If not specified, extracts from the Unreleased section header.
.PARAMETER DryRun
Preview the changes without modifying the VS Code CHANGELOG.
.EXAMPLE
./eng/scripts/Sync-VsCodeChangelog.ps1 -ChangelogPath "servers/Azure.Mcp.Server/CHANGELOG.md" -DryRun
Preview the sync for Azure.Mcp.Server without making changes.
.EXAMPLE
./eng/scripts/Sync-VsCodeChangelog.ps1 -ChangelogPath "servers/Fabric.Mcp.Server/CHANGELOG.md"
Sync the Unreleased section for Fabric.Mcp.Server.
.EXAMPLE
./eng/scripts/Sync-VsCodeChangelog.ps1 -ChangelogPath "servers/Azure.Mcp.Server/CHANGELOG.md" -Version "2.0.3"
Sync the Unreleased section and create version 2.0.3 entry in Azure.Mcp.Server VS Code CHANGELOG.
.EXAMPLE
./eng/scripts/Sync-VsCodeChangelog.ps1 -ChangelogPath "servers/Fabric.Mcp.Server/CHANGELOG.md" -Version "1.0.0"
Sync and create version 1.0.0 entry for Fabric.Mcp.Server.
#>
[CmdletBinding()]
param(
[Parameter(Mandatory = $true)]
[string]$ChangelogPath,
[Parameter(Mandatory = $false)]
[string]$Version,
[Parameter(Mandatory = $false)]
[switch]$DryRun
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot/../common/scripts/common.ps1"
$RepoRoot = $RepoRoot.Path.Replace('\', '/')
# Infer VS Code changelog path from main CHANGELOG.md path
$changelogDir = Split-Path $ChangelogPath -Parent
$VsCodeChangelogPath = Join-Path $changelogDir "vscode/CHANGELOG.md"
$MainChangelogPath = $ChangelogPath
$mainChangelogFile = Join-Path $RepoRoot $MainChangelogPath
$vscodeChangelogFile = Join-Path $RepoRoot $VsCodeChangelogPath
# Validate files exist
if (-not (Test-Path $mainChangelogFile)) {
LogError "Main CHANGELOG not found: $mainChangelogFile"
exit 1
}
if (-not (Test-Path $vscodeChangelogFile)) {
LogError "VS Code CHANGELOG not found: $vscodeChangelogFile"
exit 1
}
LogInfo ""
LogInfo "VS Code Changelog Sync"
LogInfo "======================"
LogInfo ""
# Read the main CHANGELOG
$mainContent = Get-Content -Path $mainChangelogFile -Raw
# Extract the Unreleased section (supports both "2.0.0" and "2.0.0-beta.3" formats)
$unreleasedMatch = $mainContent -match '(?ms)^## ([\d\.]+(?:-[\w\.]+)?) \(Unreleased\)\s*\n(.*?)(?=\n## |\z)'
if (-not $unreleasedMatch) {
LogError "No Unreleased section found in main CHANGELOG"
exit 1
}
$unreleasedVersion = $Matches[1]
$unreleasedContent = $Matches[2]
# Use provided version or extract from Unreleased header
if (-not $Version) {
$Version = $unreleasedVersion
}
LogInfo "Source: $mainChangelogFile"
LogInfo "Target: $vscodeChangelogFile"
LogInfo "Version: $Version"
LogInfo ""
# Parse sections from unreleased content
# Initialize sections dynamically from $RecommendedSectionHeaders (from ChangeLog-Operations.ps1 via common.ps1)
$sections = @{}
foreach ($header in $RecommendedSectionHeaders) {
$sections[$header] = @()
}
$currentSection = $null
$currentEntries = @()
foreach ($line in $unreleasedContent -split "`n") {
# Check for section headers
if ($line -match '^### (.+)$') {
# Save previous section
if ($currentSection -and $currentEntries.Count -gt 0) {
$sections[$currentSection] = $currentEntries
}
$currentSection = $Matches[1].Trim()
# Only process known sections (use $RecommendedSectionHeaders from ChangeLog-Operations.ps1)
if ($currentSection -notin $RecommendedSectionHeaders) {
LogWarning "Unknown section '$currentSection' found in main CHANGELOG - skipping"
$currentSection = $null
}
$currentEntries = @()
continue
}
# Skip lines before any section or in unknown sections
if (-not $currentSection) {
continue
}
# Collect all lines for current section (including empty lines for spacing)
# but trim trailing empty lines later
$currentEntries += $line
}
# Save last section
if ($currentSection -and $currentEntries.Count -gt 0) {
# Trim trailing empty lines from entries
while ($currentEntries.Count -gt 0 -and $currentEntries[-1].Trim() -eq '') {
$currentEntries = $currentEntries[0..($currentEntries.Count - 2)]
}
$sections[$currentSection] = $currentEntries
}
# Build VS Code changelog entry
$vscodeEntry = @()
$vscodeEntry += "## $Version ($(Get-Date -Format 'yyyy-MM-dd')) (pre-release)"
$vscodeEntry += ""
# Helper function to add section if it has content
function Add-Section {
param(
[string]$SectionName,
[array]$Entries
)
if (-not $Entries -or $Entries.Count -eq 0) {
return
}
# Filter out empty entries
$nonEmptyEntries = @($Entries | Where-Object { $_.Trim() -ne '' })
if ($nonEmptyEntries.Count -eq 0) {
return
}
$script:vscodeEntry += "### $SectionName"
$script:vscodeEntry += ""
$script:vscodeEntry += $nonEmptyEntries
$script:vscodeEntry += ""
}
# Added section (from Features Added)
Add-Section -SectionName "Added" -Entries $sections['Features Added']
# Changed section (from Breaking Changes + Other Changes)
$changedEntries = @()
$breakingChanges = @($sections['Breaking Changes'] | Where-Object { $_.Trim() -ne '' })
if ($breakingChanges.Count -gt 0) {
$changedEntries += $breakingChanges | ForEach-Object {
if ($_ -match '^-\s+(.+)$') {
# Add "**Breaking:**" prefix to breaking changes
"- **Breaking:** $($Matches[1])"
} else {
$_
}
}
}
$otherChanges = @($sections['Other Changes'] | Where-Object { $_.Trim() -ne '' })
if ($otherChanges.Count -gt 0) {
$changedEntries += $otherChanges
}
Add-Section -SectionName "Changed" -Entries $changedEntries
# Fixed section (from Bugs Fixed)
Add-Section -SectionName "Fixed" -Entries $sections['Bugs Fixed']
# Trim trailing empty lines (with safety check for empty array)
while ($vscodeEntry.Count -gt 0 -and $vscodeEntry[-1] -eq "") {
$vscodeEntry = $vscodeEntry[0..($vscodeEntry.Count - 2)]
}
$vscodeEntryText = $vscodeEntry -join "`n"
if ($DryRun) {
LogInfo "Preview of new VS Code CHANGELOG entry:"
LogInfo "========================================"
LogInfo ""
LogInfo $vscodeEntryText
LogInfo ""
LogWarning "DRY RUN - No files were modified"
exit 0
}
# Read current VS Code changelog
$vscodeContent = Get-Content -Path $vscodeChangelogFile -Raw
# Find insertion point (after "# Release History" header)
$headerMatch = $vscodeContent -match '(?ms)^(# Release History\s*\n)'
if (-not $headerMatch) {
LogError "Could not find '# Release History' header in VS Code CHANGELOG"
exit 1
}
$headerEnd = $Matches[0].Length
$beforeHeader = $vscodeContent.Substring(0, $headerEnd)
$afterHeader = $vscodeContent.Substring($headerEnd)
# Insert new entry
$newVscodeContent = $beforeHeader + "`n" + $vscodeEntryText + "`n`n" + $afterHeader.TrimStart("`n", "`r")
# Write updated VS Code changelog
$newVscodeContent | Set-Content -Path $vscodeChangelogFile -NoNewline -Encoding UTF8
LogSuccess "✓ Synced Unreleased section to VS Code CHANGELOG"
LogInfo " Version: $Version"
LogInfo " Location: $vscodeChangelogFile"
LogInfo ""
LogInfo "Summary:"
$addedCount = @($sections['Features Added'] | Where-Object { $_.Trim() -ne '' }).Count
$breakingCount = @($sections['Breaking Changes'] | Where-Object { $_.Trim() -ne '' }).Count
$otherCount = @($sections['Other Changes'] | Where-Object { $_.Trim() -ne '' }).Count
$fixedCount = @($sections['Bugs Fixed'] | Where-Object { $_.Trim() -ne '' }).Count
if ($addedCount -gt 0) {
LogInfo " - Added: $addedCount entries"
}
if ($breakingCount -gt 0 -or $otherCount -gt 0) {
$totalChanged = $breakingCount + $otherCount
LogInfo " - Changed: $totalChanged entries ($breakingCount breaking, $otherCount other)"
}
if ($fixedCount -gt 0) {
LogInfo " - Fixed: $fixedCount entries"
}
LogInfo ""
LogInfo "Next steps:"
LogInfo "1. Review the changes in the VS Code CHANGELOG"
LogInfo "2. Commit the updated VS Code CHANGELOG with your release"
LogInfo ""
@@ -0,0 +1,5 @@
pr: 123 # Optional - Auto-detected from git commit during compilation if not provided
changes: # Required
- section: "Other Changes" # Required - Valid sections: "New Features", "Breaking Changes", "Bug Fixes", "Other Changes"
description: "Updated ModelContextProtocol to version 2.1.0" # Required
subsection: "Dependency Updates" # Optional - Valid subsections: "Dependency Updates"
+1 -1
View File
@@ -2623,7 +2623,7 @@ Before submitting:
**REQUIRED**: All new commands must update the following documentation files:
- [ ] **CHANGELOG.md**: Add entry under "Unreleased" section describing the new command(s)
- [ ] **Changelog Entry**: Create a new changelog entry YAML file manually or by using the `./eng/scripts/New-ChangelogEntry.ps1` script/. See `docs/changelog-entries.md` for details.
- [ ] **servers/Azure.Mcp.Server/docs/azmcp-commands.md**: Add command documentation with description, syntax, parameters, and examples
- [ ] **Run metadata update script**: Execute `.\eng\scripts\Update-AzCommandsMetadata.ps1` to update tool metadata in azmcp-commands.md (required for CI validation)
- [ ] **README.md**: Update the supported services table and add example prompts demonstrating the new command(s) in the appropriate toolset section
@@ -0,0 +1,5 @@
pr: 123 # Optional - Auto-detected from git commit during compilation if not provided
changes: # Required
- section: "Other Changes" # Required - Valid sections: "New Features", "Breaking Changes", "Bug Fixes", "Other Changes"
description: "Updated ModelContextProtocol to version 2.1.0" # Required
subsection: "Dependency Updates" # Optional - Valid subsections: "Dependency Updates"
@@ -0,0 +1,5 @@
pr: 123 # Optional - Auto-detected from git commit during compilation if not provided
changes: # Required
- section: "Other Changes" # Required - Valid sections: "New Features", "Breaking Changes", "Bug Fixes", "Other Changes"
description: "Updated ModelContextProtocol to version 2.1.0" # Required
subsection: "Dependency Updates" # Optional - Valid subsections: "Dependency Updates"