Skip to content

Commit 68e7b36

Browse files
committed
Optional title and type
1 parent 21623a9 commit 68e7b36

File tree

4 files changed

+81
-37
lines changed

4 files changed

+81
-37
lines changed

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; }

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -29,23 +29,25 @@ static GitHubPrService()
2929
/// <summary>
3030
/// Fetches pull request information from GitHub
3131
/// </summary>
32-
/// <param name="prUrl">The PR URL (e.g., https://github.com/owner/repo/pull/123 or owner/repo#123)</param>
32+
/// <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>
33+
/// <param name="owner">Optional: GitHub repository owner (used when prUrl is just a number)</param>
34+
/// <param name="repo">Optional: GitHub repository name (used when prUrl is just a number)</param>
3335
/// <param name="ctx">Cancellation token</param>
3436
/// <returns>PR information or null if fetch fails</returns>
35-
public async Task<GitHubPrInfo?> FetchPrInfoAsync(string prUrl, CancellationToken ctx = default)
37+
public async Task<GitHubPrInfo?> FetchPrInfoAsync(string prUrl, string? owner = null, string? repo = null, CancellationToken ctx = default)
3638
{
3739
try
3840
{
39-
var (owner, repo, prNumber) = ParsePrUrl(prUrl);
40-
if (owner == null || repo == null || prNumber == null)
41+
var (parsedOwner, parsedRepo, prNumber) = ParsePrUrl(prUrl, owner, repo);
42+
if (parsedOwner == null || parsedRepo == null || prNumber == null)
4143
{
42-
_logger.LogWarning("Unable to parse PR URL: {PrUrl}", prUrl);
44+
_logger.LogWarning("Unable to parse PR URL: {PrUrl}. Owner: {Owner}, Repo: {Repo}", prUrl, owner, repo);
4345
return null;
4446
}
4547

4648
// Add GitHub token if available (for rate limiting and private repos)
4749
var githubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN");
48-
using var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.github.com/repos/{owner}/{repo}/pulls/{prNumber}");
50+
using var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.github.com/repos/{parsedOwner}/{parsedRepo}/pulls/{prNumber}");
4951
if (!string.IsNullOrEmpty(githubToken))
5052
{
5153
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", githubToken);
@@ -92,7 +94,7 @@ static GitHubPrService()
9294
}
9395
}
9496

95-
private static (string? owner, string? repo, int? prNumber) ParsePrUrl(string prUrl)
97+
private static (string? owner, string? repo, int? prNumber) ParsePrUrl(string prUrl, string? defaultOwner = null, string? defaultRepo = null)
9698
{
9799
// Handle full URL: https://github.com/owner/repo/pull/123
98100
if (prUrl.StartsWith("https://github.com/", StringComparison.OrdinalIgnoreCase) ||
@@ -128,6 +130,15 @@ private static (string? owner, string? repo, int? prNumber) ParsePrUrl(string pr
128130
}
129131
}
130132

133+
// Handle just a PR number when owner/repo are provided
134+
if (int.TryParse(prUrl, out var prNumber))
135+
{
136+
if (!string.IsNullOrWhiteSpace(defaultOwner) && !string.IsNullOrWhiteSpace(defaultRepo))
137+
{
138+
return (defaultOwner, defaultRepo, prNumber);
139+
}
140+
}
141+
131142
return (null, null, null);
132143
}
133144

src/services/Elastic.Documentation.Services/ChangelogService.cs

Lines changed: 48 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,34 @@ Cancel ctx
4040
return false;
4141
}
4242

