Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 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
7 changes: 7 additions & 0 deletions docs/cli/changelog/add.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,20 @@ When running inside GitHub Actions, `changelog add` automatically reads the foll
| --- | --- | --- |
| `CHANGELOG_PR_NUMBER` | `--prs` | `github.event.pull_request.number` |
| `CHANGELOG_TITLE` | `--title` | `steps.evaluate.outputs.title` |
| `CHANGELOG_DESCRIPTION` | `--description` | `steps.evaluate.outputs.description` |
| `CHANGELOG_TYPE` | `--type` | `steps.evaluate.outputs.type` |
| `CHANGELOG_PRODUCTS` | `--products` | `steps.evaluate.outputs.products` |
| `CHANGELOG_OWNER` | `--owner` | `github.repository_owner` |
| `CHANGELOG_REPO` | `--repo` | `github.event.repository.name` |

**Precedence**: explicit CLI arguments always take priority over environment variables. Environment variables are only used when the corresponding CLI argument is not provided.

`CHANGELOG_DESCRIPTION` has additional precedence rules related to release note extraction:

- If `--description` is provided on the command line, it always wins.
- If `--no-extract-release-notes` is passed (or `extract.release_notes: false` is set in the changelog configuration), `CHANGELOG_DESCRIPTION` is ignored. This prevents a description that was extracted by `evaluate-pr` from being applied when extraction has been disabled.
- Otherwise, `CHANGELOG_DESCRIPTION` fills `--description` when it is not set on the command line.

