diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml new file mode 100644 index 000000000..91c562b94 --- /dev/null +++ b/.github/workflows/generate-changelog.yml @@ -0,0 +1,175 @@ +name: Generate Changelog + +on: + workflow_dispatch: + inputs: + since: + description: 'Generate changelog since this tag/commit (optional)' + required: false + type: string + unreleased: + description: 'Include unreleased changes' + required: false + type: boolean + default: true + +permissions: + contents: write + pull-requests: write + +jobs: + generate-changelog: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install git-cliff + run: | + curl -LsSf https://github.com/orhun/git-cliff/releases/latest/download/git-cliff-x86_64-unknown-linux-gnu.tar.gz | tar xzf - -C /usr/local/bin + + - name: Create git-cliff configuration + run: | + cat > cliff.toml << 'EOF' + [changelog] + # changelog header + header = """ + # Changelog + + All notable changes to this project will be documented in this file. + + The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), + and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + """ + # template for the changelog body + body = """ + {% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} + {% else %}\ + ## [Unreleased] + {% endif %}\ + {% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}**{{commit.scope}}**: {% endif %}\ + {{ commit.message | upper_first }}\ + {% if commit.links %} ({% for link in commit.links %}[{{link.text}}]({{link.href}}){% if not loop.last %}, {% endif %}{% endfor %}){% endif %} + {% endfor %} + {% endfor %}\ + + {% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} + ### Contributors + + Thanks to all the contributors who made this release possible: + + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} + - [@{{ contributor.username }}](https://github.com/{{ contributor.username }}) - *first-time contributor* ๐ŸŽ‰ + {% endfor %} + {% for contributor in github.contributors | filter(attribute="is_first_time", value=false) %} + - [@{{ contributor.username }}](https://github.com/{{ contributor.username }}) + {% endfor %} + {% endif %}\ + + """ + # remove the leading and trailing whitespace from the template + trim = true + # changelog footer + footer = """ + --- + + ## Previous Releases + + For release history, please refer to the individual server changelogs: + - [Azure MCP Server CHANGELOG](./servers/Azure.Mcp.Server/CHANGELOG.md) + - [Template MCP Server CHANGELOG](./servers/Template.Mcp.Server/CHANGELOG.md) + """ + + [git] + # parse the commits based on https://www.conventionalcommits.org + conventional_commits = true + # filter out commits that are not conventional + filter_unconventional = false + # process each line of a commit as an individual commit + split_commits = false + # regex for preprocessing the commit messages + commit_preprocessors = [ + # Replace issue numbers with links + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/microsoft/mcp/issues/${2}))"}, + ] + # regex for parsing and grouping commits + commit_parsers = [ + { message = "^feat", group = "๐Ÿš€ Features"}, + { message = "^fix", group = "๐Ÿ› Bug Fixes"}, + { message = "^doc", group = "๐Ÿ“š Documentation"}, + { message = "^perf", group = "โšก Performance"}, + { message = "^refactor", group = "๐Ÿšœ Refactor"}, + { message = "^style", group = "๐ŸŽจ Styling"}, + { message = "^test", group = "๐Ÿงช Testing"}, + { message = "^chore\\(release\\): prepare for", skip = true}, + { message = "^chore\\(deps.*\\)", skip = true}, + { message = "^chore\\(pr\\)", skip = true}, + { message = "^chore\\(pull\\)", skip = true}, + { message = "^chore|^ci", group = "โš™๏ธ Miscellaneous Tasks"}, + { body = ".*security", group = "๐Ÿ›ก๏ธ Security"}, + { message = "^revert", group = "โ—€๏ธ Revert"}, + ] + # protect breaking changes from being skipped due to matching a skipping commit_parser + protect_breaking_commits = false + # filter out commits that are not matched by commit parsers + filter_commits = false + # regex for matching git tags + tag_pattern = "v[0-9].*" + # regex for skipping tags + skip_tags = "v0.1.0-beta.1" + # regex for ignoring tags + ignore_tags = "" + # sort the tags topologically + topo_order = false + # sort the commits inside sections by oldest/newest order + sort_commits = "oldest" + # limit the number of commits included in the changelog. + # limit_commits = 42 + + [remote.github] + owner = "microsoft" + repo = "mcp" + token = "${{ secrets.GITHUB_TOKEN }}" + EOF + + - name: Generate changelog + run: | + # Generate changelog based on inputs + if [ "${{ github.event.inputs.since }}" != "" ]; then + since_arg="--include-path '*' --since ${{ github.event.inputs.since }}" + else + since_arg="" + fi + + if [ "${{ github.event.inputs.unreleased }}" = "true" ]; then + unreleased_arg="--unreleased" + else + unreleased_arg="" + fi + + git-cliff --config cliff.toml $since_arg $unreleased_arg --output CHANGELOG.md + + - name: Create Pull Request + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: "docs: update CHANGELOG.md with generated entries" + title: "๐Ÿ“ Update CHANGELOG.md with generated entries" + body: | + This PR updates the CHANGELOG.md file with automatically generated entries based on commit messages. + + The changelog is generated using git-cliff and follows conventional commit patterns. + + Generated with inputs: + - Since: ${{ github.event.inputs.since || 'beginning' }} + - Include unreleased: ${{ github.event.inputs.unreleased }} + branch: update-changelog-automated + base: main \ No newline at end of file diff --git a/.github/workflows/update-changelog-contributors.yml b/.github/workflows/update-changelog-contributors.yml new file mode 100644 index 000000000..9f53633b3 --- /dev/null +++ b/.github/workflows/update-changelog-contributors.yml @@ -0,0 +1,156 @@ +name: Update CHANGELOG with Contributors + +on: + release: + types: [published] + repository_dispatch: + types: [update-changelog] + workflow_dispatch: + inputs: + tag_name: + description: 'Tag name for the release' + required: true + type: string + +permissions: + contents: write + pull-requests: write + +jobs: + update-changelog: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Need full history for contributor analysis + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Get release information + id: release + run: | + if [ "${{ github.event_name }}" = "release" ]; then + echo "tag_name=${{ github.event.release.tag_name }}" >> $GITHUB_OUTPUT + echo "release_name=${{ github.event.release.name }}" >> $GITHUB_OUTPUT + echo "release_body<> $GITHUB_OUTPUT + echo "${{ github.event.release.body }}" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + elif [ "${{ github.event_name }}" = "repository_dispatch" ]; then + echo "tag_name=${{ github.event.client_payload.tag_name }}" >> $GITHUB_OUTPUT + echo "release_name=${{ github.event.client_payload.release_name }}" >> $GITHUB_OUTPUT + echo "release_body=${{ github.event.client_payload.release_body }}" >> $GITHUB_OUTPUT + else + echo "tag_name=${{ github.event.inputs.tag_name }}" >> $GITHUB_OUTPUT + echo "release_name=${{ github.event.inputs.tag_name }}" >> $GITHUB_OUTPUT + echo "release_body=" >> $GITHUB_OUTPUT + fi + + - name: Get previous release tag + id: previous_release + run: | + # Get the previous release tag for comparison + previous_tag=$(git tag --sort=-version:refname | grep -v "${{ steps.release.outputs.tag_name }}" | head -n 1) + if [ -z "$previous_tag" ]; then + # If no previous tag, use first commit + previous_tag=$(git rev-list --max-parents=0 HEAD) + fi + echo "previous_tag=$previous_tag" >> $GITHUB_OUTPUT + + - name: Get contributors since last release + id: contributors + run: | + tag_name="${{ steps.release.outputs.tag_name }}" + previous_tag="${{ steps.previous_release.outputs.previous_tag }}" + + echo "Getting contributors between $previous_tag and $tag_name" + + # Get all contributors (commit authors and co-authors) since last release + contributors=$(git log --format='%aN <%aE>' $previous_tag..$tag_name 2>/dev/null || git log --format='%aN <%aE>' $previous_tag..HEAD) + + # Also get co-authors from commit messages + co_authors=$(git log --format='%b' $previous_tag..$tag_name 2>/dev/null || git log --format='%b' $previous_tag..HEAD | grep -i "co-authored-by:" | sed 's/^[[:space:]]*[Cc]o-[Aa]uthored-[Bb]y:[[:space:]]*//') + + # Combine and deduplicate contributors + all_contributors=$(echo -e "$contributors\n$co_authors" | sort -u | grep -v "^$" | grep -v "azure-sdk" | grep -v "noreply@github.com") + + # Format contributors for markdown + formatted_contributors="" + while IFS= read -r contributor; do + if [ ! -z "$contributor" ]; then + # Extract name and email + name=$(echo "$contributor" | sed 's/ <.*>//') + email=$(echo "$contributor" | sed 's/.*<\(.*\)>.*/\1/') + + # Get GitHub username if possible + username=$(curl -s "https://api.github.com/search/users?q=$email" | jq -r '.items[0].login // empty' 2>/dev/null || echo "") + + if [ ! -z "$username" ] && [ "$username" != "null" ]; then + formatted_contributors="${formatted_contributors}- [@$username](https://github.com/$username) ($name)\n" + else + formatted_contributors="${formatted_contributors}- $name\n" + fi + fi + done <<< "$all_contributors" + + echo "contributors<> $GITHUB_OUTPUT + echo -e "$formatted_contributors" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + + - name: Update CHANGELOG + run: | + tag_name="${{ steps.release.outputs.tag_name }}" + release_name="${{ steps.release.outputs.release_name }}" + contributors="${{ steps.contributors.outputs.contributors }}" + + # Get current date + release_date=$(date +%Y-%m-%d) + + # Prepare the new release section + release_section="## [$tag_name] - $release_date\n\n" + + # Add release body if available + if [ ! -z "${{ steps.release.outputs.release_body }}" ]; then + release_section="${release_section}${{ steps.release.outputs.release_body }}\n\n" + fi + + # Add contributors section + if [ ! -z "$contributors" ]; then + release_section="${release_section}### Contributors\n\n" + release_section="${release_section}Thanks to all the contributors who made this release possible:\n\n" + release_section="${release_section}$contributors\n" + fi + + # Create a temporary file + temp_file=$(mktemp) + + # Insert the new release section after the ## [Unreleased] section + awk -v release="$release_section" ' + /^## \[Unreleased\]/ { + print + # Print everything until the next ## heading or --- + while ((getline) > 0 && !/^##/ && !/^---/) { + print + } + # Print the new release section + printf "%s\n", release + # Print the line that ended the loop (if it was a heading) + if (/^##/ || /^---/) print + } + !/^## \[Unreleased\]/ { print } + ' CHANGELOG.md > "$temp_file" + + # Replace the original file + mv "$temp_file" CHANGELOG.md + + - name: Commit and push changes + run: | + git config --local user.email "action@github.com" + git config --local user.name "GitHub Action" + + if git diff --quiet CHANGELOG.md; then + echo "No changes to CHANGELOG.md" + else + git add CHANGELOG.md + git commit -m "Update CHANGELOG.md for release ${{ steps.release.outputs.tag_name }}" + git push + fi diff --git a/.gitignore b/.gitignore index 97483d294..ab764c517 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,6 @@ node_modules/ generated/ /docs/commandline + +# Test scripts for contributor functionality +test-contributors.sh diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 000000000..1a29441f6 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,33 @@ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Automatic contributor recognition in release notes + +### Changed + +### Fixed + +### Removed + +--- + +## Previous Releases + +For release history, please refer to the individual server changelogs: +- [Azure MCP Server CHANGELOG](./servers/Azure.Mcp.Server/CHANGELOG.md) +- [Template MCP Server CHANGELOG](./servers/Template.Mcp.Server/CHANGELOG.md) + +--- + +## Contributors ๐Ÿ‘ฅ + +We appreciate all the contributors who have helped improve this project! Contributors are automatically recognized in each release. + + \ No newline at end of file diff --git a/README.md b/README.md index 074e29037..bccd01b72 100644 --- a/README.md +++ b/README.md @@ -130,6 +130,17 @@ Check out the [Azure Developer CLI (azd) templates](https://azure.github.io/awes - [MCP SDKs and Building Blocks](https://modelcontextprotocol.io/sdk) - [MCP Specification](https://spec.modelcontextprotocol.io/specification/) +## ๐Ÿ‘ฅ Contributor Recognition + +We automatically recognize and thank all contributors in our [CHANGELOG.md](./CHANGELOG.md) for each release! The system: + +- โœจ **Automatically extracts contributors** from commits and co-author tags +- ๐Ÿš€ **Updates CHANGELOG.md** with contributor recognition for each release +- ๐ŸŽฏ **Supports conventional commits** for organized change categorization +- ๐Ÿ“ **Provides manual changelog generation** using git-cliff + +Learn more about how contributor recognition works in our [Contributor Recognition Guide](./docs/CONTRIBUTOR_RECOGNITION.md). + ## Contributing This project welcomes contributions and suggestions. Most contributions require you to agree to a diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 000000000..ba0556e39 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,111 @@ +[changelog] +# changelog header +header = """ +# Changelog + +All notable changes to this project will be documented in this file. + +The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), +and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +""" +# template for the changelog body +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [Unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group | striptags | trim | upper_first }} + {% for commit in commits %} + - {% if commit.scope %}**{{commit.scope}}**: {% endif %}\ + {{ commit.message | upper_first }}\ + {% if commit.links %} ({% for link in commit.links %}[{{link.text}}]({{link.href}}){% if not loop.last %}, {% endif %}{% endfor %}){% endif %} + {% endfor %} +{% endfor %}\ + +{% if github.contributors | filter(attribute="is_first_time", value=true) | length != 0 %} + ### Contributors ๐Ÿ‘ฅ + + Thanks to all the contributors who made this release possible: + + {% for contributor in github.contributors | filter(attribute="is_first_time", value=true) %} + - [@{{ contributor.username }}](https://github.com/{{ contributor.username }}) - *first-time contributor* ๐ŸŽ‰ + {% endfor %} + {% for contributor in github.contributors | filter(attribute="is_first_time", value=false) %} + - [@{{ contributor.username }}](https://github.com/{{ contributor.username }}) + {% endfor %} +{% endif %}\ + +""" +# remove the leading and trailing whitespace from the template +trim = true +# changelog footer +footer = """ +--- + +## Previous Releases + +For release history, please refer to the individual server changelogs: +- [Azure MCP Server CHANGELOG](./servers/Azure.Mcp.Server/CHANGELOG.md) +- [Template MCP Server CHANGELOG](./servers/Template.Mcp.Server/CHANGELOG.md) + +--- + +## Contributors ๐Ÿ‘ฅ + +We appreciate all the contributors who have helped improve this project! Contributors are automatically recognized in each release. + + +""" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out commits that are not conventional +filter_unconventional = false +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Replace issue numbers with links + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/microsoft/mcp/issues/${2}))"}, + # Replace PR numbers with links + { pattern = '\((\w+\s)?#([0-9]+)\)', replace = "([#${2}](https://github.com/microsoft/mcp/pull/${2}))"}, +] +# regex for parsing and grouping commits +commit_parsers = [ + { message = "^feat", group = "๐Ÿš€ Features"}, + { message = "^fix", group = "๐Ÿ› Bug Fixes"}, + { message = "^doc", group = "๐Ÿ“š Documentation"}, + { message = "^perf", group = "โšก Performance"}, + { message = "^refactor", group = "๐Ÿšœ Refactor"}, + { message = "^style", group = "๐ŸŽจ Styling"}, + { message = "^test", group = "๐Ÿงช Testing"}, + { message = "^chore\\(release\\): prepare for", skip = true}, + { message = "^chore\\(deps.*\\)", skip = true}, + { message = "^chore\\(pr\\)", skip = true}, + { message = "^chore\\(pull\\)", skip = true}, + { message = "^chore|^ci", group = "โš™๏ธ Miscellaneous Tasks"}, + { body = ".*security", group = "๐Ÿ›ก๏ธ Security"}, + { message = "^revert", group = "โ—€๏ธ Revert"}, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = false +# filter out commits that are not matched by commit parsers +filter_commits = false +# regex for matching git tags +tag_pattern = ".*" +# regex for skipping tags +skip_tags = "" +# regex for ignoring tags +ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" + +[remote.github] +owner = "microsoft" +repo = "mcp" \ No newline at end of file diff --git a/docs/CONTRIBUTOR_RECOGNITION.md b/docs/CONTRIBUTOR_RECOGNITION.md new file mode 100644 index 000000000..99c946215 --- /dev/null +++ b/docs/CONTRIBUTOR_RECOGNITION.md @@ -0,0 +1,94 @@ +# Automatic Contributor Recognition in CHANGELOG + +This repository now automatically recognizes and thanks contributors in the CHANGELOG.md file for each release. + +## How it Works + +### Automatic Release Updates +When a new release is published on GitHub, the `update-changelog-contributors.yml` workflow automatically: + +1. **Extracts contributors** from git commits between the previous release and the current release +2. **Identifies co-authors** from commit messages using "Co-authored-by:" tags +3. **Formats contributor information** for inclusion in the CHANGELOG +4. **Updates the CHANGELOG.md** file with the new release section including contributors +5. **Commits and pushes** the changes back to the repository + +### Manual Changelog Generation +You can also manually generate a complete changelog using conventional commits: + +1. Go to the **Actions** tab in the GitHub repository +2. Run the **"Generate Changelog"** workflow +3. Optionally specify: + - **Since**: A specific tag or commit to generate changes from + - **Include unreleased**: Whether to include unreleased changes + +This will create a Pull Request with the generated CHANGELOG.md. + +## Configuration + +### Git Cliff Configuration +The changelog generation uses `git-cliff` with configuration in `cliff.toml`. This supports: + +- **Conventional Commits**: Automatically categorizes changes based on commit prefixes +- **GitHub Integration**: Pulls contributor information from GitHub API +- **Custom Grouping**: Organizes changes into logical sections (Features, Bug Fixes, etc.) + +### Supported Commit Conventions +Use these prefixes in your commit messages for automatic categorization: + +- `feat:` - ๐Ÿš€ Features +- `fix:` - ๐Ÿ› Bug Fixes +- `docs:` - ๐Ÿ“š Documentation +- `perf:` - โšก Performance +- `refactor:` - ๐Ÿšœ Refactor +- `style:` - ๐ŸŽจ Styling +- `test:` - ๐Ÿงช Testing +- `chore:` - โš™๏ธ Miscellaneous Tasks +- `ci:` - โš™๏ธ Miscellaneous Tasks +- `revert:` - โ—€๏ธ Revert + +### Co-author Recognition +To give credit to co-authors, include this in your commit message: +``` +Co-authored-by: Name +``` + +## Examples + +### Release with Contributors +```markdown +## [1.2.0] - 2024-01-15 + +### Features +- Added new MCP server functionality + +### Contributors ๐Ÿ‘ฅ + +Thanks to all the contributors who made this release possible: + +- [@username1](https://github.com/username1) - *first-time contributor* ๐ŸŽ‰ +- [@username2](https://github.com/username2) +- John Doe +``` + +### Manual Testing +You can test contributor extraction locally using: +```bash +# Test contributors since last tag +./test-contributors.sh + +# Test contributors since specific tag +./test-contributors.sh v1.0.0 +``` + +## Benefits + +1. **Community Recognition**: Automatically thanks all contributors +2. **Reduced Conflicts**: Avoids manual CHANGELOG editing conflicts +3. **Consistency**: Uses standard formatting and conventions +4. **Automation**: Reduces manual maintenance overhead +5. **GitHub Integration**: Leverages GitHub's contributor information + +## Integration with Release Process + +The contributor recognition integrates seamlessly with the existing Azure DevOps release pipelines. When releases are created via `gh release create`, the GitHub webhook triggers the contributor update workflow automatically. \ No newline at end of file