43-
// Try to fetch PR information if PR URL is provided
44-
var prInfo = await TryFetchPrInfoAsync(input.Pr, ctx);
45-
if (prInfo != null)
43+
// Validate that if PR is just a number, owner and repo must be provided
44+
if (!string.IsNullOrWhiteSpace(input.Pr) && int.TryParse(input.Pr, out _))
45+
{
46+
if (string.IsNullOrWhiteSpace(input.Owner) || string.IsNullOrWhiteSpace(input.Repo))
47+
{
48+
collector.EmitError(string.Empty, "When --pr is specified as just a number, both --owner and --repo must be provided");
49+
return false;
50+
}
51+
}
52+
53+
// If PR is specified, try to fetch PR information and derive title/type
54+
if (!string.IsNullOrWhiteSpace(input.Pr))
4655
{
47-
// Use PR title if title was not explicitly provided or is empty
48-
// Note: title is a required parameter, but user might pass empty string
56+
var prInfo = await TryFetchPrInfoAsync(input.Pr, input.Owner, input.Repo, ctx);
57+
if (prInfo == null)
58+
{
59+
collector.EmitError(string.Empty, $"Failed to fetch PR information from GitHub for PR: {input.Pr}. Cannot derive title and type.");
60+
return false;
61+
}
62+
63+
// Use PR title if title was not explicitly provided
4964
if (string.IsNullOrWhiteSpace(input.Title))
5065
{
66+
if (string.IsNullOrWhiteSpace(prInfo.Title))
67+
{
68+
collector.EmitError(string.Empty, $"PR {input.Pr} does not have a title. Please provide --title or ensure the PR has a title.");
69+
return false;
70+
}
5171
input.Title = prInfo.Title;
5272
_logger.LogInformation("Using PR title: {Title}", input.Title);
5373
}
@@ -56,18 +76,26 @@ Cancel ctx
5676
_logger.LogDebug("Using explicitly provided title, ignoring PR title");
5777
}
5878

59-
// Map labels to type if type was not explicitly provided or is empty
60-
// Note: type is a required parameter, but user might pass empty string
61-
if (string.IsNullOrWhiteSpace(input.Type) && config.LabelToType != null)
79+
// Map labels to type if type was not explicitly provided
80+
if (string.IsNullOrWhiteSpace(input.Type))
6281
{
82+
if (config.LabelToType == null || config.LabelToType.Count == 0)
83+
{
84+
collector.EmitError(string.Empty, $"Cannot derive type from PR {input.Pr} labels: no label-to-type mapping configured in changelog.yml. Please provide --type or configure label_to_type in changelog.yml.");
85+
return false;
86+
}
87+
6388
var mappedType = MapLabelsToType(prInfo.Labels, config.LabelToType);
64-
if (mappedType != null)
89+
if (mappedType == null)
6590
{
66-
input.Type = mappedType;
67-
_logger.LogInformation("Mapped PR labels to type: {Type}", input.Type);
91+
var availableLabels = prInfo.Labels.Length > 0 ? string.Join(", ", prInfo.Labels) : "none";
92+
collector.EmitError(string.Empty, $"Cannot derive type from PR {input.Pr} labels ({availableLabels}). No matching label found in label_to_type mapping. Please provide --type or add a label mapping in changelog.yml.");
93+
return false;
6894
}
95+
input.Type = mappedType;
96+
_logger.LogInformation("Mapped PR labels to type: {Type}", input.Type);
6997
}
70-
else if (!string.IsNullOrWhiteSpace(input.Type))
98+
else
7199
{
72100
_logger.LogDebug("Using explicitly provided type, ignoring PR labels");
73101
}
@@ -87,21 +115,17 @@ Cancel ctx
87115
_logger.LogDebug("Using explicitly provided areas, ignoring PR labels");
88116
}
89117
}
90-
else if (!string.IsNullOrWhiteSpace(input.Pr))
91-
{
92-
_logger.LogWarning("PR URL was provided but GitHub information could not be fetched. Continuing with provided values.");
93-
}
94118

95-
// Validate required fields
119+
// Validate required fields (must be provided either explicitly or derived from PR)
96120
if (string.IsNullOrWhiteSpace(input.Title))
97121
{
98-
collector.EmitError(string.Empty, "Title is required");
122+
collector.EmitError(string.Empty, "Title is required. Provide --title or specify --pr to derive it from the PR.");
99123
return false;
100124
}
101125

102126
if (string.IsNullOrWhiteSpace(input.Type))
103127
{
104-
collector.EmitError(string.Empty, "Type is required");
128+
collector.EmitError(string.Empty, "Type is required. Provide --type or specify --pr to derive it from PR labels (requires label_to_type mapping in changelog.yml).");
105129
return false;
106130
}
107131

@@ -284,10 +308,11 @@ Cancel ctx
284308