The filename strategy is controlled by the `filename` option in `changelog.yml` (defaulting to `timestamp`). Refer to [changelog.example.yml](https://github.com/elastic/docs-builder/blob/main/config/changelog.example.yml) for details.

This allows the CI action to invoke `changelog add` with a minimal command line:
Expand Down
1 change: 1 addition & 0 deletions docs/cli/changelog/evaluate-pr.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ docs-builder changelog evaluate-pr [options...] [-h|--help]
| `should-generate` | `true` if `changelog add` should run |
| `should-upload` | `true` if the artifact should be uploaded |
| `title` | Resolved PR title |
| `description` | Release note extracted from the PR body (when `extract.release_notes` is enabled and a release note is found). Long or multi-line release notes (>120 characters) are placed here. Passed downstream as `CHANGELOG_DESCRIPTION` for `changelog add`. |
| `type` | Resolved changelog type |
| `products` | Comma-separated product specs resolved from PR labels via `pivot.products` mappings (e.g., `cloud-hosted, cloud-serverless`) |
| `label-table` | Markdown table of configured label-to-type mappings |
Expand Down
11 changes: 11 additions & 0 deletions docs/contribute/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,6 +363,17 @@ Ideally this task will be automated such that it's performed by a bot or GitHub
If you run it from the command line, you must precede any special characters (such as backquotes) with a backslash escape character (`\`).
:::

### CI two-step flow

When automated via the [changelog GitHub Actions](https://github.com/elastic/docs-actions/tree/main/changelog), changelog creation is a two-step process:

1. **`changelog evaluate-pr`** inspects the PR (title, labels, body) and produces outputs such as `title`, `type`, `description`, and `products`.
2. **`changelog add`** reads those outputs from `CHANGELOG_*` environment variables and generates the changelog YAML file.

The `description` output from step 1 contains the release note extracted from the PR body (when `extract.release_notes` is enabled). If extraction is disabled — either by setting `extract.release_notes: false` in `changelog.yml` or by passing `--no-extract-release-notes` to `changelog add` — the `CHANGELOG_DESCRIPTION` environment variable is ignored and the extracted description is not written to the changelog.

Refer to [CI auto-detection](/cli/changelog/add.md#ci-auto-detection) for the full list of environment variables and precedence rules.

For up-to-date command usage information, use the `-h` option or refer to [](/cli/changelog/add.md).

### Authorization
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ public async Task<bool> CreateChangelog(IDiagnosticsCollector collector, CreateC
{
try
{
var cliDescription = input.Description;
input = EnrichFromCI(input);

// Load changelog configuration
Expand All @@ -92,6 +93,16 @@ public async Task<bool> CreateChangelog(IDiagnosticsCollector collector, CreateC
// Apply config defaults to input (for extract_release_notes, extract_issues)
input = ApplyConfigDefaults(input, config);

// When extraction is disabled (by CLI or config), discard any CI-injected description
// that originated from evaluate-pr's release-note extraction.
if (input.ExtractReleaseNotes == false
&& string.IsNullOrWhiteSpace(cliDescription)
&& !string.IsNullOrWhiteSpace(input.Description))
{
_logger.LogInformation("Clearing CI-provided description because release note extraction is disabled");
input = input with { Description = null };
}

// Multiple PRs: one changelog per PR (--use-pr-number uses PR number as each filename)
if (input.Prs != null && input.Prs.Length > 1)
return await CreateChangelogsForMultiplePrsAsync(collector, input, config, ctx);
Expand Down Expand Up @@ -412,11 +423,16 @@ internal CreateChangelogArguments EnrichFromCI(CreateChangelogArguments input)
? input.Products
: ProductArgument.ParseProductSpecs(ciProducts);

// Skip CI-provided description when extraction is explicitly disabled via CLI
var enrichedDescription = !string.IsNullOrWhiteSpace(input.Description)
? input.Description
: input.ExtractReleaseNotes == false ? null : ciDescription;
Comment thread
reakaleek marked this conversation as resolved.
Outdated

return input with
{
Prs = enrichedPrs,
Title = !string.IsNullOrWhiteSpace(input.Title) ? input.Title : ciTitle,
Description = !string.IsNullOrWhiteSpace(input.Description) ? input.Description : ciDescription,
Description = enrichedDescription,
Type = !string.IsNullOrWhiteSpace(input.Type) ? input.Type : ciType,
Owner = input.Owner ?? ciOwner,
Repo = !string.IsNullOrWhiteSpace(input.Repo) ? input.Repo : ciRepo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ public abstract class CreateChangelogTestBase(ITestOutputHelper output) : Change
{
protected IGitHubPrService MockGitHubService { get; } = A.Fake<IGitHubPrService>();

protected ChangelogCreationService CreateService() =>
new(LoggerFactory, ConfigurationContext, MockGitHubService, FileSystem);
protected ChangelogCreationService CreateService(IEnvironmentVariables? env = null) =>
new(LoggerFactory, ConfigurationContext, MockGitHubService, FileSystem, env);

protected async Task<string> CreateConfigDirectory(string configContent)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
using AwesomeAssertions;
using Elastic.Changelog.Creation;
using Elastic.Changelog.GitHub;
using Elastic.Documentation.Configuration;
using FakeItEasy;

namespace Elastic.Changelog.Tests.Changelogs.Create;
Expand Down Expand Up @@ -469,4 +470,144 @@ public async Task CreateChangelog_WhenExtractNotSpecifiedByCli_UsesConfigExtract
yamlContent.Should().Contain("title: Implement new aggregation API");
yamlContent.Should().NotContain("Adds support for new aggregation types");
}

[Fact]
public async Task CreateChangelog_InCI_ExtractionDisabledByCli_ClearsCIDescription()
{
var prInfo = new GitHubPrInfo
{
Title = "Implement new aggregation API",
Body = "Release Notes: Adds support for new aggregation types",
Labels = ["type:feature"]
};

A.CallTo(() => MockGitHubService.FetchPrInfoAsync(
"https://github.com/elastic/elasticsearch/pull/12345",
null,
null,
A<CancellationToken>._))
.Returns(prInfo);

// language=yaml
var configContent =
"""
pivot:
types:
feature: "type:feature"
bug-fix:
breaking-change:
lifecycles:
- preview
- beta
- ga
""";
var configPath = await CreateConfigDirectory(configContent);

// Simulate CI environment where evaluate-pr exported the extracted note
var env = A.Fake<IEnvironmentVariables>();
A.CallTo(() => env.IsRunningOnCI).Returns(true);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_PR_NUMBER")).Returns("12345");
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_TITLE")).Returns("Implement new aggregation API");
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_DESCRIPTION")).Returns("Adds support for new aggregation types");
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_TYPE")).Returns("feature");
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_OWNER")).Returns(null);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_REPO")).Returns(null);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_PRODUCTS")).Returns(null);

var service = CreateService(env);

var input = new CreateChangelogArguments
{
Prs = ["https://github.com/elastic/elasticsearch/pull/12345"],
Products = [new ProductArgument { Product = "elasticsearch", Target = "9.2.0", Lifecycle = "ga" }],
Config = configPath,
Output = CreateOutputDirectory(),
ExtractReleaseNotes = false
};

// Act
var result = await service.CreateChangelog(Collector, input, TestContext.Current.CancellationToken);

// Assert - extraction disabled, so CI description should be suppressed
result.Should().BeTrue();
Collector.Errors.Should().Be(0);

var files = FileSystem.Directory.GetFiles(input.Output, "*.yaml");
files.Should().HaveCount(1);

var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current.CancellationToken);
yamlContent.Should().Contain("title: Implement new aggregation API");
yamlContent.Should().NotContain("Adds support for new aggregation types");
}

[Fact]
public async Task CreateChangelog_InCI_ExtractionDisabledByConfig_ClearsCIDescription()
{
var prInfo = new GitHubPrInfo
{
Title = "Implement new aggregation API",
Body = "Release Notes: Adds support for new aggregation types",
Labels = ["type:feature"]
};

A.CallTo(() => MockGitHubService.FetchPrInfoAsync(
"https://github.com/elastic/elasticsearch/pull/12345",
null,
null,
A<CancellationToken>._))
.Returns(prInfo);

// language=yaml
var configContent =
"""
extract:
release_notes: false
pivot:
types:
feature: "type:feature"
bug-fix:
breaking-change:
lifecycles:
- preview
- beta
- ga
""";
var configPath = await CreateConfigDirectory(configContent);

// Simulate CI where evaluate-pr (using a different config) exported the note
var env = A.Fake<IEnvironmentVariables>();
A.CallTo(() => env.IsRunningOnCI).Returns(true);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_PR_NUMBER")).Returns("12345");
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_TITLE")).Returns("Implement new aggregation API");
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_DESCRIPTION")).Returns("Adds support for new aggregation types");
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_TYPE")).Returns("feature");
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_OWNER")).Returns(null);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_REPO")).Returns(null);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_PRODUCTS")).Returns(null);

var service = CreateService(env);

var input = new CreateChangelogArguments
{
Prs = ["https://github.com/elastic/elasticsearch/pull/12345"],
Products = [new ProductArgument { Product = "elasticsearch", Target = "9.2.0", Lifecycle = "ga" }],
Config = configPath,
Output = CreateOutputDirectory(),
ExtractReleaseNotes = null
};

// Act
var result = await service.CreateChangelog(Collector, input, TestContext.Current.CancellationToken);

// Assert - config disables extraction, so CI description should be cleared
result.Should().BeTrue();
Collector.Errors.Should().Be(0);

var files = FileSystem.Directory.GetFiles(input.Output, "*.yaml");
files.Should().HaveCount(1);

var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current.CancellationToken);
yamlContent.Should().Contain("title: Implement new aggregation API");
yamlContent.Should().NotContain("Adds support for new aggregation types");
Comment thread
reakaleek marked this conversation as resolved.
}
Comment thread
reakaleek marked this conversation as resolved.
}
74 changes: 74 additions & 0 deletions tests/Elastic.Changelog.Tests/Creation/CIEnrichmentTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ private static CreateChangelogArguments DefaultInput() =>
private static IEnvironmentVariables FakeCIEnv(
string? prNumber = null,
string? title = null,
string? description = null,
string? type = null,
string? owner = null,
string? repo = null,
Expand All @@ -27,6 +28,7 @@ private static IEnvironmentVariables FakeCIEnv(
A.CallTo(() => env.IsRunningOnCI).Returns(true);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_PR_NUMBER")).Returns(prNumber);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_TITLE")).Returns(title);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_DESCRIPTION")).Returns(description);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_TYPE")).Returns(type);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_OWNER")).Returns(owner);
A.CallTo(() => env.GetEnvironmentVariable("CHANGELOG_REPO")).Returns(repo);
Expand Down Expand Up @@ -231,4 +233,76 @@ public void EnrichFromCI_InCI_NoProducts_RemainsEmpty()

result.Products.Should().BeEmpty();
}

[Fact]
public void EnrichFromCI_InCI_Description_FillsMissingDescription()
{
var env = FakeCIEnv(prNumber: "42", title: "Fix", description: "Extracted release note");
var service = CreateServiceWithEnv(env);
var input = DefaultInput();

var result = service.EnrichFromCI(input);

result.Description.Should().Be("Extracted release note");
}

[Fact]
public void EnrichFromCI_InCI_ExplicitDescription_CLIWins()
{
var env = FakeCIEnv(prNumber: "42", title: "Fix", description: "CI description");
var service = CreateServiceWithEnv(env);
var input = DefaultInput() with { Description = "My explicit description" };

var result = service.EnrichFromCI(input);

result.Description.Should().Be("My explicit description");
}

[Fact]
public void EnrichFromCI_InCI_ExtractionDisabled_SkipsCIDescription()
{
var env = FakeCIEnv(prNumber: "42", title: "Fix", description: "Extracted release note");
var service = CreateServiceWithEnv(env);
var input = DefaultInput() with { ExtractReleaseNotes = false };

var result = service.EnrichFromCI(input);

result.Description.Should().BeNull();
}

[Fact]
public void EnrichFromCI_InCI_ExtractionDisabled_ExplicitDescription_CLIWins()
{
var env = FakeCIEnv(prNumber: "42", title: "Fix", description: "CI description");
var service = CreateServiceWithEnv(env);
var input = DefaultInput() with { ExtractReleaseNotes = false, Description = "My explicit description" };

var result = service.EnrichFromCI(input);

result.Description.Should().Be("My explicit description");
}

[Fact]
public void EnrichFromCI_InCI_ExtractionNull_UsesCIDescription()
{
var env = FakeCIEnv(prNumber: "42", title: "Fix", description: "Extracted release note");
var service = CreateServiceWithEnv(env);
var input = DefaultInput() with { ExtractReleaseNotes = null };

var result = service.EnrichFromCI(input);

result.Description.Should().Be("Extracted release note");
}

[Fact]
public void EnrichFromCI_InCI_ExtractionEnabled_UsesCIDescription()
{
var env = FakeCIEnv(prNumber: "42", title: "Fix", description: "Extracted release note");
var service = CreateServiceWithEnv(env);
var input = DefaultInput() with { ExtractReleaseNotes = true };

var result = service.EnrichFromCI(input);

result.Description.Should().Be("Extracted release note");
}
}
Loading