Skip to content

Read changelog entries in other formats, defined as data - #4528

Open
theletterf wants to merge 6 commits into
mainfrom
feature/changelog-es-entry-format
Open

theletterf wants to merge 6 commits into
mainfrom
feature/changelog-es-entry-format

Conversation

@theletterf

@theletterf theletterf commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

While working out what Elasticsearch still needs for unified release notes, I found that docs-builder can't read the changelog entries Elasticsearch already writes. Rather than teach it that one format in code, this makes entry formats data that a repository opts into.

Why

elastic/elasticsearch keeps one file per PR in docs/changelog/ with pr, summary, area, type and issues, plus optional breaking, deprecation and highlight blocks. docs-builder rejected all 715 of them (Property 'area' not found or Property 'summary' not found). That has two effects today:

  • Adding the shared release-notes.yml workflow to Elasticsearch would fail release-notes / validate on every PR that adds an entry, about a quarter of recent PRs.
  • The Elasticsearch pool on the CDN already holds 772 files in that format, and commit-range bundling (release unified bundle, release serverless) fails with "matches PR #N but could not be parsed" when a PR in the range has one.

Beats, elastic-agent and fleet-server have the same problem with a third format (elastic-agent-changelog-tool fragments), so a one-off conversion wouldn't last.

What

  • Formats are data. config/changelog-entry-formats.yml (embedded) defines each format as a mapping from docs-builder fields to source fields: from (first path with a value wins, nested paths like breaking.details), split, unless, template, present, per-field values translations, and ignore. Any other key in a source file is an error, so typos still surface. It ships two formats:
    • elasticsearch: summary → title, area → areas, bug/breaking/breaking-java/new-aggregation/upgrade → bug-fix/breaking-change/breaking-change/feature/other, block details/impact/body → description/impact, highlight → highlight, source_repo → prs: elastic/<repo>#<pr>.
    • elastic-agent-changelog-tool: kind, summary, description, component, pr, issue, impact, action.
  • Repositories opt in. entry_format: <name> in changelog.yml. An unknown name is a configuration error that lists the known ones. Nothing changes for a repository that doesn't set it.
  • Conversion happens where the repository's config is loaded.
    • changelog validate converts before validating. Converted entries have no products, so a missing products is accepted when products.default is set.
    • changelog upload uploads the converted entry, so the pool holds docs-builder entries from then on.
    • Commit-range bundling reads a pool entry in the docs-builder format first and falls back to the repository's format, which covers files uploaded before it set entry_format. A pool entry without products takes them like a synthesized entry: pivot.products labels, then products.default. Those PRs now have their metadata fetched for that.
  • Timestamped file names. A format with files: any (elastic-agent-changelog-tool) has file names that don't carry the PR. changelog validate assigns such a file to the PR that adds it; changelog upload looks up the PR that added each file that names none (commits?path= then commits/{sha}/pulls) and uploads the entry under that PR's number; commit-range bundling matches a pool entry that still names no PR to the PR in the range that added a file with that name (one pulls/{n}/files call per PR without a pool entry, only when such entries exist).
  • Docs: an "Other entry formats" section in create.md, entry_format in configure-ref.md, and a commented line in changelog.example.yml.

Checks

  • Elastic.Changelog.Tests: 1670/1670 pass. New tests cover both shipped formats, a format defined in the test, unknown source keys and target fields, the config key, upload conversion, validation, and a commit-range bundle with a mixed pool (two Elasticsearch-format entries, one resolved by products.default and one by a label, plus one docs-builder entry). Without entry_format, an Elasticsearch-format pool entry still fails to parse, as before.
  • docs-builder --path docs --strict: 0 errors.
  • I ran the locally built changelog validate over all 715 files in elastic/elasticsearch with its docs/changelog.yml plus entry_format: elasticsearch: 697 pass. The other 18 used the schema area Snapshot/Restore; the format now translates it and Extract&Transform to the names the Elasticsearch config uses, and with [DOCS] Read changelog entries with the elasticsearch entry format elasticsearch#162667 all 718 files on main pass.
  • Every top-level key in the current beats, elastic-agent and fleet-server fragments is covered by elastic-agent-changelog-tool.

Not covered

  • Other bundling paths (changelog bundle with a product filter, outside a commit range) don't add products to converted entries.
  • The upload step in elastic/docs-actions (changelog/upload) doesn't pass GITHUB_TOKEN to docs-builder yet, so the upload-time PR lookup would run unauthenticated until it does. That's a one-line follow-up there.
  • Elasticsearch's 772 pool files stay in their format until they are re-uploaded. Range bundling reads them through the fallback meanwhile.

Fixes elastic/docs-eng-team#1106

🤖 Generated with Claude Code

https://claude.ai/code/session_01TjZFCvQhL3DdrcjFKU6jDT

elastic/elasticsearch and elastic/elasticsearch-serverless write entries with
summary, area, their own type names, and breaking, deprecation and highlight
blocks. docs-builder rejected all 715 Elasticsearch files with "Property 'area'
not found", and range bundling failed on the 772 such files already in the
Elasticsearch entry pool.

