Skip to content

Changelog profile parity - phase 1 & 2#2791

Open
lcawl wants to merge 11 commits intomainfrom
profile-parity
Open

Changelog profile parity - phase 1 & 2#2791
lcawl wants to merge 11 commits intomainfrom
profile-parity

Conversation

@lcawl
Copy link
Contributor

@lcawl lcawl commented Feb 26, 2026

Summary

This pull request implements the first two phases of an effort to get the profile-based docs-builder changelog bundle and docs-builder changelog remove invocations to parity with the command-line-option method.

Design Principles

  1. Mutually exclusive invocation: When using a profile (changelog bundle <profile> <arg> or changelog remove <profile> <arg>), any filter- or output-related command options must be rejected with an error. Profile config provides all such values.
  2. Allow minimal CLI overrides: Only for remove: --dry-run and --force may be used with profiles. No other CLI options are allowed when using a profile.
  3. Path derivation from config: When using a profile, paths are derived from the changelog configuration file (discovered by default, e.g. docs/changelog.yml):
    • Input directory (bundle.directory): The directory containing changelog YAML files for both bundle and remove.
    • Bundles directory (bundle.output_directory): For remove, the directory scanned for bundle dependencies. No --config or --directory override — profile invocations rely on the config file.
  4. Core functionality parity: Both invocation styles must support the same core filtering and output behaviors; profiles achieve this via config fields.

Phase 1: Mutual Exclusivity and Bug Fix

1.1 Reject extra options when using profile mode

Files: ChangelogCommand.cs

For both Bundle and Remove:

  • When isProfileMode is true, after validating profileArg is present, add a single check that rejects any of the following if specified:
    • Bundle: --all, --input-products, --output-products, --prs, --issues, --output, --repo, --owner, --resolve, --no-resolve, --hide-features, --config, --directory
    • Remove: --all, --products, --prs, --issues, --repo, --owner, --config, --directory, --bundles-dir
  • Allow with profile: For remove only, --dry-run and --force. For bundle, no CLI options allowed.
  • Emit a clear error: e.g. "When using a profile, do not specify --&lt;option&gt;. Paths and filters come from the changelog configuration. Allowed options with profiles (remove only): --dry-run, --force."
  • Return exit code 1 and exit before invoking the service.

Outcome: This fixes the incidental bug (silent --output discard) and enforces mutual exclusivity.

1.2 Derive paths from config in profile mode

When using a profile, the changelog configuration file supplies all paths. Profile-based commands require the config file to exist; if it cannot be found, return an error.

Config discovery for profile mode:

  • Try, in order: ./changelog.yml, then ./docs/changelog.yml (relative to current working directory).
  • This ensures profile-based commands work when run from the folder containing the config, even if that folder does not follow the default convention (e.g. my-custom/path/changelog.yml — run from my-custom/path/ to find it).
  • No --config override in profile mode.

If config not found: Return an error (do not fall back to defaults). Error message should advise:

  • Create the configuration by running docs-builder changelog init, or
  • Re-run the command from the folder where changelog.yml exists (e.g. if the config is in docs/, run from the project root; if the config is in my-folder/, run from my-folder/).

Path derivation from config:

  • Input directory: Use bundle.directory from config. Ensure ChangelogBundlingService.ApplyConfigDefaults and ChangelogRemoveService.ApplyConfigDefaults use config when CLI did not supply --directory (which will always be the case in profile mode).
  • Bundles directory (remove only): Use bundle.output_directory from config for the dependency check. Ensure ChangelogRemoveService.ResolveBundlesDirectory uses config when BundlesDir was not supplied (always in profile mode).

Implementation note: ChangelogConfigurationLoader.LoadChangelogConfiguration currently returns ChangelogConfiguration.Default with a warning when the file is missing. For profile mode, the caller (or loader when invoked with profile context) must treat a missing config as a hard error and emit the advice above.

