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. NEVERmain. 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 sub-bullets. - 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--drafteven 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) andelastic/client-generator-net(generator changes). Optional grounding; don't fabricate matches.
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 |
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.
Pick the top ≤5 bullets, prioritized:
- Bullets grounded in BOTH a closed issue AND an upstream PR (highest signal)
- Issue-grounded fixes
- New endpoints (spec-grounded if possible)
- Removed/consolidated response types (call out as breaking when applicable)
- New query/aggregation/enum types
- Generator improvements with user-visible effects
- Dependency bumps (only if user-visible behavior changes)
Cap at 5 even if more candidates qualify.
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 top ≤5 across the union. 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
-
targetCommitish= major.minor branch (NOT main) - Tag matches the version, no
vprefix (e.g.9.3.6, notv9.3.6) - Every backport line references the ORIGINAL main PR only (the
[X.Y]backport PR is NOT linked) - Every PR with a related issue references it (
Fixes <url>or(#X)) - Every non-regen PR has ≤2 sub-bullets (or none if title is fully self-describing)
- Regenerate line has ≤5 grounded sub-bullets
- Regen bullets attempted upstream correlation (spec and/or generator) where signals suggest it
- Multiple regen PRs combined into one line, both linked
-
## New Contributorskept exactly as GH generated it (or omitted if absent) -
**Full Changelog**: .../compare/<prev>...<current>line present - No category headers added
-
gh release createinvocation includes--draft
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 regen bullets / unsubstantiated bullets | Cap at 5; ground each in a diff signal, closed issue, or upstream PR |
| >2 sub-bullets on a non-regen PR | Cap is 2 |
| 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
Source: elastic/elasticsearch-net — distributed by TomeVault.