NormalizeYaml and DeserializeEntry now convert that format to the docs-builder
schema. The files have no products: validation accepts them when
products.default is set, and range bundling resolves their products from the
pull request labels, then products.default, like a synthesized entry.

Fixes elastic/docs-eng-team#1106

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjZFCvQhL3DdrcjFKU6jDT
Comment thread docs/data/release-notes/create.md Resolved
An Elasticsearch-format entry with source_repo now puts the qualified
reference in prs, so validation still compares pr with the file name.
The docs say that only commit-range bundles fill products for these
entries.

Related to elastic/docs-eng-team#1106

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjZFCvQhL3DdrcjFKU6jDT
@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Replace the hardcoded Elasticsearch conversion with entry formats defined in
config/changelog-entry-formats.yml. A repository selects one with
entry_format in its changelog.yml, and docs-builder converts its entry files
where it reads them with that configuration: changelog validate, changelog
upload (the pool then holds docs-builder entries), and commit-range bundling
(for pool entries uploaded before the repository set it).

Ships two formats: elasticsearch and elastic-agent-changelog-tool (beats,
elastic-agent, fleet-server). Adding a format is a change to the YAML file.

Related to elastic/docs-eng-team#1106

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjZFCvQhL3DdrcjFKU6jDT
@theletterf theletterf changed the title Read Elasticsearch-format changelog entries Read changelog entries in other formats, defined as data Oct 20, 2026
Comment thread config/changelog-entry-formats.yml Resolved
An entry format with files: any has file names that don't carry the PR,
as with elastic-agent-changelog-tool fragments. changelog validate
assigns such a file to the pull request that adds it. changelog upload
looks up the pull request that added each file that names none and
uploads the entry under that PR. Commit-range bundling matches a pool
entry that names no PR to the PR in the range that added a file with the
same name. Translate the Elasticsearch schema area names that the
elasticsearch docs/changelog.yml spells differently.

Related to elastic/docs-eng-team#1106

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjZFCvQhL3DdrcjFKU6jDT
- Upload keeps the file name as the key for formats with files: any, so
  several fragments of one PR don't overwrite each other.
- Upload reads entry_format even when --directory overrides the directory.
- Converted entries no longer count as PR-alias markers in conflict
  messages.
- Range bundling attributes a fragment to the PR that added it, not one
  that edited it, keeps looking while fragments remain unowned, and keeps
  file-name matching for docs-builder entries named <pr>.yaml.

Related to elastic/docs-eng-team#1106

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TjZFCvQhL3DdrcjFKU6jDT
@theletterf

Copy link
Copy Markdown
Member Author

@Mpdreamz Ready to review

@theletterf

Copy link
Copy Markdown
Member Author

@Mpdreamz Let's mothball this one till repos decide on whether to use a changelog file without following our standard, I'd say. This still means we might have to rewrite all existing .yml files though.

ChangelogEntryValidator.cs conflicted in the Validate doc comment: this
branch documents the new productsFromDefaults parameter, main adds a
remarks block on products.available. Kept both; the code itself merged
cleanly.

Two tests added by this branch read the bundle from the top level of the
output directory. Main now writes bundles in a subfolder, so they look
in all directories, as the other tests in that file already do.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Comment on lines +332 to +334
var orphans = candidates
.Select((candidate, index) => (candidate, index))
.Where(c => c.candidate.Entry != null && c.candidate.FileNameNumbers.Count == 0 && c.candidate.Prs.Count == 0)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: P3 (Low)

Timestamped entries that fail parsing also need attribution by their added-file identity. ParseCandidate records a conversion failure as Entry = null, but this filter excludes it from attribution, and its timestamp cannot match the actual PR number. A PR that added an invalid pool fragment is therefore treated as having no entry and gets a synthesized note, potentially returning success instead of reporting the malformed authoritative entry.

Generated by Libra. React with 👍 or 👎 to give feedback on this comment.