1.3 Bundle-specific validation

  • Move the profile-vs-options rejection so it runs before processedOutput is computed from --output, since we will now error on --output in profile mode.
  • Ensure that when profile is used, the service receives Output = null so it uses the profile's output pattern (no need to merge CLI output anymore).

Phase 2: Profile Config Enhancements (Changelog Bundle)

2.1 Add output_products to profile config

Files:

Config example:

profiles:
  kibana-release:
    products: "kibana {version}, security {version}, observability {version}"
    output_products: "kibana {version}"
    output: "kibana-{version}.yaml"

2.2 Add repo and owner to profile config

Files: Same YAML/domain/config-loader files as above.

  • Add Repo (string?) and Owner (string?) to BundleProfileYaml and BundleProfile.
  • In ProcessProfile, when the profile defines repo or owner, set them on the returned input.
  • If neither is set in the profile, the bundle's product entries will have no repo field — the same state that already exists without --repo in option-based mode. The existing render-time fallback (product ID used as repo) applies, which may produce broken links when product ID ≠ GitHub repo name. Users who need correct links must set repo in the profile config.

No automatic derivation: Unlike option-based mode where --repo is explicit, profile mode has no --repo flag. The products.yml repository field is not a reliable source for automatic derivation, because its absence is ambiguous — some products genuinely share their name with their GitHub repo (e.g. elasticsearch), while others do not but have no repository field recorded (e.g. cloud-serverless, whose GitHub repo is cloud). Attempting to derive repo from product.Repository ?? productId would silently produce broken links for the latter case.

For option-based mode: No change. --repo and --owner are already explicit; existing behavior is unchanged.

Future improvement (out of scope): Populating missing repository fields in products.yml for products where product ID ≠ GitHub repo name (such as cloud-serverlesscloud) would improve render-time link resolution for both modes. This is a separate cleanup task.

2.2.1 Implementation changes

NOTE: This plan changed over the course of the PR, since I decided to add a "default" bundle.repo and bundle.owner that can be optionally overridden at the profile level. These values can then serve as the default for the --repo and --owner in the non-profile scenario too.

The change affected:

  • ChangelogBundlingService.ApplyConfigDefaults — adds Repo = input.Repo ?? config.Bundle.Repo and Owner = input.Owner ?? config.Bundle.Owner
  • ChangelogRemoveService.ApplyConfigDefaults — same

Precedence for both services and both modes:

explicit --repo / --owner CLI flag
  → bundle.repo / bundle.owner in changelog.yml
    → nothing (renderer falls back to product ID at render time)

So for a repo like cloud where cloud-serverless is the product ID, you can now set it once in changelog.yml:

bundle:
  repo: cloud
  owner: elastic

...and never need --repo cloud --owner elastic on the command line again, whether you're using option-based or profile-based commands.


Phase 5: Documentation and Examples

This pull request also includes the following pieces of phase 5:

Tests

(7 new tests across 2 files):

BundleChangelogsTests.cs:

  • BundleChangelogs_WithProfile_OutputProducts_OverridesProductsArray — verifies that output_products in a profile replaces the products array in the bundle output (using wildcard lifecycle to match preview changelogs while advertising ga in the output)
  • BundleChangelogs_WithProfile_RepoAndOwner_WritesValuesToProductEntries — verifies that repo from a profile is written to the bundle product entries (owner is for normalization only, not persisted in the bundle)
  • BundleChangelogs_WithProfile_NoRepoOwner_PreservesExistingFallbackBehavior — verifies that a profile without repo/owner produces clean output with no repo: field
  • BundleChangelogs_WithProfileMode_MissingConfig_ReturnsErrorWithAdvice — verifies error with advice when no changelog.yml is found
  • BundleChangelogs_WithProfileMode_ConfigAtCurrentDir_LoadsSuccessfully — verifies auto-discovery of ./changelog.yml
  • BundleChangelogs_WithProfileMode_ConfigAtDocsSubdir_LoadsSuccessfully — verifies auto-discovery of ./docs/changelog.yml

