Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/changelog-publish.yml
Original file line number Diff line number Diff line change
@@ -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
41 changes: 41 additions & 0 deletions docs/changelog.yml
Original file line number Diff line number Diff line change
@@ -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
13 changes: 7 additions & 6 deletions src/services/Elastic.Changelog/GitHub/ReleaseNoteParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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;

Expand All @@ -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)
{
Expand Down
110 changes: 110 additions & 0 deletions tests/Elastic.Changelog.Tests/ReleaseNoteParserTests.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading