Skip to content

Extract shared PR title construction to MessageBuilderUtils#14045

Closed
thavaahariharangit wants to merge 7 commits into
mainfrom
harry/shared-pr-title-construction
Closed

Extract shared PR title construction to MessageBuilderUtils#14045
thavaahariharangit wants to merge 7 commits into
mainfrom
harry/shared-pr-title-construction

Conversation

@thavaahariharangit

@thavaahariharangit thavaahariharangit commented Jan 29, 2026

Copy link
Copy Markdown
Contributor

What are you trying to accomplish?

Add a utility module that centralizes PR title construction logic, enabling reuse across different message builder implementations.

Context:

  • Single ecosystem PR titles (individual/group updates) are constructed in dependabot-core
  • Multi-ecosystem PR titles are constructed in dependabot-api

Since dependabot-api uses dependabot-core as a dependency, we're extracting the shared title construction logic into dependabot-core. This ensures future updates to title construction affect both builders consistently.

Anything you want to highlight for special attention from reviewers?

Only the PR title construction logic is extracted to this shared utility. The other two properties pr_message and commit_message remain separate because:

  • dependabot-core: Constructs messages from scratch based on dependency metadata
  • dependabot-api: Combines existing messages from pending PRs

Since these have fundamentally different approaches, sharing them wouldn't be necessary.

How will you know you've accomplished your goal?

  • All existing tests continue to pass
  • No new tests added as this is a pure refactoring with no behavioral changes

Checklist

  • I have run the complete test suite to ensure all tests and linters pass.
  • I have thoroughly tested my code changes to ensure they work as expected, including adding additional tests for new functionality.
  • I have written clear and descriptive commit messages.
  • I have provided a detailed description of the changes in the pull request, including the problem it addresses, how it fixes the problem, and any relevant details about the implementation.
  • I have ensured that the code is well-documented and easy to understand.

Add a utility module that centralizes PR title construction logic,
enabling reuse across different message builder implementations.
This ensures future updates to title construction affect all
builders consistently.
@thavaahariharangit
thavaahariharangit requested a review from a team as a code owner January 29, 2026 12:55
# @return [String] The constructed PR title
#
# @example Basic usage
# build_pr_title(title: "bump foo to 1.0", prefix: "chore: ")

@yeikel yeikel Jan 29, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this be spit into further arguments or a custom input class?

For example

  • Prefix
  • Dependency name
  • current version
  • new version

@thavaahariharangit thavaahariharangit Jan 29, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for raising this point, @yeikel.

However, Not all PR titles follow a uniform structure with dependency name, current version, and new version.

For grouped dependency updates, the title structure is fundamentally different:

  • Grouped updates may include multiple dependencies, so there's no single dependency name, current version, or new version to display
  • Multi-ecosystem groups use an entirely different format, such as:
Bump the "group-name" group with N updates across multiple ecosystems

The primary goal of this PR is to address the duplication identified by @kbukum1 in a review comment. The following pattern was being duplicated in two places:

  • The API layer (when grouping multiple ecosystem PRs together)
  • The core layer (when setting the prefix for regular PRs)
def combined_pr_title
  title = base_title
  title = capitalize_first_word(title) if pr_name_prefixer&.capitalize_first_word?
  "#{pr_name_prefix}#{title}"
end

This extraction consolidates the shared title construction logic (applying prefix and capitalization rules) into MessageBuilderUtils, while keeping the base_title generation flexible for each caller to define based on their specific context.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank for the context and explanation; I did forget about grouped updates and you're totally right!

thavaahariharangit and others added 3 commits January 29, 2026 14:27
…sage_builder_utils.rb

Co-authored-by: Yeikel Santana <email@yeikel.com>
…sage_builder_utils.rb

Co-authored-by: Yeikel Santana <email@yeikel.com>
…sage_builder_utils.rb

Co-authored-by: Yeikel Santana <email@yeikel.com>
).returns(String)
end
def self.build_pr_title(title:, prefix: "", capitalize_first_word: false)
result_title = title.dup

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this dup call needed?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yeikel

The .dup is needed because the next line mutates the string in-place:

result_title[0] = T.must(result_title[0]).capitalize

Using string[0] = ... modifies the original string object. Without .dup, the caller's original title string would be mutated, which could cause unexpected side effects.

params(
prefix: T.nilable(String),
prefix_development: T.nilable(String),
include_scope: T.nilable(T::Boolean)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this actually be nil? I thought it could only be true or false, but I could be wrong

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, include_scope can be nil.

This value originates from the dependabot.yml configuration file:

version: 2

multi-ecosystem-groups:
  all-updates:
    schedule:
      interval: "weekly"
    commit-message:
      prefix: "chore"
      include: "scope"

The include property is optional, so there are three possible states:

  • Absent → parsed as nil (use default behavior)
  • Present with "scope" → converted to true (include scope in prefix)
  • Present with other/empty value → could be false (explicitly disable scope)

This is why the method correctly uses T.nilable(T::Boolean) for the type signature and checks unless include_scope.nil? rather than just if include_scope

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, thank your for the explanation. Just for my own understanding, what is the "default behavior"

Or in other words, what is the difference between "default behavior" and false?

@thavaahariharangit

Copy link
Copy Markdown
Contributor Author

This work is covered by this PR

#14285

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants