| name | release-runbook |
| description | Use when drafting, creating, or updating a GitHub release for elastic/elasticsearch-net (any 9.x or 8.19.x patch). Covers branch targeting, backport linking, per-PR sub-bullets, and how to summarize regenerate-client PRs with optional upstream correlation to elastic/elasticsearch-specification and elastic/client-generator-net. |
elasticsearch-net Release Runbook
How to draft a GitHub release for elastic/elasticsearch-net. Always work from the GitHub auto-generated baseline and enhance it — don't write release notes from scratch.
Conventions (must follow)
targetCommitish = the major.minor branch. 9.3.x → 9.3, 9.4.x → 9.4, 8.19.x → 8.19. NEVER main. Most important rule.
- Backports reference the original main PR only. When the GH baseline lists a
[X.Y] backport, replace it with the original main PR (drop the [X.Y] prefix). The backport PR is not linked at all — not the URL, not the number, not the bot author. Rationale: attribute the work to where it was originally done; the backport is an internal mechanism, not user-facing.
- Issue references.
Fixes <full-issue-url> for main bullets, (#issue) shorthand for sub-bullets.
- Regenerate PR uses the
[X.Y] branch-prefixed PR, never the main one. Summarize with up to 5 grounded non-breaking sub-bullets, plus every breaking change (see rule 9).
- Multiple regenerate PRs in one release. Combine into one line item; link both PRs.
- Always create the release as a draft. Use
gh release create ... --draft. Never publish — the user reviews and publishes manually. Default to --draft even when the user's request doesn't explicitly say "draft" — a request to "create release X.Y.Z" is not authorization to publish. Rationale: publishing has user-visible consequences (notifications, package release pipelines, public visibility) the user wants to control manually after reviewing the generated notes.
- Every non-regen PR gets up to 2 sub-bullets describing what changed/fixed/improved. Skip when the title is already fully self-describing.
- Best-effort upstream correlation for the regen PR — try to match diffs to PRs in
elastic/elasticsearch-specification (spec changes) and elastic/client-generator-net (generator changes). Optional grounding; don't fabricate matches.
- Breaking changes are never capped. Every breaking change gets its own sub-bullet, prefixed
**Breaking** (or **Breaking (area)** when there are enough to group by Elasticsearch area), and these do not count against the sub-bullet caps in rules 4 and 7. List them after the non-breaking bullets. When a release carries any breaking change, open the notes with a > [!WARNING] callout telling readers to review the list before upgrading. Rationale: a cap that silently drops a breaking change leaves users to discover it as a compile error after upgrading; 8.19.0 set the > [!WARNING] precedent.
Quick reference: version → target branch
| Version line | Target branch |
|---|
| 9.4.x | 9.4 |
| 9.3.x | 9.3 |
| 8.19.x | 8.19 |
Read the version from the user's request, pick the branch deterministically. If the version line is unfamiliar, ask before guessing.
Workflow
Six phases: gather → baseline → enhance non-regen → analyze regen → review → draft.
1. Gather PRs and closed issues since the previous tag
Find the previous tag on the same major.minor:
gh release list -R elastic/elasticsearch-net --limit 10
PRs merged into the branch since the previous tag's date:
gh pr list -R elastic/elasticsearch-net --state merged --base <branch> \
--search "merged:>=<prev-tag-date>" \
--json number,title,author,baseRefName,body,url,closingIssuesReferences
Recently closed issues — used in step 4 to ground regen sub-bullets:
gh issue list -R elastic/elasticsearch-net --state closed \
--search "closed:>=<prev-tag-date>" \
--json number,title,closedAt,url --limit 50
2. Generate the GH baseline
gh api repos/elastic/elasticsearch-net/releases/generate-notes \
-F tag_name=X.Y.Z -F target_commitish=X.Y -F previous_tag_name=X.Y.W \
-q .body
This produces the canonical skeleton: ## What's Changed, then bullets, optional ## New Contributors, then **Full Changelog**: .../compare/X.Y.W...X.Y.Z. Use it as-is — do not add ### Bug Fixes / ### Features category headers (no prior release uses them).
3. Enhance each non-regenerate line
For every non-regen PR in the baseline:
Detect backports. Signals: title starts with [X.Y], body mentions "backport"/"cherry-pick"/an original PR. Find the original:
gh pr view <num> -R elastic/elasticsearch-net --json body,commits
gh pr list -R elastic/elasticsearch-net --state merged --base main \
--search "<title-keywords-without-[X.Y]-prefix>"
If a backport, replace the line with the original main PR's title (no [X.Y] prefix), author, and URL. The backport PR is not mentioned.
Link related issues. From gh pr view <num> --json closingIssuesReferences,body, render as Fixes https://github.com/elastic/elasticsearch-net/issues/<n> inline.
Add ≤2 sub-bullets describing what changed/fixed/improved. Sources in priority order:
- PR body — if it explains the problem/fix in plain language, distill it.
- Closing issue title/body — the user-visible symptom or expected behavior.
- The diff — for small targeted PRs, the changed code paths often imply the bullet (e.g. "now supports X in Y", "no longer throws Z when W").
Skip sub-bullets entirely when the title is already fully self-describing — simple version bumps, trivial renames, dependency updates. Don't pad.
Format:
* <PR title> by @<author> in <PR url>
* <bullet 1>
* <bullet 2>
4. Analyze the [X.Y] Regenerate client PR(s)
This is the heart of the runbook. The PR body is always "As titled.", so all signal comes from the diff.
Pick the branch-prefixed PR, e.g. [9.3] Regenerate client. NEVER the main Regenerate client PR.
gh pr view <num> -R elastic/elasticsearch-net --json files,additions,deletions
gh pr diff <num> -R elastic/elasticsearch-net
Categorize changes by file-path signal:
| Signal | Category |
|---|
New *Request.g.cs + *Response.g.cs triplet under _Generated/Api/<area>/ | New endpoint (likely spec change) |
Deleted *Response.g.cs under _Generated/Api/ | Removed/consolidated response (potentially breaking; spec change) |
New *.g.cs under _Generated/Types/QueryDsl/ or Aggregations/ | New query/aggregation type (spec change) |
New small enum/type file under _Generated/Types/ (e.g. *Flag.g.cs) | New enum value or type (spec change) |
Added DefaultRequestConfiguration { Accept = ... } block | Content-type fix (spec change — often resolves a deserialization bug) |
*.csproj version change (e.g. Elastic.Transport) | Dependency bump |
| Same code-shape change applied uniformly across many existing files (converter pattern, attribute, init block) | Generator change — likely from elastic/client-generator-net |
Small +/- on existing *Request.g.cs (XML doc only) | Skip — not user-facing |
In a modified *.g.cs: changed property type, removed/renamed member, or a member gaining/losing required | Breaking change (uncapped, rule 9) |
A sealed class becoming interface I* alongside new per-variant types and a *Factory | Breaking change: variant family split |
Changed union arm or discriminator string in a modified *.Converters.g.cs | Behavior fix — the old code mis-deserialized that arm |
Breaking changes hide in modified files, not added ones. The path signals above only catch new and deleted files, so diff the modified generated sources too:
git diff <prev-tag> origin/<branch> --name-status -- src/Elastic.Clients.Elasticsearch/_Generated
git diff <prev-tag> origin/<branch> -- '**/_Generated/**/*.g.cs'
A regen touching hundreds of files is too large to read directly, and the request-converter metadata and XML doc churn swamp the real signal. Delegate the sweep to a subagent, telling it to ignore .g.xml files, XML doc comments, SpecReferences.xml and request-converter metadata, and to report changed property types, removed members, new required members and renamed members. Verify each reported breaking change against the actual diff hunk before writing it into the notes.
Cross-reference with the closed-issue list from step 1. Match symptoms to categorized changes (e.g. issue "FieldType missing Wildcard" → enum addition; issue "SearchMvtAsync TransportException" → content-type fix). When matched, use the issue title for the bullet text and link (#issue).
Correlate to upstream repos (best effort). Two upstream sources can drive client diffs:
Spec changes (new endpoints, types, enums, fields) → query elastic/elasticsearch-specification:
gh pr list -R elastic/elasticsearch-specification --state merged \
--search "merged:>=<prev-tag-date>" --json number,title,url,body --limit 50
Match by path/area (client _Generated/Api/Reindex/Cancel*.g.cs ↔ spec PR touching specification/_global/reindex/CancelReindex*) or by keyword (spec PR title "Add Wildcard to FieldType" ↔ client adds Wildcard enum value).
Generator changes (uniform code-shape changes across many files) → query elastic/client-generator-net:
gh pr list -R elastic/client-generator-net --state merged \
--search "merged:>=<prev-tag-date>" --json number,title,url,body --limit 50
When a match is found, use the upstream PR's title/body to write a more accurate bullet — the spec/generator PR usually states the user-facing intent more clearly than the client diff. Linking the upstream PR in the release notes is optional (prior releases haven't); default to enriched bullet text rather than added URLs unless the upstream PR is materially significant (major area, breaking change).
If correlation isn't obvious within a few minutes, fall back to client-side analysis. Don't fabricate matches.
List every breaking change in its own sub-bullet (uncapped, per rule 9). Then pick the top ≤5 non-breaking bullets, prioritized:
- Bullets grounded in BOTH a closed issue AND an upstream PR (highest signal)
- Issue-grounded fixes
- New endpoints (spec-grounded if possible)
- Behavior fixes where a generated converter or discriminator was previously wrong
- New query/aggregation/enum types
- Generator improvements with user-visible effects
- Dependency bumps (only if user-visible behavior changes)
Cap the non-breaking bullets at 5 even if more candidates qualify. Never drop a breaking change to fit a cap.
Format:
* Regenerate client by @<author> in https://github.com/elastic/elasticsearch-net/pull/<num>
* Fixes <symptom> (#<issue>)
* <other category bullet>
5. Combining multiple regenerate PRs
If two or more [X.Y] Regenerate client PRs fall in the release window, diff each, categorize each, combine + dedupe, then pick the top ≤5 non-breaking bullets across the union (breaking changes stay uncapped). Link both PRs on one line:
* Regenerate client by @<author> in <PR1-url> and <PR2-url>
* <combined-bullet-1>
* ...
6. Keep ## New Contributors verbatim
GitHub auto-generates this section from the contributor graph. Don't invent it, don't remove it. If GH omitted it, there are no new contributors this release — that's correct.
7. Pre-draft checklist
8. Create the draft
ALWAYS use --draft. Never publish a release. Hand the resulting draft URL to the user; the user reviews and publishes manually.
gh release create X.Y.Z \
--target X.Y \
--title X.Y.Z \
--draft \
--notes-file release-notes.md
Never include AI/Claude attribution anywhere in release notes.
Common mistakes
| Mistake | Why wrong |
|---|
Targeting main | Releases must cut from 9.3 (or matching minor) |
Using the Regenerate client PR from main | Use the [X.Y] branch-prefixed one |
Adding ### Bug Fixes category headers | No prior release uses them |
Tagging as v9.3.6 | Tags have no v prefix |
Linking the [X.Y] backport PR (or both) | Reference ONLY the original main PR — the backport is not linked |
| >5 non-breaking regen bullets / unsubstantiated bullets | Cap non-breaking at 5; ground each in a diff signal, closed issue, or upstream PR |
| >2 non-breaking sub-bullets on a non-regen PR | Cap is 2 |
| Dropping a breaking change to respect a cap | Breaking changes are uncapped; a missed one becomes a compile error for users |
| Padding sub-bullets when the title already describes the PR fully | Skip them entirely in that case |
| Fabricating an upstream PR correlation | Only correlate when the match is clear; otherwise client-diff analysis |
Publishing the release directly (omitting --draft) | Always create as draft; the user publishes manually |
Canonical example — 9.3.5 (verbatim)
Use this as the structural target. Note that prior releases (including 9.3.5) do not yet have the new ≤2-bullet sub-bullets on non-regen PRs — that rule applies on top of this structure going forward.
## What's Changed
* Hand-craft IndexSettingsTimeSeriesConverter to handle out-of-range dates by @flobernd in https://github.com/elastic/elasticsearch-net/pull/8873
* Fix DefaultMappingFor not consuming `IdPropertyName` and `DisableIdInference` by @flobernd in https://github.com/elastic/elasticsearch-net/pull/8877
* Regenerate client by @flobernd in https://github.com/elastic/elasticsearch-net/pull/8883
* Fixes `FieldType` enum doesnt have `Wildcard` value" (#8826)
* Fixes `SearchMvtAsync` throws `TransportException` on successful `HTTP 200 _mvt` responses with valid binary tile payload (#8867)
* Align LINQ-to-ES|QL integration with `Elastic.Esql` 0.11.0 by @flobernd in https://github.com/elastic/elasticsearch-net/pull/8886
**Full Changelog**: https://github.com/elastic/elasticsearch-net/compare/9.3.4...9.3.5