Skip to content

Commit d381e55

Browse files
Update changelog to pull from PR (#2338)
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
1 parent 2573bd6 commit d381e55

File tree

12 files changed

+1383
-38
lines changed

12 files changed

+1383
-38
lines changed

config/changelog.yml.example

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,3 +54,25 @@ available_products:
5454
- cloud-enterprise
5555
# Add more products as needed
5656

57+
# GitHub label mappings (optional - used when --pr option is specified)
58+
# Maps GitHub PR labels to changelog type values
59+
# When a PR has a label that matches a key, the corresponding type value is used
60+
label_to_type:
61+
# Example mappings - customize based on your label naming conventions
62+
# "type:feature": feature
63+
# "type:bug": bug-fix
64+
# "type:enhancement": enhancement
65+
# "type:breaking": breaking-change
66+
# "type:security": security
67+
68+
# Maps GitHub PR labels to changelog area values
69+
# Multiple labels can map to the same area, and a single label can map to multiple areas (comma-separated)
70+
label_to_areas:
71+
# Example mappings - customize based on your label naming conventions
72+
# "area:search": search
73+
# "area:security": security
74+
# "area:ml": machine-learning
75+
# "area:observability": observability
76+
# "area:index": index-management
77+
# "area:multiple": "search, security" # Multiple areas comma-separated
78+

docs/cli/release/changelog-add.md

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,20 +38,30 @@ docs-builder changelog add [options...] [-h|--help]
3838
`--output <string?>`
3939
: Optional: Output directory for the changelog fragment. Defaults to current directory.
4040

41+
`--owner <string?>`
42+
: Optional: GitHub repository owner (used when `--pr` is just a number).
43+
4144
`--products <List<ProductInfo>>`
4245
: Required: Products affected in format "product target lifecycle, ..." (for example, `"elasticsearch 9.2.0 ga, cloud-serverless 2025-08-05"`).
4346
: The valid product identifiers are listed in [products.yml](https://github.com/elastic/docs-builder/blob/main/config/products.yml).
4447
: The valid lifecycles are listed in [ChangelogConfiguration.cs](https://github.com/elastic/docs-builder/blob/main/src/services/Elastic.Documentation.Services/Changelog/ChangelogConfiguration.cs).
4548

4649
`--pr <string?>`
47-
: Optional: Pull request number.
50+
: Optional: Pull request URL or number (if `--owner` and `--repo` are provided).
51+
: If specified, `--title` can be derived from the PR.
52+
: If mappings are configured, `--areas` and `--type` can also be derived from the PR.
53+
54+
`--repo <string?>`
55+
: Optional: GitHub repository name (used when `--pr` is just a number).
4856

4957
`--subtype <string?>`
5058
: Optional: Subtype for breaking changes (for example, `api`, `behavioral`, or `configuration`).
5159
: The valid subtypes are listed in [ChangelogConfiguration.cs](https://github.com/elastic/docs-builder/blob/main/src/services/Elastic.Documentation.Services/Changelog/ChangelogConfiguration.cs).
5260

5361
`--title <string>`
54-
: Required: A short, user-facing title (max 80 characters)
62+
: A short, user-facing title (max 80 characters)
63+
: Required if `--pr` is not specified.
64+
: If both `--pr` and `--title` are specified, the latter value is used instead of what exists in the PR.
5565

5666
`--type <string>`
5767
: Required: Type of change (for example, `feature`, `enhancement`, `bug-fix`, or `breaking-change`).

docs/contribute/changelog.md

Lines changed: 82 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,20 +25,22 @@ Usage: changelog add [options...] [-h|--help] [--version]
2525
Add a new changelog fragment from command-line input
2626

2727
Options:
28-
--title <string> Required: A short, user-facing title (max 80 characters) (Required)
29-
--type <string> Required: Type of change (feature, enhancement, bug-fix, breaking-change, etc.) (Required)
30-
--products <List<ProductInfo>> Required: Products affected in format "product target lifecycle, ..." (e.g., "elasticsearch 9.2.0 ga, cloud-serverless 2025-08-05") (Required)
31-
--subtype <string?> Optional: Subtype for breaking changes (api, behavioral, configuration, etc.) (Default: null)
32-
--areas <string[]?> Optional: Area(s) affected (comma-separated or specify multiple times) (Default: null)
33-
--pr <string?> Optional: Pull request URL (Default: null)
34-
--issues <string[]?> Optional: Issue URL(s) (comma-separated or specify multiple times) (Default: null)
35-
--description <string?> Optional: Additional information about the change (max 600 characters) (Default: null)
36-
--impact <string?> Optional: How the user's environment is affected (Default: null)
37-
--action <string?> Optional: What users must do to mitigate (Default: null)
38-
--feature-id <string?> Optional: Feature flag ID (Default: null)
39-
--highlight <bool?> Optional: Include in release highlights (Default: null)
40-
--output <string?> Optional: Output directory for the changelog fragment. Defaults to current directory (Default: null)
41-
--config <string?> Optional: Path to the changelog.yml configuration file. Defaults to 'docs/changelog.yml' (Default: null)
28+
--products <List<ProductInfo>> Required: Products affected in format "product target lifecycle, ..." (e.g., "elasticsearch 9.2.0 ga, cloud-serverless 2025-08-05") [Required]
29+
--title <string?> Optional: A short, user-facing title (max 80 characters). Required if --pr is not specified. If --pr and --title are specified, the latter value is used instead of what exists in the PR. [Default: null]
30+
--type <string?> Optional: Type of change (feature, enhancement, bug-fix, breaking-change, etc.). Required if --pr is not specified. If mappings are configured, type can be derived from the PR. [Default: null]
31+
--subtype <string?> Optional: Subtype for breaking changes (api, behavioral, configuration, etc.) [Default: null]
32+
--areas <string[]?> Optional: Area(s) affected (comma-separated or specify multiple times) [Default: null]
33+
--pr <string?> Optional: Pull request URL or PR number (if --owner and --repo are provided). If specified, --title can be derived from the PR. If mappings are configured, --areas and --type can also be derived from the PR. [Default: null]
34+
--owner <string?> Optional: GitHub repository owner (used when --pr is just a number) [Default: null]
35+
--repo <string?> Optional: GitHub repository name (used when --pr is just a number) [Default: null]
36+
--issues <string[]?> Optional: Issue URL(s) (comma-separated or specify multiple times) [Default: null]
37+
--description <string?> Optional: Additional information about the change (max 600 characters) [Default: null]
38+
--impact <string?> Optional: How the user's environment is affected [Default: null]
39+
--action <string?> Optional: What users must do to mitigate [Default: null]
40+
--feature-id <string?> Optional: Feature flag ID [Default: null]
41+
--highlight <bool?> Optional: Include in release highlights [Default: null]
42+
--output <string?> Optional: Output directory for the changelog fragment. Defaults to current directory [Default: null]
43+
--config <string?> Optional: Path to the changelog.yml configuration file. Defaults to 'docs/changelog.yml' [Default: null]
4244
```
4345
4446
### Product format
@@ -76,22 +78,32 @@ If a configuration file exists, the command validates all its values before gene
7678
- If the configuration file contains `lifecycle`, `product`, `subtype`, or `type` values that don't match the values in `products.yml` and `ChangelogConfiguration.cs`, validation fails. The changelog file is not created.
7779
- If the configuration file contains `areas` values and they don't match what you specify in the `--areas` command option, validation fails. The changelog file is not created.
7880
81+
### GitHub label mappings
82+
83+
You can optionally add `label_to_type` and `label_to_areas` mappings in your changelog configuration.
84+
When you run the command with the `--pr` option, it can use these mappings to fill in the `type` and `areas` in your changelog based on your pull request labels.
85+
86+
Refer to [changelog.yml.example](https://github.com/elastic/docs-builder/blob/main/config/changelog.yml.example).
87+
7988
## Examples
8089
90+
### Multiple products
91+
8192
The following command creates a changelog for a bug fix that applies to two products:
8293
8394
```sh
8495
docs-builder changelog add \
85-
--title "Fixes enrich and lookup join resolution based on minimum transport version" \
86-
--type bug-fix \ <1>
87-
--products "elasticsearch 9.2.3, cloud-serverless 2025-12-02" \ <2>
96+
--title "Fixes enrich and lookup join resolution based on minimum transport version" \ <1>
97+
--type bug-fix \ <2>
98+
--products "elasticsearch 9.2.3, cloud-serverless 2025-12-02" \ <3>
8899
--areas "ES|QL"
89-
--pr "https://github.com/elastic/elasticsearch/pull/137431" <3>
100+
--pr "https://github.com/elastic/elasticsearch/pull/137431" <4>
90101
```
91102
92-
1. The type values are defined in [ChangelogConfiguration.cs](https://github.com/elastic/docs-builder/blob/main/src/services/Elastic.Documentation.Services/Changelog/ChangelogConfiguration.cs).
93-
2. The product values are defined in [products.yml](https://github.com/elastic/docs-builder/blob/main/config/products.yml).
94-
3. At this time, the PR value can be a number or a URL; it is not validated.
103+
1. This option is required only if you want to override what's derived from the PR title.
104+
2. The type values are defined in [ChangelogConfiguration.cs](https://github.com/elastic/docs-builder/blob/main/src/services/Elastic.Documentation.Services/Changelog/ChangelogConfiguration.cs).
105+
3. The product values are defined in [products.yml](https://github.com/elastic/docs-builder/blob/main/config/products.yml).
106+
4. The `--pr` value can be a full URL (such as `https://github.com/owner/repo/pull/123`, a short format (such as `owner/repo#123`) or just a number (in which case you must also provide `--owner` and `--repo` options).
95107

96108
The output file has the following format:
97109

@@ -107,3 +119,52 @@ title: Fixes enrich and lookup join resolution based on minimum transport versio
107119
areas:
108120
- ES|QL
109121
```
122+
123+
### PR label mappings
124+
125+
You can update your changelog configuration file to contain GitHub label mappings, for example:
126+
127+
```yaml
128+
# Available areas (optional - if not specified, all areas are allowed)
129+
available_areas:
130+
- search
131+
- security
132+
- machine-learning
133+
- observability
134+
- index-management
135+
- ES|QL
136+
# Add more areas as needed
137+
138+
# GitHub label mappings (optional - used when --pr option is specified)
139+
# Maps GitHub PR labels to changelog type values
140+
# When a PR has a label that matches a key, the corresponding type value is used
141+
label_to_type:
142+
# Example mappings - customize based on your label naming conventions
143+
">enhancement": enhancement
144+
">breaking": breaking-change
145+
146+
# Maps GitHub PR labels to changelog area values
147+
# Multiple labels can map to the same area, and a single label can map to multiple areas (comma-separated)
148+
label_to_areas:
149+
# Example mappings - customize based on your label naming conventions
150+
":Search Relevance/ES|QL": "ES|QL"
151+
```
152+
153+
When you use the `--pr` option to derive information from a pull request, it can make use of those mappings:
154+
155+
```sh
156+
docs-builder changelog add --pr https://github.com/elastic/elasticsearch/pull/139272 --products "elasticsearch 9.3.0" --config test/changelog.yml
157+
```
158+
159+
In this case, the changelog file derives the title, type, and areas:
160+
161+
```yaml
162+
pr: https://github.com/elastic/elasticsearch/pull/139272
163+
type: enhancement
164+
products:
165+
- product: elasticsearch
166+
target: 9.3.0
167+
areas:
168+
- ES|QL
169+
title: '[ES|QL] Take TOP_SNIPPETS out of snapshot'
170+
```

src/services/Elastic.Documentation.Services/Changelog/ChangelogConfiguration.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ public class ChangelogConfiguration
4646

4747
public List<string>? AvailableProducts { get; set; }
4848

49+
/// <summary>
50+
/// Mapping from GitHub label names to changelog type values
51+
/// </summary>
52+
public Dictionary<string, string>? LabelToType { get; set; }
53+
54+
/// <summary>
55+
/// Mapping from GitHub label names to changelog area values
56+
/// Multiple labels can map to the same area, and a single label can map to multiple areas (comma-separated)
57+
/// </summary>
58+
public Dictionary<string, string>? LabelToAreas { get; set; }
59+
4960
public static ChangelogConfiguration Default => new();
5061
}
5162

src/services/Elastic.Documentation.Services/Changelog/ChangelogInput.cs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@ namespace Elastic.Documentation.Services.Changelog;
99
/// </summary>
1010
public class ChangelogInput
1111
{
12-
public required string Title { get; set; }
13-
public required string Type { get; set; }
12+
public string? Title { get; set; }
13+
public string? Type { get; set; }
1414
public required List<ProductInfo> Products { get; set; }
1515
public string? Subtype { get; set; }
1616
public string[] Areas { get; set; } = [];
1717
public string? Pr { get; set; }
18+
public string? Owner { get; set; }
19+
public string? Repo { get; set; }
1820
public string[] Issues { get; set; } = [];
1921
public string? Description { get; set; }
2022
public string? Impact { get; set; }
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
// Licensed to Elasticsearch B.V under one or more agreements.
2+
// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
3+
// See the LICENSE file in the project root for more information
4+
5+
using System.Net.Http.Headers;
6+
using System.Text.Json;
7+
using System.Text.Json.Serialization;
8+
using Microsoft.Extensions.Logging;
9+
10+
namespace Elastic.Documentation.Services.Changelog;
11+
12+
/// <summary>
13+
/// Service for fetching pull request information from GitHub
14+
/// </summary>
15+
public partial class GitHubPrService(ILoggerFactory loggerFactory) : IGitHubPrService
16+
{
17+
private readonly ILogger<GitHubPrService> _logger = loggerFactory.CreateLogger<GitHubPrService>();
18+
private static readonly HttpClient HttpClient = new();
19+
20+
static GitHubPrService()
21+
{
22+
HttpClient.DefaultRequestHeaders.Add("User-Agent", "docs-builder");
23+
HttpClient.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json");
24+
}
25+
26+
/// <summary>
27+
/// Fetches pull request information from GitHub
28+
/// </summary>
29+
/// <param name="prUrl">The PR URL (e.g., https://github.com/owner/repo/pull/123, owner/repo#123, or just a number if owner/repo are provided)</param>
30+
/// <param name="owner">Optional: GitHub repository owner (used when prUrl is just a number)</param>
31+
/// <param name="repo">Optional: GitHub repository name (used when prUrl is just a number)</param>
32+
/// <param name="ctx">Cancellation token</param>
33+
/// <returns>PR information or null if fetch fails</returns>
34+
public async Task<GitHubPrInfo?> FetchPrInfoAsync(string prUrl, string? owner = null, string? repo = null, CancellationToken ctx = default)
35+
{
36+
try
37+
{
38+
var (parsedOwner, parsedRepo, prNumber) = ParsePrUrl(prUrl, owner, repo);
39+
if (parsedOwner == null || parsedRepo == null || prNumber == null)
40+
{
41+
_logger.LogWarning("Unable to parse PR URL: {PrUrl}. Owner: {Owner}, Repo: {Repo}", prUrl, owner, repo);
42+
return null;
43+
}
44+
45+
// Add GitHub token if available (for rate limiting and private repos)
46+
var githubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
47+
using var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.github.com/repos/{parsedOwner}/{parsedRepo}/pulls/{prNumber}");
48+
if (!string.IsNullOrEmpty(githubToken))
49+
{
50+
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", githubToken);
51+
}
52+
53+
_logger.LogDebug("Fetching PR info from: {ApiUrl}", request.RequestUri);
54+
55+
var response = await HttpClient.SendAsync(request, ctx);
56+
if (!response.IsSuccessStatusCode)
57+
{
58+
_logger.LogWarning("Failed to fetch PR info. Status: {StatusCode}, Reason: {ReasonPhrase}", response.StatusCode, response.ReasonPhrase);
59+
return null;
60+
}
61+
62+
var jsonContent = await response.Content.ReadAsStringAsync(ctx);
63+
var prData = JsonSerializer.Deserialize(jsonContent, GitHubPrJsonContext.Default.GitHubPrResponse);
64+
65+
if (prData == null)
66+
{
67+
_logger.LogWarning("Failed to deserialize PR response");
68+
return null;
69+
}
70+
71+
return new GitHubPrInfo
72+
{
73+
Title = prData.Title,
74+
Labels = prData.Labels?.Select(l => l.Name).ToArray() ?? []
75+
};
76+
}
77+
catch (HttpRequestException ex)
78+
{
79+
_logger.LogWarning(ex, "HTTP error fetching PR info from GitHub");
80+
return null;
81+
}
82+
catch (TaskCanceledException)
83+
{
84+
_logger.LogWarning("Request timeout fetching PR info from GitHub");
85+
return null;
86+
}
87+
catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or ThreadAbortException))
88+
{
89+
_logger.LogWarning(ex, "Unexpected error fetching PR info from GitHub");
90+
return null;
91+
}
92+
}
93+
94+
private static (string? owner, string? repo, int? prNumber) ParsePrUrl(string prUrl, string? defaultOwner = null, string? defaultRepo = null)
95+
{
96+
// Handle full URL: https://github.com/owner/repo/pull/123
97+
if (prUrl.StartsWith("https://github.com/", StringComparison.OrdinalIgnoreCase) ||
98+
prUrl.StartsWith("http://github.com/", StringComparison.OrdinalIgnoreCase))
99+
{
100+
var uri = new Uri(prUrl);
101+
var segments = uri.Segments;
102+
// segments[0] is "/", segments[1] is "owner/", segments[2] is "repo/", segments[3] is "pull/", segments[4] is "123"
103+
if (segments.Length >= 5 && segments[3].Equals("pull/", StringComparison.OrdinalIgnoreCase))
104+
{
105+
var owner = segments[1].TrimEnd('/');
106+
var repo = segments[2].TrimEnd('/');
107+
if (int.TryParse(segments[4], out var prNum))
108+
{
109+
return (owner, repo, prNum);
110+
}
111+
}
112+
}
113+
114+
// Handle short format: owner/repo#123
115+
var hashIndex = prUrl.LastIndexOf('#');
116+
if (hashIndex > 0 && hashIndex < prUrl.Length - 1)
117+
{
118+
var repoPart = prUrl[..hashIndex];
119+
var prPart = prUrl[(hashIndex + 1)..];
120+
if (int.TryParse(prPart, out var prNum))
121+
{
122+
var repoParts = repoPart.Split('/');
123+
if (repoParts.Length == 2)
124+
{
125+
return (repoParts[0], repoParts[1], prNum);
126+
}
127+
}
128+
}
129+
130+
// Handle just a PR number when owner/repo are provided
131+
if (int.TryParse(prUrl, out var prNumber) &&
132+
!string.IsNullOrWhiteSpace(defaultOwner) && !string.IsNullOrWhiteSpace(defaultRepo))
133+
{
134+
return (defaultOwner, defaultRepo, prNumber);
135+
}
136+
137+
return (null, null, null);
138+
}
139+
140+
private sealed class GitHubPrResponse
141+
{
142+
public string Title { get; set; } = string.Empty;
143+
public List<GitHubLabel>? Labels { get; set; }
144+
}
145+
146+
private sealed class GitHubLabel
147+
{
148+
public string Name { get; set; } = string.Empty;
149+
}
150+
151+
[JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)]
152+
[JsonSerializable(typeof(GitHubPrResponse))]
153+
[JsonSerializable(typeof(GitHubLabel))]
154+
[JsonSerializable(typeof(List<GitHubLabel>))]
155+
private sealed partial class GitHubPrJsonContext : JsonSerializerContext;
156+
}
157+
158+
/// <summary>
159+
/// Information about a GitHub pull request
160+
/// </summary>
161+
public class GitHubPrInfo
162+
{
163+
public string Title { get; set; } = string.Empty;
164+
public string[] Labels { get; set; } = [];
165+
}
166+

0 commit comments

Comments
 (0)