From 0611132950ae467b46cb9b310011d36ef92cd253 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Fri, 18 Sep 2026 13:25:11 +0200 Subject: [PATCH 1/4] changelog init: add --mode and --workflows flags for onboarding paths Extends `docs-builder changelog init` with two new flags: - `--mode `: selects the changelog.yml template and whether to include a release trigger. gh-release uses a new minimal template (bundle section only, no pivot or rules). changelog-auto and changelog-manual use the full template. Omitting --mode preserves the existing default behaviour. - `--workflows`: generates the three release-notes-*.yml GitHub Actions caller files under .github/workflows/. Skips any file that already exists. Emits a warning for gh-release and changelog-auto modes that bundle-on-release: true requires a non-GITHUB_TOKEN release publisher. Also warns when existing *.yaml files are found in the changelog directory (TopDirectoryOnly scanning means they will be uploaded on the first push to main). Closes #4151 Co-Authored-By: Claude Sonnet 4.6 --- config/changelog.example-gh-release.yml | 20 +++ .../docs-builder/Commands/ChangelogCommand.cs | 161 +++++++++++++++++- src/tooling/docs-builder/docs-builder.csproj | 1 + 3 files changed, 178 insertions(+), 4 deletions(-) create mode 100644 config/changelog.example-gh-release.yml diff --git a/config/changelog.example-gh-release.yml b/config/changelog.example-gh-release.yml new file mode 100644 index 0000000000..e13181bd35 --- /dev/null +++ b/config/changelog.example-gh-release.yml @@ -0,0 +1,20 @@ +# Changelog Configuration — GitHub-release-driven mode +# Bundles are built directly from a GitHub release's notes at publish time. +# No per-PR changelog entries are checked in. +# Run `docs-builder changelog init --mode gh-release` to re-generate this file. + +# Bundle configuration +bundle: + # Input directory (unused in gh-release mode, kept for compatibility) + directory: docs/changelog + # Output directory for bundled changelog files + output_directory: docs/releases + # changelog-init-bundle-seed + # PR/issue link allowlist: when set (including []), only links to these owner/repo pairs are kept + # in bundle output; others are rewritten to '# PRIVATE:' sentinels. + # Run `changelog init` in a GitHub clone to pre-fill owner, repo, and link_allow_repos, or set + # bundle.owner, bundle.repo, and link_allow_repos manually. Example: + # link_allow_repos: + # - elastic/my-repo + # repo: my-repo + # owner: elastic diff --git a/src/tooling/docs-builder/Commands/ChangelogCommand.cs b/src/tooling/docs-builder/Commands/ChangelogCommand.cs index 5c245c8a11..0739164946 100644 --- a/src/tooling/docs-builder/Commands/ChangelogCommand.cs +++ b/src/tooling/docs-builder/Commands/ChangelogCommand.cs @@ -64,13 +64,17 @@ IEnvironmentVariables environmentVariables /// Bundle output directory. Defaults to docs/releases. /// GitHub owner for seeding bundle defaults. Overrides the value inferred from git remote origin. /// GitHub repository name for seeding bundle defaults. Overrides the value inferred from git remote origin. + /// Onboarding mode: gh-release (bundle from GitHub release notes, no per-PR entries), changelog-auto (per-PR entries, automated bundle at release), or changelog-manual (per-PR entries, manual bundling). Defaults to changelog-manual behaviour when omitted. + /// When set, generates the three release-notes-*.yml GitHub Actions caller files under .github/workflows/. [NoOptionsInjection] public Task Init( [ExpandUserProfile, RejectSymbolicLinks] DirectoryInfo? path = null, [ExpandUserProfile, RejectSymbolicLinks] DirectoryInfo? changelogDir = null, [ExpandUserProfile, RejectSymbolicLinks] DirectoryInfo? bundlesDir = null, string? owner = null, - string? repo = null + string? repo = null, + string? mode = null, + bool workflows = false ) { var rootPath = path?.FullName ?? Path.GetFullPath("."); @@ -101,17 +105,25 @@ public Task Init( var useNonDefaultBundlesDir = bundlesDir != null; var repoRoot = Paths.FindGitRoot(docsFolder)?.FullName ?? docsFolder.FullName; + var isGhRelease = string.Equals(mode, "gh-release", StringComparison.OrdinalIgnoreCase); + var bundleOnRelease = isGhRelease || string.Equals(mode, "changelog-auto", StringComparison.OrdinalIgnoreCase); + // Create changelog.yml from example if it does not exist if (!_fileSystem.File.Exists(configPath)) { + var templateResourceName = isGhRelease + ? "Documentation.Builder.changelog.example-gh-release.yml" + : "Documentation.Builder.changelog.example.yml"; + var templateFileName = isGhRelease ? "changelog.example-gh-release.yml" : "changelog.example.yml"; + byte[]? templateBytes = null; - using (var stream = typeof(ChangelogCommands).Assembly.GetManifestResourceStream("Documentation.Builder.changelog.example.yml")) + using (var stream = typeof(ChangelogCommands).Assembly.GetManifestResourceStream(templateResourceName)) { if (stream == null) { // Fallback: try config relative to current directory (for development) var localConfigDir = _fileSystem.Path.Join(Directory.GetCurrentDirectory(), "config"); - var localConfigPath = _fileSystem.Path.Join(localConfigDir, "changelog.example.yml"); + var localConfigPath = _fileSystem.Path.Join(localConfigDir, templateFileName); if (_fileSystem.File.Exists(localConfigPath)) { templateBytes = _fileSystem.File.ReadAllBytes(localConfigPath); @@ -127,7 +139,7 @@ public Task Init( if (templateBytes == null || templateBytes.Length == 0) { - collector.EmitError(string.Empty, "Could not find changelog.example.yml template. Ensure docs-builder is built correctly."); + collector.EmitError(string.Empty, $"Could not find {templateFileName} template. Ensure docs-builder is built correctly."); return Task.FromResult(1); } @@ -217,9 +229,150 @@ public Task Init( } } + // Warn about existing fixture entries that could be accidentally uploaded + WarnAboutFixtureEntries(changelogPath, configPath); + + // Generate GitHub Actions workflow caller files when requested + if (workflows) + { + var configRelPath = GetPathForConfig(repoRoot, configPath).Trim('"'); + var workflowsDir = _fileSystem.Path.Join(repoRoot, ".github", "workflows"); + if (!_fileSystem.Directory.Exists(workflowsDir)) + { + try + { + _ = _fileSystem.Directory.CreateDirectory(workflowsDir); + } + catch (IOException ex) + { + collector.EmitError(string.Empty, $"Failed to create .github/workflows directory: {ex.Message}", ex); + return Task.FromResult(1); + } + } + + WriteWorkflowFile(workflowsDir, "release-notes.yml", BuildReleaseNotesWorkflow(configRelPath, bundleOnRelease)); + WriteWorkflowFile(workflowsDir, "release-notes-comments.yml", BuildReleaseNotesCommentsWorkflow()); + WriteWorkflowFile(workflowsDir, "release-notes-changelog-file.yml", BuildReleaseNotesChangelogFileWorkflow(configRelPath)); + + if (bundleOnRelease) + { + _logger.LogWarning( + "bundle-on-release: true only fires when the release is published by a human or a non-GITHUB_TOKEN. " + + "If your release workflow uses github.token, add the bundle jobs inline instead. " + + "See: https://elastic.github.io/docs-builder/data/release-notes/onboard/github-release-driven" + ); + } + } + return Task.FromResult(0); } + private void WarnAboutFixtureEntries(string changelogPath, string configPath) + { + if (!_fileSystem.Directory.Exists(changelogPath)) + return; + + var configFileName = _fileSystem.Path.GetFileName(configPath); + var existingEntries = _fileSystem + .Directory + .GetFiles(changelogPath, "*.yml") + .Concat(_fileSystem.Directory.GetFiles(changelogPath, "*.yaml")) + .Where(f => !string.Equals(_fileSystem.Path.GetFileName(f), configFileName, StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (existingEntries.Count > 0) + { + _logger.LogWarning( + "Found {Count} existing .yaml file(s) in {ChangelogPath}. " + + "Only files directly in this directory are uploaded (TopDirectoryOnly). " + + "Move demo/sample/fixture entries to {ChangelogPath}/examples/ before the first push to main, " + + "or they may be included in release bundles.", + existingEntries.Count, + changelogPath, + changelogPath + ); + } + } + + private void WriteWorkflowFile(string workflowsDir, string fileName, string content) + { + var filePath = _fileSystem.Path.Join(workflowsDir, fileName); + if (_fileSystem.File.Exists(filePath)) + { + _logger.LogWarning("Workflow file already exists, skipping: {FilePath}", filePath); + return; + } + + try + { + _fileSystem.File.WriteAllText(filePath, content); + _logger.LogInformation("Created workflow file: {FilePath}", filePath); + } + catch (IOException ex) + { + _logger.LogWarning("Failed to write workflow file '{FilePath}': {Message}", filePath, ex.Message); + } + } + + private static string BuildReleaseNotesWorkflow(string configRelPath, bool bundleOnRelease) + { + var releaseTrigger = bundleOnRelease ? " release:\n types: [published]\n" : string.Empty; + var bundleWith = bundleOnRelease ? " bundle-on-release: true\n" : string.Empty; + return "name: Release notes\n" + + "on:\n" + + " pull_request:\n" + + " types: [opened, synchronize, reopened, labeled, unlabeled]\n" + + " push:\n" + + " branches: [main]\n" + + releaseTrigger + + "permissions: {}\n" + + "jobs:\n" + + " release-notes:\n" + + " permissions:\n" + + " contents: read\n" + + " pull-requests: read\n" + + " packages: read\n" + + " id-token: write\n" + + " uses: elastic/docs-actions/.github/workflows/release-notes.yml@v1\n" + + " with:\n" + + $" config: {configRelPath}\n" + + bundleWith; + } + + private static string BuildReleaseNotesCommentsWorkflow() => + "name: Release notes comments\n" + + "on:\n" + + " workflow_run:\n" + + " workflows: [Release notes]\n" + + " types: [completed]\n" + + "permissions: {}\n" + + "jobs:\n" + + " comment:\n" + + " if: github.event.workflow_run.event == 'pull_request'\n" + + " permissions:\n" + + " pull-requests: write\n" + + " actions: read\n" + + " uses: elastic/docs-actions/.github/workflows/release-notes-comments.yml@v1\n"; + + private static string BuildReleaseNotesChangelogFileWorkflow(string configRelPath) => + "name: Release notes changelog file\n" + + "on:\n" + + " workflow_run:\n" + + " workflows: [Release notes]\n" + + " types: [completed]\n" + + "permissions: {}\n" + + "jobs:\n" + + " changelog-file:\n" + + " permissions:\n" + + " contents: write\n" + + " pull-requests: write\n" + + " id-token: write\n" + + " packages: read\n" + + " actions: write\n" + + " uses: elastic/docs-actions/.github/workflows/release-notes-changelog-file.yml@v1\n" + + " with:\n" + + $" config: {configRelPath}\n"; + /// Create a new changelog entry YAML file. /// Optional: Products affected in format "product [lifecycle], ..." (e.g., "cloud-serverless, kibana" or "elasticsearch ga"). Do not include a version or date; that is an error. Use 'changelog note' for version-listed items. If not specified, products are inferred from the repository or config defaults. /// Optional: What users must do to mitigate diff --git a/src/tooling/docs-builder/docs-builder.csproj b/src/tooling/docs-builder/docs-builder.csproj index 63cbb5997f..b15185780d 100644 --- a/src/tooling/docs-builder/docs-builder.csproj +++ b/src/tooling/docs-builder/docs-builder.csproj @@ -37,6 +37,7 @@ + From b98073a31d96ada240e5d578d9c09450031480ec Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Fri, 18 Sep 2026 13:31:24 +0200 Subject: [PATCH 2/4] Regenerate CLI schema after changelog init --mode/--workflows additions Co-Authored-By: Claude Sonnet 4.6 --- docs/cli-schema.json | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/docs/cli-schema.json b/docs/cli-schema.json index 7da1e8df2c..a8094785c7 100644 --- a/docs/cli-schema.json +++ b/docs/cli-schema.json @@ -4099,6 +4099,21 @@ "required": false, "summary": "GitHub repository name for seeding bundle defaults. Overrides the value inferred from git remote origin." }, + { + "role": "flag", + "name": "mode", + "type": "string", + "required": false, + "summary": "Onboarding mode: gh-release (bundle from GitHub release notes, no per-PR entries), changelog-auto (per-PR entries, automated bundle at release), or changelog-manual (per-PR entries, manual bundling). Defaults to changelog-manual behaviour when omitted." + }, + { + "role": "flag", + "name": "workflows", + "type": "boolean", + "required": false, + "summary": "When set, generates the three release-notes-*.yml GitHub Actions caller files under .github/workflows/.", + "defaultValue": "false" + }, { "role": "flag", "name": "log-level", From cd9baf1eeae0cb1791b274e0b34160d99e4c2ae8 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Tue, 22 Sep 2026 15:30:55 +0200 Subject: [PATCH 3/4] Simplify changelog init for golden onboarding path --- config/changelog.example-gh-release.yml | 20 -- config/changelog.example.yml | 317 +----------------- docs/cli-schema.json | 31 +- .../ChangelogTemplateSeeder.cs | 61 ---- .../docs-builder/Commands/ChangelogCommand.cs | 178 +--------- src/tooling/docs-builder/docs-builder.csproj | 1 - .../ChangelogTemplateSeederTests.cs | 238 ------------- 7 files changed, 18 insertions(+), 828 deletions(-) delete mode 100644 config/changelog.example-gh-release.yml delete mode 100644 src/Elastic.Documentation.Configuration/ChangelogTemplateSeeder.cs delete mode 100644 tests/Elastic.Documentation.Configuration.Tests/ChangelogTemplateSeederTests.cs diff --git a/config/changelog.example-gh-release.yml b/config/changelog.example-gh-release.yml deleted file mode 100644 index e13181bd35..0000000000 --- a/config/changelog.example-gh-release.yml +++ /dev/null @@ -1,20 +0,0 @@ -# Changelog Configuration — GitHub-release-driven mode -# Bundles are built directly from a GitHub release's notes at publish time. -# No per-PR changelog entries are checked in. -# Run `docs-builder changelog init --mode gh-release` to re-generate this file. - -# Bundle configuration -bundle: - # Input directory (unused in gh-release mode, kept for compatibility) - directory: docs/changelog - # Output directory for bundled changelog files - output_directory: docs/releases - # changelog-init-bundle-seed - # PR/issue link allowlist: when set (including []), only links to these owner/repo pairs are kept - # in bundle output; others are rewritten to '# PRIVATE:' sentinels. - # Run `changelog init` in a GitHub clone to pre-fill owner, repo, and link_allow_repos, or set - # bundle.owner, bundle.repo, and link_allow_repos manually. Example: - # link_allow_repos: - # - elastic/my-repo - # repo: my-repo - # owner: elastic diff --git a/config/changelog.example.yml b/config/changelog.example.yml index 0decc8a5d9..e30a65ef75 100644 --- a/config/changelog.example.yml +++ b/config/changelog.example.yml @@ -1,312 +1,21 @@ -# Changelog Configuration -# This file configures the valid values for changelog fields using the pivot structure. -# Place this file as `changelog.yml` in the `docs/` directory -# -# NOTE: All list-like fields accept BOTH forms: -# - Comma-separated string: "value1, value2, value3" -# - YAML list: -# - value1 -# - value2 -# - value3 -# Both forms produce the same result. Choose whichever is more readable for your use case. +# Changelog configuration +# Place this file at docs/changelog.yml. -# Filename strategy for generated changelog files. -# Controls how files created by 'changelog add' are named. -# pr — use the PR number (e.g., 12345.yaml). -# issue — use the issue number (e.g., 67890.yaml). -# timestamp — use a Unix timestamp with a title slug (e.g., 1735689600-fix-search.yaml). Default. -# Can be overridden per invocation with --use-pr-number or --use-issue-number CLI flags. -filename: timestamp - -# Products configuration (optional) -# If not specified, all products from products.yml are allowed (including those -# that have 'public-reference' disabled). -products: - # List of available product IDs (empty = all from products.yml) - # Accepts string or list: "elasticsearch, kibana" or [elasticsearch, kibana] - available: [] - # Default products when --products not specified - # default: - # - product: elasticsearch - # lifecycle: ga - -# Extraction configuration -# Controls automatic extraction of information from PR descriptions -extract: - # Auto-extract release notes from PR descriptions (default: true) - # Can be overridden by CLI --no-extract-release-notes - release_notes: true - # Auto-extract linked references (default: true) - # When using --prs: looks for patterns like "Fixes #123", "Closes #456" in PR body to derive issues. - # When using --issues: looks for patterns like "Fixed by #123" in issue body to derive PRs. - # Can be overridden by CLI --no-extract-issues - issues: true - # Remove square-bracket prefixes from PR titles (default: false) - # When enabled (true or via --strip-title-prefix), titles like "[ES|QL] Fix bug" become "Fix bug". - # Can be overridden by CLI --strip-title-prefix - strip_title_prefix: false - -# Available lifecycle values (strongly typed: preview, beta, ga) -# Accepts string or list: "preview, beta, ga" or a YAML list -lifecycles: - - preview - - beta - - ga - - experimental - -# Pivot configuration for types, subtypes, and areas with label mappings. -# By default we render changelogs grouped by type > subtype > area. -# NOTE: subtype and area are optional and either may be omitted. -# Labels are specified in a "label -> value" format -pivot: - # Type definitions with optional labels - # At a minimum, feature, bug-fix, and breaking-change must be configured. - # Keys are type names, values can be: - # - simple string: comma-separated label list (e.g., ">bug, >fix") - # - YAML list: [">bug", ">fix"] - # - empty/null: no labels for this type - # - object: { labels: "...", subtypes: {...} } for breaking-change type only - # (labels and subtype values also accept string or list) - types: - # Complex object form with subtypes (ONLY allowed for breaking-change) - # Subtypes help categorize breaking changes by their nature - # Both labels and subtype values accept string or list form - breaking-change: - labels: ">breaking, >bc" - # Equivalent list form: - # labels: - # - ">breaking" - # - ">bc" - subtypes: - api: ">api-breaking" - behavioral: ">behavioral-breaking" - configuration: ">config-breaking" - dependency: ">dependency-breaking" - subscription: ">subscription-breaking" - plugin: ">plugin-breaking" - security: ">security-breaking" - other: - # Simple string form: labels as comma-separated string - bug-fix: ">bug" - deprecation: ">deprecation" - # Empty block: no labels (YAML null) - feature: - enhancement: - docs: - known-issue: - other: - regression: - security: - - # Labels that trigger the highlight flag (accepts string or list) - # String form: highlight: ">highlight, >release-highlight" - # List form: - # highlight: - # - ">highlight" - # - ">release-highlight" - - # Area definitions with labels - # Keys are area display names (can contain commas), values are label strings or lists - # Each label maps to exactly one area. - # String form: "label1, label2" | List form: [label1, label2] - # To map one label to multiple areas, repeat the label under each area name. - areas: - # Example mappings - customize based on your label naming conventions - # Autoscaling: ":Distributed Coordination/Autoscaling" - # Search: ":Search/Search" - # Security: ":Security/Security" - # Watcher: ":Data Management/Watcher" - # To map a label to multiple areas (e.g., "Team:Search" to both "Search" and "Observability"): - # Search: - # - ":Search/Search" - # - "Team:Search" - # Observability: - # - "Team:Search" - # List form example: - # Search: - # - ":Search/Search" - # - ":Search/Ranking" - - # Product definitions with labels (optional). - # Keys are product spec strings; values are label strings or lists that trigger that product. - # A product spec string is: " [] []" - # - Product ID only: "elasticsearch" - # - Product + target: "kibana 9.2.0" - # - Full spec: "cloud-serverless 2025-06 ga" - # When a PR has labels matching any product entry, all matching products are added. - # Precedence: --products CLI option > pivot.products label mapping > products.default > repo inference. - # - # products: - # 'elasticsearch': - # - ":stack/elasticsearch" - # 'kibana': - # - ":stack/kibana" - # 'cloud-serverless': - # - ":cloud/serverless" - # # Specify a target version if known: - # # 'elasticsearch 9.2.0': - # # - ":feature/new-in-9.2" - - # Feature ID definitions with labels (optional). - # Keys are feature-id values written to the changelog; values are label strings or lists. - # When a PR has a matching label and --feature-id is not set, that feature-id is applied. - # If multiple labels map to distinct feature-ids, a warning is emitted and the first match is used. - # Precedence: --feature-id CLI option > pivot.features label mapping. - # Unreleased feature IDs should also be listed under bundle.profiles..hide_features. - # - # features: - # 'feature:new-search-api': - # - "feature-flag:new-search-api" - # - ":Feature/NewSearchApi" - -# Rules configuration — controls which PRs create changelogs and which changelogs are published. -# All list-like fields (exclude, include, exclude_types) accept BOTH forms: -# - Comma-separated string: "value1, value2, value3" -# - YAML list: [value1, value2, value3] -# -# For details and examples, refer to the [rules documentation](https://github.com/elastic/docs-builder/blob/main/docs/contribute/configure-changelogs-ref.md#rules). -rules: - # match: any - - # Create — controls which PRs generate changelogs. - # create: - # exclude: ">non-issue, >test" - # # Or equivalently: - # # exclude: - # # - ">non-issue" - # # - ">test" - # # match: any - # products: - # 'elasticsearch, kibana': - # exclude: ">test" - # 'cloud-serverless': - # exclude: "ILM" - - # Bundle — filtering applied during 'changelog bundle' and 'changelog gh-release'. - # See https://github.com/elastic/docs-builder/blob/main/docs/contribute/configure-changelogs-ref.md#rules-bundle for Mode 1 / 2 / 3 behavior. - # - # Mode 2 (global content): use global lists only — no `products` key, or `products: {}`. - # Example — include changelogs that list elasticsearch OR kibana (evaluated per changelog): - # bundle: - # match_products: any - # include_products: - # - elasticsearch - # - kibana - # exclude_types: "deprecation, known-issue" - # - # Example — exclude only when the changelog lists BOTH products (conjunction); extras allowed: - # bundle: - # match_products: conjunction - # exclude_products: - # - kibana - # - observability - # - # Mode 3 (per-product context): non-empty `rules.bundle.products` — global include/exclude/type/area - # under `bundle:` are NOT applied for filtering; configure filters under each product key. - # Example: - # bundle: - # exclude_types: [docs] # ignored for filtering in Mode 3 — duplicate under each product if needed - # products: - # observability: - # exclude_products: ["cloud-enterprise"] - # exclude_types: ["docs"] - # include_areas: - # - "APM" - # - "Infrastructure monitoring" - # kibana: - # match_products: all - # include_products: ["kibana", "security"] - # exclude_types: ["docs"] - -# Bundle configuration (profiles and defaults) bundle: # Input directory containing changelog YAML files directory: docs/changelog # Output directory for bundled changelog files output_directory: docs/releases - # Optional: default description text for bundles. Supports {version}, {lifecycle}, {owner}, and {repo} placeholders. - # Use YAML literal block scalar (|) for multiline descriptions. - # description: | - # This release includes new features and bug fixes. - # - # For more information, see the [release notes](https://www.elastic.co/docs/release-notes/product#product-{version}). - # changelog-init-bundle-seed - # PR/issue link allowlist: when set (including []), only links to these owner/repo pairs are kept - # in bundle output; others are rewritten to '# PRIVATE:' sentinels. - # There is no implicit allow: you must list every repo whose links should appear, including your - # own (bundle.repo as owner/repo). When omitted entirely, no link filtering is applied. - # Run `changelog init` in a GitHub clone to pre-fill owner, repo, and link_allow_repos, or set - # bundle.owner, bundle.repo, and link_allow_repos manually. Example — allow only this repo: - # link_allow_repos: - # - elastic/kibana - # To allow cross-repo links, add more owner/repo entries (for example elastic/elasticsearch). - # Optional: default GitHub repo name applied to all profiles that do not specify their own. - # Used by the {changelog} directive to generate correct PR/issue links when the product ID - # differs from the GitHub repository name. Can be overridden per profile. - # repo: elasticsearch - # Optional: default GitHub owner applied to all profiles that do not specify their own. - # owner: elastic - # Optional: branch whose CDN changelog pool (changelog/{org}/{repo}/{branch}/...) is sourced from when - # bundling entries from the CDN. Defaults to "main" when unset. Can be overridden per profile. - # branch: main - # Optional: control auto-population of release-date for all profiles by default. - # When true (default), auto-populate release dates. Profiles can override this setting. - # release_dates: true - # Named bundle profiles for different release scenarios. - # Profiles can be used with both 'changelog bundle' and 'changelog remove': - # docs-builder changelog bundle elasticsearch-release 9.2.0 - # docs-builder changelog remove elasticsearch-release 9.2.0 - # When used with 'changelog remove', only the 'products' field is applied. - # The 'output_products', 'repo', 'owner', and 'hide_features' fields are - # bundle-specific and are ignored for removal. - # Bundle file names are derived by convention as '{repo}-{product}-{version}.yaml' from the - # authoring repo and the profile's primary output product; setting an explicit 'output' - # pattern is an error. If no repo can be resolved, the name falls back to '{product}-{version}.yaml'. - profiles: - # Example: Elasticsearch release profile (filter by changelog fields) - # elasticsearch-release: - # # Filter: which input changelogs to include ({version} and {lifecycle} are substituted at runtime) - # products: "elasticsearch {version} {lifecycle}" - # # The bundle's products metadata; also determines the conventional output - # # file name ({repo}-{product}-{version}.yaml from the authoring repo and the first product). - # output_products: "elasticsearch {version}" - # # Optional: profile-specific description (overrides bundle.description) - # # description: | - # # Elasticsearch {version} includes: - # # - Performance improvements - # # - Bug fixes and stability enhancements - # # - # # Download the release binaries: https://github.com/{owner}/{repo}/releases/tag/v{version} - # # Optional: control auto-population of release-date for this profile. - # # When true (default), auto-populate release dates. When false, equivalent to --no-release-date. - # # release_dates: true - # Example: GitHub release profile (fetches PR list directly from a GitHub release) - # Use when you want to bundle or remove changelogs based on a published GitHub release. - # elasticsearch-gh-release: - # source: github_release # Fetch PR list from GitHub release instead of filtering input changelogs - # repo: elasticsearch # GitHub repository (required if bundle.repo is not set) - # owner: elastic # GitHub owner (optional; defaults to bundle.owner or "elastic") - # output_products: "elasticsearch {version} {lifecycle}" - - # Example: Serverless release profile (filter by promotion report, PR, or issue list) - # serverless-release: - # 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: GitHub repository for link and file name generation (overrides bundle.repo if set) - # repo: elasticsearch - # owner: elastic - # # Feature IDs to hide when bundling with this profile (accepts string or list) - # hide_features: - # - feature-flag-1 - # - feature-flag-2 +pivot: + # Map pull request labels to standard changelog types. + # At a minimum, feature, bug-fix, and breaking-change must be configured. + types: + feature: "feature" + bug-fix: "bug, fix" + breaking-change: "breaking" - # Example: Multi-product profile (Mode 3 rule context = first product alphabetically, e.g. kibana). - # For security-only rules, add a separate profile with output_products listing only security. - # kibana-security-release: - # output: "kibana-security-{version}.yaml" - # output_products: "kibana {version}, security {version}" +rules: + create: + # Labels that mean this pull request needs no changelog entry + exclude: "changelog:skip, chore, dependencies, ci" diff --git a/docs/cli-schema.json b/docs/cli-schema.json index a8094785c7..b2783a27c2 100644 --- a/docs/cli-schema.json +++ b/docs/cli-schema.json @@ -4045,7 +4045,7 @@ ], "name": "init", "summary": "Create changelog.yml and the changelog/releases directory structure.", - "notes": "Discovers the docs folder via docset.yml; falls back to creating PATH/docs.\nWhen changelog.yml already exists, updates only the paths specified via or .\nSeeds bundle.owner, bundle.repo, and bundle.link_allow_repos from the git remote origin when available.", + "notes": "Discovers the docs folder via docset.yml; falls back to creating PATH/docs.\nWhen changelog.yml already exists, updates only the paths specified via or .", "usage": "docs-builder changelog init [options]", "examples": [], "parameters": [ @@ -4085,35 +4085,6 @@ } ] }, - { - "role": "flag", - "name": "owner", - "type": "string", - "required": false, - "summary": "GitHub owner for seeding bundle defaults. Overrides the value inferred from git remote origin." - }, - { - "role": "flag", - "name": "repo", - "type": "string", - "required": false, - "summary": "GitHub repository name for seeding bundle defaults. Overrides the value inferred from git remote origin." - }, - { - "role": "flag", - "name": "mode", - "type": "string", - "required": false, - "summary": "Onboarding mode: gh-release (bundle from GitHub release notes, no per-PR entries), changelog-auto (per-PR entries, automated bundle at release), or changelog-manual (per-PR entries, manual bundling). Defaults to changelog-manual behaviour when omitted." - }, - { - "role": "flag", - "name": "workflows", - "type": "boolean", - "required": false, - "summary": "When set, generates the three release-notes-*.yml GitHub Actions caller files under .github/workflows/.", - "defaultValue": "false" - }, { "role": "flag", "name": "log-level", diff --git a/src/Elastic.Documentation.Configuration/ChangelogTemplateSeeder.cs b/src/Elastic.Documentation.Configuration/ChangelogTemplateSeeder.cs deleted file mode 100644 index 1ec54a711b..0000000000 --- a/src/Elastic.Documentation.Configuration/ChangelogTemplateSeeder.cs +++ /dev/null @@ -1,61 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -namespace Elastic.Documentation.Configuration; - -/// -/// Applies bundle.owner, bundle.repo, and bundle.link_allow_repos seeding -/// to the changelog template placeholder. Pure string transformation with no I/O. -/// -public static class ChangelogTemplateSeeder -{ - internal const string Placeholder = " # changelog-init-bundle-seed"; - - /// - /// Replaces or removes the # changelog-init-bundle-seed placeholder in template content. - /// CLI values take precedence over git-inferred values. When only repo is known, owner defaults to elastic. - /// - public static string ApplyBundleRepoSeed(string content, string? ownerCli, string? repoCli, string? gitOwner, string? gitRepo) - { - var gitMatched = gitOwner is not null && gitRepo is not null; - - var resolvedRepo = string.IsNullOrWhiteSpace(repoCli) ? gitRepo : repoCli.Trim(); - var resolvedOwner = string.IsNullOrWhiteSpace(ownerCli) ? gitOwner : ownerCli.Trim(); - if (!string.IsNullOrWhiteSpace(resolvedRepo) && string.IsNullOrWhiteSpace(resolvedOwner)) - resolvedOwner = "elastic"; - - var shouldSeed = !string.IsNullOrWhiteSpace(resolvedOwner) - && !string.IsNullOrWhiteSpace(resolvedRepo) - && (!string.IsNullOrWhiteSpace(ownerCli) || !string.IsNullOrWhiteSpace(repoCli) || gitMatched); - - var eol = content.Contains("\r\n", StringComparison.Ordinal) ? "\r\n" : "\n"; - - var block = shouldSeed - ? $" owner: {QuoteForYaml(resolvedOwner!)}{eol} repo: {QuoteForYaml(resolvedRepo!)}{eol} link_allow_repos:{eol} - {QuoteForYaml($"{resolvedOwner}/{resolvedRepo}")}{eol}" - : ""; - - var placeholderWithEol = Placeholder + eol; - if (content.Contains(placeholderWithEol, StringComparison.Ordinal)) - return content.Replace(placeholderWithEol, block, StringComparison.Ordinal); - - return content.Replace(Placeholder, shouldSeed ? block.TrimEnd('\r', '\n') : string.Empty, StringComparison.Ordinal); - } - - internal static string QuoteForYaml(string value) => - value.Contains(':') - || value.Contains(' ') - || value.Contains('#') - || value.Contains('"') - || value.Contains('\\') - || value.Contains('\n') - || value.Contains('\r') - || value.Contains('\t') - ? $"\"{value - .Replace("\\", "\\\\") - .Replace("\"", "\\\"") - .Replace("\r", "\\r") - .Replace("\n", "\\n") - .Replace("\t", "\\t")}\"" - : value; -} diff --git a/src/tooling/docs-builder/Commands/ChangelogCommand.cs b/src/tooling/docs-builder/Commands/ChangelogCommand.cs index 3d70cfb6dc..25addfe97e 100644 --- a/src/tooling/docs-builder/Commands/ChangelogCommand.cs +++ b/src/tooling/docs-builder/Commands/ChangelogCommand.cs @@ -57,24 +57,15 @@ IEnvironmentVariables environmentVariables /// /// Discovers the docs folder via docset.yml; falls back to creating PATH/docs. /// When changelog.yml already exists, updates only the paths specified via or . - /// Seeds bundle.owner, bundle.repo, and bundle.link_allow_repos from the git remote origin when available. /// /// Repository root. Defaults to cwd. /// Changelog entry directory. Defaults to docs/changelog. /// Bundle output directory. Defaults to docs/releases. - /// GitHub owner for seeding bundle defaults. Overrides the value inferred from git remote origin. - /// GitHub repository name for seeding bundle defaults. Overrides the value inferred from git remote origin. - /// Onboarding mode: gh-release (bundle from GitHub release notes, no per-PR entries), changelog-auto (per-PR entries, automated bundle at release), or changelog-manual (per-PR entries, manual bundling). Defaults to changelog-manual behaviour when omitted. - /// When set, generates the three release-notes-*.yml GitHub Actions caller files under .github/workflows/. [NoOptionsInjection] public Task Init( [ExpandUserProfile, RejectSymbolicLinks] DirectoryInfo? path = null, [ExpandUserProfile, RejectSymbolicLinks] DirectoryInfo? changelogDir = null, - [ExpandUserProfile, RejectSymbolicLinks] DirectoryInfo? bundlesDir = null, - string? owner = null, - string? repo = null, - string? mode = null, - bool workflows = false + [ExpandUserProfile, RejectSymbolicLinks] DirectoryInfo? bundlesDir = null ) { var rootPath = path?.FullName ?? Path.GetFullPath("."); @@ -105,25 +96,17 @@ public Task Init( var useNonDefaultBundlesDir = bundlesDir != null; var repoRoot = Paths.FindGitRoot(docsFolder)?.FullName ?? docsFolder.FullName; - var isGhRelease = string.Equals(mode, "gh-release", StringComparison.OrdinalIgnoreCase); - var bundleOnRelease = isGhRelease || string.Equals(mode, "changelog-auto", StringComparison.OrdinalIgnoreCase); - // Create changelog.yml from example if it does not exist if (!_fileSystem.File.Exists(configPath)) { - var templateResourceName = isGhRelease - ? "Documentation.Builder.changelog.example-gh-release.yml" - : "Documentation.Builder.changelog.example.yml"; - var templateFileName = isGhRelease ? "changelog.example-gh-release.yml" : "changelog.example.yml"; - byte[]? templateBytes = null; - using (var stream = typeof(ChangelogCommands).Assembly.GetManifestResourceStream(templateResourceName)) + using (var stream = typeof(ChangelogCommands).Assembly.GetManifestResourceStream("Documentation.Builder.changelog.example.yml")) { if (stream == null) { // Fallback: try config relative to current directory (for development) var localConfigDir = _fileSystem.Path.Join(Directory.GetCurrentDirectory(), "config"); - var localConfigPath = _fileSystem.Path.Join(localConfigDir, templateFileName); + var localConfigPath = _fileSystem.Path.Join(localConfigDir, "changelog.example.yml"); if (_fileSystem.File.Exists(localConfigPath)) { templateBytes = _fileSystem.File.ReadAllBytes(localConfigPath); @@ -139,7 +122,7 @@ public Task Init( if (templateBytes == null || templateBytes.Length == 0) { - collector.EmitError(string.Empty, $"Could not find {templateFileName} template. Ensure docs-builder is built correctly."); + collector.EmitError(string.Empty, "Could not find changelog.example.yml template. Ensure docs-builder is built correctly."); return Task.FromResult(1); } @@ -158,8 +141,6 @@ public Task Init( content = content.Replace("output_directory: docs/releases", $"output_directory: {outputValue}"); } - content = ApplyChangelogInitBundleRepoSeed(content, owner, repo, repoRoot); - try { _fileSystem.File.WriteAllBytes(configPath, Encoding.UTF8.GetBytes(content)); @@ -229,150 +210,9 @@ public Task Init( } } - // Warn about existing fixture entries that could be accidentally uploaded - WarnAboutFixtureEntries(changelogPath, configPath); - - // Generate GitHub Actions workflow caller files when requested - if (workflows) - { - var configRelPath = GetPathForConfig(repoRoot, configPath).Trim('"'); - var workflowsDir = _fileSystem.Path.Join(repoRoot, ".github", "workflows"); - if (!_fileSystem.Directory.Exists(workflowsDir)) - { - try - { - _ = _fileSystem.Directory.CreateDirectory(workflowsDir); - } - catch (IOException ex) - { - collector.EmitError(string.Empty, $"Failed to create .github/workflows directory: {ex.Message}", ex); - return Task.FromResult(1); - } - } - - WriteWorkflowFile(workflowsDir, "release-notes.yml", BuildReleaseNotesWorkflow(configRelPath, bundleOnRelease)); - WriteWorkflowFile(workflowsDir, "release-notes-comments.yml", BuildReleaseNotesCommentsWorkflow()); - WriteWorkflowFile(workflowsDir, "release-notes-changelog-file.yml", BuildReleaseNotesChangelogFileWorkflow(configRelPath)); - - if (bundleOnRelease) - { - _logger.LogWarning( - "bundle-on-release: true only fires when the release is published by a human or a non-GITHUB_TOKEN. " + - "If your release workflow uses github.token, add the bundle jobs inline instead. " + - "See: https://elastic.github.io/docs-builder/data/release-notes/onboard/github-release-driven" - ); - } - } - return Task.FromResult(0); } - private void WarnAboutFixtureEntries(string changelogPath, string configPath) - { - if (!_fileSystem.Directory.Exists(changelogPath)) - return; - - var configFileName = _fileSystem.Path.GetFileName(configPath); - var existingEntries = _fileSystem - .Directory - .GetFiles(changelogPath, "*.yml") - .Concat(_fileSystem.Directory.GetFiles(changelogPath, "*.yaml")) - .Where(f => !string.Equals(_fileSystem.Path.GetFileName(f), configFileName, StringComparison.OrdinalIgnoreCase)) - .ToList(); - - if (existingEntries.Count > 0) - { - _logger.LogWarning( - "Found {Count} existing .yaml file(s) in {ChangelogPath}. " + - "Only files directly in this directory are uploaded (TopDirectoryOnly). " + - "Move demo/sample/fixture entries to {ChangelogPath}/examples/ before the first push to main, " + - "or they may be included in release bundles.", - existingEntries.Count, - changelogPath, - changelogPath - ); - } - } - - private void WriteWorkflowFile(string workflowsDir, string fileName, string content) - { - var filePath = _fileSystem.Path.Join(workflowsDir, fileName); - if (_fileSystem.File.Exists(filePath)) - { - _logger.LogWarning("Workflow file already exists, skipping: {FilePath}", filePath); - return; - } - - try - { - _fileSystem.File.WriteAllText(filePath, content); - _logger.LogInformation("Created workflow file: {FilePath}", filePath); - } - catch (IOException ex) - { - _logger.LogWarning("Failed to write workflow file '{FilePath}': {Message}", filePath, ex.Message); - } - } - - private static string BuildReleaseNotesWorkflow(string configRelPath, bool bundleOnRelease) - { - var releaseTrigger = bundleOnRelease ? " release:\n types: [published]\n" : string.Empty; - var bundleWith = bundleOnRelease ? " bundle-on-release: true\n" : string.Empty; - return "name: Release notes\n" - + "on:\n" - + " pull_request:\n" - + " types: [opened, synchronize, reopened, labeled, unlabeled]\n" - + " push:\n" - + " branches: [main]\n" - + releaseTrigger - + "permissions: {}\n" - + "jobs:\n" - + " release-notes:\n" - + " permissions:\n" - + " contents: read\n" - + " pull-requests: read\n" - + " packages: read\n" - + " id-token: write\n" - + " uses: elastic/docs-actions/.github/workflows/release-notes.yml@v1\n" - + " with:\n" - + $" config: {configRelPath}\n" - + bundleWith; - } - - private static string BuildReleaseNotesCommentsWorkflow() => - "name: Release notes comments\n" - + "on:\n" - + " workflow_run:\n" - + " workflows: [Release notes]\n" - + " types: [completed]\n" - + "permissions: {}\n" - + "jobs:\n" - + " comment:\n" - + " if: github.event.workflow_run.event == 'pull_request'\n" - + " permissions:\n" - + " pull-requests: write\n" - + " actions: read\n" - + " uses: elastic/docs-actions/.github/workflows/release-notes-comments.yml@v1\n"; - - private static string BuildReleaseNotesChangelogFileWorkflow(string configRelPath) => - "name: Release notes changelog file\n" - + "on:\n" - + " workflow_run:\n" - + " workflows: [Release notes]\n" - + " types: [completed]\n" - + "permissions: {}\n" - + "jobs:\n" - + " changelog-file:\n" - + " permissions:\n" - + " contents: write\n" - + " pull-requests: write\n" - + " id-token: write\n" - + " packages: read\n" - + " actions: write\n" - + " uses: elastic/docs-actions/.github/workflows/release-notes-changelog-file.yml@v1\n" - + " with:\n" - + $" config: {configRelPath}\n"; - /// Create a new changelog entry YAML file. /// Optional: Products affected in format "product [lifecycle], ..." (e.g., "cloud-serverless, kibana" or "elasticsearch ga"). Do not include a version or date; that is an error. Use 'changelog note' for version-listed items. If not specified, products are inferred from the repository or config defaults. /// Optional: What users must do to mitigate @@ -2476,16 +2316,6 @@ private static string GetPathForConfig(string repoPath, string targetPath) return pathForConfig; } - private string ApplyChangelogInitBundleRepoSeed(string content, string? ownerCli, string? repoCli, string repoRoot) - { - string? gitOwner = null; - string? gitRepo = null; - if (GitRemoteConfigurationReader.TryReadOriginUrl(_fileSystem, repoRoot, out var originUrl)) - _ = GitHubRemoteParser.TryParseGitHubComOwnerRepo(originUrl, out gitOwner, out gitRepo); - - return ChangelogTemplateSeeder.ApplyBundleRepoSeed(content, ownerCli, repoCli, gitOwner, gitRepo); - } - private async Task ResolveBundleDescription( string? description, string? descriptionFile, diff --git a/src/tooling/docs-builder/docs-builder.csproj b/src/tooling/docs-builder/docs-builder.csproj index b15185780d..63cbb5997f 100644 --- a/src/tooling/docs-builder/docs-builder.csproj +++ b/src/tooling/docs-builder/docs-builder.csproj @@ -37,7 +37,6 @@ - diff --git a/tests/Elastic.Documentation.Configuration.Tests/ChangelogTemplateSeederTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ChangelogTemplateSeederTests.cs deleted file mode 100644 index cb4e8c857a..0000000000 --- a/tests/Elastic.Documentation.Configuration.Tests/ChangelogTemplateSeederTests.cs +++ /dev/null @@ -1,238 +0,0 @@ -// Licensed to Elasticsearch B.V under one or more agreements. -// Elasticsearch B.V licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information - -using AwesomeAssertions; - -namespace Elastic.Documentation.Configuration.Tests; - -public class ChangelogTemplateSeederTests -{ - private const string Template = "bundle:\n release_dates: true\n # changelog-init-bundle-seed\n # some other comment\n"; - - private const string TemplateWindows = - "bundle:\r\n release_dates: true\r\n # changelog-init-bundle-seed\r\n # some other comment\r\n"; - - [Fact] - public void ApplyBundleRepoSeed_GitOwnerAndRepo_SeedsTemplate() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - Template, - ownerCli: null, - repoCli: null, - gitOwner: "elastic", - gitRepo: "kibana" - ); - - result.Should().Contain(" owner: elastic\n"); - result.Should().Contain(" repo: kibana\n"); - result.Should().Contain(" - elastic/kibana\n"); - result.Should().NotContain("changelog-init-bundle-seed"); - } - - [Fact] - public void ApplyBundleRepoSeed_CliOwnerAndRepo_SeedsTemplate() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - Template, - ownerCli: "myorg", - repoCli: "myrepo", - gitOwner: null, - gitRepo: null - ); - - result.Should().Contain(" owner: myorg\n"); - result.Should().Contain(" repo: myrepo\n"); - result.Should().Contain(" - myorg/myrepo\n"); - } - - [Fact] - public void ApplyBundleRepoSeed_CliOverridesGit() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - Template, - ownerCli: "override-owner", - repoCli: "override-repo", - gitOwner: "elastic", - gitRepo: "kibana" - ); - - result.Should().Contain(" owner: override-owner\n"); - result.Should().Contain(" repo: override-repo\n"); - result.Should().Contain(" - override-owner/override-repo\n"); - } - - [Fact] - public void ApplyBundleRepoSeed_CliRepoOnly_OwnerDefaultsToElastic() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - Template, - ownerCli: null, - repoCli: "myrepo", - gitOwner: null, - gitRepo: null - ); - - result.Should().Contain(" owner: elastic\n"); - result.Should().Contain(" repo: myrepo\n"); - result.Should().Contain(" - elastic/myrepo\n"); - } - - [Fact] - public void ApplyBundleRepoSeed_CliOwnerOnly_NoRepo_RemovesPlaceholder() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed(Template, ownerCli: "myorg", repoCli: null, gitOwner: null, gitRepo: null); - - result.Should().NotContain("changelog-init-bundle-seed"); - result.Should().NotContain(" owner:"); - result.Should().NotContain(" repo:"); - } - - [Fact] - public void ApplyBundleRepoSeed_NeitherCliNorGit_RemovesPlaceholder() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed(Template, ownerCli: null, repoCli: null, gitOwner: null, gitRepo: null); - - result.Should().NotContain("changelog-init-bundle-seed"); - result.Should().NotContain(" owner:"); - result.Should().NotContain(" repo:"); - result.Should().Contain(" # some other comment\n"); - } - - [Fact] - public void ApplyBundleRepoSeed_WhitespaceCliValues_TreatedAsAbsent() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - Template, - ownerCli: " ", - repoCli: " ", - gitOwner: "elastic", - gitRepo: "kibana" - ); - - result.Should().Contain(" owner: elastic\n"); - result.Should().Contain(" repo: kibana\n"); - } - - [Fact] - public void ApplyBundleRepoSeed_WindowsLineEndings_PreservesStyle() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - TemplateWindows, - ownerCli: null, - repoCli: null, - gitOwner: "elastic", - gitRepo: "kibana" - ); - - result.Should().Contain(" owner: elastic\r\n"); - result.Should().Contain(" repo: kibana\r\n"); - result.Should().NotContain(" owner: elastic\n repo:"); - } - - [Fact] - public void ApplyBundleRepoSeed_MissingPlaceholder_ReturnsContentUnchanged() - { - var content = "bundle:\n release_dates: true\n"; - - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - content, - ownerCli: "elastic", - repoCli: "kibana", - gitOwner: null, - gitRepo: null - ); - - result.Should().Be(content); - } - - [Fact] - public void ApplyBundleRepoSeed_ValuesNeedingYamlQuoting_AreQuoted() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - Template, - ownerCli: "my org", - repoCli: "my:repo", - gitOwner: null, - gitRepo: null - ); - - result.Should().Contain(" owner: \"my org\"\n"); - result.Should().Contain(" repo: \"my:repo\"\n"); - result.Should().Contain(" - \"my org/my:repo\"\n"); - } - - [Fact] - public void ApplyBundleRepoSeed_CliRepoOverridesGitRepo_KeepsGitOwner() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - Template, - ownerCli: null, - repoCli: "other-repo", - gitOwner: "elastic", - gitRepo: "kibana" - ); - - result.Should().Contain(" owner: elastic\n"); - result.Should().Contain(" repo: other-repo\n"); - result.Should().Contain(" - elastic/other-repo\n"); - } - - [Fact] - public void ApplyBundleRepoSeed_PlaceholderAtEofWithoutNewline_Seeds() - { - var content = "bundle:\n release_dates: true\n # changelog-init-bundle-seed"; - - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - content, - ownerCli: null, - repoCli: null, - gitOwner: "elastic", - gitRepo: "kibana" - ); - - result.Should().Contain(" owner: elastic"); - result.Should().Contain(" repo: kibana"); - result.Should().NotContain("changelog-init-bundle-seed"); - } - - [Fact] - public void ApplyBundleRepoSeed_PlaceholderAtEofWithoutNewline_RemovesWhenNoSeed() - { - var content = "bundle:\n release_dates: true\n # changelog-init-bundle-seed"; - - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed(content, ownerCli: null, repoCli: null, gitOwner: null, gitRepo: null); - - result.Should().Be("bundle:\n release_dates: true\n"); - result.Should().NotContain("changelog-init-bundle-seed"); - } - - [Fact] - public void ApplyBundleRepoSeed_BackslashInValue_IsEscapedInYaml() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - Template, - ownerCli: @"path\org", - repoCli: "repo", - gitOwner: null, - gitRepo: null - ); - - result.Should().Contain(@" owner: ""path\\org"""); - result.Should().Contain(" repo: repo\n"); - } - - [Fact] - public void ApplyBundleRepoSeed_ControlCharsInValue_AreEscapedInYaml() - { - var result = ChangelogTemplateSeeder.ApplyBundleRepoSeed( - Template, - ownerCli: "org\tname", - repoCli: "repo", - gitOwner: null, - gitRepo: null - ); - - result.Should().Contain(@" owner: ""org\tname"""); - } -} From c7a0c82ad547fa73c420b541546a663ffe2390e9 Mon Sep 17 00:00:00 2001 From: Fabrizio Ferri Benedetti Date: Tue, 29 Sep 2026 15:59:52 +0200 Subject: [PATCH 4/4] Update cmd-init.md to reflect simplified init command Remove --owner, --repo, and bundle seeding from the description and examples. The command now generates the minimal golden path config with no git-inferred seeding. Co-Authored-By: Claude Sonnet 4.6 --- docs/cli/changelog/cmd-init.md | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/cli/changelog/cmd-init.md b/docs/cli/changelog/cmd-init.md index a8db47f0aa..26093d4fd1 100644 --- a/docs/cli/changelog/cmd-init.md +++ b/docs/cli/changelog/cmd-init.md @@ -8,9 +8,9 @@ The command locates the docs folder using the following priority: 2. A `docs` folder without `docset.yml`. 3. If no docs folder exists, creates `{path}/docs`. -The command creates a `changelog.yml` configuration file and `changelog` and `releases` subdirectories in the docs folder. +The command creates a `changelog.yml` configuration file and `changelog` and `releases` subdirectories in the docs folder. The generated `changelog.yml` contains the minimal configuration for the automated release notes path: type mappings for `feature`, `bug-fix`, and `breaking-change`, and a skip-label rule. -When the template is written for the first time, the command seeds `bundle.owner`, `bundle.repo`, and `bundle.link_allow_repos` from your `git` remote `origin` (when it points at `github.com`) and/or from `--owner` and `--repo`. CLI values override values inferred from `git`. If neither source provides enough information, the placeholder lines are removed from the template for manual editing. +When `changelog.yml` already exists, the command updates only the directory paths if `--changelog-dir` or `--bundles-dir` are specified, and leaves all other content unchanged. ## Examples @@ -25,7 +25,4 @@ docs-builder changelog init --path /path/to/my-repo docs-builder changelog init \ --changelog-dir ./my-changelogs \ --bundles-dir ./my-releases - -# Explicitly set GitHub owner and repo (useful in CI without git) -docs-builder changelog init --owner elastic --repo kibana ```