elastic / elastic/docs-builder

[Feature Request]: Improve handling of optional version in profile-based changelog bundles

Open
#2,973 3 comments 0 reactions 0 assignees View on GitHub
ai-triaged ai:eng-question ai:writer-question enhancement needs triage stale
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

Open the contributing guide

Assessment

This issue has not been assessed yet.

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.