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
3 changes: 3 additions & 0 deletions config/changelog.example.yml
Original file line number Diff line number Diff line change
Expand Up @@ -294,6 +294,9 @@ bundle:
# output_products: "cloud-serverless {version}" # omit lifecycle
# # output_products: "cloud-serverless {version} {lifecycle}" # derive (ga for ISO dates)
# # output_products: "cloud-serverless {version} preview" # hardcode for exceptional releases
# # Optional: replace bundle.output_directory for this profile (same as --output as a directory).
# # The conventional {repo}-{product}-{version}.yaml name is written in this folder.
# # output_directory: docs/releases/cloud-serverless
# # Optional: profile-specific GitHub repo name (overrides bundle.repo if set).
# # Only needed when this profile's product ID differs from the repository name.
# repo: elasticsearch
Expand Down
2 changes: 1 addition & 1 deletion docs/cli-schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -3246,7 +3246,7 @@
"name": "output",
"type": "string",
"required": false,
"summary": "Output path for the bundled changelog. A .yml/.yaml file is used as-is. A directory, or omitting this option, writes {repo}-{product}-{version}.yaml (from --repo / bundle.repo / git origin, --output-products then --input-products, then --release-version). Warns and writes changelog-bundle.yaml if product or version cannot be resolved, or {product}-{version}.yaml if no repo resolves. Not supported in profile-based commands (same convention from the profile)."
"summary": "Output path for the bundled changelog. A .yml/.yaml file is used as-is. A directory, or omitting this option, writes {repo}-{product}-{version}.yaml (from --repo / bundle.repo / git origin, --output-products then --input-products, then --release-version). Warns and writes changelog-bundle.yaml if product or version cannot be resolved, or {product}-{version}.yaml if no repo resolves. Not supported in profile-based commands; the equivalent configuration option is bundle.profiles.\u003Cname\u003E.output_directory (directory only; same convention for the file name)."
},
{
"role": "flag",
Expand Down
3 changes: 2 additions & 1 deletion docs/cli/changelog/cmd-bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,10 @@ bundle:
output_products: "elasticsearch {version}"
serverless-release:
output_products: "cloud-serverless {version}"
output_directory: docs/releases/cloud-serverless
```

The bundle's file name is derived by convention as `{repo}-{product}-{version}.yaml` from the authoring repository (`--repo`, then the profile's `repo`, then `bundle.repo`, then the git `origin`), the profile's primary output product, and the version argument. For example, `docs-builder changelog bundle serverless-release 2026-08-31` writes `docs/releases/elasticsearch-cloud-serverless-2026-08-31.yaml`. If no repository can be resolved, the command warns and falls back to `{product}-{version}.yaml`. Setting an explicit `output` pattern on a profile is a hard error, and no two profiles may share a primary output product — they would collide on the same conventional target.
The bundle's file name is derived by convention as `{repo}-{product}-{version}.yaml` from the authoring repository (`--repo`, then the profile's `repo`, then `bundle.repo`, then the git `origin`), the profile's primary output product, and the version argument. For example, `docs-builder changelog bundle elasticsearch-release 9.2.0` writes `docs/releases/elasticsearch-elasticsearch-9.2.0.yaml`. A profile `output_directory` replaces `bundle.output_directory` for that profile (the same as option-mode `--output` when it is a directory): `docs-builder changelog bundle serverless-release 2026-08-31` writes `docs/releases/cloud-serverless/elasticsearch-cloud-serverless-2026-08-31.yaml`. If no repository can be resolved, the command warns and falls back to `{product}-{version}.yaml`. Setting an explicit `output` pattern on a profile is a hard error, and no two profiles may share a primary output product — they would collide on the same conventional target.

## Option-based mode

Expand Down
8 changes: 5 additions & 3 deletions docs/cli/changelog/cmd-upload.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ Use `--artifact-type` to choose what to upload:

| Value | Uploads | Default directory |
| ----- | ------- | ----------------- |
| `bundle` | Consolidated bundle YAML files | `bundle.output_directory` from `changelog.yml`, or `docs/releases` |
| `bundle` | Consolidated bundle YAML files | `bundle.output_directory` from `changelog.yml`, each profile's `output_directory` when set, or `docs/releases` |
| `changelog` | Individual changelog entry YAML files | `bundle.directory` from `changelog.yml`, or `docs/changelog` |

Keying differs by artifact type:
Expand Down Expand Up @@ -126,10 +126,12 @@ If it's necessary to re-trigger downstream scrubbers without changing file conte

Directory resolution order:

1. `--directory` — explicit override for this run
2. `changelog.yml` — `bundle.output_directory` (bundles) or `bundle.directory` (changelog entries)
1. `--directory` — explicit override for this run (that folder only)
2. `changelog.yml` — for bundles, `bundle.output_directory` plus each profile `output_directory`; for changelog entries, `bundle.directory`
3. Built-in default — `docs/releases` (bundles) or `docs/changelog` (changelog entries)

Each bundle directory is scanned non-recursively. A profile that writes under `docs/releases/cloud-serverless` is included because that path is listed as the profile's `output_directory`, not because the global folder is walked.

Use `--config` to point at a `changelog.yml` file other than `docs/changelog.yml`.

## Examples
Expand Down
6 changes: 4 additions & 2 deletions docs/data/release-notes/bundle.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,13 +78,15 @@ bundle:
profiles:
serverless-report:
output_products: "cloud-serverless {version}" <3>
output_directory: docs/releases/cloud-serverless <4>
elasticsearch-release:
output_products: "elasticsearch {version} {lifecycle}"
```

1. The directory that contains changelog files.
2. The directory that contains changelog bundles.
3. The bundle's product metadata, which affects the rules that are applied and the product and version titles that ultimately appear in the documentation. If omitted, it's derived from all the changelogs in the bundle. The authoring repo (`bundle.repo` here) and the first product also determine the bundle's file name, which is derived by convention as `{repo}-{product}-{version}.yaml` under `output_directory`.
2. The default directory that contains changelog bundles. Profiles that omit `output_directory` write here.
3. The bundle's product metadata, which affects the rules that are applied and the product and version titles that ultimately appear in the documentation. If omitted, it's derived from all the changelogs in the bundle. The authoring repo (`bundle.repo` here) and the first product also determine the bundle's file name, which is derived by convention as `{repo}-{product}-{version}.yaml`.
4. Optional. Replaces `bundle.output_directory` for this profile (the same as option-mode `--output` when it is a directory). This profile writes `docs/releases/cloud-serverless/elasticsearch-cloud-serverless-{version}.yaml` so a `{changelog}` directive can point at that folder without mixing other products.

### Bundle by GitHub releases [profile-gh-release]

Expand Down
8 changes: 6 additions & 2 deletions docs/data/release-notes/configure-ref.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ These settings are relevant to one or all of the `changelog bundle`, `changelog
| `bundle.branch` | Branch whose CDN changelog pool (`changelog/{org}/{repo}/{branch}/...`) entries are sourced from when bundling (default: `main`). Refer to [Entry sourcing](#bundle-entry-sourcing). |
| `bundle.directory` | Input directory containing changelog YAML files (default: `docs/changelog`). |
| `bundle.link_allow_repos` | List of `owner/repo` pairs whose PR/issue links are preserved. When set (including empty `[]`), links to unlisted repos become `# PRIVATE:` sentinels. |
| `bundle.output_directory` | Output directory for bundled files (default: `docs/releases`). Conventional `{repo}-{product}-{version}.yaml` names are written here in profile mode and in option mode when `--output` is omitted. Passing `--output` as a directory writes that same file name in the directory you specify instead. |
| `bundle.output_directory` | Output directory for bundled files (default: `docs/releases`). Conventional `{repo}-{product}-{version}.yaml` names are written here in profile mode (unless the profile sets `output_directory`) and in option mode when `--output` is omitted. Passing `--output` as a directory, or setting a profile `output_directory`, writes that same file name in the directory you specify instead. |
| `bundle.owner` | Default GitHub repository owner (for example, `elastic`). Also the org segment of uploaded changelog-entry keys (`changelog/{org}/{repo}/{branch}/...`) and CDN entry sourcing. |
| `bundle.release_dates` | When `true`, bundles include a `release-date` field (default: true). |
| `bundle.repo` | Default GitHub repository name (for example, `elasticsearch`). Used by the `{changelog}` directive to generate correct PR and issue links, to scope uploaded changelog-entry keys (`changelog/{org}/{repo}/{branch}/...`) and CDN entry sourcing, and as the `{repo}` segment of bundle file names (`{repo}-{product}-{version}.yaml`). Only needed when the product ID doesn't match the GitHub repository name (or to override the git remote). |
Expand Down Expand Up @@ -125,9 +125,13 @@ These settings are located in the `bundle.profiles.<name>` section of the config

`output`
: Removed. Bundle output names are derived by convention as `{repo}-{product}-{version}.yaml` from the authoring repository (`--repo`, then the profile's `repo`, then `bundle.repo`, then the git `origin`), the profile's primary output product (the first product in `output_products`, or `products`), and the version argument. If no repository can be resolved, the command warns and falls back to `{product}-{version}.yaml`. Setting `output` on any profile is a hard error at bundle time; remove the field.
: When no primary product or version resolves (for example, a promotion-report invocation without a version argument), the output path falls back in order to: `bundle.output_directory/changelog-bundle.yaml` (if `bundle.output_directory` is configured), then `changelog-bundle.yaml` in the input directory.
: When no primary product or version resolves (for example, a promotion-report invocation without a version argument), the output path falls back in order to: the profile's [`output_directory`](#bundle-profiles) (if set), then `bundle.output_directory/changelog-bundle.yaml` (if `bundle.output_directory` is configured), then `changelog-bundle.yaml` in the input directory.
: No two profiles in the same configuration may share a primary output product — they would resolve to the same `{repo}-{product}-{version}.yaml` target for any given version, which is also a hard error.

`output_directory`
: Directory for this profile's bundle file. Replaces [`bundle.output_directory`](#bundle-basic) for that profile the same way option-mode `--output` does when it is a directory: the conventional `{repo}-{product}-{version}.yaml` name is written in the folder you specify (for example `docs/releases/cloud-serverless`). The path is used as written (repo-relative); it is not joined as a child of the global `bundle.output_directory`.
: A `.yml` or `.yaml` file path is a hard error — that is what `output` used to allow. Omit this setting to keep writing under the global `bundle.output_directory`.

`output_products`
: The bundle's `products` metadata, which affects the bundle rules that are applied and the product and version titles that ultimately appear in documentation.
: Supports `{version}` and `{lifecycle}` placeholders.
Expand Down
2 changes: 1 addition & 1 deletion docs/syntax/changelog.md
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ docs/
└── release-notes.md # Page with :::{changelog}
```

The `bundle.directory` and `bundle.output_directory` settings in `changelog.yml` apply to the `changelog bundle` and `changelog gh-release` CLI commands. The directive's bundles folder is controlled by its first argument or defaults to `changelog/bundles/` relative to the docset root.
The `bundle.directory` and `bundle.output_directory` settings in `changelog.yml` apply to the `changelog bundle` and `changelog gh-release` CLI commands. A profile `output_directory` replaces the global bundle output folder for that profile only. The directive's bundles folder is independent: it is the first argument, or `changelog/bundles/` relative to the docset root, and it is **not** recursive. Point the directive at the same folder the profile writes to (for example `docs/releases/cloud-serverless`) so the page only loads that product's bundles.

## Version ordering

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,15 @@ public record BundleProfile
[Obsolete("No longer supported: bundle output names are derived by convention as '{repo}-{product}-{version}.yaml' from the authoring repo and the profile's output_products. Setting 'output' is a hard error at bundle time.")]
public string? Output { get; init; }

/// <summary>
/// Profile-specific output directory. Replaces <see cref="BundleConfiguration.OutputDirectory"/>
/// for this profile the same way option-mode <c>--output</c> as a directory replaces it. The
/// conventional <c>{repo}-{product}-{version}.yaml</c> name is joined onto this path. A
/// <c>.yml</c>/<c>.yaml</c> value is a hard error (use of free-form filenames is what
/// <see cref="Output"/> used to allow).
/// </summary>
public string? OutputDirectory { get; init; }

/// <summary>
/// Output products pattern. When set, overrides the products array derived from matched changelogs.
/// Supports {version} and {lifecycle} placeholders.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,7 @@ BundleConfigurationYaml yaml
#pragma warning disable CS0618 // Output stays parseable for one release cycle so ValidateProfileOutputs can emit an actionable error
Output = kvp.Value.Output,
#pragma warning restore CS0618
OutputDirectory = kvp.Value.OutputDirectory,
OutputProducts = kvp.Value.OutputProducts,
Description = kvp.Value.Description,
Repo = kvp.Value.Repo,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@ internal sealed record BundleProfileYaml
/// </summary>
public string? Output { get; set; }

/// <summary>
/// Profile-specific output directory. Replaces <c>bundle.output_directory</c> for this profile.
/// </summary>
public string? OutputDirectory { get; set; }

/// <summary>
/// Output products pattern. Overrides the products array derived from matched changelogs.
/// Supports {version} and {lifecycle} placeholders.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -697,7 +697,7 @@ Cancel ctx
input.Config
)
);
outputPath = JoinProfileOutputPath(config.Bundle.OutputDirectory, input.OutputDirectory, config.Bundle.Directory, fileName);
outputPath = JoinProfileOutputPath(config, input, fileName);
}

// Parse output_products pattern with version/lifecycle substitution
Expand Down Expand Up @@ -1153,7 +1153,7 @@ Cancel ctx
_fileSystem,
new BundleOutputNameRequest(primaryProduct, planVersion, input.Repo, profileDef.Repo, config?.Bundle?.Repo, input.Config)
);
outputPath = JoinProfileOutputPath(config?.Bundle?.OutputDirectory, input.OutputDirectory, config?.Bundle?.Directory, fileName);
outputPath = JoinProfileOutputPath(config, input, fileName);
}
else
outputPath = ResolveResolvedOutputPath(collector, input, config);
Expand Down Expand Up @@ -1229,7 +1229,7 @@ private string ResolveResolvedOutputPath(

var outputDir = !string.IsNullOrWhiteSpace(input.Output)
? input.Output
: config?.Bundle?.OutputDirectory
: ResolveConfiguredOutputDirectory(config, input)
?? input.OutputDirectory
?? input.Directory
?? config?.Bundle?.Directory
Expand All @@ -1249,20 +1249,34 @@ private string ResolveResolvedOutputPath(
}

/// <summary>
/// Resolution order: bundle.output_directory → input.OutputDirectory (programmatic override)
/// bundle.directory CWD.
/// Profile <c>output_directory</c> replaces <c>bundle.output_directory</c> (same as option-mode
/// <c>--output</c> as a directory). Then <c>input.OutputDirectory</c>, <c>bundle.directory</c>, CWD.
/// </summary>
private string JoinProfileOutputPath(
string? configOutputDirectory,
string? inputOutputDirectory,
string? configDirectory,
string fileName
)
private string JoinProfileOutputPath(ChangelogConfiguration? config, BundleChangelogsArguments input, string fileName)
{
var outputDir = configOutputDirectory ?? inputOutputDirectory ?? configDirectory ?? _fileSystem.Directory.GetCurrentDirectory();
var outputDir = ResolveConfiguredOutputDirectory(config, input)
?? input.OutputDirectory
?? config?.Bundle?.Directory
?? _fileSystem.Directory.GetCurrentDirectory();
return _fileSystem.Path.Join(outputDir, fileName).OptionalWindowsReplace();
}

/// <summary>
/// Profile <c>output_directory</c> when the invoked profile sets it; otherwise
/// <c>bundle.output_directory</c>.
/// </summary>
private static string? ResolveConfiguredOutputDirectory(ChangelogConfiguration? config, BundleChangelogsArguments input)
{
if (
!string.IsNullOrWhiteSpace(input.Profile)
&& config?.Bundle?.Profiles?.TryGetValue(input.Profile, out var profile) == true
&& !string.IsNullOrWhiteSpace(profile.OutputDirectory)
)
return profile.OutputDirectory;

return config?.Bundle?.OutputDirectory;
}

/// <summary>The first concrete product id from a profile's <c>output_products</c>/<c>products</c> pattern.</summary>
private static string? ResolvePrimaryProductFromProfile(BundleProfile profileDef)
{
Expand Down Expand Up @@ -1290,15 +1304,27 @@ private static bool ValidateProfileOutputs(IDiagnosticsCollector collector, Chan
foreach (var (name, profile) in profiles)
{
#pragma warning disable CS0618 // intentionally reading the obsolete field to reject profiles that still set it
if (string.IsNullOrWhiteSpace(profile.Output))
continue;
if (!string.IsNullOrWhiteSpace(profile.Output))
#pragma warning restore CS0618
collector.EmitError(
string.Empty,
$"Profile '{name}': 'output' is no longer supported. Remove it — bundle output names are now derived by convention " +
$"as '{BundleOutputNaming.PrefixedConvention}' from the profile's output_products and authoring repo."
);
valid = false;
{
collector.EmitError(
string.Empty,
$"Profile '{name}': 'output' is no longer supported. Remove it — bundle output names are now derived by convention " +
$"as '{BundleOutputNaming.PrefixedConvention}' from the profile's output_products and authoring repo."
);
valid = false;
}

if (BundleOutputNaming.IsYamlFilePath(profile.OutputDirectory))
{
collector.EmitError(
string.Empty,
$"Profile '{name}': 'output_directory' must be a directory, not a file path. " +
$"The bundle file name is derived by convention as '{BundleOutputNaming.PrefixedConvention}'. " +
"To choose a folder, set output_directory the same way option-mode --output does when it is a directory."
);
valid = false;
}
}

var collisions = profiles
Expand Down
Loading
Loading