diff --git a/config/changelog.example.yml b/config/changelog.example.yml index 4fbd12a11..3c1b7664c 100644 --- a/config/changelog.example.yml +++ b/config/changelog.example.yml @@ -27,8 +27,10 @@ extract: # Auto-extract release notes from PR descriptions (default: true) # Can be overridden by CLI --no-extract-release-notes release_notes: true - # Auto-extract issues from PR body (default: true) - # Looks for patterns like "Fixes #123", "Closes #456", etc. + # 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 # Available lifecycle values (strongly typed: preview, beta, ga) diff --git a/docs/changelog/bundles/0.100.0.yaml b/docs/changelog/bundles/0.100.0.yaml index 884b0a0f0..98a9b225f 100644 --- a/docs/changelog/bundles/0.100.0.yaml +++ b/docs/changelog/bundles/0.100.0.yaml @@ -10,7 +10,8 @@ entries: target: 0.100.0 areas: - CLI - pr: https://github.com/elastic/docs-builder/pull/2350 + prs: + - https://github.com/elastic/docs-builder/pull/2350 highlight: true description: | Adds support for configuring PR labels that block changelog creation. @@ -25,7 +26,8 @@ entries: target: 0.100.0 areas: - UI - pr: https://github.com/elastic/docs-builder/pull/2470 + prs: + - https://github.com/elastic/docs-builder/pull/2470 description: | Fixes an issue where HTMX navigation was not working correctly when selecting pages from the "Find Pages" dropdown menu. @@ -36,7 +38,8 @@ entries: target: 0.100.0 areas: - Build - pr: https://github.com/elastic/docs-builder/pull/2473 + prs: + - https://github.com/elastic/docs-builder/pull/2473 description: | Addresses F# nullability warnings in the codebase to improve code quality and reduce potential null reference issues. @@ -47,7 +50,8 @@ entries: target: 0.100.0 areas: - Assembler - pr: https://github.com/elastic/docs-builder/pull/2471 + prs: + - https://github.com/elastic/docs-builder/pull/2471 description: | Prevents speculative builds from running for repositories that are already publishing with non-versioned branches, avoiding unnecessary diff --git a/docs/changelog/fsharp-nullability-warnings.yaml b/docs/changelog/fsharp-nullability-warnings.yaml index dc5b3714c..e8d447dcc 100644 --- a/docs/changelog/fsharp-nullability-warnings.yaml +++ b/docs/changelog/fsharp-nullability-warnings.yaml @@ -5,7 +5,8 @@ products: target: 0.100.0 areas: - Build -pr: https://github.com/elastic/docs-builder/pull/2473 +prs: + - https://github.com/elastic/docs-builder/pull/2473 description: | Addresses F# nullability warnings in the codebase to improve code quality and reduce potential null reference issues. diff --git a/docs/changelog/htmx-navigation-fix.yaml b/docs/changelog/htmx-navigation-fix.yaml index bc6ad8954..fbb221504 100644 --- a/docs/changelog/htmx-navigation-fix.yaml +++ b/docs/changelog/htmx-navigation-fix.yaml @@ -5,7 +5,8 @@ products: target: 0.100.0 areas: - UI -pr: https://github.com/elastic/docs-builder/pull/2470 +prs: + - https://github.com/elastic/docs-builder/pull/2470 description: | Fixes an issue where HTMX navigation was not working correctly when selecting pages from the "Find Pages" dropdown menu. diff --git a/docs/changelog/pr-label-blockers.yaml b/docs/changelog/pr-label-blockers.yaml index 295fabaea..393d98509 100644 --- a/docs/changelog/pr-label-blockers.yaml +++ b/docs/changelog/pr-label-blockers.yaml @@ -5,7 +5,8 @@ products: target: 0.100.0 areas: - CLI -pr: https://github.com/elastic/docs-builder/pull/2350 +prs: + - https://github.com/elastic/docs-builder/pull/2350 description: | Adds support for configuring PR labels that block changelog creation. This allows teams to mark PRs that should not generate changelog entries diff --git a/docs/changelog/sample-feature-099.yaml b/docs/changelog/sample-feature-099.yaml index e4d332a06..955f3ef90 100644 --- a/docs/changelog/sample-feature-099.yaml +++ b/docs/changelog/sample-feature-099.yaml @@ -4,7 +4,8 @@ type: feature description: | Added CLI commands for managing release notes: `changelog add`, `changelog bundle`, and `changelog render`. This enables automated generation and formatting of release notes from structured YAML files. -pr: "1234" +prs: + - "1234" products: - product: docs-builder target: 0.99.0 diff --git a/docs/changelog/speculative-builds-fix.yaml b/docs/changelog/speculative-builds-fix.yaml index 620b3f9b2..0908afe98 100644 --- a/docs/changelog/speculative-builds-fix.yaml +++ b/docs/changelog/speculative-builds-fix.yaml @@ -5,7 +5,8 @@ products: target: 0.100.0 areas: - Assembler -pr: https://github.com/elastic/docs-builder/pull/2471 +prs: + - https://github.com/elastic/docs-builder/pull/2471 description: | Prevents speculative builds from running for repositories that are already publishing with non-versioned branches, avoiding unnecessary diff --git a/docs/cli/release/changelog-add.md b/docs/cli/release/changelog-add.md index d3214dd94..d9e889182 100644 --- a/docs/cli/release/changelog-add.md +++ b/docs/cli/release/changelog-add.md @@ -48,16 +48,25 @@ docs-builder changelog add [options...] [-h|--help] : If the content contains any special characters such as backquotes, you must precede it with a backslash escape character (`\`). `--issues ` -: Optional: Issue numbers (comma-separated or specify multiple times). +: Optional: Issue URL(s) or number(s) (comma-separated), or a path to a newline-delimited file containing issue URLs or numbers. Can be specified multiple times. +: Each occurrence can be either comma-separated issues (e.g., `--issues "https://github.com/owner/repo/issues/123,456"`) or a file path (e.g., `--issues /path/to/file.txt`). +: When specifying issues directly, provide comma-separated values. +: When specifying a file path, provide a single value that points to a newline-delimited file. +: If `--owner` and `--repo` are provided, issue numbers can be used instead of URLs. +: If specified, `--title` can be derived from the issue. +: Creates one changelog file per issue. `--no-extract-issues` -: Optional: Turn off extraction of linked issues from PR body (for example, "Fixes #123"). By default, linked issues are extracted when using `--prs`. +: Optional: Turn off extraction of linked references. +: When using `--prs`: turns off extraction of linked issues from the PR body (for example, "Fixes #123"). +: When using `--issues`: turns off extraction of linked PRs from the issue body (for example, "Fixed by #123"). +: By default, linked references are extracted in both cases. `--output ` : Optional: Output directory for the changelog fragment. Defaults to current directory. `--owner ` -: Optional: GitHub repository owner (used when `--pr` is just a number). +: Optional: GitHub repository owner (used when `--prs` or `--issues` contains just numbers). `--products >` : Required: Products affected in format "product target lifecycle, ..." (for example, `"elasticsearch 9.2.0 ga, cloud-serverless 2025-08-05"`). @@ -76,7 +85,7 @@ docs-builder changelog add [options...] [-h|--help] : If there are `block ... create` definitions in the changelog configuration file and a PR has a blocking label for any product in `--products`, that PR is skipped and no changelog file is created for it. `--repo ` -: Optional: GitHub repository name (used when `--pr` is just a number). +: Optional: GitHub repository name (used when `--prs` or `--issues` contains just numbers). `--strip-title-prefix` : Optional: When used with `--prs`, remove square brackets and text within them from the beginning of PR titles, and also remove a colon if it follows the closing bracket. @@ -90,14 +99,25 @@ docs-builder changelog add [options...] [-h|--help] `--title ` : A short, user-facing title (max 80 characters) -: Required if `--pr` is not specified. -: If both `--pr` and `--title` are specified, the latter value is used instead of what exists in the PR. +: Required if neither `--prs` nor `--issues` is specified. +: If both `--prs` and `--title` are specified, the latter value is used instead of what exists in the PR. : If the content contains any special characters such as backquotes, you must precede it with a backslash escape character (`\`). `--type ` -: Required: Type of change (for example, `feature`, `enhancement`, `bug-fix`, or `breaking-change`). +: Required if neither `--prs` nor `--issues` is specified. Type of change (for example, `feature`, `enhancement`, `bug-fix`, or `breaking-change`). +: If mappings are configured, type can be derived from the PR or issue. : The valid types are listed in [ChangelogConfiguration.cs](https://github.com/elastic/docs-builder/blob/main/src/services/Elastic.Documentation.Services/Changelog/ChangelogConfiguration.cs). `--use-pr-number` -: Optional: Use the PR number as the filename instead of generating it from a unique ID and title. -: When using this option, you must also provide the `--pr` option. +: Optional: Use the PR number(s) as the filename instead of generating it from a timestamp and title. +: With multiple PRs, uses hyphen-separated numbers (for example, `137431-137432.yaml`). +: Requires `--prs`. Mutually exclusive with `--use-issue-number`. + +`--use-issue-number` +: Optional: Use the issue number(s) as the filename instead of generating it from a timestamp and title. +: With multiple issues, uses hyphen-separated numbers (for example, `12345-12346.yaml`). +: Requires `--issues`. When both `--issues` and `--prs` are specified, still uses the issue number for the filename if this flag is set. Mutually exclusive with `--use-pr-number`. + +:::{important} +`--use-pr-number` and `--use-issue-number` are mutually exclusive; specify only one. +::: diff --git a/docs/cli/release/changelog-bundle.md b/docs/cli/release/changelog-bundle.md index e4b3a882d..fda2266fe 100644 --- a/docs/cli/release/changelog-bundle.md +++ b/docs/cli/release/changelog-bundle.md @@ -29,7 +29,7 @@ These arguments apply to profile-based bundling: `--all` : Include all changelogs from the directory. -: Only one filter option can be specified: `--all`, `--input-products`, or `--prs`. +: Only one filter option can be specified: `--all`, `--input-products`, `--prs`, or `--issues`. `--config ` : Optional: Path to the changelog.yml configuration file. @@ -46,7 +46,7 @@ These arguments apply to profile-based bundling: `--input-products ?>` : Filter by products in format "product target lifecycle, ..." -: Only one filter option can be specified: `--all`, `--input-products`, or `--prs`. +: Only one filter option can be specified: `--all`, `--input-products`, `--prs`, or `--issues`. : When specified, all three parts (product, target, lifecycle) are required but can be wildcards (`*`). For example: - `"cloud-serverless 2025-12-02 ga, cloud-serverless 2025-12-06 beta"` - exact matches @@ -55,6 +55,13 @@ These arguments apply to profile-based bundling: - `"* 9.3.* *"` - match any product with target starting with "9.3." - `"* * *"` - match all changelogs (equivalent to `--all`) +`--issues ` +: Filter by issue URLs or numbers (comma-separated), or a path to a newline-delimited file containing issue URLs or numbers. Can be specified multiple times. +: Only one filter option can be specified: `--all`, `--input-products`, `--prs`, or `--issues`. +: Each occurrence can be either comma-separated issues (e.g., `--issues "https://github.com/owner/repo/issues/123,456"`) or a file path (e.g., `--issues /path/to/file.txt`). +: When specifying issues directly, provide comma-separated values. +: When specifying a file path, provide a single value that points to a newline-delimited file. + `--no-resolve` : Optional: Explicitly turn off the `resolve` option if it's specified in the changelog configuration file. @@ -68,17 +75,17 @@ These arguments apply to profile-based bundling: : This value replaces information that would otherwise by derived from changelogs. `--owner ` -: The GitHub repository owner, which is required when pull requests are specified as numbers. +: The GitHub repository owner, which is required when pull requests or issues are specified as numbers. `--prs ` : Filter by pull request URLs or numbers (comma-separated), or a path to a newline-delimited file containing PR URLs or numbers. Can be specified multiple times. -: Only one filter option can be specified: `--all`, `--input-products`, or `--prs`. +: Only one filter option can be specified: `--all`, `--input-products`, `--prs`, or `--issues`. : Each occurrence can be either comma-separated PRs (e.g., `--prs "https://github.com/owner/repo/pull/123,6789"`) or a file path (e.g., `--prs /path/to/file.txt`). : When specifying PRs directly, provide comma-separated values. : When specifying a file path, provide a single value that points to a newline-delimited file. `--repo ` -: The GitHub repository name, which is required when PRs are specified as numbers. +: The GitHub repository name, which is required when pull requests or issues are specified as numbers. `--resolve` : Optional: Copy the contents of each changelog file into the entries array. diff --git a/docs/cli/release/changelog-render.md b/docs/cli/release/changelog-render.md index c4ced5ab0..727ce202e 100644 --- a/docs/cli/release/changelog-render.md +++ b/docs/cli/release/changelog-render.md @@ -33,7 +33,7 @@ docs-builder changelog render [options...] [-h|--help] : For example, `--input "/path/to/changelog-bundle.yaml|/path/to/changelogs|elasticsearch|keep-links"`. : Only `bundle-file-path` is required for each bundle. : Use `repo` if your changelogs do not contain full URLs for the pull requests or issues; otherwise they will be incorrectly derived with "elastic/elastic" in the URL by default. -: Use `link-visibility` to control whether PR/issue links are shown or hidden for entries from this bundle. Valid values are `keep-links` (default) or `hide-links`. Use `hide-links` for bundles from private repositories. +: Use `link-visibility` to control whether PR/issue links are shown or hidden for entries from this bundle. Valid values are `keep-links` (default) or `hide-links`. Use `hide-links` for bundles from private repositories. When `hide-links` is set, all links are hidden for each affected entry — changelog entries can contain multiple PR links (`prs`) and issue links (`issues`), and all of them are hidden or shown together. : Paths support tilde (`~`) expansion and relative paths. :::{note} @@ -92,6 +92,14 @@ When `--file-type asciidoc` is specified, the command generates a single asciido The asciidoc output uses attribute references for links (for example, `{repo-pull}NUMBER[#NUMBER]`). +### Multiple PR and issue links + +Changelog entries can reference multiple pull requests and issues using the `prs` and `issues` array fields. When an entry has multiple links, all of them are rendered inline for that entry: + +```md +* Fix ML calendar event update scalability issues. [#136886](https://github.com/elastic/elastic/pull/136886) [#136900](https://github.com/elastic/elastic/pull/136900) +``` + ## Examples ### Render a single bundle diff --git a/docs/contribute/_snippets/changelog-fields.md b/docs/contribute/_snippets/changelog-fields.md index 23e34d72e..ebe6f6a05 100644 --- a/docs/contribute/_snippets/changelog-fields.md +++ b/docs/contribute/_snippets/changelog-fields.md @@ -70,10 +70,10 @@ issues: # They are externalized in the release docs so users can follow the links and # understand the context. -pr: +prs: -# An optional string that contains the pull request identifier. -# It is externalized in the release docs so users can follow the link and find more details. +# An optional array of pull request identifiers (URLs or numbers). +# Each value is externalized in the release docs so users can follow the links and find more details. subtype: diff --git a/docs/contribute/changelog.md b/docs/contribute/changelog.md index de146faad..48590f524 100644 --- a/docs/contribute/changelog.md +++ b/docs/contribute/changelog.md @@ -231,7 +231,7 @@ For example, to create a new token with the minimum authority to read pull reque 4. Under **Resource owner** if you're an Elastic employee, select **Elastic**. 5. Set an expiration date. 6. Under **Repository access**, select **Only select repositories** and choose the repositories you want to access. -7. Under **Permissions** > **Repository permissions**, set **Pull requests** to **Read-only**. +7. Under **Permissions** > **Repository permissions**, set **Pull requests** to **Read-only**. If you want to be able to read issue details, do the same for **Issues**. 8. Click **Generate token**. 9. Copy the token to a safe location and use it in the `GITHUB_TOKEN` environment variable. @@ -262,15 +262,29 @@ If you want to use the PR number as the filename instead, add the `--use-pr-numb ```sh docs-builder changelog add \ - --pr https://github.com/elastic/elasticsearch/pull/137431 \ + --prs https://github.com/elastic/elasticsearch/pull/137431 \ --products "elasticsearch 9.2.3" \ --use-pr-number ``` -This creates a file named `137431.yaml` instead of the default timestamp-based filename. +With a single PR, this creates a file named `137431.yaml`. With multiple PRs, the filename aggregates the numbers (e.g., `137431-137432.yaml`). + +Use `--use-issue-number` to name the file by issue number(s). When you specify `--issues` without `--prs`, the command fetches the issue from GitHub and derives the title, type, and areas from the issue (using the same label mappings as for PRs). When both `--issues` and `--prs` are specified, `--use-issue-number` still uses the issue number for the filename: + +```sh +docs-builder changelog add \ + --issues https://github.com/elastic/elasticsearch/issues/12345 \ + --products "elasticsearch 9.2.3" \ + --config docs/changelog.yml \ + --use-issue-number +``` + +The command derives the title from the issue title, maps labels to type and areas (if configured), extracts release notes from the issue body, and extracts linked PRs (e.g., "Fixed by #123"). You can omit `--title` and `--type` when the issue has appropriate labels. Multiple issues can be specified comma-separated or via a file path (like `--prs`), creating one changelog per issue. + +This creates a file named `12345.yaml` (or `12345-12346.yaml` for multiple issues). :::{important} -When using `--use-pr-number`, you must also provide the `--pr` option. The PR number is extracted from the PR URL or number you provide. +`--use-pr-number` and `--use-issue-number` are mutually exclusive; specify only one. `--use-pr-number` requires `--prs`. `--use-issue-number` requires `--issues`. The numbers are extracted from the URLs or identifiers you provide. ::: ### Examples @@ -317,7 +331,7 @@ pivot: "ES|QL": ":Search Relevance/ES|QL" ``` -When you use the `--prs` option to derive information from a pull request, it can make use of those mappings: +When you use the `--prs` option to derive information from a pull request, it can make use of those mappings. Similarly, when you use the `--issues` option (without `--prs`), the command derives title, type, and areas from the GitHub issue labels using the same mappings: ```sh docs-builder changelog add \ @@ -328,7 +342,7 @@ docs-builder changelog add \ ``` In this case, the changelog file derives the title, type, and areas from the pull request. -The command also looks for patterns like `Fixes #123`, `Closes owner/repo#456`, `Resolves https://github.com/.../issues/789` in the pull request to derive its issues (unless you turn off this behavior in the changelog configuration file or use `--no-extract-issues`). +The command also looks for patterns like `Fixes #123`, `Closes owner/repo#456`, `Resolves https://github.com/.../issues/789` in the pull request to derive its issues. Similarly, when using `--issues`, the command extracts linked PRs from the issue body (for example, "Fixed by #123"). You can turn off this behavior in either case with the `--no-extract-issues` flag or by setting `extract.issues: false` in the changelog configuration file. The `extract.issues` setting applies to both directions: issues extracted from PR bodies (when using `--prs`) and PRs extracted from issue bodies (when using `--issues`). The `--strip-title-prefix` option in this example means that if the PR title has a prefix in square brackets (such as `[ES|QL]` or `[Security]`), it is automatically removed from the changelog title. Multiple square bracket prefixes are also supported (e.g., `[Discover][ESQL] Title` becomes `Title`). If a colon follows the closing bracket, it is also removed. @@ -474,6 +488,7 @@ You can specify only one of the following filter options: - `--all`: Include all changelogs from the directory. - `--input-products`: Include changelogs for the specified products. Refer to [Filter by product](#changelog-bundle-product). - `--prs`: Include changelogs for the specified pull request URLs or numbers, or a path to a newline-delimited file containing PR URLs or numbers. Go to [Filter by pull requests](#changelog-bundle-pr). +- `--issues`: Include changelogs for the specified issue URLs or numbers, or a path to a newline-delimited file containing issue URLs or numbers. Go to [Filter by issues](#changelog-bundle-issues). By default, the output file contains only the changelog file names and checksums. You can optionally use the `--resolve` command option to pull all of the content from each changelog into the bundle. @@ -577,6 +592,19 @@ entries: If you add the `--resolve` option, the contents of each changelog will be included in the output file. +### Filter by issues [changelog-bundle-issues] + +You can use the `--issues` option to create a bundle of changelogs that relate to those GitHub issues. +Provide either a comma-separated list of issues (`--issues "https://github.com/owner/repo/issues/123,456"`) or a path to a newline-delimited file (`--issues /path/to/file.txt`). +Issues can be identified by a full URL (such as `https://github.com/owner/repo/issues/123`), a short format (such as `owner/repo#123`), or just a number (in which case you must also provide `--owner` and `--repo` options). + +```sh +docs-builder changelog bundle --issues "12345,12346" \ + --repo elasticsearch \ + --owner elastic \ + --output-products "elasticsearch 9.2.2 ga" +``` + ### Filter by pull request file [changelog-bundle-file] If you have a file that lists pull requests (such as PRs associated with a GitHub release): @@ -620,7 +648,8 @@ entries: - product: elasticsearch areas: - Aggregations - pr: https://github.com/elastic/elasticsearch/pull/108875 + prs: + - https://github.com/elastic/elasticsearch/pull/108875 ... ``` @@ -743,13 +772,15 @@ For example, the `index.md` output file contains information derived from the ch * Convert BytesTransportResponse when proxying response from/to local node. [#135873](https://github.com/elastic/elastic/pull/135873) **Machine Learning** -* Fix ML calendar event update scalability issues. [#136886](https://github.com/elastic/elastic/pull/136886) +* Fix ML calendar event update scalability issues. [#136886](https://github.com/elastic/elastic/pull/136886) [#136900](https://github.com/elastic/elastic/pull/136900) **Aggregations** * Break on FieldData when building global ordinals. [#108875](https://github.com/elastic/elastic/pull/108875) ``` -To comment out the pull request and issue links, for example if they relate to a private repository, add `hide-links` to the `--input` option for that bundle. This allows you to selectively hide links per bundle when merging changelogs from multiple repositories. +When a changelog entry includes multiple values in its `prs` or `issues` arrays, all links are rendered inline for that entry, as shown in the Machine Learning example above. + +To comment out the pull request and issue links, for example if they relate to a private repository, add `hide-links` to the `--input` option for that bundle. This allows you to selectively hide links per bundle when merging changelogs from multiple repositories. When `hide-links` is set, all PR and issue links for affected entries are hidden together. If you have changelogs with `feature-id` values and you want them to be omitted from the output, use the `--hide-features` option. Feature IDs specified via `--hide-features` are **merged** with any `hide-features` already present in the bundle files. This means both CLI-specified and bundle-embedded features are hidden in the output. diff --git a/docs/syntax/changelog.md b/docs/syntax/changelog.md index 6a6b74817..d99d873b6 100644 --- a/docs/syntax/changelog.md +++ b/docs/syntax/changelog.md @@ -371,7 +371,13 @@ To add `hide-features` to a bundle, use the `--hide-features` option when runnin ## Private repository link hiding -PR and issue links are automatically hidden (commented out) for bundles from private repositories. This is determined by checking the `assembler.yml` configuration: +Changelog entries can reference multiple pull requests and issues via the `prs` and `issues` array fields. When an entry is rendered, all of its links are shown inline: + +```md +* Fix ML calendar event update scalability issues. [#136886](https://github.com/elastic/elastic/pull/136886) [#136900](https://github.com/elastic/elastic/pull/136900) +``` + +PR and issue links are automatically hidden (commented out) for bundles from private repositories. When links are hidden, **all** PR and issue links for an affected entry are hidden together. This is determined by checking the `assembler.yml` configuration: - Repositories marked with `private: true` in `assembler.yml` will have their links hidden - For merged bundles (e.g., `elasticsearch+kibana`), links are hidden if ANY component repository is private diff --git a/src/Elastic.Documentation.Configuration/ReleaseNotes/Bundle.cs b/src/Elastic.Documentation.Configuration/ReleaseNotes/Bundle.cs index 0483fcb15..f19c4ddc4 100644 --- a/src/Elastic.Documentation.Configuration/ReleaseNotes/Bundle.cs +++ b/src/Elastic.Documentation.Configuration/ReleaseNotes/Bundle.cs @@ -55,6 +55,7 @@ public sealed record BundledEntryDto public string? Subtype { get; set; } public List? Areas { get; set; } public string? Pr { get; set; } + public List? Prs { get; set; } public List? Issues { get; set; } } diff --git a/src/Elastic.Documentation.Configuration/ReleaseNotes/ChangelogEntry.cs b/src/Elastic.Documentation.Configuration/ReleaseNotes/ChangelogEntry.cs index 410dea606..33b7bd3b4 100644 --- a/src/Elastic.Documentation.Configuration/ReleaseNotes/ChangelogEntry.cs +++ b/src/Elastic.Documentation.Configuration/ReleaseNotes/ChangelogEntry.cs @@ -14,6 +14,7 @@ namespace Elastic.Documentation.Configuration.ReleaseNotes; public record ChangelogEntryDto { public string? Pr { get; set; } + public List? Prs { get; set; } public List? Issues { get; set; } public string? Type { get; set; } public string? Subtype { get; set; } diff --git a/src/Elastic.Documentation.Configuration/ReleaseNotes/ReleaseNotesSerialization.cs b/src/Elastic.Documentation.Configuration/ReleaseNotes/ReleaseNotesSerialization.cs index 73c64ce9c..9141b096e 100644 --- a/src/Elastic.Documentation.Configuration/ReleaseNotes/ReleaseNotesSerialization.cs +++ b/src/Elastic.Documentation.Configuration/ReleaseNotes/ReleaseNotesSerialization.cs @@ -106,7 +106,7 @@ public static string SerializeBundle(Bundle bundle) private static ChangelogEntry ToEntry(ChangelogEntryDto dto) => new() { - Pr = dto.Pr, + Prs = dto.Prs ?? (dto.Pr != null ? [dto.Pr] : null), Issues = dto.Issues, Type = ParseEntryType(dto.Type), Subtype = ParseEntrySubtype(dto.Subtype), @@ -122,7 +122,7 @@ public static string SerializeBundle(Bundle bundle) private static ChangelogEntry ToEntry(BundledEntry entry) => new() { - Pr = entry.Pr, + Prs = entry.Prs, Issues = entry.Issues, Type = entry.Type ?? ChangelogEntryType.Invalid, Subtype = entry.Subtype, @@ -171,7 +171,7 @@ public static string SerializeBundle(Bundle bundle) Highlight = dto.Highlight, Subtype = ParseEntrySubtype(dto.Subtype), Areas = dto.Areas, - Pr = dto.Pr, + Prs = dto.Prs ?? (dto.Pr != null ? [dto.Pr] : null), Issues = dto.Issues }; @@ -225,7 +225,7 @@ private static ChangelogEntryType ParseEntryType(string? value) private static ChangelogEntryDto ToDto(ChangelogEntry entry) => new() { - Pr = entry.Pr, + Prs = entry.Prs?.ToList(), Issues = entry.Issues?.ToList(), Type = EntryTypeToString(entry.Type), Subtype = EntrySubtypeToString(entry.Subtype), @@ -274,7 +274,7 @@ private static ChangelogEntryType ParseEntryType(string? value) Highlight = entry.Highlight, Subtype = EntrySubtypeToString(entry.Subtype), Areas = entry.Areas?.ToList(), - Pr = entry.Pr, + Prs = entry.Prs?.ToList(), Issues = entry.Issues?.ToList() }; diff --git a/src/Elastic.Documentation/ReleaseNotes/BundledEntry.cs b/src/Elastic.Documentation/ReleaseNotes/BundledEntry.cs index 16630e670..e33c2cf83 100644 --- a/src/Elastic.Documentation/ReleaseNotes/BundledEntry.cs +++ b/src/Elastic.Documentation/ReleaseNotes/BundledEntry.cs @@ -42,8 +42,8 @@ public record BundledEntry /// Areas affected by this changelog entry. public IReadOnlyList? Areas { get; init; } - /// Pull request URL or reference. - public string? Pr { get; init; } + /// Pull request URLs or references. + public IReadOnlyList? Prs { get; init; } /// Related issue URLs or references. public IReadOnlyList? Issues { get; init; } diff --git a/src/Elastic.Documentation/ReleaseNotes/ChangelogEntry.cs b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntry.cs index 47e40dd8b..28f8091ad 100644 --- a/src/Elastic.Documentation/ReleaseNotes/ChangelogEntry.cs +++ b/src/Elastic.Documentation/ReleaseNotes/ChangelogEntry.cs @@ -10,8 +10,8 @@ namespace Elastic.Documentation.ReleaseNotes; /// public record ChangelogEntry { - /// Pull request URL or reference. - public string? Pr { get; init; } + /// Pull request URLs or references. + public IReadOnlyList? Prs { get; init; } /// Related issue URLs or references. public IReadOnlyList? Issues { get; init; } @@ -63,7 +63,7 @@ public record ChangelogEntry Highlight = Highlight, Subtype = Subtype, Areas = Areas, - Pr = Pr, + Prs = Prs, Issues = Issues }; } diff --git a/src/Elastic.Documentation/ReleaseNotes/ChangelogTextUtilities.cs b/src/Elastic.Documentation/ReleaseNotes/ChangelogTextUtilities.cs index e6e84bea7..30cb1f062 100644 --- a/src/Elastic.Documentation/ReleaseNotes/ChangelogTextUtilities.cs +++ b/src/Elastic.Documentation/ReleaseNotes/ChangelogTextUtilities.cs @@ -166,6 +166,37 @@ public static string StripSquareBracketPrefix(string title) return null; } + /// + /// Extracts issue number from issue URL or reference. + /// + public static int? ExtractIssueNumber(string issueUrl, string? defaultOwner = null, string? defaultRepo = null) + { + if (issueUrl.StartsWith("https://github.com/", StringComparison.OrdinalIgnoreCase) || + issueUrl.StartsWith("http://github.com/", StringComparison.OrdinalIgnoreCase)) + { + var uri = new Uri(issueUrl); + var segments = uri.Segments; + if (segments.Length >= 5 && + segments[3].Equals("issues/", StringComparison.OrdinalIgnoreCase) && + int.TryParse(segments[4].TrimEnd('/'), out var issueNum)) + return issueNum; + } + + var hashIndex = issueUrl.LastIndexOf('#'); + if (hashIndex > 0 && hashIndex < issueUrl.Length - 1) + { + var issuePart = issueUrl[(hashIndex + 1)..]; + if (int.TryParse(issuePart, out var issueNum)) + return issueNum; + } + + if (int.TryParse(issueUrl, out var issueNumber) && + !string.IsNullOrWhiteSpace(defaultOwner) && !string.IsNullOrWhiteSpace(defaultRepo)) + return issueNumber; + + return null; + } + /// /// Formats PR link as markdown. /// diff --git a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs index acfa72b4d..44b557347 100644 --- a/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs +++ b/src/Elastic.Markdown/Myst/Directives/Changelog/ChangelogInlineRenderer.cs @@ -312,16 +312,14 @@ private static void RenderSingleEntry(StringBuilder sb, ChangelogEntry entry, st private static void RenderEntryLinks(StringBuilder sb, ChangelogEntry entry, string repo, bool hideLinks) { - var hasPr = !string.IsNullOrWhiteSpace(entry.Pr); if (hideLinks) { - // When hiding links, put them on separate lines as comments _ = sb.AppendLine(); - if (hasPr) + foreach (var pr in entry.Prs ?? []) { _ = sb.Append(" "); - _ = sb.AppendLine(ChangelogTextUtilities.FormatPrLink(entry.Pr!, repo, hidePrivateLinks: true)); + _ = sb.AppendLine(ChangelogTextUtilities.FormatPrLink(pr, repo, hidePrivateLinks: true)); } foreach (var issue in entry.Issues ?? []) { @@ -331,11 +329,10 @@ private static void RenderEntryLinks(StringBuilder sb, ChangelogEntry entry, str return; } - // Default: render links inline _ = sb.Append(' '); - if (hasPr) + foreach (var pr in entry.Prs ?? []) { - _ = sb.Append(ChangelogTextUtilities.FormatPrLink(entry.Pr!, repo, hidePrivateLinks: false)); + _ = sb.Append(ChangelogTextUtilities.FormatPrLink(pr, repo, hidePrivateLinks: false)); _ = sb.Append(' '); } foreach (var issue in entry.Issues ?? []) @@ -399,17 +396,16 @@ private static void RenderDetailedEntry(StringBuilder sb, ChangelogEntry entry, private static void RenderDetailedEntryLinks(StringBuilder sb, ChangelogEntry entry, string repo, bool hideLinks) { - var hasPr = !string.IsNullOrWhiteSpace(entry.Pr); + var hasPrs = entry.Prs is { Count: > 0 }; var hasIssues = entry.Issues is { Count: > 0 }; - if (!hasPr && !hasIssues) + if (!hasPrs && !hasIssues) return; if (hideLinks) { - // When hiding links, put them on separate lines as comments - if (hasPr) - _ = sb.AppendLine(ChangelogTextUtilities.FormatPrLink(entry.Pr!, repo, hidePrivateLinks: true)); + foreach (var pr in entry.Prs ?? []) + _ = sb.AppendLine(ChangelogTextUtilities.FormatPrLink(pr, repo, hidePrivateLinks: true)); foreach (var issue in entry.Issues ?? []) _ = sb.AppendLine(ChangelogTextUtilities.FormatIssueLink(issue, repo, hidePrivateLinks: true)); _ = sb.AppendLine("For more information, check the pull request or issue above."); @@ -417,14 +413,21 @@ private static void RenderDetailedEntryLinks(StringBuilder sb, ChangelogEntry en return; } - // Default: render links inline _ = sb.Append("For more information, check "); - if (hasPr) - _ = sb.Append(ChangelogTextUtilities.FormatPrLink(entry.Pr!, repo, hidePrivateLinks: false)); + var first = true; + foreach (var pr in entry.Prs ?? []) + { + if (!first) + _ = sb.Append(' '); + _ = sb.Append(ChangelogTextUtilities.FormatPrLink(pr, repo, hidePrivateLinks: false)); + first = false; + } foreach (var issue in entry.Issues ?? []) { - _ = sb.Append(' '); + if (!first) + _ = sb.Append(' '); _ = sb.Append(ChangelogTextUtilities.FormatIssueLink(issue, repo, hidePrivateLinks: false)); + first = false; } _ = sb.AppendLine("."); _ = sb.AppendLine(); diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs index 6ee592de4..22437087f 100644 --- a/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs +++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundleAmendService.cs @@ -236,7 +236,7 @@ private string GenerateAmendFilePath(string bundlePath, int amendNumber) Highlight = entry.Highlight, Subtype = entry.Subtype, Areas = entry.Areas, - Pr = entry.Pr, + Prs = entry.Prs, Issues = entry.Issues }; } diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs index 19bc0ec54..3a89a13ab 100644 --- a/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs +++ b/src/services/Elastic.Changelog/Bundling/ChangelogBundlingService.cs @@ -31,6 +31,7 @@ public record BundleChangelogsArguments public IReadOnlyList? OutputProducts { get; init; } public bool Resolve { get; init; } public string[]? Prs { get; init; } + public string[]? Issues { get; init; } public string? Owner { get; init; } public string? Repo { get; init; } @@ -83,6 +84,9 @@ public partial class ChangelogBundlingService( [GeneratedRegex(@"github\.com/([^/]+)/([^/]+)/pull/(\d+)", RegexOptions.IgnoreCase)] private static partial Regex GitHubPrUrlRegex(); + [GeneratedRegex(@"github\.com/([^/]+)/([^/]+)/issues/(\d+)", RegexOptions.IgnoreCase)] + private static partial Regex GitHubIssueUrlRegex(); + public async Task BundleChangelogs(IDiagnosticsCollector collector, BundleChangelogsArguments input, Cancel ctx) { try @@ -108,11 +112,26 @@ public async Task BundleChangelogs(IDiagnosticsCollector collector, Bundle if (!ValidateInput(collector, input)) return false; - // Load PR filter values - var prFilterLoader = new PrFilterLoader(_fileSystem); - var prFilterResult = await prFilterLoader.LoadPrsAsync(collector, input.Prs, input.Owner, input.Repo, ctx); - if (!prFilterResult.IsValid) - return false; + // Load PR or issue filter values + var prsToMatch = new HashSet(StringComparer.OrdinalIgnoreCase); + var issuesToMatch = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (input.Prs is { Length: > 0 }) + { + var prFilterLoader = new PrFilterLoader(_fileSystem); + var prFilterResult = await prFilterLoader.LoadPrsAsync(collector, input.Prs, input.Owner, input.Repo, ctx); + if (!prFilterResult.IsValid) + return false; + prsToMatch = prFilterResult.PrsToMatch; + } + else if (input.Issues is { Length: > 0 }) + { + var issueFilterLoader = new IssueFilterLoader(_fileSystem); + var issueFilterResult = await issueFilterLoader.LoadIssuesAsync(collector, input.Issues, input.Owner, input.Repo, ctx); + if (!issueFilterResult.IsValid) + return false; + issuesToMatch = issueFilterResult.IssuesToMatch; + } // Determine output path var outputPath = input.Output ?? _fileSystem.Path.Combine(input.Directory, "changelog-bundle.yaml"); @@ -130,7 +149,7 @@ public async Task BundleChangelogs(IDiagnosticsCollector collector, Bundle _logger.LogInformation("Found {Count} YAML files in directory", yamlFiles.Count); // Build filter criteria - var filterCriteria = BuildFilterCriteria(input, prFilterResult.PrsToMatch); + var filterCriteria = BuildFilterCriteria(input, prsToMatch, issuesToMatch); // Match changelog entries var entryMatcher = new ChangelogEntryMatcher(_fileSystem, ReleaseNotesSerialization.GetEntryDeserializer(), _logger); @@ -342,7 +361,7 @@ private bool ValidateInput(IDiagnosticsCollector collector, BundleChangelogsArgu return false; } - // Validate filter options + // Validate filter options - exactly one of: --all, --input-products, --prs, --issues var specifiedFilters = new List(); if (input.All) specifiedFilters.Add("--all"); @@ -350,23 +369,29 @@ private bool ValidateInput(IDiagnosticsCollector collector, BundleChangelogsArgu specifiedFilters.Add("--input-products"); if (input.Prs is { Length: > 0 }) specifiedFilters.Add("--prs"); + if (input.Issues is { Length: > 0 }) + specifiedFilters.Add("--issues"); if (specifiedFilters.Count == 0) { - collector.EmitError(string.Empty, "At least one filter option must be specified: --all, --input-products, or --prs"); + collector.EmitError(string.Empty, "At least one filter option must be specified: --all, --input-products, --prs, or --issues"); return false; } if (specifiedFilters.Count > 1) { - collector.EmitError(string.Empty, $"Multiple filter options cannot be specified together. You specified: {string.Join(", ", specifiedFilters)}. Please use only one filter option: --all, --input-products, or --prs"); + collector.EmitError(string.Empty, + $"Multiple filter options cannot be specified together. You specified: {string.Join(", ", specifiedFilters)}. Please use only one filter option: --all, --input-products, --prs, or --issues"); return false; } return true; } - private static ChangelogFilterCriteria BuildFilterCriteria(BundleChangelogsArguments input, HashSet prsToMatch) + private static ChangelogFilterCriteria BuildFilterCriteria( + BundleChangelogsArguments input, + HashSet prsToMatch, + HashSet issuesToMatch) { var productFilters = new List(); if (input.InputProducts is { Count: > 0 }) @@ -387,6 +412,7 @@ private static ChangelogFilterCriteria BuildFilterCriteria(BundleChangelogsArgum IncludeAll = input.All, ProductFilters = productFilters, PrsToMatch = prsToMatch, + IssuesToMatch = issuesToMatch, DefaultOwner = input.Owner, DefaultRepo = input.Repo }; @@ -505,4 +531,67 @@ internal static string NormalizePrForComparison(string pr, string? defaultOwner, // Return as-is for comparison (fallback) return pr.ToLowerInvariant(); } + + internal static string NormalizeIssueForComparison(string issue, string? defaultOwner, string? defaultRepo) + { + issue = issue.Trim(); + + if (issue.StartsWith("https://github.com/", StringComparison.OrdinalIgnoreCase) || + issue.StartsWith("http://github.com/", StringComparison.OrdinalIgnoreCase)) + { + var match = GitHubIssueUrlRegex().Match(issue); + if (match.Success && match.Groups.Count >= 4) + { + var owner = match.Groups[1].Value.Trim(); + var repo = match.Groups[2].Value.Trim(); + var issuePart = match.Groups[3].Value.Trim(); + if (!string.IsNullOrWhiteSpace(owner) && !string.IsNullOrWhiteSpace(repo) && + int.TryParse(issuePart, out var issueNum)) + return $"{owner}/{repo}#{issueNum}".ToLowerInvariant(); + } + + try + { + var uri = new Uri(issue); + var segments = uri.Segments; + if (segments.Length >= 5 && segments[3].Equals("issues/", StringComparison.OrdinalIgnoreCase)) + { + var owner = segments[1].TrimEnd('/').Trim(); + var repo = segments[2].TrimEnd('/').Trim(); + var issuePart = segments[4].TrimEnd('/').Trim(); + if (!string.IsNullOrWhiteSpace(owner) && !string.IsNullOrWhiteSpace(repo) && + int.TryParse(issuePart, out var issueNum)) + return $"{owner}/{repo}#{issueNum}".ToLowerInvariant(); + } + } + catch (UriFormatException) + { + // Fall through + } + } + + var hashIndex = issue.LastIndexOf('#'); + if (hashIndex > 0 && hashIndex < issue.Length - 1) + { + var repoPart = issue[..hashIndex].Trim(); + var issuePart = issue[(hashIndex + 1)..].Trim(); + if (int.TryParse(issuePart, out var issueNum)) + { + var repoParts = repoPart.Split('/'); + if (repoParts.Length == 2) + { + var owner = repoParts[0].Trim(); + var repo = repoParts[1].Trim(); + if (!string.IsNullOrWhiteSpace(owner) && !string.IsNullOrWhiteSpace(repo)) + return $"{owner}/{repo}#{issueNum}".ToLowerInvariant(); + } + } + } + + if (int.TryParse(issue, out var issueNumber) && + !string.IsNullOrWhiteSpace(defaultOwner) && !string.IsNullOrWhiteSpace(defaultRepo)) + return $"{defaultOwner}/{defaultRepo}#{issueNumber}".ToLowerInvariant(); + + return issue.ToLowerInvariant(); + } } diff --git a/src/services/Elastic.Changelog/Bundling/ChangelogEntryMatcher.cs b/src/services/Elastic.Changelog/Bundling/ChangelogEntryMatcher.cs index 7e1938d9e..52eb92c62 100644 --- a/src/services/Elastic.Changelog/Bundling/ChangelogEntryMatcher.cs +++ b/src/services/Elastic.Changelog/Bundling/ChangelogEntryMatcher.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information using System.IO.Abstractions; +using System.Linq; using Elastic.Documentation.Configuration.ReleaseNotes; using Elastic.Documentation.Diagnostics; using Elastic.Documentation.ReleaseNotes; @@ -28,27 +29,33 @@ public async Task MatchChangelogsAsync( { var changelogEntries = new List(); var matchedPrs = new HashSet(StringComparer.OrdinalIgnoreCase); - var seenChangelogs = new HashSet(); // For deduplication (using checksum) + var matchedIssues = new HashSet(StringComparer.OrdinalIgnoreCase); + var seenChangelogs = new HashSet(); foreach (var filePath in yamlFiles) { - var entry = await ProcessFileAsync(collector, filePath, criteria, seenChangelogs, matchedPrs, ctx); + var entry = await ProcessFileAsync(collector, filePath, criteria, seenChangelogs, matchedPrs, matchedIssues, ctx); if (entry != null) changelogEntries.Add(entry); } - // Warn about unmatched PRs if filtering by PRs if (criteria.PrsToMatch.Count > 0) { - var unmatchedPrs = criteria.PrsToMatch.Where(pr => !matchedPrs.Contains(pr)).ToList(); - foreach (var unmatchedPr in unmatchedPrs) - collector.EmitWarning(string.Empty, $"No changelog file found for PR: {unmatchedPr}"); + foreach (var pr in criteria.PrsToMatch.Where(pr => !matchedPrs.Contains(pr))) + collector.EmitWarning(string.Empty, $"No changelog file found for PR: {pr}"); + } + + if (criteria.IssuesToMatch.Count > 0) + { + foreach (var issue in criteria.IssuesToMatch.Where(issue => !matchedIssues.Contains(issue))) + collector.EmitWarning(string.Empty, $"No changelog file found for issue: {issue}"); } return new ChangelogMatchResult { Entries = changelogEntries, - MatchedPrs = matchedPrs + MatchedPrs = matchedPrs, + MatchedIssues = matchedIssues }; } @@ -58,6 +65,7 @@ public async Task MatchChangelogsAsync( ChangelogFilterCriteria criteria, HashSet seenChangelogs, HashSet matchedPrs, + HashSet matchedIssues, Cancel ctx) { try @@ -79,8 +87,7 @@ public async Task MatchChangelogsAsync( return null; } - // Apply filters using YAML DTO (string-based for wildcard matching) - if (!MatchesFilter(yamlDto, criteria, matchedPrs)) + if (!MatchesFilter(yamlDto, criteria, matchedPrs, matchedIssues)) return null; // Add to seen set @@ -114,7 +121,8 @@ public async Task MatchChangelogsAsync( private static bool MatchesFilter( ChangelogEntryDto data, ChangelogFilterCriteria criteria, - HashSet matchedPrs) + HashSet matchedPrs, + HashSet matchedIssues) { if (criteria.IncludeAll) return true; @@ -125,6 +133,9 @@ private static bool MatchesFilter( if (criteria.PrsToMatch.Count > 0) return MatchesPrFilter(data, criteria, matchedPrs); + if (criteria.IssuesToMatch.Count > 0) + return MatchesIssueFilter(data, criteria, matchedIssues); + return true; } @@ -157,18 +168,45 @@ private static bool MatchesPrFilter( ChangelogFilterCriteria criteria, HashSet matchedPrs) { - if (string.IsNullOrWhiteSpace(data.Pr)) + var prs = data.Prs ?? (data.Pr != null ? [data.Pr] : null); + if (prs is not { Count: > 0 }) return false; - // Normalize PR for comparison - var normalizedPr = ChangelogBundlingService.NormalizePrForComparison(data.Pr, criteria.DefaultOwner, criteria.DefaultRepo); - foreach (var pr in criteria.PrsToMatch) + foreach (var dataPr in prs.Where(pr => !string.IsNullOrWhiteSpace(pr))) { - var normalizedPrToMatch = ChangelogBundlingService.NormalizePrForComparison(pr, criteria.DefaultOwner, criteria.DefaultRepo); - if (normalizedPr == normalizedPrToMatch) + var normalizedPr = ChangelogBundlingService.NormalizePrForComparison(dataPr, criteria.DefaultOwner, criteria.DefaultRepo); + foreach (var pr in criteria.PrsToMatch) { - _ = matchedPrs.Add(pr); - return true; + var normalizedPrToMatch = ChangelogBundlingService.NormalizePrForComparison(pr, criteria.DefaultOwner, criteria.DefaultRepo); + if (normalizedPr == normalizedPrToMatch) + { + _ = matchedPrs.Add(pr); + return true; + } + } + } + + return false; + } + + private static bool MatchesIssueFilter(ChangelogEntryDto data, ChangelogFilterCriteria criteria, HashSet matchedIssues) + { + if (data.Issues is not { Count: > 0 }) + return false; + + foreach (var dataIssue in data.Issues) + { + if (string.IsNullOrWhiteSpace(dataIssue)) + continue; + var normalizedIssue = ChangelogBundlingService.NormalizeIssueForComparison(dataIssue, criteria.DefaultOwner, criteria.DefaultRepo); + foreach (var issue in criteria.IssuesToMatch) + { + var normalizedIssueToMatch = ChangelogBundlingService.NormalizeIssueForComparison(issue, criteria.DefaultOwner, criteria.DefaultRepo); + if (normalizedIssue == normalizedIssueToMatch) + { + _ = matchedIssues.Add(issue); + return true; + } } } @@ -203,6 +241,7 @@ public record ChangelogFilterCriteria public required bool IncludeAll { get; init; } public required IReadOnlyList ProductFilters { get; init; } public required HashSet PrsToMatch { get; init; } + public required HashSet IssuesToMatch { get; init; } public string? DefaultOwner { get; init; } public string? DefaultRepo { get; init; } } @@ -235,4 +274,5 @@ public record ChangelogMatchResult { public required IReadOnlyList Entries { get; init; } public required HashSet MatchedPrs { get; init; } + public required HashSet MatchedIssues { get; init; } } diff --git a/src/services/Elastic.Changelog/Bundling/IssueFilterLoader.cs b/src/services/Elastic.Changelog/Bundling/IssueFilterLoader.cs new file mode 100644 index 000000000..933dde00a --- /dev/null +++ b/src/services/Elastic.Changelog/Bundling/IssueFilterLoader.cs @@ -0,0 +1,218 @@ +// 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 System.IO.Abstractions; +using Elastic.Documentation.Diagnostics; + +namespace Elastic.Changelog.Bundling; + +/// +/// Service for loading issue filter values from files or command line. +/// +public class IssueFilterLoader(IFileSystem fileSystem) +{ + /// + /// Loads issue filter values from the provided input. + /// Values can be file paths, URLs, short format (owner/repo#number), or issue numbers. + /// + public async Task LoadIssuesAsync( + IDiagnosticsCollector collector, + string[]? issues, + string? owner, + string? repo, + Cancel ctx) + { + var issuesToMatch = new HashSet(StringComparer.OrdinalIgnoreCase); + + if (issues is not { Length: > 0 }) + { + return new IssueFilterResult + { + IsValid = true, + IssuesToMatch = issuesToMatch + }; + } + + var nonExistentFiles = new List(); + + if (issues.Length == 1) + { + var result = await ProcessSingleValueAsync(collector, issues[0], issuesToMatch, ctx); + if (!result) + { + return new IssueFilterResult + { + IsValid = false, + IssuesToMatch = issuesToMatch + }; + } + } + else + { + await ProcessMultipleValuesAsync(issuesToMatch, nonExistentFiles, issues, ctx); + + if (nonExistentFiles.Count > 0) + { + if (issuesToMatch.Count == 0) + { + collector.EmitError(nonExistentFiles[0], $"File does not exist: {nonExistentFiles[0]}"); + return new IssueFilterResult + { + IsValid = false, + IssuesToMatch = issuesToMatch + }; + } + + foreach (var file in nonExistentFiles) + collector.EmitWarning(file, $"File does not exist, skipping: {file}"); + } + } + + if (!ValidateNumericIssues(collector, issuesToMatch, owner, repo)) + { + return new IssueFilterResult + { + IsValid = false, + IssuesToMatch = issuesToMatch + }; + } + + return new IssueFilterResult + { + IsValid = true, + IssuesToMatch = issuesToMatch + }; + } + + private async Task ProcessSingleValueAsync( + IDiagnosticsCollector collector, + string singleValue, + HashSet issuesToMatch, + Cancel ctx) + { + var isUrl = IsUrl(singleValue); + + if (!isUrl && fileSystem.File.Exists(singleValue)) + { + await ReadIssuesFromFileAsync(singleValue, issuesToMatch, ctx); + return true; + } + + if (!isUrl) + { + if (IsShortFormat(singleValue)) + { + _ = issuesToMatch.Add(singleValue); + return true; + } + + if (LooksLikeFilePath(singleValue)) + { + collector.EmitError(singleValue, $"File does not exist: {singleValue}"); + return false; + } + + _ = issuesToMatch.Add(singleValue); + return true; + } + + _ = issuesToMatch.Add(singleValue); + return true; + } + + private async Task ProcessMultipleValuesAsync( + HashSet issuesToMatch, + List nonExistentFiles, + string[] values, + Cancel ctx) + { + foreach (var value in values) + { + var isUrl = IsUrl(value); + + if (!isUrl && fileSystem.File.Exists(value)) + { + await ReadIssuesFromFileAsync(value, issuesToMatch, ctx); + } + else if (isUrl) + { + _ = issuesToMatch.Add(value); + } + else if (IsShortFormat(value)) + { + _ = issuesToMatch.Add(value); + } + else if (LooksLikeFilePath(value)) + { + nonExistentFiles.Add(value); + } + else + { + _ = issuesToMatch.Add(value); + } + } + } + + private static bool ValidateNumericIssues( + IDiagnosticsCollector collector, + HashSet issuesToMatch, + string? owner, + string? repo) + { + var hasNumericOnly = issuesToMatch + .Where(issue => !IsUrl(issue) && !IsShortFormat(issue)) + .Any(issue => int.TryParse(issue, out _)); + + if (hasNumericOnly && (string.IsNullOrWhiteSpace(owner) || string.IsNullOrWhiteSpace(repo))) + { + collector.EmitError(string.Empty, + "When --issues contains issue numbers (not URLs or owner/repo#number format), both --owner and --repo must be provided"); + return false; + } + + return true; + } + + private async Task ReadIssuesFromFileAsync(string filePath, HashSet issuesToMatch, Cancel ctx) + { + var content = await fileSystem.File.ReadAllTextAsync(filePath, ctx); + var lines = content + .Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(p => !string.IsNullOrWhiteSpace(p)); + + foreach (var line in lines) + _ = issuesToMatch.Add(line); + } + + private static bool IsUrl(string value) => + value.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || + value.StartsWith("https://", StringComparison.OrdinalIgnoreCase); + + private static bool IsShortFormat(string value) + { + var hashIndex = value.LastIndexOf('#'); + if (hashIndex <= 0 || hashIndex >= value.Length - 1) + return false; + + var repoPart = value[..hashIndex]; + var numPart = value[(hashIndex + 1)..]; + var repoParts = repoPart.Split('/'); + + return repoParts.Length == 2 && int.TryParse(numPart, out _); + } + + private bool LooksLikeFilePath(string value) => + value.Contains(fileSystem.Path.DirectorySeparatorChar) || + value.Contains(fileSystem.Path.AltDirectorySeparatorChar) || + fileSystem.Path.HasExtension(value); +} + +/// +/// Result of loading issue filter values +/// +public record IssueFilterResult +{ + public required bool IsValid { get; init; } + public required HashSet IssuesToMatch { get; init; } +} diff --git a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs index 0edc2f5b8..6534d8977 100644 --- a/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs +++ b/src/services/Elastic.Changelog/Creation/ChangelogCreationService.cs @@ -36,6 +36,7 @@ public record CreateChangelogArguments public string? Output { get; init; } public string? Config { get; init; } public bool UsePrNumber { get; init; } + public bool UseIssueNumber { get; init; } public bool StripTitlePrefix { get; init; } public bool ExtractReleaseNotes { get; init; } public bool ExtractIssues { get; init; } @@ -45,16 +46,17 @@ public record CreateChangelogArguments /// Service for creating changelog entries /// public class ChangelogCreationService( - ILoggerFactory logFactory, - IConfigurationContext configurationContext, - IGitHubPrService? githubPrService = null, - IFileSystem? fileSystem = null +ILoggerFactory logFactory, +IConfigurationContext configurationContext, +IGitHubPrService? githubPrService = null, +IFileSystem? fileSystem = null ) : IService { private readonly ILogger _logger = logFactory.CreateLogger(); private readonly ChangelogConfigurationLoader _configLoader = new(logFactory, configurationContext, fileSystem ?? new FileSystem()); private readonly CreateChangelogArgumentsValidator _validator = new(configurationContext); private readonly PrInfoProcessor _prProcessor = new(githubPrService, logFactory.CreateLogger()); + private readonly IssueInfoProcessor _issueProcessor = new(githubPrService, logFactory.CreateLogger()); private readonly ChangelogFileWriter _fileWriter = new(fileSystem ?? new FileSystem(), logFactory.CreateLogger()); private readonly ProductInferService _productInferService = new( configurationContext.ProductsConfiguration); @@ -82,10 +84,29 @@ public async Task CreateChangelog(IDiagnosticsCollector collector, CreateC input = input with { Products = inferredProducts }; } - // Handle multiple PRs if provided (more than one PR) + // When --use-pr-number and multiple PRs, create one aggregated changelog + if (input.Prs is { Length: > 1 } && input.UsePrNumber) + return await CreateAggregatedChangelogAsync(collector, input, config, ctx); + + // Multiple PRs without --use-pr-number: one changelog per PR if (input.Prs != null && input.Prs.Length > 1) return await CreateChangelogsForMultiplePrsAsync(collector, input, config, ctx); + // Issues-only flow: derive from GitHub issues when no PRs + if (input.Issues is { Length: > 0 } && (input.Prs == null || input.Prs.Length == 0)) + { + // If all required fields are already provided, no GitHub derivation is needed. + // Create a single changelog file with all issues recorded as metadata. + var titleProvided = !string.IsNullOrWhiteSpace(input.Title); + var typeProvided = !string.IsNullOrWhiteSpace(input.Type); + if (titleProvided && typeProvided) + return await CreateSingleChangelogAsync(collector, input, config, ctx); + + if (input.Issues.Length > 1) + return await CreateChangelogsForMultipleIssuesAsync(collector, input, config, ctx); + return await CreateSingleChangelogFromIssueAsync(collector, input, config, ctx); + } + // Single PR or no PR - use existing logic return await CreateSingleChangelogAsync(collector, input, config, ctx); } @@ -226,7 +247,125 @@ private async Task CreateSingleChangelogAsync( collector, input, config, - prUrl, + string.IsNullOrWhiteSpace(input.Title), + string.IsNullOrWhiteSpace(input.Type), + ctx); + } + + private async Task CreateAggregatedChangelogAsync( + IDiagnosticsCollector collector, + CreateChangelogArguments input, + ChangelogConfiguration config, + Cancel ctx) + { + if (input.Prs == null || input.Prs.Length == 0) + return false; + + if (!_validator.ValidateMultiplePrFormat(collector, input.Prs, input.Owner, input.Repo)) + return false; + + // Check first PR for blockers + var firstPr = input.Prs[0].Trim(); + var (shouldSkip, _) = await _prProcessor.CheckPrForBlockersAsync( + collector, firstPr, input.Owner, input.Repo, input.Products, config, ctx); + + if (shouldSkip) + return true; + + // Process first PR for derived fields (title, type, areas, etc.) + var prResult = await _prProcessor.ProcessPrAsync(collector, input, config, firstPr, ctx); + if (prResult.ShouldSkip) + return true; + + if (prResult.DerivedFields != null) + input = ApplyDerivedFields(input, prResult.DerivedFields); + + if (!_validator.ValidateRequiredFields(collector, input, prResult.FetchFailed)) + return false; + + if (!_validator.ValidateAgainstConfiguration(collector, input, config)) + return false; + + // Write single changelog with all PRs (filename will be e.g. 137431-137432.yaml) + return await _fileWriter.WriteChangelogAsync( + collector, + input, + config, + string.IsNullOrWhiteSpace(input.Title), + string.IsNullOrWhiteSpace(input.Type), + ctx); + } + + private async Task CreateChangelogsForMultipleIssuesAsync( + IDiagnosticsCollector collector, + CreateChangelogArguments input, + ChangelogConfiguration config, + Cancel ctx) + { + if (input.Issues == null || input.Issues.Length == 0) + return false; + + if (!_validator.ValidateMultipleIssueFormat(collector, input.Issues, input.Owner, input.Repo)) + return false; + + var successCount = 0; + var skippedCount = 0; + + foreach (var issueUrl in input.Issues.Select(i => i.Trim()).Where(i => !string.IsNullOrWhiteSpace(i))) + { + var (shouldSkip, _) = await _issueProcessor.CheckIssueForBlockersAsync( + collector, issueUrl, input.Owner, input.Repo, input.Products, config, ctx); + + if (shouldSkip) + { + skippedCount++; + continue; + } + + var issueInput = input with { Issues = [issueUrl] }; + var result = await CreateSingleChangelogFromIssueAsync(collector, issueInput, config, ctx); + if (result) + successCount++; + } + + if (successCount == 0 && skippedCount == 0) + return false; + + _logger.LogInformation("Processed {SuccessCount} issue(s) successfully, skipped {SkippedCount} issue(s)", successCount, skippedCount); + return successCount > 0; + } + + private async Task CreateSingleChangelogFromIssueAsync( + IDiagnosticsCollector collector, + CreateChangelogArguments input, + ChangelogConfiguration config, + Cancel ctx) + { + var issueUrl = input.Issues is { Length: > 0 } ? input.Issues[0] : null; + + if (!_validator.ValidateIssueFormat(collector, issueUrl, input.Owner, input.Repo)) + return false; + + var issueResult = await _issueProcessor.ProcessIssueAsync(collector, input, config, issueUrl!, ctx); + + if (issueResult.ShouldSkip) + return true; + + if (issueResult.DerivedFields != null) + input = ApplyDerivedFields(input, issueResult.DerivedFields); + else if (!issueResult.FetchFailed) + return false; + + if (!_validator.ValidateRequiredFields(collector, input, issueResult.FetchFailed, fromIssue: true)) + return false; + + if (!_validator.ValidateAgainstConfiguration(collector, input, config)) + return false; + + return await _fileWriter.WriteChangelogAsync( + collector, + input, + config, string.IsNullOrWhiteSpace(input.Title), string.IsNullOrWhiteSpace(input.Type), ctx); @@ -243,6 +382,7 @@ input with Description = derived.Description != null && string.IsNullOrWhiteSpace(input.Description) ? derived.Description : input.Description, Areas = derived.Areas != null && (input.Areas == null || input.Areas.Length == 0) ? derived.Areas : input.Areas, Highlight = derived.Highlight ?? input.Highlight, - Issues = derived.Issues != null && (input.Issues == null || input.Issues.Length == 0) ? derived.Issues : input.Issues + Issues = derived.Issues != null && (input.Issues == null || input.Issues.Length == 0) ? derived.Issues : input.Issues, + Prs = derived.Prs != null && (input.Prs == null || input.Prs.Length == 0) ? derived.Prs : input.Prs }; } diff --git a/src/services/Elastic.Changelog/Creation/ChangelogFileWriter.cs b/src/services/Elastic.Changelog/Creation/ChangelogFileWriter.cs index 9c15af8e4..3e0dc3159 100644 --- a/src/services/Elastic.Changelog/Creation/ChangelogFileWriter.cs +++ b/src/services/Elastic.Changelog/Creation/ChangelogFileWriter.cs @@ -25,13 +25,12 @@ public async Task WriteChangelogAsync( IDiagnosticsCollector collector, CreateChangelogArguments input, ChangelogConfiguration config, - string? prUrl, bool titleMissing, bool typeMissing, Cancel ctx) { // Build changelog data from input - var changelogData = BuildChangelogData(input, prUrl); + var changelogData = BuildChangelogData(input); // Generate YAML file var yamlContent = GenerateYaml(changelogData, config, titleMissing, typeMissing); @@ -42,7 +41,7 @@ public async Task WriteChangelogAsync( _ = fileSystem.Directory.CreateDirectory(outputDir); // Generate filename - var filename = GenerateFilename(collector, input, prUrl); + var filename = GenerateFilename(collector, input); var filePath = fileSystem.Path.Combine(outputDir, filename); // Write file with explicit UTF-8 encoding to ensure proper character handling @@ -52,30 +51,55 @@ public async Task WriteChangelogAsync( return true; } - private string GenerateFilename(IDiagnosticsCollector collector, CreateChangelogArguments input, string? prUrl) + private string GenerateFilename(IDiagnosticsCollector collector, CreateChangelogArguments input) { - if (input.UsePrNumber && !string.IsNullOrWhiteSpace(prUrl)) + if (input.UsePrNumber && input.Prs is { Length: > 0 }) { - // Use PR number as filename when --use-pr-number is specified - var prNumber = ChangelogTextUtilities.ExtractPrNumber(prUrl, input.Owner, input.Repo); - if (prNumber.HasValue) - return $"{prNumber.Value}.yaml"; + var numbers = input.Prs + .Select(pr => ChangelogTextUtilities.ExtractPrNumber(pr, input.Owner, input.Repo)) + .Where(n => n.HasValue) + .Select(n => n!.Value) + .Distinct() + .OrderBy(n => n) + .ToList(); + + if (numbers.Count > 0) + return $"{string.Join("-", numbers)}.yaml"; + + collector.EmitWarning(string.Empty, $"Failed to extract PR numbers from PRs. Falling back to timestamp-based filename."); + } - // Fall back to timestamp-slug format if PR number extraction fails - collector.EmitWarning(string.Empty, $"Failed to extract PR number from '{prUrl}'. Falling back to timestamp-based filename."); + if (input.UseIssueNumber && input.Issues is { Length: > 0 }) + { + var numbers = input.Issues + .Select(issue => ChangelogTextUtilities.ExtractIssueNumber(issue, input.Owner, input.Repo)) + .Where(n => n.HasValue) + .Select(n => n!.Value) + .Distinct() + .OrderBy(n => n) + .ToList(); + + if (numbers.Count > 0) + return $"{string.Join("-", numbers)}.yaml"; + + collector.EmitWarning(string.Empty, "Failed to extract issue numbers from issues. Falling back to timestamp-based filename."); } // Default: timestamp-slug.yaml var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); + var firstPr = input.Prs is { Length: > 0 } ? input.Prs[0] : null; + var firstIssue = input.Issues is { Length: > 0 } ? input.Issues[0] : null; var slug = string.IsNullOrWhiteSpace(input.Title) - ? string.IsNullOrWhiteSpace(prUrl) - ? "changelog" - : $"pr-{prUrl.Replace("/", "-").Replace(":", "-")}" + ? firstPr != null + ? $"pr-{firstPr.Replace("/", "-").Replace(":", "-")}" + : firstIssue != null + ? $"issue-{firstIssue.Replace("/", "-").Replace(":", "-")}" + : "changelog" : ChangelogTextUtilities.SanitizeFilename(input.Title); return $"{timestamp}-{slug}.yaml"; } - private static ChangelogEntry BuildChangelogData(CreateChangelogArguments input, string? prUrl) + private static ChangelogEntry BuildChangelogData(CreateChangelogArguments input) { var entryType = ChangelogEntryTypeExtensions.TryParse(input.Type, out var parsed, ignoreCase: true, allowMatchingMetadataAttribute: true) ? parsed @@ -97,7 +121,7 @@ private static ChangelogEntry BuildChangelogData(CreateChangelogArguments input, Action = input.Action, FeatureId = input.FeatureId, Highlight = input.Highlight, - Pr = prUrl ?? (input.Prs != null && input.Prs.Length > 0 ? input.Prs[0] : null), + Prs = input.Prs is { Length: > 0 } ? input.Prs.ToList() : null, Products = input.Products.Select(p => p.ToProductReference()).ToList(), Areas = input.Areas is { Length: > 0 } ? input.Areas.ToList() : null, Issues = input.Issues is { Length: > 0 } ? input.Issues.ToList() : null @@ -216,8 +240,8 @@ private static string GenerateYaml(ChangelogEntry data, ChangelogConfiguration c # An optional array of strings that contain the issues that are # relevant to the PR. - # pr: - # An optional string that contains the pull request number. + # prs: + # An optional array of strings that contain the pull request numbers. # subtype: # An optional string that applies only to breaking changes. diff --git a/src/services/Elastic.Changelog/Creation/CreateChangelogArgumentsValidator.cs b/src/services/Elastic.Changelog/Creation/CreateChangelogArgumentsValidator.cs index bc0c8feb7..3a3eda3d6 100644 --- a/src/services/Elastic.Changelog/Creation/CreateChangelogArgumentsValidator.cs +++ b/src/services/Elastic.Changelog/Creation/CreateChangelogArgumentsValidator.cs @@ -46,12 +46,44 @@ public bool ValidateMultiplePrFormat(IDiagnosticsCollector collector, string[] p } /// - /// Validates required fields after PR processing. + /// Validates that if an issue is just a number, owner and repo must be provided. + /// + public bool ValidateIssueFormat(IDiagnosticsCollector collector, string? issueUrl, string? owner, string? repo) + { + if (!string.IsNullOrWhiteSpace(issueUrl) + && int.TryParse(issueUrl.Trim(), out _) + && (string.IsNullOrWhiteSpace(owner) || string.IsNullOrWhiteSpace(repo))) + { + collector.EmitError(string.Empty, "When --issues is specified as just a number, both --owner and --repo must be provided"); + return false; + } + + return true; + } + + /// + /// Validates that if all issues are just numbers, owner and repo must be provided. + /// + public bool ValidateMultipleIssueFormat(IDiagnosticsCollector collector, string[] issues, string? owner, string? repo) + { + var allAreNumbers = issues.All(i => int.TryParse(i.Trim(), out _)); + if (allAreNumbers && (string.IsNullOrWhiteSpace(owner) || string.IsNullOrWhiteSpace(repo))) + { + collector.EmitError(string.Empty, "When --issues contains only numbers, both --owner and --repo must be provided"); + return false; + } + + return true; + } + + /// + /// Validates required fields after PR or issue processing. /// public bool ValidateRequiredFields( IDiagnosticsCollector collector, CreateChangelogArguments input, - bool prFetchFailed) + bool prFetchFailed, + bool fromIssue = false) { // Validate title if (string.IsNullOrWhiteSpace(input.Title)) @@ -60,7 +92,8 @@ public bool ValidateRequiredFields( collector.EmitWarning(string.Empty, "Title is missing. The changelog will be created with title commented out. Please manually update the title field."); else { - collector.EmitError(string.Empty, "Title is required. Provide --title or specify --prs to derive it from the PR."); + var titleHint = fromIssue ? "specify --issues to derive it from the issue" : "specify --prs or --issues to derive it"; + collector.EmitError(string.Empty, $"Title is required. Provide --title or {titleHint}."); return false; } } @@ -72,7 +105,8 @@ public bool ValidateRequiredFields( collector.EmitWarning(string.Empty, "Type is missing. The changelog will be created with type commented out. Please manually update the type field."); else { - collector.EmitError(string.Empty, "Type is required. Provide --type or specify --prs to derive it from PR labels (requires pivot.types mapping in changelog.yml)."); + var source = fromIssue ? "issue" : "PR"; + collector.EmitError(string.Empty, $"Type is required. Provide --type or specify --prs/--issues to derive it from {source} labels (requires pivot.types mapping in changelog.yml)."); return false; } } diff --git a/src/services/Elastic.Changelog/Creation/IssueInfoProcessor.cs b/src/services/Elastic.Changelog/Creation/IssueInfoProcessor.cs new file mode 100644 index 000000000..e0c1e23ea --- /dev/null +++ b/src/services/Elastic.Changelog/Creation/IssueInfoProcessor.cs @@ -0,0 +1,255 @@ +// 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 Elastic.Changelog.GitHub; +using Elastic.Documentation.Configuration.Changelog; +using Elastic.Documentation.Diagnostics; +using Elastic.Documentation.ReleaseNotes; +using Microsoft.Extensions.Logging; + +namespace Elastic.Changelog.Creation; + +/// +/// Service for fetching and processing GitHub issue information for changelog derivation +/// +public class IssueInfoProcessor(IGitHubPrService? githubService, ILogger logger) +{ + /// + /// Fetches issue information and derives changelog fields from it. + /// + public async Task ProcessIssueAsync( + IDiagnosticsCollector collector, + CreateChangelogArguments input, + ChangelogConfiguration config, + string issueUrl, + Cancel ctx) + { + var issueInfo = await TryFetchIssueInfoAsync(issueUrl, input.Owner, input.Repo, ctx); + + if (issueInfo == null) + { + collector.EmitWarning(string.Empty, $"Failed to fetch issue information from GitHub for issue: {issueUrl}. Generating basic changelog with provided values."); + return new IssueProcessingResult + { + FetchFailed = true, + ShouldSkip = false + }; + } + + if (ShouldSkipIssueDueToLabelBlockers(issueInfo.Labels.ToArray(), input.Products, config, collector, issueUrl)) + { + return new IssueProcessingResult + { + FetchFailed = false, + ShouldSkip = true + }; + } + + var derivedFields = DeriveFieldsFromIssue(collector, input, config, issueInfo, issueUrl); + + return new IssueProcessingResult + { + FetchFailed = false, + ShouldSkip = false, + DerivedFields = derivedFields, + IssueInfo = issueInfo + }; + } + + /// + /// Checks if an issue should be skipped due to label blockers + /// + public async Task<(bool shouldSkip, GitHubIssueInfo? issueInfo)> CheckIssueForBlockersAsync( + IDiagnosticsCollector collector, + string issueUrl, + string? owner, + string? repo, + IReadOnlyList products, + ChangelogConfiguration config, + Cancel ctx) + { + var issueInfo = await TryFetchIssueInfoAsync(issueUrl, owner, repo, ctx); + + if (issueInfo == null) + { + collector.EmitWarning(string.Empty, $"Failed to fetch issue information from GitHub for issue: {issueUrl}. Generating basic changelog with provided values."); + return (false, null); + } + + var shouldSkip = ShouldSkipIssueDueToLabelBlockers(issueInfo.Labels.ToArray(), products, config, collector, issueUrl); + return (shouldSkip, issueInfo); + } + + private DerivedPrFields? DeriveFieldsFromIssue( + IDiagnosticsCollector collector, + CreateChangelogArguments input, + ChangelogConfiguration config, + GitHubIssueInfo issueInfo, + string issueUrl) + { + var derived = new DerivedPrFields(); + + if (input.ExtractReleaseNotes) + { + var (releaseNoteTitle, releaseNoteDescription) = ReleaseNotesExtractor.ExtractReleaseNotes(issueInfo.Body); + + if (releaseNoteTitle != null && string.IsNullOrWhiteSpace(input.Title)) + { + derived.Title = releaseNoteTitle; + logger.LogInformation("Using extracted release note as title: {Title}", derived.Title); + } + + if (releaseNoteDescription != null && string.IsNullOrWhiteSpace(input.Description)) + { + derived.Description = releaseNoteDescription; + logger.LogInformation("Using extracted release note as description (length: {Length} characters)", releaseNoteDescription.Length); + } + } + + if (string.IsNullOrWhiteSpace(input.Title) && derived.Title == null) + { + if (string.IsNullOrWhiteSpace(issueInfo.Title)) + { + collector.EmitError(string.Empty, $"Issue {issueUrl} does not have a title. Please provide --title or ensure the issue has a title."); + return null; + } + + var issueTitle = issueInfo.Title; + if (input.StripTitlePrefix) + issueTitle = ChangelogTextUtilities.StripSquareBracketPrefix(issueTitle); + derived.Title = issueTitle; + logger.LogInformation("Using issue title: {Title}", derived.Title); + } + else if (!string.IsNullOrWhiteSpace(input.Title)) + logger.LogDebug("Using explicitly provided title, ignoring issue title"); + + if (string.IsNullOrWhiteSpace(input.Type)) + { + if (config.LabelToType == null || config.LabelToType.Count == 0) + { + collector.EmitError(string.Empty, $"Cannot derive type from issue {issueUrl} labels: no type mapping configured in changelog.yml. Please provide --type or configure pivot.types in changelog.yml."); + return null; + } + + var mappedType = PrInfoProcessor.MapLabelsToType(issueInfo.Labels.ToArray(), config.LabelToType); + if (mappedType == null) + { + var availableLabels = issueInfo.Labels.Count > 0 ? string.Join(", ", issueInfo.Labels) : "none"; + collector.EmitError(string.Empty, $"Cannot derive type from issue {issueUrl} labels ({availableLabels}). No matching label found in type mapping. Please provide --type or add pivot.types with labels in changelog.yml."); + return null; + } + derived.Type = mappedType; + logger.LogInformation("Mapped issue labels to type: {Type}", derived.Type); + } + else + logger.LogDebug("Using explicitly provided type, ignoring issue labels"); + + if ((input.Areas == null || input.Areas.Length == 0) && config.LabelToAreas != null) + { + var mappedAreas = PrInfoProcessor.MapLabelsToAreas(issueInfo.Labels.ToArray(), config.LabelToAreas); + if (mappedAreas.Count > 0) + { + derived.Areas = mappedAreas.ToArray(); + logger.LogInformation("Mapped issue labels to areas: {Areas}", string.Join(", ", mappedAreas)); + } + } + else if (input.Areas is { Length: > 0 }) + logger.LogDebug("Using explicitly provided areas, ignoring issue labels"); + + if (input.Highlight == null && config.HighlightLabels is { Count: > 0 }) + { + var hasHighlightLabel = issueInfo.Labels.Any(label => + config.HighlightLabels.Contains(label, StringComparer.OrdinalIgnoreCase)); + if (hasHighlightLabel) + { + derived.Highlight = true; + logger.LogInformation("Issue has highlight label, setting highlight: true"); + } + } + else if (input.Highlight != null) + logger.LogDebug("Using explicitly provided highlight value, ignoring issue labels"); + + // Include the current issue in Issues array + derived.Issues = input.Issues is { Length: > 0 } + ? input.Issues + : [issueUrl]; + + // Extract linked PRs from issue body + if (input.ExtractIssues && issueInfo.LinkedPrs.Count > 0) + { + derived.Prs = issueInfo.LinkedPrs.ToArray(); + logger.LogInformation("Extracted {Count} linked PRs from issue body: {Prs}", + issueInfo.LinkedPrs.Count, string.Join(", ", issueInfo.LinkedPrs)); + } + + return derived; + } + + private bool ShouldSkipIssueDueToLabelBlockers( + string[] issueLabels, + IReadOnlyList products, + ChangelogConfiguration config, + IDiagnosticsCollector collector, + string issueUrl) + { + var createRules = config.Rules?.Create; + if (createRules == null) + return false; + + if (createRules.ByProduct is { Count: > 0 }) + { + foreach (var product in products) + { + var normalizedProductId = product.Product?.Replace('_', '-') ?? string.Empty; + if (createRules.ByProduct.TryGetValue(normalizedProductId, out var productRules)) + { + if (PrInfoProcessor.ShouldSkipByCreateRules(issueLabels, productRules, collector, issueUrl, product.Product)) + return true; + } + else if (PrInfoProcessor.ShouldSkipByCreateRules(issueLabels, createRules, collector, issueUrl, null)) + return true; + } + return false; + } + + return PrInfoProcessor.ShouldSkipByCreateRules(issueLabels, createRules, collector, issueUrl, null); + } + + private async Task TryFetchIssueInfoAsync(string? issueUrl, string? owner, string? repo, Cancel ctx) + { + if (string.IsNullOrWhiteSpace(issueUrl) || githubService == null) + return null; + + try + { + var issueInfo = await githubService.FetchIssueInfoAsync(issueUrl, owner, repo, ctx); + if (issueInfo != null) + logger.LogInformation("Successfully fetched issue information from GitHub"); + else + logger.LogWarning("Unable to fetch issue information from GitHub. Continuing with provided values."); + return issueInfo; + } + catch (Exception ex) + { + if (ex is OutOfMemoryException or + StackOverflowException or + AccessViolationException or + ThreadAbortException) + throw; + logger.LogWarning(ex, "Error fetching issue information from GitHub. Continuing with provided values."); + return null; + } + } +} + +/// +/// Result of processing issue information +/// +public record IssueProcessingResult +{ + public required bool FetchFailed { get; init; } + public required bool ShouldSkip { get; init; } + public DerivedPrFields? DerivedFields { get; init; } + public GitHubIssueInfo? IssueInfo { get; init; } +} diff --git a/src/services/Elastic.Changelog/Creation/PrInfoProcessor.cs b/src/services/Elastic.Changelog/Creation/PrInfoProcessor.cs index c62879f54..4364c71fd 100644 --- a/src/services/Elastic.Changelog/Creation/PrInfoProcessor.cs +++ b/src/services/Elastic.Changelog/Creation/PrInfoProcessor.cs @@ -229,7 +229,7 @@ private bool ShouldSkipPrDueToLabelBlockers( return ShouldSkipByCreateRules(prLabels, createRules, collector, prUrl, null); } - private static bool ShouldSkipByCreateRules( + internal static bool ShouldSkipByCreateRules( string[] prLabels, CreateRules rules, IDiagnosticsCollector collector, @@ -307,11 +307,11 @@ AccessViolationException or } } - private static string? MapLabelsToType(string[] labels, IReadOnlyDictionary labelToTypeMapping) => labels + internal static string? MapLabelsToType(string[] labels, IReadOnlyDictionary labelToTypeMapping) => labels .Select(label => labelToTypeMapping.TryGetValue(label, out var mappedType) ? mappedType : null) .FirstOrDefault(mappedType => mappedType != null); - private static List MapLabelsToAreas(string[] labels, IReadOnlyDictionary labelToAreasMapping) + internal static List MapLabelsToAreas(string[] labels, IReadOnlyDictionary labelToAreasMapping) { var areas = new HashSet(); var areaList = labels @@ -336,7 +336,7 @@ public record PrProcessingResult } /// -/// Fields derived from PR information +/// Fields derived from PR or issue information /// public record DerivedPrFields { @@ -346,4 +346,9 @@ public record DerivedPrFields public string[]? Areas { get; set; } public bool? Highlight { get; set; } public string[]? Issues { get; set; } + + /// + /// Linked PRs derived from issue body (when creating changelog from --issues) + /// + public string[]? Prs { get; set; } } diff --git a/src/services/Elastic.Changelog/GitHub/GitHubIssueInfo.cs b/src/services/Elastic.Changelog/GitHub/GitHubIssueInfo.cs new file mode 100644 index 000000000..19cf15989 --- /dev/null +++ b/src/services/Elastic.Changelog/GitHub/GitHubIssueInfo.cs @@ -0,0 +1,20 @@ +// 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.Changelog.GitHub; + +/// +/// Information about a GitHub issue +/// +public record GitHubIssueInfo +{ + public string Title { get; set; } = ""; + public string Body { get; set; } = ""; + public IReadOnlyList Labels { get; set; } = []; + + /// + /// Linked pull requests extracted from issue body (e.g., "Fixed by #123", "PR #456") + /// + public IReadOnlyList LinkedPrs { get; set; } = []; +} diff --git a/src/services/Elastic.Changelog/GitHub/GitHubPrService.cs b/src/services/Elastic.Changelog/GitHub/GitHubPrService.cs index c3c0d38d2..6f0d494ae 100644 --- a/src/services/Elastic.Changelog/GitHub/GitHubPrService.cs +++ b/src/services/Elastic.Changelog/GitHub/GitHubPrService.cs @@ -178,6 +178,143 @@ private static IReadOnlyList ExtractLinkedIssues(string body, string prO return [.. issues]; } + /// + /// Fetches issue information from GitHub + /// + public async Task FetchIssueInfoAsync(string issueUrl, string? owner = null, string? repo = null, CancellationToken ctx = default) + { + try + { + var (parsedOwner, parsedRepo, issueNumber) = ParseIssueUrl(issueUrl, owner, repo); + if (parsedOwner == null || parsedRepo == null || issueNumber == null) + { + _logger.LogWarning("Unable to parse issue URL: {IssueUrl}. Owner: {Owner}, Repo: {Repo}", issueUrl, owner, repo); + return null; + } + + var githubToken = Environment.GetEnvironmentVariable("GITHUB_TOKEN"); + using var request = new HttpRequestMessage(HttpMethod.Get, $"https://api.github.com/repos/{parsedOwner}/{parsedRepo}/issues/{issueNumber}"); + if (!string.IsNullOrEmpty(githubToken)) + request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", githubToken); + + _logger.LogDebug("Fetching issue info from: {ApiUrl}", request.RequestUri); + + var response = await HttpClient.SendAsync(request, ctx); + if (!response.IsSuccessStatusCode) + { + _logger.LogWarning("Failed to fetch issue info. Status: {StatusCode}, Reason: {ReasonPhrase}", response.StatusCode, response.ReasonPhrase); + return null; + } + + var jsonContent = await response.Content.ReadAsStringAsync(ctx); + var issueData = JsonSerializer.Deserialize(jsonContent, GitHubPrJsonContext.Default.GitHubIssueResponse); + + if (issueData == null) + { + _logger.LogWarning("Failed to deserialize issue response"); + return null; + } + + var linkedPrs = ExtractLinkedPrs(issueData.Body ?? string.Empty, parsedOwner, parsedRepo); + + return new GitHubIssueInfo + { + Title = issueData.Title, + Body = issueData.Body ?? string.Empty, + Labels = issueData.Labels?.Select(l => l.Name).ToList() ?? [], + LinkedPrs = linkedPrs + }; + } + catch (HttpRequestException ex) + { + _logger.LogWarning(ex, "HTTP error fetching issue info from GitHub"); + return null; + } + catch (TaskCanceledException) + { + _logger.LogWarning("Request timeout fetching issue info from GitHub"); + return null; + } + catch (Exception ex) when (ex is not (OutOfMemoryException or StackOverflowException or ThreadAbortException)) + { + _logger.LogWarning(ex, "Unexpected error fetching issue info from GitHub"); + return null; + } + } + + private static (string? owner, string? repo, int? issueNumber) ParseIssueUrl(string issueUrl, string? defaultOwner = null, string? defaultRepo = null) + { + if (issueUrl.StartsWith("https://github.com/", StringComparison.OrdinalIgnoreCase) || + issueUrl.StartsWith("http://github.com/", StringComparison.OrdinalIgnoreCase)) + { + var uri = new Uri(issueUrl); + var segments = uri.Segments; + if (segments.Length >= 5 && segments[3].Equals("issues/", StringComparison.OrdinalIgnoreCase)) + { + var owner = segments[1].TrimEnd('/'); + var repo = segments[2].TrimEnd('/'); + if (int.TryParse(segments[4], out var issueNum)) + return (owner, repo, issueNum); + } + } + + var hashIndex = issueUrl.LastIndexOf('#'); + if (hashIndex > 0 && hashIndex < issueUrl.Length - 1) + { + var repoPart = issueUrl[..hashIndex]; + var issuePart = issueUrl[(hashIndex + 1)..]; + if (int.TryParse(issuePart, out var issueNum)) + { + var repoParts = repoPart.Split('/'); + if (repoParts.Length == 2) + return (repoParts[0], repoParts[1], issueNum); + } + } + + if (int.TryParse(issueUrl, out var issueNumber) && + !string.IsNullOrWhiteSpace(defaultOwner) && !string.IsNullOrWhiteSpace(defaultRepo)) + return (defaultOwner, defaultRepo, issueNumber); + + return (null, null, null); + } + + /// + /// Extracts linked PRs from issue body. + /// Matches patterns like "Fixed by #123", "PR #456", "https://github.com/owner/repo/pull/789" + /// + private static IReadOnlyList ExtractLinkedPrs(string body, string issueOwner, string issueRepo) + { + if (string.IsNullOrWhiteSpace(body)) + return []; + + var prs = new HashSet(StringComparer.OrdinalIgnoreCase); + + // Full GitHub PR URL + foreach (Match match in MyRegex().Matches(body)) + _ = prs.Add(match.Value); + + // Cross-repo: owner/repo#123 in context of PR (e.g., "Fixed by owner/repo#123") + var crossRepoPattern = @"(?:fixed\s+by|pr|merge[sd]?|via)\s+([a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+)#(\d+)"; + foreach (Match match in Regex.Matches(body, crossRepoPattern, RegexOptions.IgnoreCase)) + { + var repoPath = match.Groups[1].Value; + var prNum = match.Groups[2].Value; + _ = prs.Add($"https://github.com/{repoPath}/pull/{prNum}"); + } + + // Same-repo: #123 + var sameRepoPattern = @"(?:fixed\s+by|pr|merge[sd]?|via)\s+#(\d+)"; + foreach (var prNum in Enumerable + .Select( + Enumerable.Cast(Regex.Matches(body, sameRepoPattern, RegexOptions.IgnoreCase)), + match => match.Groups[1].Value)) + { + _ = prs.Add($"https://github.com/{issueOwner}/{issueRepo}/pull/{prNum}"); + } + + return [.. prs]; + } + private sealed class GitHubPrResponse { public string Title { get; set; } = string.Empty; @@ -185,6 +322,13 @@ private sealed class GitHubPrResponse public List? Labels { get; set; } } + private sealed class GitHubIssueResponse + { + public string Title { get; set; } = string.Empty; + public string Body { get; set; } = string.Empty; + public List? Labels { get; set; } + } + private sealed class GitHubLabel { public string Name { get; set; } = string.Empty; @@ -192,7 +336,11 @@ private sealed class GitHubLabel [JsonSourceGenerationOptions(PropertyNameCaseInsensitive = true)] [JsonSerializable(typeof(GitHubPrResponse))] + [JsonSerializable(typeof(GitHubIssueResponse))] [JsonSerializable(typeof(GitHubLabel))] [JsonSerializable(typeof(List))] private sealed partial class GitHubPrJsonContext : JsonSerializerContext; + + [GeneratedRegex(@"https://github\.com/([a-zA-Z0-9_-]+/[a-zA-Z0-9_-]+)/pull/(\d+)", RegexOptions.IgnoreCase, "en-CA")] + private static partial Regex MyRegex(); } diff --git a/src/services/Elastic.Changelog/GitHub/IGitHubPrService.cs b/src/services/Elastic.Changelog/GitHub/IGitHubPrService.cs index 41d24ba4d..ff2bc5553 100644 --- a/src/services/Elastic.Changelog/GitHub/IGitHubPrService.cs +++ b/src/services/Elastic.Changelog/GitHub/IGitHubPrService.cs @@ -5,7 +5,7 @@ namespace Elastic.Changelog.GitHub; /// -/// Service interface for fetching pull request information from GitHub +/// Service interface for fetching pull request and issue information from GitHub /// public interface IGitHubPrService { @@ -18,4 +18,14 @@ public interface IGitHubPrService /// Cancellation token /// PR information or null if fetch fails Task FetchPrInfoAsync(string prUrl, string? owner = null, string? repo = null, CancellationToken ctx = default); + + /// + /// Fetches issue information from GitHub + /// + /// The issue URL (e.g., https://github.com/owner/repo/issues/123, owner/repo#123, or just a number if owner/repo are provided) + /// Optional: GitHub repository owner (used when issueUrl is just a number) + /// Optional: GitHub repository name (used when issueUrl is just a number) + /// Cancellation token + /// Issue information or null if fetch fails + Task FetchIssueInfoAsync(string issueUrl, string? owner = null, string? repo = null, CancellationToken ctx = default); } diff --git a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs index 0b54ca081..2b5829332 100644 --- a/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs +++ b/src/services/Elastic.Changelog/GithubRelease/GitHubReleaseChangelogService.cs @@ -264,7 +264,7 @@ private async Task ProcessPrReference( : null }], Areas = labelDerivedAreas, - Pr = prUrl + Prs = [prUrl] }; // Generate YAML content diff --git a/src/services/Elastic.Changelog/Rendering/Asciidoc/AsciidocRendererBase.cs b/src/services/Elastic.Changelog/Rendering/Asciidoc/AsciidocRendererBase.cs index 439893a70..b2d523f87 100644 --- a/src/services/Elastic.Changelog/Rendering/Asciidoc/AsciidocRendererBase.cs +++ b/src/services/Elastic.Changelog/Rendering/Asciidoc/AsciidocRendererBase.cs @@ -40,26 +40,23 @@ private static void RenderEntryTitleAndLinks(StringBuilder sb, ChangelogEntry en _ = sb.Append("* "); _ = sb.Append(ChangelogTextUtilities.Beautify(entry.Title)); - var hasPr = !string.IsNullOrWhiteSpace(entry.Pr); + var hasPrs = entry.Prs is { Count: > 0 }; var hasIssues = entry.Issues is { Count: > 0 }; - if (!hasPr && !hasIssues) + if (!hasPrs && !hasIssues) return; _ = sb.Append(' '); - if (hasPr) + foreach (var pr in entry.Prs ?? []) { - _ = sb.Append(ChangelogTextUtilities.FormatPrLinkAsciidoc(entry.Pr!, entryRepo, hideLinks)); + _ = sb.Append(ChangelogTextUtilities.FormatPrLinkAsciidoc(pr, entryRepo, hideLinks)); _ = sb.Append(' '); } - if (hasIssues) + foreach (var issue in entry.Issues ?? []) { - foreach (var issue in entry.Issues!) - { - _ = sb.Append(ChangelogTextUtilities.FormatIssueLinkAsciidoc(issue, entryRepo, hideLinks)); - _ = sb.Append(' '); - } + _ = sb.Append(ChangelogTextUtilities.FormatIssueLinkAsciidoc(issue, entryRepo, hideLinks)); + _ = sb.Append(' '); } } diff --git a/src/services/Elastic.Changelog/Rendering/BundleValidationService.cs b/src/services/Elastic.Changelog/Rendering/BundleValidationService.cs index d15f94ff0..5020fd7c5 100644 --- a/src/services/Elastic.Changelog/Rendering/BundleValidationService.cs +++ b/src/services/Elastic.Changelog/Rendering/BundleValidationService.cs @@ -204,9 +204,11 @@ private static bool ValidateResolvedEntry( } // Track PRs for duplicate detection - if (!string.IsNullOrWhiteSpace(entry.Pr)) + foreach (var pr in entry.Prs ?? []) { - var normalizedPr = ChangelogBundlingService.NormalizePrForComparison(entry.Pr, null, null); + if (string.IsNullOrWhiteSpace(pr)) + continue; + var normalizedPr = ChangelogBundlingService.NormalizePrForComparison(pr, null, null); if (!seenPrs.TryGetValue(normalizedPr, out var prBundleList)) { prBundleList = []; @@ -281,9 +283,11 @@ private async Task ValidateFileReferenceEntryAsync( } // Track PRs for duplicate detection - if (!string.IsNullOrWhiteSpace(entryData.Pr)) + foreach (var pr in entryData.Prs ?? []) { - var normalizedPr = ChangelogBundlingService.NormalizePrForComparison(entryData.Pr, null, null); + if (string.IsNullOrWhiteSpace(pr)) + continue; + var normalizedPr = ChangelogBundlingService.NormalizePrForComparison(pr, null, null); if (!seenPrs.TryGetValue(normalizedPr, out var prBundleList)) { prBundleList = []; diff --git a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs index 298e8c6d3..b7d248f85 100644 --- a/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs +++ b/src/services/Elastic.Changelog/Rendering/ChangelogRenderingService.cs @@ -244,16 +244,22 @@ private static void EmitBlockedEntryWarnings( private static string GetEntryIdentifier(ChangelogEntry entry, ChangelogRenderContext context) { - // Try to extract PR number if available - if (!string.IsNullOrWhiteSpace(entry.Pr)) + if (entry.Prs is { Count: > 0 }) { var repo = context.EntryToRepo.GetValueOrDefault(entry, context.Repo); - var prNumber = ChangelogTextUtilities.ExtractPrNumber(entry.Pr, "elastic", repo); + var prNumber = ChangelogTextUtilities.ExtractPrNumber(entry.Prs[0], "elastic", repo); if (prNumber.HasValue) return $"for PR {prNumber.Value}"; } - // Fall back to title if no PR is available + if (entry.Issues is { Count: > 0 }) + { + var repo = context.EntryToRepo.GetValueOrDefault(entry, context.Repo); + var issueNumber = ChangelogTextUtilities.ExtractIssueNumber(entry.Issues[0], "elastic", repo); + if (issueNumber.HasValue) + return $"for issue {issueNumber.Value}"; + } + return $"'{entry.Title}'"; } diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs index 8296907a5..a9da1c518 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/IndexMarkdownRenderer.cs @@ -164,50 +164,42 @@ private static void RenderEntriesByArea( var hasCommentedLinks = false; if (entryHideLinks) { - // When hiding private links, put them on separate lines as comments with proper indentation - if (!string.IsNullOrWhiteSpace(entry.Pr)) + foreach (var pr in entry.Prs ?? []) { _ = sb.AppendLine(); if (shouldHide) _ = sb.Append("% "); _ = sb.Append(" "); - _ = sb.Append(ChangelogTextUtilities.FormatPrLink(entry.Pr, entryRepo, entryHideLinks)); + _ = sb.Append(ChangelogTextUtilities.FormatPrLink(pr, entryRepo, entryHideLinks)); hasCommentedLinks = true; } - if (entry.Issues is { Count: > 0 }) + foreach (var issue in entry.Issues ?? []) { - foreach (var issue in entry.Issues) - { - _ = sb.AppendLine(); - if (shouldHide) - _ = sb.Append("% "); - _ = sb.Append(" "); - _ = sb.Append(ChangelogTextUtilities.FormatIssueLink(issue, entryRepo, entryHideLinks)); - hasCommentedLinks = true; - } + _ = sb.AppendLine(); + if (shouldHide) + _ = sb.Append("% "); + _ = sb.Append(" "); + _ = sb.Append(ChangelogTextUtilities.FormatIssueLink(issue, entryRepo, entryHideLinks)); + hasCommentedLinks = true; } - // Add a newline after the last link if there are commented links if (hasCommentedLinks) _ = sb.AppendLine(); } else { _ = sb.Append(' '); - if (!string.IsNullOrWhiteSpace(entry.Pr)) + foreach (var pr in entry.Prs ?? []) { - _ = sb.Append(ChangelogTextUtilities.FormatPrLink(entry.Pr, entryRepo, entryHideLinks)); + _ = sb.Append(ChangelogTextUtilities.FormatPrLink(pr, entryRepo, entryHideLinks)); _ = sb.Append(' '); } - if (entry.Issues is { Count: > 0 }) + foreach (var issue in entry.Issues ?? []) { - foreach (var issue in entry.Issues) - { - _ = sb.Append(ChangelogTextUtilities.FormatIssueLink(issue, entryRepo, entryHideLinks)); - _ = sb.Append(' '); - } + _ = sb.Append(ChangelogTextUtilities.FormatIssueLink(issue, entryRepo, entryHideLinks)); + _ = sb.Append(' '); } } diff --git a/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs b/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs index adb29380e..aa5bef39e 100644 --- a/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs +++ b/src/services/Elastic.Changelog/Rendering/Markdown/MarkdownRendererBase.cs @@ -52,36 +52,37 @@ protected static (HashSet bundleProductIds, string entryRepo, bool hideL /// protected static void RenderPrIssueLinks(StringBuilder sb, ChangelogEntry entry, string entryRepo, bool entryHideLinks) { - var hasPr = !string.IsNullOrWhiteSpace(entry.Pr); + var hasPrs = entry.Prs is { Count: > 0 }; var hasIssues = entry.Issues is { Count: > 0 }; - if (!hasPr && !hasIssues) + if (!hasPrs && !hasIssues) return; if (entryHideLinks) { - // When hiding private links, put them on separate lines as comments - if (hasPr) - _ = sb.AppendLine(ChangelogTextUtilities.FormatPrLink(entry.Pr!, entryRepo, entryHideLinks)); - if (hasIssues) - { - foreach (var issue in entry.Issues!) - _ = sb.AppendLine(ChangelogTextUtilities.FormatIssueLink(issue, entryRepo, entryHideLinks)); - } + foreach (var pr in entry.Prs ?? []) + _ = sb.AppendLine(ChangelogTextUtilities.FormatPrLink(pr, entryRepo, entryHideLinks)); + foreach (var issue in entry.Issues ?? []) + _ = sb.AppendLine(ChangelogTextUtilities.FormatIssueLink(issue, entryRepo, entryHideLinks)); _ = sb.AppendLine("For more information, check the pull request or issue above."); } else { _ = sb.Append("For more information, check "); - if (hasPr) - _ = sb.Append(ChangelogTextUtilities.FormatPrLink(entry.Pr!, entryRepo, entryHideLinks)); - if (hasIssues) + var first = true; + foreach (var pr in entry.Prs ?? []) + { + if (!first) + _ = sb.Append(' '); + _ = sb.Append(ChangelogTextUtilities.FormatPrLink(pr, entryRepo, entryHideLinks)); + first = false; + } + foreach (var issue in entry.Issues ?? []) { - foreach (var issue in entry.Issues!) - { + if (!first) _ = sb.Append(' '); - _ = sb.Append(ChangelogTextUtilities.FormatIssueLink(issue, entryRepo, entryHideLinks)); - } + _ = sb.Append(ChangelogTextUtilities.FormatIssueLink(issue, entryRepo, entryHideLinks)); + first = false; } _ = sb.AppendLine("."); diff --git a/src/services/Elastic.Documentation.Assembler/Mcp/Resources/Guidelines/changelog.md.txt b/src/services/Elastic.Documentation.Assembler/Mcp/Resources/Guidelines/changelog.md.txt index 31ae3f4c2..f0de2d70a 100644 --- a/src/services/Elastic.Documentation.Assembler/Mcp/Resources/Guidelines/changelog.md.txt +++ b/src/services/Elastic.Documentation.Assembler/Mcp/Resources/Guidelines/changelog.md.txt @@ -17,7 +17,7 @@ Changelogs are YAML files that follow a common schema. Each file describes a sin ## Optional fields checklist - [ ] **`description`:** Additional context (max 600 characters). Include when title alone is not self-explanatory. -- [ ] **`pr`:** Pull request number or URL. +- [ ] **`prs`:** Array of pull request numbers or URLs. - [ ] **`issues`:** Array of related issue URLs. - [ ] **`areas`:** Array of affected components, features, or product areas. - [ ] **`highlight`:** Boolean for release highlights. diff --git a/src/services/Elastic.Documentation.Assembler/Mcp/Resources/Templates/changelog.yaml b/src/services/Elastic.Documentation.Assembler/Mcp/Resources/Templates/changelog.yaml index 8450ec109..37020fa7e 100644 --- a/src/services/Elastic.Documentation.Assembler/Mcp/Resources/Templates/changelog.yaml +++ b/src/services/Elastic.Documentation.Assembler/Mcp/Resources/Templates/changelog.yaml @@ -25,8 +25,9 @@ description: | # Additional information about what changed (max 600 characters). # Focus on user value. Explain what users can do or what problems are solved. -pr: -# The pull request number or URL. +prs: +# An array of pull request numbers or URLs. +# - https://github.com/elastic/example/pull/123 issues: # An array of related issue URLs. diff --git a/src/tooling/docs-builder/Commands/ChangelogCommand.cs b/src/tooling/docs-builder/Commands/ChangelogCommand.cs index 9ec5137a4..6a947c398 100644 --- a/src/tooling/docs-builder/Commands/ChangelogCommand.cs +++ b/src/tooling/docs-builder/Commands/ChangelogCommand.cs @@ -209,20 +209,21 @@ public Task Init( /// Optional: Path to the changelog.yml configuration file. Defaults to 'docs/changelog.yml' /// Optional: Additional information about the change (max 600 characters) /// Optional: Turn off extraction of release notes from PR descriptions. By default, release notes are extracted when using --prs. Short release notes (≤120 characters, single line) are used as the title, long release notes (>120 characters or multi-line) are used as the description. - /// Optional: Turn off extraction of linked issues from PR body (e.g., "Fixes #123"). By default, linked issues are extracted when using --prs. + /// Optional: Turn off extraction of linked references. When using --prs: turns off extraction of linked issues from the PR body (e.g., "Fixes #123"). When using --issues: turns off extraction of linked PRs from the issue body (e.g., "Fixed by #123"). By default, linked references are extracted in both cases. /// Optional: Feature flag ID /// Optional: Include in release highlights /// Optional: How the user's environment is affected - /// Optional: Issue URL(s) (comma-separated or specify multiple times) - /// Optional: GitHub repository owner (used when --prs contains just numbers) + /// Optional: Issue URL(s) or number(s) (comma-separated), or a path to a newline-delimited file containing issue URLs or numbers. Can be specified multiple times. Each occurrence can be either comma-separated issues (e.g., `--issues "https://github.com/owner/repo/issues/123,456"`) or a file path (e.g., `--issues /path/to/file.txt`). If --owner and --repo are provided, issue numbers can be used instead of URLs. If specified, --title can be derived from the issue. Creates one changelog file per issue. + /// Optional: GitHub repository owner (used when --prs or --issues contains just numbers) /// Optional: Output directory for the changelog. Defaults to current directory /// Optional: Pull request URL(s) or PR number(s) (comma-separated), or a path to a newline-delimited file containing PR URLs or numbers. Can be specified multiple times. Each occurrence can be either comma-separated PRs (e.g., `--prs "https://github.com/owner/repo/pull/123,6789"`) or a file path (e.g., `--prs /path/to/file.txt`). When specifying PRs directly, provide comma-separated values. When specifying a file path, provide a single value that points to a newline-delimited file. If --owner and --repo are provided, PR numbers can be used instead of URLs. If specified, --title can be derived from the PR. If mappings are configured, --areas and --type can also be derived from the PR. Creates one changelog file per PR. - /// Optional: GitHub repository name (used when --prs contains just numbers) + /// Optional: GitHub repository name (used when --prs or --issues contains just numbers) /// Optional: When used with --prs, remove square brackets and text within them from the beginning of PR titles, and also remove a colon if it follows the closing bracket (e.g., "[Inference API] Title" becomes "Title", "[ES|QL]: Title" becomes "Title", "[Discover][ESQL] Title" becomes "Title") /// Optional: Subtype for breaking changes (api, behavioral, configuration, etc.) - /// Optional: A short, user-facing title (max 80 characters). Required if --pr is not specified. If --pr and --title are specified, the latter value is used instead of what exists in the PR. - /// Optional: Type of change (feature, enhancement, bug-fix, breaking-change, etc.). Required if --pr is not specified. If mappings are configured, type can be derived from the PR. - /// Optional: Use the PR number as the filename instead of generating it from a unique ID and title + /// Optional: A short, user-facing title (max 80 characters). Required if neither --prs nor --issues is specified. If --prs and --title are specified, the latter value is used instead of what exists in the PR. + /// Optional: Type of change (feature, enhancement, bug-fix, breaking-change, etc.). Required if neither --prs nor --issues is specified. If mappings are configured, type can be derived from the PR or issue. + /// Optional: Use the PR number(s) as the filename. With multiple PRs, uses hyphen-separated list (e.g., 137431-137432.yaml). Requires --prs. Mutually exclusive with --use-issue-number. + /// Optional: Use the issue number(s) as the filename. Requires --issues. When both --issues and --prs are specified, uses issue number if this flag is set. Mutually exclusive with --use-pr-number. /// [Command("add")] public async Task Create( @@ -246,6 +247,7 @@ public async Task Create( string? title = null, string? type = null, bool usePrNumber = false, + bool useIssueNumber = false, Cancel ctx = default ) { @@ -299,9 +301,62 @@ public async Task Create( var shouldExtractReleaseNotes = !noExtractReleaseNotes; var shouldExtractIssues = !noExtractIssues; + // Parse issues: handle both comma-separated values and file paths (mirrors PR parsing) + string[]? parsedIssues = null; + if (issues is { Length: > 0 }) + { + var allIssues = new List(); + var validIssues = issues.Where(i => !string.IsNullOrWhiteSpace(i)); + foreach (var trimmedValue in validIssues.Select(i => i.Trim())) + { + var normalizedPath = NormalizePath(trimmedValue); + if (_fileSystem.File.Exists(normalizedPath)) + { + try + { + var fileLines = await _fileSystem.File.ReadAllLinesAsync(normalizedPath, ctx); + foreach (var line in fileLines) + { + if (!string.IsNullOrWhiteSpace(line)) + allIssues.Add(line.Trim()); + } + } + catch (IOException ex) + { + collector.EmitError(string.Empty, $"Failed to read issues from file '{normalizedPath}': {ex.Message}", ex); + return 1; + } + } + else + { + var commaSeparated = trimmedValue.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + allIssues.AddRange(commaSeparated); + } + } + parsedIssues = allIssues.ToArray(); + } + // Use provided products or empty list (service will infer from repo/config if empty) var resolvedProducts = products ?? []; + if (usePrNumber && useIssueNumber) + { + collector.EmitError(string.Empty, "--use-pr-number and --use-issue-number are mutually exclusive; specify only one."); + return 1; + } + + if (usePrNumber && (parsedPrs == null || parsedPrs.Length == 0)) + { + collector.EmitError(string.Empty, "--use-pr-number requires --prs to be specified."); + return 1; + } + + if (useIssueNumber && (parsedIssues == null || parsedIssues.Length == 0)) + { + collector.EmitError(string.Empty, "--use-issue-number requires --issues to be specified."); + return 1; + } + var input = new CreateChangelogArguments { Title = title, @@ -312,7 +367,7 @@ public async Task Create( Prs = parsedPrs, Owner = owner, Repo = repo, - Issues = issues ?? [], + Issues = parsedIssues ?? [], Description = description, Impact = impact, Action = action, @@ -321,6 +376,7 @@ public async Task Create( Output = output, Config = config, UsePrNumber = usePrNumber, + UseIssueNumber = useIssueNumber, StripTitlePrefix = stripTitlePrefix, ExtractReleaseNotes = shouldExtractReleaseNotes, ExtractIssues = shouldExtractIssues @@ -338,16 +394,17 @@ async static (s, collector, state, ctx) => await s.CreateChangelog(collector, st /// /// Optional: Profile name from bundle.profiles in config (e.g., "elasticsearch-release"). When specified, the second argument is the version or promotion report URL. /// Optional: Version number or promotion report URL/path when using a profile (e.g., "9.2.0" or "https://buildkite.../promotion-report.html") - /// Include all changelogs in the directory. Only one filter option can be specified: `--all`, `--input-products`, or `--prs`. + /// Include all changelogs in the directory. Only one filter option can be specified: `--all`, `--input-products`, `--prs`, or `--issues`. /// Optional: Path to the changelog.yml configuration file. Defaults to 'docs/changelog.yml' /// Optional: Directory containing changelog YAML files. Uses config bundle.directory or defaults to current directory /// Filter by feature IDs (comma-separated), or a path to a newline-delimited file containing feature IDs. Can be specified multiple times. Entries with matching feature-id values will be commented out when the bundle is rendered (by CLI render or {changelog} directive). - /// Filter by products in format "product target lifecycle, ..." (e.g., "cloud-serverless 2025-12-02 ga, cloud-serverless 2025-12-06 beta"). When specified, all three parts (product, target, lifecycle) are required but can be wildcards (*). Examples: "elasticsearch * *" matches all elasticsearch changelogs, "cloud-serverless 2025-12-02 *" matches cloud-serverless 2025-12-02 with any lifecycle, "* 9.3.* *" matches any product with target starting with "9.3.", "* * *" matches all changelogs (equivalent to --all). Only one filter option can be specified: `--all`, `--input-products`, or `--prs`. + /// Filter by products in format "product target lifecycle, ..." (e.g., "cloud-serverless 2025-12-02 ga, cloud-serverless 2025-12-06 beta"). When specified, all three parts (product, target, lifecycle) are required but can be wildcards (*). Examples: "elasticsearch * *" matches all elasticsearch changelogs, "cloud-serverless 2025-12-02 *" matches cloud-serverless 2025-12-02 with any lifecycle, "* 9.3.* *" matches any product with target starting with "9.3.", "* * *" matches all changelogs (equivalent to --all). Only one filter option can be specified: `--all`, `--input-products`, `--prs`, or `--issues`. + /// Filter by issue URLs or numbers (comma-separated), or a path to a newline-delimited file containing issue URLs or numbers. Can be specified multiple times. Each occurrence can be either comma-separated issues (e.g., `--issues "https://github.com/owner/repo/issues/123,456"`) or a file path (e.g., `--issues /path/to/file.txt`). When specifying issues directly, provide comma-separated values. When specifying a file path, provide a single value that points to a newline-delimited file. Only one filter option can be specified: `--all`, `--input-products`, `--prs`, or `--issues`. /// Optional: Output path for the bundled changelog. Can be either (1) a directory path, in which case 'changelog-bundle.yaml' is created in that directory, or (2) a file path ending in .yml or .yaml. Uses config bundle.output_directory or defaults to 'changelog-bundle.yaml' in the input directory /// Optional: Explicitly set the products array in the output file in format "product target lifecycle, ...". Overrides any values from changelogs. - /// GitHub repository owner (required only when PRs are specified as numbers) - /// Filter by pull request URLs or numbers (comma-separated), or a path to a newline-delimited file containing PR URLs or numbers. Can be specified multiple times. Only one filter option can be specified: `--all`, `--input-products`, or `--prs`. - /// GitHub repository name. Used for PR filtering when PRs are specified as numbers, and also sets the repo field in the bundle output for generating correct PR/issue links. If not specified, the product ID is used as the repo name in links. + /// GitHub repository owner (required when PRs or issues are specified as numbers) + /// Filter by pull request URLs or numbers (comma-separated), or a path to a newline-delimited file containing PR URLs or numbers. Can be specified multiple times. Only one filter option can be specified: `--all`, `--input-products`, `--prs`, or `--issues`. + /// GitHub repository name. Used for PR or issue filtering when PRs or issues are specified as numbers, and also sets the repo field in the bundle output for generating correct PR/issue links. If not specified, the product ID is used as the repo name in links. /// Optional: Copy the contents of each changelog file into the entries array. Uses config bundle.resolve or defaults to false. /// Optional: Explicitly turn off resolve (overrides config). /// @@ -362,6 +419,7 @@ public async Task Bundle( [ProductInfoParser] List? inputProducts = null, string? output = null, [ProductInfoParser] List? outputProducts = null, + string[]? issues = null, string? owner = null, string[]? prs = null, string? repo = null, @@ -374,10 +432,9 @@ public async Task Bundle( var service = new ChangelogBundlingService(logFactory, configurationContext); - // Check if using profile mode vs raw flag mode var isProfileMode = !string.IsNullOrWhiteSpace(profile); - // Process each --prs occurrence: each can be comma-separated PRs or a file path + // Process each --prs occurrence var allPrs = new List(); if (prs is { Length: > 0 }) { @@ -399,6 +456,26 @@ public async Task Bundle( } } + // Process each --issues occurrence + var allIssues = new List(); + if (issues is { Length: > 0 }) + { + foreach (var issuesValue in issues.Where(p => !string.IsNullOrWhiteSpace(p))) + { + if (issuesValue.Contains(',')) + { + var commaSeparated = issuesValue + .Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Where(p => !string.IsNullOrWhiteSpace(p)); + allIssues.AddRange(commaSeparated); + } + else + { + allIssues.Add(issuesValue); + } + } + } + // In raw mode (no profile), validate filter options if (!isProfileMode) { @@ -409,10 +486,12 @@ public async Task Bundle( specifiedFilters.Add("--input-products"); if (allPrs.Count > 0) specifiedFilters.Add("--prs"); + if (allIssues.Count > 0) + specifiedFilters.Add("--issues"); if (specifiedFilters.Count == 0) { - collector.EmitError(string.Empty, "At least one filter option must be specified: --all, --input-products, --prs, or use a profile (e.g., 'bundle elasticsearch-release 9.2.0')"); + collector.EmitError(string.Empty, "At least one filter option must be specified: --all, --input-products, --prs, --issues, or use a profile (e.g., 'bundle elasticsearch-release 9.2.0')"); _ = collector.StartAsync(ctx); await collector.WaitForDrain(); await collector.StopAsync(ctx); @@ -421,7 +500,7 @@ public async Task Bundle( if (specifiedFilters.Count > 1) { - collector.EmitError(string.Empty, $"Multiple filter options cannot be specified together. You specified: {string.Join(", ", specifiedFilters)}. Please use only one filter option: --all, --input-products, or --prs"); + collector.EmitError(string.Empty, $"Multiple filter options cannot be specified together. You specified: {string.Join(", ", specifiedFilters)}. Please use only one filter option: --all, --input-products, --prs, or --issues"); _ = collector.StartAsync(ctx); await collector.WaitForDrain(); await collector.StopAsync(ctx); @@ -430,10 +509,9 @@ public async Task Bundle( } else { - // In profile mode, validate that no raw flags are used - if (all || (inputProducts != null && inputProducts.Count > 0) || allPrs.Count > 0) + if (all || (inputProducts != null && inputProducts.Count > 0) || allPrs.Count > 0 || allIssues.Count > 0) { - collector.EmitError(string.Empty, "When using a profile, do not specify --all, --input-products, or --prs. The profile configuration determines the filter."); + collector.EmitError(string.Empty, "When using a profile, do not specify --all, --input-products, --prs, or --issues. The profile configuration determines the filter."); _ = collector.StartAsync(ctx); await collector.WaitForDrain(); await collector.StopAsync(ctx); @@ -554,8 +632,9 @@ public async Task Bundle( All = all, InputProducts = inputProducts, OutputProducts = outputProducts, - Resolve = shouldResolve ?? false, // Will be overridden by config if null + Resolve = shouldResolve ?? false, Prs = allPrs.Count > 0 ? allPrs.ToArray() : null, + Issues = allIssues.Count > 0 ? allIssues.ToArray() : null, Owner = owner, Repo = repo, Profile = profile, @@ -574,7 +653,7 @@ async static (s, collector, state, ctx) => await s.BundleChangelogs(collector, s /// /// Render bundled changelog(s) to markdown or asciidoc files /// - /// Required: Bundle input(s) in format "bundle-file-path|changelog-file-path|repo|link-visibility" (use pipe as delimiter). To merge multiple bundles, separate them with commas. Only bundle-file-path is required. link-visibility can be "hide-links" or "keep-links" (default). Paths support tilde (~) expansion and relative paths. + /// Required: Bundle input(s) in format "bundle-file-path|changelog-file-path|repo|link-visibility" (use pipe as delimiter). To merge multiple bundles, separate them with commas. Only bundle-file-path is required. link-visibility can be "hide-links" or "keep-links" (default). Use "hide-links" for private repositories; when set, all PR and issue links for each affected entry are hidden (entries may have multiple links via the prs and issues arrays). Paths support tilde (~) expansion and relative paths. /// Optional: Path to the changelog.yml configuration file. Defaults to 'docs/changelog.yml' /// Optional: Output file type. Valid values: "markdown" or "asciidoc". Defaults to "markdown" /// Filter by feature IDs (comma-separated), or a path to a newline-delimited file containing feature IDs. Can be specified multiple times. Entries with matching feature-id values will be commented out in the output. diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs index ea6f7c68c..3f2cb7166 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleAmendTests.cs @@ -41,7 +41,8 @@ private async Task CreateResolvedBundle(CancellationToken ct) products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; var changelogFile = FileSystem.Path.Combine(_changelogDir, "1755268130-existing.yaml"); @@ -79,7 +80,8 @@ private async Task CreateUnresolvedBundle(CancellationToken ct) products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; var changelogFile = FileSystem.Path.Combine(_changelogDir, "1755268130-existing.yaml"); @@ -121,7 +123,8 @@ private async Task CreateNewChangelogFile(CancellationToken ct) products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - "200" description: A new enhancement added via amend """; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs index d2463ddfb..bad804736 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleChangelogsTests.cs @@ -41,7 +41,8 @@ public async Task BundleChangelogs_WithAllOption_CreatesValidBundle() - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -51,7 +52,8 @@ public async Task BundleChangelogs_WithAllOption_CreatesValidBundle() products: - product: kibana target: 9.2.0 - pr: https://github.com/elastic/kibana/pull/200 + prs: + - https://github.com/elastic/kibana/pull/200 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-first-changelog.yaml"); @@ -98,7 +100,8 @@ public async Task BundleChangelogs_WithProductsFilter_FiltersCorrectly() - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -109,7 +112,8 @@ public async Task BundleChangelogs_WithProductsFilter_FiltersCorrectly() - product: kibana target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/kibana/pull/200 + prs: + - https://github.com/elastic/kibana/pull/200 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-elasticsearch-feature.yaml"); @@ -151,7 +155,8 @@ public async Task BundleChangelogs_WithPrsFilter_FiltersCorrectly() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -161,7 +166,8 @@ public async Task BundleChangelogs_WithPrsFilter_FiltersCorrectly() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - https://github.com/elastic/elasticsearch/pull/200 """; // language=yaml var changelog3 = @@ -171,7 +177,8 @@ public async Task BundleChangelogs_WithPrsFilter_FiltersCorrectly() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/300 + prs: + - https://github.com/elastic/elasticsearch/pull/300 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-first-pr.yaml"); @@ -201,6 +208,105 @@ public async Task BundleChangelogs_WithPrsFilter_FiltersCorrectly() bundleContent.Should().NotContain("name: 1755268150-third-pr.yaml"); } + [Fact] + public async Task BundleChangelogs_WithIssuesFilter_FiltersCorrectly() + { + // Arrange + // language=yaml + var changelog1 = + """ + title: First issue + type: feature + products: + - product: elasticsearch + target: 9.2.0 + issues: + - https://github.com/elastic/elasticsearch/issues/100 + """; + // language=yaml + var changelog2 = + """ + title: Second issue + type: feature + products: + - product: elasticsearch + target: 9.2.0 + issues: + - https://github.com/elastic/elasticsearch/issues/200 + """; + // language=yaml + var changelog3 = + """ + title: Third issue + type: feature + products: + - product: elasticsearch + target: 9.2.0 + issues: + - https://github.com/elastic/elasticsearch/issues/300 + """; + + var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-first-issue.yaml"); + var file2 = FileSystem.Path.Combine(_changelogDir, "1755268140-second-issue.yaml"); + var file3 = FileSystem.Path.Combine(_changelogDir, "1755268150-third-issue.yaml"); + await FileSystem.File.WriteAllTextAsync(file1, changelog1, TestContext.Current.CancellationToken); + await FileSystem.File.WriteAllTextAsync(file2, changelog2, TestContext.Current.CancellationToken); + await FileSystem.File.WriteAllTextAsync(file3, changelog3, TestContext.Current.CancellationToken); + + var input = new BundleChangelogsArguments + { + Directory = _changelogDir, + Issues = ["https://github.com/elastic/elasticsearch/issues/100", "https://github.com/elastic/elasticsearch/issues/200"], + Output = FileSystem.Path.Combine(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml") + }; + + // Act + var result = await Service.BundleChangelogs(Collector, input, TestContext.Current.CancellationToken); + + // Assert + result.Should().BeTrue(); + Collector.Errors.Should().Be(0); + + var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current.CancellationToken); + bundleContent.Should().Contain("name: 1755268130-first-issue.yaml"); + bundleContent.Should().Contain("name: 1755268140-second-issue.yaml"); + bundleContent.Should().NotContain("name: 1755268150-third-issue.yaml"); + } + + [Fact] + public async Task BundleChangelogs_WithOldPrFormat_StillMatchesWhenFilteringByPrs() + { + // Backward compat: changelog with legacy pr: (single string) should still match --prs filter + // language=yaml + var changelogWithOldFormat = + """ + title: Legacy PR changelog + type: feature + products: + - product: elasticsearch + target: 9.2.0 + pr: https://github.com/elastic/elasticsearch/pull/999 + """; + + var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-legacy-pr.yaml"); + await FileSystem.File.WriteAllTextAsync(file1, changelogWithOldFormat, TestContext.Current.CancellationToken); + + var input = new BundleChangelogsArguments + { + Directory = _changelogDir, + Prs = ["https://github.com/elastic/elasticsearch/pull/999"], + Output = FileSystem.Path.Combine(FileSystem.Path.GetTempPath(), Guid.NewGuid().ToString(), "bundle.yaml") + }; + + var result = await Service.BundleChangelogs(Collector, input, TestContext.Current.CancellationToken); + + result.Should().BeTrue(); + Collector.Errors.Should().Be(0); + + var bundleContent = await FileSystem.File.ReadAllTextAsync(input.Output, TestContext.Current.CancellationToken); + bundleContent.Should().Contain("name: 1755268130-legacy-pr.yaml"); + } + [Fact] public async Task BundleChangelogs_WithPrsFilterAndUnmatchedPrs_EmitsWarnings() { @@ -214,7 +320,8 @@ public async Task BundleChangelogs_WithPrsFilterAndUnmatchedPrs_EmitsWarnings() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-first-pr.yaml"); @@ -260,7 +367,8 @@ public async Task BundleChangelogs_WithPrsFileFilter_FiltersCorrectly() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -270,7 +378,8 @@ public async Task BundleChangelogs_WithPrsFileFilter_FiltersCorrectly() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - https://github.com/elastic/elasticsearch/pull/200 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-first-pr.yaml"); @@ -321,7 +430,8 @@ public async Task BundleChangelogs_WithPrNumberAndOwnerRepo_FiltersCorrectly() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-pr-number.yaml"); @@ -360,7 +470,8 @@ public async Task BundleChangelogs_WithShortPrFormat_FiltersCorrectly() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/133609 + prs: + - https://github.com/elastic/elasticsearch/pull/133609 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-short-format.yaml"); @@ -441,7 +552,8 @@ public async Task BundleChangelogs_WithNoFilterOption_ReturnsError() - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -452,7 +564,8 @@ public async Task BundleChangelogs_WithNoFilterOption_ReturnsError() - product: kibana target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/kibana/pull/200 + prs: + - https://github.com/elastic/kibana/pull/200 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-first-changelog.yaml"); @@ -510,7 +623,8 @@ public async Task BundleChangelogs_WithMultipleProducts_CreatesValidBundle() products: - product: cloud-serverless target: 2025-12-02 - pr: https://github.com/elastic/cloud-serverless/pull/100 + prs: + - https://github.com/elastic/cloud-serverless/pull/100 """; // language=yaml var changelog2 = @@ -520,7 +634,8 @@ public async Task BundleChangelogs_WithMultipleProducts_CreatesValidBundle() products: - product: cloud-serverless target: 2025-12-06 - pr: https://github.com/elastic/cloud-serverless/pull/200 + prs: + - https://github.com/elastic/cloud-serverless/pull/200 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-cloud-feature1.yaml"); @@ -568,7 +683,8 @@ public async Task BundleChangelogs_WithWildcardProductFilter_MatchesAllProducts( - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -579,7 +695,8 @@ public async Task BundleChangelogs_WithWildcardProductFilter_MatchesAllProducts( - product: kibana target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/kibana/pull/200 + prs: + - https://github.com/elastic/kibana/pull/200 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-elasticsearch-feature.yaml"); @@ -620,7 +737,8 @@ public async Task BundleChangelogs_WithWildcardAllParts_EquivalentToAll() - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -631,7 +749,8 @@ public async Task BundleChangelogs_WithWildcardAllParts_EquivalentToAll() - product: kibana target: 9.3.0 lifecycle: beta - pr: https://github.com/elastic/kibana/pull/200 + prs: + - https://github.com/elastic/kibana/pull/200 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-elasticsearch-feature.yaml"); @@ -672,7 +791,8 @@ public async Task BundleChangelogs_WithPrefixWildcardTarget_MatchesCorrectly() - product: elasticsearch target: 9.3.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -683,7 +803,8 @@ public async Task BundleChangelogs_WithPrefixWildcardTarget_MatchesCorrectly() - product: elasticsearch target: 9.3.1 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - https://github.com/elastic/elasticsearch/pull/200 """; // language=yaml var changelog3 = @@ -694,7 +815,8 @@ public async Task BundleChangelogs_WithPrefixWildcardTarget_MatchesCorrectly() - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/300 + prs: + - https://github.com/elastic/elasticsearch/pull/300 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-es-9.3.0.yaml"); @@ -761,7 +883,8 @@ public async Task BundleChangelogs_WithUrlAsPrs_TreatsAsPrIdentifier() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/123 + prs: + - https://github.com/elastic/elasticsearch/pull/123 """; var changelogFile = FileSystem.Path.Combine(_changelogDir, "1755268130-test-pr.yaml"); await FileSystem.File.WriteAllTextAsync(changelogFile, changelog, TestContext.Current.CancellationToken); @@ -800,7 +923,8 @@ public async Task BundleChangelogs_WithNonExistentFileAndOtherPrs_EmitsWarning() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/123 + prs: + - https://github.com/elastic/elasticsearch/pull/123 """; var changelogFile = FileSystem.Path.Combine(_changelogDir, "1755268130-test-pr.yaml"); await FileSystem.File.WriteAllTextAsync(changelogFile, changelog, TestContext.Current.CancellationToken); @@ -843,7 +967,8 @@ public async Task BundleChangelogs_WithOutputProducts_OverridesChangelogProducts products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -853,7 +978,8 @@ public async Task BundleChangelogs_WithOutputProducts_OverridesChangelogProducts products: - product: kibana target: 9.2.0 - pr: https://github.com/elastic/kibana/pull/200 + prs: + - https://github.com/elastic/kibana/pull/200 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-elasticsearch-feature.yaml"); @@ -909,7 +1035,8 @@ public async Task BundleChangelogs_WithMultipleProducts_IncludesAllProducts() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -919,7 +1046,8 @@ public async Task BundleChangelogs_WithMultipleProducts_IncludesAllProducts() products: - product: kibana target: 9.2.0 - pr: https://github.com/elastic/kibana/pull/200 + prs: + - https://github.com/elastic/kibana/pull/200 """; // language=yaml var changelog3 = @@ -931,7 +1059,8 @@ public async Task BundleChangelogs_WithMultipleProducts_IncludesAllProducts() target: 9.2.0 - product: kibana target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/300 + prs: + - https://github.com/elastic/elasticsearch/pull/300 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-elasticsearch.yaml"); @@ -978,7 +1107,8 @@ public async Task BundleChangelogs_WithInputProducts_IncludesLifecycleInProducts - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -989,7 +1119,8 @@ public async Task BundleChangelogs_WithInputProducts_IncludesLifecycleInProducts - product: elasticsearch target: 9.3.0 lifecycle: beta - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - https://github.com/elastic/elasticsearch/pull/200 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-elasticsearch-ga.yaml"); @@ -1037,7 +1168,8 @@ public async Task BundleChangelogs_WithOutputProducts_IncludesLifecycleInProduct products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-elasticsearch.yaml"); @@ -1085,7 +1217,8 @@ public async Task BundleChangelogs_ExtractsLifecycleFromChangelogEntries() - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -1096,7 +1229,8 @@ public async Task BundleChangelogs_ExtractsLifecycleFromChangelogEntries() - product: elasticsearch target: 9.3.0 lifecycle: beta - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - https://github.com/elastic/elasticsearch/pull/200 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-elasticsearch-ga.yaml"); @@ -1142,7 +1276,8 @@ public async Task BundleChangelogs_WithInputProductsWildcardLifecycle_ExtractsAc - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1-feature.yaml"); @@ -1191,7 +1326,8 @@ public async Task BundleChangelogs_WithMultipleTargets_WarningIncludesLifecycle( - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml var changelog2 = @@ -1202,7 +1338,8 @@ public async Task BundleChangelogs_WithMultipleTargets_WarningIncludesLifecycle( - product: elasticsearch target: 9.2.0 lifecycle: beta - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - https://github.com/elastic/elasticsearch/pull/200 """; // language=yaml var changelog3 = @@ -1212,7 +1349,8 @@ public async Task BundleChangelogs_WithMultipleTargets_WarningIncludesLifecycle( products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/300 + prs: + - https://github.com/elastic/elasticsearch/pull/300 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-elasticsearch-ga.yaml"); @@ -1257,7 +1395,8 @@ public async Task BundleChangelogs_WithResolve_CopiesChangelogContents() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 areas: - Search description: This is a test feature @@ -1289,7 +1428,8 @@ public async Task BundleChangelogs_WithResolve_CopiesChangelogContents() bundleContent.Should().Contain("title: Test feature"); bundleContent.Should().Contain("product: elasticsearch"); bundleContent.Should().Contain("target: 9.2.0"); - bundleContent.Should().Contain("pr: https://github.com/elastic/elasticsearch/pull/100"); + bundleContent.Should().Contain("prs:"); + bundleContent.Should().Contain("https://github.com/elastic/elasticsearch/pull/100"); bundleContent.Should().Contain("areas:"); bundleContent.Should().Contain("- Search"); bundleContent.Should().Contain("description: This is a test feature"); @@ -1310,7 +1450,8 @@ public async Task BundleChangelogs_WithResolve_PreservesSpecialCharactersInUtf8( - product: elasticsearch target: 9.3.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 description: | This feature includes special characters: - Ampersand: & symbol @@ -1395,7 +1536,8 @@ public async Task BundleChangelogs_WithDirectoryOutputPath_CreatesDefaultFilenam products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-test-feature.yaml"); @@ -1575,7 +1717,8 @@ public async Task BundleChangelogs_WithHideFeaturesOption_IncludesHideFeaturesIn products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-feature.yaml"); @@ -1616,7 +1759,8 @@ public async Task BundleChangelogs_WithoutHideFeaturesOption_OmitsHideFeaturesFi products: - product: elasticsearch target: 9.3.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-feature.yaml"); @@ -1656,7 +1800,8 @@ public async Task BundleChangelogs_WithHideFeaturesFromFile_IncludesHideFeatures products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-feature.yaml"); @@ -1702,7 +1847,8 @@ public async Task BundleChangelogs_WithRepoOption_IncludesRepoInBundleProducts() products: - product: cloud-serverless target: 2025-12-02 - pr: https://github.com/elastic/cloud/pull/100 + prs: + - https://github.com/elastic/cloud/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-serverless-feature.yaml"); @@ -1742,7 +1888,8 @@ public async Task BundleChangelogs_WithoutRepoOption_OmitsRepoFieldInOutput() products: - product: elasticsearch target: 9.3.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-es-feature.yaml"); @@ -1782,7 +1929,8 @@ public async Task BundleChangelogs_WithOutputProductsAndRepo_IncludesRepoInAllPr products: - product: elasticsearch target: 9.3.0 - pr: https://github.com/elastic/cloud/pull/100 + prs: + - https://github.com/elastic/cloud/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-feature.yaml"); @@ -1844,7 +1992,8 @@ public async Task BundleChangelogs_WithConfigOutputDirectory_WhenOutputNotSpecif products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-feature.yaml"); @@ -1902,7 +2051,8 @@ public async Task BundleChangelogs_WithConfigDirectory_WhenDirectoryIsCurrentDir products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-feature.yaml"); @@ -1963,7 +2113,8 @@ public async Task BundleChangelogs_WithProfileHideFeatures_IncludesHideFeaturesI - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-feature.yaml"); @@ -2029,7 +2180,8 @@ public async Task BundleChangelogs_WithProfileAndCliHideFeatures_MergesBothSourc - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-feature.yaml"); @@ -2097,7 +2249,8 @@ public async Task BundleChangelogs_WithProfileAndCliHideFeatures_DeduplicatesFea - product: elasticsearch target: 9.2.0 lifecycle: ga - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-feature.yaml"); @@ -2153,7 +2306,8 @@ public async Task BundleChangelogs_WithComments_ProducesNormalizedChecksum() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml @@ -2164,7 +2318,8 @@ public async Task BundleChangelogs_WithComments_ProducesNormalizedChecksum() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var file1 = FileSystem.Path.Combine(_changelogDir, "1755268130-with-comments.yaml"); @@ -2213,7 +2368,8 @@ public async Task BundleChangelogs_WithAndWithoutComments_ProduceSameChecksum() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/500 + prs: + - https://github.com/elastic/elasticsearch/pull/500 """; // language=yaml @@ -2224,7 +2380,8 @@ public async Task BundleChangelogs_WithAndWithoutComments_ProduceSameChecksum() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/500 + prs: + - https://github.com/elastic/elasticsearch/pull/500 """; // Bundle with comments @@ -2279,7 +2436,8 @@ public async Task BundleChangelogs_WithDifferentData_ProducesDifferentChecksum() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; // language=yaml @@ -2290,7 +2448,8 @@ public async Task BundleChangelogs_WithDifferentData_ProducesDifferentChecksum() products: - product: elasticsearch target: 9.3.0 - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - https://github.com/elastic/elasticsearch/pull/200 """; // Bundle first file @@ -2360,7 +2519,8 @@ public async Task AmendBundle_WithComments_ProducesNormalizedChecksum() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - https://github.com/elastic/elasticsearch/pull/100 """; var changelogFile = FileSystem.Path.Combine(changelogDir, "1755268130-amend-feature.yaml"); @@ -2426,7 +2586,8 @@ public async Task AmendBundle_WithResolve_ProducesNormalizedChecksum() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - https://github.com/elastic/elasticsearch/pull/200 """; var changelogFile = FileSystem.Path.Combine(changelogDir, "1755268140-resolved-feature.yaml"); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/BundleLoading/BundleLoaderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/BundleLoading/BundleLoaderTests.cs index d00830a9e..e1c8828f7 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/BundleLoading/BundleLoaderTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/BundleLoading/BundleLoaderTests.cs @@ -41,7 +41,8 @@ public void LoadBundles_WithValidBundles_ReturnsLoadedBundles() entries: - title: Test feature type: feature - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; _fileSystem.File.WriteAllText($"{bundlesFolder}/9.3.0.yaml", bundleContent); @@ -195,7 +196,8 @@ public void ResolveEntries_WithFileReferences_LoadsFromFiles() """ title: Feature from file type: feature - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: A feature loaded from a file """; _fileSystem.File.WriteAllText($"{changelogDir}/entries/feature.yaml", entryContent); @@ -957,7 +959,8 @@ public void LoadBundles_RepoFieldSerializesAndDeserializesCorrectly() entries: - title: Test feature type: feature - pr: https://github.com/elastic/elasticsearch/pull/123 + prs: + - "123" """; _fileSystem.File.WriteAllText($"{bundlesFolder}/2025-02-01.yaml", bundleContent); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/PrFetchFailureTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/PrFetchFailureTests.cs index d4b941cf8..53a14775f 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/PrFetchFailureTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/PrFetchFailureTests.cs @@ -53,7 +53,8 @@ public async Task CreateChangelog_WithPrOptionButPrFetchFails_WithTitleAndType_C var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current.CancellationToken); yamlContent.Should().Contain("title: Manual title provided"); yamlContent.Should().Contain("type: feature"); - yamlContent.Should().Contain("pr: https://github.com/elastic/elasticsearch/pull/12345"); + yamlContent.Should().Contain("prs:"); + yamlContent.Should().Contain("https://github.com/elastic/elasticsearch/pull/12345"); } [Fact] @@ -97,7 +98,8 @@ public async Task CreateChangelog_WithPrOptionButPrFetchFails_WithoutTitleAndTyp var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current.CancellationToken); yamlContent.Should().Contain("# title: # TODO: Add title"); yamlContent.Should().Contain("# type: # TODO: Add type"); - yamlContent.Should().Contain("pr: https://github.com/elastic/elasticsearch/pull/12345"); + yamlContent.Should().Contain("prs:"); + yamlContent.Should().Contain("https://github.com/elastic/elasticsearch/pull/12345"); yamlContent.Should().Contain("products:"); // Should not contain uncommented title/type var lines = yamlContent.Split('\n'); @@ -153,6 +155,7 @@ public async Task CreateChangelog_WithMultiplePrsButPrFetchFails_GeneratesBasicC yamlContent.Should().Contain("title: Shared title"); yamlContent.Should().Contain("type: bug-fix"); // Should reference at least one of the PRs (when filenames collide, the last one wins) - yamlContent.Should().MatchRegex(@"pr:\s*(https://github\.com/elastic/elasticsearch/pull/12345|https://github\.com/elastic/elasticsearch/pull/67890)"); + yamlContent.Should().Contain("prs:"); + yamlContent.Should().MatchRegex(@"(https://github\.com/elastic/elasticsearch/pull/12345|https://github\.com/elastic/elasticsearch/pull/67890)"); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Create/PrIntegrationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Create/PrIntegrationTests.cs index dcd4ea407..ae21d0095 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Create/PrIntegrationTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Create/PrIntegrationTests.cs @@ -77,7 +77,8 @@ public async Task CreateChangelog_WithPrOption_FetchesPrInfoAndDerivesTitle() var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current.CancellationToken); yamlContent.Should().Contain("title: Implement new aggregation API"); yamlContent.Should().Contain("type: feature"); - yamlContent.Should().Contain("pr: https://github.com/elastic/elasticsearch/pull/12345"); + yamlContent.Should().Contain("prs:"); + yamlContent.Should().Contain("https://github.com/elastic/elasticsearch/pull/12345"); } [Fact] @@ -143,7 +144,64 @@ public async Task CreateChangelog_WithUsePrNumber_CreatesFileWithPrNumberAsFilen var yamlContent = await FileSystem.File.ReadAllTextAsync(files[0], TestContext.Current.CancellationToken); yamlContent.Should().Contain("type: bug-fix"); - yamlContent.Should().Contain("pr: https://github.com/elastic/elasticsearch/pull/140034"); + yamlContent.Should().Contain("prs:"); + yamlContent.Should().Contain("https://github.com/elastic/elasticsearch/pull/140034"); + } + + [Fact] + public async Task CreateChangelog_WithUseIssueNumberAndBothIssuesAndPrs_UseIssueNumberForFilename() + { + // When both --issues and --prs are specified, --use-issue-number should still determine the filename + var prInfo = new GitHubPrInfo + { + Title = "Release notes test", + Labels = ["type:feature"] + }; + + A.CallTo(() => MockGitHubService.FetchPrInfoAsync( + "https://github.com/elastic/kibana/pull/250840", + null, + null, + A._)) + .Returns(prInfo); + + // language=yaml + var configContent = + """ + pivot: + types: + feature: "type:feature" + bug-fix: + breaking-change: + lifecycles: + - ga + """; + var configPath = await CreateConfigDirectory(configContent); + + var service = CreateService(); + + var input = new CreateChangelogArguments + { + Issues = ["https://github.com/elastic/kibana/issues/233425"], + Prs = ["https://github.com/elastic/kibana/pull/250840"], + Products = [new ProductArgument { Product = "kibana", Target = "9.2.0", Lifecycle = "ga" }], + Config = configPath, + Output = CreateOutputDirectory(), + UseIssueNumber = true, + Title = "Release notes test", + Type = "feature" + }; + + var result = await service.CreateChangelog(Collector, input, TestContext.Current.CancellationToken); + + result.Should().BeTrue(); + Collector.Errors.Should().Be(0); + + var files = FileSystem.Directory.GetFiles(input.Output!, "*.yaml"); + files.Should().HaveCount(1); + + var fileName = Path.GetFileName(files[0]); + fileName.Should().Be("233425.yaml", "the filename should use the issue number when UseIssueNumber is true, even with PRs present"); } [Fact] @@ -481,7 +539,7 @@ public async Task CreateChangelog_WithMixedPrsFromFileAndCommaSeparated_Processe // Verify both PRs were processed yamlContents.Should().Contain(c => c.Contains("title: PR from comma-separated")); yamlContents.Should().Contain(c => c.Contains("title: PR from file")); - yamlContents.Should().Contain(c => c.Contains("pr: https://github.com/elastic/elasticsearch/pull/1111")); - yamlContents.Should().Contain(c => c.Contains("pr: https://github.com/elastic/elasticsearch/pull/2222")); + yamlContents.Should().Contain(c => c.Contains("prs:") && c.Contains("https://github.com/elastic/elasticsearch/pull/1111")); + yamlContents.Should().Contain(c => c.Contains("prs:") && c.Contains("https://github.com/elastic/elasticsearch/pull/2222")); } } diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/BasicRenderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/BasicRenderTests.cs index 24a5363d4..750163b9d 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/BasicRenderTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/BasicRenderTests.cs @@ -26,7 +26,8 @@ public async Task RenderChangelogs_WithValidBundle_CreatesMarkdownFiles() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This is a test feature """; @@ -92,7 +93,8 @@ public async Task RenderChangelogs_WithMultipleBundles_MergesAndRenders() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; // language=yaml var changelog2 = @@ -102,7 +104,8 @@ public async Task RenderChangelogs_WithMultipleBundles_MergesAndRenders() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - "200" """; var file1 = FileSystem.Path.Combine(changelogDir1, "1755268130-first.yaml"); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/BlockConfigurationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/BlockConfigurationTests.cs index fd5edf411..42e7fd960 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/BlockConfigurationTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/BlockConfigurationTests.cs @@ -28,7 +28,8 @@ public async Task RenderChangelogs_WithBlockedArea_CommentsOutMatchingEntries() target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This feature should be blocked """; @@ -43,7 +44,8 @@ public async Task RenderChangelogs_WithBlockedArea_CommentsOutMatchingEntries() target: 2026-01-26 areas: - Search - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" description: This feature should be visible """; @@ -152,7 +154,8 @@ public async Task RenderChangelogs_WithBlockedType_CommentsOutMatchingEntries() target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This deprecation should be blocked """; @@ -167,7 +170,8 @@ public async Task RenderChangelogs_WithBlockedType_CommentsOutMatchingEntries() target: 2026-01-26 areas: - Search - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" description: This deprecation should be visible """; @@ -180,7 +184,8 @@ public async Task RenderChangelogs_WithBlockedType_CommentsOutMatchingEntries() products: - product: cloud-serverless target: 2026-01-26 - pr: https://github.com/elastic/elasticsearch/pull/102 + prs: + - "102" description: This feature should be visible """; @@ -306,7 +311,8 @@ public async Task RenderChangelogs_WithGlobalBlockedArea_CommentsOutMatchingEntr target: 9.2.0 areas: - Internal - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This feature should be blocked globally """; @@ -321,7 +327,8 @@ public async Task RenderChangelogs_WithGlobalBlockedArea_CommentsOutMatchingEntr target: 9.2.0 areas: - Search - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" description: This feature should be visible """; @@ -428,7 +435,8 @@ public async Task RenderChangelogs_WithProductSpecificOverride_OverridesGlobalBl target: 2026-01-26 areas: - Internal - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This should be visible for cloud-serverless """; @@ -443,7 +451,8 @@ public async Task RenderChangelogs_WithProductSpecificOverride_OverridesGlobalBl target: 9.2.0 areas: - Internal - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" description: This should be blocked for elasticsearch """; @@ -580,7 +589,8 @@ public async Task RenderChangelogs_WithBlockedArea_BreakingChange_UsesBlockComme target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This breaking change should be blocked impact: Users will be affected action: Update your code @@ -597,7 +607,8 @@ public async Task RenderChangelogs_WithBlockedArea_BreakingChange_UsesBlockComme target: 2026-01-26 areas: - Search - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" description: This breaking change should be visible impact: Users will be affected action: Update your code @@ -713,7 +724,8 @@ public async Task RenderChangelogs_WithBlockedArea_KnownIssue_UsesBlockComments( target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This known issue should be blocked impact: Users may experience issues action: Workaround available @@ -730,7 +742,8 @@ public async Task RenderChangelogs_WithBlockedArea_KnownIssue_UsesBlockComments( target: 2026-01-26 areas: - Search - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" description: This known issue should be visible impact: Users may experience issues action: Workaround available @@ -847,7 +860,8 @@ public async Task RenderChangelogs_WithMultipleBlockedAreas_CommentsOutAllMatchi target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; // language=yaml @@ -860,7 +874,8 @@ public async Task RenderChangelogs_WithMultipleBlockedAreas_CommentsOutAllMatchi target: 2026-01-26 areas: - Internal - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" """; // language=yaml @@ -873,7 +888,8 @@ public async Task RenderChangelogs_WithMultipleBlockedAreas_CommentsOutAllMatchi target: 2026-01-26 areas: - Search - pr: https://github.com/elastic/elasticsearch/pull/102 + prs: + - "102" """; var changelogFile1 = FileSystem.Path.Combine(changelogDir, "1755268130-allocation.yaml"); @@ -987,7 +1003,8 @@ public async Task RenderChangelogs_WithSubsections_CommentsOutEmptySubsectionHea target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This feature should be blocked """; @@ -1002,7 +1019,8 @@ public async Task RenderChangelogs_WithSubsections_CommentsOutEmptySubsectionHea target: 2026-01-26 areas: - Search - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" description: This feature should be visible """; @@ -1111,7 +1129,8 @@ public async Task RenderChangelogs_WithAllEntriesBlocked_ShowsNoItemsMessage() target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This feature should be blocked """; @@ -1126,7 +1145,8 @@ public async Task RenderChangelogs_WithAllEntriesBlocked_ShowsNoItemsMessage() target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" description: This enhancement should be blocked """; @@ -1227,7 +1247,8 @@ public async Task RenderChangelogs_WithAllBreakingChangesBlocked_ShowsNoBreaking target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This breaking change should be blocked impact: Users will be affected action: Update your code @@ -1325,7 +1346,8 @@ public async Task RenderChangelogs_WithAllDeprecationsBlocked_ShowsNoDeprecation target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This deprecation should be blocked impact: Users will be affected action: Update your code @@ -1422,7 +1444,8 @@ public async Task RenderChangelogs_WithAllKnownIssuesBlocked_ShowsNoKnownIssuesM target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This known issue should be blocked impact: Users may experience issues action: Workaround available @@ -1520,7 +1543,8 @@ public async Task RenderChangelogs_WithBlockedEntries_EmitsWarnings() target: 2026-01-26 areas: - Allocation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This feature should be blocked """; @@ -1533,7 +1557,8 @@ public async Task RenderChangelogs_WithBlockedEntries_EmitsWarnings() products: - product: cloud-serverless target: 2026-01-26 - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" description: This deprecation should be blocked """; @@ -1549,7 +1574,8 @@ public async Task RenderChangelogs_WithBlockedEntries_EmitsWarnings() areas: - Allocation - Internal - pr: https://github.com/elastic/elasticsearch/pull/102 + prs: + - "102" description: This feature should be blocked """; diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/BundleValidationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/BundleValidationTests.cs index 6ad99644d..0ab972f97 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/BundleValidationTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/BundleValidationTests.cs @@ -18,7 +18,8 @@ public class BundleValidationTests(ITestOutputHelper output) : RenderChangelogTe products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" """; // language=yaml @@ -29,7 +30,8 @@ public class BundleValidationTests(ITestOutputHelper output) : RenderChangelogTe products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/102 + prs: + - "102" """; // language=yaml @@ -40,7 +42,8 @@ public class BundleValidationTests(ITestOutputHelper output) : RenderChangelogTe products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/103 + prs: + - "103" """; // language=yaml @@ -53,7 +56,8 @@ public class BundleValidationTests(ITestOutputHelper output) : RenderChangelogTe products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/104 + prs: + - "104" """; // language=yaml @@ -64,7 +68,8 @@ public class BundleValidationTests(ITestOutputHelper output) : RenderChangelogTe products: - product: kibana target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/999 + prs: + - "999" """; [Fact] @@ -196,7 +201,8 @@ public async Task AmendFileEntry_WithResolvedData_SkipsChecksumValidation() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - "200" """; await FileSystem.File.WriteAllTextAsync(amend1, amendContent, TestContext.Current.CancellationToken); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/ChecksumValidationTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/ChecksumValidationTests.cs index 60aab2514..c0415dc50 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/ChecksumValidationTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/ChecksumValidationTests.cs @@ -20,7 +20,8 @@ public class ChecksumValidationTests(ITestOutputHelper output) : RenderChangelog products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; // language=yaml @@ -31,7 +32,8 @@ public class ChecksumValidationTests(ITestOutputHelper output) : RenderChangelog products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; // language=yaml @@ -42,7 +44,8 @@ public class ChecksumValidationTests(ITestOutputHelper output) : RenderChangelog products: - product: kibana target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/999 + prs: + - "999" """; [Fact] @@ -131,7 +134,8 @@ public async Task ValidateBundle_ResolvedEntry_SkipsChecksumValidation() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/DuplicateHandlingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/DuplicateHandlingTests.cs index db9f88bd0..64c35c7b6 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/DuplicateHandlingTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/DuplicateHandlingTests.cs @@ -29,7 +29,8 @@ public async Task RenderChangelogs_WithDuplicateFileName_EmitsWarning() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; var fileName = "1755268130-duplicate.yaml"; @@ -110,7 +111,8 @@ public async Task RenderChangelogs_WithDuplicateFileNameInSameBundle_EmitsWarnin products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; var fileName = "1755268130-test-feature.yaml"; @@ -180,7 +182,8 @@ public async Task RenderChangelogs_WithDuplicatePr_EmitsWarning() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; // language=yaml var changelog2 = @@ -190,7 +193,8 @@ public async Task RenderChangelogs_WithDuplicatePr_EmitsWarning() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; var file1 = FileSystem.Path.Combine(changelogDir1, "1755268130-first.yaml"); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs index bbdd8b2b7..b8f08a839 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/ErrorHandlingTests.cs @@ -170,7 +170,8 @@ public async Task RenderChangelogs_WithResolvedEntry_ValidatesAndRenders() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; await FileSystem.File.WriteAllTextAsync(bundleFile, bundleContent, TestContext.Current.CancellationToken); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/HideFeaturesTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/HideFeaturesTests.cs index 2da04da70..908c6a96c 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/HideFeaturesTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/HideFeaturesTests.cs @@ -28,7 +28,8 @@ public async Task RenderChangelogs_WithHideFeatures_CommentsOutMatchingEntries() - product: elasticsearch target: 9.2.0 feature-id: feature:hidden-api - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This feature should be hidden """; @@ -41,7 +42,8 @@ public async Task RenderChangelogs_WithHideFeatures_CommentsOutMatchingEntries() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" description: This feature should be visible """; @@ -121,7 +123,8 @@ public async Task RenderChangelogs_WithHideFeatures_BreakingChange_UsesBlockComm - product: elasticsearch target: 9.2.0 feature-id: feature:hidden-breaking - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This breaking change should be hidden impact: Users will be affected action: Update your code @@ -195,7 +198,8 @@ public async Task RenderChangelogs_WithHideFeatures_Deprecation_UsesBlockComment - product: elasticsearch target: 9.2.0 feature-id: feature:hidden-deprecation - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This deprecation should be hidden """; @@ -262,7 +266,8 @@ public async Task RenderChangelogs_WithHideFeatures_CommaSeparated_CommentsOutMa - product: elasticsearch target: 9.2.0 feature-id: feature:first - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; // language=yaml @@ -274,7 +279,8 @@ public async Task RenderChangelogs_WithHideFeatures_CommaSeparated_CommentsOutMa - product: elasticsearch target: 9.2.0 feature-id: feature:second - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" """; // language=yaml @@ -285,7 +291,8 @@ public async Task RenderChangelogs_WithHideFeatures_CommaSeparated_CommentsOutMa products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/102 + prs: + - "102" """; var changelogFile1 = FileSystem.Path.Combine(changelogDir, "1755268130-first.yaml"); @@ -359,7 +366,8 @@ public async Task RenderChangelogs_WithHideFeatures_FromFile_CommentsOutMatching - product: elasticsearch target: 9.2.0 feature-id: feature:from-file - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; var changelogFile = FileSystem.Path.Combine(changelogDir, "1755268130-hidden.yaml"); @@ -425,7 +433,8 @@ public async Task RenderChangelogs_WithHideFeatures_CaseInsensitive_MatchesFeatu - product: elasticsearch target: 9.2.0 feature-id: Feature:UpperCase - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; var changelogFile = FileSystem.Path.Combine(changelogDir, "1755268130-hidden.yaml"); @@ -487,7 +496,8 @@ public async Task RenderChangelogs_WithBundleHideFeatures_CommentsOutMatchingEnt - product: elasticsearch target: 9.2.0 feature-id: feature:from-bundle - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; // language=yaml @@ -498,7 +508,8 @@ public async Task RenderChangelogs_WithBundleHideFeatures_CommentsOutMatchingEnt products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" """; var changelogFile1 = FileSystem.Path.Combine(changelogDir, "1755268130-hidden.yaml"); @@ -571,7 +582,8 @@ public async Task RenderChangelogs_MergesCLIAndBundleHideFeatures() - product: elasticsearch target: 9.2.0 feature-id: feature:cli-hidden - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; // language=yaml @@ -583,7 +595,8 @@ public async Task RenderChangelogs_MergesCLIAndBundleHideFeatures() - product: elasticsearch target: 9.2.0 feature-id: feature:bundle-hidden - pr: https://github.com/elastic/elasticsearch/pull/101 + prs: + - "101" """; // language=yaml @@ -594,7 +607,8 @@ public async Task RenderChangelogs_MergesCLIAndBundleHideFeatures() products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/102 + prs: + - "102" """; var changelogFile1 = FileSystem.Path.Combine(changelogDir, "1755268130-cli.yaml"); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/HighlightsRenderTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/HighlightsRenderTests.cs index 26cda30c8..c6b4d6ebd 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/HighlightsRenderTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/HighlightsRenderTests.cs @@ -29,7 +29,8 @@ public async Task RenderChangelogs_WithHighlightedEntries_CreatesHighlightsFile( lifecycle: ga description: Adds Cloud Connect functionality to Kibana highlight: true - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; var changelogFile = FileSystem.Path.Combine(changelogDir, "1755268130-highlight-feature.yaml"); @@ -100,7 +101,8 @@ public async Task RenderChangelogs_WithoutHighlightedEntries_DoesNotCreateHighli products: - product: elasticsearch target: 9.3.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; var changelogFile = FileSystem.Path.Combine(changelogDir, "1755268130-regular-feature.yaml"); @@ -161,7 +163,8 @@ public async Task RenderChangelogs_WithHighlightedEntries_IncludesHighlightsInAs target: 9.3.0 description: This is a highlighted enhancement highlight: true - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - "200" """; var changelogFile = FileSystem.Path.Combine(changelogDir, "1755268130-highlight-enhancement.yaml"); @@ -230,7 +233,8 @@ public async Task RenderChangelogs_WithMultipleHighlightedEntries_GroupsByArea() areas: - Search highlight: true - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; // language=yaml @@ -244,7 +248,8 @@ public async Task RenderChangelogs_WithMultipleHighlightedEntries_GroupsByArea() areas: - Indexing highlight: true - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - "200" """; var file1 = FileSystem.Path.Combine(changelogDir, "1755268130-search.yaml"); diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs index 7bcf92543..5d560eb40 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/OutputFormatTests.cs @@ -27,7 +27,8 @@ public async Task RenderChangelogs_WithCustomConfigPath_UsesSpecifiedConfigFile( products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This is a test feature """; @@ -111,7 +112,8 @@ public async Task RenderChangelogs_WithAsciidocFileType_CreatesSingleAsciidocFil products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: This is a test feature """; @@ -188,7 +190,8 @@ public async Task RenderChangelogs_WithAsciidocFileType_ValidatesAsciidocFormat( products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" description: Added new search capabilities """; @@ -200,7 +203,8 @@ public async Task RenderChangelogs_WithAsciidocFileType_ValidatesAsciidocFormat( products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/200 + prs: + - "200" description: Fixed a critical search issue """; @@ -213,7 +217,8 @@ public async Task RenderChangelogs_WithAsciidocFileType_ValidatesAsciidocFormat( products: - product: elasticsearch target: 9.2.0 - pr: https://github.com/elastic/elasticsearch/pull/300 + prs: + - "300" description: Changed API endpoint structure impact: Users need to update their API calls action: Update API client libraries diff --git a/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs b/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs index 308915cf7..f92c121cd 100644 --- a/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs +++ b/tests/Elastic.Changelog.Tests/Changelogs/Render/TitleTargetTests.cs @@ -26,7 +26,8 @@ public async Task RenderChangelogs_WithoutTitleAndNoTargets_EmitsWarning() type: feature products: - product: elasticsearch - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; var changelogFile = FileSystem.Path.Combine(changelogDir, "1755268130-test-feature.yaml"); @@ -86,7 +87,8 @@ public async Task RenderChangelogs_WithTitleAndNoTargets_NoWarning() type: feature products: - product: elasticsearch - pr: https://github.com/elastic/elasticsearch/pull/100 + prs: + - "100" """; var changelogFile = FileSystem.Path.Combine(changelogDir, "1755268130-test-feature.yaml"); diff --git a/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/ChangelogTextUtilitiesTests.cs b/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/ChangelogTextUtilitiesTests.cs index 474ef6bf8..ff7ac9230 100644 --- a/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/ChangelogTextUtilitiesTests.cs +++ b/tests/Elastic.Documentation.Configuration.Tests/ReleaseNotes/ChangelogTextUtilitiesTests.cs @@ -75,6 +75,24 @@ public void ExtractPrNumber_WithDefaultOwnerRepo_ExtractsNumber() result.Should().Be(123); } + [Theory] + [InlineData("https://github.com/elastic/elasticsearch/issues/123", 123)] + [InlineData("https://github.com/owner/repo/issues/456", 456)] + [InlineData("elastic/elasticsearch#789", 789)] + [InlineData("123", null)] + public void ExtractIssueNumber_ExtractsNumber(string input, int? expected) + { + var result = ChangelogTextUtilities.ExtractIssueNumber(input); + result.Should().Be(expected); + } + + [Fact] + public void ExtractIssueNumber_WithDefaultOwnerRepo_ExtractsNumber() + { + var result = ChangelogTextUtilities.ExtractIssueNumber("123", "elastic", "elasticsearch"); + result.Should().Be(123); + } + [Theory] [InlineData("v1.0.0", "ga")] [InlineData("v1.0.0-beta1", "beta")] diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs index c680ef3f2..364d86e9d 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogBasicTests.cs @@ -32,7 +32,8 @@ public ChangelogBasicTests(ITestOutputHelper output) : base(output, target: 9.3.0 areas: - Search - pr: "123456" + prs: + - "123456" description: This is a great new feature. - title: Fix important bug type: bug-fix @@ -41,7 +42,8 @@ public ChangelogBasicTests(ITestOutputHelper output) : base(output, target: 9.3.0 areas: - Indexing - pr: "123457" + prs: + - "123457" """)); [Fact] @@ -92,7 +94,8 @@ public ChangelogMultipleBundlesTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.2.0 - pr: "111111" + prs: + - "111111" """)); FileSystem.AddFile("docs/changelog/bundles/9.3.0.yaml", new MockFileData( @@ -107,7 +110,8 @@ public ChangelogMultipleBundlesTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "222222" + prs: + - "222222" """)); FileSystem.AddFile("docs/changelog/bundles/9.10.0.yaml", new MockFileData( @@ -122,7 +126,8 @@ public ChangelogMultipleBundlesTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.10.0 - pr: "333333" + prs: + - "333333" """)); } @@ -169,7 +174,8 @@ public ChangelogCustomPathTests(ITestOutputHelper output) : base(output, products: - product: my-product target: 1.0.0 - pr: "1" + prs: + - "1" """)); [Fact] @@ -247,7 +253,8 @@ public ChangelogWithBreakingChangesTests(ITestOutputHelper output) : base(output description: The API has changed significantly. impact: Users must update their code. action: Follow the migration guide. - pr: "222222" + prs: + - "222222" """)); [Fact] @@ -294,7 +301,8 @@ public ChangelogWithDeprecationsTests(ITestOutputHelper output) : base(output, description: The old API is deprecated. impact: The API will be removed in a future version. action: Use the new API instead. - pr: "333333" + prs: + - "333333" """)); [Fact] @@ -369,7 +377,8 @@ public ChangelogAbsolutePathTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "444444" + prs: + - "444444" """)); [Fact] @@ -403,13 +412,15 @@ public ChangelogSectionOrderTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Security fix type: security products: - product: elasticsearch target: 9.3.0 - pr: "222222" + prs: + - "222222" - title: Breaking API change type: breaking-change products: @@ -418,7 +429,8 @@ public ChangelogSectionOrderTests(ITestOutputHelper output) : base(output, description: API changed. impact: Users must update. action: Follow guide. - pr: "333333" + prs: + - "333333" - title: Known issue type: known-issue products: @@ -427,7 +439,8 @@ public ChangelogSectionOrderTests(ITestOutputHelper output) : base(output, description: Issue exists. impact: Some impact. action: Workaround available. - pr: "444444" + prs: + - "444444" - title: Deprecated feature type: deprecation products: @@ -436,13 +449,15 @@ public ChangelogSectionOrderTests(ITestOutputHelper output) : base(output, description: Feature deprecated. impact: Will be removed. action: Use new feature. - pr: "555555" + prs: + - "555555" - title: Bug fix type: bug-fix products: - product: elasticsearch target: 9.3.0 - pr: "666666" + prs: + - "666666" """)); [Fact] @@ -506,13 +521,15 @@ public ChangelogHeaderLevelsTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Bug fix type: bug-fix products: - product: elasticsearch target: 9.3.0 - pr: "222222" + prs: + - "222222" """)); [Fact] diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs index 8e01ad4a0..d3683fd7c 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogConfigTests.cs @@ -30,7 +30,8 @@ public ChangelogConfigLoadAutoDiscoverTests(ITestOutputHelper output) : base(out products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Deprecation notice type: deprecation products: @@ -39,7 +40,8 @@ public ChangelogConfigLoadAutoDiscoverTests(ITestOutputHelper output) : base(out description: This API is deprecated. impact: Users should migrate. action: Use the new API. - pr: "222222" + prs: + - "222222" - title: Known issue type: known-issue products: @@ -47,7 +49,8 @@ public ChangelogConfigLoadAutoDiscoverTests(ITestOutputHelper output) : base(out target: 9.3.0 description: There is a known issue. impact: Some users may be affected. - pr: "333333" + prs: + - "333333" """)); // Add changelog config with publish blockers @@ -114,7 +117,8 @@ public ChangelogConfigLoadExplicitPathTests(ITestOutputHelper output) : base(out products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Internal docs type: docs products: @@ -122,7 +126,8 @@ public ChangelogConfigLoadExplicitPathTests(ITestOutputHelper output) : base(out target: 9.3.0 areas: - Internal - pr: "222222" + prs: + - "222222" """)); // Add custom config at explicit path @@ -178,13 +183,15 @@ public ChangelogConfigLoadFromDocsSubfolderTests(ITestOutputHelper output) : bas products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Other change type: other products: - product: elasticsearch target: 9.3.0 - pr: "222222" + prs: + - "222222" """)); // Add config in docs/docs/changelog.yml (docs subfolder) @@ -228,7 +235,8 @@ public ChangelogConfigNotFoundTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" """)); [Fact] @@ -262,7 +270,8 @@ public ChangelogConfigExplicitPathNotFoundTests(ITestOutputHelper output) : base products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" """)); [Fact] @@ -297,7 +306,8 @@ public ChangelogConfigPriorityTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Deprecation notice type: deprecation products: @@ -306,13 +316,15 @@ public ChangelogConfigPriorityTests(ITestOutputHelper output) : base(output, description: Deprecated. impact: None. action: Upgrade. - pr: "222222" + prs: + - "222222" - title: Other change type: other products: - product: elasticsearch target: 9.3.0 - pr: "333333" + prs: + - "333333" """)); // Add both config files - root should take priority @@ -366,7 +378,8 @@ public ChangelogConfigEmptyBlockTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" """)); // Config file exists but has no block section @@ -410,7 +423,8 @@ public ChangelogConfigMixedBlockersTests(ITestOutputHelper output) : base(output target: 9.3.0 areas: - Search - pr: "111111" + prs: + - "111111" - title: Deprecation in Search type: deprecation products: @@ -421,7 +435,8 @@ public ChangelogConfigMixedBlockersTests(ITestOutputHelper output) : base(output description: Deprecated. impact: None. action: Upgrade. - pr: "222222" + prs: + - "222222" - title: Feature in Internal type: feature products: @@ -429,13 +444,15 @@ public ChangelogConfigMixedBlockersTests(ITestOutputHelper output) : base(output target: 9.3.0 areas: - Internal - pr: "333333" + prs: + - "333333" - title: Bug fix type: bug-fix products: - product: elasticsearch target: 9.3.0 - pr: "444444" + prs: + - "444444" """)); // Config with both type and area blockers @@ -494,7 +511,8 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) products: - product: kibana target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Internal feature type: feature products: @@ -502,7 +520,8 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) target: 9.3.0 areas: - Internal - pr: "222222" + prs: + - "222222" - title: Observability feature type: feature products: @@ -510,7 +529,8 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) target: 9.3.0 areas: - Elastic Observability - pr: "333333" + prs: + - "333333" """)); // Config with product-specific blocker for kibana @@ -575,7 +595,8 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Internal feature type: feature products: @@ -583,7 +604,8 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) target: 9.3.0 areas: - Internal - pr: "222222" + prs: + - "222222" """)); // Config with product-specific blockers @@ -648,7 +670,8 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: ES Internal feature type: feature products: @@ -656,7 +679,8 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) target: 9.3.0 areas: - ES Internal - pr: "222222" + prs: + - "222222" - title: Kibana Internal feature type: feature products: @@ -664,7 +688,8 @@ protected override void AddToFileSystem(MockFileSystem fileSystem) target: 9.3.0 areas: - Kibana Internal - pr: "333333" + prs: + - "333333" """)); // Config with different blockers for different products diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs index 381d6bde8..edeb98f51 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogHideLinksTests.cs @@ -109,7 +109,8 @@ public ChangelogLinksDefaultBehaviorTests(ITestOutputHelper output) : base(outpu products: - product: elasticsearch target: 9.3.0 - pr: "123456" + prs: + - "123456" issues: - "78901" - "78902" @@ -161,7 +162,8 @@ public ChangelogLinksHiddenForPrivateRepoTests(ITestOutputHelper output) : base( products: - product: elasticsearch target: 9.3.0 - pr: "123456" + prs: + - "123456" issues: - "78901" - "78902" @@ -230,7 +232,8 @@ public ChangelogLinksHiddenInDetailedEntriesTests(ITestOutputHelper output) : ba description: API has changed. impact: Users must update. action: Follow migration guide. - pr: "999888" + prs: + - "999888" issues: - "777666" - title: Deprecation with PR @@ -241,7 +244,8 @@ public ChangelogLinksHiddenInDetailedEntriesTests(ITestOutputHelper output) : ba description: Old API deprecated. impact: Will be removed. action: Use new API. - pr: "555444" + prs: + - "555444" """)); public override async ValueTask InitializeAsync() @@ -308,7 +312,8 @@ public ChangelogLinksShownForPublicRepoTests(ITestOutputHelper output) : base(ou products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" """)); public override async ValueTask InitializeAsync() @@ -356,7 +361,8 @@ public ChangelogLinksWithMergedBundlesTests(ITestOutputHelper output) : base(out products: - product: elasticsearch target: 2025-08-05 - pr: "111111" + prs: + - "111111" """)); FileSystem.AddFile("docs/changelog/bundles/kibana-2025-08-05.yaml", new MockFileData( @@ -371,7 +377,8 @@ public ChangelogLinksWithMergedBundlesTests(ITestOutputHelper output) : base(out products: - product: kibana target: 2025-08-05 - pr: "222222" + prs: + - "222222" """)); } @@ -430,7 +437,8 @@ public ChangelogLinksWithMergedPublicReposTests(ITestOutputHelper output) : base products: - product: elasticsearch target: 2025-08-05 - pr: "111111" + prs: + - "111111" """)); FileSystem.AddFile("docs/changelog/bundles/kibana-2025-08-05.yaml", new MockFileData( @@ -445,7 +453,8 @@ public ChangelogLinksWithMergedPublicReposTests(ITestOutputHelper output) : base products: - product: kibana target: 2025-08-05 - pr: "222222" + prs: + - "222222" """)); } diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogMergeTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogMergeTests.cs index 4aae76da2..10073950a 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogMergeTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogMergeTests.cs @@ -36,7 +36,8 @@ public ChangelogMergeSameTargetTests(ITestOutputHelper output) : base(output, target: 2025-08-05 areas: - Dashboard - pr: "111111" + prs: + - "111111" """)); FileSystem.AddFile("docs/changelog/bundles/elasticsearch-2025-08-05.yaml", new MockFileData( @@ -53,13 +54,15 @@ public ChangelogMergeSameTargetTests(ITestOutputHelper output) : base(output, target: 2025-08-05 areas: - Search - pr: "222222" + prs: + - "222222" - title: Elasticsearch bugfix for August 5th type: bug-fix products: - product: elasticsearch target: 2025-08-05 - pr: "222223" + prs: + - "222223" """)); FileSystem.AddFile("docs/changelog/bundles/serverless-2025-08-05.yaml", new MockFileData( @@ -76,7 +79,8 @@ public ChangelogMergeSameTargetTests(ITestOutputHelper output) : base(output, target: 2025-08-05 areas: - API - pr: "333333" + prs: + - "333333" """)); // A different release date with single bundle @@ -92,7 +96,8 @@ public ChangelogMergeSameTargetTests(ITestOutputHelper output) : base(output, products: - product: kibana target: 2025-08-01 - pr: "444444" + prs: + - "444444" """)); } @@ -192,7 +197,8 @@ public ChangelogMergeDifferentTargetsTests(ITestOutputHelper output) : base(outp products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" """)); FileSystem.AddFile("docs/changelog/bundles/9.2.0.yaml", new MockFileData( @@ -207,7 +213,8 @@ public ChangelogMergeDifferentTargetsTests(ITestOutputHelper output) : base(outp products: - product: elasticsearch target: 9.2.0 - pr: "222222" + prs: + - "222222" """)); FileSystem.AddFile("docs/changelog/bundles/9.1.0.yaml", new MockFileData( @@ -222,7 +229,8 @@ public ChangelogMergeDifferentTargetsTests(ITestOutputHelper output) : base(outp products: - product: elasticsearch target: 9.1.0 - pr: "333333" + prs: + - "333333" """)); } @@ -275,13 +283,15 @@ public ChangelogMergeSingleBundleTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Bug fix in 9.3.0 type: bug-fix products: - product: elasticsearch target: 9.3.0 - pr: "111112" + prs: + - "111112" """)); [Fact] @@ -329,7 +339,8 @@ public ChangelogMergeMixedVersionTypesTests(ITestOutputHelper output) : base(out products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" """)); // Date-based version @@ -345,7 +356,8 @@ public ChangelogMergeMixedVersionTypesTests(ITestOutputHelper output) : base(out products: - product: kibana target: 2025-08-05 - pr: "222222" + prs: + - "222222" """)); } diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogPathResolutionTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogPathResolutionTests.cs index 6d915f1a1..ba6a7e16a 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogPathResolutionTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogPathResolutionTests.cs @@ -34,7 +34,8 @@ public ChangelogBundlesFolderRelativePathTests(ITestOutputHelper output) : base( products: - product: test-product target: 1.0.0 - pr: "12345" + prs: + - "12345" """)); [Fact] @@ -67,7 +68,8 @@ public ChangelogBundlesFolderDocsetRootRelativeTests(ITestOutputHelper output) : products: - product: test-product target: 2.0.0 - pr: "67890" + prs: + - "67890" """)); [Fact] @@ -107,7 +109,8 @@ public ChangelogConfigRelativePathTests(ITestOutputHelper output) : base(output, products: - product: test-product target: 1.0.0 - pr: "11111" + prs: + - "11111" - title: Blocked entry type: deprecation products: @@ -116,7 +119,8 @@ public ChangelogConfigRelativePathTests(ITestOutputHelper output) : base(output, description: Deprecated. impact: None. action: Upgrade. - pr: "22222" + prs: + - "22222" """)); FileSystem.AddFile("docs/config/my-changelog.yml", new MockFileData( @@ -165,7 +169,8 @@ public ChangelogConfigDocsetRootRelativePathTests(ITestOutputHelper output) : ba products: - product: test-product target: 1.0.0 - pr: "33333" + prs: + - "33333" - title: Internal feature type: feature products: @@ -173,7 +178,8 @@ public ChangelogConfigDocsetRootRelativePathTests(ITestOutputHelper output) : ba target: 1.0.0 areas: - Internal - pr: "44444" + prs: + - "44444" """)); FileSystem.AddFile("docs/settings/changelog-config.yml", new MockFileData( @@ -219,7 +225,8 @@ public ChangelogBundlesFolderNestedRelativePathTests(ITestOutputHelper output) : products: - product: nested-product target: 3.0.0 - pr: "99999" + prs: + - "99999" """)); [Fact] @@ -261,7 +268,8 @@ public ChangelogPathEdgeCaseTests(ITestOutputHelper output) : base(output, products: - product: edge-product target: 1.0.0 - pr: "55555" + prs: + - "55555" """)); [Fact] @@ -293,13 +301,15 @@ public ChangelogConfigAndBundlesRelativePathsTests(ITestOutputHelper output) : b products: - product: combined-product target: 1.0.0 - pr: "66666" + prs: + - "66666" - title: Blocked by config type: other products: - product: combined-product target: 1.0.0 - pr: "77777" + prs: + - "77777" """)); FileSystem.AddFile("docs/config/changelog.yml", new MockFileData( diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogSubsectionsTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogSubsectionsTests.cs index dc6cd7898..17cc02f84 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogSubsectionsTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogSubsectionsTests.cs @@ -29,7 +29,8 @@ public ChangelogSubsectionsDisabledByDefaultTests(ITestOutputHelper output) : ba target: 9.3.0 areas: - Search - pr: "111111" + prs: + - "111111" - title: Feature in Indexing type: feature products: @@ -37,7 +38,8 @@ public ChangelogSubsectionsDisabledByDefaultTests(ITestOutputHelper output) : ba target: 9.3.0 areas: - Indexing - pr: "222222" + prs: + - "222222" """)); [Fact] @@ -82,7 +84,8 @@ public ChangelogSubsectionsEnabledTests(ITestOutputHelper output) : base(output, target: 9.3.0 areas: - Search - pr: "111111" + prs: + - "111111" - title: Feature in Indexing type: feature products: @@ -90,7 +93,8 @@ public ChangelogSubsectionsEnabledTests(ITestOutputHelper output) : base(output, target: 9.3.0 areas: - Indexing - pr: "222222" + prs: + - "222222" """)); [Fact] @@ -135,7 +139,8 @@ public ChangelogSubsectionsExplicitFalseTests(ITestOutputHelper output) : base(o target: 9.3.0 areas: - Search - pr: "111111" + prs: + - "111111" """)); [Fact] diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs index 5c30920ff..db028143e 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogTocFilteringTests.cs @@ -34,19 +34,22 @@ public ChangelogPublishBlockerFiltersTocTests(ITestOutputHelper output) : base(o products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Docs update type: docs products: - product: elasticsearch target: 9.3.0 - pr: "222222" + prs: + - "222222" - title: Other stuff type: other products: - product: elasticsearch target: 9.3.0 - pr: "333333" + prs: + - "333333" """)); // Block docs and other types via publish blocker @@ -148,21 +151,24 @@ public ChangelogHideFeaturesFiltersTocTests(ITestOutputHelper output) : base(out products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Hidden other change 1 type: other feature-id: hidden-feature-1 products: - product: elasticsearch target: 9.3.0 - pr: "222222" + prs: + - "222222" - title: Hidden other change 2 type: other feature-id: hidden-feature-2 products: - product: elasticsearch target: 9.3.0 - pr: "333333" + prs: + - "333333" """)); [Fact] @@ -227,14 +233,16 @@ public ChangelogPartialFilterRetainsTocTests(ITestOutputHelper output) : base(ou products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Hidden other change type: other feature-id: hidden-feature products: - product: elasticsearch target: 9.3.0 - pr: "222222" + prs: + - "222222" """)); [Fact] @@ -285,20 +293,23 @@ public ChangelogCombinedFiltersFilterTocTests(ITestOutputHelper output) : base(o products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Docs entry blocked by type type: docs products: - product: elasticsearch target: 9.3.0 - pr: "222222" + prs: + - "222222" - title: Bug fix hidden by feature type: bug-fix feature-id: hidden-feature products: - product: elasticsearch target: 9.3.0 - pr: "333333" + prs: + - "333333" """)); // Block docs type via publish blocker @@ -379,7 +390,8 @@ public ChangelogPublishBlockerAreaFiltersTocTests(ITestOutputHelper output) : ba products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Internal docs type: docs products: @@ -387,7 +399,8 @@ public ChangelogPublishBlockerAreaFiltersTocTests(ITestOutputHelper output) : ba target: 9.3.0 areas: - Internal - pr: "222222" + prs: + - "222222" - title: Internal other change type: other products: @@ -395,7 +408,8 @@ public ChangelogPublishBlockerAreaFiltersTocTests(ITestOutputHelper output) : ba target: 9.3.0 areas: - Internal - pr: "333333" + prs: + - "333333" """)); FileSystem.AddFile("docs/changelog.yml", new MockFileData( @@ -466,7 +480,8 @@ public ChangelogAllEntriesFilteredTocTests(ITestOutputHelper output) : base(outp target: 9.3.0 areas: - Internal - pr: "111111" + prs: + - "111111" - title: Internal bug fix type: bug-fix products: @@ -474,7 +489,8 @@ public ChangelogAllEntriesFilteredTocTests(ITestOutputHelper output) : base(outp target: 9.3.0 areas: - Internal - pr: "222222" + prs: + - "222222" """)); FileSystem.AddFile("docs/changelog.yml", new MockFileData( @@ -529,13 +545,15 @@ public ChangelogMultipleBundlesTocFilteringTests(ITestOutputHelper output) : bas products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Docs in 9.3.0 type: docs products: - product: elasticsearch target: 9.3.0 - pr: "222222" + prs: + - "222222" """)); // 9.2.0 only has docs entries (all will be blocked) @@ -551,7 +569,8 @@ public ChangelogMultipleBundlesTocFilteringTests(ITestOutputHelper output) : bas products: - product: elasticsearch target: 9.2.0 - pr: "333333" + prs: + - "333333" """)); FileSystem.AddFile("docs/changelog.yml", new MockFileData( diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs index 7d46ba8b1..0ee58bfab 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogTypeFilterTests.cs @@ -35,13 +35,15 @@ public ChangelogTypeFilterDefaultTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Bug fix type: bug-fix products: - product: elasticsearch target: 9.3.0 - pr: "222222" + prs: + - "222222" - title: Breaking API change type: breaking-change products: @@ -50,7 +52,8 @@ public ChangelogTypeFilterDefaultTests(ITestOutputHelper output) : base(output, description: API changed. impact: Users must update. action: Follow guide. - pr: "333333" + prs: + - "333333" - title: Known issue type: known-issue products: @@ -59,7 +62,8 @@ public ChangelogTypeFilterDefaultTests(ITestOutputHelper output) : base(output, description: Issue exists. impact: Some impact. action: Workaround available. - pr: "444444" + prs: + - "444444" - title: Deprecated feature type: deprecation products: @@ -68,7 +72,8 @@ public ChangelogTypeFilterDefaultTests(ITestOutputHelper output) : base(output, description: Feature deprecated. impact: Will be removed. action: Use new feature. - pr: "555555" + prs: + - "555555" """)); [Fact] @@ -136,13 +141,15 @@ public ChangelogTypeFilterAllTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Bug fix type: bug-fix products: - product: elasticsearch target: 9.3.0 - pr: "222222" + prs: + - "222222" - title: Breaking API change type: breaking-change products: @@ -151,7 +158,8 @@ public ChangelogTypeFilterAllTests(ITestOutputHelper output) : base(output, description: API changed. impact: Users must update. action: Follow guide. - pr: "333333" + prs: + - "333333" - title: Known issue type: known-issue products: @@ -160,7 +168,8 @@ public ChangelogTypeFilterAllTests(ITestOutputHelper output) : base(output, description: Issue exists. impact: Some impact. action: Workaround available. - pr: "444444" + prs: + - "444444" - title: Deprecated feature type: deprecation products: @@ -169,7 +178,8 @@ public ChangelogTypeFilterAllTests(ITestOutputHelper output) : base(output, description: Feature deprecated. impact: Will be removed. action: Use new feature. - pr: "555555" + prs: + - "555555" """)); [Fact] @@ -217,7 +227,8 @@ public ChangelogTypeFilterBreakingChangeTests(ITestOutputHelper output) : base(o products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Breaking API change type: breaking-change products: @@ -226,7 +237,8 @@ public ChangelogTypeFilterBreakingChangeTests(ITestOutputHelper output) : base(o description: API changed. impact: Users must update. action: Follow guide. - pr: "333333" + prs: + - "333333" - title: Known issue type: known-issue products: @@ -235,7 +247,8 @@ public ChangelogTypeFilterBreakingChangeTests(ITestOutputHelper output) : base(o description: Issue exists. impact: Some impact. action: Workaround available. - pr: "444444" + prs: + - "444444" """)); [Fact] @@ -284,7 +297,8 @@ public ChangelogTypeFilterDeprecationTests(ITestOutputHelper output) : base(outp products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Deprecated feature type: deprecation products: @@ -293,7 +307,8 @@ public ChangelogTypeFilterDeprecationTests(ITestOutputHelper output) : base(outp description: Feature deprecated. impact: Will be removed. action: Use new feature. - pr: "555555" + prs: + - "555555" - title: Another deprecation type: deprecation products: @@ -302,7 +317,8 @@ public ChangelogTypeFilterDeprecationTests(ITestOutputHelper output) : base(outp description: Another deprecated feature. impact: Also will be removed. action: Migrate to new API. - pr: "666666" + prs: + - "666666" """)); [Fact] @@ -350,7 +366,8 @@ public ChangelogTypeFilterKnownIssueTests(ITestOutputHelper output) : base(outpu products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Known issue 1 type: known-issue products: @@ -359,7 +376,8 @@ public ChangelogTypeFilterKnownIssueTests(ITestOutputHelper output) : base(outpu description: Issue exists. impact: Some impact. action: Workaround available. - pr: "444444" + prs: + - "444444" - title: Known issue 2 type: known-issue products: @@ -368,7 +386,8 @@ public ChangelogTypeFilterKnownIssueTests(ITestOutputHelper output) : base(outpu description: Another issue. impact: Different impact. action: Different workaround. - pr: "555555" + prs: + - "555555" """)); [Fact] @@ -416,7 +435,8 @@ public ChangelogTypeFilterInvalidTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Breaking change type: breaking-change products: @@ -425,7 +445,8 @@ public ChangelogTypeFilterInvalidTests(ITestOutputHelper output) : base(output, description: Breaking. impact: Impact. action: Action. - pr: "222222" + prs: + - "222222" """)); [Fact] @@ -472,7 +493,8 @@ public ChangelogTypeFilterCaseInsensitiveTests(ITestOutputHelper output) : base( products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Breaking change type: breaking-change products: @@ -481,7 +503,8 @@ public ChangelogTypeFilterCaseInsensitiveTests(ITestOutputHelper output) : base( description: Breaking. impact: Impact. action: Action. - pr: "222222" + prs: + - "222222" """)); [Fact] @@ -524,7 +547,8 @@ public ChangelogTypeFilterWithSubsectionsTests(ITestOutputHelper output) : base( target: 9.3.0 areas: - Search - pr: "111111" + prs: + - "111111" - title: Indexing feature type: feature products: @@ -532,7 +556,8 @@ public ChangelogTypeFilterWithSubsectionsTests(ITestOutputHelper output) : base( target: 9.3.0 areas: - Indexing - pr: "222222" + prs: + - "222222" - title: Breaking change type: breaking-change products: @@ -541,7 +566,8 @@ public ChangelogTypeFilterWithSubsectionsTests(ITestOutputHelper output) : base( description: Breaking. impact: Impact. action: Action. - pr: "333333" + prs: + - "333333" """)); [Fact] @@ -584,7 +610,8 @@ public ChangelogTypeFilterGeneratedAnchorsTests(ITestOutputHelper output) : base products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Breaking change type: breaking-change products: @@ -593,7 +620,8 @@ public ChangelogTypeFilterGeneratedAnchorsTests(ITestOutputHelper output) : base description: Breaking. impact: Impact. action: Action. - pr: "222222" + prs: + - "222222" """)); [Fact] @@ -632,7 +660,8 @@ public ChangelogTypeFilterTableOfContentsTests(ITestOutputHelper output) : base( products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" - title: Deprecated feature type: deprecation products: @@ -641,7 +670,8 @@ public ChangelogTypeFilterTableOfContentsTests(ITestOutputHelper output) : base( description: Deprecated. impact: Impact. action: Action. - pr: "222222" + prs: + - "222222" """)); [Fact] @@ -681,7 +711,8 @@ public ChangelogTypeFilterEmptyKnownIssueTests(ITestOutputHelper output) : base( products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" """)); [Fact] @@ -716,7 +747,8 @@ public ChangelogTypeFilterEmptyBreakingChangeTests(ITestOutputHelper output) : b products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" """)); [Fact] @@ -751,7 +783,8 @@ public ChangelogTypeFilterEmptyDeprecationTests(ITestOutputHelper output) : base products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" """)); [Fact] @@ -788,7 +821,8 @@ public ChangelogTypeFilterEmptyDefaultTests(ITestOutputHelper output) : base(out description: API changed. impact: Users must update. action: Follow guide. - pr: "111111" + prs: + - "111111" """)); [Fact] diff --git a/tests/Elastic.Markdown.Tests/Directives/ChangelogVersionSortingTests.cs b/tests/Elastic.Markdown.Tests/Directives/ChangelogVersionSortingTests.cs index 2921ef8f6..6fd986777 100644 --- a/tests/Elastic.Markdown.Tests/Directives/ChangelogVersionSortingTests.cs +++ b/tests/Elastic.Markdown.Tests/Directives/ChangelogVersionSortingTests.cs @@ -30,7 +30,8 @@ public ChangelogDateVersionedBundlesTests(ITestOutputHelper output) : base(outpu products: - product: cloud-serverless target: 2025-08-01 - pr: "111111" + prs: + - "111111" """)); FileSystem.AddFile("docs/changelog/bundles/2025-08-15.yaml", new MockFileData( @@ -45,7 +46,8 @@ public ChangelogDateVersionedBundlesTests(ITestOutputHelper output) : base(outpu products: - product: cloud-serverless target: 2025-08-15 - pr: "222222" + prs: + - "222222" """)); FileSystem.AddFile("docs/changelog/bundles/2025-08-05.yaml", new MockFileData( @@ -60,7 +62,8 @@ public ChangelogDateVersionedBundlesTests(ITestOutputHelper output) : base(outpu products: - product: cloud-serverless target: 2025-08-05 - pr: "333333" + prs: + - "333333" """)); } @@ -118,7 +121,8 @@ public ChangelogMixedVersionTypesTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.3.0 - pr: "111111" + prs: + - "111111" """)); FileSystem.AddFile("docs/changelog/bundles/9.2.0.yaml", new MockFileData( @@ -133,7 +137,8 @@ public ChangelogMixedVersionTypesTests(ITestOutputHelper output) : base(output, products: - product: elasticsearch target: 9.2.0 - pr: "222222" + prs: + - "222222" """)); FileSystem.AddFile("docs/changelog/bundles/2025-08-05.yaml", new MockFileData( @@ -148,7 +153,8 @@ public ChangelogMixedVersionTypesTests(ITestOutputHelper output) : base(output, products: - product: cloud-serverless target: 2025-08-05 - pr: "333333" + prs: + - "333333" """)); FileSystem.AddFile("docs/changelog/bundles/2025-07-01.yaml", new MockFileData( @@ -163,7 +169,8 @@ public ChangelogMixedVersionTypesTests(ITestOutputHelper output) : base(output, products: - product: cloud-serverless target: 2025-07-01 - pr: "444444" + prs: + - "444444" """)); } @@ -231,7 +238,8 @@ public ChangelogRawVersionFallbackTests(ITestOutputHelper output) : base(output, products: - product: experimental target: release-alpha - pr: "111111" + prs: + - "111111" """)); FileSystem.AddFile("docs/changelog/bundles/release-beta.yaml", new MockFileData( @@ -246,7 +254,8 @@ public ChangelogRawVersionFallbackTests(ITestOutputHelper output) : base(output, products: - product: experimental target: release-beta - pr: "222222" + prs: + - "222222" """)); }