285309
private static ChangelogData BuildChangelogData(ChangelogInput input)
286310
{
311+
// Title and Type are guaranteed to be non-null at this point due to validation above
287312
var data = new ChangelogData
288313
{
289-
Title = input.Title,
290-
Type = input.Type,
314+
Title = input.Title!,
315+
Type = input.Type!,
291316
Subtype = input.Subtype,
292317
Description = input.Description,
293318
Impact = input.Impact,
@@ -427,7 +452,7 @@ private static string SanitizeFilename(string input)
427452
return sanitized;
428453
}
429454

430-
private async Task<GitHubPrInfo?> TryFetchPrInfoAsync(string? prUrl, Cancel ctx)
455+
private async Task<GitHubPrInfo?> TryFetchPrInfoAsync(string? prUrl, string? owner, string? repo, Cancel ctx)
431456
{
432457
if (string.IsNullOrWhiteSpace(prUrl) || _githubPrService == null)
433458
{
@@ -436,7 +461,7 @@ private static string SanitizeFilename(string input)
436461

437462
try
438463
{
439-
var prInfo = await _githubPrService.FetchPrInfoAsync(prUrl, ctx);
464+
var prInfo = await _githubPrService.FetchPrInfoAsync(prUrl, owner, repo, ctx);
440465
if (prInfo != null)
441466
{
442467
_logger.LogInformation("Successfully fetched PR information from GitHub");

src/tooling/docs-builder/Commands/ChangelogCommand.cs

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,14 @@ public Task<int> Default()
3131
/// <summary>
3232
/// Add a new changelog fragment from command-line input
3333
/// </summary>
34-
/// <param name="title">Required: A short, user-facing title (max 80 characters)</param>
35-
/// <param name="type">Required: Type of change (feature, enhancement, bug-fix, breaking-change, etc.)</param>
34+
/// <param name="title">Optional: A short, user-facing title (max 80 characters). Required if --pr is not specified. If --pr is specified, will be derived from PR title if not provided.</param>
35+
/// <param name="type">Optional: Type of change (feature, enhancement, bug-fix, breaking-change, etc.). Required if --pr is not specified. If --pr is specified, will be derived from PR labels if not provided.</param>
3636
/// <param name="products">Required: Products affected in format "product target lifecycle, ..." (e.g., "elasticsearch 9.2.0 ga, cloud-serverless 2025-08-05")</param>
3737
/// <param name="subtype">Optional: Subtype for breaking changes (api, behavioral, configuration, etc.)</param>
3838
/// <param name="areas">Optional: Area(s) affected (comma-separated or specify multiple times)</param>
39-
/// <param name="pr">Optional: Pull request URL</param>
39+
/// <param name="pr">Optional: Pull request URL or PR number (if --owner and --repo are provided). If specified, --title and --type can be derived from the PR.</param>
40+
/// <param name="owner">Optional: GitHub repository owner (used when --pr is just a number)</param>
41+
/// <param name="repo">Optional: GitHub repository name (used when --pr is just a number)</param>
4042
/// <param name="issues">Optional: Issue URL(s) (comma-separated or specify multiple times)</param>
4143
/// <param name="description">Optional: Additional information about the change (max 600 characters)</param>
4244
/// <param name="impact">Optional: How the user's environment is affected</param>
@@ -48,12 +50,14 @@ public Task<int> Default()
4850
/// <param name="ctx"></param>
4951
[Command("add")]
5052
public async Task<int> Create(
51-
string title,
52-
string type,
5353
[ProductInfoParser] List<ProductInfo> products,
54+
string? title = null,
55+
string? type = null,
5456
string? subtype = null,
5557
string[]? areas = null,
5658
string? pr = null,
59+
string? owner = null,
60+
string? repo = null,
5761
string[]? issues = null,
5862
string? description = null,
5963
string? impact = null,
@@ -78,6 +82,8 @@ public async Task<int> Create(
7882
Subtype = subtype,
7983
Areas = areas ?? [],
8084
Pr = pr,
85+
Owner = owner,
86+
Repo = repo,
8187
Issues = issues ?? [],
8288
Description = description,
8389
Impact = impact,

0 commit comments

Comments
 (0)