From e6df909828e6e056f26bf9cae1a15539c4fe9329 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Tue, 11 Nov 2025 16:10:02 -0800 Subject: [PATCH 01/26] Added development plan --- docs/changelog-management-system.md | 480 ++++++++++++++++++ .../changelog/sample-entry.yml | 0 2 files changed, 480 insertions(+) create mode 100644 docs/changelog-management-system.md create mode 100644 servers/Azure.Mcp.Server/changelog/sample-entry.yml diff --git a/docs/changelog-management-system.md b/docs/changelog-management-system.md new file mode 100644 index 0000000000..d99ebd1a4c --- /dev/null +++ b/docs/changelog-management-system.md @@ -0,0 +1,480 @@ +# 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 +- ✅ Easier code reviews (changelog entry visible in PR) +- ✅ 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 (compiled output) +└── changelog-entries/ # Individual entry files + ├── 1731260400123.yml + ├── 1731260405789.yml + ├── ... + └── README.md # Documentation for contributors +``` + +**Why keep it simple with a single flat directory?** +- We only release from one branch at a time, so we never need multiple folders +- Simpler workflow: contributors only need to know one location +- All files get compiled and removed together on release +- No unnecessary nesting + +--- + +## YAML File Format + +### Filename Convention + +**Format:** `{unix-timestamp-milliseconds}.yml` + +**Example:** `1731260400123.yml` + +**Why this approach?** +- **Uniqueness**: Millisecond precision makes collisions extremely unlikely (1000 unique values per second) +- **Sortable**: Files naturally sort chronologically +- **Simple**: Just a number, no need to coordinate PR numbers +- **Pre-PR friendly**: Can create entries before opening a PR +- **Cross-platform safe**: No special characters + +**How to generate:** +```powershell +# In PowerShell +[DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + +# In Bash +date +%s%3N +``` + +### YAML Schema + +```yaml +# Required fields +section: "Features Added" # One of: Features Added, Breaking Changes, Bugs Fixed, Other Changes +description: "Added support for User-Assigned Managed Identity via the `AZURE_CLIENT_ID` environment variable." +pr: 1033 # PR number (integer) - can be added later if not known yet + +# Optional fields +subsection: null # Optional: "Dependency Updates", "Telemetry", etc. +``` + +**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. + +### Valid Values + +**Sections (required):** +- `Features Added` +- `Breaking Changes` +- `Bugs Fixed` +- `Other Changes` + +**Subsections (optional):** +- `Dependency Updates` +- `Telemetry` +- Any custom subsection for grouping related changes + +**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#", + "type": "object", + "required": ["section", "description", "pr"], + "properties": { + "section": { + "type": "string", + "enum": [ + "Features Added", + "Breaking Changes", + "Bugs Fixed", + "Other Changes" + ] + }, + "subsection": { + "type": ["string", "null"] + }, + "description": { + "type": "string", + "minLength": 10 + }, + "pr": { + "type": "integer", + "minimum": 1 + } + }, + "additionalProperties": false +} +``` + +### 2. Generator Script + +**File:** `eng/scripts/New-ChangelogEntry.ps1` + +Helper script to create properly formatted changelog entries. + +**Usage:** + +```powershell +# Interactive mode (prompts for all fields) +./eng/scripts/New-ChangelogEntry.ps1 + +# With parameters +./eng/scripts/New-ChangelogEntry.ps1 ` + -Description "Added new feature" ` + -Section "Features Added" ` + -PR 1234 + +# With subsection +./eng/scripts/New-ChangelogEntry.ps1 ` + -Description "Updated Azure.Core to 1.2.3" ` + -Section "Other Changes" ` + -Subsection "Dependency Updates" ` + -PR 1234 +``` + +**Features:** +- Interactive prompts for section, description, PR number +- Auto-generates filename with timestamp +- Validates YAML against schema +- Places file in correct 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 -DryRun + +# Compile entries into CHANGELOG.md +./eng/scripts/Compile-Changelog.ps1 + +# Compile and remove YAML files after successful compilation +./eng/scripts/Compile-Changelog.ps1 -DeleteFiles + +# Custom changelog path +./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "path/to/CHANGELOG.md" +``` + +**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 under "Unreleased" +- Optional deletion of YAML files after compilation +- Error handling for missing/invalid files +- Summary output + +**Compilation Logic:** + +1. Read all `.yml` files from `changelog-entries/` (excluding README.md) +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 "Unreleased" section in CHANGELOG.md +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 ` + -Description "Your change description" ` + -Section "Features Added" ` + -PR + ``` + +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 + ./eng/scripts/Compile-Changelog.ps1 -DryRun + ``` + +2. **Compile and clean up:** + ```powershell + ./eng/scripts/Compile-Changelog.ps1 -DeleteFiles + ``` + +3. **Update version in CHANGELOG.md:** + - Change "Unreleased" to actual version number + - Add release date + +4. **Commit the compiled changelog** + +5. **Tag the release** + +--- + +## Example Output + +### Input YAML Files + +**File:** `1731260400123.yml` +```yaml +section: "Features Added" +description: "Added support for User-Assigned Managed Identity via the `AZURE_CLIENT_ID` environment variable." +pr: 1033 +``` + +**File:** `1731260405789.yml` +```yaml +section: "Features Added" +description: "Added support for speech recognition from an audio file with Fast Transcription via the command `azmcp_speech_stt_recognize`." +pr: 1054 +``` + +**File:** `1731260410456.yml` +```yaml +section: "Other Changes" +subsection: "Telemetry" +description: "Added `ToolId` into telemetry, based on `IBaseCommand.Id`, a unique GUID for each command." +pr: 1028 +``` + +### Compiled Output in CHANGELOG.md + +```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)] + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + +#### Telemetry +- Added `ToolId` into telemetry, based on `IBaseCommand.Id`, a unique GUID for each command. [[#1028](https://github.com/microsoft/mcp/pull/1028)] +``` + +--- + +## 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` +- Should be numeric (timestamp format) - warning if not +- 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: | + # Validate YAML syntax and schema + ./eng/scripts/Validate-ChangelogEntries.ps1 + + # Test compilation (dry run) + ./eng/scripts/Compile-Changelog.ps1 -DryRun +``` + +### Pre-commit Hooks (Optional) + +- Validate YAML schema for changed `.yml` files in `changelog-entries/` +- Ensure filename follows numeric timestamp convention +- Warn if YAML file is missing a `pr` field (can be added later) + +--- + +## Implementation Checklist + +- [ ] Create `changelog-entries/` directory +- [ ] Create JSON schema: `eng/schemas/changelog-entry.schema.json` +- [ ] Create generator script: `eng/scripts/New-ChangelogEntry.ps1` +- [ ] Create compiler script: `eng/scripts/Compile-Changelog.ps1` +- [ ] Create documentation: `changelog-entries/README.md` +- [ ] Update `CONTRIBUTING.md` with new changelog workflow +- [ ] Update `docs/new-command.md` to mention changelog entries for AI coding agents +- [ ] Update `AGENTS.md` to include changelog workflow for AI agents +- [ ] Add CI validation for YAML files (optional but recommended) +- [ ] Create sample YAML files for testing +- [ ] 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 | +|----------|------|------|---------| +| **Timestamp milliseconds** (chosen) | Unique, sortable, simple, pre-PR friendly | None significant | ✅ Best choice | +| 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 timestamp? + +The timestamp is in milliseconds, giving 1000 unique values per second. Collisions are extremely unlikely. If one does occur, Git will highlight a file is being modified instead of added, and you can simply regenerate the timestamp. + +### 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 multiple YAML files with different filenames (different timestamps). + +### 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 -Description "Your change" -Section "Features Added" -PR + ``` + +2. Or manually create `changelog-entries/{timestamp}.yml`: + ```yaml + section: "Features Added" + description: "Your change description" + pr: 1234 # Can be added later if not known yet + ``` + +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 diff --git a/servers/Azure.Mcp.Server/changelog/sample-entry.yml b/servers/Azure.Mcp.Server/changelog/sample-entry.yml new file mode 100644 index 0000000000..e69de29bb2 From d41612ec4352bbf3c0b1b5bd406c27996be4a2ac Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 13:47:50 -0800 Subject: [PATCH 02/26] Added and updated docs --- AGENTS.md | 4 +- CONTRIBUTING.md | 16 +- eng/schemas/changelog-entry.schema.json | 34 ++++ .../changelog-entries/README.md | 178 ++++++++++++++++++ servers/Azure.Mcp.Server/docs/new-command.md | 6 +- 5 files changed, 232 insertions(+), 6 deletions(-) create mode 100644 eng/schemas/changelog-entry.schema.json create mode 100644 servers/Azure.Mcp.Server/changelog-entries/README.md diff --git a/AGENTS.md b/AGENTS.md index 140afc1d4d..8374c7b3be 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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. Check out `/servers/Azure.Mcp.Server/changelog-entries/README.md` for instructions. - 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. Check out `/servers/Azure.Mcp.Server/changelog-entries/README.md` for instructions. 5. **Add CODEOWNERS entry** for new toolset ### Spelling and Content Validation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index bed00151b4..88e8a5be34 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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) @@ -140,9 +142,17 @@ 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: + ```powershell + ./eng/scripts/New-ChangelogEntry.ps1 -Description -Section -PR + ``` + - Or manually create a YAML file in `servers/Azure.Mcp.Server/changelog-entries/` (see `/servers/Azure.Mcp.Server/changelog-entries/README.md` for details) + - 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 +167,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 diff --git a/eng/schemas/changelog-entry.schema.json b/eng/schemas/changelog-entry.schema.json new file mode 100644 index 0000000000..834f389550 --- /dev/null +++ b/eng/schemas/changelog-entry.schema.json @@ -0,0 +1,34 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Changelog Entry", + "description": "Schema for individual changelog entry YAML files", + "type": "object", + "required": ["section", "description", "pr"], + "properties": { + "section": { + "type": "string", + "description": "The changelog section this entry belongs to", + "enum": [ + "Features Added", + "Breaking Changes", + "Bugs Fixed", + "Other Changes" + ] + }, + "subsection": { + "type": ["string", "null"], + "description": "Optional subsection for grouping related changes (e.g., 'Dependency Updates', 'Telemetry')" + }, + "description": { + "type": "string", + "description": "Description of the change", + "minLength": 10 + }, + "pr": { + "type": "integer", + "description": "Pull request number", + "minimum": 1 + } + }, + "additionalProperties": false +} diff --git a/servers/Azure.Mcp.Server/changelog-entries/README.md b/servers/Azure.Mcp.Server/changelog-entries/README.md new file mode 100644 index 0000000000..949c02b797 --- /dev/null +++ b/servers/Azure.Mcp.Server/changelog-entries/README.md @@ -0,0 +1,178 @@ +# Changelog Entries + +This directory contains individual changelog entry files that are compiled into the main `CHANGELOG.md` during the release process. + +## 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. + +## 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 + +### Using the Generator Script (Recommended) + +The easiest way to create a changelog entry is using the generator script: + +```powershell +# Interactive mode (prompts for all fields) +./eng/scripts/New-ChangelogEntry.ps1 + +# With parameters +./eng/scripts/New-ChangelogEntry.ps1 ` + -Description "Added support for User-Assigned Managed Identity" ` + -Section "Features Added" ` + -PR 1033 + +# With subsection +./eng/scripts/New-ChangelogEntry.ps1 ` + -Description "Updated Azure.Core to 1.2.3" ` + -Section "Other Changes" ` + -Subsection "Dependency Updates" ` + -PR 1234 +``` + +### Manual Creation + +If you prefer to create the file manually: + +1. **Generate a unique filename** using the current timestamp in milliseconds: + ```powershell + # PowerShell + [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() + + # Bash + date +%s%3N + ``` + Example: `1731260400123.yml` + +2. **Create a YAML file** with this structure: + ```yaml + section: "Features Added" + description: "Your change description here" + subsection: null # Or a subsection name like "Dependency Updates" + pr: 1234 # Can be added later if not known yet + ``` + +3. **Save the file** in the `servers/Azure.Mcp.Server/changelog-entries/` directory + +## YAML File Format + +### Required Fields + +- **section**: The changelog section (one of: `Features Added`, `Breaking Changes`, `Bugs Fixed`, `Other Changes`) +- **description**: Description of the change (minimum 10 characters) +- **pr**: Pull request number (integer) + +### Optional Fields + +- **subsection**: Optional subsection for grouping (e.g., `Dependency Updates`, `Telemetry`) + +### Valid Sections + +- `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.) + +### Example Entry + +**Filename:** `1731260400123.yml` + +```yaml +section: "Features Added" +description: "Added support for User-Assigned Managed Identity via the `AZURE_CLIENT_ID` environment variable." +subsection: null +pr: 1033 +``` + +## Workflow + +### For Contributors + +1. **Make your code changes** +2. **Create a changelog entry** (if your change needs one) +3. **Commit both your code and the YAML file** in the same PR +4. **Open your PR** - no CHANGELOG.md conflicts! + +You can create the YAML file before you have a PR number. Just set `pr: 0` initially and update it later when you know the PR number. + +### For Release Managers + +Before tagging a release: + +1. **Preview compilation:** + ```powershell + ./eng/scripts/Compile-Changelog.ps1 -DryRun + ``` + +2. **Compile entries:** + ```powershell + ./eng/scripts/Compile-Changelog.ps1 -DeleteFiles + ``` + +3. **Update CHANGELOG.md:** + - Change "Unreleased" to the actual version number + - Add release date + +4. **Commit and tag the release** + +## Compiled Output + +When compiled, entries are grouped by section and subsection: + +```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)] + +### Breaking Changes + +### Bugs Fixed + +### Other Changes + +#### Telemetry +- Added `ToolId` into telemetry. [[#1028](https://github.com/microsoft/mcp/pull/1028)] +``` + +## Validation + +The scripts automatically validate your YAML files against the schema at `eng/schemas/changelog-entry.schema.json`. + +To manually validate: +```powershell +# The New-ChangelogEntry.ps1 script validates automatically +./eng/scripts/New-ChangelogEntry.ps1 + +# The Compile-Changelog.ps1 script also validates +./eng/scripts/Compile-Changelog.ps1 -DryRun +``` + +## Tips + +- **Filename collisions**: The timestamp is in milliseconds, giving 1000 unique values per second. Collisions are extremely unlikely. +- **PR number unknown**: You can create the entry before opening a PR. Just use `pr: 0` and update it later. +- **Edit existing entry**: Just edit the YAML file and commit the change. +- **Multiple entries**: Create multiple YAML files with different timestamps. +- **Subsections**: Use sparingly for grouping related changes (e.g., dependency updates). + +## Questions? + +See the full documentation at `docs/changelog-management-system.md` or reach out to the maintainers. diff --git a/servers/Azure.Mcp.Server/docs/new-command.md b/servers/Azure.Mcp.Server/docs/new-command.md index ff63308be8..52ec7b54da 100644 --- a/servers/Azure.Mcp.Server/docs/new-command.md +++ b/servers/Azure.Mcp.Server/docs/new-command.md @@ -2602,7 +2602,11 @@ 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 changelog entry YAML file (if your change is a new feature, bug fix, or breaking change): + ```powershell + ./eng/scripts/New-ChangelogEntry.ps1 -Description -Section -PR + ``` + See `/servers/Azure.Mcp.Server/changelog-entries/README.md` for details. Skip for internal refactoring, test-only changes, or minor updates. - [ ] **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 From 611d9702e7acb57a8260f6109febda24520c0f35 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 13:47:54 -0800 Subject: [PATCH 03/26] Added scripts --- eng/scripts/Compile-Changelog.ps1 | 298 +++++++++++++++++++++++++++++ eng/scripts/New-ChangelogEntry.ps1 | 209 ++++++++++++++++++++ 2 files changed, 507 insertions(+) create mode 100644 eng/scripts/Compile-Changelog.ps1 create mode 100644 eng/scripts/New-ChangelogEntry.ps1 diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 new file mode 100644 index 0000000000..37c2b4a03d --- /dev/null +++ b/eng/scripts/Compile-Changelog.ps1 @@ -0,0 +1,298 @@ +#!/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 "Unreleased" section. + +.PARAMETER ChangelogPath + Path to the CHANGELOG.md file. Defaults to servers/Azure.Mcp.Server/CHANGELOG.md. + +.PARAMETER ChangelogEntriesPath + Path to the changelog-entries directory. Defaults to servers/Azure.Mcp.Server/changelog-entries. + +.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 -DryRun + + Preview what will be compiled without making changes. + +.EXAMPLE + ./eng/scripts/Compile-Changelog.ps1 + + Compile entries into CHANGELOG.md. + +.EXAMPLE + ./eng/scripts/Compile-Changelog.ps1 -DeleteFiles + + Compile entries and remove YAML files after successful compilation. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [string]$ChangelogPath = "servers/Azure.Mcp.Server/CHANGELOG.md", + + [Parameter(Mandatory = $false)] + [string]$ChangelogEntriesPath = "servers/Azure.Mcp.Server/changelog-entries", + + [Parameter(Mandatory = $false)] + [switch]$DryRun, + + [Parameter(Mandatory = $false)] + [switch]$DeleteFiles +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Get repository root +$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +$changelogFile = Join-Path $repoRoot $ChangelogPath +$changelogEntriesDir = Join-Path $repoRoot $ChangelogEntriesPath +$schemaPath = Join-Path $repoRoot "eng/schemas/changelog-entry.schema.json" + +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 +$yamlFiles = Get-ChildItem -Path $changelogEntriesDir -Filter "*.yml" -File | Where-Object { $_.Name -ne "README.yml" } + +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 PowerShell-Yaml module if not available +$yamlModule = Get-Module -ListAvailable -Name "powershell-yaml" +if (-not $yamlModule) { + Write-Host "Installing powershell-yaml module..." -ForegroundColor Yellow + Install-Module -Name powershell-yaml -Force -Scope CurrentUser -AllowClobber +} +Import-Module powershell-yaml -ErrorAction Stop + +# Load schema +$schema = Get-Content -Path $schemaPath -Raw | ConvertFrom-Json + +# Parse and validate YAML files +$entries = @() +$validSections = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") + +foreach ($file in $yamlFiles) { + Write-Host "Processing: $($file.Name)" -ForegroundColor Gray + + # Validate filename format (should be numeric timestamp) + if ($file.BaseName -notmatch '^\d+$') { + Write-Warning " Filename '$($file.Name)' doesn't follow timestamp convention (numeric only)" + } + + try { + $yamlContent = Get-Content -Path $file.FullName -Raw + $entry = $yamlContent | ConvertFrom-Yaml + + # Validate required fields + if (-not $entry.section) { + Write-Error " Missing required field 'section' in $($file.Name)" + continue + } + + if ($entry.section -notin $validSections) { + Write-Error " Invalid section '$($entry.section)' in $($file.Name). Must be one of: $($validSections -join ', ')" + continue + } + + if (-not $entry.description) { + Write-Error " Missing required field 'description' in $($file.Name)" + continue + } + + if ($entry.description.Length -lt 10) { + Write-Error " Description too short in $($file.Name) (minimum 10 characters)" + continue + } + + if (-not $entry.pr -or $entry.pr -eq 0) { + Write-Warning " Missing or invalid PR number in $($file.Name)" + } + elseif ($entry.pr -lt 1) { + Write-Error " Invalid PR number in $($file.Name) (must be positive)" + continue + } + + # Add to entries collection + $entries += [PSCustomObject]@{ + Section = $entry.section + Subsection = if ($entry.subsection -and $entry.subsection -ne "null") { $entry.subsection } else { $null } + Description = $entry.description + PR = $entry.pr + Filename = $file.Name + } + + Write-Host " ✓ Valid" -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 +$sectionOrder = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") +$groupedEntries = @{} + +foreach ($section in $sectionOrder) { + $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) { + if (-not $groupedEntries[$section].ContainsKey($entry.Subsection)) { + $groupedEntries[$section][$entry.Subsection] = @() + } + $groupedEntries[$section][$entry.Subsection] += $entry + } + } +} + +# Generate markdown +$markdown = @() + +foreach ($section in $sectionOrder) { + if (-not $groupedEntries.ContainsKey($section)) { + continue + } + + $markdown += "" + $markdown += "### $section" + $markdown += "" + + $sectionData = $groupedEntries[$section] + + # Entries without subsection first + if ($sectionData.ContainsKey("")) { + foreach ($entry in $sectionData[""]) { + $prLink = if ($entry.PR -gt 0) { " [[#$($entry.PR)](https://github.com/microsoft/mcp/pull/$($entry.PR))]" } else { "" } + $markdown += "- $($entry.Description)$prLink" + } + } + + # Entries with subsections + $subsections = $sectionData.Keys | Where-Object { $_ -ne "" } | Sort-Object + foreach ($subsection in $subsections) { + $markdown += "" + $markdown += "#### $subsection" + foreach ($entry in $sectionData[$subsection]) { + $prLink = if ($entry.PR -gt 0) { " [[#$($entry.PR)](https://github.com/microsoft/mcp/pull/$($entry.PR))]" } else { "" } + $markdown += "- $($entry.Description)$prLink" + } + } +} + +# Preview output +Write-Host "Compiled Output:" -ForegroundColor Cyan +Write-Host "================" -ForegroundColor Cyan +Write-Host "" +$markdown | ForEach-Object { Write-Host $_ -ForegroundColor Gray } +Write-Host "" + +if ($DryRun) { + Write-Host "DRY RUN - No files were modified" -ForegroundColor Yellow + exit 0 +} + +# Read existing CHANGELOG.md +$changelogContent = Get-Content -Path $changelogFile -Raw + +# Find the Unreleased section +$unreleasedPattern = '(?s)(##\s+.*?Unreleased.*?\r?\n)(.*?)(?=##\s+\d+\.\d+\.\d+|$)' +$match = [regex]::Match($changelogContent, $unreleasedPattern) + +if (-not $match.Success) { + Write-Error "Could not find 'Unreleased' section in CHANGELOG.md" + exit 1 +} + +$unreleasedHeader = $match.Groups[1].Value +$existingContent = $match.Groups[2].Value + +# Check if there's already content under unreleased +if ($existingContent.Trim()) { + Write-Warning "Existing content found under 'Unreleased' section" + Write-Host "This script will append new entries. You may need to manually review and merge." -ForegroundColor Yellow + Write-Host "" +} + +# Insert compiled entries +$newContent = $unreleasedHeader + ($markdown -join "`n") + "`n" + $existingContent + +# Replace in changelog +$updatedChangelog = $changelogContent -replace [regex]::Escape($unreleasedHeader + $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 diff --git a/eng/scripts/New-ChangelogEntry.ps1 b/eng/scripts/New-ChangelogEntry.ps1 new file mode 100644 index 0000000000..dd1d45624f --- /dev/null +++ b/eng/scripts/New-ChangelogEntry.ps1 @@ -0,0 +1,209 @@ +#!/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 (e.g., "Dependency Updates", "Telemetry"). + +.PARAMETER PR + Pull request number (integer). + +.PARAMETER ChangelogEntriesPath + Path to the changelog-entries directory. Defaults to servers/Azure.Mcp.Server/changelog-entries. + +.EXAMPLE + ./eng/scripts/New-ChangelogEntry.ps1 + + Runs in interactive mode, prompting for all required fields. + +.EXAMPLE + ./eng/scripts/New-ChangelogEntry.ps1 -Description "Added new feature" -Section "Features Added" -PR 1234 + + Creates a changelog entry with the specified parameters. + +.EXAMPLE + ./eng/scripts/New-ChangelogEntry.ps1 -Description "Updated Azure.Core to 1.2.3" -Section "Other Changes" -Subsection "Dependency Updates" -PR 1234 + + Creates a changelog entry with a subsection. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [ValidateLength(10, 1000)] + [string]$Description, + + [Parameter(Mandatory = $false)] + [ValidateSet("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes")] + [string]$Section, + + [Parameter(Mandatory = $false)] + [string]$Subsection, + + [Parameter(Mandatory = $false)] + [ValidateRange(1, [int]::MaxValue)] + [int]$PR, + + [Parameter(Mandatory = $false)] + [string]$ChangelogEntriesPath = "servers/Azure.Mcp.Server/changelog-entries" +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Get repository root +$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +$changelogEntriesDir = Join-Path $repoRoot $ChangelogEntriesPath +$schemaPath = Join-Path $repoRoot "eng/schemas/changelog-entry.schema.json" + +# 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 "`nChangelog Entry Creator" -ForegroundColor Cyan + Write-Host "======================" -ForegroundColor Cyan + Write-Host "" + + $Description = Read-Host "Description (minimum 10 characters)" + while ($Description.Length -lt 10) { + Write-Host "Description must be at least 10 characters long." -ForegroundColor Red + $Description = Read-Host "Description (minimum 10 characters)" + } +} + +if (-not $Section) { + Write-Host "`nSelect a section:" -ForegroundColor Cyan + Write-Host "1. Features Added" + Write-Host "2. Breaking Changes" + Write-Host "3. Bugs Fixed" + Write-Host "4. Other Changes" + + $choice = Read-Host "`nEnter choice (1-4)" + $Section = switch ($choice) { + "1" { "Features Added" } + "2" { "Breaking Changes" } + "3" { "Bugs Fixed" } + "4" { "Other Changes" } + default { + Write-Error "Invalid choice. Please select 1-4." + exit 1 + } + } +} + +if (-not $Subsection -and $Section -eq "Other Changes") { + $subsectionInput = Read-Host "`nSubsection (optional, press Enter to skip)" + if ($subsectionInput) { + $Subsection = $subsectionInput + } +} + +if (-not $PR) { + $prInput = Read-Host "`nPR number (press Enter to skip if not known yet)" + if ($prInput) { + $PR = [int]$prInput + } +} + +# Generate filename with timestamp in milliseconds +$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() +$filename = "$timestamp.yml" +$filepath = Join-Path $changelogEntriesDir $filename + +# Create YAML content +$yamlContent = @" +section: "$Section" +description: "$Description" +"@ + +if ($Subsection) { + $yamlContent += "`nsubsection: `"$Subsection`"" +} else { + $yamlContent += "`nsubsection: null" +} + +if ($PR) { + $yamlContent += "`npr: $PR" +} else { + $yamlContent += "`npr: 0 # TODO: Update with actual PR number" +} + +# Write YAML file +$yamlContent | Set-Content -Path $filepath -Encoding UTF8 -NoNewline + +Write-Host "`n✓ Created changelog entry: $filename" -ForegroundColor Green +Write-Host " Location: $filepath" -ForegroundColor Gray +Write-Host " Section: $Section" -ForegroundColor Gray +if ($Subsection) { + Write-Host " Subsection: $Subsection" -ForegroundColor Gray +} +Write-Host " Description: $Description" -ForegroundColor Gray +if ($PR) { + Write-Host " PR: #$PR" -ForegroundColor Gray +} else { + Write-Host " PR: Not set (remember to update before merging)" -ForegroundColor Yellow +} + +# Validate against schema if available +if (Test-Path $schemaPath) { + Write-Host "`nValidating against schema..." -ForegroundColor Cyan + + # 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 + $schemaData = Get-Content -Path $schemaPath -Raw | ConvertFrom-Json + + # Basic validation + $validSections = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") + if ($yamlData.section -notin $validSections) { + Write-Error "Invalid section: $($yamlData.section)" + exit 1 + } + + if ($yamlData.description.Length -lt 10) { + Write-Error "Description must be at least 10 characters" + exit 1 + } + + if ($PR -and $yamlData.pr -lt 1) { + Write-Error "PR number must be positive" + exit 1 + } + + Write-Host "✓ Validation passed" -ForegroundColor Green + } + catch { + Write-Warning "Could not validate YAML: $_" + } + } + else { + Write-Host " Note: Install 'powershell-yaml' module for automatic validation" -ForegroundColor Gray + Write-Host " Run: Install-Module -Name powershell-yaml" -ForegroundColor Gray + } +} + +Write-Host "`nNext steps:" -ForegroundColor Cyan +Write-Host "1. Commit this file with your changes" -ForegroundColor Gray +if (-not $PR) { + Write-Host "2. Update the 'pr' field in the YAML file once you have a PR number" -ForegroundColor Gray +} +Write-Host "" From 924b6505911ac843d1eb3d81d8b928980c067814 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 14:30:31 -0800 Subject: [PATCH 04/26] Removed old YAML file --- servers/Azure.Mcp.Server/changelog/sample-entry.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 servers/Azure.Mcp.Server/changelog/sample-entry.yml diff --git a/servers/Azure.Mcp.Server/changelog/sample-entry.yml b/servers/Azure.Mcp.Server/changelog/sample-entry.yml deleted file mode 100644 index e69de29bb2..0000000000 From 2ae6319c13318e7ac1a8c3480d60edb534bbd867 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 15:45:06 -0800 Subject: [PATCH 05/26] Added some QoL changes to the scripts --- eng/scripts/Compile-Changelog.ps1 | 225 ++++++++++++++++++++++++++--- eng/scripts/New-ChangelogEntry.ps1 | 67 +++++++-- 2 files changed, 256 insertions(+), 36 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 37c2b4a03d..4adfc96e24 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -54,6 +54,19 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" +# 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.ToLower()) +} + # Get repository root $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $changelogFile = Join-Path $repoRoot $ChangelogPath @@ -121,7 +134,12 @@ foreach ($file in $yamlFiles) { continue } - if ($entry.section -notin $validSections) { + # Validate and normalize section (case-insensitive) + $matchedSection = $validSections | Where-Object { $_ -ieq $entry.section } + if ($matchedSection) { + # Use the properly cased version + $normalizedSection = $matchedSection + } else { Write-Error " Invalid section '$($entry.section)' in $($file.Name). Must be one of: $($validSections -join ', ')" continue } @@ -144,9 +162,9 @@ foreach ($file in $yamlFiles) { continue } - # Add to entries collection + # Add to entries collection with normalized section name $entries += [PSCustomObject]@{ - Section = $entry.section + Section = $normalizedSection Subsection = if ($entry.subsection -and $entry.subsection -ne "null") { $entry.subsection } else { $null } Description = $entry.description PR = $entry.pr @@ -250,26 +268,191 @@ $unreleasedPattern = '(?s)(##\s+.*?Unreleased.*?\r?\n)(.*?)(?=##\s+\d+\.\d+\.\d+ $match = [regex]::Match($changelogContent, $unreleasedPattern) if (-not $match.Success) { - Write-Error "Could not find 'Unreleased' section in CHANGELOG.md" - exit 1 -} - -$unreleasedHeader = $match.Groups[1].Value -$existingContent = $match.Groups[2].Value - -# Check if there's already content under unreleased -if ($existingContent.Trim()) { - Write-Warning "Existing content found under 'Unreleased' section" - Write-Host "This script will append new entries. You may need to manually review and merge." -ForegroundColor Yellow - Write-Host "" + Write-Warning "No 'Unreleased' section found in CHANGELOG.md" + Write-Host "Creating new Unreleased section..." -ForegroundColor Yellow + + # Create a new unreleased section at the top of the changelog + $newUnreleasedSection = "## Unreleased`n" + $newUnreleasedSection += ($markdown -join "`n") + "`n`n" + + # Insert after the first heading (usually # Changelog or # Release History) + $firstHeadingPattern = '(#[^#].*?\r?\n)' + $firstHeadingMatch = [regex]::Match($changelogContent, $firstHeadingPattern) + + if ($firstHeadingMatch.Success) { + $updatedChangelog = $changelogContent -replace $firstHeadingPattern, "$($firstHeadingMatch.Value)`n$newUnreleasedSection" + } else { + # If no heading found, just prepend to the file + $updatedChangelog = $newUnreleasedSection + $changelogContent + } +} else { + $unreleasedHeader = $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+(.+)$') { + # Entry line + if ($currentSection) { + if ($currentSubsection) { + $existingSections[$currentSection][$currentSubsection] += $line + } else { + $existingSections[$currentSection][""] += $line + } + } + } + } + + # Build new content by merging existing and new entries + $newContent = $unreleasedHeader + + foreach ($section in $sectionOrder) { + # 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 + } + + $newContent += "`n### $section`n" + + # Merge entries without subsection + $existingMainEntries = @() + if ($hasExisting -and $existingSections[$section].ContainsKey("")) { + $existingMainEntries = @($existingSections[$section][""]) + } + $newMainEntries = @() + + if ($hasNew -and $groupedEntries[$section].ContainsKey("")) { + foreach ($entry in $groupedEntries[$section][""]) { + $description = $entry.Description + # Ensure description ends with a period + if (-not $description.EndsWith(".")) { + $description = $description + "." + } + $prLink = if ($entry.PR -gt 0) { " [[#$($entry.PR)](https://github.com/microsoft/mcp/pull/$($entry.PR))]" } else { "" } + $newMainEntries += "- $description$prLink" + } + } + + # Append new entries after existing ones (with empty line before entries) + $totalEntries = $existingMainEntries.Count + $newMainEntries.Count + if ($totalEntries -gt 0) { + $newContent += "`n" # Empty line before entries + foreach ($line in $existingMainEntries) { + if ($line) { + $newContent += "$line`n" + } + } + foreach ($line in $newMainEntries) { + $newContent += "$line`n" + } + } + + # Merge subsections - normalize subsection names (case-insensitive, title case) + $allSubsections = @() + $subsectionMapping = @{} # Maps normalized (title-cased) name to actual name + + 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 + } + } + } + } + } + + $allSubsections = $subsectionMapping.Keys | Sort-Object + + foreach ($subsectionTitleCased in $allSubsections) { + $mapping = $subsectionMapping[$subsectionTitleCased] + $newContent += "`n#### $subsectionTitleCased`n" + $newContent += "`n" # Empty line before entries + + # Existing subsection entries + if ($mapping.Existing -and $hasExisting -and $existingSections[$section].ContainsKey($mapping.Existing)) { + foreach ($line in $existingSections[$section][$mapping.Existing]) { + $newContent += "$line`n" + } + } + + # New subsection entries + if ($mapping.New -and $hasNew -and $groupedEntries[$section].ContainsKey($mapping.New)) { + foreach ($entry in $groupedEntries[$section][$mapping.New]) { + $description = $entry.Description + # Ensure description ends with a period + if (-not $description.EndsWith(".")) { + $description = $description + "." + } + $prLink = if ($entry.PR -gt 0) { " [[#$($entry.PR)](https://github.com/microsoft/mcp/pull/$($entry.PR))]" } else { "" } + $newContent += "- $description$prLink`n" + } + } + } + } + + # Ensure there's an empty line at the end of the Unreleased section + if (-not $newContent.EndsWith("`n`n")) { + $newContent += "`n" + } + + # Replace in changelog + $updatedChangelog = $changelogContent -replace [regex]::Escape($unreleasedHeader + $existingContent), $newContent } -# Insert compiled entries -$newContent = $unreleasedHeader + ($markdown -join "`n") + "`n" + $existingContent - -# Replace in changelog -$updatedChangelog = $changelogContent -replace [regex]::Escape($unreleasedHeader + $existingContent), $newContent - # Write updated CHANGELOG.md $updatedChangelog | Set-Content -Path $changelogFile -Encoding UTF8 -NoNewline diff --git a/eng/scripts/New-ChangelogEntry.ps1 b/eng/scripts/New-ChangelogEntry.ps1 index dd1d45624f..d428075abe 100644 --- a/eng/scripts/New-ChangelogEntry.ps1 +++ b/eng/scripts/New-ChangelogEntry.ps1 @@ -41,18 +41,15 @@ [CmdletBinding()] param( [Parameter(Mandatory = $false)] - [ValidateLength(10, 1000)] [string]$Description, [Parameter(Mandatory = $false)] - [ValidateSet("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes")] [string]$Section, [Parameter(Mandatory = $false)] [string]$Subsection, [Parameter(Mandatory = $false)] - [ValidateRange(1, [int]::MaxValue)] [int]$PR, [Parameter(Mandatory = $false)] @@ -62,6 +59,40 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" +# Valid sections for validation +$validSections = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") + +# 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 { + Write-Host "" + Write-Host "ERROR: Invalid section '$Section'" -ForegroundColor Red + Write-Host "" + Write-Host "Valid sections are:" -ForegroundColor Yellow + Write-Host " - Features Added" -ForegroundColor Yellow + Write-Host " - Breaking Changes" -ForegroundColor Yellow + Write-Host " - Bugs Fixed" -ForegroundColor Yellow + Write-Host " - Other Changes" -ForegroundColor Yellow + Write-Host "" + Write-Host "Example usage:" -ForegroundColor Cyan + Write-Host ' .\eng\scripts\New-ChangelogEntry.ps1 -Section "Features Added" -Description "..." -PR 1234' -ForegroundColor Gray + Write-Host "" + exit 1 + } +} + +# Validate PR parameter if provided +if ($PR -and $PR -lt 1) { + Write-Host "" + Write-Host "ERROR: PR number must be a positive integer (got: $PR)" -ForegroundColor Red + Write-Host "" + exit 1 +} + # Get repository root $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $changelogEntriesDir = Join-Path $repoRoot $ChangelogEntriesPath @@ -100,7 +131,9 @@ if (-not $Section) { "3" { "Bugs Fixed" } "4" { "Other Changes" } default { - Write-Error "Invalid choice. Please select 1-4." + Write-Host "" + Write-Host "ERROR: Invalid choice '$choice'. Please select 1-4." -ForegroundColor Red + Write-Host "" exit 1 } } @@ -109,7 +142,9 @@ if (-not $Section) { if (-not $Subsection -and $Section -eq "Other Changes") { $subsectionInput = Read-Host "`nSubsection (optional, press Enter to skip)" if ($subsectionInput) { - $Subsection = $subsectionInput + # Title case the subsection + $textInfo = (Get-Culture).TextInfo + $Subsection = $textInfo.ToTitleCase($subsectionInput.ToLower()) } } @@ -120,6 +155,11 @@ if (-not $PR) { } } +# Ensure description ends with a period +if (-not $Description.EndsWith(".")) { + $Description = $Description + "." +} + # Generate filename with timestamp in milliseconds $timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() $filename = "$timestamp.yml" @@ -170,22 +210,19 @@ if (Test-Path $schemaPath) { try { $yamlData = Get-Content -Path $filepath -Raw | ConvertFrom-Yaml - $schemaData = Get-Content -Path $schemaPath -Raw | ConvertFrom-Json - - # Basic validation - $validSections = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") - if ($yamlData.section -notin $validSections) { - Write-Error "Invalid section: $($yamlData.section)" - exit 1 - } + # Basic validation (section already validated at top of script) if ($yamlData.description.Length -lt 10) { - Write-Error "Description must be at least 10 characters" + Write-Host "" + Write-Host "ERROR: Description must be at least 10 characters" -ForegroundColor Red + Write-Host "" exit 1 } if ($PR -and $yamlData.pr -lt 1) { - Write-Error "PR number must be positive" + Write-Host "" + Write-Host "ERROR: PR number must be positive" -ForegroundColor Red + Write-Host "" exit 1 } From 71ff06c5f09c63f2453f66e551ecb0a18eb6475f Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 16:20:27 -0800 Subject: [PATCH 06/26] Added a way to provide a specific version to compile entries into --- eng/scripts/Compile-Changelog.ps1 | 159 +++++++++++++++--- .../changelog-entries/README.md | 17 +- 2 files changed, 146 insertions(+), 30 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 4adfc96e24..06b6122392 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -6,7 +6,11 @@ .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 "Unreleased" section. + 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. Defaults to servers/Azure.Mcp.Server/CHANGELOG.md. @@ -14,6 +18,10 @@ .PARAMETER ChangelogEntriesPath Path to the changelog-entries directory. Defaults to servers/Azure.Mcp.Server/changelog-entries. +.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. @@ -28,7 +36,12 @@ .EXAMPLE ./eng/scripts/Compile-Changelog.ps1 - Compile entries into CHANGELOG.md. + Compile entries into the Unreleased section of CHANGELOG.md. + +.EXAMPLE + ./eng/scripts/Compile-Changelog.ps1 -Version "2.0.0-beta.3" + + Compile entries into the 2.0.0-beta.3 version section. .EXAMPLE ./eng/scripts/Compile-Changelog.ps1 -DeleteFiles @@ -44,6 +57,9 @@ param( [Parameter(Mandatory = $false)] [string]$ChangelogEntriesPath = "servers/Azure.Mcp.Server/changelog-entries", + [Parameter(Mandatory = $false)] + [string]$Version, + [Parameter(Mandatory = $false)] [switch]$DryRun, @@ -206,10 +222,12 @@ foreach ($section in $sectionOrder) { } foreach ($entry in $withSubsection) { - if (-not $groupedEntries[$section].ContainsKey($entry.Subsection)) { - $groupedEntries[$section][$entry.Subsection] = @() + # Normalize subsection name to title case for grouping + $normalizedSubsection = ConvertTo-TitleCase $entry.Subsection + if (-not $groupedEntries[$section].ContainsKey($normalizedSubsection)) { + $groupedEntries[$section][$normalizedSubsection] = @() } - $groupedEntries[$section][$entry.Subsection] += $entry + $groupedEntries[$section][$normalizedSubsection] += $entry } } } @@ -241,6 +259,7 @@ foreach ($section in $sectionOrder) { foreach ($subsection in $subsections) { $markdown += "" $markdown += "#### $subsection" + $markdown += "" # Empty line before subsection entries foreach ($entry in $sectionData[$subsection]) { $prLink = if ($entry.PR -gt 0) { " [[#$($entry.PR)](https://github.com/microsoft/mcp/pull/$($entry.PR))]" } else { "" } $markdown += "- $($entry.Description)$prLink" @@ -248,6 +267,92 @@ foreach ($section in $sectionOrder) { } } +# Read existing CHANGELOG.md to determine target version +$changelogContent = Get-Content -Path $changelogFile -Raw + +# Determine target version section +$targetVersionHeader = $null +$isUnreleased = $false + +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 + $isUnreleased = $targetVersionHeader -match '\(Unreleased\)' + 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 + $firstSectionPattern = '(?m)^##\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 + $isUnreleased = $true + 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 + $currentVersion = $firstSectionMatch.Groups[1].Value.Trim() + Write-Host "Current version: $currentVersion" -ForegroundColor Gray + + # Parse semantic version (handles formats like "2.0.0-beta.3" or "1.5.2") + if ($currentVersion -match '^(\d+)\.(\d+)\.(\d+)(?:-(.+?)\.(\d+))?') { + $major = [int]$matches[1] + $minor = [int]$matches[2] + $patch = [int]$matches[3] + $prerelease = $matches[4] + $prereleaseNum = if ($matches[5]) { [int]$matches[5] } else { 0 } + + # Increment based on prerelease status + if ($prerelease) { + # Increment prerelease number (e.g., beta.3 -> beta.4) + $nextVersion = "$major.$minor.$patch-$prerelease.$($prereleaseNum + 1)" + } else { + # Increment patch version (e.g., 1.5.2 -> 1.5.3) + $nextVersion = "$major.$minor.$($patch + 1)" + } + + $targetVersionHeader = "## $nextVersion (Unreleased)" + $isUnreleased = $true + 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 "" + # Preview output Write-Host "Compiled Output:" -ForegroundColor Cyan Write-Host "================" -ForegroundColor Cyan @@ -259,34 +364,32 @@ if ($DryRun) { Write-Host "DRY RUN - No files were modified" -ForegroundColor Yellow exit 0 } - -# Read existing CHANGELOG.md -$changelogContent = Get-Content -Path $changelogFile -Raw - -# Find the Unreleased section -$unreleasedPattern = '(?s)(##\s+.*?Unreleased.*?\r?\n)(.*?)(?=##\s+\d+\.\d+\.\d+|$)' -$match = [regex]::Match($changelogContent, $unreleasedPattern) +# 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) if (-not $match.Success) { - Write-Warning "No 'Unreleased' section found in CHANGELOG.md" - Write-Host "Creating new Unreleased section..." -ForegroundColor Yellow + Write-Host "Target section '$targetVersionHeader' not found in CHANGELOG.md" -ForegroundColor Yellow + Write-Host "Creating new section..." -ForegroundColor Yellow - # Create a new unreleased section at the top of the changelog - $newUnreleasedSection = "## Unreleased`n" - $newUnreleasedSection += ($markdown -join "`n") + "`n`n" + # Create a new section + $newVersionSection = "$targetVersionHeader`n" + $newVersionSection += ($markdown -join "`n") + "`n`n" - # Insert after the first heading (usually # Changelog or # Release History) - $firstHeadingPattern = '(#[^#].*?\r?\n)' - $firstHeadingMatch = [regex]::Match($changelogContent, $firstHeadingPattern) + # Find the first ## section and insert before it + $firstSectionPattern = '(?m)^##\s+' + $firstSectionMatch = [regex]::Match($changelogContent, $firstSectionPattern) - if ($firstHeadingMatch.Success) { - $updatedChangelog = $changelogContent -replace $firstHeadingPattern, "$($firstHeadingMatch.Value)`n$newUnreleasedSection" + if ($firstSectionMatch.Success) { + # Insert the new section right before the first ## section + $insertPosition = $firstSectionMatch.Index + $updatedChangelog = $changelogContent.Insert($insertPosition, "$newVersionSection") } else { - # If no heading found, just prepend to the file - $updatedChangelog = $newUnreleasedSection + $changelogContent + # If no ## section found, append to the end of the file + $updatedChangelog = $changelogContent + "`n$newVersionSection" } } else { - $unreleasedHeader = $match.Groups[1].Value + $versionHeader = $match.Groups[1].Value $existingContent = $match.Groups[2].Value # Parse existing content by sections @@ -335,7 +438,7 @@ if (-not $match.Success) { } # Build new content by merging existing and new entries - $newContent = $unreleasedHeader + $newContent = $versionHeader foreach ($section in $sectionOrder) { # Check if we have entries (existing or new) for this section @@ -444,13 +547,13 @@ if (-not $match.Success) { } } - # Ensure there's an empty line at the end of the Unreleased section + # Ensure there's an empty line at the end of the version section if (-not $newContent.EndsWith("`n`n")) { $newContent += "`n" } # Replace in changelog - $updatedChangelog = $changelogContent -replace [regex]::Escape($unreleasedHeader + $existingContent), $newContent + $updatedChangelog = $changelogContent -replace [regex]::Escape($versionHeader + $existingContent), $newContent } # Write updated CHANGELOG.md diff --git a/servers/Azure.Mcp.Server/changelog-entries/README.md b/servers/Azure.Mcp.Server/changelog-entries/README.md index 949c02b797..9bf11d7e0a 100644 --- a/servers/Azure.Mcp.Server/changelog-entries/README.md +++ b/servers/Azure.Mcp.Server/changelog-entries/README.md @@ -116,19 +116,32 @@ Before tagging a release: 1. **Preview compilation:** ```powershell + # Compile to the default Unreleased section ./eng/scripts/Compile-Changelog.ps1 -DryRun + + # Or compile to a specific version + ./eng/scripts/Compile-Changelog.ps1 -Version "2.0.0-beta.3" -DryRun ``` 2. **Compile entries:** ```powershell + # Compile to Unreleased section and delete YAML files ./eng/scripts/Compile-Changelog.ps1 -DeleteFiles + + # Or compile to a specific version + ./eng/scripts/Compile-Changelog.ps1 -Version "2.0.0-beta.3" -DeleteFiles ``` -3. **Update CHANGELOG.md:** +3. **Version behavior:** + - 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 + +4. **Update CHANGELOG.md** (if compiling to Unreleased): - Change "Unreleased" to the actual version number - Add release date -4. **Commit and tag the release** +5. **Commit and tag the release** ## Compiled Output From 2b8ff98604a0a4a70c38f9850b4ca1bad96e6e8b Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 16:27:49 -0800 Subject: [PATCH 07/26] Updated dry run code --- eng/scripts/Compile-Changelog.ps1 | 106 ++++++++++++++++++------------ 1 file changed, 65 insertions(+), 41 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 06b6122392..4cf1851a46 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -353,42 +353,19 @@ if ($Version) { Write-Host "Target section: $targetVersionHeader" -ForegroundColor Cyan Write-Host "" -# Preview output -Write-Host "Compiled Output:" -ForegroundColor Cyan -Write-Host "================" -ForegroundColor Cyan -Write-Host "" -$markdown | ForEach-Object { Write-Host $_ -ForegroundColor Gray } -Write-Host "" - -if ($DryRun) { - Write-Host "DRY RUN - No files were modified" -ForegroundColor Yellow - exit 0 -} # 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) { - Write-Host "Target section '$targetVersionHeader' not found in CHANGELOG.md" -ForegroundColor Yellow - Write-Host "Creating new section..." -ForegroundColor Yellow - - # Create a new section - $newVersionSection = "$targetVersionHeader`n" - $newVersionSection += ($markdown -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" - } + # New section - just use the new entries + $mergedContent += $markdown } else { + # Existing section - merge with existing content $versionHeader = $match.Groups[1].Value $existingContent = $match.Groups[2].Value @@ -437,9 +414,8 @@ if (-not $match.Success) { } } - # Build new content by merging existing and new entries - $newContent = $versionHeader - + # Build merged content by combining existing and new entries + $isFirstSection = $true foreach ($section in $sectionOrder) { # Check if we have entries (existing or new) for this section $hasExisting = $existingSections.ContainsKey($section) @@ -449,7 +425,13 @@ if (-not $match.Success) { continue } - $newContent += "`n### $section`n" + # Add empty line before section (but not before the very first section) + if (-not $isFirstSection) { + $mergedContent += "" + } + $isFirstSection = $false + + $mergedContent += "### $section" # Merge entries without subsection $existingMainEntries = @() @@ -473,14 +455,14 @@ if (-not $match.Success) { # Append new entries after existing ones (with empty line before entries) $totalEntries = $existingMainEntries.Count + $newMainEntries.Count if ($totalEntries -gt 0) { - $newContent += "`n" # Empty line before entries + $mergedContent += "" # Empty line before entries foreach ($line in $existingMainEntries) { if ($line) { - $newContent += "$line`n" + $mergedContent += $line } } foreach ($line in $newMainEntries) { - $newContent += "$line`n" + $mergedContent += $line } } @@ -522,13 +504,14 @@ if (-not $match.Success) { foreach ($subsectionTitleCased in $allSubsections) { $mapping = $subsectionMapping[$subsectionTitleCased] - $newContent += "`n#### $subsectionTitleCased`n" - $newContent += "`n" # Empty line before entries + $mergedContent += "" + $mergedContent += "#### $subsectionTitleCased" + $mergedContent += "" # Empty line before entries # Existing subsection entries if ($mapping.Existing -and $hasExisting -and $existingSections[$section].ContainsKey($mapping.Existing)) { foreach ($line in $existingSections[$section][$mapping.Existing]) { - $newContent += "$line`n" + $mergedContent += $line } } @@ -541,11 +524,52 @@ if (-not $match.Success) { $description = $description + "." } $prLink = if ($entry.PR -gt 0) { " [[#$($entry.PR)](https://github.com/microsoft/mcp/pull/$($entry.PR))]" } else { "" } - $newContent += "- $description$prLink`n" + $mergedContent += "- $description$prLink" } } } } +} + +# 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 if (-not $newContent.EndsWith("`n`n")) { From 2514c8e781b9958de867ae190e8a2660e5f5ae7e Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 16:38:54 -0800 Subject: [PATCH 08/26] Fixed the compile script to not delete entries with different indentation --- eng/scripts/Compile-Changelog.ps1 | 169 +++++++++++++----------------- 1 file changed, 75 insertions(+), 94 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 4cf1851a46..e1e0694cc6 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -83,11 +83,73 @@ function ConvertTo-TitleCase { return $textInfo.ToTitleCase($Text.ToLower()) } +# Helper function to format a changelog entry with description and PR link +function Format-ChangelogEntry { + param( + [string]$Description, + [int]$PR + ) + + # Ensure description ends with a period + $formattedDescription = $Description + if (-not $formattedDescription.EndsWith(".")) { + $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 +} + # Get repository root $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $changelogFile = Join-Path $repoRoot $ChangelogPath $changelogEntriesDir = Join-Path $repoRoot $ChangelogEntriesPath -$schemaPath = Join-Path $repoRoot "eng/schemas/changelog-entry.schema.json" Write-Host "Changelog Compiler" -ForegroundColor Cyan Write-Host "==================" -ForegroundColor Cyan @@ -125,9 +187,6 @@ if (-not $yamlModule) { } Import-Module powershell-yaml -ErrorAction Stop -# Load schema -$schema = Get-Content -Path $schemaPath -Raw | ConvertFrom-Json - # Parse and validate YAML files $entries = @() $validSections = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") @@ -232,47 +291,11 @@ foreach ($section in $sectionOrder) { } } -# Generate markdown -$markdown = @() - -foreach ($section in $sectionOrder) { - if (-not $groupedEntries.ContainsKey($section)) { - continue - } - - $markdown += "" - $markdown += "### $section" - $markdown += "" - - $sectionData = $groupedEntries[$section] - - # Entries without subsection first - if ($sectionData.ContainsKey("")) { - foreach ($entry in $sectionData[""]) { - $prLink = if ($entry.PR -gt 0) { " [[#$($entry.PR)](https://github.com/microsoft/mcp/pull/$($entry.PR))]" } else { "" } - $markdown += "- $($entry.Description)$prLink" - } - } - - # Entries with subsections - $subsections = $sectionData.Keys | Where-Object { $_ -ne "" } | Sort-Object - foreach ($subsection in $subsections) { - $markdown += "" - $markdown += "#### $subsection" - $markdown += "" # Empty line before subsection entries - foreach ($entry in $sectionData[$subsection]) { - $prLink = if ($entry.PR -gt 0) { " [[#$($entry.PR)](https://github.com/microsoft/mcp/pull/$($entry.PR))]" } else { "" } - $markdown += "- $($entry.Description)$prLink" - } - } -} - # Read existing CHANGELOG.md to determine target version $changelogContent = Get-Content -Path $changelogFile -Raw # Determine target version section $targetVersionHeader = $null -$isUnreleased = $false if ($Version) { # User specified a version - find it in the changelog @@ -289,7 +312,6 @@ if ($Version) { } $targetVersionHeader = $versionMatch.Value - $isUnreleased = $targetVersionHeader -match '\(Unreleased\)' Write-Host "✓ Found version section: $targetVersionHeader" -ForegroundColor Green Write-Host "" } else { @@ -306,7 +328,6 @@ if ($Version) { # Check if it's Unreleased if ($firstSectionFull -match '\(Unreleased\)') { $targetVersionHeader = $firstSectionFull - $isUnreleased = $true Write-Host "✓ Found Unreleased section: $targetVersionHeader" -ForegroundColor Green Write-Host "" } else { @@ -336,7 +357,6 @@ if ($Version) { } $targetVersionHeader = "## $nextVersion (Unreleased)" - $isUnreleased = $true Write-Host "Next version: $nextVersion" -ForegroundColor Green Write-Host "" } else { @@ -402,8 +422,9 @@ if (-not $match.Success) { } } } - elseif ($line -match '^-\s+(.+)$') { - # Entry line + 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 @@ -442,13 +463,7 @@ if (-not $match.Success) { if ($hasNew -and $groupedEntries[$section].ContainsKey("")) { foreach ($entry in $groupedEntries[$section][""]) { - $description = $entry.Description - # Ensure description ends with a period - if (-not $description.EndsWith(".")) { - $description = $description + "." - } - $prLink = if ($entry.PR -gt 0) { " [[#$($entry.PR)](https://github.com/microsoft/mcp/pull/$($entry.PR))]" } else { "" } - $newMainEntries += "- $description$prLink" + $newMainEntries += Format-ChangelogEntry -Description $entry.Description -PR $entry.PR } } @@ -467,39 +482,7 @@ if (-not $match.Success) { } # Merge subsections - normalize subsection names (case-insensitive, title case) - $allSubsections = @() - $subsectionMapping = @{} # Maps normalized (title-cased) name to actual name - - 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 - } - } - } - } - } - + $subsectionMapping = Build-SubsectionMapping -ExistingSections $existingSections -GroupedEntries $groupedEntries -Section $section $allSubsections = $subsectionMapping.Keys | Sort-Object foreach ($subsectionTitleCased in $allSubsections) { @@ -518,13 +501,7 @@ if (-not $match.Success) { # New subsection entries if ($mapping.New -and $hasNew -and $groupedEntries[$section].ContainsKey($mapping.New)) { foreach ($entry in $groupedEntries[$section][$mapping.New]) { - $description = $entry.Description - # Ensure description ends with a period - if (-not $description.EndsWith(".")) { - $description = $description + "." - } - $prLink = if ($entry.PR -gt 0) { " [[#$($entry.PR)](https://github.com/microsoft/mcp/pull/$($entry.PR))]" } else { "" } - $mergedContent += "- $description$prLink" + $mergedContent += Format-ChangelogEntry -Description $entry.Description -PR $entry.PR } } } @@ -571,9 +548,13 @@ if (-not $match.Success) { # Build the new content from merged array $newContent = ($mergedContent -join "`n") - # Ensure there's an empty line at the end of the version section + # Ensure there's an empty line at the end of the version section (two newlines total) if (-not $newContent.EndsWith("`n`n")) { - $newContent += "`n" + if ($newContent.EndsWith("`n")) { + $newContent += "`n" + } else { + $newContent += "`n`n" + } } # Replace in changelog From 1588122d9bd767a052eb52b49f8d379593f734eb Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 16:47:37 -0800 Subject: [PATCH 09/26] Added support for multi-line entries --- eng/scripts/Compile-Changelog.ps1 | 68 ++++++++++++++++--- .../changelog-entries/README.md | 25 ++++++- 2 files changed, 84 insertions(+), 9 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index e1e0694cc6..d179f6feb0 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -90,15 +90,67 @@ function Format-ChangelogEntry { [int]$PR ) - # Ensure description ends with a period - $formattedDescription = $Description - if (-not $formattedDescription.EndsWith(".")) { - $formattedDescription += "." + # 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 = @() + + for ($i = 0; $i -lt $lines.Length; $i++) { + $line = $lines[$i] + + if ($i -eq 0) { + # First line: main bullet point + # Ensure it ends with colon if followed by a list, otherwise period + if ($lines.Length -gt 1 -and $lines[1].TrimStart() -match '^-\s+') { + # Next line is a bullet, so first line should end with colon + if (-not $line.EndsWith(":")) { + $line += ":" + } + } + else { + # Regular multi-line text, ensure period at end + if (-not $line.EndsWith(".")) { + $line += "." + } + } + $formattedLines += "- $line" + } + elseif ($i -eq $lines.Length - 1) { + # Last line: add PR link here + $trimmedLine = $line.TrimStart() + $indent = $line.Substring(0, $line.Length - $trimmedLine.Length) + + # For bullet items, don't add period; for regular text, ensure period + $needsPeriod = -not ($trimmedLine -match '^-\s+') + if ($needsPeriod -and -not $trimmedLine.EndsWith(".")) { + $trimmedLine += "." + } + + # Add PR link if available + $prLink = if ($PR -gt 0) { " [[#$PR](https://github.com/microsoft/mcp/pull/$PR)]" } else { "" } + $formattedLines += " $indent$trimmedLine$prLink" + } + else { + # Middle lines: preserve indentation and add 2 spaces for nesting + $formattedLines += " $line" + } + } + + return $formattedLines -join "`n" + } + else { + # Single-line description: original logic + $formattedDescription = $Description + if (-not $formattedDescription.EndsWith(".")) { + $formattedDescription += "." + } + + # Add PR link if available + $prLink = if ($PR -gt 0) { " [[#$PR](https://github.com/microsoft/mcp/pull/$PR)]" } else { "" } + return "- $formattedDescription$prLink" } - - # 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) diff --git a/servers/Azure.Mcp.Server/changelog-entries/README.md b/servers/Azure.Mcp.Server/changelog-entries/README.md index 9bf11d7e0a..ab473d88f3 100644 --- a/servers/Azure.Mcp.Server/changelog-entries/README.md +++ b/servers/Azure.Mcp.Server/changelog-entries/README.md @@ -88,7 +88,9 @@ If you prefer to create the file manually: - `Bugs Fixed` - Bug fixes - `Other Changes` - Everything else (dependency updates, refactoring, etc.) -### Example Entry +### Example Entries + +**Simple entry:** **Filename:** `1731260400123.yml` @@ -99,6 +101,27 @@ subsection: null pr: 1033 ``` +**Multi-line entry with a list:** + +**Filename:** `1731260401234.yml` + +```yaml +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 +subsection: null +pr: 945 +``` + +**Note:** When using multi-line descriptions with the `|` block scalar: +- The first line becomes the main bullet point +- If the following lines are bullet items (`- item`), they'll be automatically indented as sub-bullets +- The PR link is added to the last line +- Trailing newlines are automatically handled + ## Workflow ### For Contributors From 1b7403b46fe5229b431a190d436003107243faea Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 16:51:34 -0800 Subject: [PATCH 10/26] Added trimming for multi-line --- eng/scripts/Compile-Changelog.ps1 | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index d179f6feb0..04af3a0983 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -90,6 +90,9 @@ function Format-ChangelogEntry { [int]$PR ) + # Trim leading and trailing whitespace from the entire description + $Description = $Description.Trim() + # Check if description contains multiple lines if ($Description.Contains("`n")) { # Remove trailing newlines from YAML block scalars @@ -98,10 +101,11 @@ function Format-ChangelogEntry { $formattedLines = @() for ($i = 0; $i -lt $lines.Length; $i++) { - $line = $lines[$i] + $line = $lines[$i].TrimEnd() # Trim trailing spaces from each line if ($i -eq 0) { - # First line: main bullet point + # 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 ($lines.Length -gt 1 -and $lines[1].TrimStart() -match '^-\s+') { # Next line is a bullet, so first line should end with colon @@ -119,8 +123,10 @@ function Format-ChangelogEntry { } elseif ($i -eq $lines.Length - 1) { # Last line: add PR link here + # Line is already trimmed at the end, preserve leading indentation $trimmedLine = $line.TrimStart() - $indent = $line.Substring(0, $line.Length - $trimmedLine.Length) + $leadingSpaces = $line.Length - $trimmedLine.Length + $indent = if ($leadingSpaces -gt 0) { $line.Substring(0, $leadingSpaces) } else { "" } # For bullet items, don't add period; for regular text, ensure period $needsPeriod = -not ($trimmedLine -match '^-\s+') @@ -134,6 +140,7 @@ function Format-ChangelogEntry { } else { # Middle lines: preserve indentation and add 2 spaces for nesting + # Line is already trimmed at the end $formattedLines += " $line" } } From 86d7d6b52dd56b0144fae0e70f2ed93525242e7f Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 16:55:52 -0800 Subject: [PATCH 11/26] Added multi-line support to New-ChangelogEntry.ps1 --- eng/scripts/New-ChangelogEntry.ps1 | 68 ++++++++++++++++--- .../changelog-entries/README.md | 23 ++++++- 2 files changed, 79 insertions(+), 12 deletions(-) diff --git a/eng/scripts/New-ChangelogEntry.ps1 b/eng/scripts/New-ChangelogEntry.ps1 index d428075abe..1ae2e29000 100644 --- a/eng/scripts/New-ChangelogEntry.ps1 +++ b/eng/scripts/New-ChangelogEntry.ps1 @@ -36,6 +36,17 @@ ./eng/scripts/New-ChangelogEntry.ps1 -Description "Updated Azure.Core to 1.2.3" -Section "Other Changes" -Subsection "Dependency Updates" -PR 1234 Creates a changelog entry with a subsection. + +.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()] @@ -59,6 +70,19 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" +# 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.ToLower()) +} + # Valid sections for validation $validSections = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") @@ -93,6 +117,11 @@ if ($PR -and $PR -lt 1) { exit 1 } +# Normalize subsection to title case if provided +if ($Subsection) { + $Subsection = ConvertTo-TitleCase -Text $Subsection.Trim() +} + # Get repository root $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $changelogEntriesDir = Join-Path $repoRoot $ChangelogEntriesPath @@ -109,11 +138,17 @@ if (-not $Description) { Write-Host "`nChangelog Entry Creator" -ForegroundColor Cyan Write-Host "======================" -ForegroundColor Cyan Write-Host "" + 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 10 characters)" + # Trim whitespace from user input + $Description = $Description.Trim() + while ($Description.Length -lt 10) { Write-Host "Description must be at least 10 characters long." -ForegroundColor Red $Description = Read-Host "Description (minimum 10 characters)" + $Description = $Description.Trim() } } @@ -139,12 +174,12 @@ if (-not $Section) { } } -if (-not $Subsection -and $Section -eq "Other Changes") { +# Allow subsection for any section in interactive mode +if (-not $PSBoundParameters.ContainsKey('Subsection')) { $subsectionInput = Read-Host "`nSubsection (optional, press Enter to skip)" if ($subsectionInput) { - # Title case the subsection - $textInfo = (Get-Culture).TextInfo - $Subsection = $textInfo.ToTitleCase($subsectionInput.ToLower()) + # Trim and title case the subsection + $Subsection = ConvertTo-TitleCase -Text $subsectionInput.Trim() } } @@ -155,10 +190,8 @@ if (-not $PR) { } } -# Ensure description ends with a period -if (-not $Description.EndsWith(".")) { - $Description = $Description + "." -} +# Trim whitespace from description if provided via parameter +$Description = $Description.Trim() # Generate filename with timestamp in milliseconds $timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() @@ -166,10 +199,25 @@ $filename = "$timestamp.yml" $filepath = Join-Path $changelogEntriesDir $filename # Create YAML content -$yamlContent = @" +# 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" + $indentedLines = $descriptionLines | ForEach-Object { " $_" } + $yamlContent = @" section: "$Section" -description: "$Description" +description: | +$($indentedLines -join "`n") "@ +} else { + # Single-line description - use quoted string + # Escape double quotes in the description + $escapedDescription = $Description -replace '"', '\"' + $yamlContent = @" +section: "$Section" +description: "$escapedDescription" +"@ +} if ($Subsection) { $yamlContent += "`nsubsection: `"$Subsection`"" diff --git a/servers/Azure.Mcp.Server/changelog-entries/README.md b/servers/Azure.Mcp.Server/changelog-entries/README.md index ab473d88f3..f58ec6cc6f 100644 --- a/servers/Azure.Mcp.Server/changelog-entries/README.md +++ b/servers/Azure.Mcp.Server/changelog-entries/README.md @@ -37,14 +37,33 @@ The easiest way to create a changelog entry is using the generator script: -Section "Features Added" ` -PR 1033 -# With subsection +# With subsection (automatically title-cased) ./eng/scripts/New-ChangelogEntry.ps1 ` -Description "Updated Azure.Core to 1.2.3" ` -Section "Other Changes" ` - -Subsection "Dependency Updates" ` + -Subsection "dependency updates" ` -PR 1234 + +# Multi-line description with a list +$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 ``` +**Features:** +- Automatic title-casing of subsections +- Whitespace trimming from descriptions +- Support for multi-line descriptions with lists +- Interactive validation + ### Manual Creation If you prefer to create the file manually: From 5e40403a2497b9f60cef7b2adf00fd68a5f2cfe3 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 17:30:09 -0800 Subject: [PATCH 12/26] Added a script for syncing the main CHANGELOG with that one for VS Code --- eng/scripts/Sync-VsCodeChangelog.ps1 | 257 +++++++++++++++++++++++++++ 1 file changed, 257 insertions(+) create mode 100644 eng/scripts/Sync-VsCodeChangelog.ps1 diff --git a/eng/scripts/Sync-VsCodeChangelog.ps1 b/eng/scripts/Sync-VsCodeChangelog.ps1 new file mode 100644 index 0000000000..e5548c91f7 --- /dev/null +++ b/eng/scripts/Sync-VsCodeChangelog.ps1 @@ -0,0 +1,257 @@ +#!/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 Azure MCP 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 MainChangelogPath + Path to the main CHANGELOG.md file. Defaults to servers/Azure.Mcp.Server/CHANGELOG.md. + +.PARAMETER VsCodeChangelogPath + Path to the VS Code extension CHANGELOG.md file. Defaults to servers/Azure.Mcp.Server/vscode/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 -DryRun + + Preview the sync without making changes. + +.EXAMPLE + ./eng/scripts/Sync-VsCodeChangelog.ps1 -Version "2.0.3" + + Sync the Unreleased section and create version 2.0.3 entry in VS Code CHANGELOG. +#> + +[CmdletBinding()] +param( + [Parameter(Mandatory = $false)] + [string]$MainChangelogPath = "servers/Azure.Mcp.Server/CHANGELOG.md", + + [Parameter(Mandatory = $false)] + [string]$VsCodeChangelogPath = "servers/Azure.Mcp.Server/vscode/CHANGELOG.md", + + [Parameter(Mandatory = $false)] + [string]$Version, + + [Parameter(Mandatory = $false)] + [switch]$DryRun +) + +Set-StrictMode -Version Latest +$ErrorActionPreference = "Stop" + +# Get repository root +$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent +$mainChangelogFile = Join-Path $repoRoot $MainChangelogPath +$vscodeChangelogFile = Join-Path $repoRoot $VsCodeChangelogPath + +# Validate files exist +if (-not (Test-Path $mainChangelogFile)) { + Write-Error "Main CHANGELOG not found: $mainChangelogFile" + exit 1 +} + +if (-not (Test-Path $vscodeChangelogFile)) { + Write-Error "VS Code CHANGELOG not found: $vscodeChangelogFile" + exit 1 +} + +Write-Host "`nVS Code Changelog Sync" -ForegroundColor Cyan +Write-Host "======================" -ForegroundColor Cyan +Write-Host "" + +# Read the main CHANGELOG +$mainContent = Get-Content -Path $mainChangelogFile -Raw + +# Extract the Unreleased section +$unreleasedMatch = $mainContent -match '(?ms)^## ([\d\.]+-[\w\.]+) \(Unreleased\)\s*\n(.*?)(?=\n## |\z)' +if (-not $unreleasedMatch) { + Write-Error "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 +} + +Write-Host "Source: $mainChangelogFile" -ForegroundColor Gray +Write-Host "Target: $vscodeChangelogFile" -ForegroundColor Gray +Write-Host "Version: $Version" -ForegroundColor Gray +Write-Host "" + +# Parse sections from unreleased content +$sections = @{ + 'Features Added' = @() + 'Breaking Changes' = @() + 'Bugs Fixed' = @() + 'Other Changes' = @() +} + +$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() + $currentEntries = @() + continue + } + + # Skip lines before any section + 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 line +while ($vscodeEntry[-1] -eq "") { + $vscodeEntry = $vscodeEntry[0..($vscodeEntry.Count - 2)] +} + +$vscodeEntryText = $vscodeEntry -join "`n" + +if ($DryRun) { + Write-Host "Preview of new VS Code CHANGELOG entry:" -ForegroundColor Cyan + Write-Host "========================================" -ForegroundColor Cyan + Write-Host "" + Write-Host $vscodeEntryText + Write-Host "" + Write-Host "DRY RUN - No files were modified" -ForegroundColor Yellow + 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) { + Write-Error "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 + +Write-Host "✓ Synced Unreleased section to VS Code CHANGELOG" -ForegroundColor Green +Write-Host " Version: $Version" -ForegroundColor Gray +Write-Host " Location: $vscodeChangelogFile" -ForegroundColor Gray +Write-Host "" +Write-Host "Summary:" -ForegroundColor Cyan + +$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) { + Write-Host " - Added: $addedCount entries" -ForegroundColor Gray +} +if ($breakingCount -gt 0 -or $otherCount -gt 0) { + $totalChanged = $breakingCount + $otherCount + Write-Host " - Changed: $totalChanged entries ($breakingCount breaking, $otherCount other)" -ForegroundColor Gray +} +if ($fixedCount -gt 0) { + Write-Host " - Fixed: $fixedCount entries" -ForegroundColor Gray +} +Write-Host "" +Write-Host "Next steps:" -ForegroundColor Cyan +Write-Host "1. Review the changes in the VS Code CHANGELOG" -ForegroundColor Gray +Write-Host "2. Commit the updated VS Code CHANGELOG with your release" -ForegroundColor Gray +Write-Host "" From e9eb2c1c6afbdd230e1f19e169300e1b2501a894 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 17:56:52 -0800 Subject: [PATCH 13/26] Updated the changelog compilation script to remove empty sections --- eng/scripts/Compile-Changelog.ps1 | 57 ++++++++++++++++++------------- 1 file changed, 33 insertions(+), 24 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 04af3a0983..cf0b2b9f36 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -505,18 +505,13 @@ if (-not $match.Success) { continue } - # Add empty line before section (but not before the very first section) - if (-not $isFirstSection) { - $mergedContent += "" - } - $isFirstSection = $false - - $mergedContent += "### $section" + # Collect entries for this section + $sectionContent = @() # Merge entries without subsection $existingMainEntries = @() if ($hasExisting -and $existingSections[$section].ContainsKey("")) { - $existingMainEntries = @($existingSections[$section][""]) + $existingMainEntries = @($existingSections[$section][""] | Where-Object { $_.Trim() -ne '' }) } $newMainEntries = @() @@ -526,17 +521,15 @@ if (-not $match.Success) { } } - # Append new entries after existing ones (with empty line before entries) - $totalEntries = $existingMainEntries.Count + $newMainEntries.Count - if ($totalEntries -gt 0) { - $mergedContent += "" # Empty line before entries + # Add main entries to section content + $totalMainEntries = $existingMainEntries.Count + $newMainEntries.Count + if ($totalMainEntries -gt 0) { + $sectionContent += "" # Empty line before entries foreach ($line in $existingMainEntries) { - if ($line) { - $mergedContent += $line - } + $sectionContent += $line } foreach ($line in $newMainEntries) { - $mergedContent += $line + $sectionContent += $line } } @@ -546,23 +539,39 @@ if (-not $match.Success) { foreach ($subsectionTitleCased in $allSubsections) { $mapping = $subsectionMapping[$subsectionTitleCased] - $mergedContent += "" - $mergedContent += "#### $subsectionTitleCased" - $mergedContent += "" # Empty line before entries + $subsectionEntries = @() - # Existing subsection entries + # Existing subsection entries (filter out empty lines) if ($mapping.Existing -and $hasExisting -and $existingSections[$section].ContainsKey($mapping.Existing)) { - foreach ($line in $existingSections[$section][$mapping.Existing]) { - $mergedContent += $line - } + $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]) { - $mergedContent += Format-ChangelogEntry -Description $entry.Description -PR $entry.PR + $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 } } } From a05df68cbba2ea2e37726607cb0d91c37c540e8c Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 19:04:03 -0800 Subject: [PATCH 14/26] Added support for any MCP server in this repo --- eng/scripts/Compile-Changelog.ps1 | 80 ++++++++++++++++--- eng/scripts/New-ChangelogEntry.ps1 | 36 +++++++-- eng/scripts/Sync-VsCodeChangelog.ps1 | 39 +++++++-- .../changelog-entries/README.md | 59 ++++++++++++-- 4 files changed, 178 insertions(+), 36 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index cf0b2b9f36..424ef89636 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -12,11 +12,15 @@ If there is no "Unreleased" section and no version is specified, a new "Unreleased" section is created using the next semantic version number. +.PARAMETER ServerName + Name of the server to compile changelog for (e.g., "Azure.Mcp.Server", "Fabric.Mcp.Server"). + Defaults to "Azure.Mcp.Server". + .PARAMETER ChangelogPath - Path to the CHANGELOG.md file. Defaults to servers/Azure.Mcp.Server/CHANGELOG.md. + Path to the CHANGELOG.md file. If not specified, uses servers/{ServerName}/CHANGELOG.md. .PARAMETER ChangelogEntriesPath - Path to the changelog-entries directory. Defaults to servers/Azure.Mcp.Server/changelog-entries. + Path to the changelog-entries directory. If not specified, uses servers/{ServerName}/changelog-entries. .PARAMETER Version Target version section to compile entries into (e.g., "2.0.0-beta.3", "1.5.2"). @@ -31,17 +35,22 @@ .EXAMPLE ./eng/scripts/Compile-Changelog.ps1 -DryRun - Preview what will be compiled without making changes. + Preview what will be compiled for Azure.Mcp.Server without making changes. .EXAMPLE - ./eng/scripts/Compile-Changelog.ps1 + ./eng/scripts/Compile-Changelog.ps1 -ServerName "Fabric.Mcp.Server" - Compile entries into the Unreleased section of CHANGELOG.md. + Compile entries into the Unreleased section for Fabric.Mcp.Server. .EXAMPLE ./eng/scripts/Compile-Changelog.ps1 -Version "2.0.0-beta.3" - Compile entries into the 2.0.0-beta.3 version section. + Compile entries into the 2.0.0-beta.3 version section for Azure.Mcp.Server. + +.EXAMPLE + ./eng/scripts/Compile-Changelog.ps1 -ServerName "Fabric.Mcp.Server" -Version "1.0.0" + + Compile entries into the 1.0.0 version section for Fabric.Mcp.Server. .EXAMPLE ./eng/scripts/Compile-Changelog.ps1 -DeleteFiles @@ -52,10 +61,13 @@ [CmdletBinding()] param( [Parameter(Mandatory = $false)] - [string]$ChangelogPath = "servers/Azure.Mcp.Server/CHANGELOG.md", + [string]$ServerName = "Azure.Mcp.Server", + + [Parameter(Mandatory = $false)] + [string]$ChangelogPath, [Parameter(Mandatory = $false)] - [string]$ChangelogEntriesPath = "servers/Azure.Mcp.Server/changelog-entries", + [string]$ChangelogEntriesPath, [Parameter(Mandatory = $false)] [string]$Version, @@ -68,7 +80,15 @@ param( ) Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" +$ErrorActionPreference = 'Stop' + +# Set default paths based on ServerName +if (-not $ChangelogPath) { + $ChangelogPath = "servers/$ServerName/CHANGELOG.md" +} +if (-not $ChangelogEntriesPath) { + $ChangelogEntriesPath = "servers/$ServerName/changelog-entries" +} # Helper function to convert text to title case (capitalize first letter of each word) function ConvertTo-TitleCase { @@ -227,7 +247,7 @@ if (-not (Test-Path $changelogFile)) { } # Get all YAML files -$yamlFiles = Get-ChildItem -Path $changelogEntriesDir -Filter "*.yml" -File | Where-Object { $_.Name -ne "README.yml" } +$yamlFiles = @(Get-ChildItem -Path $changelogEntriesDir -Filter "*.yml" -File | Where-Object { $_.Name -ne "README.yml" }) if ($yamlFiles.Count -eq 0) { Write-Host "No changelog entries found in $changelogEntriesDir" -ForegroundColor Yellow @@ -378,7 +398,8 @@ if ($Version) { Write-Host "No version specified - looking for Unreleased section..." -ForegroundColor Cyan # Find the first ## header after any initial # headers - $firstSectionPattern = '(?m)^##\s+(.+?)(\s+\(.*?\))?\s*$' + # 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) { @@ -395,7 +416,12 @@ if ($Version) { Write-Host "Creating new Unreleased section with next version number..." -ForegroundColor Yellow # Parse current version to determine next version - $currentVersion = $firstSectionMatch.Groups[1].Value.Trim() + # 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 (handles formats like "2.0.0-beta.3" or "1.5.2") @@ -432,6 +458,34 @@ if ($Version) { Write-Host "Target section: $targetVersionHeader" -ForegroundColor Cyan Write-Host "" +# Generate markdown content from grouped entries (needed for new sections) +$newEntriesMarkdown = @() +foreach ($section in $sectionOrder) { + 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) @@ -442,7 +496,7 @@ $mergedContent += $targetVersionHeader if (-not $match.Success) { # New section - just use the new entries - $mergedContent += $markdown + $mergedContent += $newEntriesMarkdown } else { # Existing section - merge with existing content $versionHeader = $match.Groups[1].Value diff --git a/eng/scripts/New-ChangelogEntry.ps1 b/eng/scripts/New-ChangelogEntry.ps1 index 1ae2e29000..034594cab2 100644 --- a/eng/scripts/New-ChangelogEntry.ps1 +++ b/eng/scripts/New-ChangelogEntry.ps1 @@ -19,23 +19,32 @@ .PARAMETER PR Pull request number (integer). +.PARAMETER ServerName + Name of the server to create changelog entry for (e.g., "Azure.Mcp.Server", "Fabric.Mcp.Server"). + Defaults to "Azure.Mcp.Server". + .PARAMETER ChangelogEntriesPath - Path to the changelog-entries directory. Defaults to servers/Azure.Mcp.Server/changelog-entries. + Path to the changelog-entries directory. If not specified, uses servers/{ServerName}/changelog-entries. .EXAMPLE ./eng/scripts/New-ChangelogEntry.ps1 - Runs in interactive mode, prompting for all required fields. + Runs in interactive mode for Azure.Mcp.Server, prompting for all required fields. + +.EXAMPLE + ./eng/scripts/New-ChangelogEntry.ps1 -ServerName "Fabric.Mcp.Server" + + Runs in interactive mode for Fabric.Mcp.Server. .EXAMPLE ./eng/scripts/New-ChangelogEntry.ps1 -Description "Added new feature" -Section "Features Added" -PR 1234 - Creates a changelog entry with the specified parameters. + Creates a changelog entry for Azure.Mcp.Server with the specified parameters. .EXAMPLE - ./eng/scripts/New-ChangelogEntry.ps1 -Description "Updated Azure.Core to 1.2.3" -Section "Other Changes" -Subsection "Dependency Updates" -PR 1234 + ./eng/scripts/New-ChangelogEntry.ps1 -ServerName "Fabric.Mcp.Server" -Description "Updated Azure.Core to 1.2.3" -Section "Other Changes" -Subsection "Dependency Updates" -PR 1234 - Creates a changelog entry with a subsection. + Creates a changelog entry for Fabric.Mcp.Server with a subsection. .EXAMPLE $description = @" @@ -51,6 +60,9 @@ Added new AI Foundry tools: [CmdletBinding()] param( + [Parameter(Mandatory = $false)] + [string]$ServerName = "Azure.Mcp.Server", + [Parameter(Mandatory = $false)] [string]$Description, @@ -64,12 +76,17 @@ param( [int]$PR, [Parameter(Mandatory = $false)] - [string]$ChangelogEntriesPath = "servers/Azure.Mcp.Server/changelog-entries" + [string]$ChangelogEntriesPath ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" +# Set default path based on ServerName +if (-not $ChangelogEntriesPath) { + $ChangelogEntriesPath = "servers/$ServerName/changelog-entries" +} + # Helper function to convert text to title case (capitalize first letter of each word) function ConvertTo-TitleCase { param([string]$Text) @@ -133,6 +150,9 @@ if (-not (Test-Path $changelogEntriesDir)) { New-Item -ItemType Directory -Path $changelogEntriesDir | Out-Null } +# Determine if we're in interactive mode (missing Description, Section, or PR) +$isInteractive = (-not $Description) -or (-not $Section) -or (-not $PSBoundParameters.ContainsKey('PR')) + # Interactive mode if parameters not provided if (-not $Description) { Write-Host "`nChangelog Entry Creator" -ForegroundColor Cyan @@ -175,7 +195,7 @@ if (-not $Section) { } # Allow subsection for any section in interactive mode -if (-not $PSBoundParameters.ContainsKey('Subsection')) { +if (-not $PSBoundParameters.ContainsKey('Subsection') -and $isInteractive) { $subsectionInput = Read-Host "`nSubsection (optional, press Enter to skip)" if ($subsectionInput) { # Trim and title case the subsection @@ -183,7 +203,7 @@ if (-not $PSBoundParameters.ContainsKey('Subsection')) { } } -if (-not $PR) { +if (-not $PR -and $isInteractive) { $prInput = Read-Host "`nPR number (press Enter to skip if not known yet)" if ($prInput) { $PR = [int]$prInput diff --git a/eng/scripts/Sync-VsCodeChangelog.ps1 b/eng/scripts/Sync-VsCodeChangelog.ps1 index e5548c91f7..3c21d06a00 100644 --- a/eng/scripts/Sync-VsCodeChangelog.ps1 +++ b/eng/scripts/Sync-VsCodeChangelog.ps1 @@ -4,17 +4,21 @@ Syncs the Unreleased section from the main CHANGELOG to the VS Code extension CHANGELOG. .DESCRIPTION - This script extracts the Unreleased section from the main Azure MCP Server CHANGELOG + 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 ServerName + Name of the server to sync changelog for (e.g., "Azure.Mcp.Server", "Fabric.Mcp.Server"). + Defaults to "Azure.Mcp.Server". + .PARAMETER MainChangelogPath - Path to the main CHANGELOG.md file. Defaults to servers/Azure.Mcp.Server/CHANGELOG.md. + Path to the main CHANGELOG.md file. If not specified, uses servers/{ServerName}/CHANGELOG.md. .PARAMETER VsCodeChangelogPath - Path to the VS Code extension CHANGELOG.md file. Defaults to servers/Azure.Mcp.Server/vscode/CHANGELOG.md. + Path to the VS Code extension CHANGELOG.md file. If not specified, uses servers/{ServerName}/vscode/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. @@ -25,21 +29,34 @@ .EXAMPLE ./eng/scripts/Sync-VsCodeChangelog.ps1 -DryRun - Preview the sync without making changes. + Preview the sync for Azure.Mcp.Server without making changes. + +.EXAMPLE + ./eng/scripts/Sync-VsCodeChangelog.ps1 -ServerName "Fabric.Mcp.Server" + + Sync the Unreleased section for Fabric.Mcp.Server. .EXAMPLE ./eng/scripts/Sync-VsCodeChangelog.ps1 -Version "2.0.3" - Sync the Unreleased section and create version 2.0.3 entry in VS Code CHANGELOG. + Sync the Unreleased section and create version 2.0.3 entry in Azure.Mcp.Server VS Code CHANGELOG. + +.EXAMPLE + ./eng/scripts/Sync-VsCodeChangelog.ps1 -ServerName "Fabric.Mcp.Server" -Version "1.0.0" + + Sync and create version 1.0.0 entry for Fabric.Mcp.Server. #> [CmdletBinding()] param( [Parameter(Mandatory = $false)] - [string]$MainChangelogPath = "servers/Azure.Mcp.Server/CHANGELOG.md", + [string]$ServerName = "Azure.Mcp.Server", + + [Parameter(Mandatory = $false)] + [string]$MainChangelogPath, [Parameter(Mandatory = $false)] - [string]$VsCodeChangelogPath = "servers/Azure.Mcp.Server/vscode/CHANGELOG.md", + [string]$VsCodeChangelogPath, [Parameter(Mandatory = $false)] [string]$Version, @@ -51,6 +68,14 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" +# Set default paths based on ServerName +if (-not $MainChangelogPath) { + $MainChangelogPath = "servers/$ServerName/CHANGELOG.md" +} +if (-not $VsCodeChangelogPath) { + $VsCodeChangelogPath = "servers/$ServerName/vscode/CHANGELOG.md" +} + # Get repository root $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $mainChangelogFile = Join-Path $repoRoot $MainChangelogPath diff --git a/servers/Azure.Mcp.Server/changelog-entries/README.md b/servers/Azure.Mcp.Server/changelog-entries/README.md index f58ec6cc6f..26dcfcb4d6 100644 --- a/servers/Azure.Mcp.Server/changelog-entries/README.md +++ b/servers/Azure.Mcp.Server/changelog-entries/README.md @@ -29,14 +29,25 @@ The easiest way to create a changelog entry is using the generator script: ```powershell # Interactive mode (prompts for all fields) +# Defaults to Azure.Mcp.Server ./eng/scripts/New-ChangelogEntry.ps1 -# With parameters +# For a different server +./eng/scripts/New-ChangelogEntry.ps1 -ServerName "Fabric.Mcp.Server" + +# With parameters (defaults to Azure.Mcp.Server) ./eng/scripts/New-ChangelogEntry.ps1 ` -Description "Added support for User-Assigned Managed Identity" ` -Section "Features Added" ` -PR 1033 +# For a specific server +./eng/scripts/New-ChangelogEntry.ps1 ` + -ServerName "Fabric.Mcp.Server" ` + -Description "Added new Fabric workspace tools" ` + -Section "Features Added" ` + -PR 1234 + # With subsection (automatically title-cased) ./eng/scripts/New-ChangelogEntry.ps1 ` -Description "Updated Azure.Core to 1.2.3" ` @@ -59,6 +70,7 @@ Added new AI Foundry tools: ``` **Features:** +- Works with any server (Azure.Mcp.Server, Fabric.Mcp.Server, etc.) via `-ServerName` parameter - Automatic title-casing of subsections - Whitespace trimming from descriptions - Support for multi-line descriptions with lists @@ -86,7 +98,10 @@ If you prefer to create the file manually: pr: 1234 # Can be added later if not known yet ``` -3. **Save the file** in the `servers/Azure.Mcp.Server/changelog-entries/` directory +3. **Save the file** in the appropriate server's changelog-entries directory: + - Azure MCP Server: `servers/Azure.Mcp.Server/changelog-entries/` + - Fabric MCP Server: `servers/Fabric.Mcp.Server/changelog-entries/` + - Other servers: `servers/{ServerName}/changelog-entries/` ## YAML File Format @@ -158,32 +173,51 @@ Before tagging a release: 1. **Preview compilation:** ```powershell - # Compile to the default Unreleased section + # Compile to the default Unreleased section (defaults to Azure.Mcp.Server) ./eng/scripts/Compile-Changelog.ps1 -DryRun + # For a different server + ./eng/scripts/Compile-Changelog.ps1 -ServerName "Fabric.Mcp.Server" -DryRun + # Or compile to a specific version ./eng/scripts/Compile-Changelog.ps1 -Version "2.0.0-beta.3" -DryRun ``` 2. **Compile entries:** ```powershell - # Compile to Unreleased section and delete YAML files + # Compile to Unreleased section and delete YAML files (defaults to Azure.Mcp.Server) ./eng/scripts/Compile-Changelog.ps1 -DeleteFiles + # For a specific server + ./eng/scripts/Compile-Changelog.ps1 -ServerName "Fabric.Mcp.Server" -DeleteFiles + # Or compile to a specific version ./eng/scripts/Compile-Changelog.ps1 -Version "2.0.0-beta.3" -DeleteFiles ``` -3. **Version behavior:** +3. **Server and version behavior:** + - `-ServerName`: Defaults to `Azure.Mcp.Server`, can be any server (e.g., `Fabric.Mcp.Server`) - 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 -4. **Update CHANGELOG.md** (if compiling to Unreleased): +4. **Sync the VS Code extension CHANGELOG** (if applicable): + ```powershell + # Preview the sync (Azure.Mcp.Server) + ./eng/scripts/Sync-VsCodeChangelog.ps1 -DryRun + + # For a different server + ./eng/scripts/Sync-VsCodeChangelog.ps1 -ServerName "Fabric.Mcp.Server" -DryRun + + # Apply the sync + ./eng/scripts/Sync-VsCodeChangelog.ps1 + ``` + +5. **Update CHANGELOG.md** (if compiling to Unreleased): - Change "Unreleased" to the actual version number - Add release date -5. **Commit and tag the release** +6. **Commit and tag the release** ## Compiled Output @@ -213,15 +247,24 @@ The scripts automatically validate your YAML files against the schema at `eng/sc To manually validate: ```powershell -# The New-ChangelogEntry.ps1 script validates automatically +# The New-ChangelogEntry.ps1 script validates automatically (defaults to Azure.Mcp.Server) ./eng/scripts/New-ChangelogEntry.ps1 +# For a specific server +./eng/scripts/New-ChangelogEntry.ps1 -ServerName "Fabric.Mcp.Server" + # The Compile-Changelog.ps1 script also validates ./eng/scripts/Compile-Changelog.ps1 -DryRun + +# For a specific server +./eng/scripts/Compile-Changelog.ps1 -ServerName "Fabric.Mcp.Server" -DryRun ``` ## Tips +- **Multiple servers**: All scripts support the `-ServerName` parameter (defaults to `Azure.Mcp.Server`) + - Available servers: `Azure.Mcp.Server`, `Fabric.Mcp.Server`, `Template.Mcp.Server`, etc. + - Each server has its own `changelog-entries/` folder and `CHANGELOG.md` - **Filename collisions**: The timestamp is in milliseconds, giving 1000 unique values per second. Collisions are extremely unlikely. - **PR number unknown**: You can create the entry before opening a PR. Just use `pr: 0` and update it later. - **Edit existing entry**: Just edit the YAML file and commit the change. From b6831bb787b2ec3c92a502304905cbb34ea56fd7 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 19:11:10 -0800 Subject: [PATCH 15/26] Moved a couple of docs --- .../README.md => docs/changelog-entries.md | 60 ++++++++++++++++++- .../changelog-management-system.md | 0 2 files changed, 59 insertions(+), 1 deletion(-) rename servers/Azure.Mcp.Server/changelog-entries/README.md => docs/changelog-entries.md (74%) rename docs/{ => design}/changelog-management-system.md (100%) diff --git a/servers/Azure.Mcp.Server/changelog-entries/README.md b/docs/changelog-entries.md similarity index 74% rename from servers/Azure.Mcp.Server/changelog-entries/README.md rename to docs/changelog-entries.md index 26dcfcb4d6..309574aea4 100644 --- a/servers/Azure.Mcp.Server/changelog-entries/README.md +++ b/docs/changelog-entries.md @@ -271,6 +271,64 @@ To manually validate: - **Multiple entries**: Create multiple YAML files with different timestamps. - **Subsections**: Use sparingly for grouping related changes (e.g., dependency updates). +## Why Timestamp Filenames? + +We use Unix timestamp in milliseconds (e.g., `1731260400123.yml`) for changelog entry filenames because: + +| Strategy | Pros | Cons | +|----------|------|------| +| **Timestamp milliseconds** ✅ | Unique, sortable, simple, can create before PR | None significant | +| Timestamp + PR number | Guaranteed unique, traceable | Verbose, requires PR first | +| PR number only | Simple, short | Not unique across repos, requires PR first | +| Sequential counter | Simple, short | Requires coordination, conflict-prone | +| GUID only | Guaranteed unique | Completely opaque, no sorting | + +**Key benefits:** +- **Pre-PR friendly**: Create entries before you have a PR number +- **No coordination needed**: No need to check what number to use next +- **Chronological**: Files naturally sort by creation time +- **Collision-resistant**: 1000 unique values per second makes conflicts extremely unlikely + +## 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 timestamp? + +The timestamp is in milliseconds, giving 1000 unique values per second. Collisions are extremely unlikely. If one does occur (perhaps you created two entries in the same second), Git will show a conflict and you can simply regenerate a new timestamp for one of them. + +### Can I add multiple changelog entries in one PR? + +Yes! Just create multiple YAML files with different timestamps. 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. You can create the YAML file before opening a PR by setting `pr: 0`, then update the PR number later when you know it. + +### 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 `./eng/scripts/Compile-Changelog.ps1 -DeleteFiles` after the entries are compiled into the main CHANGELOG.md. + +## Background + +This system replaces the traditional approach of directly editing `CHANGELOG.md`, which often caused merge conflicts when multiple contributors were working simultaneously. Inspired by [GitLab's changelog system](https://about.gitlab.com/blog/solving-gitlabs-changelog-conflict-crisis/), individual YAML files eliminate these conflicts while making the changelog process more structured and reviewable. + ## Questions? -See the full documentation at `docs/changelog-management-system.md` or reach out to the maintainers. +Reach out to the maintainers if you have questions or encounter issues with the changelog entry system. diff --git a/docs/changelog-management-system.md b/docs/design/changelog-management-system.md similarity index 100% rename from docs/changelog-management-system.md rename to docs/design/changelog-management-system.md From 74e0690c5f3331b912619c2f4b1cffdc78426699 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 14 Nov 2025 20:29:27 -0800 Subject: [PATCH 16/26] Updated other docs --- AGENTS.md | 4 +- CONTRIBUTING.md | 6 +-- PR-DESCRIPTION.md | 124 ++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 PR-DESCRIPTION.md diff --git a/AGENTS.md b/AGENTS.md index 8374c7b3be..c0be851dcd 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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: Create changelog entry YAML file if the change is a new feature, bug fix, or breaking change. Check out `/servers/Azure.Mcp.Server/changelog-entries/README.md` for instructions. +- 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. Use `-ServerName` parameter for non-Azure servers. - 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. **Create changelog entry** if user-facing or critical change. Check out `/servers/Azure.Mcp.Server/changelog-entries/README.md` for instructions. +4. **Create changelog entry** if user-facing or critical change. See `docs/changelog-entries.md` for instructions. Use `-ServerName` parameter for non-Azure servers. 5. **Add CODEOWNERS entry** for new toolset ### Spelling and Content Validation diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 88e8a5be34..0aa1022794 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -143,11 +143,11 @@ If you are contributing significant changes, or if the issue is already assigned - Update [README.md](https://github.com/microsoft/mcp/blob/main/README.md) to mention the new command 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: + - Use the generator script to create a changelog entry (see `docs/changelog-entries.md` for details): ```powershell - ./eng/scripts/New-ChangelogEntry.ps1 -Description -Section -PR + ./eng/scripts/New-ChangelogEntry.ps1 -ServerName -Description -Section -PR ``` - - Or manually create a YAML file in `servers/Azure.Mcp.Server/changelog-entries/` (see `/servers/Azure.Mcp.Server/changelog-entries/README.md` for details) + - 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 CODEOWNERS entry** in [CODEOWNERS](https://github.com/microsoft/mcp/blob/main/.github/CODEOWNERS) [(example)](https://github.com/microsoft/mcp/commit/08f73efe826d5d47c0f93be5ed9e614740e82091) diff --git a/PR-DESCRIPTION.md b/PR-DESCRIPTION.md new file mode 100644 index 0000000000..a6cdfbba7d --- /dev/null +++ b/PR-DESCRIPTION.md @@ -0,0 +1,124 @@ +## Implement Conflict-Free Changelog Management System + +### Summary + +This PR implements a GitLab-inspired changelog management system that eliminates merge conflicts on `CHANGELOG.md` by using individual YAML files for changelog entries. The system includes automation scripts, multi-server support, and comprehensive documentation. + +### What's Changed + +**Core System:** +- ✅ Individual YAML files per changelog entry (timestamped filenames) +- ✅ JSON schema validation for entry consistency +- ✅ Three PowerShell automation scripts for the complete workflow +- ✅ Multi-server support (Azure.Mcp.Server, Fabric.Mcp.Server, etc.) + +**Scripts Added:** +- `eng/scripts/New-ChangelogEntry.ps1` - Interactive generator for creating changelog entries +- `eng/scripts/Compile-Changelog.ps1` - Compiles YAML files into CHANGELOG.md +- `eng/scripts/Sync-VsCodeChangelog.ps1` - Syncs main CHANGELOG to VS Code extension CHANGELOG + +**Documentation:** +- `docs/changelog-entries.md` - User guide for contributors +- `docs/design/changelog-management-system.md` - Design document and implementation details +- `eng/schemas/changelog-entry.schema.json` - JSON schema for validation + +**Infrastructure:** +- Created `changelog-entries/` folders for Fabric.Mcp.Server and Template.Mcp.Server +- Created VS Code CHANGELOG.md files for each server +- Updated CONTRIBUTING.md and AGENTS.md with changelog workflow + +### Key Features + +**1. No More Merge Conflicts** +Contributors create individual YAML files instead of editing CHANGELOG.md directly: +```yaml +section: "Features Added" +description: "Added support for User-Assigned Managed Identity" +pr: 1033 +``` + +**2. Multi-Server Support** +All scripts accept a `-ServerName` parameter (defaults to `Azure.Mcp.Server`): +```powershell +./eng/scripts/New-ChangelogEntry.ps1 -ServerName "Fabric.Mcp.Server" +./eng/scripts/Compile-Changelog.ps1 -ServerName "Fabric.Mcp.Server" -DryRun +``` + +**3. Smart Compilation** +- Groups entries by section and subsection +- Handles multi-line descriptions with nested lists +- Auto-creates "Unreleased" sections when needed +- Supports compiling to specific version sections +- Removes empty sections from output + +**4. VS Code Extension Integration** +Automatically syncs main CHANGELOG to VS Code extension CHANGELOG with section mapping: +- "Features Added" → "Added" +- "Breaking Changes" + "Other Changes" → "Changed" +- "Bugs Fixed" → "Fixed" + +### Usage Examples + +**For Contributors:** +```powershell +# Create a changelog entry +./eng/scripts/New-ChangelogEntry.ps1 -Description "Added new feature" -Section "Features Added" -PR 1234 + +# Interactive mode +./eng/scripts/New-ChangelogEntry.ps1 +``` + +**For Release Managers:** +```powershell +# Preview compilation +./eng/scripts/Compile-Changelog.ps1 -DryRun + +# Compile and delete YAML files +./eng/scripts/Compile-Changelog.ps1 -DeleteFiles + +# Sync to VS Code extension +./eng/scripts/Sync-VsCodeChangelog.ps1 +``` + +### Technical Details + +- **Filename convention**: Unix timestamp in milliseconds (e.g., `1731260400123.yml`) + - Pre-PR friendly (can create before PR number is known) + - Chronologically sortable + - 1000 unique values per second (collision-resistant) + +- **Version handling**: Supports both `## 2.0.0 (Unreleased)` and `## [0.0.1] - 2025-09-16` formats + +- **Validation**: All entries validated against JSON schema during creation and compilation + +### Benefits + +- 🚀 **Zero merge conflicts** on CHANGELOG.md +- 📝 **Easier code reviews** - changelog entries visible in PR diffs +- ✅ **Automated formatting** - consistent output every time +- 🔍 **Better Git history** - clear attribution of who added each entry +- 🎯 **Structured data** - validated against schema +- 🌐 **Multi-server ready** - works across all MCP servers in the repo + +### Testing + +Tested with: +- ✅ Creating entries for Azure.Mcp.Server and Fabric.Mcp.Server +- ✅ Compiling to Unreleased and specific version sections +- ✅ Multi-line descriptions with nested bullet lists +- ✅ Empty section removal +- ✅ VS Code CHANGELOG syncing +- ✅ Both interactive and non-interactive modes + +### Migration Notes + +Existing workflow remains valid during transition: +- Old entries in CHANGELOG.md stay as-is +- New entries use YAML files going forward +- Scripts handle both scenarios gracefully + +### Related Documentation + +- See `docs/changelog-entries.md` for contributor guide +- See `docs/design/changelog-management-system.md` for design details +- Inspired by [GitLab's changelog system](https://about.gitlab.com/blog/solving-gitlabs-changelog-conflict-crisis/) From 944c81d0ccafc00cabbb6becfb89a4f3f9bad323 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Wed, 19 Nov 2025 13:45:16 -0800 Subject: [PATCH 17/26] Updated new-command.md --- servers/Azure.Mcp.Server/docs/new-command.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/servers/Azure.Mcp.Server/docs/new-command.md b/servers/Azure.Mcp.Server/docs/new-command.md index 52ec7b54da..09f45ab28c 100644 --- a/servers/Azure.Mcp.Server/docs/new-command.md +++ b/servers/Azure.Mcp.Server/docs/new-command.md @@ -2604,9 +2604,9 @@ Before submitting: - [ ] **Changelog Entry**: Create a changelog entry YAML file (if your change is a new feature, bug fix, or breaking change): ```powershell - ./eng/scripts/New-ChangelogEntry.ps1 -Description -Section -PR + ./eng/scripts/New-ChangelogEntry.ps1 -ServerName -Description -Section -PR ``` - See `/servers/Azure.Mcp.Server/changelog-entries/README.md` for details. Skip for internal refactoring, test-only changes, or minor updates. + See `docs/changelog-entries.md` for details. Skip for internal refactoring, test-only changes, or minor updates. - [ ] **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 From 31a75ab862f6390d6203471dfb5a8722695d589b Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Fri, 21 Nov 2025 19:14:18 -0800 Subject: [PATCH 18/26] 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. --- AGENTS.md | 4 +- CONTRIBUTING.md | 9 +- PR-DESCRIPTION.md | 124 ------ docs/changelog-entries.md | 373 +++++++++---------- docs/design/changelog-management-system.md | 307 +++++++++------ eng/schemas/changelog-entry.schema.json | 61 +-- eng/scripts/Compile-Changelog.ps1 | 131 ++++--- eng/scripts/New-ChangelogEntry.ps1 | 199 +++++++--- eng/scripts/Sync-VsCodeChangelog.ps1 | 44 +-- servers/Azure.Mcp.Server/docs/new-command.md | 6 +- 10 files changed, 650 insertions(+), 608 deletions(-) delete mode 100644 PR-DESCRIPTION.md diff --git a/AGENTS.md b/AGENTS.md index c0be851dcd..817312431f 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -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: Create changelog entry YAML file if the change is a new feature, bug fix, or breaking change. See `docs/changelog-entries.md` for instructions. Use `-ServerName` parameter for non-Azure servers. +- 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. **Create changelog entry** if user-facing or critical change. See `docs/changelog-entries.md` for instructions. Use `-ServerName` parameter for non-Azure servers. +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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0aa1022794..65e87eacd0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -93,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 @@ -117,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. @@ -145,7 +147,12 @@ If you are contributing significant changes, or if the issue is already assigned 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 - ./eng/scripts/New-ChangelogEntry.ps1 -ServerName -Description -Section -PR + # 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 -Section -PR + ./eng/scripts/New-ChangelogEntry.ps1 -ChangelogPath "servers/Fabric.Mcp.Server/CHANGELOG.md" -Description -Section -PR ``` - 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. diff --git a/PR-DESCRIPTION.md b/PR-DESCRIPTION.md deleted file mode 100644 index a6cdfbba7d..0000000000 --- a/PR-DESCRIPTION.md +++ /dev/null @@ -1,124 +0,0 @@ -## Implement Conflict-Free Changelog Management System - -### Summary - -This PR implements a GitLab-inspired changelog management system that eliminates merge conflicts on `CHANGELOG.md` by using individual YAML files for changelog entries. The system includes automation scripts, multi-server support, and comprehensive documentation. - -### What's Changed - -**Core System:** -- ✅ Individual YAML files per changelog entry (timestamped filenames) -- ✅ JSON schema validation for entry consistency -- ✅ Three PowerShell automation scripts for the complete workflow -- ✅ Multi-server support (Azure.Mcp.Server, Fabric.Mcp.Server, etc.) - -**Scripts Added:** -- `eng/scripts/New-ChangelogEntry.ps1` - Interactive generator for creating changelog entries -- `eng/scripts/Compile-Changelog.ps1` - Compiles YAML files into CHANGELOG.md -- `eng/scripts/Sync-VsCodeChangelog.ps1` - Syncs main CHANGELOG to VS Code extension CHANGELOG - -**Documentation:** -- `docs/changelog-entries.md` - User guide for contributors -- `docs/design/changelog-management-system.md` - Design document and implementation details -- `eng/schemas/changelog-entry.schema.json` - JSON schema for validation - -**Infrastructure:** -- Created `changelog-entries/` folders for Fabric.Mcp.Server and Template.Mcp.Server -- Created VS Code CHANGELOG.md files for each server -- Updated CONTRIBUTING.md and AGENTS.md with changelog workflow - -### Key Features - -**1. No More Merge Conflicts** -Contributors create individual YAML files instead of editing CHANGELOG.md directly: -```yaml -section: "Features Added" -description: "Added support for User-Assigned Managed Identity" -pr: 1033 -``` - -**2. Multi-Server Support** -All scripts accept a `-ServerName` parameter (defaults to `Azure.Mcp.Server`): -```powershell -./eng/scripts/New-ChangelogEntry.ps1 -ServerName "Fabric.Mcp.Server" -./eng/scripts/Compile-Changelog.ps1 -ServerName "Fabric.Mcp.Server" -DryRun -``` - -**3. Smart Compilation** -- Groups entries by section and subsection -- Handles multi-line descriptions with nested lists -- Auto-creates "Unreleased" sections when needed -- Supports compiling to specific version sections -- Removes empty sections from output - -**4. VS Code Extension Integration** -Automatically syncs main CHANGELOG to VS Code extension CHANGELOG with section mapping: -- "Features Added" → "Added" -- "Breaking Changes" + "Other Changes" → "Changed" -- "Bugs Fixed" → "Fixed" - -### Usage Examples - -**For Contributors:** -```powershell -# Create a changelog entry -./eng/scripts/New-ChangelogEntry.ps1 -Description "Added new feature" -Section "Features Added" -PR 1234 - -# Interactive mode -./eng/scripts/New-ChangelogEntry.ps1 -``` - -**For Release Managers:** -```powershell -# Preview compilation -./eng/scripts/Compile-Changelog.ps1 -DryRun - -# Compile and delete YAML files -./eng/scripts/Compile-Changelog.ps1 -DeleteFiles - -# Sync to VS Code extension -./eng/scripts/Sync-VsCodeChangelog.ps1 -``` - -### Technical Details - -- **Filename convention**: Unix timestamp in milliseconds (e.g., `1731260400123.yml`) - - Pre-PR friendly (can create before PR number is known) - - Chronologically sortable - - 1000 unique values per second (collision-resistant) - -- **Version handling**: Supports both `## 2.0.0 (Unreleased)` and `## [0.0.1] - 2025-09-16` formats - -- **Validation**: All entries validated against JSON schema during creation and compilation - -### Benefits - -- 🚀 **Zero merge conflicts** on CHANGELOG.md -- 📝 **Easier code reviews** - changelog entries visible in PR diffs -- ✅ **Automated formatting** - consistent output every time -- 🔍 **Better Git history** - clear attribution of who added each entry -- 🎯 **Structured data** - validated against schema -- 🌐 **Multi-server ready** - works across all MCP servers in the repo - -### Testing - -Tested with: -- ✅ Creating entries for Azure.Mcp.Server and Fabric.Mcp.Server -- ✅ Compiling to Unreleased and specific version sections -- ✅ Multi-line descriptions with nested bullet lists -- ✅ Empty section removal -- ✅ VS Code CHANGELOG syncing -- ✅ Both interactive and non-interactive modes - -### Migration Notes - -Existing workflow remains valid during transition: -- Old entries in CHANGELOG.md stay as-is -- New entries use YAML files going forward -- Scripts handle both scenarios gracefully - -### Related Documentation - -- See `docs/changelog-entries.md` for contributor guide -- See `docs/design/changelog-management-system.md` for design details -- Inspired by [GitLab's changelog system](https://about.gitlab.com/blog/solving-gitlabs-changelog-conflict-crisis/) diff --git a/docs/changelog-entries.md b/docs/changelog-entries.md index 309574aea4..39479c5078 100644 --- a/docs/changelog-entries.md +++ b/docs/changelog-entries.md @@ -1,10 +1,65 @@ # Changelog Entries -This directory contains individual changelog entry files that are compiled into the main `CHANGELOG.md` during the release process. +## Overview -## Why Individual Files? +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. -Using separate YAML files for each changelog entry **eliminates merge conflicts** on `CHANGELOG.md` when multiple contributors work on the same branch simultaneously. +**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" ` + -PR 1234 +``` + +That's it! The script creates the YAML file for you. Commit it 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 @@ -23,205 +78,174 @@ Create a changelog entry for: - ❌ Test-only changes - ❌ Minor code cleanup or formatting -### Using the Generator Script (Recommended) +### File Format + +**Structure**: One file per PR, supporting multiple changelog entries + +```yaml +pr: # Required: PR number (use 0 if not known yet) +changes: # Required: Array of changes (minimum 1) + - section: # Required + description: # Required + subsection: # 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. + +### Using the Generator Script -The easiest way to create a changelog entry is 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 -# Interactive mode (prompts for all fields) -# Defaults to Azure.Mcp.Server ./eng/scripts/New-ChangelogEntry.ps1 +``` -# For a different server -./eng/scripts/New-ChangelogEntry.ps1 -ServerName "Fabric.Mcp.Server" +#### One-line -# With parameters (defaults to Azure.Mcp.Server) +```powershell ./eng/scripts/New-ChangelogEntry.ps1 ` + -ChangelogPath "servers//CHANGELOG.md" ` -Description "Added support for User-Assigned Managed Identity" ` -Section "Features Added" ` - -PR 1033 - -# For a specific server -./eng/scripts/New-ChangelogEntry.ps1 ` - -ServerName "Fabric.Mcp.Server" ` - -Description "Added new Fabric workspace tools" ` - -Section "Features Added" ` -PR 1234 +``` -# With subsection (automatically title-cased) +##### With a subsection + +```powershell ./eng/scripts/New-ChangelogEntry.ps1 ` - -Description "Updated Azure.Core to 1.2.3" ` + -ChangelogPath "servers//CHANGELOG.md" ` + -Description "Updated ModelContextProtocol.AspNetCore to version 0.4.0-preview.3" ` -Section "Other Changes" ` - -Subsection "dependency updates" ` + -Subsection "Dependency Updates" ` -PR 1234 +``` + +##### With a multi-line entry -# Multi-line description with a list +```powershell $description = @" -Added new AI Foundry tools: +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//CHANGELOG.md" ` -Description $description ` -Section "Features Added" ` - -PR 945 + -PR 1234 ``` -**Features:** -- Works with any server (Azure.Mcp.Server, Fabric.Mcp.Server, etc.) via `-ServerName` parameter -- Automatic title-casing of subsections -- Whitespace trimming from descriptions -- Support for multi-line descriptions with lists -- Interactive validation - ### Manual Creation -If you prefer to create the file manually: - -1. **Generate a unique filename** using the current timestamp in milliseconds: - ```powershell - # PowerShell - [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() - - # Bash - date +%s%3N - ``` - Example: `1731260400123.yml` - -2. **Create a YAML file** with this structure: - ```yaml - section: "Features Added" - description: "Your change description here" - subsection: null # Or a subsection name like "Dependency Updates" - pr: 1234 # Can be added later if not known yet - ``` - -3. **Save the file** in the appropriate server's changelog-entries directory: - - Azure MCP Server: `servers/Azure.Mcp.Server/changelog-entries/` - - Fabric MCP Server: `servers/Fabric.Mcp.Server/changelog-entries/` - - Other servers: `servers/{ServerName}/changelog-entries/` - -## YAML File Format - -### Required Fields - -- **section**: The changelog section (one of: `Features Added`, `Breaking Changes`, `Bugs Fixed`, `Other Changes`) -- **description**: Description of the change (minimum 10 characters) -- **pr**: Pull request number (integer) - -### Optional Fields - -- **subsection**: Optional subsection for grouping (e.g., `Dependency Updates`, `Telemetry`) - -### Valid Sections - -- `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.) - -### Example Entries +If you prefer to do things manually, create a new YAML file with your changes and save it in the appropriate `servers//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:** - -**Filename:** `1731260400123.yml` +#### Simple entry ```yaml -section: "Features Added" -description: "Added support for User-Assigned Managed Identity via the `AZURE_CLIENT_ID` environment variable." -subsection: null -pr: 1033 +pr: 1234 +changes: + - section: "Features Added" + description: "Added support for User-Assigned Managed Identity" ``` -**Multi-line entry with a list:** - -**Filename:** `1731260401234.yml` +#### Multi-line entry ```yaml -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 -subsection: null -pr: 945 +pr: 1234 +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 ``` -**Note:** When using multi-line descriptions with the `|` block scalar: -- The first line becomes the main bullet point -- If the following lines are bullet items (`- item`), they'll be automatically indented as sub-bullets -- The PR link is added to the last line -- Trailing newlines are automatically handled - -## Workflow +#### Using a subsection -### For Contributors +```yaml +pr: 1234 +changes: + - section: "Other Changes" + subsection: "Dependency Updates" + description: "Updated ModelContextProtocol.AspNetCore to version 0.4.0-preview.3" +``` -1. **Make your code changes** -2. **Create a changelog entry** (if your change needs one) -3. **Commit both your code and the YAML file** in the same PR -4. **Open your PR** - no CHANGELOG.md conflicts! +#### Multiple-entries -You can create the YAML file before you have a PR number. Just set `pr: 0` initially and update it later when you know the PR number. +```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" + - section: "Other Changes" + subsection: "Dependency Updates" + description: "Updated ModelContextProtocol.AspNetCore to version 0.4.0-preview.3" +``` -### For Release Managers +## Preparing a New Release -Before tagging a release: +If you are a release manager, follow these steps before initiaiting a new release pipeline run: -1. **Preview compilation:** +1. Preview the changelog section for the version you are about to release:* ```powershell - # Compile to the default Unreleased section (defaults to Azure.Mcp.Server) - ./eng/scripts/Compile-Changelog.ps1 -DryRun - - # For a different server - ./eng/scripts/Compile-Changelog.ps1 -ServerName "Fabric.Mcp.Server" -DryRun + # Compile to the default Unreleased section for Azure MCP Server + ./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -DryRun # Or compile to a specific version - ./eng/scripts/Compile-Changelog.ps1 -Version "2.0.0-beta.3" -DryRun + ./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -Version "" -DryRun ``` -2. **Compile entries:** +2. Compile entries and delete files: ```powershell - # Compile to Unreleased section and delete YAML files (defaults to Azure.Mcp.Server) - ./eng/scripts/Compile-Changelog.ps1 -DeleteFiles - - # For a specific server - ./eng/scripts/Compile-Changelog.ps1 -ServerName "Fabric.Mcp.Server" -DeleteFiles - + # Compile to Unreleased section and delete YAML files for Azure MCP Server + ./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -DeleteFiles + # Or compile to a specific version - ./eng/scripts/Compile-Changelog.ps1 -Version "2.0.0-beta.3" -DeleteFiles + ./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -Version "" -DeleteFiles ``` -3. **Server and version behavior:** - - `-ServerName`: Defaults to `Azure.Mcp.Server`, can be any server (e.g., `Fabric.Mcp.Server`) - - If `-Version` is specified: Entries are compiled into that version section (must exist in CHANGELOG.md) + 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 -4. **Sync the VS Code extension CHANGELOG** (if applicable): +3. Sync the VS Code extension CHANGELOG (if applicable): ```powershell - # Preview the sync (Azure.Mcp.Server) - ./eng/scripts/Sync-VsCodeChangelog.ps1 -DryRun - - # For a different server - ./eng/scripts/Sync-VsCodeChangelog.ps1 -ServerName "Fabric.Mcp.Server" -DryRun + # Preview the sync + ./eng/scripts/Sync-VsCodeChangelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -DryRun # Apply the sync - ./eng/scripts/Sync-VsCodeChangelog.ps1 + ./eng/scripts/Sync-VsCodeChangelog.ps1 -ChangelogPath "servers//CHANGELOG.md" ``` -5. **Update CHANGELOG.md** (if compiling to Unreleased): - - Change "Unreleased" to the actual version number - - Add release date +4. Update release date in CHANGELOG.md +5. Commit and initiate the release process -6. **Commit and tag the release** +### Compiled Output -## Compiled Output - -When compiled, entries are grouped by section and subsection: +When compiled, entries are grouped by section and subsection. Empty sections will not be included. ```markdown ## 2.0.0-beta.3 (Unreleased) @@ -231,63 +255,32 @@ When compiled, entries are grouped by section and subsection: - 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)] -### Breaking Changes - -### Bugs Fixed - ### Other Changes -#### Telemetry -- Added `ToolId` into telemetry. [[#1028](https://github.com/microsoft/mcp/pull/1028)] -``` - -## Validation +#### Dependency Updates -The scripts automatically validate your YAML files against the schema at `eng/schemas/changelog-entry.schema.json`. +- 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)] +``` -To manually validate: -```powershell -# The New-ChangelogEntry.ps1 script validates automatically (defaults to Azure.Mcp.Server) -./eng/scripts/New-ChangelogEntry.ps1 +**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. -# For a specific server -./eng/scripts/New-ChangelogEntry.ps1 -ServerName "Fabric.Mcp.Server" +### Validation -# The Compile-Changelog.ps1 script also validates -./eng/scripts/Compile-Changelog.ps1 -DryRun +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: -# For a specific server -./eng/scripts/Compile-Changelog.ps1 -ServerName "Fabric.Mcp.Server" -DryRun +```powershell +./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -DryRun ``` ## Tips -- **Multiple servers**: All scripts support the `-ServerName` parameter (defaults to `Azure.Mcp.Server`) +- **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` -- **Filename collisions**: The timestamp is in milliseconds, giving 1000 unique values per second. Collisions are extremely unlikely. + - Example paths: `servers/Azure.Mcp.Server/CHANGELOG.md`, `servers/Fabric.Mcp.Server/CHANGELOG.md` - **PR number unknown**: You can create the entry before opening a PR. Just use `pr: 0` and update it later. -- **Edit existing entry**: Just edit the YAML file and commit the change. -- **Multiple entries**: Create multiple YAML files with different timestamps. -- **Subsections**: Use sparingly for grouping related changes (e.g., dependency updates). - -## Why Timestamp Filenames? - -We use Unix timestamp in milliseconds (e.g., `1731260400123.yml`) for changelog entry filenames because: - -| Strategy | Pros | Cons | -|----------|------|------| -| **Timestamp milliseconds** ✅ | Unique, sortable, simple, can create before PR | None significant | -| Timestamp + PR number | Guaranteed unique, traceable | Verbose, requires PR first | -| PR number only | Simple, short | Not unique across repos, requires PR first | -| Sequential counter | Simple, short | Requires coordination, conflict-prone | -| GUID only | Guaranteed unique | Completely opaque, no sorting | - -**Key benefits:** -- **Pre-PR friendly**: Create entries before you have a PR number -- **No coordination needed**: No need to check what number to use next -- **Chronological**: Files naturally sort by creation time -- **Collision-resistant**: 1000 unique values per second makes conflicts extremely unlikely +- **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 @@ -303,13 +296,13 @@ Yes! Just edit the YAML file and commit the change. It will be picked up in the 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 timestamp? +### What if two entries use the same filename? -The timestamp is in milliseconds, giving 1000 unique values per second. Collisions are extremely unlikely. If one does occur (perhaps you created two entries in the same second), Git will show a conflict and you can simply regenerate a new timestamp for one of them. +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 multiple YAML files with different timestamps. This is common when a PR includes several distinct user-facing changes. +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? @@ -323,12 +316,8 @@ No! Only include entries for changes worth mentioning to users: ### What happens to the YAML files after release? -They're deleted by the release manager using `./eng/scripts/Compile-Changelog.ps1 -DeleteFiles` after the entries are compiled into the main CHANGELOG.md. - -## Background - -This system replaces the traditional approach of directly editing `CHANGELOG.md`, which often caused merge conflicts when multiple contributors were working simultaneously. Inspired by [GitLab's changelog system](https://about.gitlab.com/blog/solving-gitlabs-changelog-conflict-crisis/), individual YAML files eliminate these conflicts while making the changelog process more structured and reviewable. +They're deleted by the release manager using the `-DeleteFiles` flag when compiling entries into the main `CHANGELOG.md`. -## Questions? +### What if I have any other questions? Reach out to the maintainers if you have questions or encounter issues with the changelog entry system. diff --git a/docs/design/changelog-management-system.md b/docs/design/changelog-management-system.md index d99ebd1a4c..30a2b18c43 100644 --- a/docs/design/changelog-management-system.md +++ b/docs/design/changelog-management-system.md @@ -6,7 +6,6 @@ This document describes the implementation of a conflict-free changelog manageme **Key Benefits:** - ✅ No merge conflicts on CHANGELOG.md -- ✅ Easier code reviews (changelog entry visible in PR) - ✅ Automated compilation and formatting - ✅ Structured, validated data - ✅ Flexible organization with sections and subsections @@ -18,19 +17,18 @@ This document describes the implementation of a conflict-free changelog manageme ``` servers/Azure.Mcp.Server/ -├── CHANGELOG.md # Main changelog (compiled output) +├── CHANGELOG.md # Main changelog └── changelog-entries/ # Individual entry files - ├── 1731260400123.yml - ├── 1731260405789.yml + ├── vcolin7-fix-changelog-system.yml + ├── jfree-documentation-update.yml ├── ... - └── README.md # Documentation for contributors + └── alzimmermsft-improve-performance.yml ``` **Why keep it simple with a single flat directory?** -- We only release from one branch at a time, so we never need multiple folders -- Simpler workflow: contributors only need to know one location -- All files get compiled and removed together on release -- No unnecessary nesting +- 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 --- @@ -38,52 +36,42 @@ servers/Azure.Mcp.Server/ ### Filename Convention -**Format:** `{unix-timestamp-milliseconds}.yml` +**Format:** `{username-brief-change-description}.yml` -**Example:** `1731260400123.yml` +**Example:** `jdoe-add-user-authentication.yml` **Why this approach?** -- **Uniqueness**: Millisecond precision makes collisions extremely unlikely (1000 unique values per second) -- **Sortable**: Files naturally sort chronologically -- **Simple**: Just a number, no need to coordinate PR numbers +- **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 -- **Cross-platform safe**: No special characters - -**How to generate:** -```powershell -# In PowerShell -[DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() - -# In Bash -date +%s%3N -``` ### YAML Schema ```yaml -# Required fields -section: "Features Added" # One of: Features Added, Breaking Changes, Bugs Fixed, Other Changes -description: "Added support for User-Assigned Managed Identity via the `AZURE_CLIENT_ID` environment variable." -pr: 1033 # PR number (integer) - can be added later if not known yet - -# Optional fields -subsection: null # Optional: "Dependency Updates", "Telemetry", etc. +pr: # Required: PR number (use 0 if not known yet) +changes: # Required: Array of changes (minimum 1) + - section: # Required + description: # Required + subsection: # Optional ``` -**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. +#### Required Fields -### Valid Values +- **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 -**Sections (required):** -- `Features Added` -- `Breaking Changes` -- `Bugs Fixed` -- `Other Changes` +#### Optional Fields -**Subsections (optional):** -- `Dependency Updates` -- `Telemetry` -- Any custom subsection for grouping related changes +- **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 @@ -109,28 +97,49 @@ 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": ["section", "description", "pr"], + "required": ["pr", "changes"], "properties": { - "section": { - "type": "string", - "enum": [ - "Features Added", - "Breaking Changes", - "Bugs Fixed", - "Other Changes" - ] - }, - "subsection": { - "type": ["string", "null"] - }, - "description": { - "type": "string", - "minLength": 10 - }, "pr": { "type": "integer", - "minimum": 1 + "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 @@ -141,7 +150,7 @@ Validates YAML structure and ensures required fields are present. **File:** `eng/scripts/New-ChangelogEntry.ps1` -Helper script to create properly formatted changelog entries. +Helper script to create properly formatted changelog entries. It supports both interactive and one-line modes. **Usage:** @@ -149,25 +158,41 @@ Helper script to create properly formatted changelog entries. # Interactive mode (prompts for all fields) ./eng/scripts/New-ChangelogEntry.ps1 -# With parameters +# One-line with parameters ./eng/scripts/New-ChangelogEntry.ps1 ` - -Description "Added new feature" ` + -ChangelogPath "servers//CHANGELOG.md" ` + -Description "Added support for User-Assigned Managed Identity" ` -Section "Features Added" ` -PR 1234 # With subsection ./eng/scripts/New-ChangelogEntry.ps1 ` - -Description "Updated Azure.Core to 1.2.3" ` + -ChangelogPath "servers//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//CHANGELOG.md" ` + -Description $description ` + -Section "Features Added" ` + -PR 1234 ``` **Features:** - Interactive prompts for section, description, PR number -- Auto-generates filename with timestamp +- Auto-generates filename based on username and description - Validates YAML against schema -- Places file in correct directory +- Places file in correct `changelog-entries/` directory - Creates `changelog-entries/` directory if it doesn't exist ### 3. Compiler Script @@ -180,37 +205,43 @@ Compiles all YAML entries into CHANGELOG.md. ```powershell # Preview what will be compiled (dry run) -./eng/scripts/Compile-Changelog.ps1 -DryRun +./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -DryRun # Compile entries into CHANGELOG.md -./eng/scripts/Compile-Changelog.ps1 +./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -# Compile and remove YAML files after successful compilation -./eng/scripts/Compile-Changelog.ps1 -DeleteFiles +# Compile to a specific version +./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -Version "" -# Custom changelog path -./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "path/to/CHANGELOG.md" +# Compile and remove YAML files after successful compilation +./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//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 under "Unreleased" +- 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` files from `changelog-entries/` (excluding README.md) +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 "Unreleased" section in CHANGELOG.md +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 @@ -227,6 +258,7 @@ When making a change that needs a changelog entry: 2. **Create a changelog entry:** ```powershell ./eng/scripts/New-ChangelogEntry.ps1 ` + -ChangelogPath "servers//CHANGELOG.md" ` -Description "Your change description" ` -Section "Features Added" ` -PR @@ -242,21 +274,30 @@ Before tagging a release: 1. **Preview compilation:** ```powershell - ./eng/scripts/Compile-Changelog.ps1 -DryRun + # Preview to Unreleased section + ./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -DryRun + + # Or preview to a specific version + ./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -Version "" -DryRun ``` 2. **Compile and clean up:** ```powershell - ./eng/scripts/Compile-Changelog.ps1 -DeleteFiles + ./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -DeleteFiles ``` -3. **Update version in CHANGELOG.md:** +3. **Sync VS Code extension CHANGELOG (if applicable):** + ```powershell + ./eng/scripts/Sync-VsCodeChangelog.ps1 -ChangelogPath "servers//CHANGELOG.md" + ``` + +4. **Update version in CHANGELOG.md:** - Change "Unreleased" to actual version number - Add release date -4. **Commit the compiled changelog** +5. **Commit the compiled changelog** -5. **Tag the release** +6. **Tag the release** --- @@ -264,30 +305,45 @@ Before tagging a release: ### Input YAML Files -**File:** `1731260400123.yml` +**File:** `vcolin7-managed-identity.yaml` ```yaml -section: "Features Added" -description: "Added support for User-Assigned Managed Identity via the `AZURE_CLIENT_ID` environment variable." pr: 1033 +changes: + - section: "Features Added" + description: "Added support for User-Assigned Managed Identity via the `AZURE_CLIENT_ID` environment variable." ``` -**File:** `1731260405789.yml` +**File:** `jdoe-speech-recognition.yaml` ```yaml -section: "Features Added" -description: "Added support for speech recognition from an audio file with Fast Transcription via the command `azmcp_speech_stt_recognize`." 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:** `1731260410456.yml` +**File:** `alzimmermsft-dependency-update.yaml` ```yaml -section: "Other Changes" -subsection: "Telemetry" -description: "Added `ToolId` into telemetry, based on `IBaseCommand.Id`, a unique GUID for each command." -pr: 1028 +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) @@ -295,17 +351,21 @@ pr: 1028 - 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)] - -### Breaking Changes +- 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 -#### Telemetry -- Added `ToolId` into telemetry, based on `IBaseCommand.Id`, a unique GUID for each command. [[#1028](https://github.com/microsoft/mcp/pull/1028)] +#### 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 @@ -317,8 +377,8 @@ All YAML files are validated against the JSON schema before compilation. Invalid ### Filename Validation The compiler checks that filenames follow the expected pattern: -- Must end with `.yml` -- Should be numeric (timestamp format) - warning if not +- Must end with `.yml` or `.yaml` +- Should follow `{username}-{brief-description}` format - Ignores `README.md` and other non-YAML files ### CI Integration (Recommended) @@ -329,34 +389,31 @@ Add validation to your CI pipeline: # Example GitHub Actions check - name: Validate Changelog Entries run: | - # Validate YAML syntax and schema - ./eng/scripts/Validate-ChangelogEntries.ps1 - - # Test compilation (dry run) - ./eng/scripts/Compile-Changelog.ps1 -DryRun + # Test compilation (dry run) - includes entry validation + ./eng/scripts/Compile-Changelog.ps1 -ChangelogPath "servers//CHANGELOG.md" -DryRun ``` ### Pre-commit Hooks (Optional) -- Validate YAML schema for changed `.yml` files in `changelog-entries/` -- Ensure filename follows numeric timestamp convention -- Warn if YAML file is missing a `pr` field (can be added later) +- 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 -- [ ] Create `changelog-entries/` directory -- [ ] Create JSON schema: `eng/schemas/changelog-entry.schema.json` -- [ ] Create generator script: `eng/scripts/New-ChangelogEntry.ps1` -- [ ] Create compiler script: `eng/scripts/Compile-Changelog.ps1` -- [ ] Create documentation: `changelog-entries/README.md` -- [ ] Update `CONTRIBUTING.md` with new changelog workflow -- [ ] Update `docs/new-command.md` to mention changelog entries for AI coding agents -- [ ] Update `AGENTS.md` to include changelog workflow for AI agents +- [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) -- [ ] Create sample YAML files for testing -- [ ] Test compilation with sample data +- [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 @@ -378,7 +435,8 @@ For existing unreleased entries in CHANGELOG.md: | Strategy | Pros | Cons | Verdict | |----------|------|------|---------| -| **Timestamp milliseconds** (chosen) | Unique, sortable, simple, pre-PR friendly | None significant | ✅ Best choice | +| **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 | @@ -397,9 +455,9 @@ Add it later in a follow-up PR. Each entry is a separate file, so there's no con 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 timestamp? +### Q: What if two entries use the same filename? -The timestamp is in milliseconds, giving 1000 unique values per second. Collisions are extremely unlikely. If one does occur, Git will highlight a file is being modified instead of added, and you can simply regenerate the timestamp. +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? @@ -407,7 +465,7 @@ No! Contributors only create YAML files. Release managers compile during the rel ### Q: Can I add multiple changelog entries in one PR? -Yes, just create multiple YAML files with different filenames (different timestamps). +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? @@ -452,14 +510,15 @@ When adding user-facing changes (new features, breaking changes, important bug f 1. Create a changelog entry YAML file: ```bash - ./eng/scripts/New-ChangelogEntry.ps1 -Description "Your change" -Section "Features Added" -PR + ./eng/scripts/New-ChangelogEntry.ps1 -ChangelogPath "servers//CHANGELOG.md" -Description "Your change" -Section "Features Added" -PR ``` -2. Or manually create `changelog-entries/{timestamp}.yml`: +2. Or manually create `changelog-entries/{username}-{brief-description}.yaml`: ```yaml - section: "Features Added" - description: "Your change description" - pr: 1234 # Can be added later if not known yet + 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. diff --git a/eng/schemas/changelog-entry.schema.json b/eng/schemas/changelog-entry.schema.json index 834f389550..9a43548e4a 100644 --- a/eng/schemas/changelog-entry.schema.json +++ b/eng/schemas/changelog-entry.schema.json @@ -1,33 +1,48 @@ { "$schema": "http://json-schema.org/draft-07/schema#", "title": "Changelog Entry", - "description": "Schema for individual changelog entry YAML files", + "description": "Schema for individual changelog entry YAML files. One file per PR, supporting multiple changes.", "type": "object", - "required": ["section", "description", "pr"], + "required": ["pr", "changes"], "properties": { - "section": { - "type": "string", - "description": "The changelog section this entry belongs to", - "enum": [ - "Features Added", - "Breaking Changes", - "Bugs Fixed", - "Other Changes" - ] - }, - "subsection": { - "type": ["string", "null"], - "description": "Optional subsection for grouping related changes (e.g., 'Dependency Updates', 'Telemetry')" - }, - "description": { - "type": "string", - "description": "Description of the change", - "minLength": 10 - }, "pr": { "type": "integer", - "description": "Pull request number", - "minimum": 1 + "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 diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 424ef89636..6fd37c2bbe 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -12,15 +12,10 @@ If there is no "Unreleased" section and no version is specified, a new "Unreleased" section is created using the next semantic version number. -.PARAMETER ServerName - Name of the server to compile changelog for (e.g., "Azure.Mcp.Server", "Fabric.Mcp.Server"). - Defaults to "Azure.Mcp.Server". - .PARAMETER ChangelogPath - Path to the CHANGELOG.md file. If not specified, uses servers/{ServerName}/CHANGELOG.md. - -.PARAMETER ChangelogEntriesPath - Path to the changelog-entries directory. If not specified, uses servers/{ServerName}/changelog-entries. + 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"). @@ -33,22 +28,22 @@ Delete YAML files after successful compilation. .EXAMPLE - ./eng/scripts/Compile-Changelog.ps1 -DryRun + ./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 -ServerName "Fabric.Mcp.Server" + ./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 -Version "2.0.0-beta.3" + ./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 -ServerName "Fabric.Mcp.Server" -Version "1.0.0" + ./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. @@ -60,15 +55,9 @@ [CmdletBinding()] param( - [Parameter(Mandatory = $false)] - [string]$ServerName = "Azure.Mcp.Server", - - [Parameter(Mandatory = $false)] + [Parameter(Mandatory = $true)] [string]$ChangelogPath, - [Parameter(Mandatory = $false)] - [string]$ChangelogEntriesPath, - [Parameter(Mandatory = $false)] [string]$Version, @@ -82,13 +71,9 @@ param( Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' -# Set default paths based on ServerName -if (-not $ChangelogPath) { - $ChangelogPath = "servers/$ServerName/CHANGELOG.md" -} -if (-not $ChangelogEntriesPath) { - $ChangelogEntriesPath = "servers/$ServerName/changelog-entries" -} +# 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 { @@ -100,7 +85,7 @@ function ConvertTo-TitleCase { # Use TextInfo for proper title casing $textInfo = (Get-Culture).TextInfo - return $textInfo.ToTitleCase($Text.ToLower()) + return $textInfo.ToTitleCase($Text.ToLowerInvariant()) } # Helper function to format a changelog entry with description and PR link @@ -269,6 +254,7 @@ Import-Module powershell-yaml -ErrorAction Stop # Parse and validate YAML files $entries = @() $validSections = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") +$validSubsections = @("Dependency Updates") foreach ($file in $yamlFiles) { Write-Host "Processing: $($file.Name)" -ForegroundColor Gray @@ -282,50 +268,77 @@ foreach ($file in $yamlFiles) { $yamlContent = Get-Content -Path $file.FullName -Raw $entry = $yamlContent | ConvertFrom-Yaml - # Validate required fields - if (-not $entry.section) { - Write-Error " Missing required field 'section' in $($file.Name)" + # Validate required top-level fields + if (-not $entry.ContainsKey('pr')) { + Write-Error " Missing required field 'pr' in $($file.Name)" continue } - # Validate and normalize section (case-insensitive) - $matchedSection = $validSections | Where-Object { $_ -ieq $entry.section } - if ($matchedSection) { - # Use the properly cased version - $normalizedSection = $matchedSection - } else { - Write-Error " Invalid section '$($entry.section)' in $($file.Name). Must be one of: $($validSections -join ', ')" - continue + if ($entry['pr'] -eq 0) { + Write-Warning " PR number is 0 in $($file.Name) (update when known)" } - - if (-not $entry.description) { - Write-Error " Missing required field 'description' in $($file.Name)" + elseif ($entry['pr'] -lt 0) { + Write-Error " Invalid PR number in $($file.Name) (must be non-negative)" continue } - if ($entry.description.Length -lt 10) { - Write-Error " Description too short in $($file.Name) (minimum 10 characters)" + 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 } - if (-not $entry.pr -or $entry.pr -eq 0) { - Write-Warning " Missing or invalid PR number in $($file.Name)" - } - elseif ($entry.pr -lt 1) { - Write-Error " Invalid PR number in $($file.Name) (must be positive)" - continue - } - - # Add to entries collection with normalized section name - $entries += [PSCustomObject]@{ - Section = $normalizedSection - Subsection = if ($entry.subsection -and $entry.subsection -ne "null") { $entry.subsection } else { $null } - Description = $entry.description - PR = $entry.pr - Filename = $file.Name + # 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 10) { + Write-Error " Description too short in change #$($changeCount + 1) of $($file.Name) (minimum 10 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" -ForegroundColor Green + Write-Host " ✓ Valid ($changeCount change(s))" -ForegroundColor Green } catch { Write-Error " Failed to parse $($file.Name): $_" diff --git a/eng/scripts/New-ChangelogEntry.ps1 b/eng/scripts/New-ChangelogEntry.ps1 index 034594cab2..26d8874501 100644 --- a/eng/scripts/New-ChangelogEntry.ps1 +++ b/eng/scripts/New-ChangelogEntry.ps1 @@ -14,38 +14,52 @@ The changelog section. Valid values: "Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes". .PARAMETER Subsection - Optional subsection for grouping related changes (e.g., "Dependency Updates", "Telemetry"). + Optional subsection for grouping related changes. Valid values: "Dependency Updates". .PARAMETER PR Pull request number (integer). -.PARAMETER ServerName - Name of the server to create changelog entry for (e.g., "Azure.Mcp.Server", "Fabric.Mcp.Server"). - Defaults to "Azure.Mcp.Server". +.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 ChangelogEntriesPath - Path to the changelog-entries directory. If not specified, uses servers/{ServerName}/changelog-entries. +.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 -ServerName "Fabric.Mcp.Server" + ./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 -Description "Added new feature" -Section "Features Added" -PR 1234 + ./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 -ServerName "Fabric.Mcp.Server" -Description "Updated Azure.Core to 1.2.3" -Section "Other Changes" -Subsection "Dependency Updates" -PR 1234 + ./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: @@ -61,7 +75,7 @@ Added new AI Foundry tools: [CmdletBinding()] param( [Parameter(Mandatory = $false)] - [string]$ServerName = "Azure.Mcp.Server", + [string]$ChangelogPath, [Parameter(Mandatory = $false)] [string]$Description, @@ -76,15 +90,20 @@ param( [int]$PR, [Parameter(Mandatory = $false)] - [string]$ChangelogEntriesPath + [string]$Filename ) Set-StrictMode -Version Latest $ErrorActionPreference = "Stop" -# Set default path based on ServerName -if (-not $ChangelogEntriesPath) { - $ChangelogEntriesPath = "servers/$ServerName/changelog-entries" +# Determine if we're in interactive mode (any required parameter is missing) +$isInteractive = (-not $ChangelogPath) -or (-not $Description) -or (-not $Section) -or (-not $PSBoundParameters.ContainsKey('PR')) + +# Show header once if in interactive mode +if ($isInteractive) { + Write-Host "`nChangelog Entry Creator" -ForegroundColor Cyan + Write-Host "======================" -ForegroundColor Cyan + Write-Host "" } # Helper function to convert text to title case (capitalize first letter of each word) @@ -97,11 +116,12 @@ function ConvertTo-TitleCase { # Use TextInfo for proper title casing $textInfo = (Get-Culture).TextInfo - return $textInfo.ToTitleCase($Text.ToLower()) + return $textInfo.ToTitleCase($Text.ToLowerInvariant()) } # Valid sections for validation $validSections = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") +$validSubsections = @("Dependency Updates") # Validate and normalize Section parameter if provided (case-insensitive) if ($Section) { @@ -136,9 +156,60 @@ if ($PR -and $PR -lt 1) { # Normalize subsection to title case if provided if ($Subsection) { - $Subsection = ConvertTo-TitleCase -Text $Subsection.Trim() + $Subsection = ConvertTo-TitleCase -Text $Subsection + + # Validate subsection against allowed values + $matchedSubsection = $validSubsections | Where-Object { $_ -ieq $Subsection } + if (-not $matchedSubsection) { + Write-Host "" + Write-Host "ERROR: Invalid subsection '$Subsection'" -ForegroundColor Red + Write-Host "" + Write-Host "Valid subsections are:" -ForegroundColor Yellow + foreach ($validSub in $validSubsections) { + Write-Host " - $validSub" -ForegroundColor Yellow + } + Write-Host "" + Write-Host "If you need a new subsection, please add it to the schema first." -ForegroundColor Cyan + Write-Host "" + 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 { + Write-Host "" + Write-Host "ERROR: Invalid choice '$serverChoice'. Please select 1-3." -ForegroundColor Red + Write-Host "" + 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" + # Get repository root $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $changelogEntriesDir = Join-Path $repoRoot $ChangelogEntriesPath @@ -150,14 +221,8 @@ if (-not (Test-Path $changelogEntriesDir)) { New-Item -ItemType Directory -Path $changelogEntriesDir | Out-Null } -# Determine if we're in interactive mode (missing Description, Section, or PR) -$isInteractive = (-not $Description) -or (-not $Section) -or (-not $PSBoundParameters.ContainsKey('PR')) - # Interactive mode if parameters not provided if (-not $Description) { - Write-Host "`nChangelog Entry Creator" -ForegroundColor Cyan - Write-Host "======================" -ForegroundColor Cyan - Write-Host "" Write-Host "Note: For multi-line descriptions (e.g., with lists), use the -Description parameter with a here-string." -ForegroundColor Gray Write-Host "" @@ -200,6 +265,24 @@ if (-not $PSBoundParameters.ContainsKey('Subsection') -and $isInteractive) { 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) { + Write-Host "" + Write-Host "ERROR: Invalid subsection '$Subsection'" -ForegroundColor Red + Write-Host "" + Write-Host "Valid subsections are:" -ForegroundColor Yellow + foreach ($validSub in $validSubsections) { + Write-Host " - $validSub" -ForegroundColor Yellow + } + Write-Host "" + Write-Host "If you need a new subsection, please add it to the schema first." -ForegroundColor Cyan + Write-Host "" + exit 1 + } + # Use the properly cased version + $Subsection = $matchedSubsection } } @@ -213,43 +296,52 @@ if (-not $PR -and $isInteractive) { # Trim whitespace from description if provided via parameter $Description = $Description.Trim() -# Generate filename with timestamp in milliseconds -$timestamp = [DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds() -$filename = "$timestamp.yml" +# 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 +# Create YAML content in new format: pr at top level, changes as an array +if ($PR) { + $yamlContent = "pr: $PR`n" +} else { + $yamlContent = "pr: 0 # TODO: Update with actual PR number`n" +} + +$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" - $indentedLines = $descriptionLines | ForEach-Object { " $_" } - $yamlContent = @" -section: "$Section" -description: | -$($indentedLines -join "`n") -"@ + $yamlContent += " description: |`n" + foreach ($line in $descriptionLines) { + $yamlContent += " $line`n" + } } else { # Single-line description - use quoted string # Escape double quotes in the description $escapedDescription = $Description -replace '"', '\"' - $yamlContent = @" -section: "$Section" -description: "$escapedDescription" -"@ + $yamlContent += " description: `"$escapedDescription`"`n" } if ($Subsection) { - $yamlContent += "`nsubsection: `"$Subsection`"" -} else { - $yamlContent += "`nsubsection: null" + $yamlContent += " subsection: `"$Subsection`"`n" } -if ($PR) { - $yamlContent += "`npr: $PR" -} else { - $yamlContent += "`npr: 0 # TODO: Update with actual PR number" -} +# Remove trailing newline +$yamlContent = $yamlContent.TrimEnd("`n") # Write YAML file $yamlContent | Set-Content -Path $filepath -Encoding UTF8 -NoNewline @@ -279,21 +371,30 @@ if (Test-Path $schemaPath) { try { $yamlData = Get-Content -Path $filepath -Raw | ConvertFrom-Yaml - # Basic validation (section already validated at top of script) - if ($yamlData.description.Length -lt 10) { + # Validate new format + if (-not $yamlData.pr -and $yamlData.pr -ne 0) { Write-Host "" - Write-Host "ERROR: Description must be at least 10 characters" -ForegroundColor Red + Write-Host "ERROR: PR field is required" -ForegroundColor Red Write-Host "" exit 1 } - if ($PR -and $yamlData.pr -lt 1) { + if (-not $yamlData.changes -or $yamlData.changes.Count -eq 0) { Write-Host "" - Write-Host "ERROR: PR number must be positive" -ForegroundColor Red + Write-Host "ERROR: At least one change is required" -ForegroundColor Red Write-Host "" exit 1 } + foreach ($change in $yamlData.changes) { + if ($change.description.Length -lt 10) { + Write-Host "" + Write-Host "ERROR: Description must be at least 10 characters" -ForegroundColor Red + Write-Host "" + exit 1 + } + } + Write-Host "✓ Validation passed" -ForegroundColor Green } catch { diff --git a/eng/scripts/Sync-VsCodeChangelog.ps1 b/eng/scripts/Sync-VsCodeChangelog.ps1 index 3c21d06a00..9b7b0167fe 100644 --- a/eng/scripts/Sync-VsCodeChangelog.ps1 +++ b/eng/scripts/Sync-VsCodeChangelog.ps1 @@ -10,15 +10,10 @@ - "Breaking Changes" + "Other Changes" → "Changed" - "Bugs Fixed" → "Fixed" -.PARAMETER ServerName - Name of the server to sync changelog for (e.g., "Azure.Mcp.Server", "Fabric.Mcp.Server"). - Defaults to "Azure.Mcp.Server". - -.PARAMETER MainChangelogPath - Path to the main CHANGELOG.md file. If not specified, uses servers/{ServerName}/CHANGELOG.md. - -.PARAMETER VsCodeChangelogPath - Path to the VS Code extension CHANGELOG.md file. If not specified, uses servers/{ServerName}/vscode/CHANGELOG.md. +.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. @@ -27,36 +22,30 @@ Preview the changes without modifying the VS Code CHANGELOG. .EXAMPLE - ./eng/scripts/Sync-VsCodeChangelog.ps1 -DryRun + ./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 -ServerName "Fabric.Mcp.Server" + ./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 -Version "2.0.3" + ./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 -ServerName "Fabric.Mcp.Server" -Version "1.0.0" + ./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 = $false)] - [string]$ServerName = "Azure.Mcp.Server", - - [Parameter(Mandatory = $false)] - [string]$MainChangelogPath, - - [Parameter(Mandatory = $false)] - [string]$VsCodeChangelogPath, + [Parameter(Mandatory = $true)] + [string]$ChangelogPath, [Parameter(Mandatory = $false)] [string]$Version, @@ -66,15 +55,12 @@ param( ) Set-StrictMode -Version Latest -$ErrorActionPreference = "Stop" +$ErrorActionPreference = 'Stop' -# Set default paths based on ServerName -if (-not $MainChangelogPath) { - $MainChangelogPath = "servers/$ServerName/CHANGELOG.md" -} -if (-not $VsCodeChangelogPath) { - $VsCodeChangelogPath = "servers/$ServerName/vscode/CHANGELOG.md" -} +# Infer VS Code changelog path from main CHANGELOG.md path +$changelogDir = Split-Path $ChangelogPath -Parent +$VsCodeChangelogPath = Join-Path $changelogDir "vscode/CHANGELOG.md" +$MainChangelogPath = $ChangelogPath # Get repository root $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent diff --git a/servers/Azure.Mcp.Server/docs/new-command.md b/servers/Azure.Mcp.Server/docs/new-command.md index 09f45ab28c..8c809c0408 100644 --- a/servers/Azure.Mcp.Server/docs/new-command.md +++ b/servers/Azure.Mcp.Server/docs/new-command.md @@ -2602,11 +2602,7 @@ Before submitting: **REQUIRED**: All new commands must update the following documentation files: -- [ ] **Changelog Entry**: Create a changelog entry YAML file (if your change is a new feature, bug fix, or breaking change): - ```powershell - ./eng/scripts/New-ChangelogEntry.ps1 -ServerName -Description -Section -PR - ``` - See `docs/changelog-entries.md` for details. Skip for internal refactoring, test-only changes, or minor updates. +- [ ] **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 From 82ad935bcbf8456efe4ca285e38782668136e9ab Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Wed, 26 Nov 2025 17:20:42 -0800 Subject: [PATCH 19/26] Updated Compile-Changelog.ps1 to accept files any name and using either the .yml or .yaml extension --- eng/scripts/Compile-Changelog.ps1 | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 6fd37c2bbe..509143483e 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -231,8 +231,8 @@ if (-not (Test-Path $changelogFile)) { exit 1 } -# Get all YAML files -$yamlFiles = @(Get-ChildItem -Path $changelogEntriesDir -Filter "*.yml" -File | Where-Object { $_.Name -ne "README.yml" }) +# Get all YAML files (both .yml and .yaml extensions) +$yamlFiles = @(Get-ChildItem -Path $changelogEntriesDir -Include "*.yml", "*.yaml" -File | Where-Object { $_.Name -ne "README.yml" -and $_.Name -ne "README.yaml" }) if ($yamlFiles.Count -eq 0) { Write-Host "No changelog entries found in $changelogEntriesDir" -ForegroundColor Yellow @@ -259,11 +259,6 @@ $validSubsections = @("Dependency Updates") foreach ($file in $yamlFiles) { Write-Host "Processing: $($file.Name)" -ForegroundColor Gray - # Validate filename format (should be numeric timestamp) - if ($file.BaseName -notmatch '^\d+$') { - Write-Warning " Filename '$($file.Name)' doesn't follow timestamp convention (numeric only)" - } - try { $yamlContent = Get-Content -Path $file.FullName -Raw $entry = $yamlContent | ConvertFrom-Yaml From 89e025f56b1965aa810d68baf174074c83d356b9 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Wed, 26 Nov 2025 17:45:40 -0800 Subject: [PATCH 20/26] Applied GitHub Copilot feedback for PowerShell scripts --- eng/scripts/Compile-Changelog.ps1 | 54 ++++++++++++++++++---------- eng/scripts/New-ChangelogEntry.ps1 | 18 +++------- eng/scripts/Sync-VsCodeChangelog.ps1 | 16 ++++++--- 3 files changed, 51 insertions(+), 37 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 509143483e..4f56d114b1 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -105,6 +105,10 @@ function Format-ChangelogEntry { $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 @@ -112,36 +116,42 @@ function Format-ChangelogEntry { # 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 ($lines.Length -gt 1 -and $lines[1].TrimStart() -match '^-\s+') { + 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 period at end - if (-not $line.EndsWith(".")) { + # Regular multi-line text, ensure valid sentence ending + if ($line -notmatch '[.!?\)\]]$') { $line += "." } + $formattedLines += "- $line" } - $formattedLines += "- $line" } elseif ($i -eq $lines.Length - 1) { - # Last line: add PR link here + # Last line # Line is already trimmed at the end, preserve leading indentation $trimmedLine = $line.TrimStart() $leadingSpaces = $line.Length - $trimmedLine.Length $indent = if ($leadingSpaces -gt 0) { $line.Substring(0, $leadingSpaces) } else { "" } - # For bullet items, don't add period; for regular text, ensure period - $needsPeriod = -not ($trimmedLine -match '^-\s+') - if ($needsPeriod -and -not $trimmedLine.EndsWith(".")) { - $trimmedLine += "." + if ($isList) { + # For lists, PR link was added to first line, just add the last bullet + $formattedLines += " $indent$trimmedLine" + } + else { + # For regular multi-line text, add PR link to last line + # Ensure valid sentence ending for non-bullet text + $needsPeriod = -not ($trimmedLine -match '^-\s+') + if ($needsPeriod -and $trimmedLine -notmatch '[.!?\)\]]$') { + $trimmedLine += "." + } + $formattedLines += " $indent$trimmedLine$prLink" } - - # Add PR link if available - $prLink = if ($PR -gt 0) { " [[#$PR](https://github.com/microsoft/mcp/pull/$PR)]" } else { "" } - $formattedLines += " $indent$trimmedLine$prLink" } else { # Middle lines: preserve indentation and add 2 spaces for nesting @@ -155,7 +165,8 @@ function Format-ChangelogEntry { else { # Single-line description: original logic $formattedDescription = $Description - if (-not $formattedDescription.EndsWith(".")) { + # Ensure valid sentence ending (avoid double punctuation) + if ($formattedDescription -notmatch '[.!?\)\]]$') { $formattedDescription += "." } @@ -432,18 +443,23 @@ if ($Version) { } Write-Host "Current version: $currentVersion" -ForegroundColor Gray - # Parse semantic version (handles formats like "2.0.0-beta.3" or "1.5.2") - if ($currentVersion -match '^(\d+)\.(\d+)\.(\d+)(?:-(.+?)\.(\d+))?') { + # Parse semantic version (handles formats like "2.0.0-beta.3", "2.0.0-alpha", or "1.5.2") + if ($currentVersion -match '^(\d+)\.(\d+)\.(\d+)(?:-(.+?)(?:\.(\d+))?)?$') { $major = [int]$matches[1] $minor = [int]$matches[2] $patch = [int]$matches[3] $prerelease = $matches[4] - $prereleaseNum = if ($matches[5]) { [int]$matches[5] } else { 0 } + $prereleaseNum = if ($matches[5]) { [int]$matches[5] } else { $null } # Increment based on prerelease status if ($prerelease) { - # Increment prerelease number (e.g., beta.3 -> beta.4) - $nextVersion = "$major.$minor.$patch-$prerelease.$($prereleaseNum + 1)" + if ($null -ne $prereleaseNum) { + # Has numeric suffix - increment it (e.g., beta.3 -> beta.4) + $nextVersion = "$major.$minor.$patch-$prerelease.$($prereleaseNum + 1)" + } else { + # No numeric suffix - add .1 (e.g., alpha -> alpha.1) + $nextVersion = "$major.$minor.$patch-$prerelease.1" + } } else { # Increment patch version (e.g., 1.5.2 -> 1.5.3) $nextVersion = "$major.$minor.$($patch + 1)" diff --git a/eng/scripts/New-ChangelogEntry.ps1 b/eng/scripts/New-ChangelogEntry.ps1 index 26d8874501..6b6235309b 100644 --- a/eng/scripts/New-ChangelogEntry.ps1 +++ b/eng/scripts/New-ChangelogEntry.ps1 @@ -114,9 +114,9 @@ function ConvertTo-TitleCase { return $Text } - # Use TextInfo for proper title casing - $textInfo = (Get-Culture).TextInfo - return $textInfo.ToTitleCase($Text.ToLowerInvariant()) + # Use invariant culture TextInfo for proper title casing + $textInfo = [System.Globalization.CultureInfo]::InvariantCulture.TextInfo + return $textInfo.ToTitleCase($Text.ToLowerInvariant()) } # Valid sections for validation @@ -146,14 +146,6 @@ if ($Section) { } } -# Validate PR parameter if provided -if ($PR -and $PR -lt 1) { - Write-Host "" - Write-Host "ERROR: PR number must be a positive integer (got: $PR)" -ForegroundColor Red - Write-Host "" - exit 1 -} - # Normalize subsection to title case if provided if ($Subsection) { $Subsection = ConvertTo-TitleCase -Text $Subsection @@ -331,8 +323,8 @@ if ($Description.Contains("`n")) { } } else { # Single-line description - use quoted string - # Escape double quotes in the description - $escapedDescription = $Description -replace '"', '\"' + # Escape backslashes and double quotes in the description + $escapedDescription = $Description -replace '\\', '\\\\' -replace '"', '\"' $yamlContent += " description: `"$escapedDescription`"`n" } diff --git a/eng/scripts/Sync-VsCodeChangelog.ps1 b/eng/scripts/Sync-VsCodeChangelog.ps1 index 9b7b0167fe..59c75428fe 100644 --- a/eng/scripts/Sync-VsCodeChangelog.ps1 +++ b/eng/scripts/Sync-VsCodeChangelog.ps1 @@ -85,8 +85,8 @@ Write-Host "" # Read the main CHANGELOG $mainContent = Get-Content -Path $mainChangelogFile -Raw -# Extract the Unreleased section -$unreleasedMatch = $mainContent -match '(?ms)^## ([\d\.]+-[\w\.]+) \(Unreleased\)\s*\n(.*?)(?=\n## |\z)' +# 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) { Write-Error "No Unreleased section found in main CHANGELOG" exit 1 @@ -125,11 +125,17 @@ foreach ($line in $unreleasedContent -split "`n") { } $currentSection = $Matches[1].Trim() + # Only process known sections + $validSections = @('Features Added', 'Breaking Changes', 'Bugs Fixed', 'Other Changes') + if ($currentSection -notin $validSections) { + Write-Warning "Unknown section '$currentSection' found in main CHANGELOG - skipping" + $currentSection = $null + } $currentEntries = @() continue } - # Skip lines before any section + # Skip lines before any section or in unknown sections if (-not $currentSection) { continue } @@ -203,8 +209,8 @@ Add-Section -SectionName "Changed" -Entries $changedEntries # Fixed section (from Bugs Fixed) Add-Section -SectionName "Fixed" -Entries $sections['Bugs Fixed'] -# Trim trailing empty line -while ($vscodeEntry[-1] -eq "") { +# 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)] } From b9fd2a5d01b4c755a41690f3fd76ed51e1354d8d Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Wed, 26 Nov 2025 18:23:59 -0800 Subject: [PATCH 21/26] Applied PR feedback to PS scripts --- eng/scripts/Compile-Changelog.ps1 | 49 ++++++--- eng/scripts/New-ChangelogEntry.ps1 | 159 ++++++++++++--------------- eng/scripts/Sync-VsCodeChangelog.ps1 | 73 ++++++------ 3 files changed, 138 insertions(+), 143 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 4f56d114b1..0c07f04454 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -68,6 +68,12 @@ param( [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' @@ -221,10 +227,9 @@ function Build-SubsectionMapping { return $subsectionMapping } -# Get repository root -$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$changelogFile = Join-Path $repoRoot $ChangelogPath -$changelogEntriesDir = Join-Path $repoRoot $ChangelogEntriesPath +# 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 @@ -242,8 +247,10 @@ if (-not (Test-Path $changelogFile)) { exit 1 } -# Get all YAML files (both .yml and .yaml extensions) -$yamlFiles = @(Get-ChildItem -Path $changelogEntriesDir -Include "*.yml", "*.yaml" -File | Where-Object { $_.Name -ne "README.yml" -and $_.Name -ne "README.yaml" }) +# Get all YAML files, excluding the example template file +$yamlFiles = @(Get-ChildItem -Path $changelogEntriesDir -Include "*.yml", "*.yaml" -File | Where-Object { + $_.BaseName -ne 'username-example-brief-description' +}) if ($yamlFiles.Count -eq 0) { Write-Host "No changelog entries found in $changelogEntriesDir" -ForegroundColor Yellow @@ -254,18 +261,28 @@ if ($yamlFiles.Count -eq 0) { Write-Host "Found $($yamlFiles.Count) changelog entry file(s)" -ForegroundColor Green Write-Host "" -# Install PowerShell-Yaml module if not available -$yamlModule = Get-Module -ListAvailable -Name "powershell-yaml" -if (-not $yamlModule) { - Write-Host "Installing powershell-yaml module..." -ForegroundColor Yellow - Install-Module -Name powershell-yaml -Force -Scope CurrentUser -AllowClobber +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 } -Import-Module powershell-yaml -ErrorAction Stop + +$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 = @() -$validSections = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") -$validSubsections = @("Dependency Updates") foreach ($file in $yamlFiles) { Write-Host "Processing: $($file.Name)" -ForegroundColor Gray @@ -316,8 +333,8 @@ foreach ($file in $yamlFiles) { continue } - if ($change['description'].Length -lt 10) { - Write-Error " Description too short in change #$($changeCount + 1) of $($file.Name) (minimum 10 characters)" + if ($change['description'].Length -lt $minDescriptionLength) { + Write-Error " Description too short in change #$($changeCount + 1) of $($file.Name) (minimum $minDescriptionLength characters)" continue } diff --git a/eng/scripts/New-ChangelogEntry.ps1 b/eng/scripts/New-ChangelogEntry.ps1 index 6b6235309b..a4d0abc6b7 100644 --- a/eng/scripts/New-ChangelogEntry.ps1 +++ b/eng/scripts/New-ChangelogEntry.ps1 @@ -95,15 +95,19 @@ param( 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) $isInteractive = (-not $ChangelogPath) -or (-not $Description) -or (-not $Section) -or (-not $PSBoundParameters.ContainsKey('PR')) # Show header once if in interactive mode if ($isInteractive) { - Write-Host "`nChangelog Entry Creator" -ForegroundColor Cyan - Write-Host "======================" -ForegroundColor Cyan - Write-Host "" + LogInfo "" + LogInfo "Changelog Entry Creator" + LogInfo "=======================" + LogInfo "" } # Helper function to convert text to title case (capitalize first letter of each word) @@ -119,9 +123,17 @@ function ConvertTo-TitleCase { return $textInfo.ToTitleCase($Text.ToLowerInvariant()) } -# Valid sections for validation -$validSections = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") -$validSubsections = @("Dependency Updates") +# 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) { @@ -130,18 +142,11 @@ if ($Section) { # Use the properly cased version $Section = $matchedSection } else { - Write-Host "" - Write-Host "ERROR: Invalid section '$Section'" -ForegroundColor Red - Write-Host "" - Write-Host "Valid sections are:" -ForegroundColor Yellow - Write-Host " - Features Added" -ForegroundColor Yellow - Write-Host " - Breaking Changes" -ForegroundColor Yellow - Write-Host " - Bugs Fixed" -ForegroundColor Yellow - Write-Host " - Other Changes" -ForegroundColor Yellow - Write-Host "" - Write-Host "Example usage:" -ForegroundColor Cyan - Write-Host ' .\eng\scripts\New-ChangelogEntry.ps1 -Section "Features Added" -Description "..." -PR 1234' -ForegroundColor Gray - Write-Host "" + 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 } } @@ -153,16 +158,9 @@ if ($Subsection) { # Validate subsection against allowed values $matchedSubsection = $validSubsections | Where-Object { $_ -ieq $Subsection } if (-not $matchedSubsection) { - Write-Host "" - Write-Host "ERROR: Invalid subsection '$Subsection'" -ForegroundColor Red - Write-Host "" - Write-Host "Valid subsections are:" -ForegroundColor Yellow - foreach ($validSub in $validSubsections) { - Write-Host " - $validSub" -ForegroundColor Yellow - } - Write-Host "" - Write-Host "If you need a new subsection, please add it to the schema first." -ForegroundColor Cyan - Write-Host "" + 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 @@ -186,9 +184,7 @@ if (-not $ChangelogPath) { $customPath.Trim() } default { - Write-Host "" - Write-Host "ERROR: Invalid choice '$serverChoice'. Please select 1-3." -ForegroundColor Red - Write-Host "" + LogError "Invalid choice '$serverChoice'. Please select 1-3." exit 1 } } @@ -202,10 +198,8 @@ if (-not $ChangelogPath) { $changelogDir = Split-Path $ChangelogPath -Parent $ChangelogEntriesPath = Join-Path $changelogDir "changelog-entries" -# Get repository root -$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$changelogEntriesDir = Join-Path $repoRoot $ChangelogEntriesPath -$schemaPath = Join-Path $repoRoot "eng/schemas/changelog-entry.schema.json" +# 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)) { @@ -218,36 +212,30 @@ 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 10 characters)" + $Description = Read-Host "Description (minimum $minDescriptionLength characters)" # Trim whitespace from user input $Description = $Description.Trim() - while ($Description.Length -lt 10) { - Write-Host "Description must be at least 10 characters long." -ForegroundColor Red - $Description = Read-Host "Description (minimum 10 characters)" + 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 - Write-Host "1. Features Added" - Write-Host "2. Breaking Changes" - Write-Host "3. Bugs Fixed" - Write-Host "4. Other Changes" + for ($i = 0; $i -lt $validSections.Count; $i++) { + Write-Host "$($i + 1). $($validSections[$i])" + } - $choice = Read-Host "`nEnter choice (1-4)" - $Section = switch ($choice) { - "1" { "Features Added" } - "2" { "Breaking Changes" } - "3" { "Bugs Fixed" } - "4" { "Other Changes" } - default { - Write-Host "" - Write-Host "ERROR: Invalid choice '$choice'. Please select 1-4." -ForegroundColor Red - Write-Host "" - exit 1 - } + $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 } } @@ -261,16 +249,8 @@ if (-not $PSBoundParameters.ContainsKey('Subsection') -and $isInteractive) { # Validate subsection against allowed values $matchedSubsection = $validSubsections | Where-Object { $_ -ieq $Subsection } if (-not $matchedSubsection) { - Write-Host "" - Write-Host "ERROR: Invalid subsection '$Subsection'" -ForegroundColor Red - Write-Host "" - Write-Host "Valid subsections are:" -ForegroundColor Yellow - foreach ($validSub in $validSubsections) { - Write-Host " - $validSub" -ForegroundColor Yellow - } - Write-Host "" - Write-Host "If you need a new subsection, please add it to the schema first." -ForegroundColor Cyan - Write-Host "" + 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 @@ -338,22 +318,24 @@ $yamlContent = $yamlContent.TrimEnd("`n") # Write YAML file $yamlContent | Set-Content -Path $filepath -Encoding UTF8 -NoNewline -Write-Host "`n✓ Created changelog entry: $filename" -ForegroundColor Green -Write-Host " Location: $filepath" -ForegroundColor Gray -Write-Host " Section: $Section" -ForegroundColor Gray +LogSuccess "" +LogSuccess "✓ Created changelog entry: $filename" +LogInfo " Location: $filepath" +LogInfo " Section: $Section" if ($Subsection) { - Write-Host " Subsection: $Subsection" -ForegroundColor Gray + LogInfo " Subsection: $Subsection" } -Write-Host " Description: $Description" -ForegroundColor Gray +LogInfo " Description: $Description" if ($PR) { - Write-Host " PR: #$PR" -ForegroundColor Gray + LogInfo " PR: #$PR" } else { - Write-Host " PR: Not set (remember to update before merging)" -ForegroundColor Yellow + LogWarning " PR: Not set (remember to update before merging)" } # Validate against schema if available if (Test-Path $schemaPath) { - Write-Host "`nValidating against schema..." -ForegroundColor Cyan + LogInfo "" + LogInfo "Validating against schema..." # Try to use PowerShell-Yaml module if available $yamlModule = Get-Module -ListAvailable -Name "powershell-yaml" @@ -365,43 +347,38 @@ if (Test-Path $schemaPath) { # Validate new format if (-not $yamlData.pr -and $yamlData.pr -ne 0) { - Write-Host "" - Write-Host "ERROR: PR field is required" -ForegroundColor Red - Write-Host "" + LogError "PR field is required" exit 1 } if (-not $yamlData.changes -or $yamlData.changes.Count -eq 0) { - Write-Host "" - Write-Host "ERROR: At least one change is required" -ForegroundColor Red - Write-Host "" + LogError "At least one change is required" exit 1 } foreach ($change in $yamlData.changes) { - if ($change.description.Length -lt 10) { - Write-Host "" - Write-Host "ERROR: Description must be at least 10 characters" -ForegroundColor Red - Write-Host "" + if ($change.description.Length -lt $minDescriptionLength) { + LogError "Description must be at least $minDescriptionLength characters" exit 1 } } - Write-Host "✓ Validation passed" -ForegroundColor Green + LogSuccess "✓ Validation passed" } catch { - Write-Warning "Could not validate YAML: $_" + LogWarning "Could not validate YAML: $_" } } else { - Write-Host " Note: Install 'powershell-yaml' module for automatic validation" -ForegroundColor Gray - Write-Host " Run: Install-Module -Name powershell-yaml" -ForegroundColor Gray + LogInfo " Note: Install 'powershell-yaml' module for automatic validation" + LogInfo " Run: Install-Module -Name powershell-yaml" } } -Write-Host "`nNext steps:" -ForegroundColor Cyan -Write-Host "1. Commit this file with your changes" -ForegroundColor Gray +LogInfo "" +LogInfo "Next steps:" +LogInfo "1. Commit this file with your changes" if (-not $PR) { - Write-Host "2. Update the 'pr' field in the YAML file once you have a PR number" -ForegroundColor Gray + LogInfo "2. Update the 'pr' field in the YAML file once you have a PR number" } -Write-Host "" +LogInfo "" diff --git a/eng/scripts/Sync-VsCodeChangelog.ps1 b/eng/scripts/Sync-VsCodeChangelog.ps1 index 59c75428fe..0b0804cf64 100644 --- a/eng/scripts/Sync-VsCodeChangelog.ps1 +++ b/eng/scripts/Sync-VsCodeChangelog.ps1 @@ -56,31 +56,32 @@ param( 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 - -# Get repository root -$repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent -$mainChangelogFile = Join-Path $repoRoot $MainChangelogPath -$vscodeChangelogFile = Join-Path $repoRoot $VsCodeChangelogPath +$mainChangelogFile = Join-Path $RepoRoot $MainChangelogPath +$vscodeChangelogFile = Join-Path $RepoRoot $VsCodeChangelogPath # Validate files exist if (-not (Test-Path $mainChangelogFile)) { - Write-Error "Main CHANGELOG not found: $mainChangelogFile" + LogError "Main CHANGELOG not found: $mainChangelogFile" exit 1 } if (-not (Test-Path $vscodeChangelogFile)) { - Write-Error "VS Code CHANGELOG not found: $vscodeChangelogFile" + LogError "VS Code CHANGELOG not found: $vscodeChangelogFile" exit 1 } -Write-Host "`nVS Code Changelog Sync" -ForegroundColor Cyan -Write-Host "======================" -ForegroundColor Cyan -Write-Host "" +LogInfo "" +LogInfo "VS Code Changelog Sync" +LogInfo "======================" +LogInfo "" # Read the main CHANGELOG $mainContent = Get-Content -Path $mainChangelogFile -Raw @@ -88,7 +89,7 @@ $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) { - Write-Error "No Unreleased section found in main CHANGELOG" + LogError "No Unreleased section found in main CHANGELOG" exit 1 } @@ -100,10 +101,10 @@ if (-not $Version) { $Version = $unreleasedVersion } -Write-Host "Source: $mainChangelogFile" -ForegroundColor Gray -Write-Host "Target: $vscodeChangelogFile" -ForegroundColor Gray -Write-Host "Version: $Version" -ForegroundColor Gray -Write-Host "" +LogInfo "Source: $mainChangelogFile" +LogInfo "Target: $vscodeChangelogFile" +LogInfo "Version: $Version" +LogInfo "" # Parse sections from unreleased content $sections = @{ @@ -128,7 +129,7 @@ foreach ($line in $unreleasedContent -split "`n") { # Only process known sections $validSections = @('Features Added', 'Breaking Changes', 'Bugs Fixed', 'Other Changes') if ($currentSection -notin $validSections) { - Write-Warning "Unknown section '$currentSection' found in main CHANGELOG - skipping" + LogWarning "Unknown section '$currentSection' found in main CHANGELOG - skipping" $currentSection = $null } $currentEntries = @() @@ -217,12 +218,12 @@ while ($vscodeEntry.Count -gt 0 -and $vscodeEntry[-1] -eq "") { $vscodeEntryText = $vscodeEntry -join "`n" if ($DryRun) { - Write-Host "Preview of new VS Code CHANGELOG entry:" -ForegroundColor Cyan - Write-Host "========================================" -ForegroundColor Cyan - Write-Host "" - Write-Host $vscodeEntryText - Write-Host "" - Write-Host "DRY RUN - No files were modified" -ForegroundColor Yellow + LogInfo "Preview of new VS Code CHANGELOG entry:" + LogInfo "========================================" + LogInfo "" + LogInfo $vscodeEntryText + LogInfo "" + LogWarning "DRY RUN - No files were modified" exit 0 } @@ -232,7 +233,7 @@ $vscodeContent = Get-Content -Path $vscodeChangelogFile -Raw # Find insertion point (after "# Release History" header) $headerMatch = $vscodeContent -match '(?ms)^(# Release History\s*\n)' if (-not $headerMatch) { - Write-Error "Could not find '# Release History' header in VS Code CHANGELOG" + LogError "Could not find '# Release History' header in VS Code CHANGELOG" exit 1 } @@ -246,11 +247,11 @@ $newVscodeContent = $beforeHeader + "`n" + $vscodeEntryText + "`n`n" + $afterHea # Write updated VS Code changelog $newVscodeContent | Set-Content -Path $vscodeChangelogFile -NoNewline -Encoding UTF8 -Write-Host "✓ Synced Unreleased section to VS Code CHANGELOG" -ForegroundColor Green -Write-Host " Version: $Version" -ForegroundColor Gray -Write-Host " Location: $vscodeChangelogFile" -ForegroundColor Gray -Write-Host "" -Write-Host "Summary:" -ForegroundColor Cyan +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 @@ -258,17 +259,17 @@ $otherCount = @($sections['Other Changes'] | Where-Object { $_.Trim() -ne '' }). $fixedCount = @($sections['Bugs Fixed'] | Where-Object { $_.Trim() -ne '' }).Count if ($addedCount -gt 0) { - Write-Host " - Added: $addedCount entries" -ForegroundColor Gray + LogInfo " - Added: $addedCount entries" } if ($breakingCount -gt 0 -or $otherCount -gt 0) { $totalChanged = $breakingCount + $otherCount - Write-Host " - Changed: $totalChanged entries ($breakingCount breaking, $otherCount other)" -ForegroundColor Gray + LogInfo " - Changed: $totalChanged entries ($breakingCount breaking, $otherCount other)" } if ($fixedCount -gt 0) { - Write-Host " - Fixed: $fixedCount entries" -ForegroundColor Gray + LogInfo " - Fixed: $fixedCount entries" } -Write-Host "" -Write-Host "Next steps:" -ForegroundColor Cyan -Write-Host "1. Review the changes in the VS Code CHANGELOG" -ForegroundColor Gray -Write-Host "2. Commit the updated VS Code CHANGELOG with your release" -ForegroundColor Gray -Write-Host "" +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 "" From 1abef60cd58699151ee52f465b340af7afd75ab5 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Wed, 26 Nov 2025 18:26:43 -0800 Subject: [PATCH 22/26] Added sample file --- .../changelog-entries/username-example-brief-description.yml | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 servers/Azure.Mcp.Server/changelog-entries/username-example-brief-description.yml diff --git a/servers/Azure.Mcp.Server/changelog-entries/username-example-brief-description.yml b/servers/Azure.Mcp.Server/changelog-entries/username-example-brief-description.yml new file mode 100644 index 0000000000..a1d8694d04 --- /dev/null +++ b/servers/Azure.Mcp.Server/changelog-entries/username-example-brief-description.yml @@ -0,0 +1,4 @@ +pr: 1234 +changes: + - section: "Features Added" + description: "Added a new changelog entry management system" \ No newline at end of file From 8d9758e84e6ad57bcfa55006f3c5fbfd9717189a Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Wed, 26 Nov 2025 18:35:02 -0800 Subject: [PATCH 23/26] Refactored indentation and trimming logic --- eng/scripts/Compile-Changelog.ps1 | 46 ++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 0c07f04454..9c899cc8c2 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -94,6 +94,25 @@ function ConvertTo-TitleCase { return $textInfo.ToTitleCase($Text.ToLowerInvariant()) } +# 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( @@ -104,6 +123,9 @@ function Format-ChangelogEntry { # 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 @@ -139,30 +161,28 @@ function Format-ChangelogEntry { } } elseif ($i -eq $lines.Length - 1) { - # Last line - # Line is already trimmed at the end, preserve leading indentation - $trimmedLine = $line.TrimStart() - $leadingSpaces = $line.Length - $trimmedLine.Length - $indent = if ($leadingSpaces -gt 0) { $line.Substring(0, $leadingSpaces) } else { "" } + # 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 += " $indent$trimmedLine" + $formattedLines += " $normalizedLine" } else { # For regular multi-line text, add PR link to last line # Ensure valid sentence ending for non-bullet text - $needsPeriod = -not ($trimmedLine -match '^-\s+') - if ($needsPeriod -and $trimmedLine -notmatch '[.!?\)\]]$') { - $trimmedLine += "." + $content = $normalizedLine.TrimStart() + $needsPeriod = -not ($content -match '^-\s+') -and ($content -notmatch '[.!?\)\]]$') + if ($needsPeriod) { + $normalizedLine += "." } - $formattedLines += " $indent$trimmedLine$prLink" + $formattedLines += " $normalizedLine$prLink" } } else { - # Middle lines: preserve indentation and add 2 spaces for nesting - # Line is already trimmed at the end - $formattedLines += " $line" + # Middle lines: normalize indentation and add 2 spaces for nesting + $normalizedLine = Normalize-Indentation $line.TrimEnd() + $formattedLines += " $normalizedLine" } } From b0063828ecaac9b04594c5ad16540afae82e536d Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Wed, 26 Nov 2025 18:44:39 -0800 Subject: [PATCH 24/26] Refactored a couple of scripts to use logic found in `eng/common/scripts/ChangeLog-Operations.ps1` --- eng/scripts/Compile-Changelog.ps1 | 35 ++++++++-------------------- eng/scripts/Sync-VsCodeChangelog.ps1 | 14 +++++------ 2 files changed, 16 insertions(+), 33 deletions(-) diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 9c899cc8c2..96b0da0b69 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -399,10 +399,10 @@ Write-Host "Successfully validated $($entries.Count) entry/entries" -ForegroundC Write-Host "" # Group entries by section and subsection -$sectionOrder = @("Features Added", "Breaking Changes", "Bugs Fixed", "Other Changes") +# Use $RecommendedSectionHeaders from ChangeLog-Operations.ps1 (imported via common.ps1) $groupedEntries = @{} -foreach ($section in $sectionOrder) { +foreach ($section in $RecommendedSectionHeaders) { $sectionEntries = $entries | Where-Object { $_.Section -eq $section } if ($sectionEntries) { $groupedEntries[$section] = @{} @@ -480,27 +480,12 @@ if ($Version) { } Write-Host "Current version: $currentVersion" -ForegroundColor Gray - # Parse semantic version (handles formats like "2.0.0-beta.3", "2.0.0-alpha", or "1.5.2") - if ($currentVersion -match '^(\d+)\.(\d+)\.(\d+)(?:-(.+?)(?:\.(\d+))?)?$') { - $major = [int]$matches[1] - $minor = [int]$matches[2] - $patch = [int]$matches[3] - $prerelease = $matches[4] - $prereleaseNum = if ($matches[5]) { [int]$matches[5] } else { $null } - - # Increment based on prerelease status - if ($prerelease) { - if ($null -ne $prereleaseNum) { - # Has numeric suffix - increment it (e.g., beta.3 -> beta.4) - $nextVersion = "$major.$minor.$patch-$prerelease.$($prereleaseNum + 1)" - } else { - # No numeric suffix - add .1 (e.g., alpha -> alpha.1) - $nextVersion = "$major.$minor.$patch-$prerelease.1" - } - } else { - # Increment patch version (e.g., 1.5.2 -> 1.5.3) - $nextVersion = "$major.$minor.$($patch + 1)" - } + # 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 @@ -521,7 +506,7 @@ Write-Host "" # Generate markdown content from grouped entries (needed for new sections) $newEntriesMarkdown = @() -foreach ($section in $sectionOrder) { +foreach ($section in $RecommendedSectionHeaders) { if ($groupedEntries.ContainsKey($section)) { $newEntriesMarkdown += "" $newEntriesMarkdown += "### $section" @@ -611,7 +596,7 @@ if (-not $match.Success) { # Build merged content by combining existing and new entries $isFirstSection = $true - foreach ($section in $sectionOrder) { + foreach ($section in $RecommendedSectionHeaders) { # Check if we have entries (existing or new) for this section $hasExisting = $existingSections.ContainsKey($section) $hasNew = $groupedEntries.ContainsKey($section) diff --git a/eng/scripts/Sync-VsCodeChangelog.ps1 b/eng/scripts/Sync-VsCodeChangelog.ps1 index 0b0804cf64..5baf7ca7cf 100644 --- a/eng/scripts/Sync-VsCodeChangelog.ps1 +++ b/eng/scripts/Sync-VsCodeChangelog.ps1 @@ -107,11 +107,10 @@ LogInfo "Version: $Version" LogInfo "" # Parse sections from unreleased content -$sections = @{ - 'Features Added' = @() - 'Breaking Changes' = @() - 'Bugs Fixed' = @() - 'Other Changes' = @() +# Initialize sections dynamically from $RecommendedSectionHeaders (from ChangeLog-Operations.ps1 via common.ps1) +$sections = @{} +foreach ($header in $RecommendedSectionHeaders) { + $sections[$header] = @() } $currentSection = $null @@ -126,9 +125,8 @@ foreach ($line in $unreleasedContent -split "`n") { } $currentSection = $Matches[1].Trim() - # Only process known sections - $validSections = @('Features Added', 'Breaking Changes', 'Bugs Fixed', 'Other Changes') - if ($currentSection -notin $validSections) { + # 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 } From d08bb497d353557cb9ad08b45b5378e5fbbf2312 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Wed, 26 Nov 2025 19:25:05 -0800 Subject: [PATCH 25/26] Updated test file and added examples for other servers --- .../username-example-brief-description.yml | 9 +++++---- .../username-example-brief-description.yml | 5 +++++ .../username-example-brief-description.yml | 5 +++++ 3 files changed, 15 insertions(+), 4 deletions(-) create mode 100644 servers/Fabric.Mcp.Server/changelog-entries/username-example-brief-description.yml create mode 100644 servers/Template.Mcp.Server/changelog-entries/username-example-brief-description.yml diff --git a/servers/Azure.Mcp.Server/changelog-entries/username-example-brief-description.yml b/servers/Azure.Mcp.Server/changelog-entries/username-example-brief-description.yml index a1d8694d04..8ffa2017fc 100644 --- a/servers/Azure.Mcp.Server/changelog-entries/username-example-brief-description.yml +++ b/servers/Azure.Mcp.Server/changelog-entries/username-example-brief-description.yml @@ -1,4 +1,5 @@ -pr: 1234 -changes: - - section: "Features Added" - description: "Added a new changelog entry management system" \ No newline at end of file +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" \ No newline at end of file diff --git a/servers/Fabric.Mcp.Server/changelog-entries/username-example-brief-description.yml b/servers/Fabric.Mcp.Server/changelog-entries/username-example-brief-description.yml new file mode 100644 index 0000000000..8ffa2017fc --- /dev/null +++ b/servers/Fabric.Mcp.Server/changelog-entries/username-example-brief-description.yml @@ -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" \ No newline at end of file diff --git a/servers/Template.Mcp.Server/changelog-entries/username-example-brief-description.yml b/servers/Template.Mcp.Server/changelog-entries/username-example-brief-description.yml new file mode 100644 index 0000000000..8ffa2017fc --- /dev/null +++ b/servers/Template.Mcp.Server/changelog-entries/username-example-brief-description.yml @@ -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" \ No newline at end of file From 084cd9f16f46c8a16280359cc3193dcb196ef4b1 Mon Sep 17 00:00:00 2001 From: vcolin7 Date: Wed, 26 Nov 2025 19:25:28 -0800 Subject: [PATCH 26/26] Added logic for extracting PR number from the commit that added an entry file --- docs/changelog-entries.md | 34 ++++++++-------- eng/schemas/changelog-entry.schema.json | 6 +-- eng/scripts/Compile-Changelog.ps1 | 52 ++++++++++++++++++------- eng/scripts/New-ChangelogEntry.ps1 | 23 ++++++----- 4 files changed, 71 insertions(+), 44 deletions(-) diff --git a/docs/changelog-entries.md b/docs/changelog-entries.md index 39479c5078..31f3c29e5d 100644 --- a/docs/changelog-entries.md +++ b/docs/changelog-entries.md @@ -17,11 +17,10 @@ For most contributors, here's all you need: ./eng/scripts/New-ChangelogEntry.ps1 ` -ChangelogPath "servers/Azure.Mcp.Server/CHANGELOG.md" ` -Description "Your change description here" ` - -Section "Features Added" ` - -PR 1234 + -Section "Features Added" ``` -That's it! The script creates the YAML file for you. Commit it with your code changes. +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. @@ -83,8 +82,8 @@ Create a changelog entry for: **Structure**: One file per PR, supporting multiple changelog entries ```yaml -pr: # Required: PR number (use 0 if not known yet) -changes: # Required: Array of changes (minimum 1) +pr: # Optional: PR number (auto-detected from git commit during compilation if omitted) +changes: # Required: Array of changes (minimum 1) - section: # Required description: # Required subsection: # Optional @@ -92,7 +91,6 @@ changes: # Required: Array of changes (minimum 1) #### 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 @@ -103,6 +101,7 @@ changes: # Required: Array of changes (minimum 1) #### 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 @@ -121,10 +120,11 @@ The easiest way to create a changelog entry is using the generator script locate ./eng/scripts/New-ChangelogEntry.ps1 ` -ChangelogPath "servers//CHANGELOG.md" ` -Description "Added support for User-Assigned Managed Identity" ` - -Section "Features Added" ` - -PR 1234 + -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 @@ -132,8 +132,7 @@ The easiest way to create a changelog entry is using the generator script locate -ChangelogPath "servers//CHANGELOG.md" ` -Description "Updated ModelContextProtocol.AspNetCore to version 0.4.0-preview.3" ` -Section "Other Changes" ` - -Subsection "Dependency Updates" ` - -PR 1234 + -Subsection "Dependency Updates" ``` ##### With a multi-line entry @@ -149,8 +148,7 @@ Added new Foundry tools: ./eng/scripts/New-ChangelogEntry.ps1 ` -ChangelogPath "servers//CHANGELOG.md" ` -Description $description ` - -Section "Features Added" ` - -PR 1234 + -Section "Features Added" ``` ### Manual Creation @@ -160,16 +158,16 @@ If you prefer to do things manually, create a new YAML file with your changes an #### Simple entry ```yaml -pr: 1234 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 -pr: 1234 changes: - section: "Features Added" description: | @@ -182,7 +180,6 @@ changes: #### Using a subsection ```yaml -pr: 1234 changes: - section: "Other Changes" subsection: "Dependency Updates" @@ -192,7 +189,6 @@ changes: #### Multiple-entries ```yaml -pr: 1234 changes: - section: "Features Added" description: "Added support for multiple changes per PR in changelog entries" @@ -278,7 +274,7 @@ The scripts automatically validate YAML files against the schema at `eng/schemas - 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 unknown**: You can create the entry before opening a PR. Just use `pr: 0` and update it later. +- **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. @@ -306,7 +302,9 @@ Yes! Just create a single YAML file with multiple entries under the `changes` se ### Do I need to know the PR number when creating an entry? -No. You can create the YAML file before opening a PR by setting `pr: 0`, then update the PR number later when you know it. +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? diff --git a/eng/schemas/changelog-entry.schema.json b/eng/schemas/changelog-entry.schema.json index 9a43548e4a..db15dde269 100644 --- a/eng/schemas/changelog-entry.schema.json +++ b/eng/schemas/changelog-entry.schema.json @@ -3,12 +3,12 @@ "title": "Changelog Entry", "description": "Schema for individual changelog entry YAML files. One file per PR, supporting multiple changes.", "type": "object", - "required": ["pr", "changes"], + "required": ["changes"], "properties": { "pr": { "type": "integer", - "description": "Pull request number (use 0 if not known yet)", - "minimum": 0 + "description": "Pull request number (optional - auto-detected from git commit during compilation if not provided)", + "minimum": 1 }, "changes": { "type": "array", diff --git a/eng/scripts/Compile-Changelog.ps1 b/eng/scripts/Compile-Changelog.ps1 index 96b0da0b69..d1153622ee 100644 --- a/eng/scripts/Compile-Changelog.ps1 +++ b/eng/scripts/Compile-Changelog.ps1 @@ -94,6 +94,27 @@ function ConvertTo-TitleCase { 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 { @@ -268,9 +289,10 @@ if (-not (Test-Path $changelogFile)) { } # Get all YAML files, excluding the example template file -$yamlFiles = @(Get-ChildItem -Path $changelogEntriesDir -Include "*.yml", "*.yaml" -File | Where-Object { - $_.BaseName -ne 'username-example-brief-description' -}) +# 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 @@ -311,17 +333,21 @@ foreach ($file in $yamlFiles) { $yamlContent = Get-Content -Path $file.FullName -Raw $entry = $yamlContent | ConvertFrom-Yaml - # Validate required top-level fields - if (-not $entry.ContainsKey('pr')) { - Write-Error " Missing required field 'pr' in $($file.Name)" - continue - } - - if ($entry['pr'] -eq 0) { - Write-Warning " PR number is 0 in $($file.Name) (update when known)" + # 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 0) { - Write-Error " Invalid PR number in $($file.Name) (must be non-negative)" + elseif ($entry['pr'] -lt 1) { + Write-Error " Invalid PR number in $($file.Name) (must be a positive integer)" continue } diff --git a/eng/scripts/New-ChangelogEntry.ps1 b/eng/scripts/New-ChangelogEntry.ps1 index a4d0abc6b7..372135a0d1 100644 --- a/eng/scripts/New-ChangelogEntry.ps1 +++ b/eng/scripts/New-ChangelogEntry.ps1 @@ -17,7 +17,8 @@ Optional subsection for grouping related changes. Valid values: "Dependency Updates". .PARAMETER PR - Pull request number (integer). + 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). @@ -100,7 +101,8 @@ $ErrorActionPreference = "Stop" $RepoRoot = $RepoRoot.Path.Replace('\', '/') # Determine if we're in interactive mode (any required parameter is missing) -$isInteractive = (-not $ChangelogPath) -or (-not $Description) -or (-not $Section) -or (-not $PSBoundParameters.ContainsKey('PR')) +# 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) { @@ -259,7 +261,7 @@ if (-not $PSBoundParameters.ContainsKey('Subsection') -and $isInteractive) { } if (-not $PR -and $isInteractive) { - $prInput = Read-Host "`nPR number (press Enter to skip if not known yet)" + $prInput = Read-Host "`nPR number (press Enter to auto-detect from git during compilation)" if ($prInput) { $PR = [int]$prInput } @@ -281,11 +283,12 @@ if ($Filename) { } $filepath = Join-Path $changelogEntriesDir $filename -# Create YAML content in new format: pr at top level, changes as an array +# 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 = "pr: 0 # TODO: Update with actual PR number`n" + $yamlContent = "" } $yamlContent += "changes:`n" @@ -329,7 +332,7 @@ LogInfo " Description: $Description" if ($PR) { LogInfo " PR: #$PR" } else { - LogWarning " PR: Not set (remember to update before merging)" + LogInfo " PR: Will be auto-detected from git commit" } # Validate against schema if available @@ -345,9 +348,9 @@ if (Test-Path $schemaPath) { try { $yamlData = Get-Content -Path $filepath -Raw | ConvertFrom-Yaml - # Validate new format - if (-not $yamlData.pr -and $yamlData.pr -ne 0) { - LogError "PR field is required" + # 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 } @@ -379,6 +382,6 @@ LogInfo "" LogInfo "Next steps:" LogInfo "1. Commit this file with your changes" if (-not $PR) { - LogInfo "2. Update the 'pr' field in the YAML file once you have a PR number" + LogInfo "2. The PR number will be auto-detected from the git commit when compiled" } LogInfo ""