diff --git a/CODEBASE_SUMMARY.md b/AGENTS.md similarity index 91% rename from CODEBASE_SUMMARY.md rename to AGENTS.md index 803b50fb5..7ec6134eb 100644 --- a/CODEBASE_SUMMARY.md +++ b/AGENTS.md @@ -8,8 +8,8 @@ The RabbitMQ .NET Client is a comprehensive AMQP 0-9-1 client library for .NET, - **Dual-licensed**: Apache License 2.0 and Mozilla Public License 2.0 - **Target Frameworks**: .NET 8.0 and .NET Standard 2.0 - **Language**: C# 12.0 with nullable reference types enabled -- **Current Version**: 7.2.0 (in development) -- **Latest Stable**: 7.1.2 (released March 2025) +- **Current Version**: 7.2.1 (in development) +- **Latest Stable**: 7.2.0 (released November 2025) ## Major Version 7.x Changes @@ -65,7 +65,7 @@ Wraps `Connection` to provide automatic recovery from network failures: #### 3. Channel Management (`IChannel` / `Channel`) -**Location**: `projects/RabbitMQ.Client/Impl/Channel.cs` +**Location**: `projects/RabbitMQ.Client/Impl/Channel.cs` (partial class, with `Channel.BasicPublish.cs` and `Channel.PublisherConfirms.cs`) Channels are lightweight virtual connections multiplexed over a single TCP connection: @@ -223,13 +223,12 @@ var factory = new ConnectionFactory Per-channel configuration: ```csharp -var options = new CreateChannelOptions -{ - PublisherConfirmationsEnabled = true, - PublisherConfirmationTrackingEnabled = true, - OutstandingPublisherConfirmationsRateLimiter = rateLimiter, - ContinuationTimeout = TimeSpan.FromSeconds(20) -}; +var options = new CreateChannelOptions( + publisherConfirmationsEnabled: true, + publisherConfirmationTrackingEnabled: true, + outstandingPublisherConfirmationsRateLimiter: rateLimiter, + consumerDispatchConcurrency: 1 +); ``` ## Key Design Patterns @@ -305,7 +304,7 @@ Multiple levels of shutdown: - **IntegrationFixture**: Base class for integration tests - **TestConnectionRecoveryBase**: Base for recovery tests - **RabbitMQCtl**: Wrapper for `rabbitmqctl` commands -- **ToxiproxyManager**: Network failure simulation +- **ToxiproxyManager**: Network failure simulation (in `projects/Test/Integration/`) ## Build and Packaging @@ -315,11 +314,14 @@ Multiple levels of shutdown: rabbitmq-dotnet-client/ ├── projects/ │ ├── RabbitMQ.Client/ # Main client library -│ ├── RabbitMQ.Client.OAuth2/ # OAuth2 support +│ ├── RabbitMQ.Client.OAuth2/ # OAuth2 support (source) +│ ├── RabbitMQ.Client.OAuth2-NuGet/ # OAuth2 NuGet packaging │ ├── RabbitMQ.Client.OpenTelemetry/ # OTel extensions │ ├── Test/ # Test projects │ ├── Benchmarks/ # Performance benchmarks -│ └── Applications/ # Sample applications +│ ├── Applications/ # Sample applications +│ ├── toxiproxy-netcore/ # Toxiproxy .NET client (vendored) +│ └── specs/ # AMQP 0-9-1 spec files ├── .ci/ # CI configuration ├── .github/workflows/ # GitHub Actions └── packages/ # NuGet output @@ -411,11 +413,12 @@ Configurable TLS options: ## Known Issues and Limitations -### Current Issues (as of 7.1.2) +### Current Issues (as of 7.2.0) -1. **Deadlock Scenarios**: Rare deadlocks during channel close (addressed in 7.1.1) -2. **ObjectDisposedException**: Occasional exceptions during shutdown (addressed in 7.1.2) -3. **Rate Limiter**: Issues with lease acquisition (fixed in 7.1.1) +1. **Heartbeat Crashes**: Unhandled exceptions in heartbeat timer callbacks (addressed in 7.2.1) +2. **Publisher Confirm Semaphore**: Unconditional semaphore release on cancellation (addressed in 7.2.1) +3. **Channel Shutdown**: `TryComplete` needed instead of `Complete` during channel shutdown (addressed in 7.2.1) +4. **Auto-delete Entity Recovery**: Recorded bindings not removed for auto-delete entities (addressed in 7.2.1) ### Design Limitations @@ -424,7 +427,7 @@ Configurable TLS options: 3. **Frame Size**: Maximum frame size negotiated at connection time 4. **Synchronous RPC**: Only one RPC operation per channel at a time -## Future Directions (7.2.0) +## Future Directions (7.2.1) Based on the changelog and issue tracker: diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 000000000..3286dcc89 --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,68 @@ +# RabbitMQ .NET Client release process + +## Ensure builds are green: + +* [GitHub Actions](https://github.com/rabbitmq/rabbitmq-dotnet-client/actions) + + +## Update API documentation + +Note: `main` (`6.x` and later) only + +Please see [this guide](https://github.com/rabbitmq/rabbitmq-dotnet-client/blob/main/APIDOCS.md). + + +## Update CHANGELOG + +Run `tools/generate-changelog.sh` with the previous tag and the new tag: + +``` +tools/generate-changelog.sh v7.X.Y v7.X.(Y+1) +``` + +This inserts a new section into `CHANGELOG.md` after the `# Changelog` header. +The release date is set to `UNRELEASED-DATE` as a placeholder. Review the +output with `git diff CHANGELOG.md` and edit as needed before committing. + + +## Create and push release tag + +Note: `alpha` releases are versioned by default via the MinVer package. The version is based off of the most recent tag. + +RC release: + +``` +git tag -a -s -u B1B82CC0CF84BA70147EBD05D99DE30E43EAE440 -m 'rabbitmq-dotnet-client v7.X.Y-rc.1' 'v7.X.Y-rc.1' +``` + +Final release: + +``` +git tag -a -s -u B1B82CC0CF84BA70147EBD05D99DE30E43EAE440 -m 'rabbitmq-dotnet-client v7.X.Y' 'v7.X.Y' +``` + +Push! + +``` +git push --tags +``` + +## `6.x` branch + + +### Trigger build locally + +``` +cd path\to\rabbitmq-dotnet-client +git checkout v6.X.Y +git clean -xffd +.\build.bat +dotnet build ./RabbitMQDotNetClient.sln --configuration Release --property:CONCOURSE_CI_BUILD=true +dotnet nuget push -k NUGET_API_KEY -s https://api.nuget.org/v3/index.json ./packages/RabbitMQ.Client.6.X.Y.nupkg +``` + +## `main` (`7.x`) branch + +* Close the appropriate milestone, and make a note of the link to the milestone with closed issues visible +* Use the GitHub web UI or `gh release create` command to create the new release +* GitHub actions will build and publish the release to NuGet diff --git a/tools/generate-changelog.sh b/tools/generate-changelog.sh new file mode 100755 index 000000000..fc69785ad --- /dev/null +++ b/tools/generate-changelog.sh @@ -0,0 +1,269 @@ +#!/usr/bin/env bash + +# Generate a CHANGELOG.md section for a new release by querying GitHub +# for milestone issues/PRs and discovering non-milestoned PRs via git log. +# +# Usage: tools/generate-changelog.sh v7.2.0 v7.2.1 + +set -o errexit +set -o nounset +set -o pipefail + +show_usage() { + cat << EOF +Usage: $0 + +Generate a CHANGELOG.md section for a new release. + +Arguments: + previous-tag The tag of the previous release (e.g. v7.2.0) + new-tag The tag of the new release (e.g. v7.2.1) + +Both tags must include the 'v' prefix. + +Examples: + $0 v7.2.0 v7.2.1 + $0 v7.1.2 v7.2.0 +EOF +} + +if (( $# != 2 )) +then + show_usage + exit 1 +fi + +declare -r prev_tag="$1" +declare -r new_tag="$2" + +if [[ "$prev_tag" != v* ]] || [[ "$new_tag" != v* ]] +then + echo "Error: both tags must start with 'v'" >&2 + exit 1 +fi + +# Milestone name is the version without the v prefix +declare -r milestone="${new_tag#v}" + +declare -r repo_url='https://github.com/rabbitmq/rabbitmq-dotnet-client' + +# Collect all issue numbers and their categories. +# Key: issue number, Value: "bug", "enhancement", or "other" +declare -A issue_category +# Key: issue number, Value: title +declare -A issue_title +# Collect all PR data. +# Key: pr number, Value: title +declare -A pr_title +# Key: pr number, Value: author login +declare -A pr_author + +add_issue() { + local -ri num="$1" + local -r title="$2" + local -r category="$3" + + if [[ -z "${issue_category[$num]:-}" ]] + then + issue_category[$num]="$category" + issue_title[$num]="$title" + fi +} + +categorize_labels() { + local -r labels="$1" + + if echo "$labels" | jq -e 'map(.name) | index("bug")' > /dev/null 2>&1 + then + echo 'bug' + elif echo "$labels" | jq -e 'map(.name) | index("enhancement")' > /dev/null 2>&1 + then + echo 'enhancement' + else + echo 'other' + fi +} + +echo "Fetching closed issues for milestone $milestone..." >&2 + +while IFS=$'\t' read -r num title labels +do + category=$(categorize_labels "$labels") + add_issue "$num" "$title" "$category" +done < <(gh issue list \ + --state closed \ + --milestone "$milestone" \ + --json number,title,labels \ + --jq '.[] | [.number, .title, (.labels | tojson)] | @tsv') + +echo "Fetching merged PRs for milestone $milestone..." >&2 + +while IFS=$'\t' read -r num title author closing_refs +do + pr_title[$num]="$title" + pr_author[$num]="$author" + + # Add closing issues from this PR + while read -r issue_num + do + if [[ -n "$issue_num" ]] && [[ -z "${issue_category[$issue_num]:-}" ]] + then + issue_json=$(gh issue view "$issue_num" --json title,labels) + local_title=$(echo "$issue_json" | jq -r '.title') + local_labels=$(echo "$issue_json" | jq '.labels') + category=$(categorize_labels "$local_labels") + add_issue "$issue_num" "$local_title" "$category" + fi + done < <(echo "$closing_refs" | jq -r '.[].number' 2>/dev/null) +done < <(gh pr list \ + --state merged \ + --search "milestone:$milestone" \ + --json number,title,author,closingIssuesReferences \ + --jq '.[] | [.number, .title, .author.login, (.closingIssuesReferences | tojson)] | @tsv') + +echo "Discovering non-milestoned PRs from git log ($prev_tag..main)..." >&2 + +while IFS= read -r line +do + if [[ "$line" =~ Merge\ pull\ request\ \#([0-9]+) ]] + then + pr_num="${BASH_REMATCH[1]}" + + if [[ -n "${pr_title[$pr_num]:-}" ]] + then + continue + fi + + pr_json=$(gh pr view "$pr_num" --json title,author,closingIssuesReferences) + pr_title[$pr_num]=$(echo "$pr_json" | jq -r '.title') + pr_author[$pr_num]=$(echo "$pr_json" | jq -r '.author.login') + + # Add closing issues from this PR + while read -r issue_num + do + if [[ -n "$issue_num" ]] && [[ -z "${issue_category[$issue_num]:-}" ]] + then + issue_json=$(gh issue view "$issue_num" --json title,labels) + local_title=$(echo "$issue_json" | jq -r '.title') + local_labels=$(echo "$issue_json" | jq '.labels') + category=$(categorize_labels "$local_labels") + add_issue "$issue_num" "$local_title" "$category" + fi + done < <(echo "$pr_json" | jq -r '.closingIssuesReferences[].number' 2>/dev/null) + fi +done < <(git log --oneline "$prev_tag..main") + +# Build the output +output="" + +append() { + output+="$1"$'\n' +} + +append "## [$new_tag]($repo_url/tree/$new_tag) (UNRELEASED-DATE)" +append "" +append "[Full Changelog]($repo_url/compare/$prev_tag...$new_tag)" + +# Enhancements +declare -a enhancement_nums=() +for num in "${!issue_category[@]}" +do + if [[ "${issue_category[$num]}" == 'enhancement' ]] + then + enhancement_nums+=("$num") + fi +done + +if (( ${#enhancement_nums[@]} > 0 )) +then + IFS=$'\n' read -r -d '' -a enhancement_nums < <(printf '%s\n' "${enhancement_nums[@]}" | sort -n && printf '\0') || true + append "" + append '**Implemented enhancements:**' + append "" + for num in "${enhancement_nums[@]}" + do + append "- ${issue_title[$num]} [\\#$num]($repo_url/issues/$num)" + done +fi + +# Bugs +declare -a bug_nums=() +for num in "${!issue_category[@]}" +do + if [[ "${issue_category[$num]}" == 'bug' ]] + then + bug_nums+=("$num") + fi +done + +if (( ${#bug_nums[@]} > 0 )) +then + IFS=$'\n' read -r -d '' -a bug_nums < <(printf '%s\n' "${bug_nums[@]}" | sort -n && printf '\0') || true + append "" + append '**Fixed bugs:**' + append "" + for num in "${bug_nums[@]}" + do + append "- ${issue_title[$num]} [\\#$num]($repo_url/issues/$num)" + done +fi + +# Closed issues (other) +declare -a other_nums=() +for num in "${!issue_category[@]}" +do + if [[ "${issue_category[$num]}" == 'other' ]] + then + other_nums+=("$num") + fi +done + +if (( ${#other_nums[@]} > 0 )) +then + IFS=$'\n' read -r -d '' -a other_nums < <(printf '%s\n' "${other_nums[@]}" | sort -n && printf '\0') || true + append "" + append '**Closed issues:**' + append "" + for num in "${other_nums[@]}" + do + append "- ${issue_title[$num]} [\\#$num]($repo_url/issues/$num)" + done +fi + +# Merged PRs +declare -a pr_nums=() +for num in "${!pr_title[@]}" +do + pr_nums+=("$num") +done + +if (( ${#pr_nums[@]} > 0 )) +then + IFS=$'\n' read -r -d '' -a pr_nums < <(printf '%s\n' "${pr_nums[@]}" | sort -n && printf '\0') || true + append "" + append '**Merged pull requests:**' + append "" + for num in "${pr_nums[@]}" + do + author="${pr_author[$num]}" + append "- ${pr_title[$num]} [\\#$num]($repo_url/pull/$num) ([$author](https://github.com/$author))" + done +fi + +echo "Writing to CHANGELOG.md..." >&2 + +declare -r changelog='CHANGELOG.md' +tmpfile=$(mktemp) +readonly tmpfile + +# Insert after the "# Changelog" header line +{ + head -1 "$changelog" + echo "" + printf '%s' "$output" + tail -n +2 "$changelog" +} > "$tmpfile" + +mv "$tmpfile" "$changelog" + +echo "Done. Review with: git diff CHANGELOG.md" >&2