Comment on lines +340 to +346
foreach (var pr in resolution.PullRequests)
{
if (orphans.Count == 0)
break;

// A pull request that already has a pool entry can still have added another fragment that names no PR.
var files = await addedFiles!(options.Owner, options.Repo, pr.Number, ctx);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: P3 (Low)

If even one pool entry anywhere in the repository's pool names no PR, this loop makes a pulls/{n}/files call for every PR in the range, one after another. Orphans from older releases never match a PR in the current range, so the early break rarely fires. A release range of several hundred PRs then means several hundred sequential API calls. The PR description says this costs one call per PR without a pool entry, but the code calls for every PR.

Generated by Libra. Specialist: libra-complexity. React with 👍 or 👎 to give feedback on this comment.

Comment on lines +346 to +347
var files = await addedFiles!(options.Owner, options.Repo, pr.Number, ctx);
foreach (var name in (files ?? []).Select(f => f[(f.LastIndexOf('/') + 1)..]))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: P3 (Low)

If addedFiles fails, it returns null, and this code treats that as "no files" without any diagnostic. The PR's orphan fragment then stays unattributed and is left out of the bundle. The PR gets an entry synthesized from its title instead, and the bundle succeeds silently with the wrong release-note text. Consider emitting a warning or error when the lookup fails.

Generated by Libra. Specialist: libra-complexity. React with 👍 or 👎 to give feedback on this comment.

Comment on lines +376 to +379
return (Deserialize(content), false);
}
catch (YamlException) when (entryFormat != null)
{

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: P3 (Low)

When a repository sets entry_format, any docs-builder pool entry with a genuine YAML error, such as a typo'd key, is retried in the repository's format. The original error is thrown away. The "could not be parsed" diagnostic then reports the format's errors instead, for example "Unknown key 'title'; Unknown key 'products' … for the 'elasticsearch' entry format", which hides the real problem. Consider keeping the first exception and including it in the message when the fallback also fails.

Generated by Libra. Specialist: libra-complexity. React with 👍 or 👎 to give feedback on this comment.

Comment on lines +141 to +145
if (config.EntryFormat is { FileNamesCarryPr: false })
{
// The format's file names don't carry the PR (for example timestamped fragments): a file this PR adds
// belongs to this PR.
filenamePrNumbers[relPath] = input.PrNumber;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: P2 (Medium)

Only attribute a fragment to the current PR when that PR added it, rather than assigning every changed fragment to it. File discovery includes modified files, so a PR that merely edits an older fragment—even one whose pr explicitly references another PR—now satisfies RequireChangelogFile. The presence check uses this fabricated filename-to-PR mapping, while upload and bundling retain the older PR reference, leaving the current PR without the entry that validation claimed it had.

Generated by Libra. React with 👍 or 👎 to give feedback on this comment.

Comment on lines +167 to +170
// A repository with its own entry format (entry_format in changelog.yml) has its files converted first.
var entryYaml = config.EntryFormat is { } format
? format.ToEntryYaml(rawYaml, relPath, $"https://github.com/{input.Owner}/{input.Repo}/pull/{input.PrNumber}")
: rawYaml;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: P2 (Medium)

Accept docs-builder entries before falling back to the configured source format, as the pool reader does. changelog add and the documented evaluate-pr/add automation still generate canonical fields such as title, products, and prs, regardless of entry_format. This unconditional conversion rejects those fields as unknown; upload does the same. Consequently, opting into an alternate format makes successfully generated changelogs impossible to validate or upload.

Generated by Libra. React with 👍 or 👎 to give feedback on this comment.

Comment on lines +373 to +379
// A format whose file names don't carry the PR keeps them as keys: one pull request can add several fragments,
// and keying them by PR number would make them overwrite each other. They match by their prs instead.
var (canonicalFileName, markerEntries) = entryFormat is { FileNamesCarryPr: false }
? (fileName, [])
: DeriveCanonicalFileNameAndMarkers(fileName, entry, _logger);
var primaryKey = ChangelogKeys.ChangelogFileKey(org, repo, branch, canonicalFileName);
targets.Add(new UploadTarget(filePath, primaryKey));
targets.Add(new UploadTarget(filePath, primaryKey, converted));

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: P1 (High)

Preserving fragment filenames here does not preserve them through publication. ChangelogContentScrubber.BuildCanonicalKeyAndMarkers still renames every non-note entry with PR references to <minimum-pr>.yaml, and ScrubberProcessor overwrites that public key. Two timestamped fragments attributed to PR 4242 therefore overwrite each other at 4242.yaml, despite having distinct private upload keys. Carry the fragment identity through the scrubber as well, and cover the upload-to-publication path with a regression test.

Generated by Libra. React with 👍 or 👎 to give feedback on this comment.

Comment on lines +624 to +628
var number = await _prService.FetchPullRequestAddingFileAsync(org, repo, repoPath, branch, ctx);
if (number is { } pr)
result[filePath] = $"https://github.com/{org}/{repo}/pull/{pr}";
else
_logger.LogWarning("No pull request found that added {File}; uploading it without one", filePath);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Severity: P2 (Medium)

Every upload run looks up the PR again for each fragment that names none, at two GitHub calls per file. When a lookup fails, the fragment is converted without prs. A rate limit or transient error is enough to cause this, and so is the unauthenticated upload step in docs-actions mentioned in the PR, which is limited to 60 requests an hour. Because the converted content then differs from what was uploaded before, the incremental uploader replaces the correctly attributed pool entry with one that has no PR. On a failed lookup, consider skipping the file, or keeping the existing remote object, rather than downgrading it.

Generated by Libra. Specialist: libra-complexity. React with 👍 or 👎 to give feedback on this comment.

This branch was successfully deployed

2 active deployments
docs-preview — 57f61619 Deployed Nov 4, 2026 by github-actions[bot]
assembler-preview — 57f61619 Deployed Nov 4, 2026 by github-actions[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants