From c6c2fd9e2b509429871068180302ffce3a2430c0 Mon Sep 17 00:00:00 2001 From: Felipe Cotti Date: Fri, 19 Jun 2026 11:00:52 -0300 Subject: [PATCH 1/2] changelog: parse release-drafter notes with `-` bullets and `##` headers ReleaseNoteParser only matched `*` bullets and `###` section headers, so it failed on release-drafter output that uses `-` bullets (GitHub-style `- by @<author> in #<n>`) and level-2 (`##`) emoji headers. This is the default release-drafter shape and what docs-builder itself emits, so `changelog gh-release` detected the format as Unknown, extracted zero PRs, and produced empty bundles. Accept both `*`/`-` bullets and section headers at level 2 or deeper, and detect the release-drafter format on `##` headers. Adds ReleaseNoteParser unit tests for the docs-builder format plus regressions for the legacy (`###`/`*`) and GitHub-default shapes. Co-authored-by: Cursor <cursoragent@cursor.com> --- .../GitHub/ReleaseNoteParser.cs | 13 ++- .../ReleaseNoteParserTests.cs | 110 ++++++++++++++++++ 2 files changed, 117 insertions(+), 6 deletions(-) create mode 100644 tests/Elastic.Changelog.Tests/ReleaseNoteParserTests.cs diff --git a/src/services/Elastic.Changelog/GitHub/ReleaseNoteParser.cs b/src/services/Elastic.Changelog/GitHub/ReleaseNoteParser.cs index 36c0b9fa2a..21fafb452f 100644 --- a/src/services/Elastic.Changelog/GitHub/ReleaseNoteParser.cs +++ b/src/services/Elastic.Changelog/GitHub/ReleaseNoteParser.cs @@ -63,12 +63,13 @@ public record ParsedReleaseNotes /// </summary> public static partial class ReleaseNoteParser { - // Regex for PR line: "* Title by @author in #123" or "* Title by @author in https://..." - [GeneratedRegex(@"^\*\s+(.+?)\s+by\s+@([\w-]+)\s+in\s+(?:#(\d+)|https://github\.com/[^/]+/[^/]+/pull/(\d+))", RegexOptions.Multiline | RegexOptions.IgnoreCase)] + // Regex for PR line, with either bullet char: "* Title by @author in #123", + // "- Title by @author in #123", or "... in https://github.com/owner/repo/pull/123". + [GeneratedRegex(@"^[*-]\s+(.+?)\s+by\s+@([\w-]+)\s+in\s+(?:#(\d+)|https://github\.com/[^/]+/[^/]+/pull/(\d+))", RegexOptions.Multiline | RegexOptions.IgnoreCase)] private static partial Regex PrLineRegex(); - // Regex for section headers: "### 💥 Breaking Changes" or "### ✨ Features" - [GeneratedRegex(@"^###\s+(.+)$", RegexOptions.Multiline)] + // Regex for section headers at any level >= 2: "## 🐛 Bug Fixes" or "### ✨ Features". + [GeneratedRegex(@"^#{2,}\s+(.+)$", RegexOptions.Multiline)] private static partial Regex SectionHeaderRegex(); // Regex for full changelog URL @@ -157,7 +158,7 @@ public static ParsedReleaseNotes Parse(string body) public static ReleaseNoteFormat DetectFormat(string body) { // Release Drafter format has emoji prefixed section headers like: - // "### 💥 Breaking Changes", "### ✨ Features", "### 🐛 Bug Fixes" + // "## 💥 Breaking Changes", "### ✨ Features", "## 🐛 Bug Fixes" if (HasEmojiSectionHeaders(body)) return ReleaseNoteFormat.ReleaseDrafter; @@ -169,7 +170,7 @@ public static ReleaseNoteFormat DetectFormat(string body) } private static bool HasEmojiSectionHeaders(string body) => - body.Contains("###") && ReleaseDrafterEmojis.Any(body.Contains); + body.Contains("##") && ReleaseDrafterEmojis.Any(body.Contains); private static string? ExtractFullChangelogUrl(string body) { diff --git a/tests/Elastic.Changelog.Tests/ReleaseNoteParserTests.cs b/tests/Elastic.Changelog.Tests/ReleaseNoteParserTests.cs new file mode 100644 index 0000000000..1a51819e52 --- /dev/null +++ b/tests/Elastic.Changelog.Tests/ReleaseNoteParserTests.cs @@ -0,0 +1,110 @@ +// Licensed to Elasticsearch B.V under one or more agreements. +// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information + +using AwesomeAssertions; +using Elastic.Changelog.GitHub; +using Xunit; + +namespace Elastic.Changelog.Tests; + +public class ReleaseNoteParserTests +{ + // docs-builder's own release-drafter output: level-2 emoji headers and "-" bullets + // (change-template: "- $TITLE by @$AUTHOR in #$NUMBER"). + [Fact] + public void Parse_DocsBuilderReleaseDrafterFormat_ExtractsPrsAndType() + { + const string body = + """ + ## 🐛 Bug Fixes + + - fix(frontend): switch select-dom to non-throwing optional variants by @reakaleek in #3532 + - fix: move website search input container outside htmx swap region by @reakaleek in #3524 + - fix(stepper): skip headings inside steps when calculating next step heading level by @theletterf in #3525 + + **Full Changelog**: https://github.com/elastic/docs-builder/compare/1.18.0...1.18.1 + """; + + var result = ReleaseNoteParser.Parse(body); + + result.Format.Should().Be(ReleaseNoteFormat.ReleaseDrafter); + result.PrReferences.Select(p => p.PrNumber).Should().Equal(3532, 3524, 3525); + result.PrReferences.Should().OnlyContain(p => p.InferredType == "bug-fix"); + result.FullChangelogUrl.Should().Be("https://github.com/elastic/docs-builder/compare/1.18.0...1.18.1"); + } + + [Fact] + public void Parse_DocsBuilderReleaseDrafterFormat_InfersTypePerSection() + { + const string body = + """ + ## ✨ Features + + - Add a shiny thing by @alice in #1 + + ## 🐛 Bug Fixes + + - Fix a broken thing by @bob in #2 + """; + + var result = ReleaseNoteParser.Parse(body); + + result.Format.Should().Be(ReleaseNoteFormat.ReleaseDrafter); + result.PrReferences.Should().HaveCount(2); + result.PrReferences[0].Should().Match<ExtractedPrReference>(p => p.PrNumber == 1 && p.InferredType == "feature"); + result.PrReferences[1].Should().Match<ExtractedPrReference>(p => p.PrNumber == 2 && p.InferredType == "bug-fix"); + } + + // Regression: the previous, stricter shape (### headers, "*" bullets) still parses. + [Fact] + public void Parse_LegacyReleaseDrafterFormat_StillWorks() + { + const string body = + """ + ### ✨ Features + + * Add a shiny thing by @alice in #10 + + ### 🐛 Bug Fixes + + * Fix a broken thing by @bob in #11 + """; + + var result = ReleaseNoteParser.Parse(body); + + result.Format.Should().Be(ReleaseNoteFormat.ReleaseDrafter); + result.PrReferences.Select(p => p.PrNumber).Should().Equal(10, 11); + result.PrReferences[0].InferredType.Should().Be("feature"); + result.PrReferences[1].InferredType.Should().Be("bug-fix"); + } + + [Fact] + public void Parse_GitHubDefaultFormat_AcceptsBothBullets() + { + const string body = + """ + ## What's Changed + + * Asterisk change by @alice in https://github.com/elastic/docs-builder/pull/5 + - Hyphen change by @bob in #6 + + **Full Changelog**: https://github.com/elastic/docs-builder/compare/1.0.0...1.1.0 + """; + + var result = ReleaseNoteParser.Parse(body); + + result.Format.Should().Be(ReleaseNoteFormat.GitHubDefault); + result.PrReferences.Select(p => p.PrNumber).Should().Equal(5, 6); + result.PrReferences.Should().OnlyContain(p => p.InferredType == null); + } + + [Theory] + [InlineData("## 🐛 Bug Fixes")] + [InlineData("### 🐛 Bug Fixes")] + public void DetectFormat_EmojiHeadersAtAnyLevel_IsReleaseDrafter(string header) + { + ReleaseNoteParser.DetectFormat($"{header}\n\n- Fix it by @alice in #1") + .Should().Be(ReleaseNoteFormat.ReleaseDrafter); + } +} From 9bdd8cbc2842d1aa6481888508dfa1ddb560c3a6 Mon Sep 17 00:00:00 2001 From: Felipe Cotti <felipe.cotti@elastic.co> Date: Tue, 23 Jun 2026 11:14:20 -0300 Subject: [PATCH 2/2] changelog: publish docs-builder release notes to the CDN (#3538) Add docs/changelog.yml (generated via `changelog init`, then mapped to the repository's established release-drafter labels) and a changelog-publish.yml workflow that turns each published GitHub release into a CDN-hosted bundle via `changelog gh-release`. workflow_dispatch allows backfilling past release tags. Co-authored-by: Cursor <cursoragent@cursor.com> --- .github/workflows/changelog-publish.yml | 40 ++++++++++++++++++++++++ docs/changelog.yml | 41 +++++++++++++++++++++++++ 2 files changed, 81 insertions(+) create mode 100644 .github/workflows/changelog-publish.yml create mode 100644 docs/changelog.yml diff --git a/.github/workflows/changelog-publish.yml b/.github/workflows/changelog-publish.yml new file mode 100644 index 0000000000..c4ec45a09e --- /dev/null +++ b/.github/workflows/changelog-publish.yml @@ -0,0 +1,40 @@ +name: Publish changelog bundle + +# Turns a published GitHub release (drafted by release-drafter in release.yml) into a +# CDN-hosted changelog bundle via `changelog gh-release`. Entries are derived from the +# release's PRs and their labels — there are no hand-authored changelog files. +# +# workflow_dispatch is provided to backfill past releases by tag. + +on: + release: + types: [published] + workflow_dispatch: + inputs: + version: + description: 'Release tag to bundle (e.g. 1.19.0). Defaults to the latest release.' + type: string + required: false + +permissions: {} + +concurrency: + group: changelog-publish-${{ github.event.release.tag_name || inputs.version }} + cancel-in-progress: false + +jobs: + publish: + permissions: + contents: read + packages: read + id-token: write + uses: elastic/docs-actions/.github/workflows/changelog-bundle.yml@v1 + with: + mode: gh-release + repo: docs-builder + owner: elastic + version: ${{ github.event.release.tag_name || inputs.version || 'latest' }} + config: docs/changelog.yml + # docs-builder image that includes the changelog bundle/registry support. + # Pin to a released version once one ships with it. + docs-builder-version: edge diff --git a/docs/changelog.yml b/docs/changelog.yml new file mode 100644 index 0000000000..3b10102374 --- /dev/null +++ b/docs/changelog.yml @@ -0,0 +1,41 @@ +# Changelog configuration for docs-builder's own release notes. +# +# docs-builder publishes its release notes with the `changelog gh-release` flow: +# the `Release` workflow (release-drafter) publishes a GitHub release, and +# `.github/workflows/changelog-publish.yml` turns that release into a CDN-hosted +# bundle. There are no hand-authored entry files — entries are derived from the +# release's PRs and their labels. +# +# Generated with `docs-builder changelog init`, then edited for this repo. + +# Map docs-builder's PR labels to changelog types. +# The label set is the same one enforced by `.github/workflows/required-labels.yml`, +# which derives it from `.github/release-drafter.yml` (every PR carries exactly one). +# Keep this in sync with release-drafter.yml if labels ever change. +pivot: + types: + breaking-change: "breaking" + feature: "feature" + enhancement: "enhancement" + bug-fix: "bug, fix" + docs: "documentation" + # Maintenance/automation labels (chore, dependencies, automation, ci, redesign) + # are intentionally not mapped — they're excluded from release notes below. + +# Controls which PRs become changelog entries. +rules: + # `changelog:skip` is already dropped from the release body by release-drafter + # (exclude-labels); the rest keep maintenance/automation churn out of the notes. + create: + exclude: "changelog:skip, chore, dependencies, automation, ci, redesign" + +# Bundle defaults (owner/repo/link_allow_repos seeded by `changelog init`). +bundle: + output_directory: docs/releases + resolve: true + owner: elastic + repo: docs-builder + # Keep only docs-builder's own PR/issue links in resolved bundles; anything else + # is scrubbed to a private sentinel. + link_allow_repos: + - elastic/docs-builder