ChangelogRemoveTests.cs:

  • Remove_WithProfileMode_MissingConfig_ReturnsErrorWithAdvice — verifies that the remove command also returns a clear error when no config is discoverable

Documentation

(docs/contribute/changelog.md):

  • Restructured the "Create bundles" section to introduce the mutual exclusivity concept upfront, separating option-based and profile-based usage into their own subsections
  • Added a full profile field reference table (products, output, output_products, repo, owner, hide_features) with example config
  • Updated the "Removal with profiles" section to document mutual exclusivity, --dry-run/--force exceptions, config auto-discovery, and which profile fields are ignored during removal

Steps to test

  1. Create a changelog configuration file and changelogs (e.g. use https://github.com/elastic/cloud/pull/150210)
  2. Add the new changelog configuration options to see if you can generate the same bundles using only the profiles. For example, instead of a command like this:
    docs-builder changelog bundle \
    --input-products "cloud-hosted 2025-11-* *" \
    --config ./docs/changelog.yml \
    --directory ./docs/changelog \
    --output ./docs/releases/cloud-hosted/cloud-2025-11.yaml \
    --resolve --repo cloud \
    --output-products "cloud-hosted 2025-11" 
    Update the changelog configuration file to contain profiles like this:
    bundle:
      directory: docs/changelog
      resolve: true
      repo: cloud
      owner: elastic
      profiles:
        cloud-hosted-monthly:
          products: "cloud-hosted {version}-* *"
          output: "./docs/releases/cloud-hosted/cloud-{version}.yaml"
          output_products: "cloud-hosted {version}"
    Then run profile-based commands:
    docs-builder changelog bundle cloud-hosted-monthly 2025-11
    docs-builder changelog bundle cloud-hosted-monthly 2025-12
    docs-builder changelog bundle cloud-hosted-monthly 2026-01
    docs-builder changelog bundle cloud-hosted-monthly 2026-02
    When I performed this test, the bundles produced via both methods were identical.
  3. Try adding extra command options to the profile-based command and confirm that it gets an error, for example:
    Error: When using a profile, the following options are not allowed: --output. All paths and filters are derived from the changelog configuration file.
    This error is returned for both the "bundle" and "remove" commands.
  4. Try putting the changelog configuration file in a non-default location and confirm that it gets an error:
    Error: changelog.yml not found. Profile-based commands require a changelog configuration file.
    Either run 'docs-builder changelog init' to create one, or re-run this command from the folder where changelog.yml exists (e.g. the project root if the file is at docs/changelog.yml).
    Unless you run the command from the same directory as the configuration file, which works as long as you have appropriately relative paths in the bundle.directory and bundle.output_directory settings.
  5. Verify that the "remove" command can still successfully find and delete changelog files. For example, this works:
    docs-builder changelog remove cloud-hosted-monthly 2026-02

Reviewer notes

I identified that this new output_products option doesn't currently work with the promotion report option. The AI analysis indicated that's better to address in phase 3, which already planned to make changes to the promotion report functionality (so that it worked as a command option too).

Generative AI disclosure

  1. Did you use a generative AI (GenAI) tool to assist in creating this contribution?
  • Yes
  • No
  1. If you answered "Yes" to the previous question, please specify the tool(s) and model(s) used (e.g., Google Gemini, OpenAI ChatGPT-4, etc.).

Tool(s) and model(s) used: composer-1.5, claude-4.6-sonnet-medium

…dd output_products, repo, owner profile fields

Phase 1:
- Bundle command: reject all filter/output options (--all, --input-products, --output-products, --prs, --issues, --output, --repo, --owner, --resolve, --no-resolve, --hide-features, --config, --directory) when a profile argument is given.
- Remove command: reject all options except --dry-run and --force when a profile argument is given.
- Profile-based commands now discover changelog.yml automatically (./changelog.yml then ./docs/changelog.yml) and return a helpful error if neither is found, rather than silently falling back to defaults. An explicit config path is still accepted when passed directly to the service layer (used by tests).
- ChangelogRemoveArguments.Directory changed from required to nullable; ApplyConfigDefaults follows the same null-coalescing pattern as the bundle service.
- Fixes a silent bug where --output was ignored in profile mode.

Phase 2:
- Add output_products, repo, and owner fields to BundleProfileYaml, BundleProfile, and the config loader mapping.
- ProcessProfile applies these fields when building the bundled output: output_products overrides the products array with version/lifecycle substitution; repo and owner are stored on each product entry for correct PR/issue link generation.
- MergeHideFeatures removed; profile mode now uses only the profile's hide_features (CLI --hide-features is rejected at the command layer).
- Update changelog.example.yml and docs to document all new profile fields and the mutual exclusivity requirement.

Made-with: Cursor
@github-actions
Copy link

github-actions bot commented Feb 26, 2026

- Add 7 new unit tests for bundle/remove profile features:
  - output_products overrides products array in bundle
  - repo from profile is written to bundle product entries
  - no repo/owner in profile preserves existing fallback behaviour
  - missing config in profile mode returns error with advice (bundle + remove)
  - changelog.yml discovered from CWD (./changelog.yml)
  - changelog.yml discovered from docs/ subdir (./docs/changelog.yml)
- Update docs/contribute/changelog.md:
  - Document mutual exclusivity of profile-based vs option-based bundle usage
  - Add new "Profile-based bundling" section with full field reference table
  - Document config auto-discovery behaviour for profile mode
  - Update "Removal with profiles" to document mutual exclusivity,
    allowed --dry-run/--force exceptions, and which profile fields are ignored

Made-with: Cursor
Adds repo and owner as top-level fields under bundle: in changelog.yml,
providing a default that applies to all profiles. Profile-level values
override the bundle-level default when set; otherwise the bundle-level
value is used. This avoids repeating the same repo/owner in every profile
when all profiles share the same repository.

Also adds two tests verifying the fallback and override behaviour.

Made-with: Cursor
Extends ApplyConfigDefaults in both ChangelogBundlingService and
ChangelogRemoveService to fall back to bundle.repo and bundle.owner
from config when --repo/--owner are not supplied on the CLI.

This mirrors the existing behaviour for profile-based commands and
means repo and owner rarely need to be specified on the command line
when a changelog.yml with bundle-level defaults is present.

Precedence: explicit CLI flag > bundle.repo/owner config > nothing
(renderer falls back to product ID at render time).

Adds four tests covering the config fallback and CLI-override paths
for the bundle command.

Made-with: Cursor
Updates all three places that describe --repo/--owner behaviour:

- CLI param XML docs (bundle and remove): clarify both are optional
  and fall back to bundle.repo/bundle.owner in changelog.yml
- changelog-bundle.md: expand --repo/--owner option descriptions;
  rewrite "Repository name in bundles" section to show the three-level
  precedence (CLI > profile > bundle config) with YAML examples;
  update profile fields table and the examples section to demonstrate
  bundle-level defaults with per-profile overrides
- changelog.md: add a note that --repo/--owner fall back to config
  in option-based bundling; expand profile fields table to include
  bundle-level repo/owner defaults; update the --prs and --issues
  callout text to mention the config fallback

Made-with: Cursor
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
}

// Profile-level repo/owner takes precedence; fall back to bundle-level defaults
repo = profile.Repo ?? config.Bundle?.Repo;

// Profile-level repo/owner takes precedence; fall back to bundle-level defaults
repo = profile.Repo ?? config.Bundle?.Repo;
owner = profile.Owner ?? config.Bundle?.Owner;
@lcawl lcawl marked this pull request as ready for review February 26, 2026 03:10
@lcawl lcawl requested review from a team as code owners February 26, 2026 03:10
@lcawl lcawl requested a review from reakaleek February 26, 2026 03:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant