elastic / elastic/docs-builder
[Feature Request]: Improve handling of optional version in profile-based changelog bundles
- Dominant language
- C#
- Stars
- 24
- Forks
- 44
- Avg merge
- 1d 7h
- Merged PRs (30d)
- 146
Description
### Prerequisites
- [x] I have searched existing issues to ensure this feature hasn't already been requested
- [x] I have tested using the latest version of docs-builder
### What problem are you trying to solve?
It is currently unclear how/if you can do the equivalent of a `docs-builder changelog bundle --all` command using repeatable profiles in the changelog configuration file. The Elastic Agent and Elasticsearch teams currently use the equivalent of the --all option in their tooling, so it seems likely some team will want to use that in a repeatable way in the new tooling.
I figured out that it's currently possible, but involves a throwaway (unused) command argument, which is unintuitive and unnecessary.
### Proposed Solution
- Short-term: explain how to accomplish this in the docs
- Long-term: Add an explicit `requires_version: false` field to bundle profiles that allows omitting the second positional argument when it serves no functional purpose (no placeholders, not github_release). Works for both product-pattern and report-based profiles.
The following AI-generated plan includes information about both of those steps (though I think it's proposing too many stop-gap doc updates, so that will require refinement).
# Explicit optional second argument for changelog profiles
## Approach: Explicit Configuration Field
Add a new optional boolean field `requires_version` to `[BundleProfile](src/Elastic.Documentation.Configuration/Changelog/BundleConfiguration.cs)` that defaults to `true`. When set to `false`, the profile allows omitting the second positional argument when it serves no functional purpose.
## Benefits of Explicit Approach
- **Clear intent**: Profile configuration shows exactly which profiles support optional arguments
- **Simple implementation**: Just check a boolean field instead of complex placeholder detection
- **Better error messages**: Can reference the specific profile's configuration
- **Easy testing**: Straightforward test cases with known configurations
- **No magic**: Behavior is explicit and predictable
- **Future-proof**: Works for both product-pattern and report-based profiles
## Supported Profile Types
### Product-Pattern Profiles (Current Use Case)
```yaml
serverless-release-no-version:
products: "* * *" # Filter source
output: "elasticsearch.yaml" # No {version} placeholder
output_products: "elasticsearch" # No {version} placeholder
requires_version: false # Second argument optional
```
### Report-Based Profiles (Future Use Case)
```yaml
security-fixes-profile:
# No products field - data comes from report argument
output: "security-fixes.yaml" # No {version} placeholder
output_products: "elasticsearch" # No {version} placeholder
requires_version: false # Second argument still needed, but version unused
```
Usage: `bundle security-fixes-profile ./security-report.html`
## Implementation Plan
### 1. Immediate Documentation (Helps Users Now)
**First priority**: Document the current workaround so users don't need to wait for the code changes.
### 2. Profile Configuration Schema
**File**: `[src/Elastic.Documentation.Configuration/Changelog/BundleConfiguration.cs](src/Elastic.Documentation.Configuration/Changelog/BundleConfiguration.cs)`
Add to `BundleProfile`:
```csharp
///
/// Whether this profile requires a version number or promotion report URL as the second argument.
/// Defaults to true. Set to false for profiles that don't use {version} or {lifecycle} placeholders
/// and are not github_release profiles. Works with both product-pattern and report-based profiles.
///
public bool RequiresVersion { get; init; } = true;
```
### 3. Resolver Logic Update
**File**: `[src/services/Elastic.Changelog/Bundling/ProfileFilterResolver.cs](src/services/Elastic.Changelog/Bundling/ProfileFilterResolver.cs)`
Replace the blanket validation (lines 93-97):
```csharp
if (string.IsNullOrWhiteSpace(profileArgument))
{
if (profile.RequiresVersion)
{
var reasons = new List();
if (string.Equals(profile.Source, "github_release", StringComparison.OrdinalIgnoreCase))
reasons.Add("uses 'source: github_release'");
if (!string.IsNullOrWhiteSpace(profile.Products) && (profile.Products.Contains("{version}") || profile.Products.Contains("{lifecycle}")))
reasons.Add("has {version} or {lifecycle} in 'products'");
if (!string.IsNullOrWhiteSpace(profile.Output) && (profile.Output.Contains("{version}") || profile.Output.Contains("{lifecycle}")))
reasons.Add("has {version} or {lifecycle} in 'output'");
if (!string.IsNullOrWhiteSpace(profile.OutputProducts) && (profile.OutputProducts.Contains("{version}") || profile.OutputProducts.Contains("{lifecycle}")))
reasons.Add("has {version} or {lifecycle} in 'output_products'");
var reasonText = reasons.Count > 0 ? $" (because it {string.Join(" and ", reasons)})" : "";
collector.EmitError(string.Empty, $"Profile '{profileName}' requires a version number or promotion report URL as the second argument{reasonText}");
return null;
}
// Profile allows optional version - use "unknown" as sentinel
profileArgument = "unknown";
}
```
### 4. CLI Validation Update
**File**: `[src/tooling/docs-builder/Commands/ChangelogCommand.cs](src/tooling/docs-builder/Commands/ChangelogCommand.cs)`
Update both Bundle and Remove commands (around lines 915-923) to check the profile configuration:
```csharp
// Load profile to check if version is required
if (_configLoader != null)
{
var config = await _configLoader.LoadChangelogConfigurationForProfileMode(collector, ctx);
if (config?.Bundle?.Profiles?.TryGetValue(profile, out var profileConfig) == true)
{
if (profileConfig.RequiresVersion && string.IsNullOrWhiteSpace(profileArg))
{
// Provide contextual error message based on profile configuration
var reasons = BuildRequiresVersionReasons(profileConfig);
var reasonText = reasons.Any() ? $" (because it {string.Join(" and ", reasons)})" : "";
collector.EmitError(string.Empty, $"Profile '{profile}' requires a version number or promotion report URL as the second argument{reasonText}");
return 1;
}
}
}
```
Add helper method:
```csharp
private static List BuildRequiresVersionReasons(BundleProfile profile)
{
var reasons = new List();
if (string.Equals(profile.Source, "github_release", StringComparison.OrdinalIgnoreCase))
reasons.Add("uses 'source: github_release'");
// ... similar checks for placeholders
return reasons;
}
```
### 4. Configuration Examples
**Product-pattern profile (user's current case):**
```yaml
serverless-release-no-version:
products: "* * *"
output: "elasticsearch.yaml"
output_products: "elasticsearch"
requires_version: false # Allows: bundle serverless-release-no-version
```
**Report-based profile (future case):**
```yaml
security-fixes-static:
# No products - data from report
output: "security-fixes.yaml"
output_products: "elasticsearch"
requires_version: false # Allows: bundle security-fixes-static ./report.html
```
**Invalid configurations:**
```yaml
# ERROR: Has placeholders but claims no version needed
bad-profile:
products: "elasticsearch {version} ga"
output: "release-{version}.yaml"
requires_version: false # ← Configuration error
# ERROR: GitHub release profiles always need tag
github-profile:
source: github_release
requires_version: false # ← Configuration error
```
### 6. Testing Strategy
**File**: `[tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs](tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs)`
Add test cases:
**Product-pattern profiles:**
- Profile with `requires_version: false` + `products: "* * *"` succeeds without second arg
- Same profile still works with second arg (backward compatibility)
**Report-based profiles:**
- Profile with `requires_version: false` + no products + report arg works (version="unknown")
- Profile with `requires_version: false` + no products + no args fails (needs report source)
**Error cases:**
- Profile with `requires_version: true` (default) still requires second arg
- Profile with placeholders but `requires_version: false` gets validation error
- Profile with `source: github_release` + `requires_version: false` gets validation error
**Error messages:**
- Contextual error messages explaining why version is required (placeholders, github_release, etc.)
### 7. Documentation Updates
#### 7.1 Immediate Documentation (Current Workaround)
**File**: `[docs/cli/release/changelog-bundle.md](docs/cli/release/changelog-bundle.md)`
Add note to Profile-based examples section about profiles that don't need version:
```markdown
### Profile workaround for static patterns
For profiles that use static patterns without `{version}` or `{lifecycle}` placeholders,
the second argument is still required but serves no functional purpose.
Pass any placeholder value:
```sh
# Profile with static patterns - second argument unused but required
docs-builder changelog bundle serverless-release-no-version '*'
docs-builder changelog bundle serverless-release-no-version 'unused'
docs-builder changelog bundle serverless-release-no-version 'none'
```
This limitation will be addressed in a future release with the `requires_version` profile field.
```
**File**: `[docs/contribute/changelog.md](docs/contribute/changelog.md)`
Add to profile examples section:
```markdown
### Static pattern profiles
When your profile uses only static patterns (no `{version}` or `{lifecycle}` placeholders),
the second argument is currently required but unused:
```yaml
bundle:
profiles:
static-release:
products: "* * *"
output: "static-bundle.yaml" # No {version}
output_products: "elasticsearch" # No {version}
```
```sh
# Second argument required but ignored - pass any value
docs-builder changelog bundle static-release unused
```
```
#### 7.2 Updated Documentation (After Implementation)
**File**: `[docs/cli/release/changelog-bundle.md](docs/cli/release/changelog-bundle.md)`
Update argument documentation:
```markdown
`[1] `
: Version number, promotion report URL/path, or URL list file.
: Required unless the profile has `requires_version: false`.
: For example, `9.2.0`, `https://buildkite.../promotion-report.html`, or `/path/to/prs.txt`.
```
Replace workaround section with:
```markdown
### Optional second argument
Profiles can opt out of requiring the second argument by setting `requires_version: false`:
```yaml
bundle:
profiles:
serverless-static:
products: "* * *"
output: "elasticsearch.yaml"
requires_version: false # Second argument optional
```
```sh
# No second argument needed
docs-builder changelog bundle serverless-static
# Second argument still accepted (backward compatibility)
docs-builder changelog bundle serverless-static 2026-02
```
This works when the profile:
- Has no `{version}` or `{lifecycle}` placeholders in `products`, `output`, or `output_products`
- Is not a `source: github_release` profile
- Doesn't rely on the third positional argument flow
```
**File**: `[docs/contribute/changelog.md](docs/contribute/changelog.md)`
Add profile field documentation:
```markdown
`requires_version`
: Optional. Whether this profile requires the second positional argument. Defaults to `true`.
: Set to `false` for profiles that don't use `{version}` or `{lifecycle}` placeholders and are not `github_release` profiles.
:
: **Product-pattern profiles**: Second argument omitted entirely
:
```yaml
: static-product-profile:
: products: "* * *"
: output: "bundle.yaml"
: requires_version: false
:
```
: Usage: `changelog bundle static-product-profile`
:
: **Report-based profiles**: Second argument provides data but version unused
:
```yaml
: static-report-profile:
: output: "fixes.yaml"
: output_products: "elasticsearch"
: requires_version: false
:
```
: Usage: `changelog bundle static-report-profile ./report.html`
```
Replace static pattern examples with updated versions using `requires_version: false`.
## Validation Rules
The core rule: `requires_version: false` is valid when the version string serves no functional purpose.
**Always invalid:**
- `source: github_release` profiles (need release tag as second argument)
- Third positional argument scenarios (need explicit version as second argument)
**Invalid due to placeholders:**
- `{version}` or `{lifecycle}` in `products`, `output`, or `output_products` fields
- Profile relies on version for placeholder substitution
**Valid scenarios:**
- Product-pattern profiles with static output patterns
- Report-based profiles with static output patterns (version set to "unknown")
- Any profile where version string is genuinely unused
**Configuration validation:**
- Error if `requires_version: false` + `source: github_release`
- Error if `requires_version: false` + placeholders present
- Warning if `requires_version: false` + empty `products` (likely wants report-based usage)
## Backward Compatibility
- Existing profiles continue working (default `requires_version: true`)
- Existing scripts passing second arguments continue working
- Only new behavior is allowing omission when explicitly configured
## Edge Cases Handled
- Third positional argument still requires second argument (version + report flow)
- Profile with `{version}` placeholders but `requires_version: false` gets clear validation error
- Report-based profiles still accept report as second argument (version just unused)
## Architectural Notes & Future Improvements
### Current Overloaded Design
The second positional argument serves multiple purposes:
- Version string for `{version}` placeholder substitution
- Promotion report URL/path for PR extraction
- Local report file for PR extraction
- URL list file for PR/issue extraction
This creates fragility in argument interpretation and detection logic.
### Future Static Data Source Fields (Deferred)
Consider adding explicit profile fields for static data sources:
```yaml
# Future: Static report URL
security-fixes-profile:
report_url: "https://internal-server/security-report.html"
output: "security-fixes.yaml"
# No second argument needed at all
# Future: Static PR list file
critical-fixes-profile:
url_list_file: "/path/to/critical-prs.txt"
output: "critical-fixes.yaml"
# No second argument needed at all
```
This would eliminate the overloaded argument entirely for static scenarios, but is beyond the scope of this change.
### Examples and Research
_No response_
### Alternative Solutions
Possible alternatives considered when generating the AI plan:
#### 1. Document the Workaround (Current + Documentation)
Pros:
- Zero code changes - no risk of bugs
- Works immediately with existing profiles
- Simple to understand and explain
- No impact on existing automation/scripts
Cons:
- Feels inelegant - you still have to pass unused or similar
- Doesn't solve the core UX issue you raised
Implementation: Add a note to docs that for profiles without placeholders, any value works.
#### 2. Special Sentinel Values (e.g., none, --, skip)
Pros:
- Minimal code changes - just detect sentinel in resolver
- Explicit and discoverable (shows intent in command)
- No breaking changes to existing profiles
- Clear in scripts: bundle my-profile none vs bundle my-profile unused
Cons:
- Still requires typing something
- Adds a new concept to learn
- Sentinel values could conflict with real version names
Implementation: Modify DetectArgumentType to recognize sentinel values, treat them like missing args.
#### 3. Explicit Profile Configuration (requires_version: false)
Pros:
- Very clear intent - no guessing about behavior
- Simple implementation - just check the boolean field
- Easy to test and maintain
- Great error messages possible
Cons:
- Requires updating profile configurations
- Adds another field to the profile schema
- Not backward compatible with existing profiles
Implementation:
```yaml
serverless-release-no-version:
products: "* * *"
output: "elasticsearch.yaml"
requires_version: false # New field
```
#### 4. Full Implicit Detection
Pros:
- Works automatically with existing profiles
- No configuration changes needed
- Most "magic" - just works when possible
Cons:
- Complex placeholder detection logic
- Hard to predict behavior without deep knowledge
- More test cases and edge cases
- "Surprising" behavior for users
Implementation: Add RequiresSecondPositionalArgument() logic.
#### 5. Different Command/Flag Approach
Pros:
- Could be very clean for static profiles
- Separates concerns clearly
- No impact on existing profile-based commands
Cons:
- Breaks the existing mental model
- More commands to learn
- Might feel overengineered for a rare case
Examples:
```sh
changelog bundle-static my-profile
changelog bundle my-profile --no-version
changelog bundle my-profile --static
```
### Additional Context
_No response_
### How important is this feature to you?
Nice to have
Contributor guide
Assessment
This issue has not been assessed yet.